diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index a02a39483..a64859e5c 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,58 @@ +## 2026-09-01 — E-THREE-BRANCHES-ONE-REGISTER-THE-AUDIT-AFTER-THE-COLLISION-1 + +**Status:** RECONCILIATION AUDIT — #1125/#1126/#1127 merged while this +branch's slices 1+2 were stranded; overlaps measured and corrected on rebase, +convergences kept deliberately. **Confidence:** each line verified against +main, not remembered. + +Three sessions worked the same register in parallel. What each landed: + +| PR | landed | +|---|---| +| #1125 (this branch, pre-strand) | the survey + its correction (node has a value; three readiness states; the 24×i4 answer) | +| #1126 | the I4x32-vs-facet measurement — made and **REVERTED** (operator: disregard, the type is not substrate; durable residue: the V3 register is 12 bytes everywhere, facet lanes are classid(4)+12); plus the palette TSV lanes | +| #1127 | the value-lane census (**G24N4 already ships** as `CausalWitnessFacet`'s loci reading of `ValueTenant::CausalWitness`; that lane is ruled OUT for W2b by its own loci-never-magnitude law + reserved slots `16..24`; 260/480 slab bytes free, append at 220) + `tests/w2b_one_node_field.rs` (5 disable-verified register-level falsifiers incl. the whale, codec borrowed, no lane minted) | +| this branch, stranded → rebased | the DN dissolution + mechanical/epistemic split + one-hop law, with `basin_lanes::BasinLanes`, `accumulate_children`, `hhtl::{missing_ancestors, direct_children}` | + +**Collisions found and corrected (this commit):** + +1. **My "fourth carving / not in the shipped set / only signed one" claim was + FALSE**, and the census had already proven it false while my slices sat + stranded — `G24N4` ships, signed, as loci. `basin_lanes`' module doc, my + two unpushed board entries, and the plan's RULED table are corrected; the + type is re-positioned as what it actually is: **the MAGNITUDE register in + the shipped shape** — precisely the sibling the census concluded must + exist, because the shipped lane's law forbids magnitude on it. +2. **The "which lane — dissolved" row conflated two axes.** The DN ruling + dissolves TREE-siting (no separate map store; the node at each prefix is + the row). The census constrains SLAB-siting (which tenant inside the + 480-byte value), which is real, half-answered (not `CausalWitness`), and + the operator's mint. The plan row now says both. +3. **The whale was pinned twice under two names** — register-level in + `w2b_one_node_field.rs` (borrowed codec) and carrier-level in + `basin_lanes`' tests. Kept BOTH, now cross-referenced in both directions, + so the prior-art trail is one thread instead of two. + +**Convergences, kept as evidence rather than deduped away:** the per-lane +arithmetic (exact signed sum, one clamp) was arrived at INDEPENDENTLY by +`w2b_one_node_field.rs`'s `summarise` and by `accumulate_children` — two +sessions, no shared context, same shape. That is the strongest signal yet +that the shape is right, and it is exactly what the one-hop ruling's +"accumulated including disagreement" needs. + +**Residue flagged, not fixed here:** the i4 sign-extension now has THREE +homes — `atoms::I4x32::sext4` (`pub(crate)`, reused by `basin_lanes`), +`causal_witness`'s inline `((nibble << 4) as i8) >> 4`, and `I4x64`-via- +`I4x32`. A dedup (point `causal_witness` at `sext4`) is a one-line +tech-debt item, deliberately not bundled into a reconciliation commit. + +**Process fact worth keeping:** the stranding happened because a PR merged +while its branch kept receiving pushes — same-branch continuation past a +merge point. The commits were rebased onto main per the merged-PR rule +(kept, not discarded), and this audit ran BEFORE re-pushing, so the +corrections and the stranded work land together rather than the stale +claims landing twice. + ## 2026-09-01 — E-G24N4-ALREADY-SHIPS-AND-THAT-IS-WHY-W2B-CANNOT-USE-IT-1 **Status:** FINDING — measured off `VALUE_TENANTS`, not read off prose. @@ -40,6 +95,123 @@ Census + the disable table: `.claude/plans/dismech-causal-replay-v1.md` §W2b. Falsifier: `crates/lance-graph-contract/tests/w2b_one_node_field.rs`. --- +## 2026-09-01 — E-ONE-HOP-UP-ONE-HOP-DOWN-A-PARENT-SPEAKS-ONLY-ITS-CHILDREN-1 + +**Status:** OPERATOR RULING, third round on `D-DCR-2b` — the field map's +locality law, built against in the same PR. **Confidence:** ruled; one-hop +selector + accumulator shipped with the locality made observable. + +**The law: a parent's register expresses its DIRECT children accumulated — +agreement AND disagreement — never the grandchildren. One hop up and down.** +Grandchild information reaches a grandparent only through the child's own +accumulated register, so the global field map is a COMPOSITION of one-hop +summaries, never a node reaching past its children. Layered concretely: +mammal carries accumulated agree/disagree; whale-family carries +generic-vs-mammal-specific; whale carries specific. The lanes are +upstream/downstream inheritance on the mathematical scale (Shannon +proprioception / EWA sandwich / Mengenlehre readouts over them). + +This is the tree-shaped Chapman-Kolmogorov discipline — the same locality the +substrate already holds twice: `I-SUBSTRATE-MARKOV` (transition composition, +never transitive flattening) and `FieldMask::inherit` (parent ∪ own delta). +And it settles the sweep-convergence question STRUCTURALLY rather than by +policy: a sweep is bottom-up one-hop passes, each node a pure function of its +direct children, idempotent at the fixpoint by construction. + +**Width is a floor, not a cap:** the nibbles can be expanded if necessary, +and a node further up may carry multiple 24×i4 registers where valuable. The +corpora ladder also widened — book / redmine / odoo / **AD** (literally the +DN case) / OWL / RDF — every entry landing in one of the three readiness +states already tabled. + +**Shipped:** `hhtl::direct_children` — the one-hop selector, exactly depth+1 +and never grandchildren, so any accumulator it feeds is *structurally unable* +to violate the law (locality enforced at selection, not policed in +arithmetic); `BasinLanes::accumulate_children` — per-lane saturating signed +sum (agreement stacks, disagreement pulls down, bound = the carrier's own +range; empty → SILENT). The locality is OBSERVABLE across two levels: a +grandchild move absorbed by the child's own saturation leaves the grandparent +byte-identical, while one that moves the child moves the grandparent — the +can-fire/can-stay-silent pair on the law itself. + +**A measured limitation pinned loud rather than hidden:** in ONE register a +balanced conflict (`+3` child, `−3` child, same lane) sums to `0` — +indistinguishable from silence, the whale-erasure failure mode one level up. +`a_balanced_conflict_collapses_to_silence_in_one_register` pins it as CURRENT +behaviour with a comment that the multi-register expansion must fail this pin +and force a deliberate re-shape. The expansion's semantics (net + +contested-mass?) are the operator's to shape — the pin is the case FOR it, +not a design of it. + +## 2026-09-01 — E-ASKING-WHERE-THE-MAP-LIVES-IS-ASKING-WHERE-THE-OUS-ARE-IN-A-DN-1 + +**Status:** OPERATOR CORRECTION + RULING, second round on `D-DCR-2b` — three of +four "open decisions" dissolved as category errors; the hydrate question +answered with a load-bearing split. **Confidence:** ruled; first carrier + +skeleton primitive shipped in the same PR. + +The survey's remaining "decisions" asked where the field map is WRITTEN (which +lane / tenant / Lance column), whether it is versioned, and at what granularity +a sweep runs. The operator's correction, by analogy: that is asking where the +OUs are in a distinguished name. **An HHTL position is addressed by its path, +and every truncation of the path is itself an entry** — `hhtl::NiblePath` +already ships `parent()`, `prefix(depth)`, `is_ancestor_of`. The basin-level +node is the row whose deeper tiers are zero, same store, same stride, same key +layout. There is nothing to site: + +| former decision | verdict | +|---|---| +| which lane / tenant / column | dissolved — the node at each prefix IS the row | +| versioned or live | dissolved — rows are Lance-versioned because every row is | +| sweep granularity | dissolved — a sweep is scoped by a prefix; one mechanism, caller-chosen depth | +| does the sweep eliminate | already settled by the three-kinds ruling: the sweep records; elimination is kind 2's separate reading | + +**Every node is an SoA row**, and the three corpora differ only in how much of +the tree already has rows: books — not yet minted by **DeepNSM-v2** (v2, not +v1), the ontology is built from scratch; rails without nodes — at least the +HIERARCHY exists; OWL — parent/child almost always preserved. + +**The ruling that makes incomplete hydration safe to fix: mechanical hydration +is NOT original causality.** Minting a row at a nameable DN from the hierarchy +alone is MECHANICAL — structure only (address, `is_a`/`part_of`, presence), +runnable in all three corpora. Original causality predicates — the dismech +palette, Tarski-precise assertions, the signed agreement lanes — are epistemic +knowledge: evidence-borne, provenance-carrying, never fabricated by a mint. A +mint that writes a nonzero lane is structure impersonating knowledge. The +mechanical mint's epistemic output is exactly SILENCE — all lanes zero — which +is the zero-fallback ladder holding one level up. + +**Shipped against the ruling, both zero-dep, both in contract:** + +- `basin_lanes::BasinLanes` — the 24 × i4 (`G24N4`-shape) MAGNITUDE register + over the node's own 12-byte register (reuses `I4x32::sext4`, now + `pub(crate)`, rather than another copy of the nibble codec). [Corrected + post-rebase against the #1127 census: G24N4 was NOT greenfield — it ships + as `CausalWitnessFacet`'s loci reading; what is new here is the magnitude + semantics that lane's law forbids. See the audit entry above.] `SILENT` is the default and the zero register; + `is_silent()` is the checkable half of the mint rule. The whale case holds + at the carrier level: a negative lane is RECORDED disagreement, countable, + round-tripping, distinct from both silence and agreement — and nothing else + in the register moves, so nothing was removed. +- `hhtl::missing_ancestors` — the mechanical-hydration address list: every + strict ancestor DN implied by the occupied paths that is not itself + occupied, shallow→deep so parents mint before children, `EMPTY` ignored and + never yielded. **Returns addresses only** — the signature cannot carry a + lane value, which is the type-level half of the mechanical-vs-epistemic + split. State (c) exactly: the partial-hydration test occupies the basin and + the leaf and gets back only the gap. + +**Generalizable:** when a scoping question keeps resisting an answer, check +whether the addressing scheme already answers it. A DN-shaped substrate has no +"where" questions — every nameable position is its own site — and a survey +still asking "where" is evidence the surveyor is holding a table-shaped model +of a trie-shaped store. + +**Still open, probes not rulings:** the lane-filling arithmetic (child stance → +signed value; sibling merge at the parent — whether `semiring.add` is right for +SIGNED agreement is unmeasured) and the provenance marker (where a row records +mechanically-hydrated vs original, so state (c) stays distinguishable from +state (b) after the mint — surfaced, not invented unilaterally). ## 2026-09-01 — E-AN-HHTL-POSITION-IS-A-NODE-AND-A-NODE-HAS-A-VALUE-1 @@ -76,7 +248,10 @@ payload is 12 bytes = **24 nibbles**. Every shipped `CascadeShape` carves it at BYTE granularity (`G6D2` 6×2, `G4D3` 4×3, `G3D4` 3×4 — all `CASCADE_UNITS == 12`); a nibble-granular reading is not in the shipped set. Signed i4 in `[−8, 7]` is shipped carrier semantics (`atoms::I4x32::sext4`) at other widths -only. So 24×i4 is a fourth reading of the same register, and the plan's +only. So 24×i4 is a nibble-granular reading of the same register [post-rebase +correction: not the FIRST such reading — the #1127 census measured that +`G24N4` already ships as `CausalWitnessFacet`'s loci reading; the new thing +is magnitude semantics, see the audit entry above], and the plan's recorded candidate `atoms::I4x32` was wrong — it matched the name (32 lanes, 16 B) and nothing else. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 1a16544b1..f7ff23f83 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -7,7 +7,7 @@ | D-DCR-0a | prior-art reconciliation: `contract::dismech_evidence` + `dismech-causality-v3-v1` §11 arms (2,449 / 4,076 / 361) are W1-W3's falsifier; plan §3a | **Shipped** (E-W0-MEASURED-THE-MASK-HALF-DOMINATES-...-1) | | D-DCR-1 | replay core: loco calls under the dismech vocabulary -> CausalEdge64/NarsTruth steps -> temporal.rs trace; determinism + perturbation falsifiers | **Shipped (#1120, merged `cc0046f8`)** + follow-up in PR — `lance-graph-planner/src/dismech_replay.rs` (`replay_step` / `replay_chain` / `first_divergence` / `ReplayTraceRow: LocalCausalRow`); 4 gates, 3 disable-verified red-then-green. Palette binds at the membrane (plain `u8` ordinal here); the caller supplies a durable `base_seq` and the planner DERIVES each row's `cast_seq` from it — nothing here mints a counter. (Wording corrected per CodeRabbit #1120: the earlier phrasing said `cast_seq` was caller-supplied, which reverses the API contract.) Membrane half CLOSED: contract `dismech_evidence::DISMECH_PREDICATES` (zero-dep 19-row mirror, floor 0x90, position lookup) + armed-tier fuse `lance_graph_ogar::parity::assert_dismech_palette_parity` against the real `ogar_dismech::RELATIONS`, both directions, 3 more disables verified. Codex #1120: 3 findings, all valid — `ReplayTraceRow.predicate` now carried as WITNESS (the P1 falsified the module's own doc claim), `first_divergence` contract narrowed to content `(predicate, edge)` (the review's literal whole-row remedy was measured and rejected), `next_base_seq` makes the per-STEP durable reservation explicit. 9 disables total. CodeRabbit #1120 (4 more, read after merge): board wording corrected; `validate_chain` + `UnmintedOrdinal` reject an out-of-band ordinal AT ADMISSION while replay stays total over history; `replay_chain -> Result` with `ReplayError::SequenceExhausted` checks the whole reservation up front (`base_seq + i` panicked in debug / wrapped in release at u64::MAX). 11 disables total; 10 module gates. PR #1122 review (both reviewers, same bug): `next_base_seq` saturated and handed back an ALREADY-MINTED coordinate at the top of the range — a duplicate `cast_seq`, with the test pinning it as "the saturating guard". Now `Option`; exhaustion is representable. Also corrected: admission = FIRST acceptance, never re-reading the durable log (the old wording contradicted its own replay-must-not-refuse-history argument). 12 disables | | D-DCR-2 | Mengenlehre candidate evaluation via `contract::revision::EvidenceMask` (support ∩ / refute ∖ over `dismech_evidence::Supports`) | **In PR** — `lance-graph-planner/src/dismech_candidates.rs` (`EvidenceItem` / `apply` / `evaluate` / `Evaluation` / `is_informative`). Only `Support` and `Refute` are set operations; `Partial` and `NoEvidence` are INERT by design (full-strength elimination must not be bought with partial evidence, and an asserted absence is not a licence to cut) — reported via `decisive`, never silently dropped. `narrowing` separates "decisive by stance" from "actually taught something", the primitive W5's frontier needs. 6 gates, 4 disable-verified. Spec corrected in preflight: the refute class is the evidence STANCE (`Supports`, shipped + measured), NOT the graph-construction skip filter the plan first named. The skip filter decides whether an item becomes an edge at all, so a candidate set built from the graph has already excluded it — `∖` would subtract twice. Plan §W2 carries the full correction | -| D-DCR-2b | **the field map** — propagate precision about a knowledge stage over the WHOLE field; agreement / disagreement / support chains / MISSING LINKS into the HHTL nodes; the boring `is_a`/`part_of` rails lifted into a causality graph with propagated node edges | **Queued — the substrate's real product** (operator ruling 2026-09-01: three kinds of Mengenlehre; W2 shipped only kind 3, the question mask). Kind 2 (threshold elimination — Shannon / EWA / Hambly / Lyons) is a READING of this map and belongs with W4. **Carrier named 2026-09-01** (`E-AN-HHTL-POSITION-IS-A-NODE-AND-A-NODE-HAS-A-VALUE-1`): an HHTL position is an SoA node whose VALUE lane carries the 12-byte payload read as **24 signed i4** lanes — `+` agreement / `−` disagreement / `0` silence. Still unruled: which lane, versioned vs live, sweep granularity, and a node-level hydrate step for rail-implicit positions. **Census + one-node falsifier shipped 2026-09-01** (`E-G24N4-ALREADY-SHIPS-AND-THAT-IS-WHY-W2B-CANNOT-USE-IT-1`): `G24N4` already ships on `ValueTenant::CausalWitness`, so the carrier is not greenfield — and its operator-locked loci-never-magnitude value law plus its reserved slots `16..24` rule that lane OUT for W2b; 260 of 480 slab bytes free, so space is not the constraint. `tests/w2b_one_node_field.rs` pins the carrier at one-node scale (5 falsifiers, each disable-verified) incl. the whale case; it mints no lane and reserves no byte, and gap 3 (sweep convergence) is untouched | +| D-DCR-2b | **the field map** — propagate precision about a knowledge stage over the WHOLE field; agreement / disagreement / support chains / MISSING LINKS into the HHTL nodes; the boring `is_a`/`part_of` rails lifted into a causality graph with propagated node edges | **In progress** (operator ruling 2026-09-01: three kinds of Mengenlehre; W2 shipped only kind 3, the question mask). Kind 2 (threshold elimination — Shannon / EWA / Hambly / Lyons) is a READING of this map and belongs with W4. **Carrier named 2026-09-01** (`E-AN-HHTL-POSITION-IS-A-NODE-AND-A-NODE-HAS-A-VALUE-1`): an HHTL position is an SoA node whose VALUE lane carries the 12-byte payload read as **24 signed i4** lanes — `+` agreement / `−` disagreement / `0` silence. Still unruled: which lane, versioned vs live, sweep granularity, and a node-level hydrate step for rail-implicit positions. **Census + one-node falsifier shipped 2026-09-01** (`E-G24N4-ALREADY-SHIPS-AND-THAT-IS-WHY-W2B-CANNOT-USE-IT-1`): `G24N4` already ships on `ValueTenant::CausalWitness`, so the carrier is not greenfield — and its operator-locked loci-never-magnitude value law plus its reserved slots `16..24` rule that lane OUT for W2b; 260 of 480 slab bytes free, so space is not the constraint. `tests/w2b_one_node_field.rs` pins the carrier at one-node scale (5 falsifiers, each disable-verified) incl. the whale case; it mints no lane and reserves no byte, and gap 3 (sweep convergence) is untouched. **Slices 1+2 shipped 2026-09-01 (this branch)**: DN dissolution + mechanical/epistemic split + one-hop law; `basin_lanes::BasinLanes` (magnitude register, G24N4 shape) + `accumulate_children` (one-hop, exact-sum-then-clamp) + `hhtl::{missing_ancestors, direct_children}`. Open: multi-register contested-mass semantics, provenance marker, tenant mint for the magnitude register (census: NOT CausalWitness; append margin at slab 220) | | D-DCR-3 | counterfactual replay (edge cut through `contract::counterfactual`, Pearl rung 3), two-sided load-bearing/redundant gates | **In PR** — `lance-graph-planner/src/dismech_counterfactual.rs`. Both arms go through W1's `replay_chain` (no second replay path); the cut arm reserves the range AFTER the factual one and is tagged `InferenceType::Counterfactual` (−6) so the road not taken can never read as observed truth. **Measured correction:** the verdict reads FREQUENCY, not confidence — confidence saturates at 170 across every fixture, so a confidence bar would have been a vacuous threshold. `EdgeRole` carries the cut edge's own `CausalTopology` (59-60) + `ReasoningBand` (61-63) so "explains" stays distinguishable from "relates to". Also lands `impl EpisodicEdge for CausalEdge64` (the bridge `contract::counterfactual` documents as BLOCKED — the planner is the first crate depending on both sides). 8 gates, 4 disable-verified | | D-DCR-4 | Σ transport via `jc::ewa_sandwich` + candidate-entropy readout; entropy-surface CONSOLIDATION decision recorded first | Queued | | D-DCR-5 | frontier scheduling (info-gain / rung-cost via `EpistemicMode::for_rung`) | **HELD** — operator rung 5-9 table ruling + W0 KILL check | diff --git a/.claude/plans/dismech-causal-replay-v1.md b/.claude/plans/dismech-causal-replay-v1.md index 78ebb9a43..6bc975872 100644 --- a/.claude/plans/dismech-causal-replay-v1.md +++ b/.claude/plans/dismech-causal-replay-v1.md @@ -317,7 +317,8 @@ facet register read at nibble granularity. single-sweep-per-version is a substrate decision with real cost. **Still fully open.** 4. **A node-level hydrate primitive for state (c).** Absent, and it gates (a) - and (c) both. + and (c) both. **⊘ RULED buildable (see below): it is MECHANICAL — structure + from the hierarchy only, epistemic lanes zero.** #### The value-lane census — measured, 2026-09-01 (`D-DCR-2b`) @@ -403,15 +404,93 @@ carrier to stay byte-identical while the disagreement stays legible. #### The decisions this wave still cannot make for itself -- **Which value lane** the 24×i4 summary occupies, and whether it is a new - `ValueTenant` or a carve inside an existing one. -- **Versioned or live.** `temporal.rs`'s sorted stream and Lance versions make - "the map at version v" expressible; a live mutable map does not fit the - zero-copy envelope story. -- **Sweep granularity** — whole field, one classid, one HHTL subtree. -- **Whether kind 2 reads the map or the sweep applies it.** The ruling puts the - threshold in kind 2, which argues the sweep never eliminates and elimination - is always a separate read. +#### ⊘ RULED (operator, 2026-09-01, second round) — the DN dissolution + the two-layer hydration split + +**The siting questions were a category error**, caught with the sharpest +possible analogy: asking where the map is written is asking where the OUs are +in a distinguished name. An HHTL position is addressed by its path, and every +truncation of the path is itself an entry — `hhtl::NiblePath` already ships +`parent()`, `prefix(depth)`, `is_ancestor_of`. The basin-level node is the row +whose deeper tiers are zero, in the same store, same stride, same key layout. +Three of the four "decisions" dissolve: + +| former decision | verdict | +|---|---| +| which value lane / tenant / column | **split, on reconciliation with the #1127 census**: the TREE-siting half is dissolved — the node at each prefix IS the row, no separate map store. The SLAB half (which `ValueTenant` inside the 480-byte value carries the magnitude register) is real and census-constrained: NOT `CausalWitness` (loci-never-magnitude law + reserved slots); append margin at slab 220, 260 B free; the mint is the operator's | +| versioned or live | **dissolved** — rows are Lance-versioned because every row is | +| sweep granularity | **dissolved** — a sweep is scoped by a prefix; field / classid / subtree is one mechanism at three depths | +| does the sweep eliminate | settled by the three-kinds ruling itself: the sweep RECORDS; elimination is kind 2's threshold READING, always a separate act | + +**Every node is an SoA row**, and the three corpora differ only in how much of +the tree already has rows: + +| corpus | what exists before the wave | +|---|---| +| books | nothing — the SoA rows are not yet minted by **DeepNSM-v2** (v2, not v1); the ontology is built from scratch | +| rails without nodes | at least the HIERARCHY — parent/child is nameable even where no row sits | +| OWL / any ontology | parent/child almost always preserved; rows largely present | + +In most of the three, hydration is incomplete — and the ruling's load-bearing +split is what keeps that safe to fix: + +**Mechanical hydration ≠ original causality predicates.** + +- **Mechanical hydration** mints the SoA row at a nameable DN from the + hierarchy alone. It is structural, runs in all three corpora (TOC skeleton, + rail parent/child, OWL parent/child), and writes STRUCTURE ONLY: address, + `is_a`/`part_of` edges, presence. Its epistemic output is exactly **silence** + — every agreement lane `0` — which is the zero-fallback ladder holding one + level up: an unwritten lane reads as absent, never as an assertion. +- **Original causality predicates** — the dismech palette (`causes`, + `explains`, `relates_to`, …), Tarski-precise assertions, the signed + agreement lanes — are epistemic knowledge. They arrive only from evidence + and propagation, carry provenance, and are NEVER fabricated by the minting + step. A mint that writes a nonzero lane is the wave's hardest defect class: + structure impersonating knowledge. + +#### ⊘ RULED (operator, 2026-09-01, third round) — one hop up, one hop down + +The corpora list is wider than the three-state table suggested, and every +entry lands in one of the same states: **book** (skeleton to mint), **redmine +/ odoo / AD** (hierarchy present — AD literally IS the DN case), **OWL / RDF** +(parent/child almost always preserved). Same ladder, more members. + +The register is a floor, not a cap: **the nibbles can be expanded if +necessary**, and a node further up may carry **multiple 24×i4 registers** +where that proves valuable (mammal: accumulated agree/disagree; whale family: +generic vs mammal-specific; whale: specific). The lanes are upstream/ +downstream inheritance on the mathematical scale — Shannon proprioception / +EWA sandwich / Mengenlehre readouts over them. + +**The hard constraint, load-bearing:** a parent's register expresses its +DIRECT children accumulated — agreement AND disagreement — never the +grandchildren. **One hop up and down.** Grandchild information reaches a +grandparent only through the child's own accumulated register; the global +field map is a composition of one-hop summaries. This is the tree-shaped +Chapman-Kolmogorov discipline (`I-SUBSTRATE-MARKOV`) and the same locality +`FieldMask::inherit` already has (parent ∪ own delta), and it settles the +sweep's convergence question structurally: bottom-up one-hop passes, each +node a pure function of its direct children. + +Shipped against it (slice 2): `hhtl::direct_children` (the one-hop selector — +exactly depth+1, never grandchildren, so any accumulator it feeds is +structurally unable to reach past the children) and +`BasinLanes::accumulate_children` (per-lane saturating signed sum; empty → +SILENT). Locality is made observable across two levels: a grandchild move +absorbed by the child's own saturation leaves the grandparent byte-identical; +one that moves the child moves the grandparent. + +#### Still open — probes, not rulings + +- **The contested-collapse.** In ONE register a balanced conflict (`+3` vs + `−3` on a lane) sums to `0` — indistinguishable from silence. Pinned as a + test (`a_balanced_conflict_collapses_to_silence_in_one_register`) so it + stays loud; it is the concrete case for the ruled multi-register expansion, + whose semantics (net + contested-mass? per-band?) are the operator's to + shape, not inferred here. +- **The provenance marker**: where a row records mechanically-hydrated vs + original, so state (c) stays distinguishable from state (b) after the mint. + Not invented unilaterally; surfaced as the next placement question. #### Falsifiers this wave would owe diff --git a/crates/lance-graph-contract/src/atoms.rs b/crates/lance-graph-contract/src/atoms.rs index 285ee4059..041288ced 100644 --- a/crates/lance-graph-contract/src/atoms.rs +++ b/crates/lance-graph-contract/src/atoms.rs @@ -118,9 +118,11 @@ impl I4x32 { /// Sign-extend a 4-bit two's-complement nibble to `i8` in `[−8, 7]`. /// /// Carrier-owned (the nibble codec belongs to the carrier, not a free fn); - /// `I4x64` reuses it via `I4x32::sext4`. + /// `I4x64` reuses it via `I4x32::sext4`, and so does + /// `basin_lanes::BasinLanes` (the same codec at the node's 24-lane + /// width) — crate-visible for exactly those carriers, never a free fn. #[inline] - const fn sext4(nibble: u8) -> i8 { + pub(crate) const fn sext4(nibble: u8) -> i8 { (((nibble & 0x0F) << 4) as i8) >> 4 } } diff --git a/crates/lance-graph-contract/src/basin_lanes.rs b/crates/lance-graph-contract/src/basin_lanes.rs new file mode 100644 index 000000000..e92a2cd29 --- /dev/null +++ b/crates/lance-graph-contract/src/basin_lanes.rs @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +//! `basin_lanes` — the **24 × i4** agreement-lane reading of the 12-byte +//! content-blind register (`D-DCR-2b`, the field map's value-side carrier). +//! +//! An HHTL position is a node, and the node's VALUE summarises the position's +//! children so the node speaks for itself without a second read. The carrier +//! is the register the node already owns — 12 bytes — read at NIBBLE +//! granularity: 24 signed i4 lanes in `[−8, 7]`, the **`G24N4`** shape. +//! +//! `G24N4` is NOT new here (the #1127 census corrected an earlier claim that +//! it was): it already ships as +//! [`causal_witness::CausalWitnessFacet`](crate::causal_witness::CausalWitnessFacet)'s +//! reading of `ValueTenant::CausalWitness` — but THAT lane's value law is +//! operator-locked to **loci, never strength/magnitude** (context pointers +//! into the ±8 Markov window), and its slots `16..24` are reserved. A W2b +//! child-summary is exactly the magnitude case that law forbids there. This +//! type is therefore the MAGNITUDE register in the same shape — the sibling +//! the census concluded must exist ("W2b needs its own lane") — and which +//! `ValueTenant` it occupies is the operator's mint (census: append margin at +//! slab offset 220, 260 bytes free), not decided here. +//! +//! # The sign IS the semantics +//! +//! One lane, three cells: **positive = agreement, negative = disagreement, +//! zero = silence.** The whale case falls out of the carrier itself — a whale +//! disagreeing with the mammal neighbourhood is a NEGATIVE lane, a value on +//! the node, never a removal from the set. And silence stays distinct from +//! denial (`0` vs any negative), the same distinction +//! [`Supports::NoEvidence`](crate::dismech_evidence::Supports) refuses to +//! collapse on the evidence side. +//! +//! # Mechanical hydration writes NOTHING here (operator-ruled, 2026-09-01) +//! +//! Minting a row at a nameable DN (from a book TOC, a rail's parent/child, an +//! OWL hierarchy) is MECHANICAL — structure only. Its epistemic output is +//! exactly [`SILENT`](BasinLanes::SILENT): all 24 lanes zero, the +//! zero-fallback ladder holding one level up. Original causality predicates — +//! the dismech palette, Tarski-precise assertions, these signed lanes — come +//! only from evidence and propagation. A mint that writes a nonzero lane is +//! structure impersonating knowledge; [`is_silent`](BasinLanes::is_silent) is +//! the checkable half of that rule. +//! +//! The lane-filling ARITHMETIC (how a child's stance becomes a signed value, +//! how siblings merge at the parent) is deliberately NOT here — it is +//! unmeasured, and per the plan it is a probe, not a codec. This module is +//! the carrier only. + +use crate::atoms::I4x32; + +/// Lanes in one register: 12 bytes × 2 nibbles. +pub const BASIN_LANES: usize = 24; + +/// Bytes the lanes occupy — the node's content-blind register width. +pub const BASIN_LANE_BYTES: usize = 12; + +/// Packed 24-lane signed-i4 agreement register (12 bytes, two lanes per +/// byte). Same nibble codec as [`I4x32`] / `I4x64` at the node's own width. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct BasinLanes { + bytes: [u8; BASIN_LANE_BYTES], +} + +impl BasinLanes { + /// Epistemic silence: every lane `0`. The ONLY value mechanical + /// hydration may leave behind, and what an unwritten register reads as — + /// absent, never an assertion. + pub const SILENT: Self = Self { + bytes: [0; BASIN_LANE_BYTES], + }; + + /// Pack 24 signed lanes, saturating to `[−8, 7]`. Two's-complement + /// nibble: lane `2k` → low nibble of byte `k`, lane `2k+1` → high. + #[must_use] + pub fn pack(lanes: &[i8; BASIN_LANES]) -> Self { + let mut bytes = [0u8; BASIN_LANE_BYTES]; + for (k, b) in bytes.iter_mut().enumerate() { + let lo = lanes[2 * k].clamp(-8, 7) as u8 & 0x0F; + let hi = lanes[2 * k + 1].clamp(-8, 7) as u8 & 0x0F; + *b = (hi << 4) | lo; + } + Self { bytes } + } + + /// Unpack to signed lanes (sign-extended i4, `[−8, 7]`). + #[must_use] + pub fn unpack(&self) -> [i8; BASIN_LANES] { + let mut out = [0i8; BASIN_LANES]; + for (k, b) in self.bytes.iter().enumerate() { + out[2 * k] = I4x32::sext4(b & 0x0F); + out[2 * k + 1] = I4x32::sext4(b >> 4); + } + out + } + + /// The register's raw little-endian bytes, as they sit in the row. + #[must_use] + pub const fn to_le_bytes(&self) -> [u8; BASIN_LANE_BYTES] { + self.bytes + } + + /// Read a register from its raw bytes — total, no failure mode: every + /// 12-byte pattern is 24 representable lanes. + #[must_use] + pub const fn from_le_bytes(bytes: [u8; BASIN_LANE_BYTES]) -> Self { + Self { bytes } + } + + /// Is every lane zero? `true` = epistemic silence — a mechanically + /// hydrated (or never-written) register. The mint-side guard: a hydration + /// step must leave this `true`. + #[must_use] + pub fn is_silent(&self) -> bool { + *self == Self::SILENT + } + + /// How many lanes carry a NEGATIVE value — recorded disagreement. A + /// disagreeing child is a value here, never a removal from the set. + #[must_use] + pub fn disagreement_count(&self) -> usize { + self.unpack().iter().filter(|&&v| v < 0).count() + } + + /// How many lanes carry a POSITIVE value — recorded agreement. + #[must_use] + pub fn agreement_count(&self) -> usize { + self.unpack().iter().filter(|&&v| v > 0).count() + } + + /// **One-hop accumulation** (operator-ruled, 2026-09-01): a parent's + /// register expresses its DIRECT children accumulated — agreement AND + /// disagreement — never the grandchildren. Grandchild information reaches + /// a grandparent only through the child's own accumulated register, one + /// hop at a time; the global field map is the composition of one-hop + /// summaries, never a node reaching past its children (the same locality + /// that makes `FieldMask::inherit` a parent∪delta and the substrate + /// Markov, `I-SUBSTRATE-MARKOV`). + /// + /// Per-lane merge: **exact signed sum, clamped once to `[−8, 7]`** — + /// agreement stacks, disagreement pulls down, the bound is the carrier's + /// own range. Exact-then-clamp (not stepwise saturation) makes the merge + /// associative and commutative EXACTLY, not merely in expectation. + /// + /// **Measured limitation, pinned rather than hidden:** in ONE register a + /// balanced conflict (`+3` child and `−3` child on the same lane) sums to + /// `0` — indistinguishable from silence. That is the concrete case for + /// the ruled escape hatch "the nibbles can be expanded if necessary / + /// multiple 24×i4 further up": a contested-mass register beside the net + /// register. NOT built here — the multi-register semantics are the + /// operator's to shape; the collapse is pinned by + /// `a_balanced_conflict_collapses_to_silence_in_one_register` so the gap + /// stays loud. + /// + /// `accumulate_children(&[])` is [`SILENT`](Self::SILENT) — a childless + /// position asserts nothing. + #[must_use] + pub fn accumulate_children(children: &[Self]) -> Self { + // Exact i32 sums, ONE clamp at pack: per-step saturation would make + // the result depend on child ORDER for mixed signs ([+7,+7,−7] is 0 + // stepwise but 7 summed) — caught by this module's own disable-run + // when the stepwise version survived a wrapping_add disable (pack's + // clamp masked it). Order-independence is pinned by + // `accumulation_is_independent_of_child_order`. + let mut acc = [0i32; BASIN_LANES]; + for c in children { + let lanes = c.unpack(); + for (a, v) in acc.iter_mut().zip(lanes.iter()) { + *a += i32::from(*v); + } + } + let mut out = [0i8; BASIN_LANES]; + for (o, a) in out.iter_mut().zip(acc.iter()) { + *o = (*a).clamp(-8, 7) as i8; + } + Self::pack(&out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::facet::{CascadeShape, CASCADE_UNITS}; + + #[test] + fn round_trips_every_representable_lane_value() { + for val in -8i8..=7 { + let lanes = [val; BASIN_LANES]; + assert_eq!( + BasinLanes::pack(&lanes).unpack(), + lanes, + "lane value {val} must round-trip" + ); + } + // A mixed pattern too — a uniform fixture cannot see a lane-order + // swap (lane 2k vs 2k+1 confusion round-trips on uniform input). + let mut mixed = [0i8; BASIN_LANES]; + for (i, l) in mixed.iter_mut().enumerate() { + *l = (i as i8 % 16) - 8; + } + assert_eq!(BasinLanes::pack(&mixed).unpack(), mixed); + } + + #[test] + fn saturates_outside_the_i4_range_instead_of_wrapping() { + assert_eq!( + BasinLanes::pack(&[100; BASIN_LANES]).unpack(), + [7; BASIN_LANES] + ); + assert_eq!( + BasinLanes::pack(&[-100; BASIN_LANES]).unpack(), + [-8; BASIN_LANES] + ); + // Just outside each bound: a wrapping codec would flip the SIGN here, + // which for this carrier turns agreement into disagreement — the + // worst possible corruption, so the boundary is pinned exactly. + assert_eq!( + BasinLanes::pack(&[8; BASIN_LANES]).unpack(), + [7; BASIN_LANES] + ); + assert_eq!( + BasinLanes::pack(&[-9; BASIN_LANES]).unpack(), + [-8; BASIN_LANES] + ); + } + + #[test] + fn silence_is_the_default_and_the_zero_register() { + // Zero-fallback one level up: an unwritten register IS silence. + assert_eq!(BasinLanes::default(), BasinLanes::SILENT); + assert!(BasinLanes::from_le_bytes([0; BASIN_LANE_BYTES]).is_silent()); + assert!(BasinLanes::SILENT.is_silent()); + assert_eq!(BasinLanes::SILENT.disagreement_count(), 0); + assert_eq!(BasinLanes::SILENT.agreement_count(), 0); + } + + /// The whale case at the carrier level, two-sided: disagreement is a + /// VALUE (recorded, countable, round-trips) and is distinct from BOTH + /// silence and agreement. A carrier that collapsed `−` into `0` (or into + /// removal) fails all three arms. + #[test] + fn a_negative_lane_is_recorded_disagreement_not_silence_and_not_removal() { + let mut lanes = [1i8; BASIN_LANES]; // a mammal neighbourhood agreeing + lanes[3] = -5; // the whale + let reg = BasinLanes::pack(&lanes); + assert!(!reg.is_silent(), "a disagreeing lane is not silence"); + assert_eq!(reg.disagreement_count(), 1, "the whale is RECORDED"); + assert_eq!( + reg.agreement_count(), + BASIN_LANES - 1, + "the rest of the neighbourhood is untouched — nothing was removed" + ); + assert_eq!( + reg.unpack()[3], + -5, + "the disagreement value itself survives" + ); + } + + /// Missing ≠ refuted, at the carrier level: lane 0 (silence) and a + /// negative lane are different cells. Collapsing them is the same error + /// class as `NoEvidence` narrowing a set. + #[test] + fn silence_and_denial_are_different_cells() { + let mut lanes = [0i8; BASIN_LANES]; + lanes[7] = -1; + let denied = BasinLanes::pack(&lanes); + lanes[7] = 0; + let silent = BasinLanes::pack(&lanes); + assert_ne!(denied, silent); + assert_eq!(denied.disagreement_count(), 1); + assert_eq!(silent.disagreement_count(), 0); + assert!(silent.is_silent()); + } + + /// The register width is the SAME 12 units every CascadeShape carves — + /// this reading adds no bytes to the node, it re-reads what is there. + #[test] + fn the_lane_register_is_the_cascade_register_re_read_at_nibble_grain() { + assert_eq!(BASIN_LANE_BYTES, CASCADE_UNITS); + assert_eq!(BASIN_LANES, 2 * CASCADE_UNITS); + for s in CascadeShape::ROTATIONS { + assert_eq!( + s.groups() as usize * s.levels() as usize, + BASIN_LANE_BYTES, + "every byte-granular shape covers the same register these lanes re-read" + ); + } + } + // ---- one-hop accumulation ---- + + #[test] + fn accumulation_stacks_agreement_and_records_disagreement_as_pull_down() { + let mut a = [0i8; BASIN_LANES]; + let mut b = [0i8; BASIN_LANES]; + a[0] = 2; + b[0] = 3; // both agree on lane 0 + a[1] = 4; + b[1] = -1; // contested lane 1: net stays positive but is PULLED DOWN + let acc = BasinLanes::accumulate_children(&[BasinLanes::pack(&a), BasinLanes::pack(&b)]); + let lanes = acc.unpack(); + assert_eq!(lanes[0], 5, "agreement stacks"); + assert_eq!( + lanes[1], 3, + "disagreement is IN the accumulation, not dropped" + ); + assert!( + lanes[2..].iter().all(|&v| v == 0), + "untouched lanes stay silent" + ); + } + + #[test] + fn accumulation_saturates_at_the_carrier_bound_in_both_directions() { + let up = BasinLanes::pack(&[5; BASIN_LANES]); + let down = BasinLanes::pack(&[-5; BASIN_LANES]); + assert_eq!( + BasinLanes::accumulate_children(&[up, up, up]).unpack(), + [7; BASIN_LANES] + ); + assert_eq!( + BasinLanes::accumulate_children(&[down, down, down]).unpack(), + [-8; BASIN_LANES] + ); + } + + #[test] + fn a_childless_position_accumulates_to_silence() { + assert!(BasinLanes::accumulate_children(&[]).is_silent()); + } + + /// Pinned MEASURED LIMITATION, not desired behaviour: in ONE register a + /// balanced conflict is indistinguishable from silence. This is the + /// concrete case for the ruled multi-register expansion; when that lands + /// this pin must fail and force the deliberate re-shape. + #[test] + fn a_balanced_conflict_collapses_to_silence_in_one_register() { + let mut a = [0i8; BASIN_LANES]; + let mut b = [0i8; BASIN_LANES]; + a[5] = 3; + b[5] = -3; + let acc = BasinLanes::accumulate_children(&[BasinLanes::pack(&a), BasinLanes::pack(&b)]); + assert!( + acc.is_silent(), + "one net register cannot carry contested-ness; the day it can, re-pin" + ); + } + + /// One-hop locality made OBSERVABLE across two levels: the grandparent + /// reads the grandchild only through the child's accumulated register. + /// Can-fire: a grandchild move that moves the child moves the + /// grandparent. Silence: a grandchild move ABSORBED by the child's own + /// saturation leaves the grandparent byte-identical. + #[test] + fn a_grandchild_reaches_the_grandparent_only_through_the_child() { + use crate::hhtl::{direct_children, NiblePath}; + let gp = NiblePath::root(1); + let child = gp.child(2); + let gc1 = child.child(0); + let gc2 = child.child(1); + let occupied = [gp, child, gc1, gc2]; + assert_eq!(direct_children(gp, &occupied), vec![child]); + assert_eq!(direct_children(child, &occupied), vec![gc1, gc2]); + + let lanes_of = |v: i8| { + let mut l = [0i8; BASIN_LANES]; + l[0] = v; + BasinLanes::pack(&l) + }; + let two_level = |gc1_v: i8, gc2_v: i8| { + let child_reg = BasinLanes::accumulate_children(&[lanes_of(gc1_v), lanes_of(gc2_v)]); + BasinLanes::accumulate_children(&[child_reg]) + }; + // Can-fire: the grandchild flips sign hard -> the child moves -> the + // grandparent moves. + assert_ne!(two_level(3, 3), two_level(-6, 3)); + // Silence: both grandchild states saturate the child at +7, so the + // grandparent cannot tell them apart -- the grandchild's detail is + // the CHILD's knowledge, one hop only. + assert_eq!(two_level(7, 5), two_level(6, 7)); + } + /// Falsifier that DISTINGUISHES designs: under stepwise saturation + /// ([+7,+7,−7] clamps mid-stream) child order changes the parent; under + /// exact-sum-then-clamp it cannot. Verified failing against the stepwise + /// implementation before it was replaced. + #[test] + fn accumulation_is_independent_of_child_order() { + let lanes_of = |v: i8| { + let mut l = [0i8; BASIN_LANES]; + l[0] = v; + BasinLanes::pack(&l) + }; + let a = lanes_of(7); + let b = lanes_of(7); + let c = lanes_of(-7); + assert_eq!( + BasinLanes::accumulate_children(&[a, b, c]), + BasinLanes::accumulate_children(&[c, a, b]), + "one hop is a SET of children, not a sequence" + ); + assert_eq!(BasinLanes::accumulate_children(&[a, b, c]).unpack()[0], 7); + } +} diff --git a/crates/lance-graph-contract/src/hhtl.rs b/crates/lance-graph-contract/src/hhtl.rs index 11dacc9da..3331fd93a 100644 --- a/crates/lance-graph-contract/src/hhtl.rs +++ b/crates/lance-graph-contract/src/hhtl.rs @@ -503,6 +503,77 @@ impl NiblePath { } } +/// The **mechanical-hydration address list** (`D-DCR-2b`, operator-ruled +/// 2026-09-01): every strict ancestor DN implied by `occupied` that is not +/// itself occupied — the positions that are *implicit in the hierarchy but +/// not hydrated* (readiness state c; for a book, `occupied` is the TOC's +/// leaf paths and this is the skeleton to spawn). +/// +/// Deliberately returns **addresses only**. Mechanical hydration mints +/// structure from the hierarchy; the epistemic value of every minted row is +/// [`BasinLanes::SILENT`](crate::basin_lanes::BasinLanes::SILENT) — this +/// function's signature cannot carry a lane value, which is the type-level +/// half of the mechanical-vs-epistemic split (a mint that writes knowledge +/// is structure impersonating knowledge). +/// +/// Order is deterministic: shallow → deep, then by packed path — parents +/// always precede children, so minting in returned order never creates a +/// child before its parent exists. +/// +/// [`NiblePath::EMPTY`] entries in `occupied` are ignored (no route ⇒ no +/// implied ancestors), and `EMPTY` is never yielded — depth-0 is "not yet +/// routed", not a mintable position. +/// The DIRECT children of `parent` among `occupied` — exactly depth+1 +/// descendants, **never grandchildren** (operator-ruled 2026-09-01: a +/// parent's register expresses its children accumulated, one hop only; +/// deeper positions reach it only through their own parents' registers). +/// +/// The one-hop rule made selectable: any accumulator fed by this function is +/// structurally unable to reach past the children, so locality is enforced +/// at the selection, not policed in the arithmetic. +/// +/// `parent == EMPTY` selects nothing — "no route" has no children (basins +/// are entered via [`NiblePath::root`], not as children of EMPTY). No +/// explicit guard: [`NiblePath::is_ancestor_of`] already makes EMPTY an +/// ancestor of nothing, and a guard on top of it proved undisableable — the +/// test below pins the SEMANTICS through this API, wherever they live. +#[must_use] +pub fn direct_children(parent: NiblePath, occupied: &[NiblePath]) -> Vec { + let mut out: Vec = occupied + .iter() + .copied() + .filter(|c| c.depth() == parent.depth() + 1 && parent.is_ancestor_of(*c)) + .collect(); + out.sort_by_key(|p| { + let (path, depth) = p.packed(); + (depth, path) + }); + out.dedup(); + out +} + +#[must_use] +pub fn missing_ancestors(occupied: &[NiblePath]) -> Vec { + let mut out: Vec = Vec::new(); + for &p in occupied { + let mut cur = p; + while let Some(parent) = cur.parent() { + if parent.depth() == 0 { + break; // EMPTY — not a position + } + if !occupied.contains(&parent) && !out.contains(&parent) { + out.push(parent); + } + cur = parent; + } + } + out.sort_by_key(|p| { + let (path, depth) = p.packed(); + (depth, path) + }); + out +} + #[cfg(test)] mod tests { use super::*; @@ -1037,4 +1108,111 @@ mod tests { assert_eq!(routing4.packed(), (0xABCDu64, 4)); assert_eq!(routing4.basin(), Some(0xA)); } + + // ---- missing_ancestors: the mechanical-hydration address list ---- + + #[test] + fn missing_ancestors_yields_the_full_skeleton_for_a_lone_deep_leaf() { + // A book: one TOC leaf routed 4 deep, nothing else hydrated. + let leaf = NiblePath::root(2).child(3).child(1).child(7); + let got = missing_ancestors(&[leaf]); + assert_eq!( + got, + vec![ + NiblePath::root(2), + NiblePath::root(2).child(3), + NiblePath::root(2).child(3).child(1), + ], + "all three strict ancestors, shallow→deep so parents mint first" + ); + } + + #[test] + fn missing_ancestors_is_silent_on_a_fully_hydrated_chain() { + // An ontology: parent/child preserved, every prefix occupied. + let occupied = [ + NiblePath::root(2), + NiblePath::root(2).child(3), + NiblePath::root(2).child(3).child(1), + ]; + assert!( + missing_ancestors(&occupied).is_empty(), + "state (b): nothing implicit, nothing to mint" + ); + } + + #[test] + fn missing_ancestors_dedups_shared_ancestry_across_siblings() { + // Two siblings imply the SAME two ancestors — each minted once. + let a = NiblePath::root(1).child(4).child(0); + let b = NiblePath::root(1).child(4).child(9); + let got = missing_ancestors(&[a, b]); + assert_eq!(got, vec![NiblePath::root(1), NiblePath::root(1).child(4)]); + } + + #[test] + fn missing_ancestors_ignores_empty_and_never_yields_it() { + // EMPTY is "no route", not a position: contributes nothing, appears + // never — depth-0 is not mintable. + let leaf = NiblePath::root(5).child(5); + let got = missing_ancestors(&[NiblePath::EMPTY, leaf]); + assert_eq!(got, vec![NiblePath::root(5)]); + assert!(!got.contains(&NiblePath::EMPTY)); + assert!(missing_ancestors(&[NiblePath::EMPTY]).is_empty()); + } + + #[test] + fn missing_ancestors_partial_hydration_yields_only_the_gap() { + // State (c) exactly: the rail names root(2)/…/child paths, the basin + // row exists, the middle does not. + let leaf = NiblePath::root(2).child(3).child(1).child(7); + let got = missing_ancestors(&[NiblePath::root(2), leaf]); + assert_eq!( + got, + vec![ + NiblePath::root(2).child(3), + NiblePath::root(2).child(3).child(1) + ], + "occupied ancestors are not re-minted; only the gap is" + ); + } + // ---- direct_children: the one-hop selector ---- + + #[test] + fn direct_children_selects_exactly_one_hop_never_grandchildren() { + let p = NiblePath::root(3); + let c1 = p.child(0); + let c2 = p.child(9); + let gc = c1.child(4); + let sibling_basin = NiblePath::root(4).child(1); // depth matches, ancestry does not + let occupied = [p, c1, c2, gc, sibling_basin]; + assert_eq!(direct_children(p, &occupied), vec![c1, c2]); + assert!( + !direct_children(p, &occupied).contains(&gc), + "a grandchild reaches the parent only through its own parent" + ); + assert_eq!(direct_children(c1, &occupied), vec![gc]); + } + + #[test] + fn direct_children_of_empty_is_nothing() { + let occupied = [NiblePath::root(0), NiblePath::root(7)]; + assert!( + direct_children(NiblePath::EMPTY, &occupied).is_empty(), + "no route has no children; basins are entered via root()" + ); + } + + #[test] + fn direct_children_dedups_and_orders_deterministically() { + let p = NiblePath::root(2); + let a = p.child(8); + let b = p.child(1); + let occupied = [p, a, b, a]; // duplicate occupancy entry + assert_eq!( + direct_children(p, &occupied), + vec![b, a], + "sorted by path, deduped" + ); + } } diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 2337fb1a4..927993056 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -53,6 +53,7 @@ pub mod attention_facet; pub mod auth; pub mod awareness_facet; pub mod band_reading; +pub mod basin_lanes; pub mod callcenter; pub mod cam; pub mod canonical_node; diff --git a/crates/lance-graph-contract/tests/w2b_one_node_field.rs b/crates/lance-graph-contract/tests/w2b_one_node_field.rs index 98b5f5b23..0fe5a56bc 100644 --- a/crates/lance-graph-contract/tests/w2b_one_node_field.rs +++ b/crates/lance-graph-contract/tests/w2b_one_node_field.rs @@ -29,6 +29,16 @@ //! //! Both are reasons W2b needs its own lane, recorded in the census beside this //! file. Here the facet is used purely as "the shipped 24-nibble codec". +//! +//! **Post-census landing (parallel branch, reconciled on rebase):** the +//! magnitude sibling this file calls for now exists — +//! `lance_graph_contract::basin_lanes::BasinLanes` — together with the +//! one-hop accumulator `accumulate_children` (operator-ruled: a parent +//! expresses its DIRECT children only). [`summarise`] below and +//! `accumulate_children` arrived at the SAME per-lane shape (exact signed +//! sum, one clamp) independently — convergent evidence, kept in both places +//! deliberately: this file pins the register-level carrier through the +//! borrowed codec, `basin_lanes`' tests pin the production carrier. use lance_graph_contract::causal_witness::{CausalWitnessFacet, WITNESS_LOCI};