From 9a09c668a460bbb3b6ce5cacce58ff285048b0d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 11:38:06 +0000 Subject: [PATCH 1/2] D-GSO-7: reasoning-band earning and downgrade (P7) The ReasoningBand on a CausalEdge64 (bits 61..63) is read only through band_reading::project_band (declared present, asserted provenance) and earned one rung per executed, passing proof under the shipped Pearl CausalMask: an SO observation a majority corroborates gives Association, a passed PO intervention on top gives Causal, a passed SPO counterfactual on top of that gives Counterfactual. Observations never lift past Association, a NotRun test never raises, and no rung is skipped. A failed test drops to the rung below the one it guarded; contradicting independent evidence (Quorum, silence excluded) caps at Association when a minority and drops to Surface when a majority; detected confounding (CausalMask::simpsons_paradox_risk) caps at Association. The same events replay to the same edge bits, and only bits 61..63 move. The ladder and thresholds are policy pins; Relation, Perspective, Meta and Transcendent have no obligation and are never reached. 6 tests (association cannot skip, executed-passing-only one rung, exhaustive no-rise-without-obligation sweep over 8 bands x every event, contradiction/confounding/failure lower, replay, unreadable band refuses), 7 disable runs red. Board: D-GSO-7 In PR; D-GSO-6 Shipped (#1358). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DgxrCafsJuAahpBs7oC14R --- .claude/board/STATUS_BOARD.md | 4 +- crates/cognitive-shader-driver/Cargo.toml | 5 + .../examples/README.md | 16 + .../examples/reasoning_band_probe.rs | 590 ++++++++++++++++++ 4 files changed, 613 insertions(+), 2 deletions(-) create mode 100644 crates/cognitive-shader-driver/examples/reasoning_band_probe.rs diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 290573bb5..9a991c1b7 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -32,8 +32,8 @@ Plan: `.claude/plans/2026-10-06-global-sudoku-replayable-orchestration-v1.md`. O | **D-GSO-3** | P3: global ontology agreement/disagreement probe (mammal fixture) | Shipped (#1350) | `cognitive-shader-driver/examples/ontology_agreement_probe.rs`: `NiblePath` hierarchy + `ontology_warrant::Quorum`/`SourceVerdict` folded per observation into local (self + siblings), up (ancestors), down (descendants); binary-entropy tension; all four cells + unknown reached; 6 tests, 5 disable runs red | | **D-GSO-4** | P4: entropy × topology routing probe | Shipped (#1351) | `cognitive-shader-driver/examples/entropy_topology_probe.rs` = F-ECG-1 and F-ECG-2 at probe level (F-ECG-3 open: no real corpus carries topology bits yet): bits 59..60 read through `BandDeclarations::project_truth` (topology lens, asserted provenance), known-share census as competence, `SettlementSignals::cell`, §4b walker route; equal measured entropy (0.8167 bits) → Crystal vs Glass; 6 tests, 4 disable runs red | | **D-GSO-5** | P5: first recipe quartet over existing primitives | Shipped (#1355) | `cognitive-shader-driver/examples/recipe_quartet_probe.rs`: four `match`-arm recipes over `Quorum::observe`, `Quad8::fold_product`, `Morton8x8` + `PalettePerturbation::hop`, `GadamerRevision::revise` (verdict left `NotRun`: removing the new roots is tautological, so the counterfactual moves to P7); a thread-local counting allocator shows zero allocations per recipe (no instruction vector); `ProbeRecipe` is its own type, not a shipped `recipes` ID; ordinal meanings not canonized; no Pearl projection (P7) and no selector (P6); 7 tests, 6 disable runs red | -| **D-GSO-6** | P6: deterministic, versioned recipe selector | In PR | `cognitive-shader-driver/examples/recipe_selector_probe.rs`: `SelectorPolicy::select(state)` is a pure `const fn` over a declared 4-fact `EpistemicState`; V1 = §11 order (fold, bound, local, revise), rests only on the settled state; V2 swaps two steps (differs on 2 of 16 states); a recorded `Selection` (policy + state) replays under its own policy; every cycle rests in one step per open condition and replays identically; the version covers selection only, not recipe implementations (§13 digest open); 5 tests, 5 disable runs red | -| **D-GSO-7** | P7: reasoning-band earning and downgrade | Queued | association cannot skip to causal permission; contradicting evidence lowers it; replayable | +| **D-GSO-6** | P6: deterministic, versioned recipe selector | Shipped (#1358) | `cognitive-shader-driver/examples/recipe_selector_probe.rs`: `SelectorPolicy::select(state)` is a pure `const fn` over a declared 4-fact `EpistemicState`; V1 = §11 order (fold, bound, local, revise), rests only on the settled state; V2 swaps two steps (differs on 2 of 16 states); a recorded `Selection` (policy + state) replays under its own policy; every cycle rests in one step per open condition and replays identically; the version covers selection only, not recipe implementations (§13 digest open); 5 tests, 5 disable runs red | +| **D-GSO-7** | P7: reasoning-band earning and downgrade | In PR | `cognitive-shader-driver/examples/reasoning_band_probe.rs`: `ReasoningBand` (bits 61..63) read only via `band_reading::project_band`; earned one rung per executed passing proof under the shipped Pearl `CausalMask` (SO majority → Association, PO passed → Causal, SPO passed → Counterfactual), no skipping, `NotRun` never raises; failed test, contradicting `Quorum` (minority caps at Association, majority drops to Surface) and `simpsons_paradox_risk` lower it; replay gives the same edge bits; unreadable provenance / undeclared / band-absent refuse; ladder and thresholds are policy pins; 6 tests, 7 disable runs red | | **D-GSO-8** | P8: seal boundary | Queued | replay from the prior seal reproduces the next seal | ## D-PLX — Population-law cross-check (2026-10-03) diff --git a/crates/cognitive-shader-driver/Cargo.toml b/crates/cognitive-shader-driver/Cargo.toml index 7ad763629..f79f02830 100644 --- a/crates/cognitive-shader-driver/Cargo.toml +++ b/crates/cognitive-shader-driver/Cargo.toml @@ -73,6 +73,11 @@ test = true name = "recipe_selector_probe" test = true +# D-GSO-7 reasoning-band probe; tests run under `cargo test`. +[[example]] +name = "reasoning_band_probe" +test = true + [dependencies] lance-graph-contract = { path = "../lance-graph-contract" } # Read-only ontology registry (Phase 7, v4 plan). default-features = false so the diff --git a/crates/cognitive-shader-driver/examples/README.md b/crates/cognitive-shader-driver/examples/README.md index b4497640b..445e2d2be 100644 --- a/crates/cognitive-shader-driver/examples/README.md +++ b/crates/cognitive-shader-driver/examples/README.md @@ -158,3 +158,19 @@ version covers selection only, not the recipes' implementations (plan §13). ```bash cargo run -p cognitive-shader-driver --example recipe_selector_probe ``` + +## reasoning_band_probe.rs + +D-GSO-7 (plan §18 P7). The `ReasoningBand` on a `CausalEdge64` (bits 61..63, +read only through `band_reading::project_band`) is earned one rung per executed, +passing proof: an `SO` observation a majority corroborates gives `Association`, +a passed `PO` intervention on top of it gives `Causal`, a passed `SPO` +counterfactual on top of that gives `Counterfactual`. Observations never lift +past `Association`, a test that was not run never raises, and no rung is +skipped. A failed test, contradicting independent evidence (`Quorum`) or +confounding (`CausalMask::simpsons_paradox_risk`) lowers the band, and the same +events replay to the same edge bits. The ladder and thresholds are policy pins. + +```bash +cargo run -p cognitive-shader-driver --example reasoning_band_probe +``` diff --git a/crates/cognitive-shader-driver/examples/reasoning_band_probe.rs b/crates/cognitive-shader-driver/examples/reasoning_band_probe.rs new file mode 100644 index 000000000..669cda55c --- /dev/null +++ b/crates/cognitive-shader-driver/examples/reasoning_band_probe.rs @@ -0,0 +1,590 @@ +//! D-GSO-7 (P7): reasoning-band earning and downgrade. +//! +//! Plan: `.claude/plans/2026-10-06-global-sudoku-replayable-orchestration-v1.md` +//! §7 (Pearl 2³ projection), §8 (bits 61..63 are a permission band) and §18 P7. +//! +//! Claim under test: the reasoning band on a `CausalEdge64` rises only when a +//! proof obligation was actually executed and passed, falls under +//! contradicting independent evidence or detected confounding, and every +//! change replays from the same events. +//! +//! # Reused, unchanged +//! +//! - **The band:** `ReasoningBand` in bits 61..63, written only with +//! `with_reasoning_band` and read only through +//! `band_reading::BandDeclarations::project_band` (declared `Present`, +//! asserted provenance). An unreadable register gives no transition. +//! - **The projection a test ran under:** Pearl's `CausalMask`: `SO` +//! association, `PO` intervention, `SPO` counterfactual, `SP` confounder +//! check. +//! - **Independent evidence:** `ontology_warrant::Quorum` counts (silence is +//! abstention). +//! - **Confounding:** `CausalMask::simpsons_paradox_risk` on the `SO` and `PO` +//! direction triads. +//! +//! # The ladder this probe enforces (a policy pin, not canon) +//! +//! | target band | obligation | must already hold | +//! |---|---|---| +//! | `Association` | an `SO` observation a majority of speaking sources corroborate | — | +//! | `Causal` | an intervention test under `PO`, run and passed | `Association` | +//! | `Counterfactual` | a counterfactual test under `SPO`, run and passed | `Causal` | +//! +//! One rung per obligation, no skipping: an observation never certifies more +//! than association, and a passed intervention on an edge that never held +//! association does not raise it. +//! +//! Downgrades: +//! +//! - a failed test drops the band to the rung below the one it guarded; +//! - contradicting independent evidence caps the band at `Association` when +//! the contradiction is a minority, and drops it to `Surface` when the +//! contradicting sources outnumber the corroborating ones; +//! - detected confounding caps the band at `Association`. +//! +//! `Relation`, `Perspective`, `Meta` and `Transcendent` have no obligation here +//! and are never reached. The ordinals are not the ladder; the proof chain is. +//! +//! # What this probe does not decide +//! +//! - The thresholds (majority, minority cap) are policy pins. +//! - A "test" is supplied as an outcome; running the intervention or +//! counterfactual itself is the caller's job. What the probe enforces is +//! that a `NotRun` outcome can never raise the band. +//! - No writes to stored rows. The band lives on a local `CausalEdge64`. +//! +//! Run: `cargo run -p cognitive-shader-driver --example reasoning_band_probe` +//! Tests: `cargo test -p cognitive-shader-driver --example reasoning_band_probe` + +use causal_edge::edge::CausalEdge64; +use causal_edge::layout::ReasoningBand; +use causal_edge::pearl::CausalMask; +use lance_graph_contract::band_reading::{ + BandDeclarations, BandPresence, BandReadError, BandReading, EdgeProvenance, +}; +use lance_graph_contract::class_view::ClassId; +use lance_graph_contract::ontology_warrant::Quorum; +use lance_graph_contract::rail_geometry::RailAxis; + +/// The class whose edges carry a band in this probe. +const CLASS: ClassId = 0x0901; +/// The rail the band is declared on. +const RAIL: RailAxis = RailAxis::Taxonomy; + +/// One class, band declared present. +fn declarations() -> BandDeclarations { + let mut d = BandDeclarations::new(); + d.declare( + CLASS, + RAIL, + BandReading { + band: BandPresence::Present, + ..BandReading::ZERO_FALLBACK + }, + ); + d +} + +/// How a test ended. `NotRun` is the honest default. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TestOutcome { + Passed, + Failed, + NotRun, +} + +/// One thing that happened to the claim the edge carries. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Event { + /// Observational evidence folded under a projection. + Observed { mask: CausalMask, quorum: Quorum }, + /// A proof test under a projection, with its outcome. + Tested { + mask: CausalMask, + outcome: TestOutcome, + }, + /// Independent evidence bearing on the claim: corroborating, silent and + /// conflicting sources. + Independent { quorum: Quorum }, + /// A confounder check: the `SO` and `PO` direction triads. + ConfounderCheck { so_direction: u8, po_direction: u8 }, +} + +/// Does a majority of speaking sources corroborate? Silence is not counted. +const fn majority_corroborates(q: Quorum) -> bool { + q.corroborating > q.conflicting +} + +/// The band after `event`, from `band`. Pure; the ladder above in code. +fn next_band(band: ReasoningBand, event: Event) -> ReasoningBand { + use ReasoningBand::{Association, Causal, Counterfactual, Surface}; + let cap = |b: ReasoningBand, at: ReasoningBand| { + if b.to_bits_3() > at.to_bits_3() { + at + } else { + b + } + }; + match event { + Event::Observed { mask, quorum } => { + if mask == CausalMask::SO && band == Surface && majority_corroborates(quorum) { + Association + } else { + band + } + } + Event::Tested { mask, outcome } => match (mask, outcome) { + (CausalMask::PO, TestOutcome::Passed) if band == Association => Causal, + (CausalMask::SPO, TestOutcome::Passed) if band == Causal => Counterfactual, + (CausalMask::PO, TestOutcome::Failed) => cap(band, Association), + (CausalMask::SPO, TestOutcome::Failed) => cap(band, Causal), + _ => band, + }, + Event::Independent { quorum } => { + if quorum.conflicting > quorum.corroborating { + Surface + } else if quorum.conflicting > 0 { + cap(band, Association) + } else { + band + } + } + Event::ConfounderCheck { + so_direction, + po_direction, + } => { + if CausalMask::simpsons_paradox_risk(so_direction, po_direction) { + cap(band, Association) + } else { + band + } + } + } +} + +/// Read the band through the contract, apply `event`, write it back. +fn apply( + decl: &BandDeclarations, + edge: CausalEdge64, + provenance: EdgeProvenance, + event: Event, +) -> Result { + let raw = decl.project_band(CLASS, RAIL, edge.reasoning_band().to_bits_3(), provenance)?; + let band = ReasoningBand::from_bits_3(raw); + Ok(edge.with_reasoning_band(next_band(band, event))) +} + +/// Fold a whole event sequence over an edge. Fails on the first unreadable +/// read. +fn replay( + decl: &BandDeclarations, + start: CausalEdge64, + provenance: EdgeProvenance, + events: &[Event], +) -> Result { + events + .iter() + .try_fold(start, |e, &ev| apply(decl, e, provenance, ev)) +} + +/// The full earning path: observe, intervene, counterfactual. +#[cfg(test)] +fn earning_path() -> [Event; 3] { + [ + Event::Observed { + mask: CausalMask::SO, + quorum: Quorum::new(3, 1, 1), + }, + Event::Tested { + mask: CausalMask::PO, + outcome: TestOutcome::Passed, + }, + Event::Tested { + mask: CausalMask::SPO, + outcome: TestOutcome::Passed, + }, + ] +} + +fn main() { + let decl = declarations(); + let scenario = [ + Event::Observed { + mask: CausalMask::SO, + quorum: Quorum::new(3, 1, 1), + }, + Event::Tested { + mask: CausalMask::PO, + outcome: TestOutcome::NotRun, + }, + Event::Tested { + mask: CausalMask::PO, + outcome: TestOutcome::Passed, + }, + Event::ConfounderCheck { + so_direction: 0b100, + po_direction: 0b000, + }, + Event::Tested { + mask: CausalMask::PO, + outcome: TestOutcome::Passed, + }, + Event::Tested { + mask: CausalMask::SPO, + outcome: TestOutcome::Failed, + }, + Event::Independent { + quorum: Quorum::new(1, 0, 3), + }, + ]; + let mut edge = CausalEdge64::ZERO; + println!("start: {:?}", edge.reasoning_band()); + for ev in scenario { + edge = apply(&decl, edge, EdgeProvenance::V2Stamped, ev).expect("declared"); + println!("{ev:?} -> {:?}", edge.reasoning_band()); + } + let again = replay( + &decl, + CausalEdge64::ZERO, + EdgeProvenance::V2Stamped, + &scenario, + ) + .expect("declared"); + assert_eq!(again.0, edge.0); + println!("replayed: same edge bits"); +} + +#[cfg(test)] +mod tests { + use super::*; + use ReasoningBand::*; + + const ALL_BANDS: [ReasoningBand; 8] = [ + Surface, + Association, + Relation, + Causal, + Counterfactual, + Perspective, + Meta, + Transcendent, + ]; + const ALL_MASKS: [CausalMask; 8] = [ + CausalMask::None, + CausalMask::O, + CausalMask::P, + CausalMask::PO, + CausalMask::S, + CausalMask::SO, + CausalMask::SP, + CausalMask::SPO, + ]; + + fn obs(mask: CausalMask, q: Quorum) -> Event { + Event::Observed { mask, quorum: q } + } + fn test(mask: CausalMask, outcome: TestOutcome) -> Event { + Event::Tested { mask, outcome } + } + + /// Every event the tests sweep: all masks × a few quorums for + /// observations, all masks × outcomes for tests, a few independent + /// quorums and confounder checks. + fn all_events() -> Vec { + let quorums = [ + Quorum::new(0, 0, 0), + Quorum::new(3, 1, 1), + Quorum::new(1, 0, 1), + Quorum::new(1, 0, 3), + Quorum::new(0, 5, 0), + ]; + let mut v = Vec::new(); + for m in ALL_MASKS { + for q in quorums { + v.push(obs(m, q)); + } + for o in [ + TestOutcome::Passed, + TestOutcome::Failed, + TestOutcome::NotRun, + ] { + v.push(test(m, o)); + } + } + for q in quorums { + v.push(Event::Independent { quorum: q }); + } + for (so, po) in [(0b100, 0b000), (0b100, 0b100), (0, 0)] { + v.push(Event::ConfounderCheck { + so_direction: so, + po_direction: po, + }); + } + v + } + + /// FAILS IF: any amount or kind of observational evidence lifts the band + /// past `Association`, or an observation under a non-`SO` projection + /// lifts it at all. + #[test] + fn association_cannot_skip_to_causal() { + let decl = declarations(); + let flood: Vec = ALL_MASKS + .iter() + .flat_map(|&m| std::iter::repeat_n(obs(m, Quorum::new(50, 0, 0)), 20)) + .collect(); + let e = replay(&decl, CausalEdge64::ZERO, EdgeProvenance::V2Stamped, &flood).unwrap(); + assert_eq!(e.reasoning_band(), Association); + + for m in ALL_MASKS.into_iter().filter(|&m| m != CausalMask::SO) { + assert_eq!( + next_band(Surface, obs(m, Quorum::new(9, 0, 0))), + Surface, + "{m:?}" + ); + } + // A split or silent quorum certifies nothing either. + assert_eq!( + next_band(Surface, obs(CausalMask::SO, Quorum::new(1, 0, 1))), + Surface + ); + assert_eq!( + next_band(Surface, obs(CausalMask::SO, Quorum::new(0, 9, 0))), + Surface + ); + } + + /// FAILS IF: a test that was not run, or failed, raises the band; a passed + /// intervention fails to raise `Association` to `Causal`; or a proof is + /// accepted for a rung whose predecessor is not held. + #[test] + fn only_an_executed_passing_test_raises_and_only_one_rung() { + assert_eq!( + next_band(Association, test(CausalMask::PO, TestOutcome::Passed)), + Causal + ); + assert_eq!( + next_band(Causal, test(CausalMask::SPO, TestOutcome::Passed)), + Counterfactual + ); + + for o in [TestOutcome::NotRun, TestOutcome::Failed] { + assert_eq!( + next_band(Association, test(CausalMask::PO, o)), + Association, + "{o:?}" + ); + assert_eq!(next_band(Causal, test(CausalMask::SPO, o)), Causal, "{o:?}"); + } + // No skipping: an intervention on an edge that never held association, + // and a counterfactual on an edge that never held causal permission. + assert_eq!( + next_band(Surface, test(CausalMask::PO, TestOutcome::Passed)), + Surface + ); + assert_eq!( + next_band(Association, test(CausalMask::SPO, TestOutcome::Passed)), + Association + ); + // A passed test under the wrong projection certifies nothing. + for m in [ + CausalMask::SO, + CausalMask::SP, + CausalMask::S, + CausalMask::None, + ] { + assert_eq!( + next_band(Association, test(m, TestOutcome::Passed)), + Association, + "{m:?}" + ); + } + } + + /// FAILS IF: any transition from any band raises it by more than one rung + /// of the ladder, or raises it without the matching obligation. Sweeps + /// all 8 bands × every event kind. + #[test] + fn no_rise_without_its_obligation_anywhere() { + let mut rises = 0; + for b in ALL_BANDS { + for ev in all_events() { + let n = next_band(b, ev); + if n.to_bits_3() <= b.to_bits_3() { + continue; + } + rises += 1; + let legal = matches!( + (b, n, ev), + ( + Surface, + Association, + Event::Observed { + mask: CausalMask::SO, + .. + } + ) | ( + Association, + Causal, + Event::Tested { + mask: CausalMask::PO, + outcome: TestOutcome::Passed + } + ) | ( + Causal, + Counterfactual, + Event::Tested { + mask: CausalMask::SPO, + outcome: TestOutcome::Passed + } + ) + ); + assert!(legal, "{b:?} --{ev:?}--> {n:?}"); + } + } + assert_eq!(rises, 3, "exactly the three ladder steps can rise"); + } + + /// FAILS IF: contradicting independent evidence or confounding leaves an + /// earned band in place, or silence counts as contradiction. + #[test] + fn contradiction_and_confounding_lower_the_band() { + // Majority contradiction: suspended to Surface. + assert_eq!( + next_band( + Counterfactual, + Event::Independent { + quorum: Quorum::new(1, 0, 3) + } + ), + Surface + ); + // Minority contradiction: causal permission suspended, association kept. + assert_eq!( + next_band( + Counterfactual, + Event::Independent { + quorum: Quorum::new(3, 0, 1) + } + ), + Association + ); + assert_eq!( + next_band( + Causal, + Event::Independent { + quorum: Quorum::new(3, 0, 1) + } + ), + Association + ); + // Silence and pure corroboration lower nothing. + assert_eq!( + next_band( + Causal, + Event::Independent { + quorum: Quorum::new(0, 9, 0) + } + ), + Causal + ); + assert_eq!( + next_band( + Causal, + Event::Independent { + quorum: Quorum::new(4, 2, 0) + } + ), + Causal + ); + // Confounding (SO and PO disagree in direction) caps at Association. + let confounded = Event::ConfounderCheck { + so_direction: 0b100, + po_direction: 0b000, + }; + let clean = Event::ConfounderCheck { + so_direction: 0b100, + po_direction: 0b100, + }; + assert_eq!(next_band(Counterfactual, confounded), Association); + assert_eq!(next_band(Counterfactual, clean), Counterfactual); + // A failed test drops to the rung below the one it guarded. + assert_eq!( + next_band(Counterfactual, test(CausalMask::SPO, TestOutcome::Failed)), + Causal + ); + assert_eq!( + next_band(Counterfactual, test(CausalMask::PO, TestOutcome::Failed)), + Association + ); + // A downgrade never raises: Surface stays Surface. + for ev in all_events() { + if let Event::Independent { .. } | Event::ConfounderCheck { .. } = ev { + assert_eq!(next_band(Surface, ev), Surface, "{ev:?}"); + } + } + } + + /// FAILS IF: the same events from the same edge end in different bits, the + /// band on the edge is not the one the ladder computes, or a band change + /// disturbs the rest of the edge. + #[test] + fn band_changes_replay_from_the_events() { + let decl = declarations(); + let mut events = earning_path().to_vec(); + events.push(Event::Independent { + quorum: Quorum::new(3, 0, 1), + }); + events.push(test(CausalMask::PO, TestOutcome::Passed)); + let start = CausalEdge64::ZERO.with_w_slot(17); + + let a = replay(&decl, start, EdgeProvenance::V2Stamped, &events).unwrap(); + let b = replay(&decl, start, EdgeProvenance::V2Stamped, &events).unwrap(); + assert_eq!(a.0, b.0); + // Earned to Counterfactual, cut to Association by a minority + // contradiction, re-earned to Causal by a fresh passing intervention. + assert_eq!(a.reasoning_band(), Causal); + // Only bits 61..63 moved. + assert_eq!(a.with_reasoning_band(Surface).0, start.0); + + // The trace is the fold of `next_band`, step for step. + let mut band = Surface; + let mut edge = start; + for &ev in &events { + band = next_band(band, ev); + edge = apply(&decl, edge, EdgeProvenance::V2Stamped, ev).unwrap(); + assert_eq!(edge.reasoning_band(), band); + } + } + + /// FAILS IF: a band is read from bits the contract refuses: unknown or v1 + /// provenance, an undeclared class, or a class that declares no band. + #[test] + fn an_unreadable_band_gives_no_transition() { + let decl = declarations(); + let ev = earning_path()[0]; + for p in [EdgeProvenance::Unknown, EdgeProvenance::V1Legacy] { + assert_eq!( + apply(&decl, CausalEdge64::ZERO, p, ev), + Err(BandReadError::UnknownProvenance), + "{p:?}" + ); + } + assert_eq!( + apply( + &BandDeclarations::new(), + CausalEdge64::ZERO, + EdgeProvenance::V2Stamped, + ev + ), + Err(BandReadError::UndeclaredClass(CLASS)) + ); + let mut absent = BandDeclarations::new(); + absent.declare(CLASS, RAIL, BandReading::ZERO_FALLBACK); + assert_eq!( + apply(&absent, CausalEdge64::ZERO, EdgeProvenance::V2Stamped, ev), + Err(BandReadError::BandAbsent) + ); + assert!(apply(&decl, CausalEdge64::ZERO, EdgeProvenance::V3Register, ev).is_ok()); + } +} From c46677d66847a3207e62450fb44911702f40efba Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 11:53:21 +0000 Subject: [PATCH 2/2] D-GSO-7: derive test outcomes from executed trials, never from input Codex review: Tested { outcome: Passed } let any caller fabricate a pass and raise the band, so the probe showed an asserted transition, not the P7 invariant that the obligation was actually executed. Events now carry trial data and run() executes it: - intervention (PO): treated vs control counts; passed when both arms were measured and the treated rate is higher, failed otherwise, not run when an arm is empty; - counterfactual (SPO): a removal attack on premise masks; passed when the conclusion derives with the candidate and not without it, failed when it never derives or the candidate is dispensable, not run when there is nothing to attack. There is no Passed input left. Replay re-executes the trials. New test a_pass_is_derived_from_the_trial_data; disable runs D8-D10 (rate check removed, removal never re-derives, empty arm counted as run) go red, and the earlier ladder disables still do. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DgxrCafsJuAahpBs7oC14R --- .claude/board/STATUS_BOARD.md | 2 +- .../examples/README.md | 4 +- .../examples/reasoning_band_probe.rs | 279 ++++++++++++++---- 3 files changed, 227 insertions(+), 58 deletions(-) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 9a991c1b7..c12c4b835 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -33,7 +33,7 @@ Plan: `.claude/plans/2026-10-06-global-sudoku-replayable-orchestration-v1.md`. O | **D-GSO-4** | P4: entropy × topology routing probe | Shipped (#1351) | `cognitive-shader-driver/examples/entropy_topology_probe.rs` = F-ECG-1 and F-ECG-2 at probe level (F-ECG-3 open: no real corpus carries topology bits yet): bits 59..60 read through `BandDeclarations::project_truth` (topology lens, asserted provenance), known-share census as competence, `SettlementSignals::cell`, §4b walker route; equal measured entropy (0.8167 bits) → Crystal vs Glass; 6 tests, 4 disable runs red | | **D-GSO-5** | P5: first recipe quartet over existing primitives | Shipped (#1355) | `cognitive-shader-driver/examples/recipe_quartet_probe.rs`: four `match`-arm recipes over `Quorum::observe`, `Quad8::fold_product`, `Morton8x8` + `PalettePerturbation::hop`, `GadamerRevision::revise` (verdict left `NotRun`: removing the new roots is tautological, so the counterfactual moves to P7); a thread-local counting allocator shows zero allocations per recipe (no instruction vector); `ProbeRecipe` is its own type, not a shipped `recipes` ID; ordinal meanings not canonized; no Pearl projection (P7) and no selector (P6); 7 tests, 6 disable runs red | | **D-GSO-6** | P6: deterministic, versioned recipe selector | Shipped (#1358) | `cognitive-shader-driver/examples/recipe_selector_probe.rs`: `SelectorPolicy::select(state)` is a pure `const fn` over a declared 4-fact `EpistemicState`; V1 = §11 order (fold, bound, local, revise), rests only on the settled state; V2 swaps two steps (differs on 2 of 16 states); a recorded `Selection` (policy + state) replays under its own policy; every cycle rests in one step per open condition and replays identically; the version covers selection only, not recipe implementations (§13 digest open); 5 tests, 5 disable runs red | -| **D-GSO-7** | P7: reasoning-band earning and downgrade | In PR | `cognitive-shader-driver/examples/reasoning_band_probe.rs`: `ReasoningBand` (bits 61..63) read only via `band_reading::project_band`; earned one rung per executed passing proof under the shipped Pearl `CausalMask` (SO majority → Association, PO passed → Causal, SPO passed → Counterfactual), no skipping, `NotRun` never raises; failed test, contradicting `Quorum` (minority caps at Association, majority drops to Surface) and `simpsons_paradox_risk` lower it; replay gives the same edge bits; unreadable provenance / undeclared / band-absent refuse; ladder and thresholds are policy pins; 6 tests, 7 disable runs red | +| **D-GSO-7** | P7: reasoning-band earning and downgrade | In PR | `cognitive-shader-driver/examples/reasoning_band_probe.rs`: `ReasoningBand` (bits 61..63) read only via `band_reading::project_band`; earned one rung per executed passing proof under the shipped Pearl `CausalMask` (SO majority → Association, PO intervention trial passed → Causal, SPO removal attack passed → Counterfactual), outcomes executed from trial data and never supplied, no skipping, `NotRun` never raises; failed test, contradicting `Quorum` (minority caps at Association, majority drops to Surface) and `simpsons_paradox_risk` lower it; replay gives the same edge bits; unreadable provenance / undeclared / band-absent refuse; ladder and thresholds are policy pins; 7 tests, 10 disable runs red | | **D-GSO-8** | P8: seal boundary | Queued | replay from the prior seal reproduces the next seal | ## D-PLX — Population-law cross-check (2026-10-03) diff --git a/crates/cognitive-shader-driver/examples/README.md b/crates/cognitive-shader-driver/examples/README.md index 445e2d2be..ea49f7a17 100644 --- a/crates/cognitive-shader-driver/examples/README.md +++ b/crates/cognitive-shader-driver/examples/README.md @@ -167,7 +167,9 @@ passing proof: an `SO` observation a majority corroborates gives `Association`, a passed `PO` intervention on top of it gives `Causal`, a passed `SPO` counterfactual on top of that gives `Counterfactual`. Observations never lift past `Association`, a test that was not run never raises, and no rung is -skipped. A failed test, contradicting independent evidence (`Quorum`) or +skipped. A pass is never supplied: events carry trial data (intervention +counts, a removal attack on premise masks) and the probe executes the trial to +derive the outcome. A failed test, contradicting independent evidence (`Quorum`) or confounding (`CausalMask::simpsons_paradox_risk`) lowers the band, and the same events replay to the same edge bits. The ladder and thresholds are policy pins. diff --git a/crates/cognitive-shader-driver/examples/reasoning_band_probe.rs b/crates/cognitive-shader-driver/examples/reasoning_band_probe.rs index 669cda55c..be90349ae 100644 --- a/crates/cognitive-shader-driver/examples/reasoning_band_probe.rs +++ b/crates/cognitive-shader-driver/examples/reasoning_band_probe.rs @@ -27,8 +27,24 @@ //! | target band | obligation | must already hold | //! |---|---|---| //! | `Association` | an `SO` observation a majority of speaking sources corroborate | — | -//! | `Causal` | an intervention test under `PO`, run and passed | `Association` | -//! | `Counterfactual` | a counterfactual test under `SPO`, run and passed | `Causal` | +//! | `Causal` | an intervention trial under `PO`, executed here and passed | `Association` | +//! | `Counterfactual` | a removal attack under `SPO`, executed here and passed | `Causal` | +//! +//! # A pass is computed, never supplied +//! +//! An event carries trial *data*, not a verdict. [`run`] executes the trial and +//! derives the outcome, so no caller can hand in a `Passed`: +//! +//! - **Intervention (`PO`):** treated and control counts from a run under +//! do(P). Passed when both arms were measured and the treated rate is +//! higher; failed otherwise; not run when an arm is empty. +//! - **Counterfactual (`SPO`):** a removal attack on premise masks. The +//! conclusion is the rule's premise set. Passed when it derives with the +//! candidate and stops deriving without it; failed when it does not derive +//! at all or still derives without the candidate (dispensable); not run when +//! the candidate is empty or not among the premises. +//! +//! Replay executes the trials again from the same data. //! //! One rung per obligation, no skipping: an observation never certifies more //! than association, and a passed intervention on an edge that never held @@ -48,9 +64,12 @@ //! # What this probe does not decide //! //! - The thresholds (majority, minority cap) are policy pins. -//! - A "test" is supplied as an outcome; running the intervention or -//! counterfactual itself is the caller's job. What the probe enforces is -//! that a `NotRun` outcome can never raise the band. +//! - The trial data are evidence the caller supplies; the probe does not +//! collect them. Whether that evidence is genuine is the evidence layer's +//! job; what this probe guarantees is that the band follows from executing +//! the trial on it, not from a stated outcome. +//! - The trial rules (rate comparison, set-based removal) are minimal stand-ins +//! for real intervention statistics and derivation. //! - No writes to stored rows. The band lives on a local `CausalEdge64`. //! //! Run: `cargo run -p cognitive-shader-driver --example reasoning_band_probe` @@ -85,7 +104,7 @@ fn declarations() -> BandDeclarations { d } -/// How a test ended. `NotRun` is the honest default. +/// How a trial ended. Computed by [`run`], never supplied. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum TestOutcome { Passed, @@ -93,16 +112,70 @@ enum TestOutcome { NotRun, } +/// Counts from an intervention run under do(P): hits and size of each arm. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct InterventionTrial { + treated_hits: u16, + treated_n: u16, + control_hits: u16, + control_n: u16, +} + +/// A removal attack: does the conclusion (the `rule`'s premise set) still +/// derive from `premises` once `candidate` is removed? +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct RemovalTrial { + premises: u64, + rule: u64, + candidate: u64, +} + +/// The data of one proof trial. Its projection is fixed by its kind. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Trial { + Intervention(InterventionTrial), + Counterfactual(RemovalTrial), + /// A test was named under a projection but nothing was executed. + NotRun(CausalMask), +} + +/// Execute a trial and derive its projection and outcome from its data. +fn run(trial: Trial) -> (CausalMask, TestOutcome) { + match trial { + Trial::Intervention(t) => { + let outcome = if t.treated_n == 0 || t.control_n == 0 { + TestOutcome::NotRun + } else if u32::from(t.treated_hits) * u32::from(t.control_n) + > u32::from(t.control_hits) * u32::from(t.treated_n) + { + TestOutcome::Passed + } else { + TestOutcome::Failed + }; + (CausalMask::PO, outcome) + } + Trial::Counterfactual(t) => { + let derives = |p: u64| p & t.rule == t.rule; + let outcome = if t.candidate == 0 || t.premises & t.candidate != t.candidate { + TestOutcome::NotRun + } else if derives(t.premises) && !derives(t.premises & !t.candidate) { + TestOutcome::Passed + } else { + TestOutcome::Failed + }; + (CausalMask::SPO, outcome) + } + Trial::NotRun(mask) => (mask, TestOutcome::NotRun), + } +} + /// One thing that happened to the claim the edge carries. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Event { /// Observational evidence folded under a projection. Observed { mask: CausalMask, quorum: Quorum }, - /// A proof test under a projection, with its outcome. - Tested { - mask: CausalMask, - outcome: TestOutcome, - }, + /// A proof trial, executed when the event is applied. + Tested(Trial), /// Independent evidence bearing on the claim: corroborating, silent and /// conflicting sources. Independent { quorum: Quorum }, @@ -133,7 +206,7 @@ fn next_band(band: ReasoningBand, event: Event) -> ReasoningBand { band } } - Event::Tested { mask, outcome } => match (mask, outcome) { + Event::Tested(trial) => match run(trial) { (CausalMask::PO, TestOutcome::Passed) if band == Association => Causal, (CausalMask::SPO, TestOutcome::Passed) if band == Causal => Counterfactual, (CausalMask::PO, TestOutcome::Failed) => cap(band, Association), @@ -187,6 +260,27 @@ fn replay( .try_fold(start, |e, &ev| apply(decl, e, provenance, ev)) } +/// An intervention run whose treated arm beats control (30/40 vs 12/40). +const INTERVENTION_PASS: Trial = Trial::Intervention(InterventionTrial { + treated_hits: 30, + treated_n: 40, + control_hits: 12, + control_n: 40, +}); +/// A removal attack where the candidate is a required premise. +const REMOVAL_PASS: Trial = Trial::Counterfactual(RemovalTrial { + premises: 0b0111, + rule: 0b0011, + candidate: 0b0010, +}); +/// A removal attack where the candidate is not needed: the conclusion still +/// derives without it. +const REMOVAL_FAIL: Trial = Trial::Counterfactual(RemovalTrial { + premises: 0b0111, + rule: 0b0011, + candidate: 0b0100, +}); + /// The full earning path: observe, intervene, counterfactual. #[cfg(test)] fn earning_path() -> [Event; 3] { @@ -195,14 +289,8 @@ fn earning_path() -> [Event; 3] { mask: CausalMask::SO, quorum: Quorum::new(3, 1, 1), }, - Event::Tested { - mask: CausalMask::PO, - outcome: TestOutcome::Passed, - }, - Event::Tested { - mask: CausalMask::SPO, - outcome: TestOutcome::Passed, - }, + Event::Tested(INTERVENTION_PASS), + Event::Tested(REMOVAL_PASS), ] } @@ -213,26 +301,15 @@ fn main() { mask: CausalMask::SO, quorum: Quorum::new(3, 1, 1), }, - Event::Tested { - mask: CausalMask::PO, - outcome: TestOutcome::NotRun, - }, - Event::Tested { - mask: CausalMask::PO, - outcome: TestOutcome::Passed, - }, + Event::Tested(Trial::NotRun(CausalMask::PO)), + Event::Tested(INTERVENTION_PASS), Event::ConfounderCheck { so_direction: 0b100, po_direction: 0b000, }, - Event::Tested { - mask: CausalMask::PO, - outcome: TestOutcome::Passed, - }, - Event::Tested { - mask: CausalMask::SPO, - outcome: TestOutcome::Failed, - }, + Event::Tested(INTERVENTION_PASS), + Event::Tested(REMOVAL_PASS), + Event::Tested(REMOVAL_FAIL), Event::Independent { quorum: Quorum::new(1, 0, 3), }, @@ -283,8 +360,23 @@ mod tests { fn obs(mask: CausalMask, q: Quorum) -> Event { Event::Observed { mask, quorum: q } } + /// A trial whose execution gives `(mask, outcome)`. Only `PO` and `SPO` + /// trials can pass or fail; any other projection can only be named, so it + /// maps to `NotRun`. fn test(mask: CausalMask, outcome: TestOutcome) -> Event { - Event::Tested { mask, outcome } + let trial = match (mask, outcome) { + (CausalMask::PO, TestOutcome::Passed) => INTERVENTION_PASS, + (CausalMask::PO, TestOutcome::Failed) => Trial::Intervention(InterventionTrial { + treated_hits: 10, + treated_n: 40, + control_hits: 12, + control_n: 40, + }), + (CausalMask::SPO, TestOutcome::Passed) => REMOVAL_PASS, + (CausalMask::SPO, TestOutcome::Failed) => REMOVAL_FAIL, + (m, _) => Trial::NotRun(m), + }; + Event::Tested(trial) } /// Every event the tests sweep: all masks × a few quorums for @@ -401,6 +493,89 @@ mod tests { } } + /// FAILS IF: a trial passes when its data do not support it, or the band + /// follows anything but the executed trial. There is no `Passed` input: + /// the outcome is derived from the data every time the event is applied. + #[test] + fn a_pass_is_derived_from_the_trial_data() { + let iv = |th, tn, ch, cn| { + Trial::Intervention(InterventionTrial { + treated_hits: th, + treated_n: tn, + control_hits: ch, + control_n: cn, + }) + }; + // Intervention: higher treated rate passes; equal or lower fails; + // an unmeasured arm means the trial was not run. + assert_eq!( + run(iv(30, 40, 12, 40)), + (CausalMask::PO, TestOutcome::Passed) + ); + assert_eq!( + run(iv(12, 40, 12, 40)), + (CausalMask::PO, TestOutcome::Failed) + ); + assert_eq!(run(iv(3, 4, 30, 40)), (CausalMask::PO, TestOutcome::Failed)); + assert_eq!(run(iv(3, 4, 0, 0)), (CausalMask::PO, TestOutcome::NotRun)); + // Rates, not counts: 3/4 beats 60/100 although 3 < 60. + assert_eq!( + run(iv(3, 4, 60, 100)), + (CausalMask::PO, TestOutcome::Passed) + ); + + // Removal: a required premise passes; a dispensable one fails; a + // conclusion that never derived fails; a candidate outside the + // premises gives nothing to attack. + let rm = |premises, rule, candidate| { + Trial::Counterfactual(RemovalTrial { + premises, + rule, + candidate, + }) + }; + assert_eq!( + run(rm(0b0111, 0b0011, 0b0010)), + (CausalMask::SPO, TestOutcome::Passed) + ); + assert_eq!( + run(rm(0b0111, 0b0011, 0b0100)), + (CausalMask::SPO, TestOutcome::Failed) + ); + assert_eq!( + run(rm(0b0001, 0b0011, 0b0001)), + (CausalMask::SPO, TestOutcome::Failed) + ); + assert_eq!( + run(rm(0b0011, 0b0011, 0b1000)), + (CausalMask::SPO, TestOutcome::NotRun) + ); + assert_eq!( + run(rm(0b0011, 0b0011, 0)), + (CausalMask::SPO, TestOutcome::NotRun) + ); + + // The band follows the data: the same event shape with unsupporting + // data does not raise, and replay re-executes rather than remembering. + let decl = declarations(); + let assoc = [earning_path()[0]]; + let start = replay(&decl, CausalEdge64::ZERO, EdgeProvenance::V2Stamped, &assoc).unwrap(); + let good = apply( + &decl, + start, + EdgeProvenance::V2Stamped, + Event::Tested(iv(30, 40, 12, 40)), + ); + let bad = apply( + &decl, + start, + EdgeProvenance::V2Stamped, + Event::Tested(iv(12, 40, 30, 40)), + ); + assert_eq!(good.unwrap().reasoning_band(), Causal); + assert_eq!(bad.unwrap().reasoning_band(), Association); + } + /// FAILS IF: any transition from any band raises it by more than one rung /// of the ladder, or raises it without the matching obligation. Sweeps /// all 8 bands × every event kind. @@ -414,31 +589,23 @@ mod tests { continue; } rises += 1; - let legal = matches!( - (b, n, ev), + let legal = match (b, n, ev) { ( Surface, Association, Event::Observed { mask: CausalMask::SO, .. - } - ) | ( - Association, - Causal, - Event::Tested { - mask: CausalMask::PO, - outcome: TestOutcome::Passed - } - ) | ( - Causal, - Counterfactual, - Event::Tested { - mask: CausalMask::SPO, - outcome: TestOutcome::Passed - } - ) - ); + }, + ) => true, + (Association, Causal, Event::Tested(t)) => { + run(t) == (CausalMask::PO, TestOutcome::Passed) + } + (Causal, Counterfactual, Event::Tested(t)) => { + run(t) == (CausalMask::SPO, TestOutcome::Passed) + } + _ => false, + }; assert!(legal, "{b:?} --{ev:?}--> {n:?}"); } }