From 69bd68ef151ef1a06ad85ad782ab20982111b73b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 04:50:10 +0000 Subject: [PATCH 1/7] board: post-merge hygiene for PR #627 (PR_ARC entry + LATEST_STATE row) Co-Authored-By: Claude --- .claude/board/LATEST_STATE.md | 1 + .claude/board/PR_ARC_INVENTORY.md | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index cf1acd44..11788ba9 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -100,6 +100,7 @@ Membrane consumers can now pull BOTH halves of a render `classid` BBB-safely fro | PR | Merged | Title | What it added | |---|---|---|---| +| **#627** | 2026-07-02 | classid canon:custom flip TRIGGERED (doc-only) | Operator ruling recorded + `classid-canon-custom-flip-v1.md` ACTIVE: canon `domain:appid` → hi u16, custom (`0x1000` temporary marker) → lo; `0x0701_1000` / `0x07:01::1000`; OSINT low byte = appid space (zero vocab rows, OGAR #146 67→65 fuse balanced); q2 gate WAIVED; ISSUES ×4 resolved/ruled; codex P2 guards locked (class_id via `split_classid(id).canon` never `as u16`; legacy keys demote not retire). Merge `c8e1ec4`. | | **#626** | 2026-07-02 | V3 convergence wiring: tenant-carve certification, RungElevator, P6 wave probe, seam-list plan | "Wire, don't invent": `RungLevel::{from_u8,elevate,de_elevate,pearl_level,causal_mask_bits}` + `RungElevator` (sustained-BLOCK policy over P2/P3-certified masks; converged with `escalation::rung_delta` via `apply_delta` — one ladder, two signal sources) wired through the driver (persistent elevator, `ctx.rung=1` proxy retired, grpc rung saturates-never-wraps per codex P2); BOTH V3 tenant carves matrix-certified (Cognitive + Compressed); P6 probe (wave dist == certified palette read, markov_soa verified); `[patch.crates-io] ndarray` → local sibling path (fetch deadlock gone; first in-sandbox core build, 925/925). Plan `v3-convergence-wiring-v1.md`; worker Rule 7. Branch `claude/v3-substrate-migration-review-o0yoxv`, merge `5aaee33`. | | **#542** | 2026-06-18 | E-OGAR-IS-FOUNDRY capstone + 5+3 council + the key→row baton | Foundry/Gotham = "write the OGAR class schema + inheritance"; everything else is generic machinery over it (ontology=`classid→ClassView`+inheritance, AR=DO/THINK, pipelines=`compute_dag`, apps=Jinja-over-classes, query=Cypher⇄SurrealQL one IR). Added `MailboxSoaView::row_for_local_key -> Option` (default `None`, deferred-binding — the key→row baton for a future `Backend::MailboxSoa` router). Epiphanies `E-OGAR-IS-FOUNDRY`/`E-CYPHER-IS-THE-KANBAN-AST`/`E-GUID-IS-THE-GRAPH`; plan `cypher-kanban-ast-unification-v1`. Council corrections: `from_guid_prefix` is on `NiblePath` not `NodeGuid`; "odoo proof" = CONJECTURE; `ogar-adapter-surrealql` not a crate. Branch `claude/q2-substrate-grounding`, merge `faca377f`. | | **#540** | 2026-06-18 | `lite-unified` additive default-OFF coexistence feature gate | **+35/-5, 2 files.** `lite-unified = []` in `crates/lance-graph/Cargo.toml` (empty until SurrealQL-on-lance lowering lands). **datafusion stays DEFAULT — NOT deprecated, NOT made optional.** Process, not switch; promoted per query-shape once OQ-LU-2a is green. Zero behavior change at default features. Branch `claude/lite-unified-gate`, merge `ef7e97ef`. | diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index fe2f4a73..eb406a97 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -35,6 +35,22 @@ --- +## #627 lance-graph: classid canon:custom flip TRIGGERED — migration plan v1 + operator ruling record (doc-only) + +**Status:** MERGED 2026-07-02 (merge commit `c8e1ec4`), branch `claude/v3-substrate-migration-review-o0yoxv`. Doc-only: records the operator ruling arc and activates the §2.3 migration. + +**Added:** `.claude/plans/classid-canon-custom-flip-v1.md` (the TRIGGERED atomic Canon:Custom half-order flip — canon `domain:appid` → HIGH u16, custom/marker → LOW; stored `0x0701_1000`, human-readable `0x07:01::1000`; ONE flippable `compose_classid/split_classid/CLASSID_CANON_HIGH`; mint-forward with an I-LEGACY version boundary; phases P0 route-through → P1 flip+coexist → P2 OGAR#95 app-prefix reconciliation [operator checkpoint] → P3 q2 re-mints → P4 `0x1000` retirement [operator checkpoint]). Board: EPIPHANIES `E-CLASSID-CANON-HIGH-TRIGGERED`, INTEGRATION_PLANS prepend, STATUS_BOARD `D-CCF-0..4`, ISSUES status flips ×4. + +**Locked:** OSINT low byte = APPID space domain-wise (`00` = domain, `01` = q2 — zero vocabulary rows, executed OGAR PR #146, codebook 67→65, COUNT_FUSE balanced with zero mirror changes); `0x1000` = temporary reminder, not a format bit; the SoA `class_id` discriminator derives via `split_classid(id).canon` NEVER `as u16` (codex P2 — post-flip `as u16` collapses every class to `0x1000`); legacy registry keys DEMOTE to read-only aliases, retirement only on corpus proof (codex P2 — mint-forward means persisted old-form rows resolve forever until re-baked). + +**Deferred:** P2 + P4 operator checkpoints; implementation phases (D-CCF-0 starts immediately post-merge). + +**Docs/board:** as Added; OGAR side in `DISCOVERY-MAP.md` `D-OSINT-APPID-NOT-CONCEPT` (OGAR #146, merged `a0c7936`). + +**Confidence (2026-07-02):** HIGH for the record (verbatim operator anchors in plan §0); the flip itself is unimplemented by design (doc-first). Both codex P2 findings amended pre-merge. + +**Cross-ref:** OGAR #146; #626 (V3 set completion that armed the trigger); `soa-value-tenant-migration-v2.md` §2.3; `E-CLASSID-SPLIT-ORDER-IS-A-FLIP`. + ## #626 lance-graph: V3 convergence wiring — tenant-carve certification, RungElevator, P6 wave probe, seam-list plan **Status:** MERGED 2026-07-02 (merge commit `5aaee33`), branch `claude/v3-substrate-migration-review-o0yoxv`. The "wire, don't invent" arc: every deliverable a probe or a wiring of EXISTING types (`E-V3-TENANTS-ALREADY-EXIST-WIRE-DONT-INVENT`); operator all-in, Sonnet-grindwork/Fable-decisions model split. From 3b5aea0bb1894b0d851912820021c3c5f9bcf780 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 04:55:30 +0000 Subject: [PATCH 2/7] =?UTF-8?q?contract:=20D-CCF-0=20=E2=80=94=20the=20one?= =?UTF-8?q?=20flippable=20classid=20composition,=20routed=20through,=20zer?= =?UTF-8?q?o=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ClassidOrder {CanonLow, CanonHigh} + CLASSID_ORDER (P0-pinned CanonLow) + compose_classid[_with] / split_classid[_with] / classid_canon / classid_custom / flip_classid in ogar_codebook (the module that already owned the unnamed split — wire, don't invent). Routed through the ONE definition (behavior-identical under CanonLow, probed): classid_concept_domain, classid_concept, classid_app_prefix, render_classid, hhtl::NiblePath::from_guid_prefix's custom-half-zero guard, and the carve-matrix EntityType discriminator stamps (now classid_canon(...) — the codex-P2 class-collapse guard; 'as u16' on a classid is the forbidden pattern). Probes: split-compose round-trip both orders; flip involution over all wired + post-flip ids; the legacy-boundary matrix (every routed reader == its direct mask for every codebook id under every prefix); the no-class-collapse probe (post-flip canon halves 0x0701/0x0A01/0x0E01 distinct while naive 'as u16' collapses all three to 0x1000). Gates: contract 773 w/ v2+v3 features (14 new), 759 default, doctests green, clippy -D warnings clean, fmt clean. Plan: classid-canon-custom-flip-v1.md P0. Co-Authored-By: Claude --- .../src/canonical_node.rs | 12 +- crates/lance-graph-contract/src/hhtl.rs | 15 +- .../lance-graph-contract/src/ogar_codebook.rs | 231 +++++++++++++++++- 3 files changed, 237 insertions(+), 21 deletions(-) diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index a7a8adf3..98a4175f 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -2374,7 +2374,9 @@ mod tests { // read it back — on the V3 class it is still the Osint concept 0x0700 // (the high-u16 gen-marker never leaks into the entity discriminator). let o = ValueTenant::EntityType.value_offset(); - row.value[o..o + 2].copy_from_slice(&(NodeGuid::CLASSID_OSINT_V3 as u16).to_le_bytes()); + row.value[o..o + 2].copy_from_slice( + &crate::ogar_codebook::classid_canon(NodeGuid::CLASSID_OSINT_V3).to_le_bytes(), + ); let et = u16::from_le_bytes([row.value[o], row.value[o + 1]]); assert_eq!( et, 0x0700, @@ -2484,13 +2486,17 @@ mod tests { // cold classes too — Anatomy 0x0A01 / Genetics root 0x0E00, never the // 0x1000 gen-marker. let o = ValueTenant::EntityType.value_offset(); - row.value[o..o + 2].copy_from_slice(&(NodeGuid::CLASSID_FMA_V3 as u16).to_le_bytes()); + row.value[o..o + 2].copy_from_slice( + &crate::ogar_codebook::classid_canon(NodeGuid::CLASSID_FMA_V3).to_le_bytes(), + ); assert_eq!( u16::from_le_bytes([row.value[o], row.value[o + 1]]), 0x0A01, "EntityType tenant carries the canon Anatomy concept" ); - row.value[o..o + 2].copy_from_slice(&(NodeGuid::CLASSID_CPIC_V3 as u16).to_le_bytes()); + row.value[o..o + 2].copy_from_slice( + &crate::ogar_codebook::classid_canon(NodeGuid::CLASSID_CPIC_V3).to_le_bytes(), + ); assert_eq!( u16::from_le_bytes([row.value[o], row.value[o + 1]]), 0x0E00, diff --git a/crates/lance-graph-contract/src/hhtl.rs b/crates/lance-graph-contract/src/hhtl.rs index cc0786a3..6cacfaed 100644 --- a/crates/lance-graph-contract/src/hhtl.rs +++ b/crates/lance-graph-contract/src/hhtl.rs @@ -293,20 +293,23 @@ impl NiblePath { #[must_use] pub const fn from_guid_prefix(guid: &crate::canonical_node::NodeGuid) -> Option { let parts = guid.decode(); - // In THIS v1 fold the high 4 classid nibbles must be zero — it folds - // classid_lo as the coarse tier, so a nonzero high u16 would make the + // In THIS v1 fold the CUSTOM half must be zero — it folds the CANON + // half as the coarse tier, so a nonzero custom half would make the // 20→16 nibble fold lossy. It is reported, not silently re-routed. (The // v3 fold does NOT fold classid — see from_guid_prefix_v3 — so this is a - // v1-fold constraint, not a global reserved-zero law.) - if (parts.classid >> 16) != 0 { + // v1-fold constraint, not a global reserved-zero law.) Halves come from + // the one flippable split (D-CCF-0) — identical to the historical + // `classid >> 16` / `& 0xFFFF` masks while CLASSID_ORDER is CanonLow. + let (canon, custom) = crate::ogar_codebook::split_classid(parts.classid); + if custom != 0 { return None; } // Pack root-first into 16 nibbles = 64 bits = the full u64 path: - // nibbles 0..4 (high) = classid_lo (basin = top nibble of classid_lo) + // nibbles 0..4 (high) = canon half (basin = top nibble of canon) // nibbles 4..8 = HEEL // nibbles 8..12 = HIP // nibbles 12..16 (low) = TWIG (leaf = low nibble of TWIG) - let classid_lo = (parts.classid & 0xFFFF) as u64; + let classid_lo = canon as u64; let path = (classid_lo << 48) | ((parts.heel as u64) << 32) | ((parts.hip as u64) << 16) diff --git a/crates/lance-graph-contract/src/ogar_codebook.rs b/crates/lance-graph-contract/src/ogar_codebook.rs index df4d431a..8b5f13dc 100644 --- a/crates/lance-graph-contract/src/ogar_codebook.rs +++ b/crates/lance-graph-contract/src/ogar_codebook.rs @@ -107,15 +107,18 @@ pub fn canonical_concept_domain(id: u16) -> ConceptDomain { } /// Resolve a [`NodeGuid`](crate::NodeGuid) `classid` to its [`ConceptDomain`] (D-OVC-4). The -/// codebook id is the low 16 bits of the classid (`0xDDCC` lives in the low u16); -/// the high u16 is the canon-reserved zero-fallback prefix. So a domain route is -/// `canonical_concept_domain(classid as u16)`. This is the coarse sibling of the +/// codebook id is the CANON half of the classid (under the active +/// [`CLASSID_ORDER`] — the low u16 while the order is `CanonLow`); the other +/// half is the custom/render prefix. So a domain route is +/// `canonical_concept_domain(classid_canon(classid))`. This is the coarse sibling of the /// per-family scope in [`codebook`](crate::codebook): classid (domain) selects the /// coarse codebook; `family` selects the sub-codebook (longest-prefix-wins). #[inline] #[must_use] pub fn classid_concept_domain(classid: u32) -> ConceptDomain { - canonical_concept_domain(classid as u16) + // Routes the CANON half via the one flippable split (D-CCF-0) — identical + // to the historical `classid as u16` while CLASSID_ORDER is CanonLow. + canonical_concept_domain(classid_canon(classid)) } /// Map a coarse curator `source_domain` tag (`"project"`, `"erp"`, `"german-erp"`) @@ -233,7 +236,12 @@ impl AppPrefix { #[inline] #[must_use] pub const fn render_classid(prefix: u16, concept: u16) -> u32 { - ((prefix as u32) << 16) | (concept as u32) + // The prefix is the CUSTOM half, the concept the CANON half — composed + // through the one flippable definition (D-CCF-0): identical to the + // historical `(prefix << 16) | concept` while CLASSID_ORDER is CanonLow. + // The OGAR#95 hi-u16 scheme ↔ CanonHigh reconciliation is the plan's P2 + // operator checkpoint; this route-through is what makes it one-place. + compose_classid(concept, prefix) } /// Compose a render `classid` from an [`AppPrefix`] and a **canonical-concept @@ -254,23 +262,121 @@ pub fn render_classid_for_concept(app: AppPrefix, concept: &str) -> Option canonical_concept_id(concept).map(|id| app.render(id)) } -/// The APP / render-prefix half of a full `classid` (`classid >> 16`). Mirror -/// of OGAR `ogar_vocab::app::app_of`. Pair with [`AppPrefix::from_prefix`] to -/// recover the typed app. +// ═══════════════════════════════════════════════════════════════════════════ +// The ONE flippable classid composition (D-CCF-0, `classid-canon-custom-flip-v1`) +// ═══════════════════════════════════════════════════════════════════════════ + +/// Which u16 half of a stored `classid: u32` carries the CANON (`domain:appid` +/// / concept) and which carries the CUSTOM (render prefix / the temporary +/// `0x1000` V3 marker). This is the operator's "split order that later you can +/// flip" made a type (`E-CLASSID-SPLIT-ORDER-IS-A-FLIP`): the Canon:Custom +/// half-order migration (`.claude/plans/classid-canon-custom-flip-v1.md`, +/// TRIGGERED 2026-07-02) is a one-place change of [`CLASSID_ORDER`], never +/// per-site byte surgery. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ClassidOrder { + /// Legacy order: canon in the LOW u16, custom in the HIGH (the `0xDDCC` + /// low-half convention every wired classid uses today). + CanonLow, + /// Target order: canon in the HIGH u16, custom in the LOW — stored + /// `0x0701_1000`, human-readable `0x07:01::1000` (plan §0). + CanonHigh, +} + +/// The active half-order. **P0 pins the legacy order** — every route-through +/// below is behavior-identical to the direct masks it replaces (probed). +/// Flipping this const to [`CanonHigh`](ClassidOrder::CanonHigh) IS Phase 1 of +/// the migration; it is mint-forward with a version boundary — flipping alone +/// must never reinterpret persisted ids (the registry keeps concrete-keyed +/// legacy aliases; plan §4 P3, codex P2 on #627). +pub const CLASSID_ORDER: ClassidOrder = ClassidOrder::CanonLow; + +/// Compose a classid under an explicit half-order. +#[inline] +#[must_use] +pub const fn compose_classid_with(order: ClassidOrder, canon: u16, custom: u16) -> u32 { + match order { + ClassidOrder::CanonLow => ((custom as u32) << 16) | (canon as u32), + ClassidOrder::CanonHigh => ((canon as u32) << 16) | (custom as u32), + } +} + +/// Split a classid under an explicit half-order → `(canon, custom)`. +#[inline] +#[must_use] +pub const fn split_classid_with(order: ClassidOrder, classid: u32) -> (u16, u16) { + match order { + ClassidOrder::CanonLow => (classid as u16, (classid >> 16) as u16), + ClassidOrder::CanonHigh => ((classid >> 16) as u16, classid as u16), + } +} + +/// Compose under the active [`CLASSID_ORDER`]. +#[inline] +#[must_use] +pub const fn compose_classid(canon: u16, custom: u16) -> u32 { + compose_classid_with(CLASSID_ORDER, canon, custom) +} + +/// Split under the active [`CLASSID_ORDER`] → `(canon, custom)`. +#[inline] +#[must_use] +pub const fn split_classid(classid: u32) -> (u16, u16) { + split_classid_with(CLASSID_ORDER, classid) +} + +/// The CANON half under the active order — **the** source of the SoA +/// `class_id`/`EntityType` discriminator. Post-flip, a naive `classid as u16` +/// yields the CUSTOM half (`0x1000`) for every V3 class — total class +/// collapse (codex P2 on #627) — so deriving a class discriminator any other +/// way is a forbidden pattern. +#[inline] +#[must_use] +pub const fn classid_canon(classid: u32) -> u16 { + split_classid(classid).0 +} + +/// The CUSTOM half under the active order (render prefix / marker). +#[inline] +#[must_use] +pub const fn classid_custom(classid: u32) -> u16 { + split_classid(classid).1 +} + +/// Recompose a classid under the OTHER order — the flip itself. Involutive: +/// `flip_classid(flip_classid(x)) == x` (probed below). +#[inline] +#[must_use] +pub const fn flip_classid(classid: u32) -> u32 { + let (canon, custom) = split_classid(classid); + let other = match CLASSID_ORDER { + ClassidOrder::CanonLow => ClassidOrder::CanonHigh, + ClassidOrder::CanonHigh => ClassidOrder::CanonLow, + }; + compose_classid_with(other, canon, custom) +} + +/// The APP / render-prefix half of a full `classid` — since #627, the CUSTOM +/// half under the active [`CLASSID_ORDER`] (identical to the historical +/// `classid >> 16` while the order is [`CanonLow`](ClassidOrder::CanonLow)). +/// Mirror of OGAR `ogar_vocab::app::app_of`. Pair with +/// [`AppPrefix::from_prefix`] to recover the typed app. #[inline] #[must_use] pub const fn classid_app_prefix(classid: u32) -> u16 { - (classid >> 16) as u16 + classid_custom(classid) } -/// The canonical concept-id half of a full `classid` (`classid as u16`) — the -/// shared RBAC + ontology + cross-app identity key, identical under every +/// The canonical concept-id half of a full `classid` — since #627, the CANON +/// half under the active [`CLASSID_ORDER`] (identical to the historical +/// `classid as u16` while the order is [`CanonLow`](ClassidOrder::CanonLow)) — +/// the shared RBAC + ontology + cross-app identity key, identical under every /// render prefix. Mirror of OGAR `ogar_vocab::app::concept_of`; the sibling of /// [`classid_concept_domain`], which routes this half to its [`ConceptDomain`]. #[inline] #[must_use] pub const fn classid_concept(classid: u32) -> u16 { - classid as u16 + classid_canon(classid) } /// The curated `(canonical_concept, u16)` codebook — wire-compatible mirror of @@ -620,4 +726,105 @@ mod tests { None ); } + + // ── D-CCF-0 probes — the one flippable classid composition ──────────── + + #[test] + fn classid_split_compose_round_trips_under_both_orders() { + let samples: &[(u16, u16)] = &[ + (0x0700, 0x0000), // legacy OSINT domain classid halves + (0x0701, 0x1000), // post-flip OSINT:q2 halves + (0x0A01, 0x1000), + (0x0E01, 0x1000), + (0x0901, 0x0005), // Healthcare render pair + (0x0000, 0x0000), + (0xFFFF, 0xFFFF), + ]; + for &(canon, custom) in samples { + for order in [ClassidOrder::CanonLow, ClassidOrder::CanonHigh] { + let id = compose_classid_with(order, canon, custom); + assert_eq!( + split_classid_with(order, id), + (canon, custom), + "split∘compose must be identity under {order:?}" + ); + } + } + } + + #[test] + fn classid_flip_is_involutive_and_p0_pins_legacy_order() { + // P0 pin: the active order is the legacy CanonLow — flipping this + // const IS the migration's Phase 1, never a drive-by. + assert_eq!(CLASSID_ORDER, ClassidOrder::CanonLow); + // flip(flip(x)) == x over every wired classid + the post-flip trio. + for id in [ + 0x0000_0700u32, // legacy OSINT domain class + 0x1000_0700, // pre-flip OSINT-V3 + 0x1000_0A01, + 0x1000_0E00, + 0x0701_1000, // post-flip forms (already valid u32s to flip back) + 0x0A01_1000, + 0x0E01_1000, + 0x0005_0901, // Healthcare render classid + 0x0000_0000, + 0xFFFF_FFFF, + ] { + assert_eq!( + flip_classid(flip_classid(id)), + id, + "flip must be involutive" + ); + } + } + + #[test] + fn classid_route_through_is_behavior_identical_under_legacy_order() { + // The legacy-boundary matrix (plan §3): under CanonLow every routed + // reader equals the direct mask it replaced, for every codebook id + // under every app prefix — the P0 zero-behavior gate. + for &(_, concept) in CODEBOOK { + for prefix in [0x0000u16, 0x0001, 0x0005, 0x1000] { + let id = render_classid(prefix, concept); + assert_eq!(id, ((prefix as u32) << 16) | (concept as u32)); + assert_eq!(classid_concept(id), concept); + assert_eq!(classid_app_prefix(id), prefix); + assert_eq!(classid_canon(id), id as u16); + assert_eq!(classid_custom(id), (id >> 16) as u16); + assert_eq!( + classid_concept_domain(id), + canonical_concept_domain(concept), + "domain routing invariant under the route-through" + ); + } + } + } + + #[test] + fn no_class_collapse_under_canon_high() { + // codex P2 (#627): post-flip, a naive `as u16` reads the CUSTOM half — + // 0x1000 for ALL three V3 classes — collapsing the SoA class_id + // discriminator. The canon half stays distinct; `as u16` does not. + let osint = compose_classid_with(ClassidOrder::CanonHigh, 0x0701, 0x1000); + let fma = compose_classid_with(ClassidOrder::CanonHigh, 0x0A01, 0x1000); + let cpic = compose_classid_with(ClassidOrder::CanonHigh, 0x0E01, 0x1000); + assert_eq!((osint, fma, cpic), (0x0701_1000, 0x0A01_1000, 0x0E01_1000)); + + let canons = [ + split_classid_with(ClassidOrder::CanonHigh, osint).0, + split_classid_with(ClassidOrder::CanonHigh, fma).0, + split_classid_with(ClassidOrder::CanonHigh, cpic).0, + ]; + assert_eq!( + canons, + [0x0701, 0x0A01, 0x0E01], + "canon halves stay distinct" + ); + // The forbidden pattern, demonstrated: `as u16` collapses all three. + assert_eq!( + [osint as u16, fma as u16, cpic as u16], + [0x1000, 0x1000, 0x1000], + "naive `as u16` post-flip = total class collapse (why it is forbidden)" + ); + } } From 804422b53349b7dbe2dd50cd6b971b759e7d9705 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 05:18:42 +0000 Subject: [PATCH 3/7] =?UTF-8?q?contract:=20D-CCF-1=20=E2=80=94=20flip=20CL?= =?UTF-8?q?ASSID=5FORDER=20to=20CanonHigh=20(canon=20HIGH,=20custom=20LOW)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The P1 flip of classid-canon-custom-flip-v1 (operator trigger 2026-07-02): - CLASSID_ORDER = CanonHigh; stored classids now carry canon (domain:appid) in the HIGH u16, custom (marker/render prefix) in the LOW u16. - New mint surface: CLASSID_OSINT 0x0700_0000, CLASSID_FMA 0x0A01_0000, CLASSID_PROJECT 0x0100_0000, CLASSID_ERP 0x0200_0000; V3 classes 0x0701_1000 / 0x0A01_1000 / 0x0E01_1000 (OSINT + CPIC appid normalized to :01 = q2 per the ruling). - Mint-forward boundary: every pre-flip stored form registered as a CLASSID_*_LEGACY read-only alias in BUILTIN_READ_MODES — persisted rows resolve until re-baked; retirement gated on corpus proof (codex P2 #627). - hhtl::from_guid_prefix (v1 fold) accepts pure-canon classids in BOTH stored forms and folds them to the identical path; marked classids (both halves nonzero) still refuse. - OGAR#95 reconciliation (plan P2): the app prefix IS the custom half — render_classid composes concept HIGH / prefix LOW (patient@Healthcare = 0x0901_0005); the #95 table becomes the custom-half render catalogue. - Board: EPIPHANIES E-CLASSID-FLIP-P1-LANDED, STATUS_BOARD D-CCF rows, AGENT_LOG fleet-inventory entry (same commit per board-hygiene rule). Gates: contract 773 (guid-v3-tail) / 759 (default) + doctests; clippy -D warnings; dependents green (callcenter, cognitive-shader-driver, planner). Co-Authored-By: Claude --- .claude/board/AGENT_LOG.md | 19 ++ .claude/board/EPIPHANIES.md | 31 ++ .claude/board/STATUS_BOARD.md | 6 +- crates/lance-graph-contract/Cargo.toml | 9 +- .../src/canonical_node.rs | 305 +++++++++++------- crates/lance-graph-contract/src/hhtl.rs | 122 ++++--- .../lance-graph-contract/src/ogar_codebook.rs | 158 +++++---- 7 files changed, 424 insertions(+), 226 deletions(-) diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index ee87e5ef..8e2b29c6 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,22 @@ +## 2026-07-02 — Fleet flip inventories (5× Sonnet, read-only) + P1 flip landed + +- **Agents:** q2 / OGAR+OGIT / MedCare-rs+openproject-nexgen-rs+openproject / + woa-rs / tesseract-rs — exhaustive classid half-order site inventories with + Rule-7 negative-existence declarations. Outcomes: OGIT zero; openproject + (Ruby) zero; tesseract-rs zero (contract dep is unichar-only; OCR domain + already allocated as ConceptDomain::Ocr 0x08); woa-rs one stale doc comment + (erp/canon.rs Phase-3 mint); MedCare-rs auth test literal 0x0000_0B01 + + docs; openproject-nexgen op-canon ~13 pinned literals (bit math lives + upstream in ogar_vocab); OGAR = the canonical flip site + (ogar_vocab::app 4 fns + mint.rs tests + large doc sweep) + flags + ruff_spo_address::Facet (AdaWorldAPI/ruff git dep) as companion; q2 = + osint-bake/cockpit-server compose+decompose sites, fma/ + cpic/ standalone + schemes, BAKED artifacts (osint_scene.soa, fma.soa, SAMPLE_GUIDS.tsv, + aiwar.codebook, release body.soa) needing re-bake. +- **Main thread:** D-CCF-1 (P1 flip) implemented in lance-graph-contract — + CanonHigh live, new-form constants + legacy aliases, hhtl dual-form + boundary. Gates green (773/759 + doctests + clippy + dependents). PR #628. + ## 2026-07-01 (cont.) — v3-convergence-wiring D1/D2 execution (2 Sonnet grindwork agents + Fable finish) **Main thread (Fable 5) + two Sonnet 5 agents (edit-only, shared checkout, no worktrees).** (1) **P6 agent (D-VCW-2, completed):** extended `markov_soa` tests with `p6_palette_join` — self-match exactly 1.0 under a real zero-diagonal 256×256 palette table + hand-computed table arithmetic == `best_guess_match` output; 6/6 module green; correctly refused a planner dep (the TABLE is the join object, dependency flows AriGraph→sensor never reverse). Flagged the pre-existing planner deprecation clippy debt (→ TD-DEPRECATED-ACCESSORS-BLOCK-DEP-CLIPPY). (2) **D1b agent (D-VCW-1b, killed mid-test by worker restart; Fable finished):** driver-persistent `RwLock` on `ShaderDriver` (per-call-local would never accumulate a streak — the agent's own correct design call), base-change reset so streaks never leak across dispatch contexts, gate fed POST-decision (provenance never alters the gate), `materialize_provenance(…, rung)` replaces the `ctx.rung = 1` proxy, `wire.rs`/`grpc.rs` 10-arm matches deduped through `RungLevel::from_u8`. Fable finished the second test honestly: rung is a +1 tie-weight in tactic scoring, so inequality is asserted for the EMPIRICALLY-differentiating input (rung 1→tactic 17, rung 9→tactic 3 at authoring), not claimed universal. Gates: driver 100/100, contract 755 regression green, fmt clean, driver-own lints clean (dep-closure clippy blocked by the pre-existing deprecation debt, recorded). Commit: this one. diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 03015ce9..848e1223 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,34 @@ +## 2026-07-02 — E-CLASSID-FLIP-P1-LANDED — CanonHigh is live: canon HIGH / custom LOW, legacy aliases resolve persisted rows, OGAR#95 reconciled as the custom-half render catalogue + +**Status:** SHIPPED (PR #628 arc; P0 route-through fd9bf6b → P1 flip this commit). + +1. **`CLASSID_ORDER = CanonHigh`** — one-const flip per + `E-CLASSID-SPLIT-ORDER-IS-A-FLIP`. New mint surface: v1 classes + `0x0700_0000` (OSINT) / `0x0A01_0000` (FMA) / `0x0100_0000` (PROJECT) / + `0x0200_0000` (ERP); V3 classes `0x0701_1000` (OSINT:q2, appid + normalized) / `0x0A01_1000` (FMA:q2) / `0x0E01_1000` (CPIC Genetics:q2, + normalized `:00`→`:01` per the ruling). +2. **Mint-forward boundary:** every pre-flip stored form stays registered as + a `CLASSID_*_LEGACY` read-only alias key in `BUILTIN_READ_MODES` — + persisted rows resolve forever until re-baked; retirement gated on corpus + proof (codex P2 #627). `hhtl::from_guid_prefix` (v1 fold) accepts BOTH + pure-canon stored forms and folds them to the IDENTICAL path; genuinely + marked classids (both halves nonzero) still refuse. +3. **OGAR#95 reconciliation (plan P2) resolved by construction:** the app + prefix IS the custom half — `render_classid(prefix, concept)` composes + concept HIGH / prefix LOW (`patient` under Healthcare = `0x0901_0005`). + The #95 allocation table becomes the CUSTOM-half render catalogue; prefix + VALUES unchanged. OGAR `ogar_vocab::app` flips in lockstep (operator: + "also flip ogar to match"). +4. **Domain routing on legacy-form ids intentionally does NOT route** — + old rows resolve via concrete alias keys, not `classid_concept_domain` + (documented on the function). New mints route correctly off the canon. + +Gates: contract 773 (guid-v3-tail) / 759 (default) + doctests green; clippy +-D warnings clean; dependents green (callcenter 163, cognitive-shader-driver +104, planner 204). Fleet inventories (q2 / OGAR+OGIT / medcare+openproject / +woa-rs / tesseract-rs) recorded in AGENT_LOG; consumer PRs follow. + ## 2026-07-02 — E-CLASSID-CANON-HIGH-TRIGGERED — the operator pulled the flip trigger: canon (domain:appid) moves to the HIGH half, `0x1000` was a temporary reminder, OSINT low byte = appid space, q2 gate waived **Status:** DOCTRINE (operator ruling, verbatim anchors in the plan §0). diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index b3602194..47207d41 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -4,9 +4,9 @@ Plan: `.claude/plans/classid-canon-custom-flip-v1.md`. Operator trigger 2026-07- | D-id | Title | Crate(s) | Status | Evidence | |---|---|---|---|---| -| D-CCF-0 | compose_classid/split_classid/CLASSID_CANON_HIGH + route all sites (zero behavior) | lance-graph-contract | Queued | plan §3/§4 P0 | -| D-CCF-1 | Flip + mint new-form classids (0x0701_1000 / 0x0A01_1000 / 0x0E01_1000) coexisting | lance-graph-contract | Queued | gated on P0 probes | -| D-CCF-2 | OGAR#95 hi-u16 app-prefix reconciliation | contract + OGAR | Blocked (operator checkpoint) | plan §2 row / §4 P2 | +| D-CCF-0 | compose_classid/split_classid/CLASSID_CANON_HIGH + route all sites (zero behavior) | lance-graph-contract | Shipped (fd9bf6b) | plan §3/§4 P0 | +| D-CCF-1 | Flip + mint new-form classids (0x0701_1000 / 0x0A01_1000 / 0x0E01_1000) coexisting | lance-graph-contract | In PR (#628) | gated on P0 probes | +| D-CCF-2 | OGAR#95 hi-u16 app-prefix reconciliation | contract + OGAR | In progress (resolved: prefix = custom half; OGAR flips in lockstep per operator) | plan §2 row / §4 P2 | | D-CCF-3 | q2 re-mints (osint-bake + cpic via contract pull; dissolves ISS-Q2-CPIC-MIRROR) | q2 (gate WAIVED) | Queued | plan §4 P3 | | D-CCF-4 | 0x1000 marker retirement | all | Blocked (operator checkpoint) | plan §4 P4 | diff --git a/crates/lance-graph-contract/Cargo.toml b/crates/lance-graph-contract/Cargo.toml index 4e3d5f60..ff0283ef 100644 --- a/crates/lance-graph-contract/Cargo.toml +++ b/crates/lance-graph-contract/Cargo.toml @@ -46,10 +46,11 @@ guid-v2-tail = [] # guid-v3-tail (P-A, plan: soa-value-tenant-migration-v2.md §2.1/§2.2) — gates the # V3 cascade-key per-classid entries. The V3 identity AXIS itself (TailVariant::V3 # + ReadMode::tail_variant) is unconditional, latent (nothing reads tail_variant -# yet) → non-breaking. The gated entries pin the generation marker in the HIGH -# (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. +# yet) → non-breaking. Since the classid half-order flip (P1, 2026-07-02, +# classid-canon-custom-flip-v1.md) the CANON `domain:appid` sits in the HIGH u16 +# and the gen-marker 0x1000 in the LOW (custom) u16: OSINT-V3 = 0x0701_1000, +# FMA-V3 = 0x0A01_1000, CPIC-V3 = 0x0E01_1000; the pre-flip 0x1000_DDCC forms +# stay registered as read-only legacy aliases (mint-forward). Default OFF. # # 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), diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index 98a4175f..852ec1a7 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -42,70 +42,99 @@ impl NodeGuid { // ── classids follow OGAR `ogar-vocab`'s domain-encoded `0xDDCC` codebook ── // (DD = domain high byte, CC = concept slot; CC=0x00 = domain root, reserved). - // `canonical_concept_domain(classid_lo)` (see `crate::ogar_codebook`) routes on - // `classid >> 8`. Realigned 2026-06-20 (ISS-CLASSID-OGAR-DRIFT): OSINT was - // 0x0007 (OGAR Reserved domain) → 0x0700; FMA was 0x0008 (OGAR OCR block) → - // 0x0901. Re-realigned 2026-06-24 (ISS-CLASSID-OGAR-DRIFT cont.): FMA 0x0901 - // **collided with OGAR `patient` (0x0901)** — both Health. FMA now routes to - // the new **Anatomy** domain root 0x0A01 (`anatomical_structure`); anatomy is - // public reference, not Health PHI. Surfaced by OGAR `docs/NODEGUID-CANON-AUDIT.md` - // F-1. Migration: `.claude/plans/ogar-vocab-contract-codebook-migration-v1.md`. + // The `0xDDCC` codebook id is the classid's CANON half — the HIGH u16 since + // the 2026-07-02 half-order flip (`classid-canon-custom-flip-v1` P1; + // `crate::ogar_codebook::classid_canon`). Realigned 2026-06-20 + // (ISS-CLASSID-OGAR-DRIFT): OSINT was 0x0007 (OGAR Reserved domain) → 0x0700; + // FMA was 0x0008 (OGAR OCR block) → 0x0901. Re-realigned 2026-06-24 + // (ISS-CLASSID-OGAR-DRIFT cont.): FMA 0x0901 **collided with OGAR `patient` + // (0x0901)** — both Health. FMA now routes to the new **Anatomy** domain root + // 0x0A01 (`anatomical_structure`); anatomy is public reference, not Health + // PHI. Surfaced by OGAR `docs/NODEGUID-CANON-AUDIT.md` F-1. Migration: + // `.claude/plans/ogar-vocab-contract-codebook-migration-v1.md`. + // + // MINT-FORWARD BOUNDARY (flip P1/P3): the named constants below are the + // MINT surface — they carry the new canon-HIGH form. The pre-flip stored + // forms (`0x0000_DDCC` v1 / `0x1000_DDCC` V3) stay behind as + // `CLASSID_*_LEGACY` read-only aliases so persisted rows keep resolving + // through `BUILTIN_READ_MODES`; they retire only on corpus proof that zero + // old-form rows remain (codex P2 #627 — RESERVE, DON'T RECLAIM). /// **OSINT / Palantir-Gotham** domain root (`0x07` = OSINT domain, `0x00` = - /// root). The neo4j-emulation entity graph (people / orgs / systems / events, - /// family-grouped). Resolves to [`ReadMode::OSINT`] (hot `Cognitive` value + - /// `CoarseOnly` adjacency). - pub const CLASSID_OSINT: u32 = 0x0000_0700; + /// root — "applied domain-wise" per the 2026-07-02 ruling). The + /// neo4j-emulation entity graph (people / orgs / systems / events, + /// family-grouped). Canon `0x0700` HIGH, custom `0x0000`. Resolves to + /// [`ReadMode::OSINT`] (hot `Cognitive` value + `CoarseOnly` adjacency). + pub const CLASSID_OSINT: u32 = 0x0700_0000; + /// Pre-flip stored form of [`CLASSID_OSINT`] (canon in the LOW half) — + /// read-only legacy alias for persisted rows; do NOT mint with this. + pub const CLASSID_OSINT_LEGACY: u32 = 0x0000_0700; /// **FMA anatomy** — `anatomical_structure` (`0x01`) in the **Anatomy** domain /// (`0x0A`); `0x0A00` is the Anatomy root. The Foundational Model of Anatomy /// (~70k structural entities, family = body region, bones = stability anchors). /// Anatomy is **public reference, not Health PHI** — moved off `0x0901` to - /// clear the collision with OGAR `patient`. Resolves to [`ReadMode::FMA`] - /// (cold `Compressed` reference + `CoarseOnly`). - pub const CLASSID_FMA: u32 = 0x0000_0A01; + /// clear the collision with OGAR `patient`. Canon `0x0A01` HIGH. Resolves to + /// [`ReadMode::FMA`] (cold `Compressed` reference + `CoarseOnly`). + pub const CLASSID_FMA: u32 = 0x0A01_0000; + /// Pre-flip stored form of [`CLASSID_FMA`] — read-only legacy alias. + pub const CLASSID_FMA_LEGACY: u32 = 0x0000_0A01; /// **Project-management** domain root (`0x01`) — OpenProject ↔ Redmine - /// (work items, members, versions, …). OGAR codebook `0x01XX`. Resolves to - /// [`ReadMode::PROJECT`]. - pub const CLASSID_PROJECT: u32 = 0x0000_0100; + /// (work items, members, versions, …). OGAR codebook `0x01XX`. Canon + /// `0x0100` HIGH. Resolves to [`ReadMode::PROJECT`]. + pub const CLASSID_PROJECT: u32 = 0x0100_0000; + /// Pre-flip stored form of [`CLASSID_PROJECT`] — read-only legacy alias. + pub const CLASSID_PROJECT_LEGACY: u32 = 0x0000_0100; /// **Commerce / ERP** domain root (`0x02`) — Odoo ↔ OSB (invoices, taxes, - /// partners, payments, …). OGAR codebook `0x02XX`. Resolves to [`ReadMode::ERP`]. - pub const CLASSID_ERP: u32 = 0x0000_0200; + /// partners, payments, …). OGAR codebook `0x02XX`. Canon `0x0200` HIGH. + /// Resolves to [`ReadMode::ERP`]. + pub const CLASSID_ERP: u32 = 0x0200_0000; + /// Pre-flip stored form of [`CLASSID_ERP`] — read-only legacy alias. + pub const CLASSID_ERP_LEGACY: u32 = 0x0000_0200; // ── V3 cascade-key classids (feature `guid-v3-tail`) ─────────────────────── - // The V3 generation marker lives in the HIGH (custom) u16, leaving the canon - // LOW u16 untouched, because the live contract routes domain on `classid as - // 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`), 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 - /// in the LOW u16, so [`classid_concept_domain`](crate::ogar_codebook::classid_concept_domain) - /// still routes [`Osint`](crate::ogar_codebook::ConceptDomain::Osint) (it masks - /// `classid as u16` → low half) — unlike the rejected low-half `0x0000_1007`, - /// which read domain `0x10` = `Unassigned`. Resolves to [`ReadMode::OSINT_V3`]. + // Since the 2026-07-02 half-order flip (P1): the CANON (`domain:appid`, + // e.g. `0x0701` = OSINT domain `0x07`, appid `0x01` = q2) sits in the HIGH + // u16 and the V3 generation marker `0x1000` in the LOW/custom u16 — stored + // `0x0701_1000`, human-readable `0x07:01::1000` (the operator's mnemonic). + // The marker is temporary by declaration (a "hard reminder" of the V3 + // migration); its retirement is the plan's P4 operator checkpoint. The + // appid byte normalizes to `:01` (q2, the consumer app) for OSINT and + // CPIC per the ruling ("0701 is q2 as the OSINT appid, our consumer"; + // "same for cpic also under q2"); FMA was already `:01`. Pre-flip stored + // forms (`0x1000_DDCC`) remain as `_LEGACY` read-only alias keys. + + /// **OSINT-V3** — OSINT on a [`TailVariant::V3`] cascade tail, minted for + /// q2 (appid `0x01`). Canon `0x0701` HIGH; the V3 marker `0x1000` in the + /// LOW/custom u16 — `0x07:01::1000`. + /// [`classid_concept_domain`](crate::ogar_codebook::classid_concept_domain) + /// routes [`Osint`](crate::ogar_codebook::ConceptDomain::Osint) off the + /// canon half. Resolves to [`ReadMode::OSINT_V3`]. + #[cfg(feature = "guid-v3-tail")] + pub const CLASSID_OSINT_V3: u32 = 0x0701_1000; + /// Pre-flip stored form of [`CLASSID_OSINT_V3`] (marker HIGH, canon + /// `0x0700` LOW — note the pre-normalization appid `:00`) — read-only + /// legacy alias for persisted rows; do NOT mint with this. #[cfg(feature = "guid-v3-tail")] - pub const CLASSID_OSINT_V3: u32 = 0x1000_0700; + pub const CLASSID_OSINT_V3_LEGACY: 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 + /// **FMA-V3** — FMA anatomy on a [`TailVariant::V3`] cascade tail, minted + /// for q2. Canon `0x0A01` HIGH (Anatomy domain `0x0A`, appid `0x01`); the + /// V3 marker `0x1000` in the LOW/custom u16 — `0x0A:01::1000`. /// [`classid_concept_domain`](crate::ogar_codebook::classid_concept_domain) - /// still routes [`Anatomy`](crate::ogar_codebook::ConceptDomain::Anatomy). + /// 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; + pub const CLASSID_FMA_V3: u32 = 0x0A01_1000; + /// Pre-flip stored form of [`CLASSID_FMA_V3`] — read-only legacy alias. + #[cfg(feature = "guid-v3-tail")] + pub const CLASSID_FMA_V3_LEGACY: 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 + /// [`TailVariant::V3`] cascade tail, in the **Genetics** domain (`0x0E`, + /// operator-allocated 2026-06-26 — `0x0D` was already HR), minted for q2 + /// (appid `0x01`, normalized from the pre-flip domain-root `:00` per the + /// ruling "same for cpic also under q2"). Canon `0x0E01` HIGH; the V3 + /// marker `0x1000` in the LOW/custom u16 — `0x0E:01::1000`. /// [`classid_concept_domain`](crate::ogar_codebook::classid_concept_domain) /// routes [`Genetics`](crate::ogar_codebook::ConceptDomain::Genetics). Resolves /// to [`ReadMode::CPIC_V3`]. @@ -121,7 +150,11 @@ impl NodeGuid { /// 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; + pub const CLASSID_CPIC_V3: u32 = 0x0E01_1000; + /// Pre-flip stored form of [`CLASSID_CPIC_V3`] (marker HIGH, canon + /// `0x0E00` LOW — pre-normalization appid `:00`) — read-only legacy alias. + #[cfg(feature = "guid-v3-tail")] + pub const CLASSID_CPIC_V3_LEGACY: u32 = 0x1000_0E00; /// Construct from the six canonical groups. `family`/`identity` use their low 3 bytes. /// @@ -1159,16 +1192,28 @@ static BUILTIN_READ_MODES: LazyLock> = LazyLock::new(|| { // the OGAR `0x01XX` / `0x02XX` domains; both hot business graphs (Cognitive). m.insert(NodeGuid::CLASSID_PROJECT, ReadMode::PROJECT); m.insert(NodeGuid::CLASSID_ERP, ReadMode::ERP); + // LEGACY ALIASES (flip P1/P3, codex P2 #627): the pre-flip stored forms + // (canon in the LOW half) resolve to the SAME read modes so persisted + // pre-flip rows keep reading correctly — mint-forward, never blanket + // reinterpretation. Read-only: do NOT mint with these keys. Retirement is + // a later step gated on a corpus proof that zero old-form rows remain. + m.insert(NodeGuid::CLASSID_OSINT_LEGACY, ReadMode::OSINT); + m.insert(NodeGuid::CLASSID_FMA_LEGACY, ReadMode::FMA); + m.insert(NodeGuid::CLASSID_PROJECT_LEGACY, ReadMode::PROJECT); + m.insert(NodeGuid::CLASSID_ERP_LEGACY, ReadMode::ERP); // V3 cascade-key classes (feature `guid-v3-tail`): same value model as their - // 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). + // legacy domain, on a TailVariant::V3 tail. Since the P1 flip the canon + // (`domain:appid`) is the HIGH u16 (`0x0701_1000`-form), so + // `classid_concept_domain` routes Osint / Anatomy / Genetics directly off + // the canon half; the pre-flip `0x1000_DDCC` forms stay as aliases. #[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.insert(NodeGuid::CLASSID_OSINT_V3_LEGACY, ReadMode::OSINT_V3); + m.insert(NodeGuid::CLASSID_FMA_V3_LEGACY, ReadMode::FMA_V3); + m.insert(NodeGuid::CLASSID_CPIC_V3_LEGACY, ReadMode::CPIC_V3); } m }); @@ -2052,24 +2097,37 @@ mod tests { assert_eq!(fma.value_schema, ValueSchema::Compressed); assert_eq!(fma.edge_codec, EdgeCodecFlavor::CoarseOnly); - // The classids follow OGAR `0xDDCC` (ISS-CLASSID-OGAR-DRIFT realign): - // OSINT domain root `0x0700` (`>>8 == 0x07`); FMA = `anatomical_structure` - // `0x0A01` in the **Anatomy** domain (`>>8 == 0x0A`) — re-realigned off - // `0x0901` to clear the OGAR `patient` collision. Never the pre-realign - // 0x0007 / 0x0008, nor the colliding 0x0901. - assert_eq!(NodeGuid::CLASSID_OSINT, 0x0000_0700); - assert_eq!(NodeGuid::CLASSID_FMA, 0x0000_0A01); + // The classids follow OGAR `0xDDCC` in the CANON (high-since-P1) half + // (ISS-CLASSID-OGAR-DRIFT realign): OSINT domain root `0x0700`; FMA = + // `anatomical_structure` `0x0A01` in the **Anatomy** domain — re-realigned + // off `0x0901` to clear the OGAR `patient` collision. Never the + // pre-realign 0x0007 / 0x0008, nor the colliding 0x0901. + use crate::ogar_codebook::classid_canon; + assert_eq!(NodeGuid::CLASSID_OSINT, 0x0700_0000); + assert_eq!(NodeGuid::CLASSID_FMA, 0x0A01_0000); assert_ne!( - NodeGuid::CLASSID_FMA, - 0x0000_0901, + classid_canon(NodeGuid::CLASSID_FMA), + 0x0901, "must not alias `patient`" ); - assert_eq!(NodeGuid::CLASSID_OSINT >> 8, 0x07, "OSINT domain high byte"); - assert_eq!(NodeGuid::CLASSID_FMA >> 8, 0x0A, "Anatomy domain high byte"); + assert_eq!( + classid_canon(NodeGuid::CLASSID_OSINT) >> 8, + 0x07, + "OSINT domain byte" + ); + assert_eq!( + classid_canon(NodeGuid::CLASSID_FMA) >> 8, + 0x0A, + "Anatomy domain byte" + ); assert_eq!( NodeGuid::new(NodeGuid::CLASSID_OSINT, 1, 2, 3, 0xAB, 0xCD).read_mode(), ReadMode::OSINT ); + // Mint-forward: the persisted pre-flip forms resolve to the SAME read + // modes through their legacy alias keys (read-only, never minted). + assert_eq!(classid_read_mode(NodeGuid::CLASSID_OSINT_LEGACY), osint); + assert_eq!(classid_read_mode(NodeGuid::CLASSID_FMA_LEGACY), fma); assert!(osint.is_layout_preserving() && fma.is_layout_preserving()); } @@ -2087,12 +2145,16 @@ mod tests { assert_eq!(erp.value_schema, ValueSchema::Cognitive); assert_eq!(erp.edge_codec, EdgeCodecFlavor::CoarseOnly); - // Domain roots: project `0x0100` (`>>8 == 0x01`), ERP `0x0200` - // (`>>8 == 0x02`); low byte `0x00` = the domain root (reserved concept). - assert_eq!(NodeGuid::CLASSID_PROJECT, 0x0000_0100); - assert_eq!(NodeGuid::CLASSID_ERP, 0x0000_0200); - assert_eq!(NodeGuid::CLASSID_PROJECT >> 8, 0x01); - assert_eq!(NodeGuid::CLASSID_ERP >> 8, 0x02); + // Domain roots in the CANON (high-since-P1) half: project `0x0100`, + // ERP `0x0200`; concept byte `0x00` = the domain root (reserved). + use crate::ogar_codebook::classid_canon; + assert_eq!(NodeGuid::CLASSID_PROJECT, 0x0100_0000); + assert_eq!(NodeGuid::CLASSID_ERP, 0x0200_0000); + assert_eq!(classid_canon(NodeGuid::CLASSID_PROJECT) >> 8, 0x01); + assert_eq!(classid_canon(NodeGuid::CLASSID_ERP) >> 8, 0x02); + // Mint-forward: pre-flip forms keep resolving via the legacy aliases. + assert_eq!(classid_read_mode(NodeGuid::CLASSID_PROJECT_LEGACY), project); + assert_eq!(classid_read_mode(NodeGuid::CLASSID_ERP_LEGACY), erp); assert!(project.is_layout_preserving() && erp.is_layout_preserving()); } @@ -2123,13 +2185,12 @@ mod tests { #[cfg(feature = "guid-v3-tail")] #[test] fn read_mode_osint_v3_routes_v3_tail_and_osint_domain() { - // The wired V3 exemplar proves BOTH facts at once — the whole point of the - // high-u16 gen-marker scheme: + // The wired V3 exemplar proves BOTH facts at once — the canon/custom + // split scheme (canon HIGH since the P1 flip): // (1) the third axis IS the registry field — classid_read_mode resolves // CLASSID_OSINT_V3 to TailVariant::V3 (never a public new_v3 dispatch); - // (2) the Codex-P1 fix — the gen-marker 0x1000 sits in the HIGH u16, so the - // domain router (which masks `classid as u16`) still resolves Osint, - // unlike the rejected low-half 0x1007 (which read domain 0x10). + // (2) the domain router reads the CANON half (0x0701 = OSINT:q2), so the + // gen-marker 0x1000 in the custom half never perturbs domain routing. assert_eq!( classid_read_mode(NodeGuid::CLASSID_OSINT_V3).tail_variant, TailVariant::V3 @@ -2146,27 +2207,39 @@ mod tests { ); assert_eq!(ReadMode::OSINT_V3.value_schema, ValueSchema::Cognitive); assert!(ReadMode::OSINT_V3.is_layout_preserving()); - // Concretely: marker in the HIGH half, canon domain in the LOW half. - assert_eq!(NodeGuid::CLASSID_OSINT_V3, 0x1000_0700); + // Concretely: canon (domain:appid) in the HIGH half, marker in the LOW — + // stored `0x0701_1000`, human-readable `0x07:01::1000`. The appid + // normalizes to `:01` (q2) per the 2026-07-02 ruling, so the V3 canon is + // 0x0701, not the v1 domain root 0x0700. + use crate::ogar_codebook::{classid_canon, classid_custom}; + assert_eq!(NodeGuid::CLASSID_OSINT_V3, 0x0701_1000); assert_eq!( - NodeGuid::CLASSID_OSINT_V3 >> 16, + classid_custom(NodeGuid::CLASSID_OSINT_V3), 0x1000, - "gen-marker in high u16" + "gen-marker in the custom (low) half" ); assert_eq!( - NodeGuid::CLASSID_OSINT_V3 as u16, - NodeGuid::CLASSID_OSINT as u16, - "low u16 == canon OSINT concept (0x0700)" + classid_canon(NodeGuid::CLASSID_OSINT_V3), + 0x0701, + "canon == OSINT domain 0x07, appid 0x01 (q2)" ); + // Mint-forward: the persisted pre-flip form resolves the same read mode. + assert_eq!( + classid_read_mode(NodeGuid::CLASSID_OSINT_V3_LEGACY), + ReadMode::OSINT_V3 + ); + assert_eq!(NodeGuid::CLASSID_OSINT_V3_LEGACY, 0x1000_0700); } #[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}; + use crate::ogar_codebook::{ + classid_canon, classid_concept_domain, classid_custom, 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. + // their domain routes off the CANON (high) half, unperturbed by the + // gen-marker in the custom half — the same scheme proven for OSINT-V3. // FMA-V3: Anatomy domain (0x0A) intact; cold Compressed model (mirrors FMA). assert_eq!( @@ -2182,16 +2255,21 @@ mod tests { 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, 0x0A01_1000); assert_eq!( - NodeGuid::CLASSID_FMA_V3 >> 16, + classid_custom(NodeGuid::CLASSID_FMA_V3), 0x1000, - "gen-marker high u16" + "gen-marker in the custom (low) half" ); assert_eq!( - NodeGuid::CLASSID_FMA_V3 as u16, - NodeGuid::CLASSID_FMA as u16, - "low u16 == canon FMA concept (0x0A01)" + classid_canon(NodeGuid::CLASSID_FMA_V3), + classid_canon(NodeGuid::CLASSID_FMA), + "canon == FMA concept (0x0A01), shared with v1 FMA" + ); + assert_eq!( + classid_read_mode(NodeGuid::CLASSID_FMA_V3_LEGACY), + ReadMode::FMA_V3, + "pre-flip form resolves via the legacy alias" ); // CPIC-V3: the operator-allocated Genetics domain (0x0E); Compressed = the @@ -2209,16 +2287,22 @@ mod tests { 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, 0x0E01_1000); assert_eq!( - NodeGuid::CLASSID_CPIC_V3 >> 16, + classid_custom(NodeGuid::CLASSID_CPIC_V3), 0x1000, - "gen-marker high u16" + "gen-marker in the custom (low) half" + ); + assert_eq!( + classid_canon(NodeGuid::CLASSID_CPIC_V3), + 0x0E01, + "canon == Genetics domain 0x0E, appid 0x01 (q2 — normalized from \ + the pre-flip domain-root :00 per the ruling)" ); assert_eq!( - NodeGuid::CLASSID_CPIC_V3 as u16, - 0x0E00, - "low u16 == Genetics domain root (0x0E00)" + classid_read_mode(NodeGuid::CLASSID_CPIC_V3_LEGACY), + ReadMode::CPIC_V3, + "pre-flip form (canon 0x0E00) resolves via the legacy alias" ); // The three V3 classes are mutually distinct, all V3 + layout-preserving. @@ -2259,10 +2343,11 @@ mod tests { 0xF012, // identity (instance) ); - // (3) The high-u16 generation marker round-trips in the stored classid… + // (3) The generation marker (custom/low half since P1) round-trips in + // the stored classid… assert_eq!(node.classid(), NodeGuid::CLASSID_OSINT_V3); assert_eq!( - node.classid() >> 16, + crate::ogar_codebook::classid_custom(node.classid()), 0x1000, "gen-marker preserved in the key" ); @@ -2270,12 +2355,13 @@ mod tests { 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; + // - the v1 fold REFUSES this address (both classid halves nonzero — + // a marked classid under every order) → the latent EMPTY fold + // Codex flagged on #613; assert_eq!( NiblePath::from_guid_prefix(&node), None, - "v1 fold still refuses the high-u16 marker" + "v1 fold still refuses the marked classid" ); // - the v3 fold ROUTES it: HEEL·HIP·TWIG·LEAF in full (both bytes per // 8:8 tile), depth 16, classid NOT folded → never EMPTY. @@ -2370,16 +2456,17 @@ mod tests { // (5) EntityType tenant ↔ SoA class column: the u16 the slab carries is the // same discriminator `MailboxSoaView::class_id()` (alias of - // `entity_type()`) exposes per row. Stamp the canon low-u16 concept and - // read it back — on the V3 class it is still the Osint concept 0x0700 - // (the high-u16 gen-marker never leaks into the entity discriminator). + // `entity_type()`) exposes per row. Stamp the CANON half and read it + // back — on the V3 class it is the OSINT:q2 canon 0x0701 (appid + // normalized per the ruling; the gen-marker in the custom half + // never leaks into the entity discriminator). let o = ValueTenant::EntityType.value_offset(); row.value[o..o + 2].copy_from_slice( &crate::ogar_codebook::classid_canon(NodeGuid::CLASSID_OSINT_V3).to_le_bytes(), ); let et = u16::from_le_bytes([row.value[o], row.value[o + 1]]); assert_eq!( - et, 0x0700, + et, 0x0701, "EntityType tenant carries the canon Osint concept" ); @@ -2482,9 +2569,9 @@ mod tests { "edge block untouched by value-tenant writes" ); - // The EntityType discriminator carries the canon lo-u16 concept on the - // cold classes too — Anatomy 0x0A01 / Genetics root 0x0E00, never the - // 0x1000 gen-marker. + // The EntityType discriminator carries the CANON half on the cold + // classes too — Anatomy 0x0A01 / Genetics:q2 0x0E01 (appid normalized + // per the ruling), never the 0x1000 gen-marker. let o = ValueTenant::EntityType.value_offset(); row.value[o..o + 2].copy_from_slice( &crate::ogar_codebook::classid_canon(NodeGuid::CLASSID_FMA_V3).to_le_bytes(), @@ -2499,7 +2586,7 @@ mod tests { ); assert_eq!( u16::from_le_bytes([row.value[o], row.value[o + 1]]), - 0x0E00, + 0x0E01, "EntityType tenant carries the canon Genetics root" ); } diff --git a/crates/lance-graph-contract/src/hhtl.rs b/crates/lance-graph-contract/src/hhtl.rs index 6cacfaed..11dacc9d 100644 --- a/crates/lance-graph-contract/src/hhtl.rs +++ b/crates/lance-graph-contract/src/hhtl.rs @@ -272,45 +272,63 @@ impl NiblePath { /// `classid · HEEL · HIP · TWIG` cascade (identity-architecture v1 §3). /// /// The 20-nibble prefix `classid(8) | HEEL(4) | HIP(4) | TWIG(4)` overflows - /// `MAX_DEPTH = 16`. The deterministic fold drops the **HIGH 4 classid + /// `MAX_DEPTH = 16`. The deterministic fold drops the **CUSTOM 4 classid /// nibbles** and packs the remaining 16 nibbles root-first as - /// `classid_lo(4) | HEEL(4) | HIP(4) | TWIG(4)`. Returns `None` when the HIGH - /// 4 classid nibbles are nonzero — **this v1 fold** uses `classid_lo` as the - /// coarse tier, so it needs the high `u16` clear; a nonzero high `u16` is - /// reported, not silently re-routed. This is a **v1-fold constraint, NOT a - /// global classid law**: the v3 fold [`from_guid_prefix_v3`] reads the + /// `canon(4) | HEEL(4) | HIP(4) | TWIG(4)`. Returns `None` when the CUSTOM + /// half is nonzero — **this v1 fold** uses the canon half as the coarse + /// tier, so it needs the custom half clear; a marked classid is reported, + /// not silently re-routed. This is a **v1-fold constraint, NOT a global + /// classid law**: the v3 fold [`from_guid_prefix_v3`] reads the /// `(part_of:is_a)` `HEEL·HIP·TWIG·LEAF` tiers and does NOT fold `classid`, so - /// a V3 classid carries its high-`u16` generation marker freely (the schema's - /// `tail_variant` selects the fold — there is no global reserved-zero after V3). + /// a V3 classid carries its generation marker freely (the schema's + /// `tail_variant` selects the fold — there is no global custom-zero law after V3). /// - /// **Bijection invariant.** For any GUID whose `classid >> 16 == 0`, + /// **Mint-forward boundary (flip P1):** a pure-canon classid is accepted in + /// EITHER stored form — the active canon-HIGH form (`0xDDCC_0000`) or the + /// persisted pre-flip canon-LOW form (`0x0000_DDCC`) — and both fold to the + /// SAME path (same canon ⇒ same basin/coarse tier), so persisted v1 rows + /// keep routing across the flip. Only genuinely marked classids (both + /// halves nonzero under every order, e.g. the V3 `0x0701_1000` / + /// `0x1000_0700` forms) refuse the fold. + /// + /// **Bijection invariant.** For any accepted GUID, /// `from_guid_prefix(guid).prefix(d).is_ancestor_of(from_guid_prefix(guid))` /// holds for every `d in 1..=16` (`prefix(0)` is [`EMPTY`](NiblePath::EMPTY), /// which by definition is an ancestor of nothing — the "no basin routed" - /// sentinel). The routing-cache view (typically `prefix(4)` over - /// `classid_lo`) is therefore a valid HHTL ancestor of the full class path — + /// sentinel). The routing-cache view (typically `prefix(4)` over the canon + /// half) is therefore a valid HHTL ancestor of the full class path — /// the LE contract the `classid → ReadMode` keystone meets at the classid. #[must_use] pub const fn from_guid_prefix(guid: &crate::canonical_node::NodeGuid) -> Option { let parts = guid.decode(); // In THIS v1 fold the CUSTOM half must be zero — it folds the CANON // half as the coarse tier, so a nonzero custom half would make the - // 20→16 nibble fold lossy. It is reported, not silently re-routed. (The - // v3 fold does NOT fold classid — see from_guid_prefix_v3 — so this is a - // v1-fold constraint, not a global reserved-zero law.) Halves come from - // the one flippable split (D-CCF-0) — identical to the historical - // `classid >> 16` / `& 0xFFFF` masks while CLASSID_ORDER is CanonLow. + // 20→16 nibble fold lossy. Halves come from the one flippable split + // (D-CCF-0). Mint-forward boundary: if the ACTIVE order reports a + // nonzero custom, try the OTHER order — a persisted pre-flip id + // (canon LOW) reads clean there and folds to the identical path. A + // new-form id never needs the fallback (its custom half IS zero), so + // the two reads cannot disagree about an accepted id's canon. let (canon, custom) = crate::ogar_codebook::split_classid(parts.classid); - if custom != 0 { - return None; - } + let canon = if custom == 0 { + canon + } else { + let (legacy_canon, legacy_custom) = crate::ogar_codebook::split_classid_with( + crate::ogar_codebook::ClassidOrder::CanonLow, + parts.classid, + ); + if legacy_custom != 0 { + return None; + } + legacy_canon + }; // Pack root-first into 16 nibbles = 64 bits = the full u64 path: // nibbles 0..4 (high) = canon half (basin = top nibble of canon) // nibbles 4..8 = HEEL // nibbles 8..12 = HIP // nibbles 12..16 (low) = TWIG (leaf = low nibble of TWIG) - let classid_lo = canon as u64; - let path = (classid_lo << 48) + let classid_canon = canon as u64; + let path = (classid_canon << 48) | ((parts.heel as u64) << 32) | ((parts.hip as u64) << 16) | (parts.twig as u64); @@ -356,11 +374,13 @@ impl NiblePath { /// not dropped, exactly as v1/v2 keep their tail out of the `u64` path (which /// holds only 8 bytes; the full 12-byte cascade does not fit one `NiblePath`). /// - /// **`classid` is NOT folded in** (unlike v1's `classid_lo·HEEL·HIP·TWIG`), so - /// a V3 classid's high-`u16` generation marker (e.g. OSINT-V3 `0x1000_0700`) - /// is irrelevant to routing and never collapses to [`EMPTY`](NiblePath::EMPTY). - /// This is why "high `u16` is reserved-zero" is a **v1-fold** statement, NOT a - /// global classid law — the schema's `tail_variant` selects the fold. + /// **`classid` is NOT folded in** (unlike v1's `canon·HEEL·HIP·TWIG`), so + /// a V3 classid's generation marker (e.g. OSINT-V3 `0x0701_1000`, custom + /// `0x1000` in the LOW half since the P1 flip; pre-flip stored form + /// `0x1000_0700`) is irrelevant to routing and never collapses to + /// [`EMPTY`](NiblePath::EMPTY). This is why "custom half is reserved-zero" + /// is a **v1-fold** statement, NOT a global classid law — the schema's + /// `tail_variant` selects the fold. #[cfg(feature = "guid-v3-tail")] #[must_use] pub const fn from_guid_prefix_v3(guid: &crate::canonical_node::NodeGuid) -> Self { @@ -842,42 +862,49 @@ mod tests { #[test] fn from_guid_prefix_returns_full_max_depth_path() { use crate::canonical_node::NodeGuid; - // A canonical GUID with classid in the low u16 round-trips to a - // 16-nibble path with the documented root-first layout. - let g = NodeGuid::new(0x0000_ABCD, 0x1234, 0x5678, 0x9ABC, 0x00_0001, 0x00_0002); - let path = NiblePath::from_guid_prefix(&g).expect("classid_lo only ⇒ Some"); + // A pure-canon GUID folds to a 16-nibble path with the documented + // root-first layout — in BOTH stored forms (mint-forward boundary): + // the new canon-HIGH form and the persisted pre-flip canon-LOW form + // fold to the IDENTICAL path. + let legacy = NodeGuid::new(0x0000_ABCD, 0x1234, 0x5678, 0x9ABC, 0x00_0001, 0x00_0002); + let new_form = NodeGuid::new(0xABCD_0000, 0x1234, 0x5678, 0x9ABC, 0x00_0001, 0x00_0002); + let path = NiblePath::from_guid_prefix(&legacy).expect("pure canon (legacy form) ⇒ Some"); + assert_eq!( + NiblePath::from_guid_prefix(&new_form), + Some(path), + "both stored forms of the same canon fold to the same path" + ); assert_eq!(path.depth(), MAX_DEPTH, "fold occupies the full u64"); - // Root-first: top nibble of classid_lo is the basin (0xA from 0xABCD). + // Root-first: top nibble of the canon half is the basin (0xA from 0xABCD). assert_eq!(path.basin(), Some(0xA)); // Leaf: low nibble of TWIG (0xC from 0x9ABC). assert_eq!(path.leaf(), Some(0xC)); - // Packed value mirrors classid_lo|HEEL|HIP|TWIG, root-first. + // Packed value mirrors canon|HEEL|HIP|TWIG, root-first. let expected: u64 = (0xABCDu64 << 48) | (0x1234u64 << 32) | (0x5678u64 << 16) | 0x9ABCu64; assert_eq!(path.packed(), (expected, MAX_DEPTH)); } #[test] - fn from_guid_prefix_returns_none_when_high_classid_nibbles_in_use() { + fn from_guid_prefix_returns_none_when_classid_is_marked() { use crate::canonical_node::NodeGuid; - // The 20→16 fold drops the HIGH 4 classid nibbles. When the high u16 - // is nonzero, the fold is lossy — None signals it, callers don't get - // a silent collision. + // The 20→16 fold drops the CUSTOM 4 classid nibbles. When BOTH halves + // are nonzero (a genuinely marked classid — no order reads it as pure + // canon), the fold is lossy — None signals it, callers don't get a + // silent collision. let g = NodeGuid::new(0xDEAD_BEEF, 0, 0, 0, 0, 0); assert_eq!( NiblePath::from_guid_prefix(&g), None, - "high classid u16 != 0 ⇒ refuse the lossy fold" + "both classid halves nonzero ⇒ refuse the lossy fold" ); - let g = NodeGuid::new(0x0001_0000, 0, 0, 0, 0, 0); - assert_eq!( - NiblePath::from_guid_prefix(&g), - None, - "boundary: bit 16 set" - ); - // At exactly the boundary (high u16 == 0) the fold is lossless. - let g = NodeGuid::new(0x0000_FFFF, 0, 0, 0, 0, 0); + let g = NodeGuid::new(0x0001_0002, 0, 0, 0, 0, 0); + assert_eq!(NiblePath::from_guid_prefix(&g), None, "minimal marked id"); + // Pure canon in either half is lossless — accepted in both forms. + let g = NodeGuid::new(0x0001_0000, 0, 0, 0, 0, 0); // new-form canon 0x0001 + assert!(NiblePath::from_guid_prefix(&g).is_some()); + let g = NodeGuid::new(0x0000_FFFF, 0, 0, 0, 0, 0); // legacy-form canon 0xFFFF assert!(NiblePath::from_guid_prefix(&g).is_some()); } @@ -885,8 +912,9 @@ mod tests { #[test] fn from_guid_prefix_v3_routes_both_bytes_of_part_of_is_a_and_ignores_classid() { use crate::canonical_node::NodeGuid; - // OSINT-V3: classid high u16 = 0x1000 (the generation marker), so the v1 - // fold REFUSES this GUID — the latent EMPTY-fold Codex flagged. + // OSINT-V3 (`0x0701_1000`): both classid halves nonzero (canon 0x0701 + // HIGH, marker 0x1000 LOW since the P1 flip), so the v1 fold REFUSES + // this GUID — the latent EMPTY-fold Codex flagged. let g = NodeGuid::new( NodeGuid::CLASSID_OSINT_V3, 0xAB12, diff --git a/crates/lance-graph-contract/src/ogar_codebook.rs b/crates/lance-graph-contract/src/ogar_codebook.rs index 8b5f13dc..eddf87b4 100644 --- a/crates/lance-graph-contract/src/ogar_codebook.rs +++ b/crates/lance-graph-contract/src/ogar_codebook.rs @@ -108,16 +108,21 @@ pub fn canonical_concept_domain(id: u16) -> ConceptDomain { /// Resolve a [`NodeGuid`](crate::NodeGuid) `classid` to its [`ConceptDomain`] (D-OVC-4). The /// codebook id is the CANON half of the classid (under the active -/// [`CLASSID_ORDER`] — the low u16 while the order is `CanonLow`); the other -/// half is the custom/render prefix. So a domain route is +/// [`CLASSID_ORDER`] — the HIGH u16 since the P1 flip); the other half is the +/// custom/render prefix. So a domain route is /// `canonical_concept_domain(classid_canon(classid))`. This is the coarse sibling of the /// per-family scope in [`codebook`](crate::codebook): classid (domain) selects the /// coarse codebook; `family` selects the sub-codebook (longest-prefix-wins). +/// +/// **Legacy boundary:** a persisted pre-flip id (canon in the LOW half, e.g. +/// `0x0000_0700` / `0x1000_0700`) does NOT domain-route through this function +/// — it resolves via its concrete legacy-alias key in `BUILTIN_READ_MODES` +/// (mint-forward, plan §4 P3). New mints always compose via +/// [`compose_classid`] and route correctly here. #[inline] #[must_use] pub fn classid_concept_domain(classid: u32) -> ConceptDomain { - // Routes the CANON half via the one flippable split (D-CCF-0) — identical - // to the historical `classid as u16` while CLASSID_ORDER is CanonLow. + // Routes the CANON half via the one flippable split (D-CCF-0). canonical_concept_domain(classid_canon(classid)) } @@ -135,23 +140,25 @@ pub fn source_domain_concept(source_domain: &str) -> Option { } } -// ── APP / render-prefix layer (the hi u16) — wire-compat mirror of OGAR `ogar_vocab::app` ── +// ── APP / render-prefix layer (the CUSTOM half) — wire-compat mirror of OGAR `ogar_vocab::app` ── -/// The **APP / render prefix** — the high u16 of a full 32-bit `classid`. +/// The **APP / render prefix** — the CUSTOM half of a full 32-bit `classid` +/// (the LOW u16 since the P1 half-order flip). /// /// A full render classid is two orthogonal halves: /// /// ```text -/// classid : u32 = [ hi u16 : APP / render prefix ] [ lo u16 : concept ] -/// 0xAAAA (per-app ClassView lens) 0xDDCC (shared RBAC+ontology) +/// classid : u32 = [ hi u16 : CANON concept ] [ lo u16 : APP / render prefix ] +/// 0xDDCC (shared RBAC+ontology) 0xAAAA (per-app ClassView lens) /// ``` /// /// `0x0000` ([`AppPrefix::Core`]) is the shared canonical core — every -/// [`canonical_concept_id`] is `0x0000_DDCC`, additive and invariant. A +/// [`canonical_concept_id`] renders under the core lens as `0xDDCC_0000`, +/// additive and invariant. A /// non-zero prefix selects an app's render lens (its per-app `ClassView` / -/// template set) while the lo-u16 concept — the RBAC + ontology + cross-app -/// identity key — stays shared; concept/domain routing reads only the low half -/// ([`classid_concept_domain`] does `… as u16`), so it is identical under every +/// template set) while the CANON concept — the RBAC + ontology + cross-app +/// identity key — stays shared; concept/domain routing reads only the canon half +/// ([`classid_concept_domain`] routes `classid_canon(..)`), so it is identical under every /// render prefix. Mirrors OGAR `PortSpec::APP_PREFIX` (the /// `APP-CLASS-CODEBOOK-LAYOUT.md` §2 allocation table as typed data); /// wire-compatible, **no `ogar-vocab` dependency**. This is the membrane @@ -180,9 +187,11 @@ pub enum AppPrefix { } impl AppPrefix { - /// The reserved high-u16 prefix from the §2 allocation table. `const` so it - /// composes in `const` contexts. MUST match OGAR `PortSpec::APP_PREFIX` - /// (pinned by [`tests::app_prefixes_match_ogar_allocation_table`]). + /// The reserved app-prefix value from the §2 allocation table (the CUSTOM + /// half — the LOW u16 since the P1 flip; the VALUE is order-invariant). + /// `const` so it composes in `const` contexts. MUST match OGAR + /// `PortSpec::APP_PREFIX` (pinned by + /// [`tests::app_prefixes_match_ogar_allocation_table`]). #[inline] #[must_use] pub const fn prefix(self) -> u16 { @@ -197,7 +206,7 @@ impl AppPrefix { } } - /// Resolve a high-u16 prefix value back to its [`AppPrefix`]. `None` for an + /// Resolve an app-prefix value back to its [`AppPrefix`]. `None` for an /// unallocated value (`0x0006`, `0x0008`+ — reserved, costs nothing until /// an app mints its first private class). #[inline] @@ -216,7 +225,8 @@ impl AppPrefix { } /// Compose the full render `classid` for this app and a canonical concept - /// id: `(prefix << 16) | concept`. The membrane equivalent of OGAR + /// id: `compose_classid(concept, prefix)` — concept in the CANON (high) + /// half, prefix in the CUSTOM (low) half. The membrane equivalent of OGAR /// `render_classid_for::

(concept)`, reading the prefix from typed data /// rather than a `PortSpec` generic. #[inline] @@ -226,21 +236,23 @@ impl AppPrefix { } } -/// Compose a full render `classid` from an app `prefix` (high u16) and a -/// canonical `concept` id (low u16): `(prefix << 16) | concept`. Wire-compat -/// mirror of OGAR `ogar_vocab::app::render_classid`. +/// Compose a full render `classid` from an app `prefix` (CUSTOM half, low u16) +/// and a canonical `concept` id (CANON half, high u16): +/// `compose_classid(concept, prefix)`. Wire-compat mirror of OGAR +/// `ogar_vocab::app::render_classid` (which flips in lockstep). /// -/// `render_classid(0x0005, 0x0901)` → `0x0005_0901` (MedCare's `patient`); the -/// core form `render_classid(0x0000, id)` equals `id` widened to `u32` -/// (additive — a bare concept IS a render classid under the core lens). +/// `render_classid(0x0005, 0x0901)` → `0x0901_0005` (MedCare's `patient`); the +/// core form `render_classid(0x0000, id)` is `(id as u32) << 16` — the bare +/// concept in the canon half under the core lens (pre-flip it equaled `id` +/// widened; the flip moved the concept to the high half). #[inline] #[must_use] pub const fn render_classid(prefix: u16, concept: u16) -> u32 { // The prefix is the CUSTOM half, the concept the CANON half — composed - // through the one flippable definition (D-CCF-0): identical to the - // historical `(prefix << 16) | concept` while CLASSID_ORDER is CanonLow. - // The OGAR#95 hi-u16 scheme ↔ CanonHigh reconciliation is the plan's P2 - // operator checkpoint; this route-through is what makes it one-place. + // through the one flippable definition (D-CCF-0). This route-through is + // what reconciled the OGAR#95 hi-u16 app-prefix scheme with the ruling's + // canon-high order in one place: the #95 prefix table became the + // CUSTOM-half render catalogue (plan §4 P2). compose_classid(concept, prefix) } @@ -253,7 +265,8 @@ pub const fn render_classid(prefix: u16, concept: u16) -> u32 { /// ``` /// use lance_graph_contract::{render_classid_for_concept, AppPrefix}; /// // MedCare patient under the Healthcare render lens — the canonical example. -/// assert_eq!(render_classid_for_concept(AppPrefix::Healthcare, "patient"), Some(0x0005_0901)); +/// // Concept 0x0901 in the CANON (high) half, prefix 0x0005 in the CUSTOM (low). +/// assert_eq!(render_classid_for_concept(AppPrefix::Healthcare, "patient"), Some(0x0901_0005)); /// assert_eq!(render_classid_for_concept(AppPrefix::Healthcare, "not_a_concept"), None); /// ``` #[inline] @@ -283,13 +296,15 @@ pub enum ClassidOrder { CanonHigh, } -/// The active half-order. **P0 pins the legacy order** — every route-through -/// below is behavior-identical to the direct masks it replaces (probed). -/// Flipping this const to [`CanonHigh`](ClassidOrder::CanonHigh) IS Phase 1 of -/// the migration; it is mint-forward with a version boundary — flipping alone -/// must never reinterpret persisted ids (the registry keeps concrete-keyed -/// legacy aliases; plan §4 P3, codex P2 on #627). -pub const CLASSID_ORDER: ClassidOrder = ClassidOrder::CanonLow; +/// The active half-order. **P1 flipped this to the target order** +/// (operator trigger 2026-07-02; P0 landed the route-throughs behavior- +/// identically under `CanonLow` first, probed): stored classids now carry the +/// CANON half HIGH — `0x0701_1000`, human-readable `0x07:01::1000`. The flip +/// is mint-forward with a version boundary — it never reinterprets persisted +/// ids: `BUILTIN_READ_MODES` keeps concrete-keyed legacy aliases +/// (`0x1000_0700`-form / `0x0000_0700`-form) resolving forever until a corpus +/// proof shows zero stored old-form rows remain (plan §4 P3, codex P2 on #627). +pub const CLASSID_ORDER: ClassidOrder = ClassidOrder::CanonHigh; /// Compose a classid under an explicit half-order. #[inline] @@ -356,9 +371,9 @@ pub const fn flip_classid(classid: u32) -> u32 { compose_classid_with(other, canon, custom) } -/// The APP / render-prefix half of a full `classid` — since #627, the CUSTOM -/// half under the active [`CLASSID_ORDER`] (identical to the historical -/// `classid >> 16` while the order is [`CanonLow`](ClassidOrder::CanonLow)). +/// The APP / render-prefix half of a full `classid` — the CUSTOM half under +/// the active [`CLASSID_ORDER`] (the LOW u16 since the P1 flip; historically +/// `classid >> 16` under [`CanonLow`](ClassidOrder::CanonLow)). /// Mirror of OGAR `ogar_vocab::app::app_of`. Pair with /// [`AppPrefix::from_prefix`] to recover the typed app. #[inline] @@ -367,9 +382,9 @@ pub const fn classid_app_prefix(classid: u32) -> u16 { classid_custom(classid) } -/// The canonical concept-id half of a full `classid` — since #627, the CANON -/// half under the active [`CLASSID_ORDER`] (identical to the historical -/// `classid as u16` while the order is [`CanonLow`](ClassidOrder::CanonLow)) — +/// The canonical concept-id half of a full `classid` — the CANON half under +/// the active [`CLASSID_ORDER`] (the HIGH u16 since the P1 flip; historically +/// `classid as u16` under [`CanonLow`](ClassidOrder::CanonLow)) — /// the shared RBAC + ontology + cross-app identity key, identical under every /// render prefix. Mirror of OGAR `ogar_vocab::app::concept_of`; the sibling of /// [`classid_concept_domain`], which routes this half to its [`ConceptDomain`]. @@ -558,9 +573,10 @@ mod tests { } #[test] - fn classid_routes_through_low_u16() { - // The contract classids resolve to the domain their `0xDDCC` low half - // encodes — the contract↔OGAR alignment (ISS-CLASSID-OGAR-DRIFT). + fn classid_routes_through_canon_half() { + // The contract classids resolve to the domain their CANON half (the + // HIGH u16 since the P1 flip) encodes — the contract↔OGAR alignment + // (ISS-CLASSID-OGAR-DRIFT). assert_eq!( classid_concept_domain(NodeGuid::CLASSID_PROJECT), ConceptDomain::ProjectMgmt @@ -682,13 +698,14 @@ mod tests { #[test] fn render_classid_composes_decomposes_and_preserves_the_concept_half() { - // Worked examples mirrored from OGAR `ogar_vocab::app` tests. - assert_eq!(render_classid(0x0001, 0x0102), 0x0001_0102); - assert_eq!(render_classid(0x0007, 0x0102), 0x0007_0102); // Redmine twin + // Worked examples mirrored from OGAR `ogar_vocab::app` tests — the + // P1 canon-high forms (concept HIGH, prefix LOW). + assert_eq!(render_classid(0x0001, 0x0102), 0x0102_0001); + assert_eq!(render_classid(0x0007, 0x0102), 0x0102_0007); // Redmine twin - // MedCare patient — the canonical worked example: 0x0005_0901. + // MedCare patient — the canonical worked example: 0x0901_0005. let pat = render_classid_for_concept(AppPrefix::Healthcare, "patient").unwrap(); - assert_eq!(pat, 0x0005_0901); + assert_eq!(pat, 0x0901_0005); assert_eq!(classid_app_prefix(pat), 0x0005); assert_eq!(classid_concept(pat), 0x0901); assert_eq!( @@ -701,12 +718,12 @@ mod tests { ConceptDomain::Health ); - // Core (hi=0x0000) is additive: a bare concept IS a render classid. + // Core (prefix=0x0000): the bare concept sits in the CANON (high) half. let core = render_classid(0x0000, 0x0102); - assert_eq!(core, u32::from(0x0102u16)); + assert_eq!(core, (0x0102u32) << 16); assert_eq!(classid_concept(core), 0x0102); - // The render lens never perturbs the lo-u16 concept RBAC keys on. + // The render lens never perturbs the CANON concept RBAC keys on. let op = AppPrefix::OpenProject.render(0x0103); let rm = AppPrefix::Redmine.render(0x0103); assert_ne!( @@ -753,10 +770,11 @@ mod tests { } #[test] - fn classid_flip_is_involutive_and_p0_pins_legacy_order() { - // P0 pin: the active order is the legacy CanonLow — flipping this - // const IS the migration's Phase 1, never a drive-by. - assert_eq!(CLASSID_ORDER, ClassidOrder::CanonLow); + fn classid_flip_is_involutive_and_p1_pins_target_order() { + // P1 pin: the active order is the target CanonHigh (operator trigger + // 2026-07-02). Un-flipping this const is a migration reversal, never + // a drive-by. + assert_eq!(CLASSID_ORDER, ClassidOrder::CanonHigh); // flip(flip(x)) == x over every wired classid + the post-flip trio. for id in [ 0x0000_0700u32, // legacy OSINT domain class @@ -779,23 +797,37 @@ mod tests { } #[test] - fn classid_route_through_is_behavior_identical_under_legacy_order() { - // The legacy-boundary matrix (plan §3): under CanonLow every routed - // reader equals the direct mask it replaced, for every codebook id - // under every app prefix — the P0 zero-behavior gate. + fn classid_route_through_matrix_under_active_and_legacy_order() { + // The boundary matrix (plan §3), post-flip form: under the active + // CanonHigh order every routed reader equals the canon-high masks, + // for every codebook id under every app prefix — and the LEGACY + // (CanonLow) composition stays available under the explicit-order + // API for reading persisted pre-flip ids. for &(_, concept) in CODEBOOK { for prefix in [0x0000u16, 0x0001, 0x0005, 0x1000] { + // Active order: canon (concept) HIGH, custom (prefix) LOW. let id = render_classid(prefix, concept); - assert_eq!(id, ((prefix as u32) << 16) | (concept as u32)); + assert_eq!(id, ((concept as u32) << 16) | (prefix as u32)); assert_eq!(classid_concept(id), concept); assert_eq!(classid_app_prefix(id), prefix); - assert_eq!(classid_canon(id), id as u16); - assert_eq!(classid_custom(id), (id >> 16) as u16); + assert_eq!(classid_canon(id), (id >> 16) as u16); + assert_eq!(classid_custom(id), id as u16); assert_eq!( classid_concept_domain(id), canonical_concept_domain(concept), "domain routing invariant under the route-through" ); + + // Legacy boundary: the explicit CanonLow split still reads a + // persisted pre-flip id exactly as the direct masks did. + let legacy = compose_classid_with(ClassidOrder::CanonLow, concept, prefix); + assert_eq!(legacy, ((prefix as u32) << 16) | (concept as u32)); + assert_eq!( + split_classid_with(ClassidOrder::CanonLow, legacy), + (concept, prefix) + ); + // And the flip carries a legacy id to its new-form twin. + assert_eq!(flip_classid(legacy), id); } } } From 5961623712a0cea8b3a3269f6d304ff0c95c65a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 05:41:37 +0000 Subject: [PATCH 4/7] =?UTF-8?q?contract+rbac+ogar:=20mint-forward=20compat?= =?UTF-8?q?=20reader=20=E2=80=94=20the=20P0=20sweep=20missed=20rbac.rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up on the CanonHigh flip: three sites derived classid halves outside the one flippable composition and surfaced only when the flip landed — - contract rbac.rs ClassGrant::permits used 'class as u16' (the codex-P2 collapse pattern). Now routes classid_canon_compat. - lance-graph-rbac AuthProvider::classid() hand-widened u32::from(concept). Now routes contract render_classid(0x0000, concept) -> 0x0B01_0000-form. - lance-graph-ogar AUTH_*_CID / test ENCOUNTER literals in the pre-flip 0x0000_DDCC form. Now const-composed via contract render_classid. New: ogar_codebook::classid_canon_compat — the mint-forward CANON reader for surfaces serving BOTH stored forms (RBAC grants, un-re-baked corpora): active canon when plausible (>= 0x0100 && != 0x1000), legacy-order fallback otherwise; the canon slot exactly 0x1000 (domain-0x10 root) stays reserved-unusable until marker retirement (P4). RBAC authorizes pre-flip persisted rows without re-bake; both-forms grant test added. Board: EPIPHANIES E-CLASSID-COMPAT-READER (same commit). Gates: contract 774 (guid-v3-tail), rbac 30, ogar 81; clippy; fmt. Co-Authored-By: Claude --- .claude/board/EPIPHANIES.md | 26 ++++++++++ .../lance-graph-contract/src/ogar_codebook.rs | 42 +++++++++++++++ crates/lance-graph-contract/src/rbac.rs | 51 ++++++++++++------- crates/lance-graph-ogar/src/actions.rs | 23 +++++---- crates/lance-graph-ogar/src/rbac_impl.rs | 4 +- crates/lance-graph-rbac/src/auth.rs | 19 ++++--- 6 files changed, 127 insertions(+), 38 deletions(-) diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 848e1223..64268eae 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,29 @@ +## 2026-07-02 — E-CLASSID-COMPAT-READER — the P0 sweep missed rbac.rs: `class as u16` in ClassGrant::permits; fixed via a mint-forward compat reader + +**Status:** SHIPPED (follow-up on the P1 flip commit, PR #628 arc). + +**Correction:** the D-CCF-0 route-through sweep covered ogar_codebook / +hhtl / canonical_node but NOT `rbac.rs` — `ClassGrant::permits` derived its +class discriminator via `class as u16` (the exact codex-P2 collapse +pattern), and `lance-graph-rbac AuthProvider::classid()` hand-widened +`u32::from(class_id)`, and `lance-graph-ogar` carried `0x0000_0B01`-form +literals. All three surfaced only when the flip landed. + +**Mechanism added:** `ogar_codebook::classid_canon_compat` — the +mint-forward CANON reader for surfaces that must serve BOTH stored forms +(RBAC grant matching, reads over un-re-baked corpora): active canon when +plausible (`>= 0x0100 && != 0x1000`), legacy-order fallback otherwise. +Sound because real render prefixes are §2-allocated (`0x0000..0x0007`) and +the marker is `0x1000`; documented limitation — the future canon slot +exactly `0x1000` (domain-0x10 root) stays reserved-unusable until marker +retirement (P4). RBAC now authorizes pre-flip persisted rows without +re-bake (fails OPEN-to-correct-concept, never collapses classes). + +**Lesson (Rule-7 adjacent):** a route-through sweep's own coverage claim +needs the same exhaustive-grep declaration as a negative-existence claim — +"all sites routed" was asserted from the plan's §2 inventory, not from a +whole-crate `as u16`/`u32::from` sweep. + ## 2026-07-02 — E-CLASSID-FLIP-P1-LANDED — CanonHigh is live: canon HIGH / custom LOW, legacy aliases resolve persisted rows, OGAR#95 reconciled as the custom-half render catalogue **Status:** SHIPPED (PR #628 arc; P0 route-through fd9bf6b → P1 flip this commit). diff --git a/crates/lance-graph-contract/src/ogar_codebook.rs b/crates/lance-graph-contract/src/ogar_codebook.rs index eddf87b4..c0804a4b 100644 --- a/crates/lance-graph-contract/src/ogar_codebook.rs +++ b/crates/lance-graph-contract/src/ogar_codebook.rs @@ -358,6 +358,34 @@ pub const fn classid_custom(classid: u32) -> u16 { split_classid(classid).1 } +/// **Mint-forward CANON reader** for surfaces that must serve BOTH stored +/// forms — RBAC grant matching, read paths over corpora not yet re-baked to +/// the post-flip order. Strict new-form-only surfaces use [`classid_canon`]. +/// +/// Returns the canon half under the active order when it is a *plausible* +/// canon — a `0xDDCC` codebook id has domain byte `>= 0x01`, and the canon +/// half never carries the `0x1000` V3 marker — otherwise re-reads the id +/// under the legacy [`CanonLow`](ClassidOrder::CanonLow) order (where every +/// pre-flip form keeps its canon in the LOW half: core `0x0000_0901`, render +/// `0x0005_0901`, V3 `0x1000_0700` all resolve their true canon). +/// +/// Documented limitation: a future canon exactly equal to `0x1000` (the +/// domain-root slot of the currently-Unassigned domain `0x10`) would be +/// indistinguishable from the V3 marker under this heuristic — that slot is +/// reserved-unusable until the marker retires (plan §4 P4). +#[inline] +#[must_use] +pub const fn classid_canon_compat(classid: u32) -> u16 { + let (canon, custom) = split_classid(classid); + if canon >= 0x0100 && canon != 0x1000 { + canon + } else if custom != 0 { + split_classid_with(ClassidOrder::CanonLow, classid).0 + } else { + canon + } +} + /// Recompose a classid under the OTHER order — the flip itself. Involutive: /// `flip_classid(flip_classid(x)) == x` (probed below). #[inline] @@ -832,6 +860,20 @@ mod tests { } } + #[test] + fn classid_canon_compat_reads_both_stored_forms() { + // New-form ids: compat == strict canon. + for id in [0x0901_0005u32, 0x0701_1000, 0x0102_0001, 0x0700_0000] { + assert_eq!(classid_canon_compat(id), classid_canon(id)); + } + // Persisted pre-flip forms resolve their true canon via the legacy + // fallback: core, render, and V3-marked shapes. + assert_eq!(classid_canon_compat(0x0000_0901), 0x0901); // legacy core + assert_eq!(classid_canon_compat(0x0005_0901), 0x0901); // legacy render + assert_eq!(classid_canon_compat(0x1000_0700), 0x0700); // legacy V3 + assert_eq!(classid_canon_compat(0x0000_0000), 0x0000); // default class + } + #[test] fn no_class_collapse_under_canon_high() { // codex P2 (#627): post-flip, a naive `as u16` reads the CUSTOM half — diff --git a/crates/lance-graph-contract/src/rbac.rs b/crates/lance-graph-contract/src/rbac.rs index fc31214b..62e1eaf1 100644 --- a/crates/lance-graph-contract/src/rbac.rs +++ b/crates/lance-graph-contract/src/rbac.rs @@ -96,7 +96,8 @@ impl ScopeSpec { } /// The codebook class identity an authorization targets — the -/// [`NodeGuid`](crate::NodeGuid) `classid` (or its low-`u16` codebook id widened). +/// [`NodeGuid`](crate::NodeGuid) `classid` (its canon half is the codebook id; +/// compose via [`render_classid`](crate::ogar_codebook::render_classid)). /// Opaque to the kernel: it is compared and looked up, never decoded (the kernel /// "never touches a token" — only resolved keys go inward). pub type ClassId = u32; @@ -244,12 +245,13 @@ impl OpMask { /// text` blob (keystone §6 / I-K0 registry axiom: "decisions key on `classid`, /// not on text"). A role's `granted` value-tenant is a `&[ClassGrant]`. /// -/// `target_classid` is the **low `u16` codebook id** (the shared-concept half of -/// a [`NodeGuid`](crate::NodeGuid)'s `classid`) — the RBAC + ontology identity, -/// app-render-skin-independent (the hi `u16` chooses render, never grants). +/// `target_classid` is the **CANON `u16` codebook id** (the shared-concept half +/// of a [`NodeGuid`](crate::NodeGuid)'s `classid` — the HIGH u16 since the +/// 2026-07-02 half-order flip) — the RBAC + ontology identity, +/// app-render-skin-independent (the custom half chooses render, never grants). #[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PartialOrd, Ord, Hash)] pub struct ClassGrant { - /// The class this grant targets (low-`u16` codebook id). + /// The class this grant targets (canon-`u16` codebook id). pub target_classid: u16, /// The verbs this grant permits on that class. pub op_mask: OpMask, @@ -266,14 +268,19 @@ impl ClassGrant { } } - /// Whether this grant permits `op` on `class`. Matches on the **low `u16`** - /// of `class` (the codebook id), so a grant authored against the shared - /// concept applies regardless of which app's render-skin (hi `u16`) the - /// `ClassId` carries. + /// Whether this grant permits `op` on `class`. Matches on the **CANON + /// half** of `class` (the codebook id) via the mint-forward compat reader + /// [`classid_canon_compat`](crate::ogar_codebook::classid_canon_compat) — + /// a grant authored against the shared concept applies regardless of + /// which app's render-skin (custom half) the `ClassId` carries, AND + /// regardless of whether the id is a post-flip (canon HIGH) or persisted + /// pre-flip (canon LOW) stored form. Never `class as u16` — post-flip + /// that reads the custom half and collapses every class (codex P2 #627). #[inline] #[must_use] pub fn permits(&self, class: ClassId, op: &Operation<'_>) -> bool { - self.target_classid == (class as u16) && self.op_mask.permits(op) + self.target_classid == crate::ogar_codebook::classid_canon_compat(class) + && self.op_mask.permits(op) } } @@ -344,21 +351,27 @@ mod tests { } #[test] - fn class_grant_matches_on_low_u16_codebook_id() { + fn class_grant_matches_on_canon_codebook_id() { let grant = ClassGrant::new(0x0901, OpMask::READ.union(OpMask::ACT)); - // Same concept, different app render-skin (hi u16) → still permitted: - // the grant keys on the shared-concept low u16, never the render half. - let app_a: ClassId = 0x0000_0901; - let app_b: ClassId = 0xAB12_0901; let read = Operation::Read { depth: PrefetchDepth::Identity, }; - assert!(grant.permits(app_a, &read)); - assert!(grant.permits(app_b, &read)); - // Wrong concept → denied even with the verb. + // Same concept, different app render-skin (custom half) → still + // permitted: the grant keys on the shared-concept CANON half, never + // the render half. Post-flip forms: concept HIGH, any prefix LOW. + assert!(grant.permits(0x0901_0000, &read)); // core lens + assert!(grant.permits(0x0901_0005, &read)); // Healthcare lens + assert!(grant.permits(0x0901_AB12, &read)); // arbitrary custom half + // Mint-forward: persisted PRE-flip forms (canon LOW, §2-allocated + // prefixes < 0x0100 or the 0x1000 V3 marker) still match through the + // compat reader — a re-bake is never required for authorization. + assert!(grant.permits(0x0000_0901, &read)); // legacy core + assert!(grant.permits(0x0005_0901, &read)); // legacy Healthcare render + // Wrong concept → denied even with the verb (both forms). + assert!(!grant.permits(0x0902_0000, &read)); assert!(!grant.permits(0x0000_0902, &read)); // Right concept, ungranted verb → denied. - assert!(!grant.permits(app_a, &Operation::Write { predicate: "due" })); + assert!(!grant.permits(0x0901_0000, &Operation::Write { predicate: "due" })); } /// A typed [`ClassRbac`] impl whose `grant_permits` body IS [`grants_permit`] diff --git a/crates/lance-graph-ogar/src/actions.rs b/crates/lance-graph-ogar/src/actions.rs index daef168c..2e5df87a 100644 --- a/crates/lance-graph-ogar/src/actions.rs +++ b/crates/lance-graph-ogar/src/actions.rs @@ -107,16 +107,19 @@ const AUTH_ZITADEL_ACTIONS: &[ActionDef] = &[ }, ]; -// The auth-family codebook ids (keystone §7 `0x0B` core domain). Written as -// literals — NOT `ogar_vocab::class_ids::AUTH_STORE` — so this DO-arm provider is -// strictly `lance_graph_contract`-dependent and does not couple to whichever -// `ogar-vocab` git ref this crate pins (the action manifest is a contract-shaped -// artifact, exactly as `contract::action::ClassActions` documents — "generated -// downstream; the Core provides the type"). They MUST equal -// `ogar_vocab::class_ids::{AUTH_STORE, AUTH_ZITADEL}`; the lib's `parity` guard is -// what binds the codebook itself. -const AUTH_STORE_CID: u32 = 0x0000_0B01; -const AUTH_ZITADEL_CID: u32 = 0x0000_0B02; +// The auth-family codebook ids (keystone §7 `0x0B` core domain). Concept +// slots are literals — NOT `ogar_vocab::class_ids::AUTH_STORE` — so this +// DO-arm provider is strictly `lance_graph_contract`-dependent and does not +// couple to whichever `ogar-vocab` git ref this crate pins (the action +// manifest is a contract-shaped artifact, exactly as +// `contract::action::ClassActions` documents — "generated downstream; the +// Core provides the type"). They MUST equal +// `ogar_vocab::class_ids::{AUTH_STORE, AUTH_ZITADEL}`; the lib's `parity` +// guard is what binds the codebook itself. The FULL-classid widening routes +// through the contract's one flippable composition (core render lens, +// prefix 0x0000) — canon HIGH since the 2026-07-02 half-order flip. +const AUTH_STORE_CID: u32 = contract::render_classid(0x0000, 0x0B01); +const AUTH_ZITADEL_CID: u32 = contract::render_classid(0x0000, 0x0B02); /// The registry: one [`ClassActions`] row per class with a DO surface. Seeded /// with the auth family (the worked hardcoded-RBAC example); other domains append diff --git a/crates/lance-graph-ogar/src/rbac_impl.rs b/crates/lance-graph-ogar/src/rbac_impl.rs index 4f5c5728..f8b8bcf3 100644 --- a/crates/lance-graph-ogar/src/rbac_impl.rs +++ b/crates/lance-graph-ogar/src/rbac_impl.rs @@ -86,7 +86,9 @@ mod tests { } } - const ENCOUNTER: ClassId = 0x0000_0901; + // Full classid via the contract's flippable core-render composition + // (canon 0x0901 HIGH since the 2026-07-02 flip). + const ENCOUNTER: ClassId = lance_graph_contract::render_classid(0x0000, 0x0901); fn fixture() -> OgarRbac { OgarRbac::new(FixtureGrants { diff --git a/crates/lance-graph-rbac/src/auth.rs b/crates/lance-graph-rbac/src/auth.rs index 0ef57ec2..899e27a3 100644 --- a/crates/lance-graph-rbac/src/auth.rs +++ b/crates/lance-graph-rbac/src/auth.rs @@ -84,13 +84,15 @@ impl AuthProvider { .find(|p| p.class_id() == id) } - /// As a full 32-bit `ClassId` (hi-`u16` core prefix `0x0000`, lo-`u16` - /// concept) — the form [`authorize`](crate::authorize::authorize) and the - /// `NodeGuid` classid take. Auth concepts are core (cross-app), so the - /// render prefix is `0x0000`. + /// As a full 32-bit `ClassId` under the core render lens (concept in the + /// CANON high `u16` since the 2026-07-02 half-order flip, prefix `0x0000` + /// in the custom low half) — the form + /// [`authorize`](crate::authorize::authorize) and the `NodeGuid` classid + /// take. Auth concepts are core (cross-app). Routed through the + /// contract's one flippable composition — never a local widening. #[must_use] pub fn classid(self) -> ClassId { - u32::from(self.class_id()) + lance_graph_contract::render_classid(0x0000, self.class_id()) } /// The claim-key grammar for this provider — which claim names carry the @@ -204,8 +206,9 @@ mod tests { assert_eq!(AuthProvider::Zitadel.class_id(), 0x0B02); assert_eq!(AuthProvider::Zanzibar.class_id(), 0x0B03); assert_eq!(AuthProvider::OryKeto.class_id(), 0x0B04); - // Full classid is core-prefixed (hi u16 = 0x0000 — auth is cross-app). - assert_eq!(AuthProvider::Store.classid(), 0x0000_0B01); + // Full classid under the core lens: concept in the CANON high u16 + // (post-flip form), custom prefix 0x0000 — auth is cross-app. + assert_eq!(AuthProvider::Store.classid(), 0x0B01_0000); // Round-trips. for p in [ AuthProvider::Store, @@ -244,7 +247,7 @@ mod tests { assert!(id.has_role("physician")); assert!(!id.has_role("admin")); assert_eq!(id.tenant.as_deref(), Some("clinic-7")); - assert_eq!(id.auth_classid(), 0x0000_0B02); + assert_eq!(id.auth_classid(), 0x0B02_0000); } #[test] From 75e0fe39098367f0d58b10ef9ad9d496101abca9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 05:53:00 +0000 Subject: [PATCH 5/7] =?UTF-8?q?board:=20fleet-flip=20status=20=E2=80=94=20?= =?UTF-8?q?D-CCF-2=20in=20OGAR=20#147,=20D-CCF-3=20in=20flight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- .claude/board/STATUS_BOARD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 47207d41..a429b049 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -6,8 +6,8 @@ Plan: `.claude/plans/classid-canon-custom-flip-v1.md`. Operator trigger 2026-07- |---|---|---|---|---| | D-CCF-0 | compose_classid/split_classid/CLASSID_CANON_HIGH + route all sites (zero behavior) | lance-graph-contract | Shipped (fd9bf6b) | plan §3/§4 P0 | | D-CCF-1 | Flip + mint new-form classids (0x0701_1000 / 0x0A01_1000 / 0x0E01_1000) coexisting | lance-graph-contract | In PR (#628) | gated on P0 probes | -| D-CCF-2 | OGAR#95 hi-u16 app-prefix reconciliation | contract + OGAR | In progress (resolved: prefix = custom half; OGAR flips in lockstep per operator) | plan §2 row / §4 P2 | -| D-CCF-3 | q2 re-mints (osint-bake + cpic via contract pull; dissolves ISS-Q2-CPIC-MIRROR) | q2 (gate WAIVED) | Queued | plan §4 P3 | +| D-CCF-2 | OGAR#95 hi-u16 app-prefix reconciliation | contract + OGAR | In PR (OGAR #147; prefix = custom half) | plan §2 row / §4 P2 | +| D-CCF-3 | q2 re-mints (osint-bake + cpic via contract pull; dissolves ISS-Q2-CPIC-MIRROR) | q2 (gate WAIVED) | In progress (code+re-bakes in flight; cpic interim canon-high local scheme, full contract pull tracked) | plan §4 P3 | | D-CCF-4 | 0x1000 marker retirement | all | Blocked (operator checkpoint) | plan §4 P4 | ## v3-convergence-wiring-v1 — wire, don't invent (the seam list) From 3bfc3ac242ba29e675d6b5fe37841466a934ac5f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 06:13:43 +0000 Subject: [PATCH 6/7] =?UTF-8?q?board:=20fleet-flip=20execution=20complete?= =?UTF-8?q?=20=E2=80=94=206=20PRs=20open,=20D-CCF-3=20in=20q2=20#71?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- .claude/board/AGENT_LOG.md | 12 ++++++++++++ .claude/board/STATUS_BOARD.md | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index 8e2b29c6..cbebbb01 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,15 @@ +## 2026-07-02 — Fleet flip EXECUTION (2× Sonnet edit agents + main thread) — 6 PRs open + +- **PRs:** lance-graph #628 (P0+P1+compat-reader), OGAR #147 (vocab flip + + 18-file doc sweep, Sonnet agent), openproject-nexgen-rs #68, MedCare-rs + #180, woa-rs #177 (doc-only), q2 #71 (Sonnet agent: cockpit/osint-bake/ + fma/cpic; cpic interim canon-high Genetics:q2 0x0E01_000N; SAMPLE_GUIDS + regenerated from real ingest). +- **Zero-impact (no PR):** OGIT, tesseract-rs, openproject (Ruby). +- **Deferred:** q2 .soa re-bakes (runtimed [patch] unfetchable in sandbox — + safe via legacy aliases + BodyV3 dual-accept; CI/dev follow-up) + + body.soa release re-upload. Merge order: #628 → #147 → consumers. + ## 2026-07-02 — Fleet flip inventories (5× Sonnet, read-only) + P1 flip landed - **Agents:** q2 / OGAR+OGIT / MedCare-rs+openproject-nexgen-rs+openproject / diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index a429b049..91e37137 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -7,7 +7,7 @@ Plan: `.claude/plans/classid-canon-custom-flip-v1.md`. Operator trigger 2026-07- | D-CCF-0 | compose_classid/split_classid/CLASSID_CANON_HIGH + route all sites (zero behavior) | lance-graph-contract | Shipped (fd9bf6b) | plan §3/§4 P0 | | D-CCF-1 | Flip + mint new-form classids (0x0701_1000 / 0x0A01_1000 / 0x0E01_1000) coexisting | lance-graph-contract | In PR (#628) | gated on P0 probes | | D-CCF-2 | OGAR#95 hi-u16 app-prefix reconciliation | contract + OGAR | In PR (OGAR #147; prefix = custom half) | plan §2 row / §4 P2 | -| D-CCF-3 | q2 re-mints (osint-bake + cpic via contract pull; dissolves ISS-Q2-CPIC-MIRROR) | q2 (gate WAIVED) | In progress (code+re-bakes in flight; cpic interim canon-high local scheme, full contract pull tracked) | plan §4 P3 | +| D-CCF-3 | q2 re-mints (osint-bake + cpic via contract pull; dissolves ISS-Q2-CPIC-MIRROR) | q2 (gate WAIVED) | In PR (q2 #71; .soa re-bakes deferred to CI/dev; cpic interim 0x0E01_000N, full contract pull tracked) | plan §4 P3 | | D-CCF-4 | 0x1000 marker retirement | all | Blocked (operator checkpoint) | plan §4 P4 | ## v3-convergence-wiring-v1 — wire, don't invent (the seam list) From 019062a920fab163ba2104a8ac0051a4e2e767b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Jul 2026 06:17:15 +0000 Subject: [PATCH 7/7] board: exact exported symbol names in the #627 arc prose (CodeRabbit on #628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LATEST_STATE guard line now cites classid_canon(id) (split_classid returns a tuple; .canon is not a field). PR_ARC #627 entry gets an appended dated correction line naming ClassidOrder::CanonHigh + classid_canon(id) (append-only honored — original lines untouched). Co-Authored-By: Claude --- .claude/board/LATEST_STATE.md | 2 +- .claude/board/PR_ARC_INVENTORY.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 11788ba9..c7e38700 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -100,7 +100,7 @@ Membrane consumers can now pull BOTH halves of a render `classid` BBB-safely fro | PR | Merged | Title | What it added | |---|---|---|---| -| **#627** | 2026-07-02 | classid canon:custom flip TRIGGERED (doc-only) | Operator ruling recorded + `classid-canon-custom-flip-v1.md` ACTIVE: canon `domain:appid` → hi u16, custom (`0x1000` temporary marker) → lo; `0x0701_1000` / `0x07:01::1000`; OSINT low byte = appid space (zero vocab rows, OGAR #146 67→65 fuse balanced); q2 gate WAIVED; ISSUES ×4 resolved/ruled; codex P2 guards locked (class_id via `split_classid(id).canon` never `as u16`; legacy keys demote not retire). Merge `c8e1ec4`. | +| **#627** | 2026-07-02 | classid canon:custom flip TRIGGERED (doc-only) | Operator ruling recorded + `classid-canon-custom-flip-v1.md` ACTIVE: canon `domain:appid` → hi u16, custom (`0x1000` temporary marker) → lo; `0x0701_1000` / `0x07:01::1000`; OSINT low byte = appid space (zero vocab rows, OGAR #146 67→65 fuse balanced); q2 gate WAIVED; ISSUES ×4 resolved/ruled; codex P2 guards locked (class_id via `classid_canon(id)` never `as u16`; legacy keys demote not retire). Merge `c8e1ec4`. | | **#626** | 2026-07-02 | V3 convergence wiring: tenant-carve certification, RungElevator, P6 wave probe, seam-list plan | "Wire, don't invent": `RungLevel::{from_u8,elevate,de_elevate,pearl_level,causal_mask_bits}` + `RungElevator` (sustained-BLOCK policy over P2/P3-certified masks; converged with `escalation::rung_delta` via `apply_delta` — one ladder, two signal sources) wired through the driver (persistent elevator, `ctx.rung=1` proxy retired, grpc rung saturates-never-wraps per codex P2); BOTH V3 tenant carves matrix-certified (Cognitive + Compressed); P6 probe (wave dist == certified palette read, markov_soa verified); `[patch.crates-io] ndarray` → local sibling path (fetch deadlock gone; first in-sandbox core build, 925/925). Plan `v3-convergence-wiring-v1.md`; worker Rule 7. Branch `claude/v3-substrate-migration-review-o0yoxv`, merge `5aaee33`. | | **#542** | 2026-06-18 | E-OGAR-IS-FOUNDRY capstone + 5+3 council + the key→row baton | Foundry/Gotham = "write the OGAR class schema + inheritance"; everything else is generic machinery over it (ontology=`classid→ClassView`+inheritance, AR=DO/THINK, pipelines=`compute_dag`, apps=Jinja-over-classes, query=Cypher⇄SurrealQL one IR). Added `MailboxSoaView::row_for_local_key -> Option` (default `None`, deferred-binding — the key→row baton for a future `Backend::MailboxSoa` router). Epiphanies `E-OGAR-IS-FOUNDRY`/`E-CYPHER-IS-THE-KANBAN-AST`/`E-GUID-IS-THE-GRAPH`; plan `cypher-kanban-ast-unification-v1`. Council corrections: `from_guid_prefix` is on `NiblePath` not `NodeGuid`; "odoo proof" = CONJECTURE; `ogar-adapter-surrealql` not a crate. Branch `claude/q2-substrate-grounding`, merge `faca377f`. | | **#540** | 2026-06-18 | `lite-unified` additive default-OFF coexistence feature gate | **+35/-5, 2 files.** `lite-unified = []` in `crates/lance-graph/Cargo.toml` (empty until SurrealQL-on-lance lowering lands). **datafusion stays DEFAULT — NOT deprecated, NOT made optional.** Process, not switch; promoted per query-shape once OQ-LU-2a is green. Zero behavior change at default features. Branch `claude/lite-unified-gate`, merge `ef7e97ef`. | diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index eb406a97..85f01e9a 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -43,6 +43,8 @@ **Locked:** OSINT low byte = APPID space domain-wise (`00` = domain, `01` = q2 — zero vocabulary rows, executed OGAR PR #146, codebook 67→65, COUNT_FUSE balanced with zero mirror changes); `0x1000` = temporary reminder, not a format bit; the SoA `class_id` discriminator derives via `split_classid(id).canon` NEVER `as u16` (codex P2 — post-flip `as u16` collapses every class to `0x1000`); legacy registry keys DEMOTE to read-only aliases, retirement only on corpus proof (codex P2 — mint-forward means persisted old-form rows resolve forever until re-baked). +**Correction (2026-07-02, appended — CodeRabbit on #628):** the exact exported symbols are `ClassidOrder::CanonHigh` (the flippable order const `CLASSID_ORDER`, set via the enum — there is no `CLASSID_CANON_HIGH` bool) and `classid_canon(id)` (the canon accessor — `split_classid(id)` returns a tuple, `.canon` is not a field). The intent of the two guard lines above is unchanged. + **Deferred:** P2 + P4 operator checkpoints; implementation phases (D-CCF-0 starts immediately post-merge). **Docs/board:** as Added; OGAR side in `DISCOVERY-MAP.md` `D-OSINT-APPID-NOT-CONCEPT` (OGAR #146, merged `a0c7936`).