From 3cbec0d71e08ab0f82ae1c54a35e16fea532b5f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Jun 2026 17:04:12 +0000 Subject: [PATCH] =?UTF-8?q?feat(contract):=20NodeGuid::mint=5Ffor=20tail-v?= =?UTF-8?q?ariant=20carrier=20(Phase=201=20identity=E2=86=92V3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of soa-value-tenant-migration-v2.md: migrate identity to V3 by its classid's tail_variant first, before shaping value tenants (Phase 2). The parser resolves tail_variant UPSTREAM of value_schema, so the address must express V3 before tenants can be shaped to it. The key-side symmetric spine: a consumer mints with `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, …)`. Migrating a class's identity to V3 is then a one-line tail_variant flip in the registry, zero consumer rewrite ("extend the one ReadMode, never a public new_v3"). V1 -> new (u24·u24 tail; leaf ignored — V1 has no LEAF tier) V2 | V3 -> new_v2 (shared leaf·family·identity 3xu16 — V3 differs only in how the bytes are READ, the (part_of:is_a) tile, not how they are STORED, so it mints identically) No silent truncation (the footgun v2 removes): the V2/V3 arm asserts 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, 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. Board: LATEST_STATE Contract Inventory updated (same commit). Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01TzqvDqbFRzyx17EkLKBoZF --- .claude/board/LATEST_STATE.md | 2 + crates/lance-graph-contract/Cargo.toml | 7 +- .../src/canonical_node.rs | 160 ++++++++++++++++++ 3 files changed, 168 insertions(+), 1 deletion(-) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index c9f05378..ac15ff03 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -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`. diff --git a/crates/lance-graph-contract/Cargo.toml b/crates/lance-graph-contract/Cargo.toml index 5c6e4d45..4e3d5f60 100644 --- a/crates/lance-graph-contract/Cargo.toml +++ b/crates/lance-graph-contract/Cargo.toml @@ -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 diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index 6c2be389..7bf85410 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -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 { @@ -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")]