Skip to content

Commit 26abde5

Browse files
authored
Merge pull request #615 from AdaWorldAPI/claude/identity-v3-mint
feat(contract): NodeGuid::mint_for tail-variant carrier (Phase 1 identity→V3)
2 parents f686d3a + 3cbec0d commit 26abde5

3 files changed

Lines changed: 168 additions & 1 deletion

File tree

.claude/board/LATEST_STATE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ Membrane consumers can now pull BOTH halves of a render `classid` BBB-safely fro
140140

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

143+
> **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`.
144+
>
143145
> **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`.
144146
>
145147
> **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`.

crates/lance-graph-contract/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ guid-v2-tail = []
5050
# (custom) u16, preserving the canon LOW-u16 0xDDCC domain byte (so
5151
# `classid_concept_domain` still routes the legacy domain). OSINT-V3 (0x1000_0700)
5252
# is the wired exemplar; FMA-V3 (0x1000_0A01) + Genetics follow. Default OFF.
53-
guid-v3-tail = []
53+
#
54+
# Implies `guid-v2-tail`: V3 is a *reading* of the SAME leaf·family·identity 3×u16
55+
# tail bytes v2 mints (the (part_of:is_a) cascade reinterprets them, never re-carves),
56+
# so the V3 mint path (`NodeGuid::mint_for`'s V3 arm) dispatches to `new_v2`. The
57+
# tail constructor must exist whenever a V3 classid can be minted.
58+
guid-v3-tail = ["guid-v2-tail"]
5459

5560
# tenant-counters — per-ValueTenant update counters for debug instrumentation of
5661
# the SoA write cascade (the capstone NaN-census / seam-wiring measurement). OFF

crates/lance-graph-contract/src/canonical_node.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,64 @@ impl NodeGuid {
287287
])
288288
}
289289

290+
/// Mint a node by its **tail variant** — the carrier form of the Phase-1
291+
/// symmetric spine (`soa-value-tenant-migration-v2.md` §2.1): a consumer
292+
/// mints with `mint_for(classid_read_mode(c).tail_variant, …)`, NEVER by
293+
/// hardcoding `new` vs `new_v2`. The key-side analog of the value-side
294+
/// `to_node_row(classid_read_mode(c).value_schema, …)` — same
295+
/// [`classid_read_mode`] lookup, sibling field. Migrating a class's identity
296+
/// to V3 is then a one-line flip of its `tail_variant` in the registry, with
297+
/// zero consumer rewrite (the "extend the one `ReadMode`, never a public
298+
/// `new_v3`" litmus).
299+
///
300+
/// Dispatch (all three [`TailVariant`] arms exist unconditionally as enum
301+
/// values; only the constructors they call are gated):
302+
/// - [`V1`](TailVariant::V1) → [`new`](NodeGuid::new): the canonical
303+
/// `family(u24)·identity(u24)` tail. `leaf` is not part of the V1 tail and
304+
/// is intentionally ignored (the V1 cascade is HEEL·HIP·TWIG only).
305+
/// - [`V2`](TailVariant::V2) / [`V3`](TailVariant::V3) → [`new_v2`](NodeGuid::new_v2):
306+
/// the shared `leaf·family·identity` 3×u16 tail bytes. V3 differs from V2
307+
/// only in how those bytes are *read* (the `(part_of:is_a)` cascade tile),
308+
/// not how they are *stored* — so it mints through the same constructor.
309+
///
310+
/// **No silent truncation** (the footgun v2 exists to remove): the V2/V3 arm
311+
/// asserts `family`/`identity` fit `u16`, mirroring [`new`](NodeGuid::new)'s
312+
/// own 24-bit guard. An out-of-range value is a loud panic, never a wrong key.
313+
#[allow(clippy::too_many_arguments)]
314+
pub const fn mint_for(
315+
tail_variant: TailVariant,
316+
classid: u32,
317+
heel: u16,
318+
hip: u16,
319+
twig: u16,
320+
leaf: u16,
321+
family: u32,
322+
identity: u32,
323+
) -> Self {
324+
match tail_variant {
325+
TailVariant::V1 => Self::new(classid, heel, hip, twig, family, identity),
326+
TailVariant::V2 | TailVariant::V3 => {
327+
assert!(
328+
family <= 0xFFFF,
329+
"v2/v3 family must fit in 16 bits (no silent truncation)"
330+
);
331+
assert!(
332+
identity <= 0xFFFF,
333+
"v2/v3 identity must fit in 16 bits (no silent truncation)"
334+
);
335+
Self::new_v2(
336+
classid,
337+
heel,
338+
hip,
339+
twig,
340+
leaf,
341+
family as u16,
342+
identity as u16,
343+
)
344+
}
345+
}
346+
}
347+
290348
/// v2 `leaf` — bytes 10..12, the 4th HHTL routing tier (cascade terminal).
291349
#[inline]
292350
pub const fn leaf(&self) -> u16 {
@@ -1989,6 +2047,108 @@ mod tests {
19892047
);
19902048
}
19912049

2050+
#[cfg(feature = "guid-v3-tail")]
2051+
#[test]
2052+
fn mint_for_osint_v3_is_end_to_end_routable() {
2053+
// Phase-1 end-to-end (soa-value-tenant-migration-v2.md §2): mint a class's
2054+
// identity BY ITS CLASSID's tail_variant — the symmetric spine
2055+
// `mint_for(classid_read_mode(c).tail_variant, …)` — and confirm the minted
2056+
// address is V3-routable (the Codex-P2 EMPTY-fold is GONE).
2057+
use crate::hhtl::{NiblePath, MAX_DEPTH};
2058+
2059+
// (1) Resolve the tail shape from the classid — consumers never hardcode
2060+
// v1/v2/v3; the registry says which tail OSINT-V3 reads.
2061+
let tv = classid_read_mode(NodeGuid::CLASSID_OSINT_V3).tail_variant;
2062+
assert_eq!(
2063+
tv,
2064+
TailVariant::V3,
2065+
"OSINT-V3 classid resolves to the V3 tail"
2066+
);
2067+
2068+
// (2) Mint through the carrier. tv == V3 ⇒ mint_for dispatches to new_v2,
2069+
// laying the tail down as leaf·family·identity (3×u16).
2070+
let node = NodeGuid::mint_for(
2071+
tv,
2072+
NodeGuid::CLASSID_OSINT_V3,
2073+
0xAB12, // HEEL (part_of:is_a tile)
2074+
0xCD34, // HIP
2075+
0xEF56, // TWIG
2076+
0x789A, // LEAF
2077+
0xBCDE, // family (basin)
2078+
0xF012, // identity (instance)
2079+
);
2080+
2081+
// (3) The high-u16 generation marker round-trips in the stored classid…
2082+
assert_eq!(node.classid(), NodeGuid::CLASSID_OSINT_V3);
2083+
assert_eq!(
2084+
node.classid() >> 16,
2085+
0x1000,
2086+
"gen-marker preserved in the key"
2087+
);
2088+
// …and the node's OWN read_mode() (carrier form) agrees it is V3.
2089+
assert_eq!(node.read_mode().tail_variant, TailVariant::V3);
2090+
2091+
// (4) THE FIX, both directions:
2092+
// - the v1 fold REFUSES this address (classid >> 16 != 0) → the latent
2093+
// EMPTY fold Codex flagged on #613;
2094+
assert_eq!(
2095+
NiblePath::from_guid_prefix(&node),
2096+
None,
2097+
"v1 fold still refuses the high-u16 marker"
2098+
);
2099+
// - the v3 fold ROUTES it: HEEL·HIP·TWIG·LEAF in full (both bytes per
2100+
// 8:8 tile), depth 16, classid NOT folded → never EMPTY.
2101+
let p = NiblePath::from_guid_prefix_v3(&node);
2102+
assert_ne!(p, NiblePath::EMPTY, "V3 address must route, not collapse");
2103+
let expected = (0xAB12u64 << 48) | (0xCD34u64 << 32) | (0xEF56u64 << 16) | 0x789Au64;
2104+
assert_eq!(
2105+
p.packed(),
2106+
(expected, MAX_DEPTH),
2107+
"the full HEEL·HIP·TWIG·LEAF cascade is the routing prefix"
2108+
);
2109+
2110+
// (5) The tail reads back through the v2 decode (V3 shares the v2 bytes —
2111+
// family/identity are the basin tail, preserved not dropped).
2112+
let d = node.decode_v2();
2113+
assert_eq!(
2114+
(d.heel, d.hip, d.twig, d.leaf, d.family, d.identity),
2115+
(0xAB12, 0xCD34, 0xEF56, 0x789A, 0xBCDE, 0xF012)
2116+
);
2117+
}
2118+
2119+
#[cfg(feature = "guid-v2-tail")]
2120+
#[test]
2121+
fn mint_for_dispatches_to_the_right_constructor_per_tail() {
2122+
// The carrier is exactly `new` (V1) / `new_v2` (V2 & V3) — no new layout,
2123+
// just a classid-driven choice of the existing constructors. V3 shares the
2124+
// V2 *bytes* (it only reads them differently), so it mints identically.
2125+
let c = 0xDEAD_BEEF;
2126+
let (h, hp, t) = (0x1111u16, 0x2222u16, 0x3333u16);
2127+
2128+
// V1 arm == new(...): the u24 family·identity tail; `leaf` is not a V1 tier
2129+
// and is ignored (pass a sentinel to prove it is dropped).
2130+
assert_eq!(
2131+
NodeGuid::mint_for(TailVariant::V1, c, h, hp, t, 0xFFFF, 0x00_00AB, 0x00_00CD),
2132+
NodeGuid::new(c, h, hp, t, 0x00_00AB, 0x00_00CD),
2133+
"V1 arm is `new`, leaf ignored"
2134+
);
2135+
2136+
// V2 arm == new_v2(...): the leaf·family·identity 3×u16 tail.
2137+
assert_eq!(
2138+
NodeGuid::mint_for(TailVariant::V2, c, h, hp, t, 0x4444, 0x5555, 0x6666),
2139+
NodeGuid::new_v2(c, h, hp, t, 0x4444, 0x5555, 0x6666),
2140+
"V2 arm is `new_v2`"
2141+
);
2142+
2143+
// V3 arm == new_v2(...): identical stored bytes to V2 (the (part_of:is_a)
2144+
// reading is a *lens*, not a re-carve) — same constructor, same key.
2145+
assert_eq!(
2146+
NodeGuid::mint_for(TailVariant::V3, c, h, hp, t, 0x4444, 0x5555, 0x6666),
2147+
NodeGuid::new_v2(c, h, hp, t, 0x4444, 0x5555, 0x6666),
2148+
"V3 stores the same bytes as V2"
2149+
);
2150+
}
2151+
19922152
// ── GUID v2 tail (D-GV2-1) — field-isolation matrix + coexistence ─────────
19932153

19942154
#[cfg(feature = "guid-v2-tail")]

0 commit comments

Comments
 (0)