diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index df023133..bdc6d429 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,36 @@ +## 2026-06-26 — E-V3-BASINS-ARE-MEREOLOGY-NOT-LABELS — the 6 V3 basins (+ relative location) are a structural ADDRESS (mereology / HHTL X;Y coordinates), never flat labels + +**Status:** FINDING `[H]` (operator directive 2026-06-26; impl in the FMA-V3 + +CPIC-V3 mint, branch `claude/fma-cpic-v3-mint`). Sharpens `E-FACET-8-8-ALWAYS` +on the *consumer-projection* side and is a direct application of +`I-VSA-IDENTITIES` Test-0 (register-laziness). + +**The claim.** The V3 identity's 6 `(part_of:is_a)` tiles — HEEL·HIP·TWIG·LEAF +(routing) + family·identity (basin tail) — plus the *relative location within +each basin* are a **substantial address** (6 × 16 bits of hierarchical +coordinate). Spending that address on **labels** (a flat type tag a `HashMap` / +`enum` would carry) wastes it — the exact register-laziness `I-VSA-IDENTITIES` +Test-0 forbids ("does this thing have a natural name/ID? then use the register, +not the rich carrier"). The basins must instead carry **mereology / membership**: +*where a thing sits in the part-of hierarchy* (AST node position, basin +location), readable equivalently as HHTL `(X;Y)` coordinates per tile (`part_of` += X = hi byte, `is_a` = Y = lo byte). + +**The canonical case — genes (Genetics / CPIC-V3).** A gene's identity is its +**genomic position**: `genome → chromosome → region → locus → gene` is the +part-of cascade; the **human genome is the fixed schema view** the position is +taken against (hence `ValueSchema::Compressed` — a fixed reference frame, not a +hot lifecycle). Phase 2 then carries gene **expression as the coordinate +*value*** on top of that positional address. "All expression" rides the basins +as `(X;Y)`, the genome is the codebook. + +**Litmus.** Before assigning meaning to a V3 basin: *is this a coordinate +(where-it-is) or a name (what-it's-called)?* Name → it belongs in the content +store / ClassView, not the basin. Coordinate / membership → the basin is exactly +right, and the 6-tier address is the payoff. Cross-ref `E-FACET-8-8-ALWAYS` +(content-blind 8:8, consumer projects), `I-VSA-IDENTITIES` Test-0, CANON +"basin location" (`family` = basin), `CLASSID_CPIC_V3` doc-comment. + ## 2026-06-25 — E-FACET-8-8-ALWAYS — the homogeneous facet is ALWAYS 8:8 (content-blind, consumer-projected); it amortizes to a 2bit×2bit Morton tile cascade **Status:** FINDING `[H]` (operator-locked 2026-06-25; impl PR #613). Refines diff --git a/.claude/board/ISSUES.md b/.claude/board/ISSUES.md index 790046c0..fb58536b 100644 --- a/.claude/board/ISSUES.md +++ b/.claude/board/ISSUES.md @@ -1,5 +1,13 @@ # Issues Log — Open + Resolved (double-entry, append-only) +## 2026-06-26 — ISS-OGAR-GENETICS-MIRROR-PENDING — contract mirror gained `ConceptDomain::Genetics` (0x0E) ahead of OGAR; the `domains_agree` arm + OGAR side follow + +**Status:** OPEN (tracked) · Owner: OGAR `ogar-vocab` + `lance-graph-ogar` · Surfaced by: CodeRabbit on #618. The same cross-repo-arc shape as `ISS-OGAR-AUTH-MIRROR-DRIFT` / `E-CODEBOOK-MINT-IS-A-CROSS-REPO-ARC`, but **domain-only** so it does not break in isolation. + +#618 added `ConceptDomain::Genetics` + `0x0E => Genetics` to the contract mirror (`ogar_codebook.rs`) so CPIC-V3 `0x1000_0E00` routes Genetics (operator-allocated 2026-06-26). OGAR's `ogar_vocab::ConceptDomain` has **no Genetics variant yet**, and `lance-graph-ogar::parity::domains_agree` (`lib.rs:128-148`) still stops at `HR`/`Unassigned`. **Why it's safe in isolation (not a build break like the Auth drift):** the addition is a *domain enum variant + route*, NOT a CODEBOOK **concept** — `mirror::CODEBOOK.len()` is unchanged, so the compile-time `COUNT_FUSE` still holds, and `assert_codebook_parity` iterates CODEBOOK concept-ids (none at `0x0E`), so `domains_agree(0x0E00)` is never called. `domains_agree` is a `matches!` (never exhaustiveness-checked), so adding `C::Genetics` does not break compile either; the `(O::Genetics, C::Genetics)` arm **cannot** be added today because `O::Genetics` does not exist. + +**Resolution (the coordinated arc, when Genetics concepts are minted):** (1) OGAR `ogar-vocab` adds `ConceptDomain::Genetics` + `0x0E => Genetics` + any Genetics concept rows; (2) the contract mirror's `CODEBOOK` gains the matching concept rows (keeping `COUNT_FUSE` balanced); (3) `lance-graph-ogar::parity::domains_agree` gains the `(O::Genetics, C::Genetics)` arm. Per `E-CODEBOOK-MINT-IS-A-CROSS-REPO-ARC`, those three land together, never split. Until then the drift guard correctly reflects "contract ahead of OGAR on the Genetics domain." + ## 2026-06-23 — ISS-OGAR-AUTH-MIRROR-DRIFT — `0x0B` AuthStore mint broke the contract mirror's COUNT_FUSE in every consumer **Status:** RESOLVED 2026-06-23 (this commit). OGAR `ogar-vocab` PR #110 minted the `0x0B` AuthStore family (4 concepts: auth_store 0x0B01, auth_zitadel 0x0B02, auth_zanzibar 0x0B03, auth_ory_keto 0x0B04) and merged to OGAR `main`, taking `ogar_vocab::class_ids::ALL` from 39 → 43. The paired `lance-graph-contract::ogar_codebook::CODEBOOK` mirror was NOT updated in the same arc, so the compile-time `COUNT_FUSE` in `lance-graph-ogar` (`assert!(mirror::CODEBOOK.len() == ogar_vocab::class_ids::ALL.len())`) fired `error[E0080]` (`vendor/lance-graph/crates/lance-graph-ogar/src/lib.rs:113`) in **every** consumer vendoring the OGAR git dep — medcare CI went red on `cargo build`. **Resolution:** added the 4 auth rows + `ConceptDomain::Auth` + `0x0B => Auth` to the mirror, and the `(O::Auth, C::Auth)` arm to `lance-graph-ogar::parity::domains_agree` (else the runtime `assert_codebook_parity` test panics). 43 == 43 restored; `cargo test -p lance-graph-contract` green. **Process fix (see EPIPHANIES E-CODEBOOK-MINT-IS-A-CROSS-REPO-ARC):** an OGAR concept mint is a cross-repo arc — the OGAR entry + the contract mirror + the `domains_agree` arm land together, never split across sessions. **Merge note (2026-06-23):** main landed #595 (auth sync) + #597 (PRODUCT + ACCOUNTING_ACCOUNT, OGAR #111) first; on merge this branch took main's superset `ogar_codebook.rs` (45 concepts incl. the `AppPrefix` render layer), so the auth mirror rows here are subsumed — the `domains_agree` Auth arm + this finding stand. diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index ac15ff03..26c37bf7 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-26 — ADDED (Phase 1 COMPLETE — FMA-V3 + CPIC-V3 mints + Genetics domain)**: the two remaining V3 identity classes that close Phase 1. `NodeGuid::{CLASSID_FMA_V3 = 0x1000_0A01, CLASSID_CPIC_V3 = 0x1000_0E00}` + `ReadMode::{FMA_V3, CPIC_V3}` (both `{V3, Compressed, CoarseOnly}`) + `BUILTIN_READ_MODES` entries, all gated `guid-v3-tail` — mirroring the OSINT-V3 (#613) pattern: the `0x1000` gen-marker in the HIGH u16, canon domain preserved in the LOW u16 so `classid_concept_domain` still routes (`0x0A01 → Anatomy`, `0x0E00 → Genetics`). **NEW Genetics domain `0x0E`** in `ogar_codebook::ConceptDomain` (+ `0x0E => Genetics` route, parity test pins `0x0E00 → Genetics`) — **operator-allocated 2026-06-26** (`0x0D` was already HR); mirror target `ogar_vocab::ConceptDomain::Genetics` (OGAR catches up under the drift guard). **Genetics framing (operator directive, `I-VSA-IDENTITIES` Test-0):** the 6 V3 basins are genomic **mereology, not labels** — a gene's identity is its *position* in the part-of hierarchy (genome → chromosome → region → locus → gene), readable as HHTL `(X;Y)` coordinates per `(part_of:is_a)` tile; the human genome is the **fixed schema view** (hence `Compressed`, a fixed reference frame), and Phase 2 shapes gene **expression as the coordinate value**. The Phase-1 V3 set (OSINT + FMA + CPIC) is now **complete → unblocks the atomic Canon:Custom flip + Phase 2** (plan §2.3 sequencing). Confirm test `read_mode_fma_v3_and_cpic_v3_route_their_domains` (gated `guid-v3-tail`): both route their domain, resolve `tail_variant == V3`, distinct classids. Additive, layout-preserving, default-V1; **739** lib green default / **750** `guid-v3-tail`, clippy `--all-targets -D warnings` + fmt clean. Plan `soa-value-tenant-migration-v2.md` §2.2 (FMA/CPIC rows wired; Genetics `0x0E`). Branch `claude/fma-cpic-v3-mint`. +> > **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`. diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index 7bf85410..4fb4a944 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -77,9 +77,11 @@ impl NodeGuid { // u16` (`0xDDCC`; CLASSID_OSINT = 0x0700, CLASSID_FMA = 0x0A01). So // `classid_concept_domain` masks the marker off and still routes the legacy // domain — the Codex-P1 fix vs the rejected low-half `0x1007` (which read - // domain 0x10 = Unassigned). OSINT-V3 (`0x1000_0700`) is the WIRED exemplar; - // FMA-V3 (`0x1000_0A01`) + Genetics (domain TBD — `0x0D` is HR, not Genetics) - // follow once their value models are pinned. + // domain 0x10 = Unassigned). OSINT-V3 (`0x1000_0700`), FMA-V3 (`0x1000_0A01`), + // and CPIC-V3 (`0x1000_0E00`, Genetics domain `0x0E`, operator-allocated + // 2026-06-26 — `0x0D` was already HR) are the three wired V3 classes that + // complete Phase 1 (identity → V3); the atomic Canon:Custom half-order flip + // follows once the V3 set is complete (plan §2.3). /// **OSINT-V3** — OSINT on a [`TailVariant::V3`] cascade tail. The generation /// marker `0x1000` sits in the HIGH/custom u16; the canon `0x0700` is preserved @@ -90,6 +92,37 @@ impl NodeGuid { #[cfg(feature = "guid-v3-tail")] pub const CLASSID_OSINT_V3: u32 = 0x1000_0700; + /// **FMA-V3** — FMA anatomy on a [`TailVariant::V3`] cascade tail. The marker + /// `0x1000` sits in the HIGH/custom u16; the canon `0x0A01` (Anatomy domain + /// `0x0A`, `anatomical_structure`) is preserved in the LOW u16, so + /// [`classid_concept_domain`](crate::ogar_codebook::classid_concept_domain) + /// still routes [`Anatomy`](crate::ogar_codebook::ConceptDomain::Anatomy). + /// Resolves to [`ReadMode::FMA_V3`] (same cold `Compressed` model as legacy FMA). + #[cfg(feature = "guid-v3-tail")] + pub const CLASSID_FMA_V3: u32 = 0x1000_0A01; + + /// **CPIC-V3** — CPIC pharmacogenomics (gene–drug guidelines) on a + /// [`TailVariant::V3`] cascade tail, in the new **Genetics** domain (`0x0E`, + /// operator-allocated 2026-06-26 — `0x0D` was already HR). The marker `0x1000` + /// sits in the HIGH/custom u16; the canon `0x0E00` (Genetics domain root) is + /// preserved in the LOW u16, so + /// [`classid_concept_domain`](crate::ogar_codebook::classid_concept_domain) + /// routes [`Genetics`](crate::ogar_codebook::ConceptDomain::Genetics). Resolves + /// to [`ReadMode::CPIC_V3`]. + /// + /// **The 6 V3 basins are genomic MEREOLOGY, not labels** (operator directive + /// 2026-06-26; `I-VSA-IDENTITIES` Test-0, register-laziness): a gene's identity + /// is its *position* in the part-of hierarchy (genome → chromosome → region → + /// locus → gene), readable as HHTL `(X;Y)` coordinates per `(part_of:is_a)` + /// tile — never a flat type tag a `HashMap` would carry. The 6-basin + relative + /// location is a substantial address; spending it on labels wastes it. The human + /// genome is the **fixed schema view** the position is taken against, which is + /// why the value model is [`ValueSchema::Compressed`] (a fixed reference frame, + /// not a hot lifecycle); Phase 2 shapes the V3 tenants — gene expression as the + /// coordinate *value* — on top. + #[cfg(feature = "guid-v3-tail")] + pub const CLASSID_CPIC_V3: u32 = 0x1000_0E00; + /// Construct from the six canonical groups. `family`/`identity` use their low 3 bytes. /// /// Panics (incl. const-eval) when `family` or `identity` exceed 24 bits — the @@ -1014,9 +1047,9 @@ impl ReadMode { /// The **OSINT-V3** read-mode ([`NodeGuid::CLASSID_OSINT_V3`]): the same hot /// [`ValueSchema::Cognitive`] value model as legacy [`OSINT`](ReadMode::OSINT), - /// read through the new-generation [`TailVariant::V3`] cascade tail. The wired - /// V3 exemplar; FMA-V3 + Genetics-V3 follow once their classids/value models - /// are pinned. + /// read through the new-generation [`TailVariant::V3`] cascade tail. The first + /// V3 exemplar; [`FMA_V3`](ReadMode::FMA_V3) + [`CPIC_V3`](ReadMode::CPIC_V3) + /// complete the Phase-1 V3 set. #[cfg(feature = "guid-v3-tail")] pub const OSINT_V3: ReadMode = ReadMode { tail_variant: TailVariant::V3, @@ -1024,6 +1057,28 @@ impl ReadMode { edge_codec: EdgeCodecFlavor::CoarseOnly, }; + /// The **FMA-V3** read-mode ([`NodeGuid::CLASSID_FMA_V3`]): the same cold + /// [`ValueSchema::Compressed`] value model as legacy [`FMA`](ReadMode::FMA), + /// read through the new-generation [`TailVariant::V3`] cascade tail. + #[cfg(feature = "guid-v3-tail")] + pub const FMA_V3: ReadMode = ReadMode { + tail_variant: TailVariant::V3, + value_schema: ValueSchema::Compressed, + edge_codec: EdgeCodecFlavor::CoarseOnly, + }; + + /// The **CPIC-V3** read-mode ([`NodeGuid::CLASSID_CPIC_V3`], Genetics domain): + /// CPIC pharmacogenomics on a [`TailVariant::V3`] cascade tail. The value model + /// [`ValueSchema::Compressed`] is the **Phase-1 provisional** (biomedical + /// reference, mirroring FMA's cold treatment); Phase 2 pins the V3-shaped + /// tenants. Edges [`EdgeCodecFlavor::CoarseOnly`]. + #[cfg(feature = "guid-v3-tail")] + pub const CPIC_V3: ReadMode = ReadMode { + tail_variant: TailVariant::V3, + value_schema: ValueSchema::Compressed, + edge_codec: EdgeCodecFlavor::CoarseOnly, + }; + /// All three axes are layout-preserving (a tail-variant/preset/flavor /// re-interprets reserved bytes, never a stride change), so adopting any /// read-mode needs no `ENVELOPE_LAYOUT_VERSION` bump. @@ -1069,13 +1124,15 @@ static BUILTIN_READ_MODES: LazyLock> = LazyLock::new(|| { m.insert(NodeGuid::CLASSID_PROJECT, ReadMode::PROJECT); m.insert(NodeGuid::CLASSID_ERP, ReadMode::ERP); // V3 cascade-key classes (feature `guid-v3-tail`): same value model as their - // legacy domain, on a TailVariant::V3 tail. OSINT-V3 (`0x1000_0700`) is the - // wired exemplar — the high-u16 gen-marker is masked off by the domain router, - // so `classid_concept_domain` still resolves Osint. FMA-V3 + Genetics-V3 land - // here once their classids/value models are pinned. + // legacy domain, on a TailVariant::V3 tail. The high-u16 gen-marker is masked + // off by the domain router, so `classid_concept_domain` still resolves the + // legacy domain (Osint / Anatomy / Genetics). The three together complete the + // Phase-1 V3 set; the atomic Canon:Custom flip follows (plan §2.3). #[cfg(feature = "guid-v3-tail")] { m.insert(NodeGuid::CLASSID_OSINT_V3, ReadMode::OSINT_V3); + m.insert(NodeGuid::CLASSID_FMA_V3, ReadMode::FMA_V3); + m.insert(NodeGuid::CLASSID_CPIC_V3, ReadMode::CPIC_V3); } m }); @@ -2001,8 +2058,8 @@ mod tests { assert!(TailVariant::V1.is_layout_preserving()); assert!(ReadMode::DEFAULT.is_layout_preserving()); // The mechanism axis exists for all three variants (V3 is the new-gen key). - // OSINT-V3 is the wired exemplar (see read_mode_osint_v3_routes_v3_tail_and_osint_domain); - // FMA-V3 + Genetics-V3 entries are DEFERRED until their classids are pinned. + // The Phase-1 V3 set is COMPLETE: OSINT-V3 + FMA-V3 + CPIC-V3 (Genetics) are + // all wired (see read_mode_fma_v3_and_cpic_v3_route_their_domains). assert!(TailVariant::V3.is_layout_preserving()); assert!(TailVariant::V2.is_layout_preserving()); } @@ -2047,6 +2104,74 @@ mod tests { ); } + #[cfg(feature = "guid-v3-tail")] + #[test] + fn read_mode_fma_v3_and_cpic_v3_route_their_domains() { + use crate::ogar_codebook::{classid_concept_domain, ConceptDomain}; + // Phase-1 V3 set completion: FMA-V3 + CPIC-V3 resolve to the V3 tail AND + // their domain still routes through the high-u16 gen-marker (masked off by + // the domain router) — the same scheme proven for OSINT-V3. + + // FMA-V3: Anatomy domain (0x0A) intact; cold Compressed model (mirrors FMA). + assert_eq!( + classid_read_mode(NodeGuid::CLASSID_FMA_V3).tail_variant, + TailVariant::V3 + ); + assert_eq!( + classid_concept_domain(NodeGuid::CLASSID_FMA_V3), + ConceptDomain::Anatomy + ); + assert_eq!( + classid_read_mode(NodeGuid::CLASSID_FMA_V3), + ReadMode::FMA_V3 + ); + assert_eq!(ReadMode::FMA_V3.value_schema, ValueSchema::Compressed); + assert_eq!(NodeGuid::CLASSID_FMA_V3, 0x1000_0A01); + assert_eq!( + NodeGuid::CLASSID_FMA_V3 >> 16, + 0x1000, + "gen-marker high u16" + ); + assert_eq!( + NodeGuid::CLASSID_FMA_V3 as u16, + NodeGuid::CLASSID_FMA as u16, + "low u16 == canon FMA concept (0x0A01)" + ); + + // CPIC-V3: the operator-allocated Genetics domain (0x0E); Compressed = the + // fixed human-genome schema view (basins = genomic mereology, not labels). + assert_eq!( + classid_read_mode(NodeGuid::CLASSID_CPIC_V3).tail_variant, + TailVariant::V3 + ); + assert_eq!( + classid_concept_domain(NodeGuid::CLASSID_CPIC_V3), + ConceptDomain::Genetics + ); + assert_eq!( + classid_read_mode(NodeGuid::CLASSID_CPIC_V3), + ReadMode::CPIC_V3 + ); + assert_eq!(ReadMode::CPIC_V3.value_schema, ValueSchema::Compressed); + assert_eq!(NodeGuid::CLASSID_CPIC_V3, 0x1000_0E00); + assert_eq!( + NodeGuid::CLASSID_CPIC_V3 >> 16, + 0x1000, + "gen-marker high u16" + ); + assert_eq!( + NodeGuid::CLASSID_CPIC_V3 as u16, + 0x0E00, + "low u16 == Genetics domain root (0x0E00)" + ); + + // The three V3 classes are mutually distinct, all V3 + layout-preserving. + assert!(ReadMode::FMA_V3.is_layout_preserving()); + assert!(ReadMode::CPIC_V3.is_layout_preserving()); + assert_ne!(NodeGuid::CLASSID_FMA_V3, NodeGuid::CLASSID_CPIC_V3); + assert_ne!(NodeGuid::CLASSID_FMA_V3, NodeGuid::CLASSID_OSINT_V3); + } + #[cfg(feature = "guid-v3-tail")] #[test] fn mint_for_osint_v3_is_end_to_end_routable() { diff --git a/crates/lance-graph-contract/src/ogar_codebook.rs b/crates/lance-graph-contract/src/ogar_codebook.rs index 279e3f6f..df4d431a 100644 --- a/crates/lance-graph-contract/src/ogar_codebook.rs +++ b/crates/lance-graph-contract/src/ogar_codebook.rs @@ -74,7 +74,13 @@ pub enum ConceptDomain { /// bridge) and from `Health` PHI. Mirrors OGAR /// `ogar_vocab::ConceptDomain::HR` (added in OGAR PR #127). HR, - /// Any high-byte slot not yet assigned a domain (`0x03XX`–`0x06XX`, `0x0EXX`+). + /// `0x0EXX` — Genetics (pharmacogenomics; CPIC gene–drug guidelines, variant + /// annotations). Public reference knowledge, distinct from `Health` PHI. + /// Operator-allocated 2026-06-26 (`0x0D` was already HR). Mirror target + /// `ogar_vocab::ConceptDomain::Genetics` — OGAR catches up under the drift + /// guard (the parity tests pin `0x0E00 → Genetics`). + Genetics, + /// Any high-byte slot not yet assigned a domain (`0x03XX`–`0x06XX`, `0x0FXX`+). Unassigned, } @@ -95,6 +101,7 @@ pub fn canonical_concept_domain(id: u16) -> ConceptDomain { 0x0B => ConceptDomain::Auth, 0x0C => ConceptDomain::Automation, 0x0D => ConceptDomain::HR, + 0x0E => ConceptDomain::Genetics, _ => ConceptDomain::Unassigned, } } @@ -439,7 +446,9 @@ mod tests { assert_eq!(canonical_concept_domain(0x0D01), ConceptDomain::HR); assert_eq!(canonical_concept_domain(0x0D04), ConceptDomain::HR); assert_eq!(canonical_concept_domain(0x0500), ConceptDomain::Unassigned); - assert_eq!(canonical_concept_domain(0x0E00), ConceptDomain::Unassigned); + // Genetics (0x0E) operator-allocated 2026-06-26 for CPIC-V3 (was Unassigned). + assert_eq!(canonical_concept_domain(0x0E00), ConceptDomain::Genetics); + assert_eq!(canonical_concept_domain(0x0F00), ConceptDomain::Unassigned); } #[test] diff --git a/crates/lance-graph-contract/src/soa_graph.rs b/crates/lance-graph-contract/src/soa_graph.rs index 78adfb7f..bbbc2c75 100644 --- a/crates/lance-graph-contract/src/soa_graph.rs +++ b/crates/lance-graph-contract/src/soa_graph.rs @@ -160,6 +160,47 @@ fn hhtl_path(guid: &NodeGuid) -> NiblePath { NiblePath::from_guid_prefix(guid).unwrap_or(NiblePath::EMPTY) } +/// The node's basin-`family` id, decoded per its `tail_variant` — the +/// family-grouping/anchoring counterpart of [`hhtl_path`]'s V3 routing branch. +/// A V2/V3 tail stores `family` in bytes 12..14 ([`NodeGuid::family_v2`]); the V1 +/// tail in bytes 10..13 ([`NodeGuid::family`]). Reading the V1 `family()` on a +/// V2/V3 row would fold the `leaf` byte into the id (`I-LEGACY-API-FEATURE-GATED`), +/// so the family decode is routed through the canonical `classid → tail_variant` +/// mapping exactly like the path is. Under no tail feature every classid is V1, so +/// this is just `family()`. +#[inline] +fn family_of(guid: &NodeGuid) -> u32 { + #[cfg(feature = "guid-v2-tail")] + { + use crate::canonical_node::{classid_read_mode, TailVariant}; + if matches!( + classid_read_mode(guid.classid()).tail_variant, + TailVariant::V2 | TailVariant::V3 + ) { + return guid.family_v2() as u32; + } + } + guid.family() +} + +/// The node's `identity` id, decoded per its `tail_variant` (sibling of +/// [`family_of`]): V2/V3 read [`NodeGuid::identity_v2`] (bytes 14..16), V1 reads +/// [`NodeGuid::identity`] (bytes 13..16). +#[inline] +fn identity_of(guid: &NodeGuid) -> u32 { + #[cfg(feature = "guid-v2-tail")] + { + use crate::canonical_node::{classid_read_mode, TailVariant}; + if matches!( + classid_read_mode(guid.classid()).tail_variant, + TailVariant::V2 | TailVariant::V3 + ) { + return guid.identity_v2() as u32; + } + } + guid.identity() +} + /// Project a board-set into a [`GraphSnapshot`] for the Gotham/neo4j surface — /// member nodes + family nodes + (member→family, in-family, out-of-family) /// edges. Touches ONLY the 32-byte head of each row (`key` + `edges`); never the @@ -179,7 +220,7 @@ pub fn project_snapshot(rows: &[NodeRow], domain: &DomainSpec) -> GraphSnapshot let mut by_family: HashMap = HashMap::new(); let mut family_by_low: HashMap> = HashMap::new(); for row in &domain_rows { - let fam = row.key.family(); + let fam = family_of(&row.key); *by_family.entry(fam).or_insert(0) += 1; family_by_low .entry((fam & 0xFF) as u8) @@ -224,10 +265,10 @@ pub fn project_snapshot(rows: &[NodeRow], domain: &DomainSpec) -> GraphSnapshot // Member nodes + their edges (all head-only, family-adapter resolution). for row in &domain_rows { let g = row.key; - let fam = g.family(); + let fam = family_of(&g); nodes.push(RenderNode { id: g.to_string(), - label: format!("{:06x}", g.identity()), + label: format!("{:06x}", identity_of(&g)), kind: domain.name.to_string(), confidence: 1.0, props: vec![ @@ -316,7 +357,7 @@ pub fn nearest_anchor(rows: &[NodeRow], domain: &DomainSpec) -> Vec { // Representative HHTL path per anchor family (first member encountered). let mut anchor_paths: Vec<(u32, NiblePath)> = Vec::new(); for row in &domain_rows { - let fam = row.key.family(); + let fam = family_of(&row.key); if domain.anchor_families.contains(&fam) && !anchor_paths.iter().any(|(f, _)| *f == fam) { anchor_paths.push((fam, hhtl_path(&row.key))); } @@ -374,6 +415,66 @@ mod tests { } } + #[cfg(feature = "guid-v3-tail")] + #[test] + fn v3_rows_decode_family_and_identity_via_tail_variant() { + use crate::canonical_node::classid_read_mode; + // A V3-tail row (mint_for V3 -> new_v2 layout): leaf=0xAAAA @10..12, + // family=0xBBBB @12..14, identity=0xCCCC @14..16. The codex finding: the V1 + // family()/identity() decode would fold the leaf byte into the id, so the + // projection must route through the tail variant (I-LEGACY-API-FEATURE-GATED). + let tv = classid_read_mode(NodeGuid::CLASSID_FMA_V3).tail_variant; + let g = NodeGuid::mint_for( + tv, + NodeGuid::CLASSID_FMA_V3, + 1, + 0, + 0, + 0xAAAA, + 0xBBBB, + 0xCCCC, + ); + + // The tail-aware helpers read the V3 basin (family_v2 / identity_v2)… + assert_eq!(family_of(&g), 0xBBBB, "V3 family = family_v2 (12..14)"); + assert_eq!( + identity_of(&g), + 0xCCCC, + "V3 identity = identity_v2 (14..16)" + ); + // …whereas the raw V1 decode is polluted by the leaf byte (the trap). + assert_ne!( + g.family(), + 0xBBBB, + "V1 family() folds leaf into the id on a V3 row" + ); + + // project_snapshot with a V3 DomainSpec groups members by the V3 family. + let dom = DomainSpec { + classid: NodeGuid::CLASSID_FMA_V3, + name: "fma-v3", + anchor_families: &[], + in_family_edge: "adjacent-to", + out_family_edge: "part-of", + member_edge: "part-of", + }; + let rows = [NodeRow { + key: g, + edges: EdgeBlock::default(), + value: [0u8; 480], + }]; + let snap = project_snapshot(&rows, &dom); + assert!( + snap.nodes.iter().any(|n| n.kind == "Family" + && n.props.iter().any(|(k, v)| k == "family" && v == "00bbbb")), + "family node keyed by the V3 family_v2 (0x00bbbb), not the polluted V1 family" + ); + assert!( + snap.nodes.iter().any(|n| n.label == "00cccc"), + "member labeled by identity_v2" + ); + } + #[test] fn project_emits_family_nodes_and_family_adapter_edges() { // Two families (0xA, 0xB), two members each. Member 1 in family A carries