diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index 5369a3385..646b76db7 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,10 @@ +## 2026-09-03 — D-POP-2: one Sonnet grindwork worker against a verbatim spec, orchestrator-gated + +- **Why:** the post-teardown survey (D-POP-0) named the contradiction write-back as its cheapest gap (§5.4) and D-POP-1 had reported (KILL), unblocking it. Tiering: the orchestrator (main thread) read the tree, wrote the spec (`elect_and_bind` / `ElectionReport` / `WitnessLens::bind_election` + six named falsifiers) and ran every gate and disable centrally; one Sonnet worker transcribed the spec into `witness_fabric.rs` + the `lib.rs` re-export block, edit-only, no cargo. +- **Worker outcome:** both files edited as briefed; zero production-code deviations; one spec error caught and reported (the silence test's `visited: 2` literal — `rows_from` spans `0..=max_pos`, so it asserted `visited == rows.len()` instead); three `#[must_use]` results in tests needed `let _ =` (orchestrator fix). +- **Gates (orchestrator):** fmt; clippy `-p lance-graph-contract --all-targets --no-deps -D warnings` clean; `cargo test -p lance-graph-contract` 1309 lib + all integration suites green; disables A/B/C red-then-green (commit `b8642405`). +- **Board:** EPIPHANIES `E-A-PRODUCER-IS-A-PURE-FUNCTION-OF-THE-CONTENT-LOCI-1`, LATEST_STATE delta, STATUS_BOARD D-POP-2 → In PR, plan §5 gap-4 note; supersession index regenerated last. + ## 2026-08-23 — the token-seam arc: four read-only research lanes, one probe, one vacuity audit - **Why:** an operator brief asked whether ONE versioned BPE tokenization can diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index dedce9e21..8a0aec0e3 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -15,6 +15,21 @@ **Both stay LAB, not deleted, not re-probed on friendlier data** — per the pre-registration's own commitment (§4b: "Not deleted... Not re-probed on a different corpus to get a better answer"). §1c is now fully closed: every row in the harvest census table has a verdict. Refs: `thinking-engine-harvest-closure-v1` §4b (pre-registration) / §4c (results) / §5 D-TEH-3; `crates/thinking-engine/src/{semantic_chunker,spiral_segment}.rs`; `.claude/agents/falsifier-auditor.md` (the can-it-fire discipline applied to the chunker's all-zero result); the falsifiability rule (CLAUDE.md § The falsifiability rule). +## 2026-09-03 — E-A-PRODUCER-IS-A-PURE-FUNCTION-OF-THE-CONTENT-LOCI-1 — D-POP-2 closed the survey's cheapest gap: `elect_peers` now writes its own election back into the row, and the fabric's no-self-reference rule is what makes that write order-independent + +**Status:** SHIPPED (code + 6 falsifiers, this branch; `witness_fabric::{elect_and_bind, ElectionReport, WitnessLens::bind_election}`). **Confidence:** High for the mechanism; the molecule it unblocks is still unwired downstream. + +**What was missing.** `post-teardown-buildup-survey-v1` §5 gap 4: `elect_peers` computed the social loci (Quorum / Contradiction), `WitnessLens::write_register` could store a register, and nothing connected them — so `is_opinion` / `revision_trajectory` / `suggest_reopening` (the "contradiction-driven revision" molecule, §4) never had a row that carried an ELECTED contradiction, only hand-set fixtures. + +**The finding, stated as a property.** The producer is a pure function of the content loci. `CONTENT_LOCI` excludes Quorum and Contradiction ("the fabric never reads what it computes" — no self-reference), which was written as an anti-circularity rule for the READ side; it turns out to be exactly what makes the WRITE side sound: a binding written into row *i* cannot change any row *j*'s election, so the whole-wave producer is order-independent, a second run is bit-identical, and stale or garbage social loci are overwritten, never consulted. That is the difference between a producer and an accumulator, and it is pinned two-sided: seeding every row with garbage Quorum=+7 / Contradiction=−8 before the run yields the same bytes as the clean run, and the test goes red the moment `CONTENT_LOCI` includes a social locus (disable-verified). + +**Why it stays inside family 1.** A locus is a signed OFFSET (sign = orientation); the producer writes two pointers and nothing else — no magnitude, no valence, no count. No tenant, no ClassView, no layout change, no dependency. Zero-copy: one lens per focal position over the same slice, dropped before the write; the only owned value is the `Copy` 12-byte register microcopy that `bind_election` read-modify-writes into the SAME row (slots 14 and 15 only; a full 14-locus content register plus an out-of-register canary survive byte-identical, disable-verified against a `ZERO.with(..)` rewrite). + +**Three disables, all red-then-green:** `bind_election` without read-modify-write (4 of 6 fail); `CONTENT_LOCI` + `Locus::Quorum` (the garbage-seed test fails); the producer passing `|_| true` to the peer scan instead of the caller's `visible` (the visible-domain test fails — +2 must beat the invisible +1, and the same fixture picks +1 when everything is visible). + +**What it does NOT do.** The molecule's next links — `BeliefArena::revise_at` on the elected contradiction, `RevisionTrajectory.flips`, `suggest_reopening` over real revisions — are still unwired; this PR gives them a real producer to read, not a caller. No population-basin work: family 3 stays the accepted vacancy (`E-SIX-SEMANTIC-FAMILIES-MUST-NOT-IMPERSONATE-EACH-OTHER-1`). + +**Spec correction caught by the worker.** The brief's silence test asserted `visited: 2`; `rows_from` builds the whole `0..=max_pos` span, so a `|_| true` predicate visits 7. The worker asserted `visited == rows.len()` and reported the deviation instead of forcing the literal — the right call, recorded here so the spec's error is not mistaken for the code's. ## 2026-09-02 — E-THE-LIFT-GATE-FOUND-A-TIE-BLIND-SPEARMAN-1 — the D-TEH-3 comparison on a distinguishing fixture separated a same-formula copy from a wrong-estimator copy, and the tie-free fixtures the lab had used could not have diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 61d928c83..d46e3bbb3 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,18 @@ +## 2026-09-03 — branch (D-POP-2, second commit): the producer is READ by its consumers — INVENTORY DELTA + +- ADDED `crates/lance-graph-contract/tests/d_pop_2_producer_reaches_consumers.rs` (2 tests, disable-verified against a no-op `bind_election`: both red). No library surface. +- MEASURED, not added: on a row whose content loci are SMeaning + Kausal, `recipe_loci::reachable` is **0/34** before the producer — every recipe touching either locus also needs a further one — and exactly **7/34** after (`3, 7, 11, 17, 20, 27, 30`, derived from `required_loci` in the test, not hand-listed only); `21` (needs MeaningLevel) and `31` (needs P/OMeaning) stay unreachable, so the unlock is the loci gate, not blanket. `SubstrateView::project`: `confidence` NaN → finite with no SPO tenant present, `dissonance` 0 → 2/15, `free_energy` 0.45 → 0.8, `is_opinion` false → true. +- UNCHANGED: everything in the first D-POP-2 delta below. + +## 2026-09-03 — branch (D-POP-2, after #1143): the contradiction write-back producer — CONTRACT INVENTORY DELTA + +- ADDED `lance_graph_contract::witness_fabric::elect_and_bind(rows: &mut [NodeRow], visible: impl Fn(usize) -> bool) -> ElectionReport` — the D-POP-2 producer: for every visible position, `elect_peers_lens` through a lens over the same slice, then `WitnessLens::bind_election` into the focal row. Order-independent + idempotent (elections read `CONTENT_LOCI` only). Re-exported at the crate root. +- ADDED `witness_fabric::ElectionReport { visited, quorum_bound, contradiction_bound }` (counts only — positions are readable back through the lens; a list would be a second projection). Re-exported. +- ADDED `WitnessLens::bind_election(row: &mut NodeRow, election: PeerElection)` — read-modify-write of ONLY slots 14/15 (Quorum / Contradiction) through the tenant-derived offsets `at` reads from; offset 0 unbinds. +- 6 tests, 3 disable-verified (see EPIPHANIES `E-A-PRODUCER-IS-A-PURE-FUNCTION-OF-THE-CONTENT-LOCI-1`). Contract lib 1309/1309 + integration green; clippy `--all-targets --no-deps -D warnings` + fmt clean. +- UNCHANGED: every existing `witness_fabric` fn and its semantics; `causal_witness.rs`; `canonical_node.rs`; `soa_envelope.rs`; `ENVELOPE_LAYOUT_VERSION`; tenants 14/15. No new dependency. +- NOT IN THIS DELTA: any caller of the producer (planner / callcenter still build windows by hand or use `quorum_mantissa` only); the molecule's `BeliefArena::revise_at` → `RevisionTrajectory` → `suggest_reopening` wiring; anything population-basin (vacancy stands). + ## 2026-09-03 — MERGED #1144 (`30b0a66`): D-TEH-3 fate probes closed — two corrections to the delta below PR #1144 (the delta immediately below) merged after CodeRabbit + Codex review. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 55b3aa3f3..a157ed3a9 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -30,7 +30,7 @@ |---|---|---| | D-POP-0 | the survey itself: six-family inventory, atom/operator inventory, family×operator legality matrix, molecule candidates, gap list (plan §§1–5) | **Shipped (plan-only, this PR)** | | D-POP-1 | PROBE-POP-READOUT-1: composed population readout (Cam96 centroid distance → Fisher-z → occupancy bucket) vs `curiosity` / `curiosity_gestalt` at predicting later-confirmed frontier edges on the KJV stream; size-preserving shuffle null; PASS/KILL pre-registered in plan §6 | **Shipped — VERDICT KILL** (`pop_readout.rs`, 2026-09-02; `E-QUALIA-IS-RANK-INERT-AT-THE-FRONTIER-AND-POPULATION-LOSES-TO-COUNTING-1`, plan §6a). Population readout degrades frontier p@10 0.289 -> 0.011, below its own shuffle null; real partial rho 0.090 vs null p95 0.020 (weak but non-null global trend). Side-findings: `curiosity_gestalt` is rank-inert (rho = 1.000000 vs `curiosity` over 227,261 candidates, two assessments); plain frequency p@10 = 0.756 beats every cognitive arm; helix (Fisher-z / RollingFloor) is not reachable from deepnsm-v2, so those legs are NOT RUN | -| D-POP-2 | contradiction write-back producer (`elect_peers` → `WitnessLens::write_register`) — the one cheap gap that unblocks the contradiction-driven-revision molecule (plan §4) | **Queued** — not before D-POP-1 reports | +| D-POP-2 | contradiction write-back producer (`elect_peers` → `WitnessLens::write_register`) — the one cheap gap that unblocks the contradiction-driven-revision molecule (plan §4) | **In PR** (2026-09-03, this branch) — `witness_fabric::{elect_and_bind, ElectionReport, WitnessLens::bind_election}`, 6 falsifiers / 3 disable-verified; D-POP-1 reported KILL 2026-09-02, so the gate was open. See `E-A-PRODUCER-IS-A-PURE-FUNCTION-OF-THE-CONTENT-LOCI-1` | ## dismech-causal-replay-v1 diff --git a/.claude/board/SUPERSESSION-INDEX.md b/.claude/board/SUPERSESSION-INDEX.md index 061d11cae..37b453d73 100644 --- a/.claude/board/SUPERSESSION-INDEX.md +++ b/.claude/board/SUPERSESSION-INDEX.md @@ -92,7 +92,7 @@ a licence to act on it. | **READ** | `mul-consumer-census-v1` | `GateDecision` | MEASUREMENT COMPLETE — 2026-08-27. Measureme | 1/2 | | **READ** | `mul-ewa-trust-propagation-v1` | `GateDecision` | PROPOSED — PLAN/BOARD ONLY. Measure-before-c | 1/3 | | **READ** | `persistence-artifact-backed-commit-v1` | `persist_cycle` | RATIFIED (operator ruling 2026-08-09). Phase | 0/0 | -| **READ** | `post-teardown-buildup-survey-v1` | `StepMask` | SURVEY, read-only, plan-only (no code, no te | 4/4 | +| **READ** | `post-teardown-buildup-survey-v1` | `StepMask` | SURVEY, read-only, plan-only (no code, no te | 5/5 | | **READ** | `r2il-bpe-typed-genetic-recombination-v1` | `GateDecision` | PROPOSAL, §7's three falsifiers now RUN (see | 1/1 | | **READ** | `scientific-kg-substrate-v1` | `GateDecision` | PROPOSED — **scoping doc**, no code. Records | 8/9 | | **READ** | `self-reasoning-substrate-v1` | `GateDecision` | PROPOSED — doc-only. No code, no contract ch | 5/5 | diff --git a/.claude/plans/post-teardown-buildup-survey-v1.md b/.claude/plans/post-teardown-buildup-survey-v1.md index 4a5ab6344..0992ef53a 100644 --- a/.claude/plans/post-teardown-buildup-survey-v1.md +++ b/.claude/plans/post-teardown-buildup-survey-v1.md @@ -335,3 +335,22 @@ stands. What the result does license is a narrower next question, stated as a question and not a direction: the readout's honest home may be the global trend (a basin-level prior) rather than a top-k selector, and any such probe must carry the frequency control and the shuffle null from the start. + +## 8. D-POP-2 — gap §5.4 CLOSED (2026-09-03) + +`witness_fabric::elect_and_bind(rows, visible) -> ElectionReport` + +`WitnessLens::bind_election(row, election)`: the producer §5 item 4 named as +"cheap" is shipped — elected social loci written back into the focal row's own +register (slots 14/15 only), order-independent and idempotent because elections +read `CONTENT_LOCI` only. Six falsifiers, three disable-verified. Family 1 only; +no tenant, ClassView, opcode or layout change; the family-3 vacancy is untouched. +Still open on the §4 "contradiction-driven revision" molecule: a caller that +feeds the elected contradiction into `BeliefArena::revise_at` and reads +`RevisionTrajectory` / `suggest_reopening` over real revisions (gap §5.3, the +real `WitnessStream` producer, is the same missing caller seen from the other +side). Board: `E-A-PRODUCER-IS-A-PURE-FUNCTION-OF-THE-CONTENT-LOCI-1`. + +Consumer reach measured (2026-09-03, same PR): `reachable` 0/34 → 7/34 on a +SMeaning+Kausal row (`tests/d_pop_2_producer_reaches_consumers.rs`); the +§4 molecule's ladder half is now reachable from a real election, its +`BeliefArena::revise_at` half is not (planner-side caller still missing). diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 63c15091d..a5bc64a86 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -239,12 +239,13 @@ pub use step_mask::StepMask; pub use style_family::StyleFamily; pub use view_angle::ViewAngle; pub use witness_fabric::{ - absolute_agreement, belief_runs, elect_peers, elect_peers_lens, foresight_calibration, - foresight_sample, is_opinion, opinion_strength, quorum_mantissa, quorum_mantissa_lens, - resolve_chain, resolve_chain_lens, revision_trajectory, standing_wave_diagnosed, - standing_wave_diagnosed_lens, standing_wave_grounded, standing_wave_grounded_lens, - standing_wave_stratified, standing_wave_stratified_lens, suggest_reopening, superseded_runs, - superseded_spread_sample, trajectory_of, trajectory_of_lens, BeliefRun, ChainResolution, - EscalateReason, ForesightSample, PeerElection, ReopeningEvidence, ReopeningReason, - ReopeningSuggestion, RevisionTrajectory, TrajectorySignature, WaveGrounding, WitnessLens, + absolute_agreement, belief_runs, elect_and_bind, elect_peers, elect_peers_lens, + foresight_calibration, foresight_sample, is_opinion, opinion_strength, quorum_mantissa, + quorum_mantissa_lens, resolve_chain, resolve_chain_lens, revision_trajectory, + standing_wave_diagnosed, standing_wave_diagnosed_lens, standing_wave_grounded, + standing_wave_grounded_lens, standing_wave_stratified, standing_wave_stratified_lens, + suggest_reopening, superseded_runs, superseded_spread_sample, trajectory_of, + trajectory_of_lens, BeliefRun, ChainResolution, ElectionReport, EscalateReason, + ForesightSample, PeerElection, ReopeningEvidence, ReopeningReason, ReopeningSuggestion, + RevisionTrajectory, TrajectorySignature, WaveGrounding, WitnessLens, }; diff --git a/crates/lance-graph-contract/src/witness_fabric.rs b/crates/lance-graph-contract/src/witness_fabric.rs index 148d4cda6..12a758968 100644 --- a/crates/lance-graph-contract/src/witness_fabric.rs +++ b/crates/lance-graph-contract/src/witness_fabric.rs @@ -361,6 +361,115 @@ pub fn elect_peers_lens( } } +// ── D-POP-2 — the contradiction write-back producer ─────────────────────── +// +// `elect_peers` / `elect_peers_lens` compute the social loci (Quorum, +// Contradiction) from a window of real peer rows; nothing before this point +// writes that election back into the row's own register. [`elect_and_bind`] +// is that PRODUCER — the molecule "contradiction-driven revision" (plan +// `.claude/plans/post-teardown-buildup-survey-v1.md` §4) needs a row whose +// Contradiction locus is actually BOUND before [`is_opinion`] / +// [`revision_trajectory`] / [`suggest_reopening`] have anything to read. +// +// Family 1 (episodic/Markov loci) only: a locus is a signed OFFSET (a +// pointer, sign = orientation) — never a magnitude, a valence, or a count. +// The producer therefore writes exactly two POINTERS (Quorum = slot 14, +// Contradiction = slot 15) and nothing else. No new tenant, no new +// ClassView, no new type beyond [`ElectionReport`] below, no layout change, +// no new dependency; `ENVELOPE_LAYOUT_VERSION` is untouched. +// +// Zero-copy law: the row array IS the projection. The producer lenses the +// rows in place per focal position; the only owned value is the `Copy` +// 12-byte [`CausalWitnessFacet`] microcopy read-modify-written back into the +// SAME row. It never gathers a `Vec` of registers. + +impl<'a> WitnessLens<'a> { + /// The producer's WRITE half: bind `election` into `row`'s own + /// CausalWitness register as a read-modify-write of ONLY the two social + /// loci — [`Locus::Quorum`] (slot 14) and [`Locus::Contradiction`] + /// (slot 15). Every other nibble of the register, and every byte of the + /// row outside it, is left byte-identical. An election of `0` UNBINDS the + /// locus (offset 0 is "unbound" everywhere in this facet), so a re-run + /// that finds no peer clears a stale one — the producer is a pure function + /// of the content loci, never an accumulator. + #[inline] + pub fn bind_election(row: &mut NodeRow, election: PeerElection) { + // owned 12-byte microcopy (CausalWitnessFacet is Copy) — read through the + // same offsets `at` uses, then written back through `write_register`. + let current = *WitnessLens::new(core::slice::from_ref(row)) + .at(0) + .expect("a one-row lens always has position 0"); + let next = current + .with(Locus::Quorum, election.quorum_offset) + .with(Locus::Contradiction, election.contradiction_offset); + WitnessLens::write_register(row, &next); + } +} + +/// What [`elect_and_bind`] did to a standing wave — counts only, no positions +/// (positions are readable back through the lens; a list here would be a +/// second projection of the rows). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct ElectionReport { + /// Focal rows visited (= visible positions). + pub visited: usize, + /// Visited rows whose Quorum locus ended bound (non-zero). + pub quorum_bound: usize, + /// Visited rows whose Contradiction locus ended bound (non-zero). + pub contradiction_bound: usize, +} + +/// **D-POP-2 — the contradiction write-back producer.** For every position +/// `pos` in `0..rows.len()` with `visible(pos)`, elect the row's social peers +/// through the lens ([`elect_peers_lens`], peers = the visible positions within +/// the ±8 horizon) and bind the election into the row's own register +/// ([`WitnessLens::bind_election`]). Rows outside `visible` are neither +/// resolved nor written and stay byte-identical. +/// +/// `rows` must be the WHOLE standing wave (see [`WitnessLens::new`]); the +/// `visible` predicate is the address set, exactly as for every `*_lens` +/// resolver in this module — one predicate, one meaning, used for the focal +/// domain and the peer domain alike. +/// +/// # Order-independent, idempotent +/// +/// Elections read only the CONTENT loci ([`CONTENT_LOCI`] excludes Quorum and +/// Contradiction precisely so the fabric never reads what it computes), so a +/// binding written into row *i* cannot change the election of any row *j*. +/// Visiting in ascending order is therefore just an order, not a dependency: +/// the result is the same bytes for any visit order, a second run reproduces +/// the first bit-for-bit, and stale or garbage social loci present before the +/// run are overwritten rather than consulted. That is what makes this a +/// producer of the molecule "contradiction-driven revision" (plan §4) and not +/// an accumulator: [`is_opinion`] / [`revision_trajectory`] / +/// [`suggest_reopening`] read the bound Contradiction locus back from the row. +/// +/// Zero-copy: one lens per focal position over the same row slice, dropped +/// before the write; the only owned value is the `Copy` register microcopy +/// inside [`WitnessLens::bind_election`]. No gather, no `Vec` of registers. +#[must_use] +pub fn elect_and_bind(rows: &mut [NodeRow], visible: impl Fn(usize) -> bool) -> ElectionReport { + let mut report = ElectionReport::default(); + for pos in 0..rows.len() { + if !visible(pos) { + continue; + } + let election = { + let lens = WitnessLens::new(&*rows); + elect_peers_lens(pos, &lens, &visible) + }; + WitnessLens::bind_election(&mut rows[pos], election); + report.visited += 1; + if election.quorum_offset != 0 { + report.quorum_bound += 1; + } + if election.contradiction_offset != 0 { + report.contradiction_bound += 1; + } + } + report +} + /// The result of following a locus chain across the window. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ChainResolution { @@ -3000,6 +3109,276 @@ mod tests { assert!(row.value[WITNESS_REGISTER_END..].iter().all(|&b| b == 0xEE)); } + // ── D-POP-2 — the contradiction write-back producer ──────────────────── + + /// **The producer end to end.** The same fixture + /// `elect_peers_finds_kausal_dissenter_as_contradiction` uses, but run + /// through `elect_and_bind` and read back through a fresh lens: the + /// election is bound into BOTH rows (symmetric), the molecule's consumer + /// (`is_opinion`) reads the bound Contradiction back, and the content loci + /// the election was computed FROM survive untouched. + #[test] + fn elect_and_bind_binds_the_kausal_dissenter_as_contradiction() { + let focal = w(&[(Locus::SMeaning, 2), (Locus::Kausal, -3)]); // pos 5 → S@7, K@2 + let dissenter = w(&[(Locus::SMeaning, 1), (Locus::Kausal, 2)]); // pos 6 → S@7 agree, K@8 ≠ K@2 + + let mut rows = rows_from(&[(5, focal), (6, dissenter)]); + let visible = |p: usize| p == 5 || p == 6; + let report = elect_and_bind(&mut rows, visible); + assert_eq!( + report, + ElectionReport { + visited: 2, + quorum_bound: 2, + contradiction_bound: 2, + } + ); + + let lens = WitnessLens::new(&rows); + assert_eq!(lens.at(5).unwrap().quorum(), 1); + assert_eq!(lens.at(5).unwrap().contradiction(), 1); + assert_eq!(lens.at(6).unwrap().quorum(), -1); + assert_eq!( + lens.at(6).unwrap().contradiction(), + -1, + "the election is symmetric" + ); + + // The molecule's consumer reads it back. + assert!( + is_opinion(&[*lens.at(5).unwrap()]), + "a bound Contradiction locus makes the row an opinion" + ); + assert!( + !is_opinion(&[focal]), + "the PRE-run facet has no bound Contradiction — the producer is \ + what made the row an opinion" + ); + + // Content loci untouched. + assert_eq!(lens.at(5).unwrap().at(Locus::SMeaning), 2); + assert_eq!(lens.at(5).unwrap().at(Locus::Kausal), -3); + } + + /// **Silence when nothing converges.** Two bound-but-disjoint rows must + /// elect NOTHING — no quorum, no contradiction — and the producer must not + /// touch a single byte it did not need to (writing `0` into an + /// already-`0` nibble is a byte-identical no-op). + /// + /// Note: `rows_from` builds the WHOLE `0..=max_pos` standing wave (per its + /// own doc comment), so with `visible = |_| true` every position in that + /// span — including the unbound filler rows at 0..5 — is visited, not + /// only the two named positions. `report.visited` is therefore asserted + /// as `rows.len()` rather than the literal `2`; the content of the + /// assertion (nothing was bound, nothing was written) is unchanged. + #[test] + fn elect_and_bind_stays_silent_without_shared_events() { + let a = w(&[(Locus::SMeaning, 2)]); // pos 5 → S@7 + let b = w(&[(Locus::SMeaning, 4)]); // pos 6 → S@10 + + // Fixture guard (anti-vacuity): both rows ARE bound but do not converge. + assert_eq!(absolute_agreement(5, a, 6, b), 0); + assert_eq!(a.bound_count(), 1); + assert_eq!(b.bound_count(), 1); + + let mut rows = rows_from(&[(5, a), (6, b)]); + let snapshot: Vec<[u8; 480]> = rows.iter().map(|r| r.value).collect(); + + let report = elect_and_bind(&mut rows, |_| true); + assert_eq!( + report, + ElectionReport { + visited: rows.len(), + quorum_bound: 0, + contradiction_bound: 0, + } + ); + for (row, before) in rows.iter().zip(snapshot.iter()) { + assert_eq!(&row.value, before, "no shared events → no write"); + } + } + + /// **Only the two social nibbles move.** A register with all 14 content + /// loci bound to distinct offsets, PLUS pre-existing Quorum/Contradiction + /// values, survives `bind_election` except at those two slots — and the + /// canary bytes outside the register are untouched. + /// + /// This test MUST fail if `bind_election` were rewritten as + /// `CausalWitnessFacet::ZERO.with(..)` — that would wipe the content loci + /// this test asserts survive. + #[test] + fn bind_election_touches_only_the_two_social_nibbles() { + let mut facet = CausalWitnessFacet::ZERO; + for (i, &l) in Locus::ALL.iter().enumerate() { + if l == Locus::Quorum || l == Locus::Contradiction { + continue; + } + let offset = ((i as i8) % 7) + 1; // ∈ 1..=7 + facet = facet.with(l, offset); + } + facet = facet.with(Locus::Quorum, -8).with(Locus::Contradiction, 7); + + let mut row = NodeRow { + key: crate::canonical_node::NodeGuid::local(1), + edges: crate::canonical_node::EdgeBlock::default(), + value: [0xEE_u8; 480], + }; + WitnessLens::write_register(&mut row, &facet); + + WitnessLens::bind_election( + &mut row, + PeerElection { + quorum_offset: 3, + contradiction_offset: -2, + quorum_agreement: 9, + }, + ); + + let lens = WitnessLens::new(std::slice::from_ref(&row)); + let read = *lens.at(0).expect("row present"); + + for (i, &l) in Locus::ALL.iter().enumerate() { + if l == Locus::Quorum || l == Locus::Contradiction { + continue; + } + let expected = ((i as i8) % 7) + 1; + assert_eq!( + read.at(l), + expected, + "content locus {} must survive bind_election unchanged", + l.label() + ); + } + assert_eq!(read.quorum(), 3); + assert_eq!(read.contradiction(), -2); + + assert!(row.value[..WITNESS_REGISTER_START] + .iter() + .all(|&b| b == 0xEE)); + assert!(row.value[WITNESS_REGISTER_END..].iter().all(|&b| b == 0xEE)); + } + + /// **Garbage-in is overwritten, never consulted — and the producer is + /// idempotent.** A 5-row wave with mixed agreement, run clean, must + /// produce the SAME bytes whether or not stale Quorum/Contradiction + /// values were sitting in the rows before the run — and running the + /// producer a second time over its own output must not move a byte. + /// + /// This test fails if [`CONTENT_LOCI`] ever included Quorum or + /// Contradiction: the garbage seeded below would then leak into the + /// election and the clean/garbage runs would diverge. + #[test] + fn elect_and_bind_ignores_pre_existing_social_loci_and_is_idempotent() { + let fixture = [ + (3usize, w(&[(Locus::SMeaning, 4), (Locus::Kausal, 2)])), // S@7, K@5 + (4, w(&[(Locus::SMeaning, 3), (Locus::Kausal, 1)])), // S@7, K@5 — agrees with 3 + (5, w(&[(Locus::SMeaning, 2), (Locus::Kausal, -3)])), // S@7, K@2 — S agrees, K conflicts + (6, w(&[(Locus::Temporal, 1)])), // T@7 — shares nothing + (7, w(&[])), // unbound + ]; + + // Clean run. + let mut rows_a = rows_from(&fixture); + let report_a = elect_and_bind(&mut rows_a, |_| true); + let snapshot_a: Vec<[u8; 480]> = rows_a.iter().map(|r| r.value).collect(); + + // Anti-vacuity: the clean run actually bound something on both loci. + assert!( + report_a.quorum_bound >= 1, + "the fixture must produce at least one bound Quorum" + ); + assert!( + report_a.contradiction_bound >= 1, + "the fixture must produce at least one bound Contradiction" + ); + + // Garbage run: seed every row's Quorum/Contradiction BEFORE running. + let mut rows_b = rows_from(&fixture); + let clean_before: Vec<[u8; 480]> = rows_b.iter().map(|r| r.value).collect(); + for row in rows_b.iter_mut() { + let current = *WitnessLens::new(std::slice::from_ref(&*row)) + .at(0) + .expect("row present"); + let garbaged = current + .with(Locus::Quorum, 7) + .with(Locus::Contradiction, -8); + WitnessLens::write_register(row, &garbaged); + } + let garbage_before: Vec<[u8; 480]> = rows_b.iter().map(|r| r.value).collect(); + // Anti-vacuity: the garbage-seeded facets DID differ from the clean + // ones before the run. + assert_ne!( + clean_before, garbage_before, + "garbage-seeding must actually change the pre-run bytes" + ); + + let _ = elect_and_bind(&mut rows_b, |_| true); + let snapshot_b: Vec<[u8; 480]> = rows_b.iter().map(|r| r.value).collect(); + assert_eq!( + snapshot_a, snapshot_b, + "garbage social loci were overwritten, never consulted" + ); + + // Idempotence: running elect_and_bind again on its own output + // reproduces the same bytes. + let _ = elect_and_bind(&mut rows_a, |_| true); + let snapshot_a2: Vec<[u8; 480]> = rows_a.iter().map(|r| r.value).collect(); + assert_eq!(snapshot_a, snapshot_a2, "elect_and_bind must be idempotent"); + } + + /// **The `visible` domain is load-bearing, not decoration.** Hiding the + /// nearer of two equally-agreeing peers must change the election exactly + /// the way `elect_peers_lens`'s own visibility tests change theirs — both + /// arms run against a fresh copy of the SAME fixture so the predicate is + /// proven to change the outcome, not merely accepted. + #[test] + fn elect_and_bind_respects_the_visible_domain() { + let fixture = [ + (5usize, w(&[(Locus::SMeaning, 3)])), // S@8 + (6, w(&[(Locus::SMeaning, 2)])), // S@8 + (7, w(&[(Locus::SMeaning, 1)])), // S@8 + ]; + + // Arm A: hide position 6. + let mut rows_a = rows_from(&fixture); + assert_eq!(rows_a.len(), 8, "rows_from spans 0..=7"); + let snapshot_row6 = rows_a[6].value; + let report_a = elect_and_bind(&mut rows_a, |p| p != 6); + let lens_a = WitnessLens::new(&rows_a); + assert_eq!( + lens_a.at(5).unwrap().quorum(), + 2, + "the VISIBLE peer at +2 must be elected over the nearer invisible peer at +1" + ); + assert_eq!( + rows_a[6].value, snapshot_row6, + "a hidden row must never be written" + ); + assert_eq!(report_a.visited, rows_a.len() - 1); + + // Arm B (anti-vacuity, fresh copy of the same fixture): with everyone + // visible the nearer peer wins the first-maximum tie-break — proving + // arm A's predicate actually changed the outcome. + let mut rows_b = rows_from(&fixture); + let _ = elect_and_bind(&mut rows_b, |_| true); + let lens_b = WitnessLens::new(&rows_b); + assert_eq!( + lens_b.at(5).unwrap().quorum(), + 1, + "with the closer peer visible it wins the first-maximum tie-break" + ); + } + + /// An empty standing wave is a no-op: nothing visited, nothing bound. + #[test] + fn elect_and_bind_on_an_empty_wave_is_a_no_op() { + let mut rows: Vec = Vec::new(); + assert_eq!( + elect_and_bind(&mut rows, |_| true), + ElectionReport::default() + ); + } + /// **Bounds.** An out-of-range position — including on an empty lens — /// returns `None`, never panics. #[test] diff --git a/crates/lance-graph-contract/tests/d_pop_2_producer_reaches_consumers.rs b/crates/lance-graph-contract/tests/d_pop_2_producer_reaches_consumers.rs new file mode 100644 index 000000000..0176d7120 --- /dev/null +++ b/crates/lance-graph-contract/tests/d_pop_2_producer_reaches_consumers.rs @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +//! D-POP-2 follow-up — the producer reaches its shipped consumers. +//! +//! `witness_fabric::elect_and_bind` shipped the PRODUCER: it elects a focal +//! row's social peers (Quorum / Contradiction) from a window of real content +//! loci and binds the election back into the row's own register. This file +//! proves the elected loci are actually READ by the two shipped consumers the +//! survey's §4 "contradiction-driven revision" molecule sits on: +//! +//! 1. **The recipe ladder** (`recipe_loci::reachable`) gates the +//! consensus/revision recipes on Quorum / Contradiction being BOUND — so +//! before the producer runs, a row whose content loci (SMeaning, Kausal) +//! are bound but whose social loci are not cannot reach those recipes; once +//! the producer runs, exactly the recipes whose OTHER required loci are +//! already satisfied become newly reachable. +//! 2. **`SubstrateView::project`** — the logical markers the 34 kernels +//! consume (`confidence`, `dissonance`, `free_energy`) move from +//! ungrounded/zero to grounded/non-zero once the producer has bound the +//! social loci. +//! +//! Each claim is two-sided: a recipe that also needs a still-unbound locus +//! (e.g. `MeaningLevel`, `PMeaning`/`OMeaning`) stays unreachable even after +//! the producer runs — the gate is the loci, not a blanket unlock. + +use lance_graph_contract::awareness_facet::SpoFacet; +use lance_graph_contract::canonical_node::{EdgeBlock, NodeGuid, NodeRow}; +use lance_graph_contract::causal_witness::{CausalWitnessFacet, Locus}; +use lance_graph_contract::qualia::QualiaI4_16D; +use lance_graph_contract::recipe_loci::{is_grounded, reachable, required_loci}; +use lance_graph_contract::recipe_substrate::SubstrateView; +use lance_graph_contract::witness_fabric::{elect_and_bind, is_opinion, WitnessLens}; + +/// Same semantics as the private helper in `witness_fabric`'s own tests: rows +/// for positions `0..=max_pos`, blank rows are a default-class/default-basin +/// `NodeRow` with a zeroed value slab, and each named register is written +/// through [`WitnessLens::write_register`] — never a hand-poked byte offset. +fn rows_from(regs: &[(usize, CausalWitnessFacet)]) -> Vec { + let max_pos = regs.iter().map(|&(p, _)| p).max().unwrap_or(0); + let mut rows: Vec = (0..=max_pos) + .map(|_| NodeRow { + key: NodeGuid::local(1), + edges: EdgeBlock::default(), + value: [0u8; 480], + }) + .collect(); + for &(pos, facet) in regs { + WitnessLens::write_register(&mut rows[pos], &facet); + } + rows +} + +/// The shared fixture: a focal row at position 5 whose content loci +/// (`SMeaning`, `Kausal`) are bound but whose social loci are not; an +/// agreeing peer at 4 that converges on BOTH content loci (same absolute +/// events: `SMeaning` at 7, `Kausal` at 2); and a dissenting peer at 6 that +/// agrees on `SMeaning` (event 7) but points its `Kausal` cause at a +/// different event (8 vs the focal's 2) — a preserved dissent, not mere +/// unrelatedness. +fn fixture() -> (CausalWitnessFacet, CausalWitnessFacet, CausalWitnessFacet) { + let focal = CausalWitnessFacet::ZERO + .with(Locus::SMeaning, 2) // 5 + 2 = event 7 + .with(Locus::Kausal, -3); // 5 - 3 = event 2 + let agree = CausalWitnessFacet::ZERO + .with(Locus::SMeaning, 3) // 4 + 3 = event 7 (agrees) + .with(Locus::Kausal, -2); // 4 - 2 = event 2 (agrees) + let dissent = CausalWitnessFacet::ZERO + .with(Locus::SMeaning, 1) // 6 + 1 = event 7 (agrees) + .with(Locus::Kausal, 2); // 6 + 2 = event 8 (conflicts: focal's is 2) + (focal, agree, dissent) +} + +/// Run the producer over the fixture and return the focal row's ELECTED +/// register, re-read through the lens (never the input `focal` value — the +/// row's own bytes are the projection). +fn run_producer() -> ( + CausalWitnessFacet, + lance_graph_contract::witness_fabric::ElectionReport, +) { + let (focal, agree, dissent) = fixture(); + let mut rows = rows_from(&[(5, focal), (4, agree), (6, dissent)]); + let report = elect_and_bind(&mut rows, |_| true); + let f5 = *WitnessLens::new(&rows).at(5).unwrap(); + (f5, report) +} + +#[test] +fn the_producer_makes_consensus_and_revision_recipes_reachable() { + let (focal, _agree, _dissent) = fixture(); + let (f5, _report) = run_producer(); + + // Anti-vacuity on the election itself: if the election changed, every + // downstream assertion below would be measuring something else. + assert_eq!( + f5.quorum(), + -1, + "quorum must elect the agreeing peer at offset -1 (position 4)" + ); + assert_eq!( + f5.contradiction(), + 1, + "contradiction must elect the dissenting peer at offset +1 (position 6)" + ); + + let before: Vec = reachable(&focal); + let after: Vec = reachable(&f5); + + // `newly` computed from the required_loci TABLE itself, not by hand: a + // recipe becomes newly reachable by the producer iff every locus it + // requires is drawn from {SMeaning, Kausal, Quorum, Contradiction} (the + // four loci ever bound across `before`/`after` in this fixture) AND it is + // not already reachable pre-producer (i.e. it is not a subset of the + // content-only loci {SMeaning, Kausal}). + const CONTENT_AND_SOCIAL: [Locus; 4] = [ + Locus::SMeaning, + Locus::Kausal, + Locus::Quorum, + Locus::Contradiction, + ]; + const CONTENT_ONLY: [Locus; 2] = [Locus::SMeaning, Locus::Kausal]; + let newly: Vec = (1..=34u8) + .filter(|&id| { + let req = required_loci(id); + let subset_of_content_and_social = req.iter().all(|l| CONTENT_AND_SOCIAL.contains(l)); + let subset_of_content_only = req.iter().all(|l| CONTENT_ONLY.contains(l)); + subset_of_content_and_social && !subset_of_content_only + }) + .collect(); + // The table and this derived list must agree — if the table changes, + // this fails loudly instead of silently drifting. + assert_eq!( + newly, + vec![3, 7, 11, 17, 20, 27, 30], + "the required_loci table's newly-reachable set drifted from the pinned list" + ); + + for &id in &newly { + assert!( + !before.contains(&id), + "recipe {id} must NOT be reachable before the producer runs" + ); + assert!( + after.contains(&id), + "recipe {id} must become reachable after the producer runs" + ); + } + + // `after` as a set == `before` ∪ `newly` — nothing else appeared. + use std::collections::HashSet; + let after_set: HashSet = after.iter().copied().collect(); + let expected_set: HashSet = before + .iter() + .copied() + .chain(newly.iter().copied()) + .collect(); + assert_eq!( + after_set, expected_set, + "after must be exactly before ∪ newly — the producer must not unlock anything else" + ); + + // Two-sided: a recipe needing a STILL-unbound locus stays unreachable + // even after the producer runs — the gate is the loci, not a blanket + // unlock. #21 needs MeaningLevel (unbound); #31 needs PMeaning/OMeaning + // (unbound) in addition to Kausal/Contradiction/SMeaning. + assert!(!before.contains(&21) && !after.contains(&21)); + assert!(!before.contains(&31) && !after.contains(&31)); + assert!( + !is_grounded(&f5, 21), + "#21 also needs MeaningLevel, still unbound" + ); + assert!( + !is_grounded(&f5, 31), + "#31 also needs PMeaning/OMeaning, still unbound" + ); + + // Anti-vacuity on `before`/`after` sizing. + // + // NOTE (spec-ambiguity resolution): the required_loci table (verified by + // exhaustive enumeration of all 34 entries) has NO recipe whose required + // set is a subset of exactly {SMeaning, Kausal} — every entry that reads + // either locus also reads at least one further, still-unbound locus (e.g. + // #4 additionally needs OMeaning, #12 additionally needs Temporal, #29 + // additionally needs PMeaning). So for THIS fixture's focal (only + // SMeaning + Kausal bound), `before` is provably EMPTY rather than + // "non-empty via some unspecified recipe" — asserting non-emptiness here + // would assert something the table does not support. The genuinely + // falsifiable, non-vacuous claim this fixture supports is the exact + // count relationship below, which still fails loudly if the producer + // either fails to unlock the 7 newly-reachable recipes or unlocks + // anything beyond them. + assert!( + before.is_empty(), + "no recipe in required_loci requires a subset of exactly {{SMeaning, Kausal}} — \ + verified against the source table; a non-empty `before` here would mean the \ + table changed and this assumption needs re-deriving" + ); + assert_eq!( + after.len(), + before.len() + 7, + "after must gain exactly the 7 newly-reachable recipes" + ); +} + +#[test] +fn the_producer_grounds_the_substrate_view_markers() { + let (focal, _agree, _dissent) = fixture(); + let (f5, report) = run_producer(); + + // Anti-vacuity on the election, mirrored from the first test (each test + // must independently prove it is measuring the intended election). + assert_eq!(f5.quorum(), -1, "quorum must elect the agreeing peer"); + assert_eq!( + f5.contradiction(), + 1, + "contradiction must elect the dissenting peer" + ); + + let before = SubstrateView::new(SpoFacet::default(), focal, QualiaI4_16D::ZERO); + let after = SubstrateView::new(SpoFacet::default(), f5, QualiaI4_16D::ZERO); + + // Anti-vacuity: no SPO tenant is present, so `confidence` can ONLY be + // grounded through the social loci this producer binds — not through SPO + // agreement. + assert!( + !before.spo_present(), + "the fixture carries no SPO tenant — confidence must be grounded socially or not at all" + ); + + let b = before.project(); + let a = after.project(); + + // confidence: ungrounded (NaN) before any social locus is bound — + // `SubstrateView::project`'s `conf_grounded` needs SPO present OR + // Quorum/Contradiction bound, and none of those hold pre-producer. + assert!( + b.confidence.is_nan(), + "confidence must be NaN before the producer binds a social locus" + ); + assert!( + a.confidence.is_finite(), + "confidence must be grounded once Quorum/Contradiction are bound" + ); + + // dissonance: `logical_dissonance` is 0.0 while the contradiction edge is + // unbound; once contradiction (+1) and quorum (-1) are both bound and + // point at different offsets, it is `|quorum - contradiction| / 15`. + assert_eq!( + b.dissonance, 0.0, + "dissonance must be exactly 0.0 while Contradiction is unbound" + ); + let expected_dissonance = 2.0_f32 / 15.0; // |(-1) - 1| / 15, per logical_dissonance + assert!( + (a.dissonance - expected_dissonance).abs() < 1e-6, + "dissonance must equal |quorum - contradiction| / 15 = {expected_dissonance} (logical_dissonance); got {}", + a.dissonance + ); + + // free_energy (surprise): `logical_surprise` = 0.5 + contra − 0.4·situated, + // contra = 0.4 iff Contradiction is bound, situated = bound_count / 16. + // Before: bound_count = 2 (SMeaning, Kausal) → situated = 2/16, contra = 0. + let expected_before_fe = 0.5_f32 - 0.4 * (2.0 / 16.0); + // After: bound_count = 4 (SMeaning, Kausal, Quorum, Contradiction) → + // situated = 4/16, contra = 0.4. + let expected_after_fe = 0.5_f32 + 0.4 - 0.4 * (4.0 / 16.0); + assert!( + (b.free_energy - expected_before_fe).abs() < 1e-6, + "before free_energy must equal logical_surprise's formula ({expected_before_fe}); got {}", + b.free_energy + ); + assert!( + (a.free_energy - expected_after_fe).abs() < 1e-6, + "after free_energy must equal logical_surprise's formula ({expected_after_fe}); got {}", + a.free_energy + ); + assert!( + a.free_energy > b.free_energy, + "the bound contradiction must raise surprise" + ); + + // opinion: a row whose Contradiction locus survives every revision in + // its history is an opinion; an empty/never-bound history is not. + assert!( + is_opinion(&[f5]), + "f5's Contradiction locus is bound in its one-revision history → opinion" + ); + assert!( + !is_opinion(&[focal]), + "the pre-run focal has no bound Contradiction → not an opinion" + ); + + // The producer actually bound at least one contradiction this run. + assert!( + report.contradiction_bound >= 1, + "elect_and_bind must have bound at least one contradiction across the fixture" + ); +}