Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/board/LATEST_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ Membrane consumers can now pull BOTH halves of a render `classid` BBB-safely fro

## Current Contract Inventory (lance-graph-contract)

> **2026-06-25 — ADDED (Phase 1 identity→V3, the `mint_for` tail-variant carrier)**: `lance_graph_contract::canonical_node::NodeGuid::mint_for(tail_variant, classid, heel, hip, twig, leaf, family, identity)` (`const`, feature `guid-v2-tail`) — the **key-side symmetric spine** of `soa-value-tenant-migration-v2.md` §2.1: a consumer mints its identity BY ITS CLASSID's tail (`mint_for(classid_read_mode(c).tail_variant, …)`), never hardcoding `new` vs `new_v2` — the exact analog of the Phase-2 value-side `to_node_row(classid_read_mode(c).value_schema, …)`, same `classid_read_mode(c)` lookup, sibling field. Migrating a class's identity to V3 becomes a one-line `tail_variant` flip in the registry, zero consumer rewrite ("extend the one `ReadMode`, never a public `new_v3`"). Dispatch: `V1 → new` (u24·u24 tail; `leaf` ignored — V1 has no LEAF tier), `V2 | V3 → new_v2` (the shared `leaf·family·identity` 3×u16 tail — V3 differs only in how the bytes are *read*, the `(part_of:is_a)` tile, not how they are *stored*, so it mints through the same constructor). **No silent truncation** (the footgun v2 removes): the V2/V3 arm `assert!`s `family`/`identity` fit `u16`, mirroring `new`'s own 24-bit guard. **`Cargo.toml`: `guid-v3-tail = ["guid-v2-tail"]`** — V3's mint path dispatches to `new_v2`, so the tail constructor must exist whenever a V3 classid can be minted (honest gating per `I-LEGACY-API-FEATURE-GATED`). **End-to-end confirm** (`mint_for_osint_v3_is_end_to_end_routable`, gated `guid-v3-tail`): mint OSINT-V3 via the carrier → `read_mode().tail_variant == V3` → `from_guid_prefix_v3` routes non-empty at depth 16 (the full HEEL·HIP·TWIG·LEAF cascade) **while** the v1 `from_guid_prefix` still returns `None` (the Codex-P2 EMPTY-fold is gone, both directions proven) → `decode_v2` reads the tiers back; plus `mint_for_dispatches_to_the_right_constructor_per_tail` (gated `guid-v2-tail`: V1==`new`, V2==V3==`new_v2`). Additive, zero-dep, latent-default-V1 (zero re-mint of the V1/V2 corpus, RESERVE-DON'T-RECLAIM); 737 lib green default / 744 `guid-v2-tail` / 747 `guid-v3-tail`, clippy `--all-targets -D warnings` + fmt clean. Plan: `soa-value-tenant-migration-v2.md` §2 (Phase 1). Branch `claude/identity-v3-mint`.
>
> **2026-06-25 — MODULARIZED (follow-up to #613) — `lance_graph_contract::facet`**: extracted `FacetTier` / `FacetCascade` from `canonical_node` into a dedicated, reusable `facet` module (a *reading*, NOT part of the locked node layout — the cleaner factoring; `canonical_node` re-exports both for the historical path). **Reusable lane API rounded out:** `as_u128`/`from_u128` (single-register view), `rows()` (the 4 dword rows `{domain}{schema}` / `HEEL:HIP` / `TWIG:LEAF` / `family:identity`), `prefix_distance`/`shared_prefix_tiles` (the **granularity-free LCP redout** — `vpxor`+`tzcnt`; 8:8 vs nibble is a free `>>` on the count, measured), `row_match_mask` (`vpcmpeqd`-lane), plus `as_bytes`/`ref_from_bytes` — a **zero-cost reinterpret** (`#[repr(C, align(16))]`; `as_bytes` measured to lower to `mov rax,rdi`, a literal no-op; fields read straight through as single loads). One register → row(`u32`)/tile(`u16`)/prefix(bit)/nibble(Morton) lenses, each one SIMD op (module docs). Lab-test write-up deferred. Additive, zero-dep; 741 lib green (default + `guid-v3-tail`), clippy `-D warnings` + fmt clean. EPIPHANIES `E-FACET-8-8-ALWAYS`. Branch `claude/facet-module`.
>
> **2026-06-25 — ADDED (#613, the 6-tier 8:8 homogeneous facet + V3 routing fold)**: `lance_graph_contract::canonical_node::{FacetTier, FacetCascade}` — the **ALWAYS-8:8** content-blind facet substrate. `FacetTier{lo, hi}` (2 B, `const`; `as_u16` concatenated + `morton` 2bit×2bit Morton-tile projections); `FacetCascade{facet_classid: u32, tiers: [FacetTier; 6]}` (16 B = `facet_classid(4) | 6×(8:8)=12`, harvest §5.1) — a *reading* over a borrowed `[u8;16]` with `from_bytes`/`to_bytes`/`hi_chain`/`lo_chain`/`hi_distance`/`lo_distance`. **Carries NO value-slab offset** → does NOT touch the operator-LOCKED 480 B layout (the `classid→ClassView` byte-pick is the separate, panel-gated step); content-blind — only the consumer projects meaning (`part_of:is_a` / 256:256 palette centroid / `group:member` / `column:row` / concatenated u16 …), every reading amortizing to one 2bit×2bit Morton tile cascade. **Key-side V3 routing:** `hhtl::NiblePath::from_guid_prefix_v3` (feature `guid-v3-tail`) folds the 4 HHTL tiers `HEEL·HIP·TWIG·LEAF` in FULL (both bytes, depth 16) — the facet's routing prefix; `family`/`identity` stay the basin tail. `classid` NOT folded, so `soa_graph::hhtl_path` (schema-driven by `tail_variant`) routes OSINT-V3 `0x1000_0700` non-empty — fixes the Codex-P2 latent EMPTY-fold. `from_guid_prefix`'s "reserved-zero" doc/guard scoped to **v1-fold** (NOT a global classid law). Additive, zero-dep; 739 lib green (default + `guid-v3-tail`), clippy `-D warnings` + fmt clean. EPIPHANIES `E-FACET-8-8-ALWAYS`. Branch `claude/p-a-readmode-tail-variant`.
Expand Down
7 changes: 6 additions & 1 deletion crates/lance-graph-contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ guid-v2-tail = []
# (custom) u16, preserving the canon LOW-u16 0xDDCC domain byte (so
# `classid_concept_domain` still routes the legacy domain). OSINT-V3 (0x1000_0700)
# is the wired exemplar; FMA-V3 (0x1000_0A01) + Genetics follow. Default OFF.
guid-v3-tail = []
#
# Implies `guid-v2-tail`: V3 is a *reading* of the SAME leaf·family·identity 3×u16
# tail bytes v2 mints (the (part_of:is_a) cascade reinterprets them, never re-carves),
# so the V3 mint path (`NodeGuid::mint_for`'s V3 arm) dispatches to `new_v2`. The
# tail constructor must exist whenever a V3 classid can be minted.
guid-v3-tail = ["guid-v2-tail"]

# tenant-counters — per-ValueTenant update counters for debug instrumentation of
# the SoA write cascade (the capstone NaN-census / seam-wiring measurement). OFF
Expand Down
160 changes: 160 additions & 0 deletions crates/lance-graph-contract/src/canonical_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,64 @@ impl NodeGuid {
])
}

/// Mint a node by its **tail variant** — the carrier form of the Phase-1
/// symmetric spine (`soa-value-tenant-migration-v2.md` §2.1): a consumer
/// mints with `mint_for(classid_read_mode(c).tail_variant, …)`, NEVER by
/// hardcoding `new` vs `new_v2`. The key-side analog of the value-side
/// `to_node_row(classid_read_mode(c).value_schema, …)` — same
/// [`classid_read_mode`] lookup, sibling field. Migrating a class's identity
/// to V3 is then a one-line flip of its `tail_variant` in the registry, with
/// zero consumer rewrite (the "extend the one `ReadMode`, never a public
/// `new_v3`" litmus).
///
/// Dispatch (all three [`TailVariant`] arms exist unconditionally as enum
/// values; only the constructors they call are gated):
/// - [`V1`](TailVariant::V1) → [`new`](NodeGuid::new): the canonical
/// `family(u24)·identity(u24)` tail. `leaf` is not part of the V1 tail and
/// is intentionally ignored (the V1 cascade is HEEL·HIP·TWIG only).
/// - [`V2`](TailVariant::V2) / [`V3`](TailVariant::V3) → [`new_v2`](NodeGuid::new_v2):
/// the shared `leaf·family·identity` 3×u16 tail bytes. V3 differs from V2
/// only in how those bytes are *read* (the `(part_of:is_a)` cascade tile),
/// not how they are *stored* — so it mints through the same constructor.
///
/// **No silent truncation** (the footgun v2 exists to remove): the V2/V3 arm
/// asserts `family`/`identity` fit `u16`, mirroring [`new`](NodeGuid::new)'s
/// own 24-bit guard. An out-of-range value is a loud panic, never a wrong key.
#[allow(clippy::too_many_arguments)]
pub const fn mint_for(
tail_variant: TailVariant,
classid: u32,
heel: u16,
hip: u16,
twig: u16,
leaf: u16,
family: u32,
identity: u32,
) -> Self {
match tail_variant {
TailVariant::V1 => Self::new(classid, heel, hip, twig, family, identity),
TailVariant::V2 | TailVariant::V3 => {
assert!(
family <= 0xFFFF,
"v2/v3 family must fit in 16 bits (no silent truncation)"
);
assert!(
identity <= 0xFFFF,
"v2/v3 identity must fit in 16 bits (no silent truncation)"
);
Self::new_v2(
classid,
heel,
hip,
twig,
leaf,
family as u16,
identity as u16,
)
}
}
}

/// v2 `leaf` — bytes 10..12, the 4th HHTL routing tier (cascade terminal).
#[inline]
pub const fn leaf(&self) -> u16 {
Expand Down Expand Up @@ -1989,6 +2047,108 @@ mod tests {
);
}

#[cfg(feature = "guid-v3-tail")]
#[test]
fn mint_for_osint_v3_is_end_to_end_routable() {
// Phase-1 end-to-end (soa-value-tenant-migration-v2.md §2): mint a class's
// identity BY ITS CLASSID's tail_variant — the symmetric spine
// `mint_for(classid_read_mode(c).tail_variant, …)` — and confirm the minted
// address is V3-routable (the Codex-P2 EMPTY-fold is GONE).
use crate::hhtl::{NiblePath, MAX_DEPTH};

// (1) Resolve the tail shape from the classid — consumers never hardcode
// v1/v2/v3; the registry says which tail OSINT-V3 reads.
let tv = classid_read_mode(NodeGuid::CLASSID_OSINT_V3).tail_variant;
assert_eq!(
tv,
TailVariant::V3,
"OSINT-V3 classid resolves to the V3 tail"
);

// (2) Mint through the carrier. tv == V3 ⇒ mint_for dispatches to new_v2,
// laying the tail down as leaf·family·identity (3×u16).
let node = NodeGuid::mint_for(
tv,
NodeGuid::CLASSID_OSINT_V3,
0xAB12, // HEEL (part_of:is_a tile)
0xCD34, // HIP
0xEF56, // TWIG
0x789A, // LEAF
0xBCDE, // family (basin)
0xF012, // identity (instance)
);

// (3) The high-u16 generation marker round-trips in the stored classid…
assert_eq!(node.classid(), NodeGuid::CLASSID_OSINT_V3);
assert_eq!(
node.classid() >> 16,
0x1000,
"gen-marker preserved in the key"
);
// …and the node's OWN read_mode() (carrier form) agrees it is V3.
assert_eq!(node.read_mode().tail_variant, TailVariant::V3);

// (4) THE FIX, both directions:
// - the v1 fold REFUSES this address (classid >> 16 != 0) → the latent
// EMPTY fold Codex flagged on #613;
assert_eq!(
NiblePath::from_guid_prefix(&node),
None,
"v1 fold still refuses the high-u16 marker"
);
// - the v3 fold ROUTES it: HEEL·HIP·TWIG·LEAF in full (both bytes per
// 8:8 tile), depth 16, classid NOT folded → never EMPTY.
let p = NiblePath::from_guid_prefix_v3(&node);
assert_ne!(p, NiblePath::EMPTY, "V3 address must route, not collapse");
let expected = (0xAB12u64 << 48) | (0xCD34u64 << 32) | (0xEF56u64 << 16) | 0x789Au64;
assert_eq!(
p.packed(),
(expected, MAX_DEPTH),
"the full HEEL·HIP·TWIG·LEAF cascade is the routing prefix"
);

// (5) The tail reads back through the v2 decode (V3 shares the v2 bytes —
// family/identity are the basin tail, preserved not dropped).
let d = node.decode_v2();
assert_eq!(
(d.heel, d.hip, d.twig, d.leaf, d.family, d.identity),
(0xAB12, 0xCD34, 0xEF56, 0x789A, 0xBCDE, 0xF012)
);
}

#[cfg(feature = "guid-v2-tail")]
#[test]
fn mint_for_dispatches_to_the_right_constructor_per_tail() {
// The carrier is exactly `new` (V1) / `new_v2` (V2 & V3) — no new layout,
// just a classid-driven choice of the existing constructors. V3 shares the
// V2 *bytes* (it only reads them differently), so it mints identically.
let c = 0xDEAD_BEEF;
let (h, hp, t) = (0x1111u16, 0x2222u16, 0x3333u16);

// V1 arm == new(...): the u24 family·identity tail; `leaf` is not a V1 tier
// and is ignored (pass a sentinel to prove it is dropped).
assert_eq!(
NodeGuid::mint_for(TailVariant::V1, c, h, hp, t, 0xFFFF, 0x00_00AB, 0x00_00CD),
NodeGuid::new(c, h, hp, t, 0x00_00AB, 0x00_00CD),
"V1 arm is `new`, leaf ignored"
);

// V2 arm == new_v2(...): the leaf·family·identity 3×u16 tail.
assert_eq!(
NodeGuid::mint_for(TailVariant::V2, c, h, hp, t, 0x4444, 0x5555, 0x6666),
NodeGuid::new_v2(c, h, hp, t, 0x4444, 0x5555, 0x6666),
"V2 arm is `new_v2`"
);

// V3 arm == new_v2(...): identical stored bytes to V2 (the (part_of:is_a)
// reading is a *lens*, not a re-carve) — same constructor, same key.
assert_eq!(
NodeGuid::mint_for(TailVariant::V3, c, h, hp, t, 0x4444, 0x5555, 0x6666),
NodeGuid::new_v2(c, h, hp, t, 0x4444, 0x5555, 0x6666),
"V3 stores the same bytes as V2"
);
}

// ── GUID v2 tail (D-GV2-1) — field-isolation matrix + coexistence ─────────

#[cfg(feature = "guid-v2-tail")]
Expand Down
Loading