From e5995c7f70538a5c72a259ef2ab6bb2ed6e02ca9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 09:55:10 +0000 Subject: [PATCH 1/5] =?UTF-8?q?feat(arm-discovery):=20D-ARM-14=20Phase=202?= =?UTF-8?q?=20=E2=80=94=20proposer=E2=86=92hub=20landing=20(dolce=5Fid=20e?= =?UTF-8?q?mit=20+=20worked=20Wikidata=20example)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Answers "how to use aerial + the 10000^2 splat + land on Wikidata-shaped HHTL": builds the proposer->hub seam end-to-end, verifiable standalone. (a) The OD-DOLCE alignment #442 deferred to this lane: - OntologyProjector::dolce_id() emits the stable dolce_id u8 (= basin nibble, already matching dolce_id::{ENDURANT=0,..}) — the proposer hands the hub the enum-free routing key; the IRI becomes a late-resolvable label (resolve-through-cache). + DolceCategory::from_index, + is_dolce flag. (b) The worked end-to-end example (tests/wikidata_landing.rs, --features landing): - splat top-k -> extract_rules recovers all 6 DOLCE basins -> lands each on the REAL lance-graph-contract::class_view::FieldMask (presence) + hash::fnv1a_str (the StructuralSignature value). The 16^n NiblePath router is inlined (annotated) until #442's contract::hhtl lands on main. - CONFIRMED on data: corpus collapses 6->5 shape-families (film Q11424 ≡ tv-series Q5398426, sig 0xad7fade7); human ⊂ person inherits path + mask-as-delta; basin preserved down the subclass path. - Firewall intact: the lib never imports the hub. The example uses an opt-in dev-dep on the zero-dep lance-graph-contract (the exact pattern jc uses for bridge examples), gated behind the `landing` feature so the default build stays zero-dep. Only the (ClassId, signature, FieldMask) triple + dolce_id u8 cross the seam. Verified: default (zero-dep) 42/42 + clippy -D warnings clean; --features landing -> wikidata_landing green + clippy clean. Run: cargo test --manifest-path crates/lance-graph-arm-discovery/Cargo.toml \ --features landing --test wikidata_landing -- --nocapture Remaining (documented): the real jc/blasgraph splat producing the lists; the ndjson->WikidataClass loader; swap the inline nibble routing for contract::hhtl::NiblePath once #442 merges; gated on D-ARM-7 (jc::jirak). https://claude.ai/code/session_012SorR8UbtEvYmbX8cXftj7 --- .claude/board/AGENT_LOG.md | 10 + .claude/board/STATUS_BOARD.md | 2 +- crates/lance-graph-arm-discovery/Cargo.toml | 12 ++ .../src/aerial/ontology.rs | 58 ++++- .../tests/wikidata_landing.rs | 200 ++++++++++++++++++ 5 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 crates/lance-graph-arm-discovery/tests/wikidata_landing.rs diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index 7e83afc5..dfc65a54 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,13 @@ +## [Main thread / Opus] D-ARM-14 Phase 2 — proposer→hub landing (dolce_id emit + worked Wikidata example) + +**Branch:** claude/jolly-cori-clnf9-darm14-p2 (off main a77e119) | **Files:** `crates/lance-graph-arm-discovery/src/aerial/ontology.rs` (+`OntologyProjector::dolce_id`, `DolceCategory::from_index`, `is_dolce`), `Cargo.toml` (+`landing` feature + optional `lance-graph-contract` dev-dep), `tests/wikidata_landing.rs` (NEW, gated) + STATUS_BOARD (D-ARM-14 Phase 2). + +**Cargo:** DEFAULT (zero-dep) → **42/42** + clippy `-D warnings` clean. `--features landing` → the `wikidata_landing` worked example green + clippy clean (lands on REAL `lance-graph-contract` types). + +**Outcome:** Phase 2 DONE. User: "how to use aerial + the 10000² splat + add the ontology to land on Wikidata-shaped HHTL?" → built the answer. **(a) The OD-DOLCE alignment #442 deferred to my lane:** `OntologyProjector::dolce_id()` emits the stable `dolce_id` u8 (= basin nibble, already matching `dolce_id::{ENDURANT=0,..}`) — the proposer hands the hub the enum-free routing key, the IRI becomes a late-resolvable label (resolve-through-cache). **(b) The worked end-to-end example** (`tests/wikidata_landing.rs`, `--features landing`, opt-in `dev-dep lance-graph-contract` exactly like jc's bridge examples — lib stays zero-dep): splat top-k → `extract_rules` recovers all 6 DOLCE basins → lands each on the REAL `contract::class_view::FieldMask` (presence) + `hash::fnv1a_str` (StructuralSignature value); `NiblePath` 16ⁿ routing inlined (annotated, swap on #442 merge since contract::hhtl isn't on main yet). CONFIRMED on data: corpus collapses 6→5 families (film Q11424 ≡ tv Q5398426, sig 0xad7fade7), human⊂person inherits path + mask-as-delta, basin preserved down the subclass path. Respects the firewall (lib never imports the hub; the test bridges both to prove the `(ClassId, signature, FieldMask)` triple + `dolce_id` u8 seam). NOT pushed yet — awaiting confirm (prior branch merged). Map: `splat-codebook-aerial-wikidata-compression.md`. + +--- + ## [Main thread / Opus 4.7] odoo-classes-bitmask-render-v1 — authored bounded-weekend plan + 10-agent A2A wave split (pre-council) **Branch:** claude/activate-lance-graph-att-k2pHI | **Files (additive only):** diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index a0188e1b..e7a2c111 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -645,7 +645,7 @@ The missing UPSTREAM discovery leg. Today's proposers (curated L-docs + AST-extr | D-ARM-11 | `style_recipe.rs` rule 8 — ArmDiscovered backing adds `DAtom::Compute` weight 2 (provisional) | lance-graph-ontology::style_recipe | 80 | MED | **Queued** | depends on D-ARM-1 | | D-ARM-12 | End-to-end pipeline test + bench (synthetic Odoo feed → all 5 stages → council micro-batch) | lance-graph-arm-discovery::tests + benches | 400 | MED | **Queued** | depends on Waves 1-6; informs OQ-ARM-2 + OQ-ARM-7 | | D-ARM-13 | **Aerial+ Rust transcode — deterministic codebook-probe backend** (float-free). The paper's `f32` denoising autoencoder is REPLACED by an integer `CodebookDistance` oracle (palette256 distance, ρ=0.9973 vs cosine): the reconstruction probe is a codebook top-k, not a softmax over float weights. Integer evidence counts + ppm gates + `TruthU8` (= CausalEdge64 wire). `AerialProposer` impl of `Proposer`. Count loop is a row-bitset SoA (`RowMasks`) → AND+popcount, routed through `ndarray::simd::U64x8` under the `ndarray-simd` feature. | lance-graph-arm-discovery::aerial | ~1.1K | HIGH | **Shipped (branch)** | branch `claude/jolly-cori-clnf9`; standalone zero-dep crate (excluded); **33/33** tests + clippy `-D warnings` clean on BOTH default (scalar) and `--features ndarray-simd`; **zero f32 in the discovery path** (audit), float only at the `TruthValue`/`Triple` serialization edge. Bitwise-deterministic ⇒ joins the trunk; the nondeterminism firewall + D-ARM-9 IPC rationale are moot. SIMD target-cpu caveat: real AVX-512/AMX kernels need `-C target-cpu=native`/`x86-64-v4`. v1 (autoencoder) superseded per the user's no-float directive. | -| D-ARM-14 | **Splat-codebook oracle + Wikidata skeleton discovery** — wire the certified jc splat codebook into aerial as the `CodebookDistance` oracle, discover OWL/DOLCE+ SPO HHTL classes + basins, drive the `wikidata-hhtl-load.md` deterministic compression (skeleton + basins + CAM-dedup + thin rows). | lance-graph-arm-discovery::aerial + crates/jc + wikidata loader | ~? | MED | **In progress** | **Phase 1 (branch `claude/jolly-cori-clnf9-darm14`):** the two aerial-side seams — `aerial::TopKDistance` (the sparse splat-top-k `CodebookDistance` the 10000² BLASGraph splat actually emits; top-k per node, not a dense table) + `aerial::ontology::{DolceCategory, OntologyProjector}` (DOLCE 4-facet skeleton → `rdfs:subClassOf`/`rdf:type` SPO). End-to-end test: splat top-k → aerial discovers `occupation→DOLCE-class` → projects the skeleton triple. 41/41 + clippy clean (default + `ndarray-simd`), zero-dep. Float still OFFLINE in jc only (`ewa_sandwich`+`sigma_codebook_probe` ρ=0.9973+`pflug` Lε); aerial online path integer. **Remaining:** real jc/blasgraph splat producing the lists; Wikidata loader; gated on D-ARM-7 (`jc::jirak`). Map: `splat-codebook-aerial-wikidata-compression.md`; E-ARM-JC-RESOLVES-BOTH-SEAMS. | +| D-ARM-14 | **Splat-codebook oracle + Wikidata skeleton discovery** — wire the certified jc splat codebook into aerial as the `CodebookDistance` oracle, discover OWL/DOLCE+ SPO HHTL classes + basins, drive the `wikidata-hhtl-load.md` deterministic compression (skeleton + basins + CAM-dedup + thin rows). | lance-graph-arm-discovery::aerial + crates/jc + wikidata loader | ~? | MED | **In progress** | **Phase 1 (branch `claude/jolly-cori-clnf9-darm14`):** the two aerial-side seams — `aerial::TopKDistance` (the sparse splat-top-k `CodebookDistance` the 10000² BLASGraph splat actually emits; top-k per node, not a dense table) + `aerial::ontology::{DolceCategory, OntologyProjector}` (DOLCE 4-facet skeleton → `rdfs:subClassOf`/`rdf:type` SPO). End-to-end test: splat top-k → aerial discovers `occupation→DOLCE-class` → projects the skeleton triple. 41/41 + clippy clean (default + `ndarray-simd`), zero-dep. Float still OFFLINE in jc only (`ewa_sandwich`+`sigma_codebook_probe` ρ=0.9973+`pflug` Lε); aerial online path integer. **Phase 2 (branch `claude/jolly-cori-clnf9-darm14-p2`):** the proposer→hub landing. (a) `OntologyProjector::dolce_id()` — emits the stable `dolce_id` u8 (= basin nibble) the hub routes by, NOT a hardcoded IRI (the OD-DOLCE alignment #442 deferred to this lane; basin ordering already matches `dolce_id::{ENDURANT=0..}`). (b) gated worked example `tests/wikidata_landing.rs` (`--features landing`, opt-in `dev-dep lance-graph-contract` à la jc): splat top-k → aerial recovers all 6 DOLCE basins → lands each on the REAL `contract::class_view::FieldMask` + `hash::fnv1a_str` signature (+ `NiblePath` nibble-routing inlined until #442 merges). CONFIRMED on data: corpus collapses 6→5 families (film≡tv-series twin), human⊂person inherits path + mask-as-delta, basin preserved. 42/42 default (zero-dep) + landing test green, clippy clean both. **Remaining:** real jc/blasgraph splat producing the lists; the ndjson→`WikidataClass` loader; swap inline nibble→`contract::hhtl::NiblePath` on #442 merge; gated on D-ARM-7 (`jc::jirak`). Map: `splat-codebook-aerial-wikidata-compression.md`; E-ARM-JC-RESOLVES-BOTH-SEAMS. | | D-ARM-SYN-1 | Add `Implies`/`CoOccursWith` to `ruff_spo_triplet::Predicate` closed vocabulary (+ `Provenance` tier) so ARM rules load through the same `parse_triples` ndjson path as the static extractor | ruff/ruff_spo_triplet | 40 | MED | **Queued** | council-gated (deliberate ontology change); blocks SYN-2; see `.claude/knowledge/aerial-arm-ruff-spo-codegen-synergies.md` §1 | | D-ARM-SYN-2 | `CandidateRule → ruff_spo_triplet::ModelGraph` adapter so the Aerial runtime-data leg joins the `ruff_python_dto_check` static-AST leg in one graph before `expand()` | lance-graph-arm-discovery + ruff_spo_triplet | 120 | MED | **Queued** | depends on SYN-1; synergy doc §2 | | D-ARM-SYN-3 | Calibrate `ProvenanceTier::ArmDiscovered` `(f,c)` below the `op_emitter` ratification gate + below static `Inferred (0.85,0.75)` so un-ratified ARM truth is council-visible but codegen-filtered | lance-graph-contract + lance-graph-ontology::op_emitter | 30 | MED | **Queued** | depends on D-ARM-1 + SYN-1; synergy doc §3/§4 | diff --git a/crates/lance-graph-arm-discovery/Cargo.toml b/crates/lance-graph-arm-discovery/Cargo.toml index 6937fbd9..0f479e40 100644 --- a/crates/lance-graph-arm-discovery/Cargo.toml +++ b/crates/lance-graph-arm-discovery/Cargo.toml @@ -34,6 +34,14 @@ default = [] # no SIMD speedup. The default (scalar) feature does not pull ndarray at all. ndarray-simd = ["dep:ndarray"] +# D-ARM-14 Phase 2 worked example: the proposer→hub landing (splat → discovery → +# `dolce_id` + ndjson → land on the canonical hub types). Pulls the zero-dep +# `lance-graph-contract` (the exact `[dev-dependencies]` pattern `jc` uses for +# bridge examples) so `tests/wikidata_landing.rs` lands on the REAL +# `class_view::FieldMask` + `hash::fnv1a_str`. The lib stays zero-dep; this is +# off by default. Run: `cargo test --features landing --test wikidata_landing`. +landing = ["dep:lance-graph-contract"] + [dependencies.ndarray] path = "../../../ndarray" optional = true @@ -41,5 +49,9 @@ default-features = false # `std` gates `ndarray::simd` (the `U64x8` polyfill); that is all we need. features = ["std"] +[dependencies.lance-graph-contract] +path = "../lance-graph-contract" +optional = true + [lib] doctest = true diff --git a/crates/lance-graph-arm-discovery/src/aerial/ontology.rs b/crates/lance-graph-arm-discovery/src/aerial/ontology.rs index 3a83307a..fc74df26 100644 --- a/crates/lance-graph-arm-discovery/src/aerial/ontology.rs +++ b/crates/lance-graph-arm-discovery/src/aerial/ontology.rs @@ -61,6 +61,14 @@ impl DolceCategory { pub fn from_basin(n: u8) -> Option { Self::ALL.get(n as usize).copied() } + + /// The facet for a consequent **category index** (`0..3`) — the + /// discovery-side view of the cache `dolce_id`. Alias of [`Self::from_basin`] + /// with intent: a DOLCE projector's consequent category IS the `dolce_id`. + #[must_use] + pub fn from_index(category: u32) -> Option { + Self::from_basin(category as u8) + } } /// A [`FeedProjector`] that renders discovered rules as OWL/DOLCE skeleton SPO. @@ -76,6 +84,9 @@ pub struct OntologyProjector { pub predicate: String, /// Class IRI per consequent category (the skeleton classes). pub class_iris: Vec, + /// Whether the consequent class axis is the DOLCE facet set — enables + /// [`Self::dolce_id`] to emit the stable `dolce_id` u8 the hub routes by. + pub is_dolce: bool, } impl OntologyProjector { @@ -86,6 +97,7 @@ impl OntologyProjector { namespace: namespace.into(), predicate: "rdfs:subClassOf".to_string(), class_iris, + is_dolce: false, } } @@ -96,6 +108,7 @@ impl OntologyProjector { namespace: namespace.into(), predicate: "rdf:type".to_string(), class_iris, + is_dolce: false, } } @@ -103,10 +116,34 @@ impl OntologyProjector { /// [`DolceCategory`] facets — the DOLCE axis-template skeleton. #[must_use] pub fn dolce_subclass(namespace: impl Into) -> Self { - Self::subclass_of( - namespace, - DolceCategory::ALL.iter().map(|c| c.iri().to_string()).collect(), - ) + Self { + is_dolce: true, + ..Self::subclass_of( + namespace, + DolceCategory::ALL.iter().map(|c| c.iri().to_string()).collect(), + ) + } + } + + /// The stable `dolce_id` u8 (= basin nibble, `0..3`) for a DOLCE consequent — + /// the canonical, enum-free routing key the hub feeds to + /// `contract::hhtl::NiblePath::root` (#442). `None` for a non-DOLCE projector + /// or an out-of-range category. + /// + /// This is the **emit-`dolce_id`** half of the OD-DOLCE resolution: the + /// ndjson object IRI ([`FeedProjector::object`]) is a convenience LABEL the + /// hub may re-resolve from the OGIT cache, while this u8 is what both sides of + /// the firewall agree on as the basin — neither embeds the DOLCE enum. + #[must_use] + pub fn dolce_id(&self, consequent: &[Item]) -> Option { + if !self.is_dolce { + return None; + } + consequent + .first() + .map(|it| it.category) + .filter(|&c| (c as usize) < DolceCategory::ALL.len()) + .map(|c| c as u8) } } @@ -155,6 +192,19 @@ mod tests { } assert_eq!(DolceCategory::from_basin(4), None); assert_eq!(DolceCategory::Quality.iri(), "dolce:Quality"); + assert_eq!(DolceCategory::from_index(1), Some(DolceCategory::Perdurant)); + } + + #[test] + fn dolce_projector_emits_the_stable_dolce_id() { + let proj = OntologyProjector::dolce_subclass("wd:"); + // consequent category == dolce_id == basin (the hub routing key) + assert_eq!(proj.dolce_id(&[Item::new(1, 0)]), Some(0)); // Endurant + assert_eq!(proj.dolce_id(&[Item::new(1, 1)]), Some(1)); // Perdurant + assert_eq!(proj.dolce_id(&[Item::new(1, 9)]), None, "out-of-range facet"); + // a non-DOLCE projector has no dolce_id to emit + let plain = OntologyProjector::subclass_of("wd:", vec!["wd:Q5".into()]); + assert_eq!(plain.dolce_id(&[Item::new(1, 0)]), None); } /// End-to-end: a splat codebook makes each occupation near its DOLCE class; diff --git a/crates/lance-graph-arm-discovery/tests/wikidata_landing.rs b/crates/lance-graph-arm-discovery/tests/wikidata_landing.rs new file mode 100644 index 00000000..e4bb1073 --- /dev/null +++ b/crates/lance-graph-arm-discovery/tests/wikidata_landing.rs @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +//! D-ARM-14 Phase 2 — the worked end-to-end: **splat → aerial discovery → +//! `dolce_id` + ndjson → land on the canonical hub types**, on a Wikidata-shaped +//! fixture. Gated behind `--features landing` so aerial's default build stays +//! zero-dep. +//! +//! It lands on the **real merged** hub types — `lance_graph_contract::class_view::FieldMask` +//! (presence) and `hash::fnv1a_str` (the structural-signature value). The 16ⁿ +//! `NiblePath` router is #442 (not yet on `main`), so its bit-shift addressing is +//! reproduced inline here, annotated `= contract::hhtl::NiblePath`; swap the +//! `np_*` helpers for `NiblePath::{root,child,basin,is_ancestor_of}` once #442 +//! merges. Firewall intact: aerial (the lib) never imports the hub; this *test* +//! consumes both sides to prove the seam — the `(ClassId, signature, FieldMask)` +//! triple + the `dolce_id` u8 are the only things that cross. +//! +//! Run: `cargo test --manifest-path crates/lance-graph-arm-discovery/Cargo.toml \ +//! --features landing --test wikidata_landing -- --nocapture` + +#![cfg(feature = "landing")] + +use lance_graph_arm_discovery::ndjson::to_ndjson; +use lance_graph_arm_discovery::{ + extract_rules, CandidateTriple, Dataset, ExtractParams, FeatureSpec, Item, OntologyProjector, + TopKDistance, NARS_PERSONALITY_K, +}; +use lance_graph_contract::class_view::{ClassId, FieldMask}; +use lance_graph_contract::hash::fnv1a_str; + +// ── the Wikidata-shaped fixture ────────────────────────────────────────────── +// Real QIDs; `etype` = the discovery-side entity-type category; `dolce` = the +// cache-resolved DOLCE id (0=Endurant, 1=Perdurant); `path` = the P279 nibble +// descent below the basin; `props` = the representative property-id set (its +// IDENTITIES drive the shape-family, its COUNT drives the presence mask). +struct WClass { + id: ClassId, + qid: &'static str, + etype: u32, + dolce: u8, + path: &'static [u8], + props: &'static [&'static str], +} + +fn corpus() -> Vec { + vec![ + WClass { id: 1, qid: "Q215627", etype: 0, dolce: 0, path: &[0x1], props: &["P21", "P569", "P570"] }, // person + WClass { id: 2, qid: "Q5", etype: 1, dolce: 0, path: &[0x1, 0x2], props: &["P21", "P569", "P570", "P106"] }, // human ⊂ person (+occupation) + WClass { id: 3, qid: "Q515", etype: 2, dolce: 0, path: &[0x3], props: &["P1082", "P625", "P17"] }, // city + WClass { id: 4, qid: "Q11424", etype: 3, dolce: 1, path: &[0x0], props: &["P57", "P577", "P161"] }, // film + WClass { id: 5, qid: "Q5398426", etype: 4, dolce: 1, path: &[0x1], props: &["P57", "P577", "P161"] }, // tv-series ≡ film + WClass { id: 6, qid: "Q1656682", etype: 5, dolce: 1, path: &[0x2], props: &["P585", "P276"] }, // event + ] +} + +// ── contract::hhtl::NiblePath (#442) — inlined as (path, depth) until it merges ── +fn np_root(basin: u8) -> (u64, u32) { + ((basin & 0x0F) as u64, 1) +} +fn np_child((p, d): (u64, u32), n: u8) -> (u64, u32) { + if d >= 16 || n >= 16 { + (p, d) + } else { + ((p << 4) | (n & 0x0F) as u64, d + 1) + } +} +fn np_basin((p, d): (u64, u32)) -> u8 { + ((p >> (4 * (d - 1))) & 0x0F) as u8 +} +fn np_is_ancestor((pa, da): (u64, u32), (pb, db): (u64, u32)) -> bool { + da > 0 && da <= db && (pb >> (4 * (db - da))) == pa +} +fn nibble_path(basin: u8, children: &[u8]) -> (u64, u32) { + children.iter().fold(np_root(basin), |acc, &c| np_child(acc, c)) +} + +// ── hub landing primitives (REAL merged contract types) ────────────────────── +/// Presence bitmask — one bit per present property (`= contract::class_view::FieldMask`). +fn presence(props: &[&str]) -> FieldMask { + let positions: Vec = (0..props.len() as u8).collect(); + FieldMask::from_positions(&positions) +} +/// `FieldMask::inherit` (#442) — bitwise union; inlined via the public `.0`. +fn inherit(parent: FieldMask, delta: FieldMask) -> FieldMask { + FieldMask(parent.0 | delta.0) +} +/// Structural signature over the canonical (sorted, deduped) property-ID set + +/// basin — `= class_signature::StructuralSignature(fnv1a(...) as u32)`. Property +/// IDENTITIES (not positions) so person {P21,…} ≠ film {P57,…} though both fill +/// positions {0,1,2}. +fn signature(dolce: u8, props: &[&str]) -> u32 { + let mut ps: Vec<&str> = props.to_vec(); + ps.sort_unstable(); + ps.dedup(); + fnv1a_str(&format!("{dolce}|{}", ps.join(","))) as u32 +} + +/// Discover `entity-type → DOLCE-facet` from a splat-shaped codebook + data — +/// the aerial proposer half. Returns the per-etype discovered `dolce_id`. +fn discover_dolce_ids(classes: &[WClass]) -> std::collections::HashMap { + // f0 = entity-type (6), f1 = DOLCE facet (4). Plant each etype ⇒ its facet. + let spec = FeatureSpec::new(vec![6, 4]); + let mut rows = Vec::new(); + for c in classes { + for _ in 0..50 { + rows.push(vec![c.etype, c.dolce as u32]); + } + } + let data = Dataset::new(spec.clone(), rows); + // Splat top-k: each entity-type sits next to its DOLCE facet (distance 1). + let edges: Vec<(Item, Item, u32)> = classes + .iter() + .map(|c| (Item::new(0, c.etype), Item::new(1, c.dolce as u32), 1)) + .collect(); + let oracle = TopKDistance::new(spec, u32::MAX, &edges); + + let rules = extract_rules( + &oracle, + &data, + &ExtractParams { theta: 2, max_antecedent: 1, min_support_ppm: 100_000, min_confidence_ppm: 700_000 }, + ); + + // The proposer emits the stable dolce_id (NOT a hardcoded IRI) — Phase-2 fix. + let proj = OntologyProjector::dolce_subclass("wd:"); + let mut out = std::collections::HashMap::new(); + for r in &rules { + if r.antecedent.len() == 1 && r.antecedent[0].feature == 0 && r.consequent[0].feature == 1 { + if let Some(dolce) = proj.dolce_id(&r.consequent) { + out.insert(r.antecedent[0].category, dolce); + } + } + } + out +} + +#[test] +fn end_to_end_splat_to_wikidata_hhtl() { + let classes = corpus(); + + // ── Stage A+B: aerial discovers the basin (dolce_id) from splat + data ── + let discovered = discover_dolce_ids(&classes); + for c in &classes { + assert_eq!( + discovered.get(&c.etype).copied(), + Some(c.dolce), + "aerial must recover {}'s DOLCE basin from the splat", + c.qid + ); + } + eprintln!("✓ Stage A/B: aerial recovered all {} DOLCE basins from the splat", classes.len()); + + // ── Stage C: land each discovered class on the canonical hub types ── + let mut sigs = std::collections::HashSet::new(); + let mut landed = Vec::new(); + for c in &classes { + let basin = discovered[&c.etype]; // from DISCOVERY, not hardcoded + let path = nibble_path(basin, c.path); // = NiblePath::root(basin).child(..) + let mask = presence(c.props); // = FieldMask + let sig = signature(basin, c.props); // = StructuralSignature + let triple: (ClassId, u32, FieldMask) = (c.id, sig, mask); // the D-CLS triple + sigs.insert(sig); + landed.push((c.qid, basin, path, triple)); + assert_eq!(np_basin(path), basin, "basin survives down the nibble path"); + assert_eq!(mask.count(), c.props.len() as u32, "one present bit per property"); + eprintln!(" landed {:>9} → basin {basin} path {:#x} sig {sig:#010x} mask {:#06b}", c.qid, path.0, mask.0); + } + + // N4 falsification #1 — the corpus COLLAPSES to fewer shape-families. + assert!(sigs.len() < classes.len(), "shapes must collapse (6 classes → {} families)", sigs.len()); + assert_eq!(sigs.len(), 5, "film ≡ tv-series is the one twin"); + let film = classes.iter().find(|c| c.qid == "Q11424").unwrap(); + let tv = classes.iter().find(|c| c.qid == "Q5398426").unwrap(); + assert_eq!(signature(1, film.props), signature(1, tv.props), "film and tv-series share one signature"); + let person = classes.iter().find(|c| c.qid == "Q215627").unwrap(); + assert_ne!(signature(0, person.props), signature(1, film.props), "person ≠ film"); + + // N4 falsification #2 — subclass inherits PATH + MASK-as-delta, basin preserved. + let human = classes.iter().find(|c| c.qid == "Q5").unwrap(); + let person_path = nibble_path(0, person.path); + let human_path = nibble_path(0, human.path); + assert!(np_is_ancestor(person_path, human_path), "person ⊃ human in the nibble tree"); + assert_eq!(np_basin(person_path), np_basin(human_path), "subclassing preserves the DOLCE basin"); + let delta = FieldMask::from_positions(&[3]); // human's extra property (P106) bit + assert_eq!(inherit(presence(person.props), delta), presence(human.props), "human mask = person mask ⊕ P106"); + + // ── the ndjson the hub reads (the firewall-crossing wire) ── + let proj = OntologyProjector::dolce_subclass("wd:"); + // one illustrative rule: person ⇒ Endurant + let rule = lance_graph_arm_discovery::CandidateRule { + antecedent: vec![Item::new(0, person.etype)], + consequent: vec![Item::new(1, person.dolce as u32)], + cooccur: 50, + antecedent_count: 50, + window: 300, + }; + let triples = vec![CandidateTriple::from_rule(&rule, &proj, NARS_PERSONALITY_K)]; + let nd = to_ndjson(&triples); + assert!(nd.contains("\"p\":\"rdfs:subClassOf\"")); + eprintln!("✓ Stage C: corpus collapsed 6 → {} families; ndjson seam:\n{nd}", sigs.len()); +} From 4b51c5f8ff1524e4c4dc36f25f00d48659a1bd93 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 09:56:18 +0000 Subject: [PATCH 2/5] build(arm-discovery): lock the optional lance-graph-contract dep (landing feature) Cargo.lock update from the D-ARM-14 Phase 2 `landing` feature's optional dev-dep on the zero-dep lance-graph-contract. Purely additive (+134 lines: contract + its serde/serde_yaml transitive locks); the default build does not pull these. https://claude.ai/code/session_012SorR8UbtEvYmbX8cXftj7 --- crates/lance-graph-arm-discovery/Cargo.lock | 134 ++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/crates/lance-graph-arm-discovery/Cargo.lock b/crates/lance-graph-arm-discovery/Cargo.lock index 1bcdfbbd..932e89fc 100644 --- a/crates/lance-graph-arm-discovery/Cargo.lock +++ b/crates/lance-graph-arm-discovery/Cargo.lock @@ -65,19 +65,63 @@ dependencies = [ "libc", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "lance-graph-arm-discovery" version = "0.1.0" dependencies = [ + "lance-graph-contract", "ndarray", ] +[[package]] +name = "lance-graph-contract" +version = "0.1.0" +dependencies = [ + "glob", + "serde", + "serde_yaml", +] + [[package]] name = "libc" version = "0.2.186" @@ -157,14 +201,104 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rawpointer" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" From cbb01218b1b74d1d580469dfda1fc21301e01594 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 10:01:10 +0000 Subject: [PATCH 3/5] =?UTF-8?q?docs(arm-discovery):=20the=20potential=20?= =?UTF-8?q?=E2=80=94=20any=20OWL/DOLCE=20domain=20compartmentalizes=20into?= =?UTF-8?q?=20one=20HHTL,=20fed=20by=20aerial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generalizes D-ARM-14 Phase 2: medicine (SNOMED/FMA), finance (FIBO), geography, law, Odoo, and all of Wikidata are not bespoke loaders — each yields the SAME (ClassId, StructuralSignature, FieldMask) row (basin = DOLCE dolce_id, nibble path = P279 descent, FieldMask = presence, signature = shape-family). Domain enters only as content, never structure (cognitive-risc-classes N4). aerial is the per-domain runtime feed: point it at a domain splat codebook + corpus, swap the fixture, no new hub code. - knowledge/owl-dolce-hhtl-compartments-aerial-fed.md (the map + the proof chain + the compartmentalization rule + scale-freezes) - EPIPHANIES: the generalization FINDING https://claude.ai/code/session_012SorR8UbtEvYmbX8cXftj7 --- .claude/board/EPIPHANIES.md | 13 ++ .../owl-dolce-hhtl-compartments-aerial-fed.md | 141 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 .claude/knowledge/owl-dolce-hhtl-compartments-aerial-fed.md diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 89ad99b3..0932055e 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -15,6 +15,19 @@ **#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: ANY OWL/DOLCE domain compartmentalizes into the same 16ⁿ HHTL, fed by aerial — the class-meta-DTO is the universal substrate; domains differ in content, never structure (D-ARM-14 Phase 2) + +**Status:** FINDING (per-layer domain-independence is shipped/proven on 2 domains; full-scale universality is CONJECTURE — each new domain is a falsifier). Doc: `owl-dolce-hhtl-compartments-aerial-fed.md`. PR: D-ARM-14 Phase 2 (`claude/jolly-cori-clnf9-darm14-p2`). + +**The generalization (the user's "potential"):** medicine (SNOMED/FMA), finance (FIBO), geography, law, Odoo, and all of Wikidata are NOT bespoke loaders — each yields the SAME four things and lands as the SAME `(ClassId, StructuralSignature, FieldMask)` row: a **basin** (DOLCE `dolce_id` 0..3), a **nibble path** (`NiblePath`, P279 subClassOf descent), a **`FieldMask`** (property presence), a **`StructuralSignature`** (FNV-1a shape-family). Domain enters only as *content* (the property-id set + the corpus), never as *structure*. This is `cognitive-risc-classes.md` N4 operationalised — "chess + Odoo + Wikidata-anatomy through one Class+SoA+HHTL+CAM, no special-case." + +**Why it holds (proof chain, not hope):** every layer is domain-agnostic by construction — `NiblePath` takes a `basin: u8` with zero DOLCE knowledge (#442); `FieldMask` is one-bit-per-property (#441); `StructuralSignature` is label/QID/domain-independent (Odoo #441 + Wikidata #442 BOTH collapse structurally-identical classes to one family); `ClassView` is a trait both `RegistryClassView` (Odoo) and `WikidataClassView` (Wikidata) impl unchanged. DOLCE is resolved late from the cache (`basin = dolce_id`, no enum). The **aerial feed** is the part that makes each domain self-populating: similarity is an injected integer `CodebookDistance` (the 10000² splat, jc-certified), so a new domain is fed by pointing aerial at (a) a domain splat codebook + (b) a domain row corpus — `tests/wikidata_landing.rs` is the template; swap the fixture, **no new hub code**. Phase 2 proved this end-to-end (splat → recover 6 DOLCE basins → land on real `FieldMask`+signature; film≡tv collapse, human⊂person inherit). + +**Compartmentalization rule:** ONE tree axis (Abstammung/P279), DOLCE facets seed root nibbles 0x0..0x3 (0x4..0xF reserved). The DOMAIN is an ORTHOGONAL compartment (namespace/facet tag), NOT a second path — cross-domain multi-typing is a facet bit in the same `FieldMask`, like bat = mammal-path + flight-bit. Open: reconcile the OGIT *byte*-basins (0x10..) with the `NiblePath` *nibble*-basin before a multi-domain load. + +**Scale-freezes (carry from the #442 review):** `NiblePath` silent truncation at depth-16 (deep medical/bio chains collide), `StructuralSignature` u32 (birthday-collision among shape-families across many domains), the DOLCE nibble/byte addressing. None bites the 2 curated domains; all bite at full multi-domain scale — name each as a conscious freeze before the load that needs it. Gated on D-ARM-7 (`jc::jirak`) before any domain's feed writes the store. + +**Cross-ref:** `owl-dolce-hhtl-compartments-aerial-fed.md`, `wikidata-hhtl-load.md`, `ogit-owl-dolce-ontology-compartments.md`, `splat-codebook-aerial-wikidata-compression.md`; #441 (D-CLS), #442 (Wikidata-HHTL), #438 + Phase 2 (aerial feed); the D-CLS↔D-ARM-14 convergence FINDING; `cognitive-risc-classes.md` N4. ## 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 diff --git a/.claude/knowledge/owl-dolce-hhtl-compartments-aerial-fed.md b/.claude/knowledge/owl-dolce-hhtl-compartments-aerial-fed.md new file mode 100644 index 00000000..b6631e69 --- /dev/null +++ b/.claude/knowledge/owl-dolce-hhtl-compartments-aerial-fed.md @@ -0,0 +1,141 @@ + + +# KNOWLEDGE: Any OWL/DOLCE Wikidata domain → HHTL compartments, fed by aerial + +## READ BY: +- Any worker landing a NEW domain (medicine / finance / geography / law / biology …) into the class-meta-DTO + HHTL router +- Any worker on `lance-graph-arm-discovery` (the aerial proposer feed), `contract::hhtl` / `class_view`, `ontology::wikidata_hhtl` +- `truth-architect`, `integration-lead`, `palette-engineer` + +## P0 TRIGGERS: +- About to add a second/third domain ontology to the HHTL → read this first (do NOT grow a per-domain layer) +- About to give aerial a domain corpus to discover skeleton from → this is the feed contract +- About to widen `NiblePath` basins or `StructuralSignature` for "more domains" → see § Scale-freezes + +--- + +## The claim (the potential) + +**One machinery compartmentalizes *every* OWL/DOLCE-typed domain — including all +of Wikidata — into the same 16ⁿ HHTL tree, and aerial is the per-domain runtime +feed that discovers what lands where.** A medical ontology (SNOMED/FMA/ICD), a +financial one (FIBO), a geographic one, an ERP (Odoo), and Wikidata itself are +*not* special cases with bespoke loaders. Each yields the same four things — +a **basin** (DOLCE facet), a **nibble path** (P279 `subClassOf` descent), a +**`FieldMask`** (property presence), a **`StructuralSignature`** (shape-family) — +and lands as the identical `(ClassId, StructuralSignature, FieldMask)` row. +Domains differ only in *content*, never in *structure*. + +This is the `cognitive-risc-classes.md` **N4** doctrine made operational: *"don't +freeze the SoA schema until ≥2 genuinely different domains run through it … chess ++ Odoo + Wikidata-anatomy all run through the same Class+SoA+HHTL+CAM with no +special-case."* Two domains already proved it — Odoo (#441) and Wikidata (#442); +this doc is the generalization to *N*, and aerial is the part that makes each +domain self-populating from data. + +## Why it's domain-independent (the proof chain, not a hope) + +| Layer | Type | Domain-agnostic because… | Proven by | +|---|---|---|---| +| Routing | `contract::hhtl::NiblePath` | takes a `basin: u8` + child nibbles; **zero DOLCE/domain knowledge** | #442 (4 teeth-tests) | +| Presence | `contract::class_view::FieldMask` | one bit per present property — any property-id set | #441 | +| Shape | `class_signature::StructuralSignature` | FNV-1a over the canonical property-id set — label/QID/domain-independent | #441 (Odoo) + #442 (Wikidata) | +| Resolution | `contract::class_view::ClassView` | a trait; `RegistryClassView` (Odoo) and `WikidataClassView` impl it **unchanged** | #441 + #442 | +| Categorical axis | DOLCE `dolce_id` `0..3` | resolved LATE from the OGIT cache; `basin = dolce_id`, no enum embedded | OD-DOLCE (#441 `b31464d`) | +| **Discovery feed** | `aerial` (this crate) | similarity is an injected integer `CodebookDistance` (the splat); the proposer emits flat `(s,p,o,f,c)` + `dolce_id` — domain enters only as *data* | #438 + #442 Phase 2 | + +The collapse property (structurally-identical classes → one shape-family) is the +falsifier each domain must pass: Odoo's curated corpus collapses (#441 D-CLS-AUDIT), +Wikidata's does too (`film ≡ tv-series`, #442), and the aerial-fed worked example +reproduces it end-to-end (`tests/wikidata_landing.rs`). + +## The compartmentalization (how a domain maps to nibbles) + +```text +NiblePath (the ONE tree axis = Abstammung / P279 subClassOf): + + root nibble = DOLCE facet (the categorical scaffold, ogit-owl-dolce): + 0x0 Endurant 0x1 Perdurant 0x2 Quality 0x3 Abstract + 0x4..0xF reserved (append-only) for finer top super-axes + + child nibbles = the domain's subClassOf descent (16-way fan-out per level) + e.g. Endurant → …organism → …mammal → …bat (biology) + Endurant → …anatomical-structure → …organ (FMA/SNOMED) + Perdurant → …audiovisual-work → …film (Wikidata) + + DOMAIN ≠ a second path. The domain (Wikidata vs SNOMED vs FIBO vs Odoo) is an + ORTHOGONAL compartment — a namespace/facet tag, NOT a branch. "One tree axis + only" (wikidata-hhtl-load.md:46): bat = mammal-PATH + flight-BIT in the same + FieldMask, never two paths. Cross-domain multi-typing is likewise a facet bit. +``` + +Every entity in any domain therefore gets a single O(1) bit-shift address +(`NiblePath`), a presence mask, and a shape signature — the substrate the +`wikidata-hhtl-load.md` compression (skeleton + basins + CAM-dedup + thin rows) +runs over, **identically per domain**. + +> **Open design point (honest):** `ogit-owl-dolce-ontology-compartments.md` sketches +> *byte*-wide domain basins (`0x00..0x0F` universal/DOLCE, `0x10..0x19` healthcare), +> while `contract::hhtl::NiblePath::root` takes a *nibble* (`basin & 0x0F`). Reconcile +> before a multi-domain load: either domains occupy reserved top nibbles `0x4..0xF` +> as super-axes, or the domain is a namespace tag above the path. Not yet decided; +> the structural router is agnostic either way. + +## The aerial feed (what makes each domain self-populating) + +aerial is the **runtime-data proposer** — the per-domain frontend (sibling to the +static OWL/TTL `AstWalker`). For each domain: + +1. **Offline (once, certified):** build the domain's codebook distance from the + 10000² Gaussian splat (`ndarray::hpc::splat3d`), certified by jc + (`ewa_sandwich` ρ-push-forward, `sigma_codebook_probe` ρ=0.9973, `pflug` Lε). + Emit the per-node top-k as `TopKDistance` edges. +2. **Online (integer):** `extract_rules(TopKDistance, domain_rows, θ/ppm)` → + `CandidateRule`s — "entity-shape X ⇒ class/basin Y", the discovered skeleton + edges for *that* domain. +3. **Emit the seam:** `OntologyProjector::dolce_id()` → the stable basin u8; + `to_ndjson` → `{s,p,o,f,c}`. The hub lands them on `NiblePath`/`FieldMask`/ + `StructuralSignature`. The only things crossing the firewall: the + `(ClassId, signature, FieldMask)` triple + the `dolce_id` u8. + +So a new domain is "fed via aerial" by pointing it at (a) a domain splat codebook +and (b) a domain row corpus — **no new code on the hub side**. The +`tests/wikidata_landing.rs` worked example is the template; swap the fixture for +the new domain's classes. + +## Pipeline — *any* domain → HHTL + +```text +domain corpus + domain OWL/DOLCE types + │ (offline) 10000² splat → jc-certified top-k edges + ▼ +aerial: TopKDistance + extract_rules → CandidateRule (skeleton edges) + │ OntologyProjector::dolce_id + ndjson + ▼ ── firewall seam: (ClassId, signature, FieldMask) + dolce_id u8 ── +hub: NiblePath.root(basin).child(P279…) + FieldMask(presence) + signature(shape) + ▼ +the wikidata-hhtl-load.md row, per domain — skeleton + basins + CAM-dedup + thin rows +``` + +## What's proven vs CONJECTURE + +- **Proven (shipped):** the per-layer domain-independence (Odoo #441 + Wikidata #442); aerial can feed it end-to-end on a fixture (#438 + Phase 2 worked example). The seams are concrete code. +- **CONJECTURE (the potential):** that *every* OWL/DOLCE domain at full scale compartmentalizes cleanly — untested beyond the two curated corpora. Each new domain is a falsification opportunity (run its corpus; does it collapse + route?). +- **Hard prerequisite:** D-ARM-7 (the Jirak floor, `jc::jirak`) gates promoting any discovered rule to a live skeleton — required before a domain's aerial feed writes to the store. + +## Scale-freezes to watch as domains multiply (from the #442 review) + +1. **`NiblePath` truncates silently at `MAX_DEPTH=16`.** Deep P279 chains (real medical/biological hierarchies exceed 16) collapse to the same address → cross-class collision. Needs a `depth_exhausted()` signal → switch-to-ref before a deep domain loads. +2. **`StructuralSignature` is `u32`.** Across many domains the distinct-shape count can approach the birthday bound (~77k) → two shapes alias to one family. Widen (or per-domain-namespace the hash) before the load that needs it. +3. **DOLCE basin nibble (4 used / 16) + the byte-vs-nibble reconciliation above** — freeze the domain-compartment addressing consciously, append-only. + +## Cross-references + +- `crates/lance-graph-arm-discovery/` — the aerial feed; `tests/wikidata_landing.rs` the worked template; `OntologyProjector::dolce_id`. +- `contract::hhtl::NiblePath` (#442), `class_view::FieldMask` (#441), `class_signature::StructuralSignature`, `ontology::wikidata_hhtl` (#442). +- `.claude/specs/wikidata-hhtl-load.md` (the compression), `.claude/knowledge/ogit-owl-dolce-ontology-compartments.md` (the DOLCE scaffold + domain compartments), `.claude/knowledge/splat-codebook-aerial-wikidata-compression.md` (the splat→aerial→Wikidata seam mechanics). +- `crates/jc` (certification: ewa_sandwich / sigma_codebook_probe / pflug / jirak), `ndarray::hpc::splat3d` (the splat). +- `EPIPHANIES.md` E-ARM-JC-RESOLVES-BOTH-SEAMS + the D-CLS↔D-ARM-14 convergence FINDING; `cognitive-risc-classes.md` N4. From 04995a99c6abdd061ac2e8f586945604fcd5cbb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 10:29:25 +0000 Subject: [PATCH 4/5] =?UTF-8?q?refactor(arm-discovery):=20swap=20inline=20?= =?UTF-8?q?nibble=20routing=20=E2=86=92=20contract::hhtl::NiblePath=20(#44?= =?UTF-8?q?2=20merged)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Phase-2 worked example reproduced NiblePath's bit-shift addressing inline because contract::hhtl wasn't on main yet. #442 merged, so tests/wikidata_landing.rs now uses the REAL contract::hhtl::NiblePath::{root,child,basin,is_ancestor_of,depth} + FieldMask::inherit — the landing is on the canonical 16^n router, not a stand-in. Rebased onto post-#442 main (415971a). default 42/42 + clippy clean; --features landing wikidata_landing green (real NiblePath depths: person 0x1 d2 → human 0x12 d3; film≡tv collapse holds). https://claude.ai/code/session_012SorR8UbtEvYmbX8cXftj7 --- .claude/board/AGENT_LOG.md | 10 ++ .claude/board/STATUS_BOARD.md | 2 +- .../tests/wikidata_landing.rs | 92 ++++++++----------- 3 files changed, 51 insertions(+), 53 deletions(-) diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index dfc65a54..77002a15 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,13 @@ +## [Main thread / Opus] D-ARM-14 Phase 2 — rebased onto post-#442 main + swapped inline nibble → real contract::hhtl::NiblePath + +**Branch:** claude/jolly-cori-clnf9-darm14-p2 (rebased onto main 415971a, #442 merged) | **Files:** `tests/wikidata_landing.rs` (inline `np_*` helpers + inline FieldMask union → real `NiblePath::{root,child,basin,is_ancestor_of,depth,packed}` + `FieldMask::inherit`), STATUS_BOARD (D-ARM-14 row: swap done). + +**Cargo:** rebase clean (no conflicts); default **42/42** + clippy clean; `--features landing` `wikidata_landing` green + clippy clean — now landing on the REAL merged `contract::hhtl::NiblePath`. Output shows real depths (person 0x1 d2 → human 0x12 d3), 6→5 collapse holds. + +**Outcome:** DONE. User: "442 merged please rebase." #442 put `contract::hhtl::NiblePath` + `FieldMask::inherit` + `ontology::wikidata_hhtl` on main, so the rebase also unlocked the promised inline→real swap (the "swap on #442 merge" remaining item). The worked example now lands on the canonical 16ⁿ router, not a stand-in. Force-push follows (rebase rewrote the 3 P2 commits onto new main). PR #443 updated. + +--- + ## [Main thread / Opus] D-ARM-14 Phase 2 — proposer→hub landing (dolce_id emit + worked Wikidata example) **Branch:** claude/jolly-cori-clnf9-darm14-p2 (off main a77e119) | **Files:** `crates/lance-graph-arm-discovery/src/aerial/ontology.rs` (+`OntologyProjector::dolce_id`, `DolceCategory::from_index`, `is_dolce`), `Cargo.toml` (+`landing` feature + optional `lance-graph-contract` dev-dep), `tests/wikidata_landing.rs` (NEW, gated) + STATUS_BOARD (D-ARM-14 Phase 2). diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index e7a2c111..3e6b17ee 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -645,7 +645,7 @@ The missing UPSTREAM discovery leg. Today's proposers (curated L-docs + AST-extr | D-ARM-11 | `style_recipe.rs` rule 8 — ArmDiscovered backing adds `DAtom::Compute` weight 2 (provisional) | lance-graph-ontology::style_recipe | 80 | MED | **Queued** | depends on D-ARM-1 | | D-ARM-12 | End-to-end pipeline test + bench (synthetic Odoo feed → all 5 stages → council micro-batch) | lance-graph-arm-discovery::tests + benches | 400 | MED | **Queued** | depends on Waves 1-6; informs OQ-ARM-2 + OQ-ARM-7 | | D-ARM-13 | **Aerial+ Rust transcode — deterministic codebook-probe backend** (float-free). The paper's `f32` denoising autoencoder is REPLACED by an integer `CodebookDistance` oracle (palette256 distance, ρ=0.9973 vs cosine): the reconstruction probe is a codebook top-k, not a softmax over float weights. Integer evidence counts + ppm gates + `TruthU8` (= CausalEdge64 wire). `AerialProposer` impl of `Proposer`. Count loop is a row-bitset SoA (`RowMasks`) → AND+popcount, routed through `ndarray::simd::U64x8` under the `ndarray-simd` feature. | lance-graph-arm-discovery::aerial | ~1.1K | HIGH | **Shipped (branch)** | branch `claude/jolly-cori-clnf9`; standalone zero-dep crate (excluded); **33/33** tests + clippy `-D warnings` clean on BOTH default (scalar) and `--features ndarray-simd`; **zero f32 in the discovery path** (audit), float only at the `TruthValue`/`Triple` serialization edge. Bitwise-deterministic ⇒ joins the trunk; the nondeterminism firewall + D-ARM-9 IPC rationale are moot. SIMD target-cpu caveat: real AVX-512/AMX kernels need `-C target-cpu=native`/`x86-64-v4`. v1 (autoencoder) superseded per the user's no-float directive. | -| D-ARM-14 | **Splat-codebook oracle + Wikidata skeleton discovery** — wire the certified jc splat codebook into aerial as the `CodebookDistance` oracle, discover OWL/DOLCE+ SPO HHTL classes + basins, drive the `wikidata-hhtl-load.md` deterministic compression (skeleton + basins + CAM-dedup + thin rows). | lance-graph-arm-discovery::aerial + crates/jc + wikidata loader | ~? | MED | **In progress** | **Phase 1 (branch `claude/jolly-cori-clnf9-darm14`):** the two aerial-side seams — `aerial::TopKDistance` (the sparse splat-top-k `CodebookDistance` the 10000² BLASGraph splat actually emits; top-k per node, not a dense table) + `aerial::ontology::{DolceCategory, OntologyProjector}` (DOLCE 4-facet skeleton → `rdfs:subClassOf`/`rdf:type` SPO). End-to-end test: splat top-k → aerial discovers `occupation→DOLCE-class` → projects the skeleton triple. 41/41 + clippy clean (default + `ndarray-simd`), zero-dep. Float still OFFLINE in jc only (`ewa_sandwich`+`sigma_codebook_probe` ρ=0.9973+`pflug` Lε); aerial online path integer. **Phase 2 (branch `claude/jolly-cori-clnf9-darm14-p2`):** the proposer→hub landing. (a) `OntologyProjector::dolce_id()` — emits the stable `dolce_id` u8 (= basin nibble) the hub routes by, NOT a hardcoded IRI (the OD-DOLCE alignment #442 deferred to this lane; basin ordering already matches `dolce_id::{ENDURANT=0..}`). (b) gated worked example `tests/wikidata_landing.rs` (`--features landing`, opt-in `dev-dep lance-graph-contract` à la jc): splat top-k → aerial recovers all 6 DOLCE basins → lands each on the REAL `contract::class_view::FieldMask` + `hash::fnv1a_str` signature (+ `NiblePath` nibble-routing inlined until #442 merges). CONFIRMED on data: corpus collapses 6→5 families (film≡tv-series twin), human⊂person inherits path + mask-as-delta, basin preserved. 42/42 default (zero-dep) + landing test green, clippy clean both. **Remaining:** real jc/blasgraph splat producing the lists; the ndjson→`WikidataClass` loader; swap inline nibble→`contract::hhtl::NiblePath` on #442 merge; gated on D-ARM-7 (`jc::jirak`). Map: `splat-codebook-aerial-wikidata-compression.md`; E-ARM-JC-RESOLVES-BOTH-SEAMS. | +| D-ARM-14 | **Splat-codebook oracle + Wikidata skeleton discovery** — wire the certified jc splat codebook into aerial as the `CodebookDistance` oracle, discover OWL/DOLCE+ SPO HHTL classes + basins, drive the `wikidata-hhtl-load.md` deterministic compression (skeleton + basins + CAM-dedup + thin rows). | lance-graph-arm-discovery::aerial + crates/jc + wikidata loader | ~? | MED | **In progress** | **Phase 1 (branch `claude/jolly-cori-clnf9-darm14`):** the two aerial-side seams — `aerial::TopKDistance` (the sparse splat-top-k `CodebookDistance` the 10000² BLASGraph splat actually emits; top-k per node, not a dense table) + `aerial::ontology::{DolceCategory, OntologyProjector}` (DOLCE 4-facet skeleton → `rdfs:subClassOf`/`rdf:type` SPO). End-to-end test: splat top-k → aerial discovers `occupation→DOLCE-class` → projects the skeleton triple. 41/41 + clippy clean (default + `ndarray-simd`), zero-dep. Float still OFFLINE in jc only (`ewa_sandwich`+`sigma_codebook_probe` ρ=0.9973+`pflug` Lε); aerial online path integer. **Phase 2 (branch `claude/jolly-cori-clnf9-darm14-p2`):** the proposer→hub landing. (a) `OntologyProjector::dolce_id()` — emits the stable `dolce_id` u8 (= basin nibble) the hub routes by, NOT a hardcoded IRI (the OD-DOLCE alignment #442 deferred to this lane; basin ordering already matches `dolce_id::{ENDURANT=0..}`). (b) gated worked example `tests/wikidata_landing.rs` (`--features landing`, opt-in `dev-dep lance-graph-contract` à la jc): splat top-k → aerial recovers all 6 DOLCE basins → lands each on the REAL merged `contract::hhtl::NiblePath` (16ⁿ router, #442) + `class_view::FieldMask` (+`inherit`) + `hash::fnv1a_str` signature. CONFIRMED on data: corpus collapses 6→5 families (film≡tv-series twin), human⊂person inherits path + mask-as-delta, basin preserved. 42/42 default (zero-dep) + landing test green, clippy clean both. Rebased onto post-#442 main; the inline-nibble stand-in swapped for the real `NiblePath`. **Remaining:** real jc/blasgraph splat producing the lists; the ndjson→`WikidataClass` loader; gated on D-ARM-7 (`jc::jirak`). Map: `splat-codebook-aerial-wikidata-compression.md`; E-ARM-JC-RESOLVES-BOTH-SEAMS. | | D-ARM-SYN-1 | Add `Implies`/`CoOccursWith` to `ruff_spo_triplet::Predicate` closed vocabulary (+ `Provenance` tier) so ARM rules load through the same `parse_triples` ndjson path as the static extractor | ruff/ruff_spo_triplet | 40 | MED | **Queued** | council-gated (deliberate ontology change); blocks SYN-2; see `.claude/knowledge/aerial-arm-ruff-spo-codegen-synergies.md` §1 | | D-ARM-SYN-2 | `CandidateRule → ruff_spo_triplet::ModelGraph` adapter so the Aerial runtime-data leg joins the `ruff_python_dto_check` static-AST leg in one graph before `expand()` | lance-graph-arm-discovery + ruff_spo_triplet | 120 | MED | **Queued** | depends on SYN-1; synergy doc §2 | | D-ARM-SYN-3 | Calibrate `ProvenanceTier::ArmDiscovered` `(f,c)` below the `op_emitter` ratification gate + below static `Inferred (0.85,0.75)` so un-ratified ARM truth is council-visible but codegen-filtered | lance-graph-contract + lance-graph-ontology::op_emitter | 30 | MED | **Queued** | depends on D-ARM-1 + SYN-1; synergy doc §3/§4 | diff --git a/crates/lance-graph-arm-discovery/tests/wikidata_landing.rs b/crates/lance-graph-arm-discovery/tests/wikidata_landing.rs index e4bb1073..c88e89c1 100644 --- a/crates/lance-graph-arm-discovery/tests/wikidata_landing.rs +++ b/crates/lance-graph-arm-discovery/tests/wikidata_landing.rs @@ -6,27 +6,29 @@ //! fixture. Gated behind `--features landing` so aerial's default build stays //! zero-dep. //! -//! It lands on the **real merged** hub types — `lance_graph_contract::class_view::FieldMask` -//! (presence) and `hash::fnv1a_str` (the structural-signature value). The 16ⁿ -//! `NiblePath` router is #442 (not yet on `main`), so its bit-shift addressing is -//! reproduced inline here, annotated `= contract::hhtl::NiblePath`; swap the -//! `np_*` helpers for `NiblePath::{root,child,basin,is_ancestor_of}` once #442 -//! merges. Firewall intact: aerial (the lib) never imports the hub; this *test* -//! consumes both sides to prove the seam — the `(ClassId, signature, FieldMask)` -//! triple + the `dolce_id` u8 are the only things that cross. +//! It lands on the **real merged hub types** (#441 + #442): +//! `lance_graph_contract::hhtl::NiblePath` (the 16ⁿ Abstammung router), +//! `class_view::FieldMask` (presence, + `inherit`), and `hash::fnv1a_str` (the +//! structural-signature value). Firewall intact: aerial (the lib) never imports +//! the hub; this *test* consumes both sides to prove the seam — the +//! `(ClassId, signature, FieldMask)` triple + the `dolce_id` u8 are the only +//! things that cross. //! //! Run: `cargo test --manifest-path crates/lance-graph-arm-discovery/Cargo.toml \ //! --features landing --test wikidata_landing -- --nocapture` #![cfg(feature = "landing")] +use std::collections::{HashMap, HashSet}; + use lance_graph_arm_discovery::ndjson::to_ndjson; use lance_graph_arm_discovery::{ - extract_rules, CandidateTriple, Dataset, ExtractParams, FeatureSpec, Item, OntologyProjector, - TopKDistance, NARS_PERSONALITY_K, + extract_rules, CandidateRule, CandidateTriple, Dataset, ExtractParams, FeatureSpec, Item, + OntologyProjector, TopKDistance, NARS_PERSONALITY_K, }; use lance_graph_contract::class_view::{ClassId, FieldMask}; use lance_graph_contract::hash::fnv1a_str; +use lance_graph_contract::hhtl::NiblePath; // ── the Wikidata-shaped fixture ────────────────────────────────────────────── // Real QIDs; `etype` = the discovery-side entity-type category; `dolce` = the @@ -53,37 +55,16 @@ fn corpus() -> Vec { ] } -// ── contract::hhtl::NiblePath (#442) — inlined as (path, depth) until it merges ── -fn np_root(basin: u8) -> (u64, u32) { - ((basin & 0x0F) as u64, 1) -} -fn np_child((p, d): (u64, u32), n: u8) -> (u64, u32) { - if d >= 16 || n >= 16 { - (p, d) - } else { - ((p << 4) | (n & 0x0F) as u64, d + 1) - } -} -fn np_basin((p, d): (u64, u32)) -> u8 { - ((p >> (4 * (d - 1))) & 0x0F) as u8 -} -fn np_is_ancestor((pa, da): (u64, u32), (pb, db): (u64, u32)) -> bool { - da > 0 && da <= db && (pb >> (4 * (db - da))) == pa +// ── hub landing primitives (the REAL merged contract types) ────────────────── +/// The 16ⁿ Abstammung address: basin (DOLCE facet) + P279 descent. +fn nibble_path(basin: u8, children: &[u8]) -> NiblePath { + children.iter().fold(NiblePath::root(basin), |p, &c| p.child(c)) } -fn nibble_path(basin: u8, children: &[u8]) -> (u64, u32) { - children.iter().fold(np_root(basin), |acc, &c| np_child(acc, c)) -} - -// ── hub landing primitives (REAL merged contract types) ────────────────────── -/// Presence bitmask — one bit per present property (`= contract::class_view::FieldMask`). +/// Presence bitmask — one bit per present property. fn presence(props: &[&str]) -> FieldMask { let positions: Vec = (0..props.len() as u8).collect(); FieldMask::from_positions(&positions) } -/// `FieldMask::inherit` (#442) — bitwise union; inlined via the public `.0`. -fn inherit(parent: FieldMask, delta: FieldMask) -> FieldMask { - FieldMask(parent.0 | delta.0) -} /// Structural signature over the canonical (sorted, deduped) property-ID set + /// basin — `= class_signature::StructuralSignature(fnv1a(...) as u32)`. Property /// IDENTITIES (not positions) so person {P21,…} ≠ film {P57,…} though both fill @@ -97,7 +78,7 @@ fn signature(dolce: u8, props: &[&str]) -> u32 { /// Discover `entity-type → DOLCE-facet` from a splat-shaped codebook + data — /// the aerial proposer half. Returns the per-etype discovered `dolce_id`. -fn discover_dolce_ids(classes: &[WClass]) -> std::collections::HashMap { +fn discover_dolce_ids(classes: &[WClass]) -> HashMap { // f0 = entity-type (6), f1 = DOLCE facet (4). Plant each etype ⇒ its facet. let spec = FeatureSpec::new(vec![6, 4]); let mut rows = Vec::new(); @@ -122,7 +103,7 @@ fn discover_dolce_ids(classes: &[WClass]) -> std::collections::HashMap // The proposer emits the stable dolce_id (NOT a hardcoded IRI) — Phase-2 fix. let proj = OntologyProjector::dolce_subclass("wd:"); - let mut out = std::collections::HashMap::new(); + let mut out = HashMap::new(); for r in &rules { if r.antecedent.len() == 1 && r.antecedent[0].feature == 0 && r.consequent[0].feature == 1 { if let Some(dolce) = proj.dolce_id(&r.consequent) { @@ -150,19 +131,23 @@ fn end_to_end_splat_to_wikidata_hhtl() { eprintln!("✓ Stage A/B: aerial recovered all {} DOLCE basins from the splat", classes.len()); // ── Stage C: land each discovered class on the canonical hub types ── - let mut sigs = std::collections::HashSet::new(); - let mut landed = Vec::new(); + let mut sigs = HashSet::new(); for c in &classes { let basin = discovered[&c.etype]; // from DISCOVERY, not hardcoded - let path = nibble_path(basin, c.path); // = NiblePath::root(basin).child(..) - let mask = presence(c.props); // = FieldMask - let sig = signature(basin, c.props); // = StructuralSignature - let triple: (ClassId, u32, FieldMask) = (c.id, sig, mask); // the D-CLS triple + let path = nibble_path(basin, c.path); // contract::hhtl::NiblePath + let mask = presence(c.props); // contract::class_view::FieldMask + let sig = signature(basin, c.props); // StructuralSignature value + let _triple: (ClassId, u32, FieldMask) = (c.id, sig, mask); // the D-CLS triple sigs.insert(sig); - landed.push((c.qid, basin, path, triple)); - assert_eq!(np_basin(path), basin, "basin survives down the nibble path"); + assert_eq!(path.basin(), Some(basin), "basin survives down the nibble path"); assert_eq!(mask.count(), c.props.len() as u32, "one present bit per property"); - eprintln!(" landed {:>9} → basin {basin} path {:#x} sig {sig:#010x} mask {:#06b}", c.qid, path.0, mask.0); + eprintln!( + " landed {:>9} → basin {basin} path {:#x} (depth {}) sig {sig:#010x} mask {:#06b}", + c.qid, + path.packed().0, + path.depth(), + mask.0 + ); } // N4 falsification #1 — the corpus COLLAPSES to fewer shape-families. @@ -178,15 +163,18 @@ fn end_to_end_splat_to_wikidata_hhtl() { let human = classes.iter().find(|c| c.qid == "Q5").unwrap(); let person_path = nibble_path(0, person.path); let human_path = nibble_path(0, human.path); - assert!(np_is_ancestor(person_path, human_path), "person ⊃ human in the nibble tree"); - assert_eq!(np_basin(person_path), np_basin(human_path), "subclassing preserves the DOLCE basin"); + assert!(person_path.is_ancestor_of(human_path), "person ⊃ human in the nibble tree"); + assert_eq!(person_path.basin(), human_path.basin(), "subclassing preserves the DOLCE basin"); let delta = FieldMask::from_positions(&[3]); // human's extra property (P106) bit - assert_eq!(inherit(presence(person.props), delta), presence(human.props), "human mask = person mask ⊕ P106"); + assert_eq!( + presence(person.props).inherit(delta), // FieldMask::inherit + presence(human.props), + "human mask = person mask ⊕ P106" + ); // ── the ndjson the hub reads (the firewall-crossing wire) ── let proj = OntologyProjector::dolce_subclass("wd:"); - // one illustrative rule: person ⇒ Endurant - let rule = lance_graph_arm_discovery::CandidateRule { + let rule = CandidateRule { antecedent: vec![Item::new(0, person.etype)], consequent: vec![Item::new(1, person.dolce as u32)], cooccur: 50, From 7087fdac19626b85a8ba4d1e9c90d4fc733b8042 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 10:36:09 +0000 Subject: [PATCH 5/5] fix(arm-discovery): address PR #443 review (codex P2 + CodeRabbit nitpick) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - codex P2 (ontology.rs): DolceCategory::from_index range-checks the u32 BEFORE the u8 cast. 'category as u8' wrapped (256 → 0), so from_index(256) returned Some(Endurant) — silently routing an invalid index to a wrong DOLCE basin. Now compares in u32 first; added regression asserts (256, u32::MAX → None). - CodeRabbit nitpick (wikidata_landing.rs): split the one broad end-to-end test into 5 focused #[test]s sharing corpus()/helpers — basin_recovery_from_splat, landing_preserves_basin_and_mask, shape_family_collapse, subclass_inherits_path_and_mask, ndjson_seam_contains_predicate — so a failure names the regressed invariant (repo guideline: focused scenarios over broad integration tests). Verified: default 42/42 + clippy clean; --features landing 5/5 + clippy clean. https://claude.ai/code/session_012SorR8UbtEvYmbX8cXftj7 --- .../src/aerial/ontology.rs | 17 ++++- .../tests/wikidata_landing.rs | 70 +++++++++++++------ 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/crates/lance-graph-arm-discovery/src/aerial/ontology.rs b/crates/lance-graph-arm-discovery/src/aerial/ontology.rs index fc74df26..fbdeb5cc 100644 --- a/crates/lance-graph-arm-discovery/src/aerial/ontology.rs +++ b/crates/lance-graph-arm-discovery/src/aerial/ontology.rs @@ -63,11 +63,18 @@ impl DolceCategory { } /// The facet for a consequent **category index** (`0..3`) — the - /// discovery-side view of the cache `dolce_id`. Alias of [`Self::from_basin`] - /// with intent: a DOLCE projector's consequent category IS the `dolce_id`. + /// discovery-side view of the cache `dolce_id`. `None` for any index outside + /// `0..ALL.len()` (a DOLCE projector's consequent category IS the `dolce_id`). #[must_use] pub fn from_index(category: u32) -> Option { - Self::from_basin(category as u8) + // Range-check the u32 BEFORE narrowing: `category as u8` wraps + // (e.g. 256 → 0) and would silently route an invalid index to a wrong + // basin (codex P2). Compare in u32, then the cast is provably in range. + if category < Self::ALL.len() as u32 { + Self::from_basin(category as u8) + } else { + None + } } } @@ -193,6 +200,10 @@ mod tests { assert_eq!(DolceCategory::from_basin(4), None); assert_eq!(DolceCategory::Quality.iri(), "dolce:Quality"); assert_eq!(DolceCategory::from_index(1), Some(DolceCategory::Perdurant)); + assert_eq!(DolceCategory::from_index(4), None); + // u32→u8 must NOT wrap 256→0 (codex P2): an oversized index is None. + assert_eq!(DolceCategory::from_index(256), None); + assert_eq!(DolceCategory::from_index(u32::MAX), None); } #[test] diff --git a/crates/lance-graph-arm-discovery/tests/wikidata_landing.rs b/crates/lance-graph-arm-discovery/tests/wikidata_landing.rs index c88e89c1..e50ec9c7 100644 --- a/crates/lance-graph-arm-discovery/tests/wikidata_landing.rs +++ b/crates/lance-graph-arm-discovery/tests/wikidata_landing.rs @@ -114,11 +114,18 @@ fn discover_dolce_ids(classes: &[WClass]) -> HashMap { out } +// Focused scenarios (one invariant each, sharing `corpus()` + the helpers) — +// the full splat→discovery→landing flow factored into pinpoint tests so a +// failure names the invariant that regressed (repo guideline: prefer focused +// scenarios over broad integration tests). `find_qid` is the shared lookup. +fn find_qid<'a>(classes: &'a [WClass], qid: &str) -> &'a WClass { + classes.iter().find(|c| c.qid == qid).expect("qid in corpus") +} + #[test] -fn end_to_end_splat_to_wikidata_hhtl() { +fn basin_recovery_from_splat() { + // Stage A/B: aerial recovers every class's DOLCE basin from the splat + data. let classes = corpus(); - - // ── Stage A+B: aerial discovers the basin (dolce_id) from splat + data ── let discovered = discover_dolce_ids(&classes); for c in &classes { assert_eq!( @@ -128,39 +135,52 @@ fn end_to_end_splat_to_wikidata_hhtl() { c.qid ); } - eprintln!("✓ Stage A/B: aerial recovered all {} DOLCE basins from the splat", classes.len()); +} - // ── Stage C: land each discovered class on the canonical hub types ── - let mut sigs = HashSet::new(); +#[test] +fn landing_preserves_basin_and_mask() { + // Stage C: each discovered class lands on NiblePath + FieldMask; the basin + // survives the nibble descent and presence is one bit per property. + let classes = corpus(); + let discovered = discover_dolce_ids(&classes); for c in &classes { let basin = discovered[&c.etype]; // from DISCOVERY, not hardcoded let path = nibble_path(basin, c.path); // contract::hhtl::NiblePath - let mask = presence(c.props); // contract::class_view::FieldMask - let sig = signature(basin, c.props); // StructuralSignature value - let _triple: (ClassId, u32, FieldMask) = (c.id, sig, mask); // the D-CLS triple - sigs.insert(sig); + let triple: (ClassId, u32, FieldMask) = (c.id, signature(basin, c.props), presence(c.props)); assert_eq!(path.basin(), Some(basin), "basin survives down the nibble path"); - assert_eq!(mask.count(), c.props.len() as u32, "one present bit per property"); + assert_eq!(triple.0, c.id); + assert_eq!(triple.2.count(), c.props.len() as u32, "one present bit per property"); eprintln!( - " landed {:>9} → basin {basin} path {:#x} (depth {}) sig {sig:#010x} mask {:#06b}", + " landed {:>9} → basin {basin} path {:#x} (depth {}) sig {:#010x} mask {:#06b}", c.qid, path.packed().0, path.depth(), - mask.0 + triple.1, + triple.2 .0 ); } +} - // N4 falsification #1 — the corpus COLLAPSES to fewer shape-families. - assert!(sigs.len() < classes.len(), "shapes must collapse (6 classes → {} families)", sigs.len()); +#[test] +fn shape_family_collapse() { + // N4 #1: structurally-identical classes collapse to one StructuralSignature. + let classes = corpus(); + let sigs: HashSet = classes.iter().map(|c| signature(c.dolce, c.props)).collect(); + assert!(sigs.len() < classes.len(), "shapes must collapse (6 → {})", sigs.len()); assert_eq!(sigs.len(), 5, "film ≡ tv-series is the one twin"); - let film = classes.iter().find(|c| c.qid == "Q11424").unwrap(); - let tv = classes.iter().find(|c| c.qid == "Q5398426").unwrap(); - assert_eq!(signature(1, film.props), signature(1, tv.props), "film and tv-series share one signature"); - let person = classes.iter().find(|c| c.qid == "Q215627").unwrap(); + let film = find_qid(&classes, "Q11424"); + let tv = find_qid(&classes, "Q5398426"); + assert_eq!(signature(1, film.props), signature(1, tv.props), "film ≡ tv-series share one signature"); + let person = find_qid(&classes, "Q215627"); assert_ne!(signature(0, person.props), signature(1, film.props), "person ≠ film"); +} - // N4 falsification #2 — subclass inherits PATH + MASK-as-delta, basin preserved. - let human = classes.iter().find(|c| c.qid == "Q5").unwrap(); +#[test] +fn subclass_inherits_path_and_mask() { + // N4 #2: human ⊂ person — path extends, basin preserved, mask inherits-as-delta. + let classes = corpus(); + let person = find_qid(&classes, "Q215627"); + let human = find_qid(&classes, "Q5"); let person_path = nibble_path(0, person.path); let human_path = nibble_path(0, human.path); assert!(person_path.is_ancestor_of(human_path), "person ⊃ human in the nibble tree"); @@ -171,8 +191,13 @@ fn end_to_end_splat_to_wikidata_hhtl() { presence(human.props), "human mask = person mask ⊕ P106" ); +} - // ── the ndjson the hub reads (the firewall-crossing wire) ── +#[test] +fn ndjson_seam_contains_predicate() { + // The firewall-crossing wire: aerial emits the {s,p,o,f,c} the hub reads. + let classes = corpus(); + let person = find_qid(&classes, "Q215627"); let proj = OntologyProjector::dolce_subclass("wd:"); let rule = CandidateRule { antecedent: vec![Item::new(0, person.etype)], @@ -184,5 +209,4 @@ fn end_to_end_splat_to_wikidata_hhtl() { let triples = vec![CandidateTriple::from_rule(&rule, &proj, NARS_PERSONALITY_K)]; let nd = to_ndjson(&triples); assert!(nd.contains("\"p\":\"rdfs:subClassOf\"")); - eprintln!("✓ Stage C: corpus collapsed 6 → {} families; ndjson seam:\n{nd}", sigs.len()); }