From 3a5d83649ebeff931c3b1ef63b6068bc05de69e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 07:54:32 +0000 Subject: [PATCH 1/7] docs(board): post-merge hygiene for #441 (D-CLS arc Shipped) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mandatory post-merge board update (merge a77e119): - STATUS_BOARD: 5 D-CLS rows In PR -> Shipped - PR_ARC_INVENTORY: prepend #441 entry (Added/Locked/Deferred/Docs/Confidence) — records the class-flies-above-SoA invariant, C2/N3, FieldMask-no-fold (Codex P2), carrier-methods (CodeRabbit), DOLCE-from-cache + class_id-reuse decisions - LATEST_STATE: prepend Contract-Inventory blockquote (class_view + class_resolver + class_signature surface, so a new session does not re-derive them) https://claude.ai/code/session_01R9AWgFa65uPnLyS2my2d2R --- .claude/board/LATEST_STATE.md | 2 ++ .claude/board/PR_ARC_INVENTORY.md | 26 ++++++++++++++++++++++++++ .claude/board/STATUS_BOARD.md | 10 +++++----- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 26f2e3cd..bf3586dc 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -42,6 +42,8 @@ ## Current Contract Inventory (lance-graph-contract) +> **2026-05-31 — MERGED (#441, D-CLS arc, merge `a77e119`)**: `lance_graph_contract::class_view::{FieldMask (u64 presence bitmask), ClassView (resolver trait), ClassProjection, RenderRow}` + `ClassView::render_rows` (off-bits-skipped). `ClassId = u16` (reuses `soa_view::class_id`). The class meta-DTO **flies ABOVE the agnostic SoA** — labels/shape/DOLCE resolve LATE from the OGIT cache, nothing semantic in the row (C2 presence≠semantics; N3 stable positions; out-of-range mask bits IGNORED not folded — Codex P2). Ontology side: `class_resolver::RegistryClassView` (impls `ClassView` over the live `OntologyRegistry`, DOLCE via `classify_odoo`) + `odoo_blueprint::class_signature::{StructuralSignature, OdooEntity::signature()/object_view() carrier methods, audit, shape_families, curated_entities, corpus_summary}` (deterministic FNV-1a structural-hash group-by, NOT Aerial-cluster). Zero-dep preserved; extends `ontology::{ObjectView,FieldRef,DisplayTemplate}`, reuses `class_id` (no new newtype). 497 contract + 240 ontology lib tests. D-CLS-{FM,RES,SIG,AUDIT,RENDER} all Shipped. +> > **2026-05-30 — PR-in-flight addition** (D-MBX-A6 Phase 2 — Rubicon lifecycle + ExecTarget): `lance_graph_contract::kanban::{ExecTarget (Native/Jit/SurrealQl/Elixir), RubiconTransitionError}` + `KanbanColumn::{next_phases, can_transition_to}` (the Rubicon lifecycle DAG) + `KanbanMove.exec: ExecTarget` field + `MailboxSoaOwner::try_advance_phase()` (checked lifecycle enforcement → `Result`). Zero-dep; `KanbanMove` still ≤16 B; 489 contract lib tests (+4); downstream cargo-check clean. Lifecycle enforcement + planner exec-target are now contract-level invariants. Resolves the #437 deferred exec-target NOTE. Cross-ref D-MBX-A6 / #437. > > **2026-05-30 — PR-in-flight addition** (D-MBX-A6 Phase 1 — planner⟷ractor⟷surreal meta-DTO): `lance_graph_contract::kanban::{KanbanColumn (6: Planning/CognitiveWork/Evaluation/Commit/Plan/Prune), KanbanMove}` — the 4-phase Rubicon kanban transition; `KanbanMove` is `Copy`, ≤16 B, carries `MailboxId` + `witness_chain_position` (R4 pointer) + `libet_offset_us` (−550 ms anchor, D-MBX-8). `lance_graph_contract::soa_view::{MailboxSoaView, MailboxSoaOwner}` — zero-dep **borrow trait** for the transparent zero-copy SoA view (R1 "one SoA never transformed"); `&[T]` column accessors (energy/edges_raw/meta_raw/entity_type) mirror `MailboxSoA::*_at`; the read/owner split makes "view is read-only" structural (surreal implements only the read half). `orchestration::StepDomain::Kanban` variant + `"kanban."` prefix. Consumed via the EXISTING `OrchestrationBridge` (planner emits, ractor owns/drives via `MailboxSoaOwner`, surreal_container projects via read-only `MailboxSoaView`) — NO parallel DTO family (lab-vs-canonical ruling). Contract `[dependencies]` still empty. 485 contract lib tests green (+6 new); `cargo check` clean on planner/cognitive-shader-driver/supervisor (StepDomain variant additive-safe). Consumer impls deferred. See E-SOA-VIEW-IS-A-BORROW; `unified-soa-convergence-v1.md §5+§8.4`. diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index 24dc3172..14fbc8c9 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -35,6 +35,32 @@ --- +## #441 odoo-classes-bitmask-render (D-CLS arc) — classes as a SoA-view with presence bitmask + +**Status:** MERGED 2026-05-31 (merge commit `a77e119`), branch `claude/sleepy-cori-aRK2x`. 5 feature slices + 4 review-round fixes; 973 insertions, purely additive (0 deletions). Built AFTER a council + brutal-honest review of the #440 *plan* caught 3 P0 factual errors — these slices sidestep them by construction. + +**Added:** +- `lance-graph-contract::class_view` (NEW module, zero-dep): `FieldMask(u64)` presence bitmask + `ClassView` resolver **trait** + `ClassProjection` + `RenderRow` + `ClassView::render_rows` (off-bits-skipped). `ClassId = u16` (reuses the existing `soa_view::class_id` width). Extends `ontology::{ObjectView, FieldRef, DisplayTemplate}`; does not duplicate. (D-CLS-FM + D-CLS-RENDER) +- `lance-graph-ontology::class_resolver` (NEW module): `RegistryClassView` impls `ClassView` over the live `OntologyRegistry`; DOLCE resolved LATE via `classify_odoo(ogit_uri)`; per-class memo over the O(n) registry scan. (D-CLS-RES) +- `lance-graph-ontology::odoo_blueprint::class_signature` (NEW module): `StructuralSignature` + `OdooEntity::signature()`/`object_view()` (carrier methods) + `audit`/`shape_families`/`curated_entities`/`corpus_summary`. Deterministic FNV-1a over kind+field/method-kind histograms + state-machine. (D-CLS-SIG + D-CLS-AUDIT) + +**Locked:** +- **The class flies ABOVE the agnostic SoA** (classes.md "the meta-DTO resolves; it does not store"): SoA row = agnostic bytes (class_id + presence bits only); `ClassView` = the late-bound parser+schema; `FieldMask` = which optional fields are present. Nothing semantic in the row. +- **C2 — `FieldMask` is presence, NEVER semantics.** `render_rows` yields a row iff its bit is set; never branches on meaning. +- **N3 — stable append-only bit positions** (field position `i` = declared-field `i`). Out-of-range positions (≥ `MAX_FIELDS`=64) are **IGNORED, not folded** (Codex P2 fix `250b66f`) — `& 63` folding would alias pos 64→bit 0 and silently corrupt the presence contract. +- **Structural-signature = deterministic group-by-on-hash, NOT Aerial+ clustering** (that entry point does not exist — confirmed in review). Name-independent. +- **DOLCE resolved from the OGIT cache** (OD-DOLCE ratification); **reuse the existing `class_id`** (no colliding newtype). These two sidestep the canonical-enum contest + 6-vs-4 DOLCE dispute caught in the #440-plan review. +- **`signature`/`object_view` are carrier methods on `OdooEntity`**, not free functions (CodeRabbit `21c37eb`, CLAUDE.md "The Click" litmus). +- Zero-dep contract preserved; dependency-inversion (contract owns the trait, ontology owns the answers — like `MailboxSoaView`). + +**Deferred:** the askama render-**engine** crate (`render_rows` is the LOGIC, not the engine); the registry `by_entity_type_id` O(1) index (RegistryClassView memoizes over the O(n) scan today); naming the discovered shape-families (Wave-2 human step); **Wikidata-HHTL** (the classes.md-N4 second-domain falsifier reusing `FieldMask`/`signature`/`ClassView`). + +**Docs:** EPIPHANIES (the #440-plan REVIEW VERDICT, the OD ratifications, each of the 5 slices, the FieldMask-no-fold fix, and the arm-discovery-is-a-proposer-not-the-SPO-AST finding); STATUS_BOARD D-CLS-* rows → Shipped; this PR_ARC entry; LATEST_STATE Contract-Inventory blockquote. + +**Confidence (2026-05-31):** working — 497 contract + 240 ontology lib tests green; clippy `-D warnings` + rustfmt clean; merged clean after Codex P2 + 2 CodeRabbit findings (all real, all resolved in the babysit loop before merge). + +--- + ## callcenter/audit-fix — fix(callcenter): `with_jsonl_audit` returns `Result` (branch work) **Status:** On branch `claude/activate-lance-graph-att-k2pHI` (HEAD `ea2a378`, not yet a PR). 1-line `.rs` change + this board record (EPIPHANIES E-AUDIT-1, prepended 2026-05-27). diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 924c1975..6e1377d1 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -568,11 +568,11 @@ Plan path: `.claude/plans/unified-soa-convergence-v1.md`. Handover `.claude/hand | D-MBX-A6-P2 | Rubicon lifecycle enforcement + exec-target tag: `KanbanColumn::{next_phases, can_transition_to, is_absorbing}` (the lifecycle DAG) + `MailboxSoaOwner::try_advance_phase` (checked, `RubiconTransitionError`) + `ExecTarget{Native,Jit,SurrealQl,Elixir}` on `KanbanMove` | lance-graph-contract | 120 | LOW | **In PR** | builds on P1; 489 lib tests (+4); downstream cargo-check clean; gates the ractor owner-impl + planner emit (P3) | | D-MBX-A6-P3a | StyleStrategy: thinking-style -> cluster -> mechanism -> recipe_kernels Tactic selection (planning substrate; carries tau JIT addr) | lance-graph-planner | 130 | LOW | **In PR** | #439; first cut of A6-P3 consumer wiring; planner now consumes contract recipes/styles; deferred: i4-32D decode, Outcome->Candidate, tau->JIT, membrane commit | | D-MBX-A6-P3-M1 | `Tactic::requires() -> ThoughtMask` + `ThoughtField`/`ThoughtMask` (checklist-as-data keystone): 34 tactics declare their ThoughtCtx field-reads; `covered_by` = reliability-coverage gate | lance-graph-contract | 120 | LOW | **In PR** | #439; the panel-recalibrated keystone (extraction not construction); makes P1/P7/P11 derived; teeth-test asserts masks varied not stub | -| D-CLS-FM | `class_view`: FieldMask(u64 presence) + ClassView meta-DTO resolver trait + ClassProjection (the class flies ABOVE the SoA; labels resolved late from OGIT cache, zero in the bytes) — extends ObjectView, reuses class_id | lance-graph-contract | 270 | LOW | **In PR** | #441 D-CLS contract foundation; OD-gates ratified; presence!=semantics (C2); N3 stable positions; 3 teeth-tests | -| D-CLS-RES | `class_resolver`: `RegistryClassView` impls `ClassView` over the live OntologyRegistry — the ontology-side 'parser' (class_id -> shape, DOLCE resolved LATE via classify_odoo from the cache URI, memoized over the O(n) registry scan) | lance-graph-ontology | 200 | LOW | **In PR** | #441 D-CLS; makes the contract trait live; field-set supplied (D-CLS audit deferred); 4 teeth-tests | -| D-CLS-SIG | `class_signature`: deterministic structural-signature audit of curated OdooEntity consts (FNV-1a over kind+field-hist+method-hist+state-machine) -> shape-family group-by + `object_view()` derives the real ObjectView bit-basis (fills the D-CLS-RES placeholder) | lance-graph-ontology | 230 | LOW | **In PR** | #441 D-CLS; the HONEST D-CLS-3 (group-by-on-structural-hash, NOT aerial-cluster vaporware, classes.md:43); 4 teeth-tests over real l1 data | -| D-CLS-AUDIT | `class_signature` corpus audit: `curated_entities()` (all 15 l-lanes, 64 consts) + `corpus_summary()` + falsifiable test that the real curated corpus collapses entities->fewer shape-families (classes.md:42 CONFIRMED on real data, not asserted) | lance-graph-ontology | 90 | LOW | **In PR** | #441 D-CLS Wave-2 input; +clippy fix (unused FieldMask import in class_resolver) | -| D-CLS-RENDER | `ClassView::render_rows` + `RenderRow{label,predicate}` — the off-bits-skipped render surface (C2 presence-only; template-agnostic, askama engine deferred to its own crate-Wave) | lance-graph-contract | 50 | LOW | **In PR** | #441 D-CLS; the render LOGIC (classes.md:49), not the engine; +doc-lint fix | +| D-CLS-FM | `class_view`: FieldMask(u64 presence) + ClassView meta-DTO resolver trait + ClassProjection (the class flies ABOVE the SoA; labels resolved late from OGIT cache, zero in the bytes) — extends ObjectView, reuses class_id | lance-graph-contract | 270 | LOW | **Shipped** | #441 D-CLS contract foundation; OD-gates ratified; presence!=semantics (C2); N3 stable positions; 3 teeth-tests | +| D-CLS-RES | `class_resolver`: `RegistryClassView` impls `ClassView` over the live OntologyRegistry — the ontology-side 'parser' (class_id -> shape, DOLCE resolved LATE via classify_odoo from the cache URI, memoized over the O(n) registry scan) | lance-graph-ontology | 200 | LOW | **Shipped** | #441 D-CLS; makes the contract trait live; field-set supplied (D-CLS audit deferred); 4 teeth-tests | +| D-CLS-SIG | `class_signature`: deterministic structural-signature audit of curated OdooEntity consts (FNV-1a over kind+field-hist+method-hist+state-machine) -> shape-family group-by + `object_view()` derives the real ObjectView bit-basis (fills the D-CLS-RES placeholder) | lance-graph-ontology | 230 | LOW | **Shipped** | #441 D-CLS; the HONEST D-CLS-3 (group-by-on-structural-hash, NOT aerial-cluster vaporware, classes.md:43); 4 teeth-tests over real l1 data | +| D-CLS-AUDIT | `class_signature` corpus audit: `curated_entities()` (all 15 l-lanes, 64 consts) + `corpus_summary()` + falsifiable test that the real curated corpus collapses entities->fewer shape-families (classes.md:42 CONFIRMED on real data, not asserted) | lance-graph-ontology | 90 | LOW | **Shipped** | #441 D-CLS Wave-2 input; +clippy fix (unused FieldMask import in class_resolver) | +| D-CLS-RENDER | `ClassView::render_rows` + `RenderRow{label,predicate}` — the off-bits-skipped render surface (C2 presence-only; template-agnostic, askama engine deferred to its own crate-Wave) | lance-graph-contract | 50 | LOW | **Shipped** | #441 D-CLS; the render LOGIC (classes.md:49), not the engine; +doc-lint fix | --- From eb1b1415133f5017938049b76667610b74ca3f72 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 08:10:43 +0000 Subject: [PATCH 2/7] =?UTF-8?q?feat(contract):=20D-WIKI-HHTL-1=20=E2=80=94?= =?UTF-8?q?=20hhtl::NiblePath=2016^n=20bucket=20router=20+=20FieldMask::in?= =?UTF-8?q?herit=20(Wikidata-HHTL=20slice=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The downstream 16^n Abstammung bucket router #438 (D-ARM-14 P1) names but did not build: subClassOf nibble path, bit-shift O(1) addressing (root/child/basin/parent/is_ancestor_of). DOLCE-agnostic by construction (basin: u8 = dolce_id 0..3 resolved THROUGH the ontology cache per OD-DOLCE b31464d — never an embedded enum; matches both #441 dolce_id and #438 basin() orderings). + FieldMask::inherit (mask-inherits-as-delta; multi-parent = orthogonal facet bit in the same mask, NOT a 2nd path). Reuses #441 FieldMask; zero-dep preserved. The N4 second-domain falsifier for the D-CLS machinery: the Wikidata D-CLS triple (class_id, shape_hash, presence_bitmask) = (ClassId, StructuralSignature, FieldMask) — all #441. Convergent with D-ARM-14, firewall preserved (aerial = zero-dep proposer; contract/ontology = hub). 4 teeth-tests (bit-shift round-trip, depth cap + out-of-range nibble ignored, prefix reachability, multi-parent-is-a-facet-bit); 501 contract lib green; clippy -D warnings + fmt clean. Board: FINDING D-CLS<->D-ARM-14 reconciliation (EPIPHANIES) + D-WIKI-HHTL-1 row. https://claude.ai/code/session_01R9AWgFa65uPnLyS2my2d2R --- .claude/board/EPIPHANIES.md | 18 ++ .claude/board/STATUS_BOARD.md | 1 + crates/lance-graph-contract/src/class_view.rs | 14 + crates/lance-graph-contract/src/hhtl.rs | 249 ++++++++++++++++++ crates/lance-graph-contract/src/lib.rs | 1 + 5 files changed, 283 insertions(+) create mode 100644 crates/lance-graph-contract/src/hhtl.rs diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 768fb6ea..89ad99b3 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,21 @@ +## 2026-05-31 — FINDING: D-CLS (#441) ↔ D-ARM-14 (#438) converge on Wikidata-HHTL — the second-domain falsifier reuses the class-meta-DTO 1:1 (cross-session synthesis) + +**Status:** FINDING (cross-session reconciliation, confirmed by the D-ARM-14 session). Anchors the Wikidata-HHTL arc on the merged D-CLS machinery; no parallel layer grows. + +**The convergence:** 439/440/441 (D-CLS) and 436/438 (D-ARM-13/14) are convergent, not conflicting. The Wikidata "D-CLS triple" `(class_id, shape_hash, presence_bitmask)` = `(ClassId(u16), StructuralSignature, FieldMask)` — ALL #441 types. wikidata facet-bitmask = `FieldMask`; shape = `signature()`'s structural-hash (generalised from `OdooEntity` to any entity); presence = `FieldMask`. + +**The firewall split (clean territory, no collision):** +- **Proposer side (the D-ARM-14 session's lane):** `arm-discovery::aerial` stays the zero-dep PROPOSER — similarity in discovery only, skeleton-only predicates, emits `(s,p,o,f,c)`. Validated by the proposer-layering FINDING (67903a8): aerial FEEDS the AST hub, is not the hub. +- **Hub side (this/D-CLS session's lane):** `contract`/`ontology` own `FieldMask`/`signature`/`ClassView` + the new `contract::hhtl::NiblePath` router. "The hub side owns contract/ontology." + +**This slice (D-WIKI-HHTL-1):** `contract::hhtl::NiblePath` — the 16ⁿ Abstammung bucket router #438's wiring doc names as "downstream." DOLCE-agnostic (`basin: u8`). basin `0..3` = `dolce_id::{ENDURANT=0,PERDURANT=1,QUALITY=2,ABSTRACT=3}` (#441 cache u8), which ALSO matches arm-discovery's discovery-side `DolceCategory::basin()` ordering (#438) — both sides agree on the nibble WITHOUT either embedding the enum (OD-DOLCE: resolve through the cache, b31464d). + `FieldMask::inherit` (mask-inherits-as-delta; multi-parent = facet bit in the same mask, NOT a 2nd path). 4 teeth-tests; 501 contract lib green; zero-dep preserved. + +**One proposer-side alignment — NOT this session (the D-ARM-14 session owns it, its own branch):** `aerial::ontology::DolceCategory` currently hardcodes `dolce:…` IRIs; ratified pattern = emit `dolce_id` u8, resolve the IRI late from cache (proposer stores no semantics). `basin()` already matches `dolce_id` ordering → alignment, not rework. Recorded here for cross-session visibility only. + +**#438 council verdict (4f381a8):** no code action items — it reviewed a `discovery_origin` byte-grammar plan, not the crate; fixed a stale Wave-1 citation, escalated 2 spec decisions (tier-set + proposer-id width) to ISSUES, queued D-CHESS-BRINGUP-1. + +**Cross-ref:** #441 (D-CLS), #438 (D-ARM-14 P1), 67903a8 (proposer-layering), b31464d (OD-DOLCE), `splat-codebook-aerial-wikidata-compression.md`, `wikidata-hhtl-load.md`, `contract::hhtl`. + ## 2026-05-31 — FINDING (research): arm-discovery is a PROPOSER that FEEDS the SPO-AST, not the SPO-AST itself — using it AS the AST would conflate proposer↔hub + push similarity into addressing **Status:** FINDING (read-only research, answering "can lance-graph-arm-discovery be the SPO-AST?"). No code; prevents a layering mistake. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 6e1377d1..7f0bf060 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -573,6 +573,7 @@ Plan path: `.claude/plans/unified-soa-convergence-v1.md`. Handover `.claude/hand | D-CLS-SIG | `class_signature`: deterministic structural-signature audit of curated OdooEntity consts (FNV-1a over kind+field-hist+method-hist+state-machine) -> shape-family group-by + `object_view()` derives the real ObjectView bit-basis (fills the D-CLS-RES placeholder) | lance-graph-ontology | 230 | LOW | **Shipped** | #441 D-CLS; the HONEST D-CLS-3 (group-by-on-structural-hash, NOT aerial-cluster vaporware, classes.md:43); 4 teeth-tests over real l1 data | | D-CLS-AUDIT | `class_signature` corpus audit: `curated_entities()` (all 15 l-lanes, 64 consts) + `corpus_summary()` + falsifiable test that the real curated corpus collapses entities->fewer shape-families (classes.md:42 CONFIRMED on real data, not asserted) | lance-graph-ontology | 90 | LOW | **Shipped** | #441 D-CLS Wave-2 input; +clippy fix (unused FieldMask import in class_resolver) | | D-CLS-RENDER | `ClassView::render_rows` + `RenderRow{label,predicate}` — the off-bits-skipped render surface (C2 presence-only; template-agnostic, askama engine deferred to its own crate-Wave) | lance-graph-contract | 50 | LOW | **Shipped** | #441 D-CLS; the render LOGIC (classes.md:49), not the engine; +doc-lint fix | +| D-WIKI-HHTL-1 | `contract::hhtl::NiblePath`: the 16ⁿ Abstammung bucket router (subClassOf nibble path, bit-shift O(1), `root`/`child`/`basin`/`parent`/`is_ancestor_of`) + `FieldMask::inherit` (mask-inherits-as-delta). DOLCE-agnostic (`basin: u8` = dolce_id 0..3, resolved through the cache — NO enum); multi-parent = facet bit in the same mask, NOT a 2nd path. The downstream router #438 names. | lance-graph-contract | 155 | LOW | **In progress** | Wikidata-HHTL slice 1 = the N4 second-domain falsifier for the D-CLS machinery; reuses #441 FieldMask; convergent with D-ARM-14 (hub side, firewall preserved). 4 teeth-tests; 501 contract lib green. See FINDING D-CLS↔D-ARM-14 (EPIPHANIES). Deferred: the ontology cache-resolution seam (basin←DOLCE←cache, reuses classify+ClassView), the 115M streaming load. | --- diff --git a/crates/lance-graph-contract/src/class_view.rs b/crates/lance-graph-contract/src/class_view.rs index 4ca9b3a0..5532e087 100644 --- a/crates/lance-graph-contract/src/class_view.rs +++ b/crates/lance-graph-contract/src/class_view.rs @@ -122,6 +122,20 @@ impl FieldMask { pub const fn is_empty(self) -> bool { self.0 == 0 } + + /// Inherit a parent class's presence into this mask — the **mask-inherits-as- + /// delta** of the HHTL `subClassOf` walk (`wikidata-hhtl-load.md`). A child + /// IS-A its parent, so its mask carries every field the parent declares + /// present PLUS its own `delta`: a bitwise union. N3 stable positions mean the + /// parent's bits never move — the child only adds (multi-parent + /// "flying-family" facets are orthogonal bits in this same mask, never a + /// second path). Read `parent.inherit(own_delta)` → the child's full mask; + /// the union is commutative, so the direction is documentation, not a + /// constraint. See [`crate::hhtl`]. + #[inline] + pub const fn inherit(self, delta: FieldMask) -> FieldMask { + FieldMask(self.0 | delta.0) + } } /// The class as a **meta lookup that flies above the SoA** — the resolver trait. diff --git a/crates/lance-graph-contract/src/hhtl.rs b/crates/lance-graph-contract/src/hhtl.rs new file mode 100644 index 00000000..182ab110 --- /dev/null +++ b/crates/lance-graph-contract/src/hhtl.rs @@ -0,0 +1,249 @@ +//! # `hhtl` — the 16ⁿ nibble bucket router (the Abstammung tree axis). +//! +//! `wikidata-hhtl-load.md` §"HHTL = the cheap bucket router (16^n)": the ONE tree +//! axis is the `subClassOf` (P279) path, addressed as a fixed-fan-out-16 nibble +//! sequence — *"bucket path = nibble sequence → routing is bit-shift, not hash +//! lookup. O(1) arithmetic (super billig)."* This is the **downstream bucket +//! router** PR #438 (D-ARM-14 Phase 1) names but did not build: aerial discovers +//! the OWL/DOLCE skeleton + basins; this routes an entity into its 16ⁿ bucket. +//! +//! **Domain-agnostic by construction.** The router takes a `basin` nibble +//! (`0x0..=0xF`) and child nibbles; it does NOT know DOLCE. The DOLCE→basin +//! binding is resolved THROUGH the ontology cache (OD-DOLCE ratification, #441 +//! `b31464d` "DOLCE-from-cache, dissolves 6v4") — never a hard-coded enum here. +//! So the duplicated `DolceCategory` (arm-discovery discovery-side *vs* ontology +//! cache-side) is dissolved at the **resolution layer**, not by a third copy in +//! contract: the structural router has zero DOLCE knowledge. +//! +//! The DOLCE top facets seed basins `0..3` by the cache's stable `dolce_id` +//! ordering — `ENDURANT=0`, `PERDURANT=1`, `QUALITY=2`, `ABSTRACT=3` (#441 +//! `class_resolver::dolce_id`) — which is ALSO the order of arm-discovery's +//! discovery-side `DolceCategory::basin()` (#438). Both sides of the firewall +//! therefore agree on the nibble without either embedding the enum here; the +//! remaining `0x4..=0xF` basins are reserved (append-only) for finer top axes. +//! The Wikidata "D-CLS triple" `(class_id, shape_hash, presence_bitmask)` is +//! `(ClassId, StructuralSignature, FieldMask)` from #441; this path is its +//! addressing. +//! +//! **One tree axis only (`wikidata-hhtl-load.md`:46).** Multi-parent +//! ("flying-family") is NOT a second nibble path — it is an orthogonal facet bit +//! in the SAME [`FieldMask`](crate::class_view::FieldMask). *"Bat = mammal-path + +//! flight-bit, not two paths."* This keeps 16ⁿ a clean tree (cheap nibble +//! addressing) AND keeps multi-parent dedup. +//! +//! **mask-inherits-as-delta.** Walking DOWN the path is IS-A inheritance: a +//! child's presence mask is the parent's OR its own delta +//! ([`FieldMask::inherit`](crate::class_view::FieldMask::inherit)). N3 stable +//! positions mean the parent's bits never move; the child only adds. + +/// Fixed HHTL fan-out: 16 children per level (one nibble). `wikidata-hhtl-load.md`:44. +pub const FAN_OUT: u8 = 16; + +/// Max depth addressable in a single `u64` path (16 nibbles × 4 bits = 64). +/// Beyond this the bit-budget discipline says switch to a ref, not a deeper +/// nibble (`wikidata-hhtl-load.md`:71 "grows unbounded → path/ref"). +pub const MAX_DEPTH: u8 = 16; + +/// A path in the 16ⁿ Abstammung tree — a nibble sequence, root-first, packed into +/// a `u64`. Routing is bit-shift, not hash (O(1) arithmetic). +/// +/// Layout: the root (basin) nibble occupies the highest *used* nibble; each +/// [`child`](NiblePath::child) shifts the accumulated path left 4 and ORs the new +/// leaf nibble into the low 4 bits. [`depth`](NiblePath::depth) counts the nibbles +/// used, so a partially-filled `u64` is unambiguous (leading zero nibbles are +/// "not yet routed", not basin 0). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct NiblePath { + path: u64, + depth: u8, +} + +impl NiblePath { + /// The empty path — no basin routed yet. + pub const EMPTY: Self = Self { path: 0, depth: 0 }; + + /// Start a path at a `basin` nibble — the DOLCE top facet, resolved UPSTREAM + /// through the ontology cache (not decided here). The low nibble is taken + /// (`basin & 0x0F`); callers pass `0x0..=0xF`. + #[must_use] + pub const fn root(basin: u8) -> Self { + Self { + path: (basin & 0x0F) as u64, + depth: 1, + } + } + + /// Route one level deeper to child `nibble`. Returns `self` UNCHANGED once + /// [`MAX_DEPTH`] is reached (the path is full — deeper addressing needs a ref) + /// or `nibble >= FAN_OUT` (out of range — never folded onto a valid child, + /// mirroring [`FieldMask`](crate::class_view::FieldMask)'s out-of-range discipline). + #[must_use] + pub const fn child(self, nibble: u8) -> Self { + if self.depth >= MAX_DEPTH || nibble >= FAN_OUT { + self + } else { + Self { + path: (self.path << 4) | (nibble as u64), + depth: self.depth + 1, + } + } + } + + /// The basin (root) nibble — the DOLCE top facet this path lives under. + /// `None` for the empty path. + #[must_use] + pub const fn basin(self) -> Option { + if self.depth == 0 { + None + } else { + Some(((self.path >> (4 * (self.depth as u32 - 1))) & 0x0F) as u8) + } + } + + /// The leaf (deepest) nibble. `None` for the empty path. + #[must_use] + pub const fn leaf(self) -> Option { + if self.depth == 0 { + None + } else { + Some((self.path & 0x0F) as u8) + } + } + + /// The parent path (one level shallower). `None` at the basin/empty — the + /// basin has no parent in this tree (it IS the DOLCE top facet). + #[must_use] + pub const fn parent(self) -> Option { + if self.depth <= 1 { + None + } else { + Some(Self { + path: self.path >> 4, + depth: self.depth - 1, + }) + } + } + + /// Depth (number of nibbles routed). + #[must_use] + pub const fn depth(self) -> u8 { + self.depth + } + + /// Is this path a prefix of (ancestor-or-equal of) `other`? — the cheap + /// arithmetic reachability test that replaces a P279\* graph walk. An empty + /// path is an ancestor of nothing (there is no basin to share). + #[must_use] + pub const fn is_ancestor_of(self, other: Self) -> bool { + if self.depth == 0 || self.depth > other.depth { + false + } else { + // Align `other` down to self.depth, then compare the shared prefix. + (other.path >> (4 * (other.depth as u32 - self.depth as u32))) == self.path + } + } + + /// The raw packed `(path, depth)` — for SoA facet-column storage / CAM key. + #[must_use] + pub const fn packed(self) -> (u64, u8) { + (self.path, self.depth) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::class_view::FieldMask; + + #[test] + fn root_child_basin_leaf_roundtrip_is_bitshift_exact() { + // basin 0x2 (say DOLCE Quality) → child 0x5 → child 0xA. + let p = NiblePath::root(0x2).child(0x5).child(0xA); + assert_eq!(p.depth(), 3); + assert_eq!( + p.basin(), + Some(0x2), + "basin = root nibble, stable down the path" + ); + assert_eq!(p.leaf(), Some(0xA), "leaf = deepest nibble"); + // parent walks back up exactly (bit-shift inverse of child). + let pp = p.parent().unwrap(); + assert_eq!(pp, NiblePath::root(0x2).child(0x5)); + assert_eq!(pp.leaf(), Some(0x5)); + assert_eq!(p.parent().unwrap().parent().unwrap(), NiblePath::root(0x2)); + assert_eq!(NiblePath::root(0x2).parent(), None, "basin has no parent"); + assert_eq!(NiblePath::EMPTY.basin(), None); + } + + #[test] + fn depth_caps_at_max_and_rejects_out_of_range_nibble() { + // Fill to MAX_DEPTH, then one more child is a no-op (not a wrap/overflow). + let mut p = NiblePath::root(0x1); + while p.depth() < MAX_DEPTH { + p = p.child(0xF); + } + assert_eq!(p.depth(), MAX_DEPTH); + assert_eq!( + p.child(0x3), + p, + "child past MAX_DEPTH is a no-op, never wraps" + ); + // Out-of-range nibble (>= FAN_OUT) is ignored, NOT folded onto a valid child. + assert_eq!(NiblePath::root(0x1).child(16), NiblePath::root(0x1)); + assert_eq!(NiblePath::root(0x1).child(99), NiblePath::root(0x1)); + } + + #[test] + fn is_ancestor_of_is_cheap_prefix_reachability() { + let mammal = NiblePath::root(0x0).child(0x3); // Endurant → …mammal + let bat = mammal.child(0x7); + let dog = mammal.child(0x8); + assert!(mammal.is_ancestor_of(bat), "mammal is an ancestor of bat"); + assert!(mammal.is_ancestor_of(dog)); + assert!( + mammal.is_ancestor_of(mammal), + "ancestor-or-EQUAL (reflexive)" + ); + assert!( + !bat.is_ancestor_of(mammal), + "child is not an ancestor of its parent" + ); + assert!( + !bat.is_ancestor_of(dog), + "siblings are not ancestors of each other" + ); + // A different basin shares no prefix. + let process = NiblePath::root(0x1).child(0x3); + assert!( + !mammal.is_ancestor_of(process), + "different basin → not reachable" + ); + assert!( + !NiblePath::EMPTY.is_ancestor_of(bat), + "empty path is an ancestor of nothing" + ); + } + + #[test] + fn multi_parent_is_a_facet_bit_not_a_second_path() { + // "Bat = mammal-path + flight-bit, not two paths" (wikidata-hhtl-load.md:46). + // ONE nibble path (the mammal Abstammung), the flight capability is an + // orthogonal facet bit in the SAME FieldMask — never a second NiblePath. + let bat_path = NiblePath::root(0x0).child(0x3).child(0x7); // mammal → bat + // declared mammal fields (positions 0,1,2) + the flight facet bit (40). + let mammal_mask = FieldMask::from_positions(&[0, 1, 2]); + let flight_facet = FieldMask::EMPTY.with(40); + let bat_mask = mammal_mask.inherit(flight_facet); + + assert_eq!(bat_path.depth(), 3, "bat is reached by ONE path, not two"); + assert!( + bat_mask.has(0) && bat_mask.has(1) && bat_mask.has(2), + "inherits mammal fields" + ); + assert!( + bat_mask.has(40), + "carries the flight facet bit in the same mask" + ); + assert_eq!(bat_mask.count(), 4); + } +} diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 9a87b812..bf6c6523 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -57,6 +57,7 @@ pub mod faculty; pub mod grammar; pub mod graph_render; pub mod hash; +pub mod hhtl; pub mod high_heel; pub mod jit; pub mod kanban; From b6e3725c039ffc67518c7b27e0aefc8934437f86 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 08:19:55 +0000 Subject: [PATCH 3/7] =?UTF-8?q?feat(ontology):=20D-WIKI-HHTL-2=20=E2=80=94?= =?UTF-8?q?=20wikidata=5Fhhtl=20N4=20falsifier=20(class-meta-DTO=20general?= =?UTF-8?q?ises=20to=20Wikidata)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The N4 second-domain falsifier (cognitive-risc-classes.md): routes a curated set of REAL Wikidata classes (Q5 human, Q215627 person, Q515 city, Q11424 film, Q5398426 tv-series, Q1656682 event) through the SAME machinery the D-CLS arc built for Odoo — proving the Wikidata D-CLS triple (class_id, shape_hash, presence_bitmask) = (ClassId, StructuralSignature, FieldMask) is domain-independent, not Odoo-overfit. Reuses (not re-grows): contract::hhtl::NiblePath (routing, basin = cache-resolved dolce_id u8, NO DOLCE enum on the path — OD-DOLCE), FieldMask (presence/facet-bitmask), StructuralSignature + contract::hash::fnv1a (shape-family key over the canonical property-set), and the #441 ClassView trait (WikidataClassView impls it unchanged). Falsification CONFIRMED on data (5 teeth-tests): corpus collapses to fewer shape-families (film ≡ tv-series share the AV-work shape), DOLCE→basin routing through the cache u8, triple shape domain-independent, ClassView resolves a Wikidata class with render_rows off-bits-skip (C2), subclass human⊂person inherits path (is_ancestor_of) + mask-as-delta. 245 ontology lib green; clippy -D warnings + fmt clean. Firewall preserved (hub side; aerial untouched). https://claude.ai/code/session_01R9AWgFa65uPnLyS2my2d2R --- .claude/board/STATUS_BOARD.md | 3 +- crates/lance-graph-ontology/src/lib.rs | 1 + .../lance-graph-ontology/src/wikidata_hhtl.rs | 351 ++++++++++++++++++ 3 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 crates/lance-graph-ontology/src/wikidata_hhtl.rs diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 7f0bf060..a0188e1b 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -573,7 +573,8 @@ Plan path: `.claude/plans/unified-soa-convergence-v1.md`. Handover `.claude/hand | D-CLS-SIG | `class_signature`: deterministic structural-signature audit of curated OdooEntity consts (FNV-1a over kind+field-hist+method-hist+state-machine) -> shape-family group-by + `object_view()` derives the real ObjectView bit-basis (fills the D-CLS-RES placeholder) | lance-graph-ontology | 230 | LOW | **Shipped** | #441 D-CLS; the HONEST D-CLS-3 (group-by-on-structural-hash, NOT aerial-cluster vaporware, classes.md:43); 4 teeth-tests over real l1 data | | D-CLS-AUDIT | `class_signature` corpus audit: `curated_entities()` (all 15 l-lanes, 64 consts) + `corpus_summary()` + falsifiable test that the real curated corpus collapses entities->fewer shape-families (classes.md:42 CONFIRMED on real data, not asserted) | lance-graph-ontology | 90 | LOW | **Shipped** | #441 D-CLS Wave-2 input; +clippy fix (unused FieldMask import in class_resolver) | | D-CLS-RENDER | `ClassView::render_rows` + `RenderRow{label,predicate}` — the off-bits-skipped render surface (C2 presence-only; template-agnostic, askama engine deferred to its own crate-Wave) | lance-graph-contract | 50 | LOW | **Shipped** | #441 D-CLS; the render LOGIC (classes.md:49), not the engine; +doc-lint fix | -| D-WIKI-HHTL-1 | `contract::hhtl::NiblePath`: the 16ⁿ Abstammung bucket router (subClassOf nibble path, bit-shift O(1), `root`/`child`/`basin`/`parent`/`is_ancestor_of`) + `FieldMask::inherit` (mask-inherits-as-delta). DOLCE-agnostic (`basin: u8` = dolce_id 0..3, resolved through the cache — NO enum); multi-parent = facet bit in the same mask, NOT a 2nd path. The downstream router #438 names. | lance-graph-contract | 155 | LOW | **In progress** | Wikidata-HHTL slice 1 = the N4 second-domain falsifier for the D-CLS machinery; reuses #441 FieldMask; convergent with D-ARM-14 (hub side, firewall preserved). 4 teeth-tests; 501 contract lib green. See FINDING D-CLS↔D-ARM-14 (EPIPHANIES). Deferred: the ontology cache-resolution seam (basin←DOLCE←cache, reuses classify+ClassView), the 115M streaming load. | +| D-WIKI-HHTL-1 | `contract::hhtl::NiblePath`: the 16ⁿ Abstammung bucket router (subClassOf nibble path, bit-shift O(1), `root`/`child`/`basin`/`parent`/`is_ancestor_of`) + `FieldMask::inherit` (mask-inherits-as-delta). DOLCE-agnostic (`basin: u8` = dolce_id 0..3, resolved through the cache — NO enum); multi-parent = facet bit in the same mask, NOT a 2nd path. The downstream router #438 names. | lance-graph-contract | 155 | LOW | **In PR** | Wikidata-HHTL slice 1 = the 16ⁿ router (hub-side); reuses #441 FieldMask; convergent with D-ARM-14 (firewall preserved). 4 teeth-tests; 501 contract lib green. See FINDING D-CLS↔D-ARM-14 (EPIPHANIES). | +| D-WIKI-HHTL-2 | `wikidata_hhtl`: the N4 second-domain falsifier — `WikidataClass` (curated real QIDs: human/person/city/film/tv-series/event) routed through the SAME class-meta-DTO: `nibble_path()` (basin=cache dolce_id, NO enum), `presence_mask()`=FieldMask, `signature()`=StructuralSignature over the canonical property-set, `dcls_triple()`=(ClassId,StructuralSignature,FieldMask), + `WikidataClassView` impls the #441 `ClassView`. | lance-graph-ontology | 290 | LOW | **In PR** | Wikidata-HHTL slice 2; classes.md N4 CONFIRMED on data: corpus collapses to fewer shape-families (film≡tv-series twin), triple shape domain-independent, ClassView resolves Wikidata unchanged, subclass inherits path+mask-as-delta. Reuses contract::hash::fnv1a. 5 teeth-tests; 245 ontology lib green. Deferred: the 115M streaming load (separate plan). | --- diff --git a/crates/lance-graph-ontology/src/lib.rs b/crates/lance-graph-ontology/src/lib.rs index 72363f7a..f9050598 100644 --- a/crates/lance-graph-ontology/src/lib.rs +++ b/crates/lance-graph-ontology/src/lib.rs @@ -49,6 +49,7 @@ pub mod registry; pub mod schema_source; pub mod semantic_types; pub mod ttl_parse; +pub mod wikidata_hhtl; #[cfg(feature = "lance-cache")] pub mod lance_cache; diff --git a/crates/lance-graph-ontology/src/wikidata_hhtl.rs b/crates/lance-graph-ontology/src/wikidata_hhtl.rs new file mode 100644 index 00000000..cc6ef683 --- /dev/null +++ b/crates/lance-graph-ontology/src/wikidata_hhtl.rs @@ -0,0 +1,351 @@ +//! # `wikidata_hhtl` — the N4 second-domain falsifier for the class-meta-DTO. +//! +//! `cognitive-risc-classes.md` N4: the class-meta-DTO (FieldMask presence + +//! StructuralSignature shape + ClassView resolution) must generalise BEYOND Odoo, +//! or it is overfit to one ERP. **Wikidata is the falsifier domain.** This module +//! routes a curated set of *real* Wikidata classes through the SAME machinery the +//! D-CLS arc (#441) built for Odoo — proving the Wikidata "D-CLS triple" +//! `(class_id, shape_hash, presence_bitmask)` = `(ClassId, StructuralSignature, +//! FieldMask)` is **domain-independent** (`wikidata-hhtl-load.md`:33). +//! +//! ## What is reused (not re-grown) +//! - **Routing:** [`NiblePath`](lance_graph_contract::hhtl::NiblePath) — the 16ⁿ +//! Abstammung bucket router (`subClassOf`/P279 path). basin = the cache-resolved +//! DOLCE id. +//! - **Presence:** [`FieldMask`](lance_graph_contract::class_view::FieldMask) — one +//! bit per present property-id (the "facet-bitmask" of `wikidata-hhtl-load.md`). +//! - **Shape:** [`StructuralSignature`](crate::odoo_blueprint::class_signature::StructuralSignature) +//! — the shape-family key, here a hash over the canonical property-id set (the +//! Wikidata analogue of Odoo's field/method histogram). Same type, same collapse +//! property (classes.md:42): structurally-identical classes dedup. +//! - **Resolution:** [`ClassView`](lance_graph_contract::class_view::ClassView) — +//! [`WikidataClassView`] impls it, so the contract trait resolves a Wikidata +//! class exactly as `RegistryClassView` resolves an Odoo one. +//! +//! ## Through-the-cache (OD-DOLCE, #441 `b31464d`) +//! Each class carries the cache-resolved **`dolce_id` u8** (0..3 = +//! [`dolce_id`](crate::class_resolver::dolce_id)), NOT a DOLCE enum — the basin is +//! that u8 directly. No DOLCE semantics live on the Wikidata path; the cache is the +//! authority. The 115M streaming load is a separate, deferred plan +//! (`wikidata-hhtl-load.md`). + +use std::collections::HashMap; + +use lance_graph_contract::class_view::{ClassId, ClassView, FieldMask}; +use lance_graph_contract::hash::fnv1a; +use lance_graph_contract::hhtl::NiblePath; +use lance_graph_contract::ontology::{DisplayTemplate, FieldRef}; + +use crate::class_resolver::dolce_id; +use crate::odoo_blueprint::class_signature::StructuralSignature; + +/// A curated Wikidata class — the minimal shape the N4 falsifier needs (the full +/// streaming load is deferred). `properties` are the *representative core* +/// property-id set, in declaration order (position `i` = stable [`FieldMask`] bit +/// `i`, N3 append-only) — not the exhaustive Wikidata property list. +#[derive(Debug, Clone)] +pub struct WikidataClass { + /// The class discriminator (reuses the #441 `ClassId` width — u16). + pub class_id: ClassId, + /// The Wikidata QID, e.g. `"Q5"` (human). + pub qid: &'static str, + /// The English label (resolved late in real loads; carried here for the fixture). + pub label: &'static str, + /// The **cache-resolved** DOLCE id (`0..3` = [`dolce_id`]). basin = this u8. + pub dolce_id: u8, + /// The P279 `subClassOf` nibble path BELOW the basin (each `0x0..=0xF`). + pub subclass_path: &'static [u8], + /// The present property-id set (P-numbers), declaration order = bit position. + pub properties: &'static [&'static str], +} + +impl WikidataClass { + /// The HHTL nibble path: basin (DOLCE) + the P279 `subClassOf` descent. This is + /// the entity's 16ⁿ bucket address (`wikidata-hhtl-load.md`:42). + #[must_use] + pub fn nibble_path(&self) -> NiblePath { + let mut p = NiblePath::root(self.dolce_id); + for &n in self.subclass_path { + p = p.child(n); + } + p + } + + /// The presence bitmask — bit `i` set for declared property `i` (the + /// facet-bitmask). Reuses [`FieldMask`] verbatim. + #[must_use] + pub fn presence_mask(&self) -> FieldMask { + let mut m = FieldMask::EMPTY; + for (i, _) in self.properties.iter().enumerate() { + m = m.with(i as u8); + } + m + } + + /// The structural signature (shape-family key) — a deterministic hash over the + /// DOLCE id + the **canonical (sorted, deduped) property-id set**. Label- and + /// QID-independent, so two structurally-identical classes collapse to one + /// family (classes.md:42, now on Wikidata). Reuses the canonical + /// [`fnv1a`](lance_graph_contract::hash::fnv1a); truncated to the + /// [`StructuralSignature`] `u32`. + #[must_use] + pub fn signature(&self) -> StructuralSignature { + let mut props: Vec<&str> = self.properties.to_vec(); + props.sort_unstable(); + props.dedup(); + let mut buf: Vec = Vec::with_capacity(1 + props.len() * 6); + buf.push(self.dolce_id); + for p in props { + buf.extend_from_slice(p.as_bytes()); + buf.push(0); // separator — keeps {P1,P23} distinct from {P12,P3} + } + StructuralSignature(fnv1a(&buf) as u32) + } + + /// The domain-independent **D-CLS triple** — identical in *shape* to the Odoo + /// one: `(class_id, shape_hash, presence_bitmask)`. This identity IS the N4 + /// falsification target. + #[must_use] + pub fn dcls_triple(&self) -> (ClassId, StructuralSignature, FieldMask) { + (self.class_id, self.signature(), self.presence_mask()) + } + + /// The class's fields as contract [`FieldRef`]s (property-id = `predicate_iri`; + /// label defaults to the property-id, resolved late from the cache). + fn field_refs(&self) -> Vec { + self.properties + .iter() + .map(|p| FieldRef::new(*p, *p)) + .collect() + } +} + +/// The curated Wikidata corpus — 6 real classes spanning two DOLCE basins, with +/// two genuine structural twins (film ≡ TV series share the core AV-work shape) +/// and one genuine subclass chain (human ⊂ person). Honest minimal data: the +/// collapse + inheritance are properties of the *shapes*, asserted on the corpus, +/// not stipulated. +#[must_use] +pub fn curated_wikidata_classes() -> Vec { + vec![ + WikidataClass { + class_id: 1, + qid: "Q215627", + label: "person", + dolce_id: dolce_id::ENDURANT, + subclass_path: &[0x1], + properties: &["P21", "P569", "P570"], // sex, birth, death + }, + WikidataClass { + class_id: 2, + qid: "Q5", + label: "human", + dolce_id: dolce_id::ENDURANT, + subclass_path: &[0x1, 0x2], // person → human (IS-A; path extends) + properties: &["P21", "P569", "P570", "P106"], // + occupation (the delta) + }, + WikidataClass { + class_id: 3, + qid: "Q515", + label: "city", + dolce_id: dolce_id::ENDURANT, + subclass_path: &[0x3], + properties: &["P1082", "P625", "P17"], // population, coords, country + }, + WikidataClass { + class_id: 4, + qid: "Q11424", + label: "film", + dolce_id: dolce_id::PERDURANT, + subclass_path: &[0x0], + properties: &["P57", "P577", "P161"], // director, pubdate, cast + }, + WikidataClass { + class_id: 5, + qid: "Q5398426", + label: "television series", + dolce_id: dolce_id::PERDURANT, + subclass_path: &[0x1], + properties: &["P57", "P577", "P161"], // SAME core AV-work shape as film + }, + WikidataClass { + class_id: 6, + qid: "Q1656682", + label: "event", + dolce_id: dolce_id::PERDURANT, + subclass_path: &[0x2], + properties: &["P585", "P276"], // point-in-time, location + }, + ] +} + +/// Group the corpus into shape-families by [`StructuralSignature`] — the discovered +/// taxonomy (classes.md:43), now over Wikidata. Returns `(signature → member QIDs)`, +/// sorted by signature for deterministic output. +#[must_use] +pub fn shape_families(classes: &[WikidataClass]) -> Vec<(StructuralSignature, Vec<&'static str>)> { + use std::collections::BTreeMap; + let mut fams: BTreeMap> = BTreeMap::new(); + for c in classes { + fams.entry(c.signature().0).or_default().push(c.qid); + } + fams.into_iter() + .map(|(k, v)| (StructuralSignature(k), v)) + .collect() +} + +/// The ontology-side [`ClassView`] over a Wikidata corpus — the Wikidata analogue +/// of [`RegistryClassView`](crate::class_resolver::RegistryClassView). Proves the +/// #441 contract trait resolves a Wikidata class with no change. +pub struct WikidataClassView { + fields: HashMap>, + dolce: HashMap, + empty: Vec, +} + +impl WikidataClassView { + /// Build the view over a curated corpus. + #[must_use] + pub fn new(classes: &[WikidataClass]) -> Self { + let mut fields = HashMap::new(); + let mut dolce = HashMap::new(); + for c in classes { + fields.insert(c.class_id, c.field_refs()); + dolce.insert(c.class_id, c.dolce_id); + } + Self { + fields, + dolce, + empty: Vec::new(), + } + } +} + +impl ClassView for WikidataClassView { + fn fields(&self, class: ClassId) -> &[FieldRef] { + self.fields + .get(&class) + .map_or(self.empty.as_slice(), |v| v.as_slice()) + } + + fn template(&self, class: ClassId) -> DisplayTemplate { + // Same size heuristic as Odoo's `object_view` (≤4 fields → Card). + match self.fields.get(&class) { + Some(f) if f.len() <= 4 => DisplayTemplate::Card, + _ => DisplayTemplate::Detail, + } + } + + fn dolce_category_id(&self, class: ClassId) -> u8 { + // Resolved from the cache (carried as the opaque u8); default Endurant. + self.dolce + .get(&class) + .copied() + .unwrap_or(dolce_id::ENDURANT) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn by_qid(qid: &str) -> WikidataClass { + curated_wikidata_classes() + .into_iter() + .find(|c| c.qid == qid) + .unwrap() + } + + #[test] + fn dolce_resolves_to_basin_through_the_cache_u8() { + // basin = the cache-resolved dolce_id; the router never embeds DOLCE. + assert_eq!(by_qid("Q5").nibble_path().basin(), Some(dolce_id::ENDURANT)); + assert_eq!( + by_qid("Q515").nibble_path().basin(), + Some(dolce_id::ENDURANT) + ); + assert_eq!( + by_qid("Q11424").nibble_path().basin(), + Some(dolce_id::PERDURANT) + ); + assert_eq!( + by_qid("Q1656682").nibble_path().basin(), + Some(dolce_id::PERDURANT) + ); + } + + #[test] + fn corpus_collapses_to_fewer_shape_families() { + // The N4 falsification: the class-meta-DTO's collapse property (classes.md:42) + // holds on Wikidata, not just Odoo — film ≡ TV series share the AV-work shape. + let corpus = curated_wikidata_classes(); + let families = shape_families(&corpus); + assert!( + families.len() < corpus.len(), + "corpus must collapse to FEWER shape-families ({} classes → {} families)", + corpus.len(), + families.len() + ); + // film (Q11424) and TV series (Q5398426) are the genuine twins. + assert_eq!( + by_qid("Q11424").signature(), + by_qid("Q5398426").signature(), + "film and TV series share one structural signature" + ); + // human carries an extra property → it is NOT person's twin. + assert_ne!(by_qid("Q5").signature(), by_qid("Q215627").signature()); + } + + #[test] + fn dcls_triple_shape_is_domain_independent() { + // The Wikidata triple is the SAME (ClassId, StructuralSignature, FieldMask) + // as Odoo's — the identity that proves the meta-DTO is not Odoo-overfit. + let human = by_qid("Q5"); + let (id, sig, mask) = human.dcls_triple(); + assert_eq!(id, 2); + assert_eq!(sig, human.signature()); + assert_eq!( + mask.count(), + human.properties.len() as u32, + "one present bit per property" + ); + assert!(mask.has(0) && mask.has(3) && !mask.has(4)); + } + + #[test] + fn classview_resolves_a_wikidata_class_unchanged() { + // The #441 ClassView trait resolves a Wikidata class with no modification. + let corpus = curated_wikidata_classes(); + let view = WikidataClassView::new(&corpus); + assert_eq!(view.dolce_category_id(2), dolce_id::ENDURANT); // human + assert_eq!(view.field_count(2), 4); + assert_eq!(view.template(2), DisplayTemplate::Card); + assert_eq!(view.template(1), DisplayTemplate::Card); // person, 3 fields + // render_rows skips off-bits (C2 presence-only): mask with only bit 0 + bit 3. + let mask = FieldMask::EMPTY.with(0).with(3); + let rows = view.render_rows(2, mask); + assert_eq!(rows.len(), 2, "only the two set bits render"); + assert_eq!(rows[0].predicate, "P21"); + assert_eq!(rows[1].predicate, "P106"); + } + + #[test] + fn subclass_inherits_path_and_mask_as_delta() { + // human ⊂ person: the P279 path extends AND the presence mask inherits. + let person = by_qid("Q215627"); + let human = by_qid("Q5"); + assert!( + person.nibble_path().is_ancestor_of(human.nibble_path()), + "person's nibble path is a prefix of human's (IS-A reachability)" + ); + // human's mask = person's mask inheriting the occupation (P106) delta bit. + let delta = FieldMask::EMPTY.with(3); // P106 is human's 4th property (bit 3) + assert_eq!(person.presence_mask().inherit(delta), human.presence_mask()); + // basin (DOLCE) is preserved down the subclass path. + assert_eq!( + person.nibble_path().basin(), + human.nibble_path().basin(), + "subclassing never changes the DOLCE basin" + ); + } +} From b82624906fe590e7615aa29b285779b86b332e64 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 08:27:39 +0000 Subject: [PATCH 4/7] fix(ontology): cap WikidataClass fields to FieldMask width (Codex P2 #442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A WikidataClass with > 64 properties overflowed the FieldMask contract: field_count() exceeded MAX_FIELDS, presence_mask() covered only the first 64, render_rows() iterated the longer field list, and i as u8 wrapped past 256 — aliasing a high property onto a low bit. Now both presence_mask() and field_refs() cap at FieldMask::MAX_FIELDS (the same discipline as the Odoo object_view() path), and the presence loop runs i in 0..min(len,64) so i as u8 can never wrap. Teeth-test: a 70-property class -> presence/field_count cap at 64, render yields 64 rows not 70, property 64+ never aliases onto a low bit. 6 wikidata_hhtl tests; clippy -D warnings + fmt clean. https://claude.ai/code/session_01R9AWgFa65uPnLyS2my2d2R --- .../lance-graph-ontology/src/wikidata_hhtl.rs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/lance-graph-ontology/src/wikidata_hhtl.rs b/crates/lance-graph-ontology/src/wikidata_hhtl.rs index cc6ef683..d87f6e46 100644 --- a/crates/lance-graph-ontology/src/wikidata_hhtl.rs +++ b/crates/lance-graph-ontology/src/wikidata_hhtl.rs @@ -76,7 +76,11 @@ impl WikidataClass { #[must_use] pub fn presence_mask(&self) -> FieldMask { let mut m = FieldMask::EMPTY; - for (i, _) in self.properties.iter().enumerate() { + // Cap at MAX_FIELDS: positions beyond 64 are not addressable by the `u64` + // mask (the bit-budget escape is a ref, deferred). `i < 64` here, so the + // `i as u8` can never wrap (Codex P2 #442 — no aliasing onto low bits). + let cap = (FieldMask::MAX_FIELDS as usize).min(self.properties.len()); + for i in 0..cap { m = m.with(i as u8); } m @@ -113,8 +117,13 @@ impl WikidataClass { /// The class's fields as contract [`FieldRef`]s (property-id = `predicate_iri`; /// label defaults to the property-id, resolved late from the cache). fn field_refs(&self) -> Vec { + // Cap at MAX_FIELDS so `field_count() <= FieldMask::MAX_FIELDS` (the + // ClassView contract) and `render_rows` never iterates past the + // addressable mask — the same discipline as the Odoo `object_view()` path + // (Codex P2 #442). self.properties .iter() + .take(FieldMask::MAX_FIELDS as usize) .map(|p| FieldRef::new(*p, *p)) .collect() } @@ -348,4 +357,37 @@ mod tests { "subclassing never changes the DOLCE basin" ); } + + #[test] + fn oversized_class_caps_to_fieldmask_width() { + // A class with > MAX_FIELDS properties must cap — no `usize`→`u8` wraparound + // aliasing, field_count within the ClassView contract (Codex P2 #442), the + // same discipline as Odoo's `object_view()`. + let many: Vec<&'static str> = (0..70) + .map(|i| &*Box::leak(format!("P{i}").into_boxed_str())) + .collect(); + let props: &'static [&'static str] = Box::leak(many.into_boxed_slice()); + let big = WikidataClass { + class_id: 99, + qid: "Qbig", + label: "big", + dolce_id: dolce_id::ENDURANT, + subclass_path: &[], + properties: props, + }; + assert_eq!( + big.presence_mask().count(), + FieldMask::MAX_FIELDS, + "presence mask caps at MAX_FIELDS — property 64+ never wraps onto a low bit" + ); + let view = WikidataClassView::new(std::slice::from_ref(&big)); + assert_eq!( + view.field_count(99), + FieldMask::MAX_FIELDS as usize, + "field_count <= MAX_FIELDS (the ClassView contract)" + ); + // Rendering the full mask yields exactly MAX_FIELDS rows, not 70. + let rows = view.render_rows(99, big.presence_mask()); + assert_eq!(rows.len(), FieldMask::MAX_FIELDS as usize); + } } From 5ad7c68b596b568ca3799cfdc1758f5d4946918c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 08:31:22 +0000 Subject: [PATCH 5/7] =?UTF-8?q?fix(contract):=20CodeRabbit=20#442=20?= =?UTF-8?q?=E2=80=94=20root()=20rejects=20out-of-range=20basin=20(no=20sil?= =?UTF-8?q?ent=20fold)=20+=20focused=20FieldMask::inherit=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two valid CodeRabbit findings: 1. (Major) NiblePath::root() folded an out-of-range basin via (basin & 0x0F), silently aliasing e.g. 16 -> basin 0 and misrouting ancestry. Now basin >= FAN_OUT returns Self::EMPTY (the 'no route' sentinel; basin() -> None) — consistent with child()'s out-of-range no-op and FieldMask's ignore-don't-fold discipline. Valid basins (0..15, dolce_id 0..3) are unaffected. Test asserts root(16)/root(99) == EMPTY, no alias to basin 0. 2. (Minor) FieldMask::inherit had only transitive coverage; added a focused unit test in class_view.rs: inherit == bitwise union, overlap not double-counted, EMPTY identity both directions, commutative, operands unmutated. 502 contract lib green; clippy -D warnings + fmt clean. https://claude.ai/code/session_01R9AWgFa65uPnLyS2my2d2R --- crates/lance-graph-contract/src/class_view.rs | 27 +++++++++++++++++++ crates/lance-graph-contract/src/hhtl.rs | 26 ++++++++++++++---- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/crates/lance-graph-contract/src/class_view.rs b/crates/lance-graph-contract/src/class_view.rs index 5532e087..4fc8b846 100644 --- a/crates/lance-graph-contract/src/class_view.rs +++ b/crates/lance-graph-contract/src/class_view.rs @@ -312,6 +312,33 @@ mod tests { ); } + #[test] + fn field_mask_inherit_is_nondestructive_union() { + // inherit = bitwise OR — a child IS-A its parent: it carries the parent's + // present fields PLUS its own delta (focused cover, CodeRabbit #442). + let parent = FieldMask::from_positions(&[0, 2]); + let delta = FieldMask::from_positions(&[1, 2]); // bit 2 overlaps + let child = parent.inherit(delta); + assert_eq!( + child, + FieldMask(parent.0 | delta.0), + "inherit is the bitwise union" + ); + assert!(child.has(0) && child.has(1) && child.has(2)); + assert_eq!( + child.count(), + 3, + "the overlapping bit is not double-counted" + ); + // EMPTY is the identity, both directions; the union is commutative. + assert_eq!(parent.inherit(FieldMask::EMPTY), parent); + assert_eq!(FieldMask::EMPTY.inherit(parent), parent); + assert_eq!(parent.inherit(delta), delta.inherit(parent), "commutative"); + // FieldMask is Copy — neither operand is mutated by inherit. + assert_eq!(parent, FieldMask::from_positions(&[0, 2])); + assert_eq!(delta, FieldMask::from_positions(&[1, 2])); + } + #[test] fn meta_dto_projects_above_agnostic_class_mask() { let classes = FakeClasses::new(); diff --git a/crates/lance-graph-contract/src/hhtl.rs b/crates/lance-graph-contract/src/hhtl.rs index 182ab110..a755e1b3 100644 --- a/crates/lance-graph-contract/src/hhtl.rs +++ b/crates/lance-graph-contract/src/hhtl.rs @@ -63,13 +63,20 @@ impl NiblePath { pub const EMPTY: Self = Self { path: 0, depth: 0 }; /// Start a path at a `basin` nibble — the DOLCE top facet, resolved UPSTREAM - /// through the ontology cache (not decided here). The low nibble is taken - /// (`basin & 0x0F`); callers pass `0x0..=0xF`. + /// through the ontology cache (not decided here). An out-of-range + /// `basin >= FAN_OUT` (16) returns [`EMPTY`](NiblePath::EMPTY), the "no route" + /// sentinel — NOT a silent fold onto a valid basin (which would misroute + /// ancestry — CodeRabbit #442). Mirrors [`child`](NiblePath::child)'s + /// out-of-range no-op and `FieldMask`'s ignore-don't-fold discipline. #[must_use] pub const fn root(basin: u8) -> Self { - Self { - path: (basin & 0x0F) as u64, - depth: 1, + if basin >= FAN_OUT { + Self::EMPTY + } else { + Self { + path: basin as u64, + depth: 1, + } } } @@ -191,6 +198,15 @@ mod tests { // Out-of-range nibble (>= FAN_OUT) is ignored, NOT folded onto a valid child. assert_eq!(NiblePath::root(0x1).child(16), NiblePath::root(0x1)); assert_eq!(NiblePath::root(0x1).child(99), NiblePath::root(0x1)); + // root() rejects an out-of-range basin to EMPTY — never folds 16 → basin 0 + // (which would misroute ancestry; CodeRabbit #442). + assert_eq!(NiblePath::root(16), NiblePath::EMPTY); + assert_eq!(NiblePath::root(99), NiblePath::EMPTY); + assert_eq!( + NiblePath::root(16).basin(), + None, + "bad basin must not alias to basin 0" + ); } #[test] From 85963fe4f7a9a792e2297800a85d34ef2dfb1cb6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 08:41:41 +0000 Subject: [PATCH 6/7] feat(hhtl): signal NiblePath depth-exhaustion (is_full/try_child) + record #442 scale-freezes (D-ARM-14 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The D-ARM-14 session's review of #442 (merge-worthy verdict) flagged two non-blocking 'name the freeze' items; acting on both: Flag 1 (the strong one) — NiblePath::child() saturated SILENTLY past MAX_DEPTH=16, so two distinct deep P279 chains truncated at 16 would collide on one path (is_ancestor_of/basin treat them as one). Same no-silent-aliasing class as the root()/FieldMask fixes. Added is_full() (the depth_exhausted flag) + try_child() -> Option (returns None instead of saturating), so the deferred 115M loader DETECTS the ceiling and switches to a ref instead of colliding. child() stays the saturating convenience, now documented to gate on is_full(). Flag 2 + minors — recorded as TD-WIKI-SCALE (TECH_DEBT, append-only): StructuralSignature u32 birthday ceiling (~77k shape-families — a #441 contract decision to widen to u64, WITH the deferred loader, not unilaterally here); signature() per-call Vec alloc; dolce_category_id default-ENDURANT-on-unknown. All fine at curated/Odoo scale; bite only at the 115M load. signature() doc now cites the u32 freeze. 503 contract + 246 ontology lib green; clippy -D warnings + fmt clean. Firewall preserved; the proposer-side dolce_id alignment remains the D-ARM-14 session's lane. https://claude.ai/code/session_01R9AWgFa65uPnLyS2my2d2R --- .claude/board/TECH_DEBT.md | 26 +++++++ crates/lance-graph-contract/src/hhtl.rs | 69 +++++++++++++++++-- .../lance-graph-ontology/src/wikidata_hhtl.rs | 6 ++ 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/.claude/board/TECH_DEBT.md b/.claude/board/TECH_DEBT.md index bbe29c5c..1027fec1 100644 --- a/.claude/board/TECH_DEBT.md +++ b/.claude/board/TECH_DEBT.md @@ -13,6 +13,32 @@ --- +### TD-WIKI-SCALE (D-WIKI-HHTL / #442) — three scale-freezes the N4 falsifier inherits + +**Status: Open.** Surfaced by the D-ARM-14 session's review of #442 (the hub-side +Wikidata-HHTL arc). All three are FINE at the curated-corpus + Odoo scale this PR +ships; they bite only at the deferred 115M streaming load: + +1. **`StructuralSignature` u32 birthday ceiling (~77k).** `wikidata_hhtl::WikidataClass::signature()` + and Odoo's `class_signature` both return `StructuralSignature(u32)` (#441's type). ~50% collision + probability near ~77k distinct shape-families → two genuinely different shapes alias to one family + (a correctness MERGE, not a perf nit) at Wikidata scale. **Pay by:** widen `StructuralSignature` + to `u64` — a #441 contract decision (touches the Odoo signature path too); land it WITH the + deferred load slice, not unilaterally in #442. +2. **`NiblePath` MAX_DEPTH=16 ceiling — now SIGNALED, not silent.** `child()` saturates silently past + depth 16; real P279 chains run deeper, so two distinct deep classes truncated at 16 would collide on + one path. #442 adds `is_full()` + `try_child() -> Option` so the deferred loader DETECTS the ceiling + and switches to a ref (the bit-budget escape) instead of colliding. **Pay by:** the deferred loader + gates every descent on `is_full()`/`try_child()`; the ref-escape store is its own slice. +3. **`signature()` allocates+sorts a `Vec` per call; `dolce_category_id()` defaults `ENDURANT` on + unknown class_id** (silent default vs signaling absence — mirrors `RegistryClassView`). Both fine + for the fixture. **Pay by:** at load scale hash the property-set without a per-call alloc + (pre-sorted columnar input); decide whether unknown-class DOLCE should be `Option`-signaled (a #441 + `ClassView` trait decision). + +Cross-ref: #442, #441 (StructuralSignature/ClassView), the D-ARM-14 review, `wikidata-hhtl-load.md`, +FINDING D-CLS↔D-ARM-14 (EPIPHANIES). + ### TD-ARM-CARRIER-FORK (D-ARM-13 / streaming-arm-nars-discovery-v1) **Status: Open.** Surfaced by the 3-savant brutal review of D-ARM-13 diff --git a/crates/lance-graph-contract/src/hhtl.rs b/crates/lance-graph-contract/src/hhtl.rs index a755e1b3..9cf1bb9e 100644 --- a/crates/lance-graph-contract/src/hhtl.rs +++ b/crates/lance-graph-contract/src/hhtl.rs @@ -80,10 +80,15 @@ impl NiblePath { } } - /// Route one level deeper to child `nibble`. Returns `self` UNCHANGED once - /// [`MAX_DEPTH`] is reached (the path is full — deeper addressing needs a ref) - /// or `nibble >= FAN_OUT` (out of range — never folded onto a valid child, - /// mirroring [`FieldMask`](crate::class_view::FieldMask)'s out-of-range discipline). + /// Route one level deeper to child `nibble`. **Saturating:** returns `self` + /// UNCHANGED once [`MAX_DEPTH`] is reached or `nibble >= FAN_OUT` (out of range — + /// never folded onto a valid child, mirroring + /// [`FieldMask`](crate::class_view::FieldMask)'s out-of-range discipline). + /// + /// At [`MAX_DEPTH`] the silent saturation means two *distinct* deeper paths would + /// collide on this address — so a real-scale caller MUST gate on + /// [`is_full`](NiblePath::is_full) or use [`try_child`](NiblePath::try_child), + /// which signal the ceiling instead of colliding (D-ARM-14 review of #442). #[must_use] pub const fn child(self, nibble: u8) -> Self { if self.depth >= MAX_DEPTH || nibble >= FAN_OUT { @@ -96,6 +101,33 @@ impl NiblePath { } } + /// Has this path reached [`MAX_DEPTH`] — i.e. [`child`](NiblePath::child) can no + /// longer descend within the `u64`? When `true`, the bit-budget discipline + /// (`wikidata-hhtl-load.md`:71 "grows unbounded → path/ref") says switch to a + /// ref for deeper addressing: descending anyway via [`child`] is a SILENT no-op, + /// so two distinct deeper classes would collide on this same path. The deferred + /// 115M loader gates each descent on this (D-ARM-14 review of #442). + #[must_use] + pub const fn is_full(self) -> bool { + self.depth >= MAX_DEPTH + } + + /// Route one level deeper, returning `None` instead of silently saturating when + /// the path [`is_full`](NiblePath::is_full) or `nibble >= FAN_OUT`. The explicit + /// counterpart to [`child`](NiblePath::child) for callers that must NOT collide + /// distinct deep paths (the real-scale loader). + #[must_use] + pub const fn try_child(self, nibble: u8) -> Option { + if self.depth >= MAX_DEPTH || nibble >= FAN_OUT { + None + } else { + Some(Self { + path: (self.path << 4) | (nibble as u64), + depth: self.depth + 1, + }) + } + } + /// The basin (root) nibble — the DOLCE top facet this path lives under. /// `None` for the empty path. #[must_use] @@ -262,4 +294,33 @@ mod tests { ); assert_eq!(bat_mask.count(), 4); } + + #[test] + fn is_full_and_try_child_signal_depth_exhaustion() { + // child() saturates silently at MAX_DEPTH; is_full()/try_child() expose the + // ceiling so the deferred loader switches to a ref instead of colliding two + // distinct deep paths (D-ARM-14 review of #442). + let mut p = NiblePath::root(0x1); + assert!(!p.is_full()); + while !p.is_full() { + p = p.try_child(0xF).expect("descends while not full"); + } + assert_eq!(p.depth(), MAX_DEPTH); + assert!(p.is_full()); + assert_eq!( + p.try_child(0x2), + None, + "try_child signals exhaustion, not a silent collision" + ); + assert_eq!( + p.child(0x2), + p, + "child() still saturates (the convenience path)" + ); + assert_eq!( + NiblePath::root(0x1).try_child(16), + None, + "out-of-range nibble is None too" + ); + } } diff --git a/crates/lance-graph-ontology/src/wikidata_hhtl.rs b/crates/lance-graph-ontology/src/wikidata_hhtl.rs index d87f6e46..90609fc1 100644 --- a/crates/lance-graph-ontology/src/wikidata_hhtl.rs +++ b/crates/lance-graph-ontology/src/wikidata_hhtl.rs @@ -92,6 +92,12 @@ impl WikidataClass { /// family (classes.md:42, now on Wikidata). Reuses the canonical /// [`fnv1a`](lance_graph_contract::hash::fnv1a); truncated to the /// [`StructuralSignature`] `u32`. + /// + /// **Scale freeze (TD-WIKI-SCALE):** `StructuralSignature` is `u32` — ~50% + /// birthday-collision near ~77k distinct shape-families. Safe for the curated + /// corpus + Odoo; at full Wikidata load scale, widening to `u64` is a #441 + /// contract decision (flagged by the D-ARM-14 review of #442), to land WITH the + /// deferred loader, not unilaterally here. #[must_use] pub fn signature(&self) -> StructuralSignature { let mut props: Vec<&str> = self.properties.to_vec(); From 809c16233341966069df628d44ec3d7f8f0ef283 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 09:13:38 +0000 Subject: [PATCH 7/7] docs(board): add '## Open Debt' h2 to TECH_DEBT (MD001 heading-increment, CodeRabbit #442) The file jumped h1 -> h3 (all debt entries are ### by convention), tripping markdownlint MD001 on the first entry. Promoting only my entry to h2 would make it an outlier vs the other ### entries; instead added one '## Open Debt' section header before the first entry, which silences MD001 file-wide AND keeps every entry consistent at h3. Pre-existing condition (inherited the first-h3 position on prepend), fixed minimally. https://claude.ai/code/session_01R9AWgFa65uPnLyS2my2d2R --- .claude/board/TECH_DEBT.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.claude/board/TECH_DEBT.md b/.claude/board/TECH_DEBT.md index 1027fec1..0947d6cd 100644 --- a/.claude/board/TECH_DEBT.md +++ b/.claude/board/TECH_DEBT.md @@ -13,6 +13,8 @@ --- +## Open Debt + ### TD-WIKI-SCALE (D-WIKI-HHTL / #442) — three scale-freezes the N4 falsifier inherits **Status: Open.** Surfaced by the D-ARM-14 session's review of #442 (the hub-side