diff --git a/.claude/board/entries/2026-10-11-maturation3-ladder-probe.md b/.claude/board/entries/2026-10-11-maturation3-ladder-probe.md new file mode 100644 index 000000000..912024b69 --- /dev/null +++ b/.claude/board/entries/2026-10-11-maturation3-ladder-probe.md @@ -0,0 +1,100 @@ +# 2026-10-11 — Novelty → Wisdom: eight-stage 3-bit maturation ladder (D-MATURATION3-0) + +**Status:** EXPERIMENTAL probe, test-local. No production change. +`crates/lance-graph-planner/tests/maturation_ladder.rs` + +```text +cargo test -p lance-graph-planner --test maturation_ladder +cargo test --release -p lance-graph-planner --test maturation_ladder measure_ -- --nocapture +``` + +## Codebook (bits 50..52 under an explicitly declared experimental reading) + +| code | stage | earned by (executed result) | +|---|---|---| +| 7 | Staunen | violation behind one observation root | +| 6 | Anomaly | violation behind ≥ 2 independent roots | +| 5 | DogEar | `hydrate` → ProposedOnly (gap localized) | +| 4 | Candidates | `hydrate` → Partial | +| 3 | Mechanism | `hydrate` → Hydrated | +| 2 | Falsification | discriminating test: survivor passed AND rival ruled out | +| 1 | Integration | applied as a premise; ≥ 1 new inference resolved through it | +| 0 | Wisdom | from Integration, an ablation in an isolated copy has impact > 0 | + +Forward results never move a more mature state backward; only a violation +reopens it. Repeating a completed result never advances it. Unsupported +results abstain. + +## Findings + +- **VERIFIED-IN-CODE — production hydration has no feedback seam.** + `pearl::hydrate` reads only the sealed chain; a resolved relation is never a + premise. Measured on the A → B → C chain: baseline residue 2, test-local + feedback (production `hydrate` unchanged, over a premise chain) residue 0. +- **MEASURED (release, this container):** `mature` median 1.99–2.83 ns, + p95 3.0–4.6 ns, 0 allocations over 1e6 transitions. Fixed point, 3 relations + / 4 observations: baseline ~165 ns, 10 allocs, residue 2; feedback ~157 ns, + 8 allocs, residue 0. The probe loop allocates (premise pool `Vec`); that is + test scaffolding, not a production cost claim. +- **EXPERIMENTAL — anchor ablation:** cutting derived A in an isolated copy + raises residue by 2 (B and C lose their bridge); the original population is + untouched. +- **EXPERIMENTAL — lineage:** a derived premise carries the union of its + parents' observation roots and never adds one (C: 4 roots from 4 + observations). Two paths from the same roots count 2 supports, not 4. +- **EXPERIMENTAL — self-support:** only resolved relations are premises; a + two-relation cycle stays open. Disabled, it grounds itself. +- **EXPERIMENTAL — deeper Staunen:** "1 → 4 does not hold" contradicts + nothing without the anchor chain and is a violation with it. + +Disable runs, all red: forward result moving backward; derived dropping a +parent's roots; write leaking into W; write not clearing the old code; +integration without new inferences; Wisdom without ablation impact; existing +provenance accepted; discrimination without ruling out; feedback off; open +relations as premises. (The first T1 fixture hid the leak/clear disables; it +was fixed before recording.) + +## CONTRACT DECISION — migration (not taken) + +Bits 50..52 are plasticity: `CausalEdge64::learn` gates on them, +`MooreNars16` packs them at bit 50, `CausalEdgeV3` byte 3 carries them, and +D-CE64-STAUNEN-0 measured that another meaning there changes `learn`. The +probe therefore refuses `V2Stamped`, `V3Register`, `V1Legacy` and `Unknown`. +Smallest viable path: a new declared reading (a provenance/generation such +that historic rows keep plasticity), a decision where plasticity lives under +that reading, and a `learn` that does not gate on maturation bits. Not done. + +## OPEN + +- Feedback in production requires D2 provenance ids (roots here are a + test-local u64 bitset). +- `Executed` results are constructed by the test from real `hydrate` + outcomes; mapping recipe outputs onto them is undesigned. +- T5 uses hydration outcomes as the discriminating test, a weak stand-in for + a Pearl trial; certification is untouched by design. + +## Addendum, same day — Maslov Pyramid of Cognition + +Named by the operator; the model is documented in +`.claude/knowledge/maslov-pyramid-of-cognition.md`. Stage 7 renamed +Astonishment. + +- **Corrected:** the first T9 applied the new violation to the anchor itself. + It now lands on the contradicted relation (B), which starts at 7, and the + anchor (A) stays at 0. Direct counterevidence against A's own relation + still reopens A. +- **Added:** an indirect 3-cycle (A → B → C → A) stays open; disabled, it + grounds itself. A grounded 3-cycle resolves once and 64 further passes add + no root. +- **Disable runs, red:** a derived relation manufacturing its own root; + violation routed by subject only (hits the anchor). +- The root union matches the existing `lance_graph_contract::revision` mask + algebra (`independent_roots`, difference against ancestry). + +## Addendum — lineage guard restored (review on #1494) + +I had removed the "descendants are not premises" guard as unreachable. That +holds only while relations never reopen; T9 reopens an anchor. Reproduced: +ground the 3-cycle, remove the ground, reopen R0, and R2 (derived through R0) +re-grounds it. The guard is restored. A test pins that R0 stays open, and a +disable arm shows that without the guard R0 is re-grounded. diff --git a/.claude/board/entries/2026-10-11-residue-fixed-point.md b/.claude/board/entries/2026-10-11-residue-fixed-point.md new file mode 100644 index 000000000..257486d3e --- /dev/null +++ b/.claude/board/entries/2026-10-11-residue-fixed-point.md @@ -0,0 +1,25 @@ +# 2026-10-11 — Residue fixed point: only new evidence lowers it (D-RESIDUE-FP-0) + +**Status:** MEASURED. Test only, no production change. +`crates/lance-graph-planner/tests/residue_fixed_point.rs`. + +```text +cargo test -p lance-graph-planner --test residue_fixed_point +``` + +Claim: the reasoning loop is cheap (`pearl::hydrate` over the sealed chain), +so it runs to a fixed point; what remains open is residue. Residue here is +hydration debt in bits 59..63 (`IndirectUnknown` edges). + +Pinned: +- extra passes over the same evidence move no bit; +- re-sealing evidence already present does not lower the residue; +- unrelated evidence changes nothing; the missing binding lowers the residue + exactly where it lands; +- the loop moves only bits 59..60, never certification. + +Disable runs, both red: promoting on partial evidence (3 of 4 tests fail); +moving certification inside `hydrate` (the bit-isolation test fails). + +OPEN: the surprise level proposed for bits 50..52 is not in the layout (still +plasticity); its residue is untested until O14/O4 decide the v3 layout. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index e230e0f49..4a6bf1b93 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,14 +25,16 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -306 entries, 2026-08-06 .. 2026-10-11. +308 entries, 2026-08-06 .. 2026-10-11. | date | entry id | finding | file | |---|---|---|---| | 2026-10-11 | `D-TELEPORT-0` | | [2026-10-11-teleport-probe-source-restriction.md](2026-10-11-teleport-probe-source-restriction.md) | | 2026-10-11 | `synergy-ledger-classid-hotplug` | | [2026-10-11-synergy-ledger-classid-hotplug.md](2026-10-11-synergy-ledger-classid-hotplug.md) | +| 2026-10-11 | `D-RESIDUE-FP-0` | | [2026-10-11-residue-fixed-point.md](2026-10-11-residue-fixed-point.md) | | 2026-10-11 | `research-recovery-reports` | | [2026-10-11-research-recovery-reports.md](2026-10-11-research-recovery-reports.md) | | 2026-10-11 | `D-PHOTO-0` | | [2026-10-11-photo-probe-64-populations.md](2026-10-11-photo-probe-64-populations.md) | +| 2026-10-11 | `D-MATURATION3-0` | | [2026-10-11-maturation3-ladder-probe.md](2026-10-11-maturation3-ladder-probe.md) | | 2026-10-11 | `D-TELEPORT-2` | | [2026-10-11-lance-source-probe.md](2026-10-11-lance-source-probe.md) | | 2026-10-11 | `coresearch-labels-address-lifetime` | | [2026-10-11-coresearch-labels-address-lifetime.md](2026-10-11-coresearch-labels-address-lifetime.md) | | 2026-10-11 | `coresearch-instance-authz` | | [2026-10-11-coresearch-instance-authz.md](2026-10-11-coresearch-instance-authz.md) | diff --git a/.claude/knowledge/maslov-pyramid-of-cognition.md b/.claude/knowledge/maslov-pyramid-of-cognition.md new file mode 100644 index 000000000..9fd3a212d --- /dev/null +++ b/.claude/knowledge/maslov-pyramid-of-cognition.md @@ -0,0 +1,94 @@ +# Maslov Pyramid of Cognition — from Astonishment to generative Wisdom, and back + +> READ BY: truth-architect, contradiction-cartographer, trajectory-cartographer, +> v3-envelope-auditor, and any session touching CausalEdge64 bits 50..52, +> novelty / Staunen / wisdom scheduling, `pearl::hydrate` feedback, or anchor +> ablation. +> STATUS: WORKING-MODEL. The probe is EXPERIMENTAL and test-local +> (`crates/lance-graph-planner/tests/maturation_ladder.rs`, D-MATURATION3-0). +> Nothing below is wired in production. + +## Name and scope + +The operator named this model after Maslow's hierarchy. It is a proposed +computational model, not a claim that Maslow defined these stages. + +It is **not** Wang's NAL-1..9. NAL levels describe what a reasoner is able to +do. The pyramid describes how far **one discovery** has matured. + +## The eight stages (bits 50..52, 3 bits, direction 7 → 0) + +| code | stage | earned by an executed result | +|---|---|---| +| 7 | Astonishment | an observation contradicts an expectation (one root) | +| 6 | Anomaly | the contradiction has ≥ 2 independent roots | +| 5 | DogEar | hydration localizes the gap (ProposedOnly) | +| 4 | Candidates | hydration finds one binding (Partial) | +| 3 | Mechanism | hydration finds both bindings (Hydrated) | +| 2 | Falsification | a discriminating test passed the survivor AND ruled out a rival | +| 1 | Integration | used as a premise, it resolved ≥ 1 other relation | +| 0 | Wisdom | from Integration, ablating it in an isolated copy reopens ≥ 1 relation | + +Rules: + +- Stages may be skipped when an executed result justifies it. +- A forward result never moves a more mature state backward. +- Repeating a completed result (another hydration pass) never advances it. +- Only a contradiction reopens a stage. +- Wisdom is not "zero uncertainty". It is measured reuse: an ablation must + show that other relations depend on the anchor. + +## The recursion + +An anchor at 0 generates expectations through the relations derived from it. +An observation that contradicted nothing before can now contradict one of +those expectations. That contradiction lands on **the contradicted +relation**, which starts its own pyramid at 7. **The anchor stays at 0.** + +Only counterevidence against the anchor's **own** relation reopens the anchor. + +So wisdom does not end surprise. It makes new surprises recognizable. This is +recursive scaffolding, not circular confirmation. + +## Separation of contracts + +| bits | contract | role here | +|---|---|---| +| 50..52 | maturation (this model) | where the discovery stands | +| 53..58 | witness / breadcrumb | handle to the transition record | +| 59..63 | `EpistemicState5` | what is licensed; the input to ablation | +| 24..39 | F/C | statistical evidence under its declared reading | +| — | thought / recipe | which operation ran | + +Maturation never writes F/C, W or 59..63. Certification never rises because a +rival was ruled out. + +## Generative reuse is not independent evidence + +A resolved relation may become a premise for further inference. It carries +the **union of its parents' observation roots** and never adds one of its +own. This is the same mask algebra as the existing +`lance_graph_contract::revision` (`independent_roots`, `difference` against +the ancestry). Two guards keep reuse from becoming self-support: + +1. Only resolved relations may serve as premises, so neither a direct cycle + nor an indirect one (A → B → C → A) can ground itself. +2. A relation's own descendants are never its premises. Without this, a + relation reopened by counterevidence would be re-grounded by what was + derived from it, and circular support would survive the loss of its + external ground. + +Going round a grounded cycle adds nothing. + +## Status + +| claim | status | +|---|---| +| `pearl::hydrate` reads only the sealed chain; resolved relations never feed back | VERIFIED-IN-CODE | +| test-local feedback makes A → B → C resolvable without a new observation | EXPERIMENTAL (probe) | +| anchor ablation measures dependence (impact 2 on the chain) | EXPERIMENTAL (probe) | +| an anchor turns a neutral observation into new Astonishment; the anchor stays at 0 | EXPERIMENTAL (probe) | +| `mature`: ~2–3 ns median, 0 allocations (release, one container) | MEASURED | +| bits 50..52 carry the code in production | BLOCKED — they are plasticity (`learn`, `MooreNars16`, `CausalEdgeV3`) | +| production feedback | BLOCKED — needs D2 provenance ids | +| bit migration and where plasticity lives | CONTRACT DECISION (operator) | diff --git a/crates/lance-graph-planner/tests/maturation_ladder.rs b/crates/lance-graph-planner/tests/maturation_ladder.rs new file mode 100644 index 000000000..ed274f857 --- /dev/null +++ b/crates/lance-graph-planner/tests/maturation_ladder.rs @@ -0,0 +1,1075 @@ +//! **D-MATURATION3-0 — the Maslov Pyramid of Cognition as an eight-stage +//! 3-bit maturation code.** +//! +//! Named by the operator after Maslow's hierarchy. A proposed computational +//! model, not a claim about Maslow, and distinct from Wang's NAL-1..9 +//! capability levels: NAL describes what a reasoner can do; the pyramid +//! describes how far one discovery has matured, from Astonishment (7) to a +//! generative Wisdom anchor (0). Its recursion: an anchor's expectations make +//! new contradictions visible, and each new problem climbs its own pyramid +//! while the anchor stays at 0. +//! +//! EXPERIMENTAL, test-local. Nothing here changes production code. +//! +//! # Why this is test-local (CONTRACT DECISION, OPEN) +//! +//! Bits 50..52 of `CausalEdge64` are plasticity under the v2 layout: +//! `CausalEdge64::learn` gates S/P/O adoption on them, `MooreNars16` packs the +//! same field at bit 50, and `CausalEdgeV3` carries it in byte 3. The +//! `staunen_scheduler_probe` (D-CE64-STAUNEN-0) measured that writing another +//! meaning there changes what `learn` does. So no existing provenance may read +//! these bits as maturation: the reader below accepts only an explicitly +//! declared experimental reading and refuses `V2Stamped`, `V3Register`, +//! `V1Legacy` and `Unknown`. Migrating the bits is an operator decision. +//! +//! # What is tested +//! +//! - The codebook (0..7) and its bit isolation on a real `CausalEdge64`. +//! - `mature`: one small, deterministic, allocation-free transition. It moves +//! only on an executed result; it never moves because a pass completed. +//! - A test-local feedback loop that reuses production `pearl::hydrate` +//! unchanged: resolved relations become premises for later hydrations, with +//! lineage. A derived premise carries the union of its parents' observation +//! roots and never adds a root of its own. +//! - Ablation through bits 59..63 in an isolated copy, never the original. +//! +//! Run with `--nocapture` to see the measured timings. + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::cell::Cell; +use std::time::Instant; + +use causal_edge::edge::InferenceType; +use causal_edge::{CausalEdge64, CausalMask, PlasticityState}; +use lance_graph_contract::band_reading::EdgeProvenance; +use lance_graph_contract::class_view::ClassId; +use lance_graph_contract::epistemic_state5::{ + Certification3, Epi5Declarations, Epi5Gen, Epi5Reading, EpistemicState5, Topology2, +}; +use lance_graph_contract::rail_geometry::RailAxis; +use lance_graph_planner::chain_replay::ChainStep; +use lance_graph_planner::pearl::{hydrate, Hydration, Reading}; + +// ── Allocation counter (measurement only) ───────────────────────────────── + +struct Counting; +thread_local! { + // Per-thread so parallel tests do not pollute the count. + static ALLOCS: Cell = const { Cell::new(0) }; +} + +// SAFETY: forwards every call to `System` unchanged; only counts. +unsafe impl GlobalAlloc for Counting { + unsafe fn alloc(&self, l: Layout) -> *mut u8 { + ALLOCS.with(|c| c.set(c.get() + 1)); + // SAFETY: same contract as the caller's. + unsafe { System.alloc(l) } + } + unsafe fn dealloc(&self, p: *mut u8, l: Layout) { + // SAFETY: same contract as the caller's. + unsafe { System.dealloc(p, l) } + } +} + +#[global_allocator] +static GLOBAL: Counting = Counting; + +// ── The codebook ────────────────────────────────────────────────────────── + +/// Eight cognitive maturation stages. The value IS the 3-bit code. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u8)] +enum Maturation3 { + /// Consolidated explanation, demonstrably reusable as an anchor. + Wisdom = 0, + /// The validated explanation produced an inference in another context. + Integration = 1, + /// The mechanism passed a discriminating test. + Falsification = 2, + /// An explanatory mechanism exists and can be tested. + Mechanism = 3, + /// Candidate bridges identified. + Candidates = 4, + /// Missing link localized and bookmarked. + DogEar = 5, + /// The discrepancy is independently substantiated. + Anomaly = 6, + /// A new expectation violation. + Astonishment = 7, +} + +impl Maturation3 { + const ALL: [Maturation3; 8] = [ + Maturation3::Wisdom, + Maturation3::Integration, + Maturation3::Falsification, + Maturation3::Mechanism, + Maturation3::Candidates, + Maturation3::DogEar, + Maturation3::Anomaly, + Maturation3::Astonishment, + ]; + + const fn from_code(c: u8) -> Self { + Self::ALL[(c & 0b111) as usize] + } +} + +const M3_SHIFT: u32 = 50; +const M3_MASK: u64 = 0b111 << M3_SHIFT; + +/// The declared reading. Only an explicit experimental declaration may read +/// bits 50..52 as maturation; every existing provenance means plasticity. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum M3Reading { + /// Declared by this probe for its own edges. + Experimental, + /// Any edge minted by existing producers. + Existing(EdgeProvenance), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MaturationError { + /// The bits mean plasticity under this provenance. + PlasticityReading(EdgeProvenance), +} + +fn read_m3(e: CausalEdge64, r: M3Reading) -> Result { + match r { + M3Reading::Experimental => Ok(Maturation3::from_code(((e.0 & M3_MASK) >> M3_SHIFT) as u8)), + M3Reading::Existing(p) => Err(MaturationError::PlasticityReading(p)), + } +} + +/// A maturation-only write: bits 50..52 and nothing else. +fn write_m3( + e: CausalEdge64, + m: Maturation3, + r: M3Reading, +) -> Result { + read_m3(e, r)?; + Ok(CausalEdge64((e.0 & !M3_MASK) | ((m as u64) << M3_SHIFT))) +} + +// ── The transition ──────────────────────────────────────────────────────── + +/// What an executed operation measured. `Copy`, no allocation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Executed { + /// An observation contradicted an expectation. `independent_roots` counts + /// distinct observation roots behind the contradiction. + Violation { independent_roots: u32 }, + /// `pearl::hydrate` ran on this relation. + Hydrated(Hydration), + /// A discriminating test between competing mechanisms. + Discriminated { + survivor_passed: bool, + rival_ruled_out: bool, + }, + /// The relation was used as a premise elsewhere; `new_inferences` counts + /// relations that resolved only because of it. + Applied { new_inferences: u32 }, + /// The relation was cut in an isolated copy; `impact` counts dependent + /// relations that fell back to `IndirectUnknown`. + Ablated { impact: u32 }, +} + +/// The one transition. Moves only on an earned condition. A forward target +/// never moves the state backward; only a violation reopens it. Unsupported +/// results abstain. +fn mature( + current: Maturation3, + result: Executed, + reading: M3Reading, +) -> Result { + if let M3Reading::Existing(p) = reading { + return Err(MaturationError::PlasticityReading(p)); + } + use Maturation3 as M; + let target = match result { + Executed::Violation { + independent_roots: 0, + } => return Ok(current), + Executed::Violation { + independent_roots: 1, + } => return Ok(M::Astonishment), + Executed::Violation { .. } => return Ok(M::Anomaly), + Executed::Hydrated(Hydration::ProposedOnly) => M::DogEar, + Executed::Hydrated(Hydration::Partial) => M::Candidates, + Executed::Hydrated(Hydration::Hydrated) => M::Mechanism, + Executed::Discriminated { + survivor_passed: true, + rival_ruled_out: true, + } if current <= M::Mechanism => M::Falsification, + Executed::Discriminated { .. } => return Ok(current), + Executed::Applied { new_inferences } + if new_inferences > 0 && current <= M::Falsification => + { + M::Integration + } + Executed::Applied { .. } => return Ok(current), + Executed::Ablated { impact } if impact > 0 && current == M::Integration => M::Wisdom, + Executed::Ablated { .. } => return Ok(current), + }; + // Forward results never move the state backward. + Ok(current.min(target)) +} + +// ── Fixture: edges, readings, premises with lineage ─────────────────────── + +const CLASS: ClassId = 0x0902; +const RAIL: RailAxis = RailAxis::Taxonomy; +const PREDICATE: u8 = 0x91; +const EPI_BITS: u64 = 0b11111 << 59; + +fn declarations() -> Epi5Declarations { + let mut d = Epi5Declarations::new(); + d.declare( + CLASS, + RAIL, + Epi5Reading { + generation: Epi5Gen::V1, + }, + ); + d +} + +fn reading(d: &Epi5Declarations) -> Reading<'_> { + Reading { + declarations: d, + class: CLASS, + rail: RAIL, + provenance: EdgeProvenance::V2Stamped, + } +} + +fn raw_edge(s: u8, o: u8, freq: u8) -> CausalEdge64 { + CausalEdge64::pack( + s, + PREDICATE, + o, + freq, + 200, + CausalMask::SO, + 0b101, + InferenceType::Deduction, + PlasticityState::from_bits(0), + 0, + ) +} + +fn open(s: u8, o: u8) -> CausalEdge64 { + let st = EpistemicState5::new( + Epi5Gen::V1, + Topology2::IndirectUnknown, + Certification3::Related, + ); + raw_edge(s, o, 200).with_epistemic_raw5(st.raw()) +} + +fn topology(e: CausalEdge64) -> Topology2 { + EpistemicState5::decode(Epi5Gen::V1, e.epistemic_raw5()) + .expect("valid code") + .topology() +} + +/// A premise: an edge plus its lineage. +#[derive(Debug, Clone, Copy)] +struct Premise { + edge: CausalEdge64, + /// Observation roots behind it (one bit per sealed observation). + roots: u64, + /// Derivation events behind it (one bit per derived relation). + derivations: u64, +} + +/// An open relation with its lineage once resolved. +#[derive(Debug, Clone, Copy)] +struct Relation { + edge: CausalEdge64, + id: u32, + roots: u64, + derivations: u64, +} + +fn observation(s: u8, o: u8, root: u32) -> Premise { + Premise { + edge: raw_edge(s, o, 200), + roots: 1 << root, + derivations: 0, + } +} + +/// Knobs that the disable runs flip. Production behaviour is `feedback: false`. +#[derive(Clone, Copy)] +struct Loop { + feedback: bool, + /// DISABLE knob: let still-open relations act as premises. + open_premises: bool, + /// DISABLE knob: let a relation's descendants act as its premises. + ignore_lineage: bool, +} + +const LOOP: Loop = Loop { + feedback: true, + open_premises: false, + ignore_lineage: false, +}; +const BASELINE: Loop = Loop { + feedback: false, + open_premises: false, + ignore_lineage: false, +}; + +/// One pass. Every open relation tries every intermediate the premises +/// propose, through production `pearl::hydrate` over a premise chain. +/// Returns the number of relations resolved in this pass. +fn pass( + rels: &mut [Relation], + obs: &[Premise], + banned: Option, + cfg: Loop, + d: &Epi5Declarations, +) -> usize { + let mut resolved = 0; + for i in 0..rels.len() { + if topology(rels[i].edge) != Topology2::IndirectUnknown { + continue; + } + let me = rels[i].id; + // The premise pool: observations, plus (with feedback) resolved + // relations that were not derived from this relation. + let mut pool: Vec = obs.to_vec(); + if cfg.feedback { + for r in rels.iter() { + // Two self-support guards. (1) Only RESOLVED relations are + // premises, so an open cycle cannot bootstrap itself. (2) A + // relation's own descendants are never its premises, so a + // REOPENED relation cannot be re-grounded by what was derived + // from it (T8 reopen test). + let usable = Some(r.id) != banned + && (cfg.ignore_lineage || r.derivations & (1 << me) == 0) + && (cfg.open_premises || topology(r.edge) != Topology2::IndirectUnknown); + if usable { + pool.push(Premise { + edge: raw_edge(r.edge.s_idx(), r.edge.o_idx(), 200), + roots: r.roots, + derivations: r.derivations | (1 << r.id), + }); + } + } + } + let chain: Vec = pool.iter().map(|p| (0, p.edge)).collect(); + let (a, y) = (rels[i].edge.s_idx(), rels[i].edge.o_idx()); + for p in pool.iter().filter(|p| p.edge.s_idx() == a) { + let b = p.edge.o_idx(); + let (next, h) = hydrate(rels[i].edge, (a, b, y), &chain, reading(d)).expect("declared"); + if h == Hydration::Hydrated && topology(next) == Topology2::IndirectKnown { + let second = pool + .iter() + .find(|q| q.edge.s_idx() == b && q.edge.o_idx() == y) + .expect("hydrated means both bindings exist"); + rels[i].edge = next; + rels[i].roots = p.roots | second.roots; + rels[i].derivations = p.derivations | second.derivations; + resolved += 1; + break; + } + } + } + resolved +} + +/// Run to a fixed point; return the number of passes that resolved anything. +fn fixed_point( + rels: &mut [Relation], + obs: &[Premise], + banned: Option, + cfg: Loop, + d: &Epi5Declarations, +) -> usize { + for passes in 0..32 { + if pass(rels, obs, banned, cfg, d) == 0 { + return passes; + } + } + panic!("no fixed point in 32 passes"); +} + +fn residue(rels: &[Relation]) -> usize { + rels.iter() + .filter(|r| topology(r.edge) == Topology2::IndirectUnknown) + .count() +} + +fn rel(id: u32, s: u8, o: u8) -> Relation { + Relation { + edge: open(s, o), + id, + roots: 0, + derivations: 0, + } +} + +/// The dependency chain the prompt asks for: +/// sealed obs → A (1→3) → B (1→4) → C (0→4). +fn chain_world() -> (Vec, Vec) { + let obs = vec![ + observation(1, 2, 0), + observation(2, 3, 1), + observation(3, 4, 2), + observation(0, 1, 3), + ]; + let rels = vec![rel(0, 1, 3), rel(1, 1, 4), rel(2, 0, 4)]; + (obs, rels) +} + +// ── T1: decoding and bit isolation ──────────────────────────────────────── + +#[test] +fn t1_eight_codes_round_trip_and_only_bits_50_52_move() { + // W's lowest bit (53) clear and bits 50..52 all set, so a write that + // leaks into W or forgets to clear the old code is visible. + let mut base = open(9, 7).with_w_slot(0b101100); + base.set_plasticity(PlasticityState::from_bits(0b111)); + assert_eq!(base.0 & M3_MASK, M3_MASK, "fixture: old code bits set"); + assert_eq!(base.0 & (1 << 53), 0, "fixture: bit 53 clear"); + for m in Maturation3::ALL { + let e = write_m3(base, m, M3Reading::Experimental).unwrap(); + assert_eq!(read_m3(e, M3Reading::Experimental), Ok(m)); + assert_eq!( + e.0 & !M3_MASK, + base.0 & !M3_MASK, + "only bits 50..52 may change" + ); + assert_eq!(e.frequency(), base.frequency()); + assert_eq!(e.confidence(), base.confidence()); + assert_eq!(e.w_slot(), base.w_slot()); + assert_eq!(e.epistemic_raw5(), base.epistemic_raw5()); + } + for c in 0u8..8 { + assert_eq!(Maturation3::from_code(c) as u8, c); + } +} + +#[test] +fn t1_every_existing_provenance_refuses_the_maturation_reading() { + let e = open(9, 7); + for p in [ + EdgeProvenance::V2Stamped, + EdgeProvenance::V3Register, + EdgeProvenance::V1Legacy, + EdgeProvenance::Unknown, + ] { + assert_eq!( + read_m3(e, M3Reading::Existing(p)), + Err(MaturationError::PlasticityReading(p)) + ); + assert!(write_m3(e, Maturation3::Wisdom, M3Reading::Existing(p)).is_err()); + assert!(mature( + Maturation3::Integration, + Executed::Ablated { impact: 3 }, + M3Reading::Existing(p) + ) + .is_err()); + } +} + +// ── T2: cheap fixed point, no drift toward Wisdom ───────────────────────── + +#[test] +fn t2_repeating_a_completed_result_never_advances_maturation() { + let x = M3Reading::Experimental; + let mut m = Maturation3::Astonishment; + m = mature(m, Executed::Hydrated(Hydration::Hydrated), x).unwrap(); + assert_eq!(m, Maturation3::Mechanism, "a hydration may skip stages"); + for _ in 0..100 { + m = mature(m, Executed::Hydrated(Hydration::Hydrated), x).unwrap(); + } + assert_eq!( + m, + Maturation3::Mechanism, + "completed passes are not progress" + ); + // A forward result never moves a more mature state backward. + assert_eq!( + mature( + Maturation3::Falsification, + Executed::Hydrated(Hydration::Hydrated), + x + ) + .unwrap(), + Maturation3::Falsification + ); + assert_eq!( + mature( + Maturation3::Integration, + Executed::Hydrated(Hydration::Partial), + x + ) + .unwrap(), + Maturation3::Integration + ); + + let d = declarations(); + let (obs, mut rels) = chain_world(); + fixed_point(&mut rels, &obs, None, LOOP, &d); + let snapshot: Vec = rels.iter().map(|r| r.edge.0).collect(); + for _ in 0..16 { + pass(&mut rels, &obs, None, LOOP, &d); + } + assert_eq!(rels.iter().map(|r| r.edge.0).collect::>(), snapshot); +} + +// ── T3: the Eselsohr survives ───────────────────────────────────────────── + +#[test] +fn t3_a_localized_gap_keeps_its_dogear_and_witness_until_addressed() { + let x = M3Reading::Experimental; + let d = declarations(); + // 7 → 9 with a proposal 7 → 8 but no 8 → 9. + let obs = [observation(7, 8, 0)]; + let mut r = [rel(0, 7, 9)]; + let (_, h) = hydrate(r[0].edge, (7, 8, 9), &[(0, obs[0].edge)], reading(&d)).unwrap(); + let m = mature(Maturation3::Astonishment, Executed::Hydrated(h), x).unwrap(); + assert_eq!(m, Maturation3::Candidates, "one binding found: candidates"); + + // The witness handle points at the transition record (test-local log). + let log: Vec<(u32, Maturation3)> = vec![(r[0].id, m)]; + r[0].edge = write_m3(r[0].edge, m, x) + .unwrap() + .with_w_slot(log.len() as u8 - 1); + let marked = r[0].edge; + for _ in 0..8 { + pass(&mut r, &obs, None, LOOP, &d); + } + assert_eq!( + r[0].edge, marked, + "passes over the same evidence keep the mark" + ); + assert_eq!( + log[r[0].edge.w_slot() as usize], + (0, Maturation3::Candidates) + ); +} + +// ── T4: a new inference without a new observation ───────────────────────── + +#[test] +fn t4_feedback_resolves_what_the_sealed_chain_alone_cannot() { + let d = declarations(); + let (obs, mut base) = chain_world(); + fixed_point(&mut base, &obs, None, BASELINE, &d); + assert_eq!(residue(&base), 2, "baseline: only A resolves"); + + let (obs, mut fed) = chain_world(); + let passes = fixed_point(&mut fed, &obs, None, LOOP, &d); + assert_eq!(residue(&fed), 0, "feedback: A, B, then C"); + assert!(passes >= 1); + // Same observations, no new one: four roots, never more. + assert_eq!(fed[2].roots.count_ones(), 4); + assert_eq!( + fed[1].roots, 0b0111, + "B carries exactly the roots of its parents" + ); + assert_eq!( + fed[2].derivations & (1 << 1), + 1 << 1, + "C records B as a derivation, not a root" + ); +} + +// ── T5: falsification never certifies ───────────────────────────────────── + +#[test] +fn t5_a_discriminating_test_matures_but_does_not_certify() { + let x = M3Reading::Experimental; + let d = declarations(); + // 30 → 32 via 31 (both bindings) or via 33 (only one). + let chain: Vec = [(30, 31), (31, 32), (30, 33)] + .iter() + .map(|&(s, o)| (0, raw_edge(s, o, 200))) + .collect(); + let e = open(30, 32); + let (_, via31) = hydrate(e, (30, 31, 32), &chain, reading(&d)).unwrap(); + let (_, via33) = hydrate(e, (30, 33, 32), &chain, reading(&d)).unwrap(); + assert_eq!((via31, via33), (Hydration::Hydrated, Hydration::Partial)); + + let m = mature( + Maturation3::Mechanism, + Executed::Discriminated { + survivor_passed: via31 == Hydration::Hydrated, + rival_ruled_out: via33 != Hydration::Hydrated, + }, + x, + ) + .unwrap(); + assert_eq!(m, Maturation3::Falsification); + let written = write_m3(e, m, x).unwrap(); + assert_eq!( + written.0 & EPI_BITS, + e.0 & EPI_BITS, + "maturation never touches certification" + ); + + // A test that rules nothing out earns nothing. + let m2 = mature( + Maturation3::Mechanism, + Executed::Discriminated { + survivor_passed: true, + rival_ruled_out: false, + }, + x, + ) + .unwrap(); + assert_eq!(m2, Maturation3::Mechanism); +} + +// ── T6 + T7: integration, then the anchor ablation ──────────────────────── + +#[test] +fn t6_t7_an_applied_anchor_integrates_and_its_ablation_measures_wisdom() { + let x = M3Reading::Experimental; + let d = declarations(); + let (obs, mut rels) = chain_world(); + fixed_point(&mut rels, &obs, None, LOOP, &d); + let original: Vec = rels.iter().map(|r| r.edge.0).collect(); + + // T6: A was used as a premise; count relations whose lineage holds A. + let new_inferences = rels.iter().filter(|r| r.derivations & 1 != 0).count() as u32; + assert_eq!(new_inferences, 2, "B and C resolved through A"); + let mut m = mature( + Maturation3::Falsification, + Executed::Applied { new_inferences }, + x, + ) + .unwrap(); + assert_eq!(m, Maturation3::Integration); + + // T7: cut A in an isolated copy and recompute. + let (obs2, mut cut) = chain_world(); + let r_baseline = residue(&rels); + let passes_cut = fixed_point(&mut cut, &obs2, Some(0), LOOP, &d); + let impact = (residue(&cut) - r_baseline) as u32; + // A itself still resolves from observations; B and C lose their bridge. + assert_eq!(impact, 2, "AnchorImpact = R_cut - R_baseline"); + assert_eq!(topology(cut[1].edge), Topology2::IndirectUnknown); + assert_eq!(topology(cut[2].edge), Topology2::IndirectUnknown); + assert!(passes_cut <= 1); + assert_eq!( + rels.iter().map(|r| r.edge.0).collect::>(), + original, + "the original is never overwritten" + ); + + m = mature(m, Executed::Ablated { impact }, x).unwrap(); + assert_eq!(m, Maturation3::Wisdom); + + // Applying something that enabled nothing is not integration. + assert_eq!( + mature( + Maturation3::Falsification, + Executed::Applied { new_inferences: 0 }, + x + ) + .unwrap(), + Maturation3::Falsification + ); + // An anchor whose cut changes nothing earns nothing. + assert_eq!( + mature(Maturation3::Integration, Executed::Ablated { impact: 0 }, x).unwrap(), + Maturation3::Integration + ); +} + +// ── T8: no echo, no self-support ────────────────────────────────────────── + +#[test] +fn t8_two_paths_from_the_same_roots_are_not_two_supports() { + let d = declarations(); + // 5 → 7 via 6 and via 8; both first legs come from root 0, both second + // legs from root 1. + let shared = observation(5, 6, 0); + let obs = [ + shared, + Premise { + edge: raw_edge(5, 8, 200), + ..shared + }, + observation(6, 7, 1), + Premise { + edge: raw_edge(8, 7, 200), + ..observation(6, 7, 1) + }, + ]; + let paths: Vec = [6u8, 8] + .iter() + .map(|&b| { + let first = obs + .iter() + .find(|p| p.edge.s_idx() == 5 && p.edge.o_idx() == b) + .unwrap(); + let second = obs + .iter() + .find(|p| p.edge.s_idx() == b && p.edge.o_idx() == 7) + .unwrap(); + first.roots | second.roots + }) + .collect(); + let union = paths.iter().fold(0, |a, p| a | p); + assert_eq!( + union.count_ones(), + 2, + "two paths, two observations, two supports" + ); + let echo: u32 = paths.iter().map(|p| p.count_ones()).sum(); + assert_eq!( + echo, 4, + "summing per path would double-count: the disable arm" + ); + + // Self-support: two open relations that each need the other. + let obs = vec![observation(21, 22, 0), observation(22, 21, 1)]; + let mut cyc = vec![rel(0, 20, 22), rel(1, 20, 21)]; + fixed_point(&mut cyc, &obs, None, LOOP, &d); + assert_eq!( + residue(&cyc), + 2, + "neither may ground the other from nothing" + ); +} + +#[test] +fn t8_disable_open_premises_lets_a_cycle_ground_itself() { + let d = declarations(); + let obs = vec![observation(21, 22, 0), observation(22, 21, 1)]; + let mut cyc = vec![rel(0, 20, 22), rel(1, 20, 21)]; + let broken = Loop { + feedback: true, + open_premises: true, + ignore_lineage: false, + }; + fixed_point(&mut cyc, &obs, None, broken, &d); + assert!( + residue(&cyc) < 2, + "the guard is load-bearing: without it the cycle resolves" + ); +} + +// ── T9: wisdom creates new surprise ─────────────────────────────────────── + +/// An observation violates an expectation only if a resolved relation +/// predicts the opposite. +fn violates(rels: &[Relation], s: u8, o: u8, freq: u8) -> Option { + rels.iter().position(|r| { + r.edge.s_idx() == s + && r.edge.o_idx() == o + && topology(r.edge) == Topology2::IndirectKnown + && (freq < 128) != (r.edge.frequency() < 0.5) + }) +} + +#[test] +fn t9_an_anchor_turns_a_neutral_observation_into_new_astonishment() { + let x = M3Reading::Experimental; + let d = declarations(); + // "1 → 4 does not hold" (low frequency). + let (s, o, f) = (1u8, 4u8, 20u8); + + let (obs, mut without) = chain_world(); + fixed_point(&mut without, &obs, None, BASELINE, &d); + assert_eq!( + violates(&without, s, o, f), + None, + "no model of 1 → 4: nothing to contradict" + ); + + let (obs, mut with) = chain_world(); + fixed_point(&mut with, &obs, None, LOOP, &d); + // A is the anchor (Wisdom, earned as in T6/T7); B and C are resolved. + let mut m3 = [ + Maturation3::Wisdom, + Maturation3::Mechanism, + Maturation3::Mechanism, + ]; + let hit = violates(&with, s, o, f).expect("the anchor chain now predicts 1 → 4"); + assert_eq!( + hit, 1, + "the contradicted expectation is B, derived through A" + ); + m3[hit] = mature( + m3[hit], + Executed::Violation { + independent_roots: 1, + }, + x, + ) + .unwrap(); + + assert_eq!( + m3[1], + Maturation3::Astonishment, + "the new problem starts its own pyramid" + ); + assert_eq!( + m3[0], + Maturation3::Wisdom, + "the anchor that made it visible stays at 0" + ); +} + +#[test] +fn t9_direct_counterevidence_reopens_the_anchor_itself() { + let x = M3Reading::Experimental; + let d = declarations(); + let (obs, mut rels) = chain_world(); + fixed_point(&mut rels, &obs, None, LOOP, &d); + // "1 → 3 does not hold" contradicts A's own relation. + let hit = violates(&rels, 1, 3, 20).expect("A predicts 1 → 3"); + assert_eq!(hit, 0); + let m = mature( + Maturation3::Wisdom, + Executed::Violation { + independent_roots: 1, + }, + x, + ) + .unwrap(); + assert_eq!(m, Maturation3::Astonishment); +} + +// ── T10: regression and replay ──────────────────────────────────────────── + +#[test] +fn t10_counterevidence_reopens_and_old_plasticity_is_never_read_as_wisdom() { + let x = M3Reading::Experimental; + assert_eq!( + mature( + Maturation3::Wisdom, + Executed::Violation { + independent_roots: 1 + }, + x + ) + .unwrap(), + Maturation3::Astonishment + ); + assert_eq!( + mature( + Maturation3::Wisdom, + Executed::Violation { + independent_roots: 3 + }, + x + ) + .unwrap(), + Maturation3::Anomaly + ); + assert_eq!( + mature( + Maturation3::Wisdom, + Executed::Violation { + independent_roots: 0 + }, + x + ) + .unwrap(), + Maturation3::Wisdom + ); + + // A historic v2 edge whose plasticity bits happen to be 0b000. + let mut old = raw_edge(1, 2, 200); + old.set_plasticity(PlasticityState::ALL_FROZEN); + assert_eq!(old.0 & M3_MASK, 0, "same bit pattern as Wisdom"); + assert!(read_m3(old, M3Reading::Existing(EdgeProvenance::V2Stamped)).is_err()); +} + +// ── Measurement ─────────────────────────────────────────────────────────── + +#[test] +fn measure_transition_cost_and_allocations() { + let x = M3Reading::Experimental; + let inputs = [ + Executed::Violation { + independent_roots: 1, + }, + Executed::Hydrated(Hydration::Partial), + Executed::Hydrated(Hydration::Hydrated), + Executed::Discriminated { + survivor_passed: true, + rival_ruled_out: true, + }, + Executed::Applied { new_inferences: 2 }, + Executed::Ablated { impact: 2 }, + ]; + let before = ALLOCS.with(Cell::get); + let mut m = Maturation3::Astonishment; + let mut acc = 0u64; + for i in 0..1_000_000usize { + m = mature(m, inputs[i % inputs.len()], x).unwrap(); + acc = acc.wrapping_add(m as u64); + } + let allocs = ALLOCS.with(Cell::get) - before; + assert_eq!(allocs, 0, "mature must not allocate"); + + // Timing: batches of 10_000 transitions. + let mut ns: Vec = (0..200) + .map(|_| { + let t = Instant::now(); + for i in 0..10_000usize { + m = mature(std::hint::black_box(m), inputs[i % inputs.len()], x).unwrap(); + } + t.elapsed().as_nanos() as f64 / 10_000.0 + }) + .collect(); + ns.sort_by(|a, b| a.partial_cmp(b).unwrap()); + println!( + "mature: median {:.2} ns, p95 {:.2} ns per transition (debug unless --release); allocs {allocs}; checksum {acc}", + ns[100], ns[190] + ); +} + +#[test] +fn measure_feedback_overhead() { + let d = declarations(); + let time = |cfg: Loop| { + let mut ns: Vec = (0..200) + .map(|_| { + let (obs, mut rels) = chain_world(); + let t = Instant::now(); + std::hint::black_box(fixed_point(&mut rels, &obs, None, cfg, &d)); + t.elapsed().as_nanos() + }) + .collect(); + ns.sort_unstable(); + let (obs, mut rels) = chain_world(); + let before = ALLOCS.with(Cell::get); + let passes = fixed_point(&mut rels, &obs, None, cfg, &d); + let allocs = ALLOCS.with(Cell::get) - before; + (ns[100], ns[190], passes, residue(&rels), allocs) + }; + let b = time(BASELINE); + let f = time(LOOP); + for (name, r) in [("baseline", b), ("feedback", f)] { + println!( + "fixed point, 3 relations / 4 observations, {name}: median {} ns, p95 {} ns, {} resolving passes, residue {}, allocs {}", + r.0, r.1, r.2, r.3, r.4 + ); + } + assert!( + f.3 < b.3, + "feedback discovers relations the baseline cannot" + ); +} + +/// A supports B, B supports C, C supports A, with no grounding. +fn three_cycle() -> (Vec, Vec) { + let obs = vec![ + observation(41, 42, 0), + observation(42, 43, 1), + observation(43, 41, 2), + ]; + // R0 40→42 needs 40→41 (R2); R1 40→43 needs 40→42 (R0); + // R2 40→41 needs 40→43 (R1). + let rels = vec![rel(0, 40, 42), rel(1, 40, 43), rel(2, 40, 41)]; + (obs, rels) +} + +#[test] +fn t8_an_indirect_cycle_cannot_ground_itself() { + let d = declarations(); + let (obs, mut rels) = three_cycle(); + fixed_point(&mut rels, &obs, None, LOOP, &d); + assert_eq!(residue(&rels), 3); +} + +#[test] +fn t8_disable_open_premises_lets_an_indirect_cycle_ground_itself() { + let d = declarations(); + let (obs, mut rels) = three_cycle(); + let broken = Loop { + feedback: true, + open_premises: true, + ignore_lineage: false, + }; + fixed_point(&mut rels, &obs, None, broken, &d); + assert!( + residue(&rels) < 3, + "the guard is load-bearing for indirect cycles too" + ); +} + +#[test] +fn t8_a_grounded_cycle_resolves_once_and_its_roots_stay_bounded() { + let d = declarations(); + let (mut obs, mut rels) = three_cycle(); + obs.push(observation(40, 41, 3)); // grounds R0 + fixed_point(&mut rels, &obs, None, LOOP, &d); + assert_eq!(residue(&rels), 0); + let roots: Vec = rels.iter().map(|r| r.roots).collect(); + for _ in 0..64 { + pass(&mut rels, &obs, None, LOOP, &d); + } + assert_eq!( + rels.iter().map(|r| r.roots).collect::>(), + roots, + "going round the cycle adds nothing" + ); + for r in &rels { + assert!( + r.roots.count_ones() <= obs.len() as u32, + "never more roots than observations" + ); + } +} + +/// Reopen a relation: its own lineage is dropped, its descendants keep theirs. +fn reopen(rels: &mut [Relation], idx: usize) { + let st = EpistemicState5::new( + Epi5Gen::V1, + Topology2::IndirectUnknown, + Certification3::Related, + ); + rels[idx].edge = rels[idx].edge.with_epistemic_raw5(st.raw()); + rels[idx].roots = 0; + rels[idx].derivations = 0; +} + +/// Ground the 3-cycle, then remove the ground and reopen R0. R1 and R2 are +/// still resolved and were derived through R0. +fn ungrounded_after_reopen(cfg: Loop) -> Vec { + let d = declarations(); + let (mut obs, mut rels) = three_cycle(); + obs.push(observation(40, 41, 3)); + fixed_point(&mut rels, &obs, None, LOOP, &d); + assert_eq!(residue(&rels), 0, "fixture: the grounded cycle resolves"); + assert_ne!(rels[2].derivations & 1, 0, "fixture: R2 descends from R0"); + obs.pop(); + reopen(&mut rels, 0); + fixed_point(&mut rels, &obs, None, cfg, &d); + rels +} + +#[test] +fn t8_a_reopened_relation_is_not_regrounded_by_its_descendants() { + let rels = ungrounded_after_reopen(LOOP); + assert_eq!( + topology(rels[0].edge), + Topology2::IndirectUnknown, + "R0 lost its ground; R2 was derived through R0 and may not restore it" + ); +} + +#[test] +fn t8_disable_lineage_guard_lets_descendants_reground_their_source() { + let rels = ungrounded_after_reopen(Loop { + feedback: true, + open_premises: false, + ignore_lineage: true, + }); + assert_eq!( + topology(rels[0].edge), + Topology2::IndirectKnown, + "the lineage guard is load-bearing: without it R2 re-grounds R0" + ); +} diff --git a/crates/lance-graph-planner/tests/residue_fixed_point.rs b/crates/lance-graph-planner/tests/residue_fixed_point.rs new file mode 100644 index 000000000..88bfab000 --- /dev/null +++ b/crates/lance-graph-planner/tests/residue_fixed_point.rs @@ -0,0 +1,266 @@ +//! Residue fixed point: computation alone cannot lower the residue; only new +//! evidence can. +//! +//! The claim under test. The reasoning loop is cheap (`pearl::hydrate` runs +//! per edge over the sealed chain), so it runs eagerly until it reaches a +//! fixed point. What is still open after that is RESIDUE: work the substrate +//! cannot do with the evidence it has. Two things must then hold: +//! +//! 1. More passes over the same evidence change nothing, bit for bit. +//! If they did, the residue was holding computation the loop should +//! already have done. +//! 2. The residue falls only when evidence arrives that it did not have +//! before. Repeating evidence it already had does not count. +//! +//! The residue measured here is the hydration debt in bits 59..63: edges +//! whose topology is still `IndirectUnknown` (an intermediate on the path is +//! not yet known). The surprise level proposed for bits 50..52 is not in the +//! layout yet (bits 50..52 are still plasticity), so it is not tested here. +//! +//! The loop proposes candidate intermediates from the chain itself: for an +//! edge `a → y`, every `b` with a sealed step `a → b` is tried. Proposing a +//! candidate is not evidence; `hydrate` promotes only when both `a → b` and +//! `b → y` are sealed. + +use causal_edge::edge::InferenceType; +use causal_edge::{CausalEdge64, CausalMask, PlasticityState}; +use lance_graph_contract::band_reading::EdgeProvenance; +use lance_graph_contract::class_view::ClassId; +use lance_graph_contract::epistemic_state5::{ + Certification3, Epi5Declarations, Epi5Gen, Epi5Reading, EpistemicState5, Topology2, +}; +use lance_graph_contract::rail_geometry::RailAxis; +use lance_graph_planner::chain_replay::ChainStep; +use lance_graph_planner::pearl::{hydrate, Reading}; + +const CLASS: ClassId = 0x0902; +const RAIL: RailAxis = RailAxis::Taxonomy; +const PREDICATE: u8 = 0x91; + +/// Bits 61..63: the certification coordinate. +const CERT_BITS: u64 = 0b111 << 61; + +fn declarations() -> Epi5Declarations { + let mut d = Epi5Declarations::new(); + d.declare( + CLASS, + RAIL, + Epi5Reading { + generation: Epi5Gen::V1, + }, + ); + d +} + +fn reading(d: &Epi5Declarations) -> Reading<'_> { + Reading { + declarations: d, + class: CLASS, + rail: RAIL, + provenance: EdgeProvenance::V2Stamped, + } +} + +fn raw_edge(s: u8, o: u8) -> CausalEdge64 { + CausalEdge64::pack( + s, + PREDICATE, + o, + 200, + 200, + CausalMask::SO, + 0b101, + InferenceType::Deduction, + PlasticityState::S_HOT, + 0, + ) +} + +/// An open relation `s → o`: an intermediate is claimed but not known. +fn open_relation(s: u8, o: u8, c: Certification3) -> CausalEdge64 { + let state = EpistemicState5::new(Epi5Gen::V1, Topology2::IndirectUnknown, c); + raw_edge(s, o).with_epistemic_raw5(state.raw()) +} + +/// A sealed binding `s → o`. +fn step(s: u8, o: u8) -> ChainStep { + (0, raw_edge(s, o)) +} + +fn topology(e: CausalEdge64) -> Topology2 { + EpistemicState5::decode(Epi5Gen::V1, e.epistemic_raw5()) + .expect("valid code") + .topology() +} + +/// One pass of the loop over the population: every still-open edge tries each +/// candidate intermediate the chain proposes. +fn pass(edges: &mut [CausalEdge64], sealed: &[ChainStep], d: &Epi5Declarations) { + for e in edges.iter_mut() { + if topology(*e) != Topology2::IndirectUnknown { + continue; + } + let (a, y) = (e.s_idx(), e.o_idx()); + let candidates: Vec = sealed + .iter() + .filter(|(_, s)| s.s_idx() == a) + .map(|(_, s)| s.o_idx()) + .collect(); + for b in candidates { + let (next, _) = hydrate(*e, (a, b, y), sealed, reading(d)).expect("declared reading"); + *e = next; + } + } +} + +/// Residue: edges whose intermediate is still unknown. +fn residue(edges: &[CausalEdge64]) -> usize { + edges + .iter() + .filter(|e| topology(**e) == Topology2::IndirectUnknown) + .count() +} + +fn bits(edges: &[CausalEdge64]) -> Vec { + edges.iter().map(|e| e.0).collect() +} + +/// Population of three open relations, each with different evidence: +/// - `1 → 3`: both bindings sealed (`1 → 2`, `2 → 3`), resolvable now. +/// - `4 → 6`: only `4 → 5` sealed, waiting on `5 → 6`. +/// - `7 → 9`: nothing sealed. +fn population() -> Vec { + vec![ + open_relation(1, 3, Certification3::Related), + open_relation(4, 6, Certification3::Supports), + open_relation(7, 9, Certification3::Associated), + ] +} + +fn initial_chain() -> Vec { + vec![step(1, 2), step(2, 3), step(4, 5)] +} + +/// Run passes until nothing changes; return how many passes moved anything. +fn run_to_fixed_point( + edges: &mut [CausalEdge64], + sealed: &[ChainStep], + d: &Epi5Declarations, +) -> usize { + for moving in 0..16 { + let before = bits(edges); + pass(edges, sealed, d); + if bits(edges) == before { + return moving; + } + } + panic!("the loop did not reach a fixed point in 16 passes"); +} + +#[test] +fn computation_reaches_a_fixed_point_and_more_passes_change_nothing() { + let d = declarations(); + let mut edges = population(); + let sealed = initial_chain(); + + assert_eq!( + residue(&edges), + 3, + "fixture: all three relations start open" + ); + + let moving = run_to_fixed_point(&mut edges, &sealed, &d); + assert_eq!(moving, 1, "one pass absorbs everything the evidence allows"); + assert_eq!(residue(&edges), 2, "only 1 → 3 had both bindings"); + assert_eq!(topology(edges[0]), Topology2::IndirectKnown); + + // The fixed point holds bit for bit under many more passes. + let at_fixed_point = bits(&edges); + for _ in 0..32 { + pass(&mut edges, &sealed, &d); + } + assert_eq!( + bits(&edges), + at_fixed_point, + "extra passes must not move any bit" + ); +} + +#[test] +fn repeated_evidence_does_not_lower_the_residue() { + let d = declarations(); + let mut edges = population(); + let mut sealed = initial_chain(); + run_to_fixed_point(&mut edges, &sealed, &d); + let at_fixed_point = bits(&edges); + + // Seal the same bindings again, many times. Nothing new is known. + for _ in 0..8 { + sealed.push(step(4, 5)); + sealed.push(step(1, 2)); + } + let moving = run_to_fixed_point(&mut edges, &sealed, &d); + + assert_eq!(moving, 0, "repetition is not evidence"); + assert_eq!(residue(&edges), 2); + assert_eq!(bits(&edges), at_fixed_point); +} + +#[test] +fn only_new_evidence_lowers_the_residue_and_only_where_it_lands() { + let d = declarations(); + let mut edges = population(); + let mut sealed = initial_chain(); + run_to_fixed_point(&mut edges, &sealed, &d); + let before = bits(&edges); + + // Evidence that touches no open relation changes nothing. + sealed.push(step(40, 41)); + assert_eq!(run_to_fixed_point(&mut edges, &sealed, &d), 0); + assert_eq!(bits(&edges), before); + + // The missing binding for 4 → 6 arrives. + sealed.push(step(5, 6)); + let moving = run_to_fixed_point(&mut edges, &sealed, &d); + + assert_eq!(moving, 1); + assert_eq!( + residue(&edges), + 1, + "4 → 6 resolved; 7 → 9 still has nothing" + ); + assert_eq!(topology(edges[1]), Topology2::IndirectKnown); + assert_eq!( + edges[0].0, before[0], + "an already-resolved edge is untouched" + ); + assert_eq!( + edges[2].0, before[2], + "evidence elsewhere does not touch 7 → 9" + ); +} + +#[test] +fn the_loop_moves_topology_only_never_certification_or_the_rest() { + let d = declarations(); + let start = population(); + let mut edges = start.clone(); + let mut sealed = initial_chain(); + run_to_fixed_point(&mut edges, &sealed, &d); + sealed.push(step(5, 6)); + run_to_fixed_point(&mut edges, &sealed, &d); + + let topology_bits = 0b11u64 << 59; + for (after, before) in edges.iter().zip(&start) { + assert_eq!( + after.0 & !topology_bits, + before.0 & !topology_bits, + "only bits 59..60 may change" + ); + assert_eq!( + after.0 & CERT_BITS, + before.0 & CERT_BITS, + "certification never moves here" + ); + } +}