diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 20a58ef56..588285f1b 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -48,6 +48,7 @@ Plan: `.claude/plans/2026-10-06-global-sudoku-replayable-orchestration-v1.md`. O | **D-EPI-CANON-0** | Canonical joint `EpistemicState5` reading of bits 59..63 in the contract | Shipped (#1381) | `contract::epistemic_state5`: Cartesian `Topology2 × Certification3`, `raw5 = t \| c << 2`, 24 meaningful / 8 reserved; per-factor facts; declarations + `project_state5`; `CausalEdge64::{epistemic_raw5, with_epistemic_raw5}`; non-declared-reading writers deprecated | | **D-EPI-POP-0** | Facts → 32-state population: the missing operator between one edge's coordinate (u5) and a semantic condition over all coordinates (u32) | Shipped (#1381) | `contract::epistemic_state5::{Population, facts_population, EpistemicState5::bit}`: `const fn`, no DSL / query object / planner. Tests pin exact sets (`CAUSES` = {20..23}, `IND_UNKNOWN \| RELATED` = {10,14,18,22}, `IND_KNOWN \| SUPPORTS` = {13,17,21}, `RELATED` = 8..23), operator == state enumeration over all 2048 requirements with no reserved code, conjunction == intersection; 2 disable runs red (any-of semantics; reserved read as fact-less) | | **D-EPI-LEGACY-DEPROJECT-0** | Deproject the remaining `ReasoningBand` / CE64-`TrustTexture` consumers: each goes to an exact coordinate, a `facts_population(..)`, MUL, or another subsystem (e.g. counterfactual context) — never a 1:1 replacement enum | Shipped (#1381) | `entries/2026-10-07-epi-legacy-deprojection.md`: 6 ReasoningBand caller sites (1 certification question, 1 non-interference witness → both exact coordinates; 2 context/obsolete → compat; 2 refusal witnesses), 0 production CE64 `TrustTexture` consumers, 4 `TrustTexture` types with no bridge; `EdgeRole::band` removed; hinge + V3 lift migrated; 4 code loops → `facts_population` | +| **D-PEARL-IO-0** | Pearl's ladder executed in and out of `CausalEdge64`: bits 40..42 select the operator, the measurement revises bits 59..63 | In PR | `cognitive-shader-driver/examples/pearl_ladder_probe.rs`, `entries/2026-10-07-pearl-io-0-ladder-in-and-out-of-ce64.md`: SO → P7a observational folds, PO → P7a executed arms (only path to `Causes`), SPO → `counterfactual_replay` (reaction, never a certification), SP → SO-vs-PO direction; `hydrate` gates `IndirectUnknown → IndirectKnown` on both bindings. CUI BONO script: `IndirectUnknown × Related` → `IndirectKnown × Related` → `IndirectKnown × Causes`. 14 tests, 12 disable runs red. P7a model moved to `examples/shared/certification_model.rs` unchanged | | **D-PUZZLE-0** | Sudoku / crossword / chess on the same population algebra under domain laws | In progress | step 1 Sudoku shipped (#1382). Step 2 crossword (`crossword_population_fold_probe.rs`, `entries/2026-10-07-puzzle-0-crossword-same-algebra.md`): domain-free half moved to `examples/shared/population_fold.rs` (fence test, Sudoku counts unchanged); 1,000,052 crossword edges, counts equal 3 ways; filter 0.30–0.43 ns/edge in both domains. Step 2b real words (`crossword_real_words_probe.rs`, `entries/2026-10-07-puzzle-0-crossword-real-words.md`): English = DeepNSM v1 4096 + academic_20k (committed), German = DeReKo top 20k only from `DEREKO_PATH` (CC BY-NC, nothing committed); ~1.0M edges each, counts equal 3 ways. Steps 2/2b merged (#1384). Step 3 (`crossword_mask_propagation_probe.rs`, `entries/2026-10-07-puzzle-0-crossword-mask-propagation.md`): Morton cell + `(slot:offset)` lanes, DeepNSM-v2 `WordId` masks, `MooreSymbol8` letters; four arms on identical puzzles (EN 5×5: token 3.0 µs ≈ hybrid 2.9, Cartesian 5.1, literal ~1,045); with no repeated words, creation stops at 5×5 with the 18–20k vocabularies; cui-bono ablation of givens. Next: chess in `stockfish-rs` (GPL) — how it reaches the shared half is open | | **D-EPI-MIG-0** | Migrate readers/writers to the canonical reading; #1378 → conformance | Shipped (#1381) | `entries/2026-10-07-epi-mig-0.md`: affordance law (per-factor compile) + P7a (identity packing) + 6 probes + dismech + V3 lift; conformance census 24/0/8 (was 3/5/16); 3 historical-band probes cannot migrate without invention | | **D-EPI-CONFLICT-0** | (superseded by D-EPI-MIG-0: probe converted to conformance) Pin the disagreement between the D-ACR-7-declared P7a band and the affordance law on CE64 bits 59..63 | Superseded | **Current:** the same probe is the conformance test, census 24 agree / 0 disagree / 8 reserved under the Cartesian V1 (D-EPI-MIG-0). **HISTORICAL — the pre-Cartesian pins below no longer describe the code:** `cognitive-shader-driver/examples/epistemic_reading_conflict_probe.rs`: contract-projected `Causes` reads as affordance code 20 `Related` (topology 0) or refuses (1..3), `COUNTERFACTUAL_PROBE` never eligible; `CAUSES` codes 1 and 17 sit on P7a `Open` / `CausalCandidate`; codes 25, 30 on P7a reserved bands; census over 24 cells = 3 agree, 5 disagree, 16 refuse; the affordance law measures where the contract refuses (undeclared / band `Absent`). Both readings run from shared code (`shared/affordance_law.rs`, and P7a's reading moved unchanged into `shared/certification_reading.rs`, used by `relational_certification_probe` too), never copied. Pins of current state, to become agreement tests when the issue closes (`ISS-EPISTEMIC-READINGS-DISAGREE-ON-BITS-61-63`); 5 tests, 6 disable runs red (3 on the affordance codebook, 3 on P7a's: `Contributes` entailment, a reserved code admitted, `Causes` recoded) | diff --git a/.claude/board/entries/2026-10-07-pearl-io-0-ladder-in-and-out-of-ce64.md b/.claude/board/entries/2026-10-07-pearl-io-0-ladder-in-and-out-of-ce64.md new file mode 100644 index 000000000..ec040ca56 --- /dev/null +++ b/.claude/board/entries/2026-10-07-pearl-io-0-ladder-in-and-out-of-ce64.md @@ -0,0 +1,70 @@ +# 2026-10-07 — D-PEARL-IO-0: Pearl's ladder executed in and out of CausalEdge64 + +**Probe:** `cognitive-shader-driver/examples/pearl_ladder_probe.rs` +(`--features with-planner`), over `examples/shared/certification_model.rs` +(the P7a model, moved unchanged) and `lance_graph_planner::dismech_counterfactual`. +**Status:** TEST-PINNED (14 tests; 12 disable runs, one per falsifier, all red). + +## Question + +Can an edge enter with one `EpistemicState5`, run the operator its Pearl +projection selects, and leave with a different state only because the +measurement earned it? + +## Inventory (before code) + +| need | existing | verdict | +|---|---|---| +| Pearl input | `CausalMask`, bits 40..42; `simpsons_paradox_risk` | used; nothing dispatched on it before | +| observational / intervention obligations | P7a `Model` folds (`relational_certification_probe`, #1369) | moved to `examples/shared/certification_model.rs`; `certify` split into `causes` + new `certify_observational` | +| counterfactual | `dismech_counterfactual::counterfactual_replay` (one replay path, −6 tag) | used as is | +| epistemic output | `EpistemicState5`, `Epi5Declarations::project_state5`, `stamp` | used | +| missing | dispatch on bits 40..42; write-back rule; topology gate | added in the probe (`pearl::{reason, revise, hydrate}`) | + +## The seam + +- `reason(edge, evidence, edit) -> Measured` reads bits 40..42 only. SO → P7a + observational folds (at most `CausalCandidate`); PO → P7a `causes` on the + executed arms; SPO → `counterfactual_replay` with one route cut; SP → SO vs + PO direction. `Measured` has private fields: only an operator produces one. +- `revise` raises the certification to what the operator earned when that is + strictly stronger, never lowers it, and writes bits 59..63 only. SPO and SP + earn nothing. +- `hydrate` moves `IndirectUnknown` to `IndirectKnown` only when both bindings + `A → B` and `B → Y` are in the sealed chain; certification never moves there. + +## Measured (CUI BONO script) + +| step | state | +|---|---| +| start, SO | `IndirectUnknown × Related` (SO earned `Related`, no change) | +| propose unbound candidate | unchanged (`ProposedOnly`) | +| hydrate `A → B → Y` | `IndirectKnown × Related` | +| PO, executed arms | `IndirectKnown × Causes` | +| SPO, cut `B → Y` | `LoadBearing` (frequency 185 → 120), state unchanged | +| SPO, cut `A → B` | `TruthOnly` (185 → 225), state unchanged | + +- The replay's verdict is a NARS revision over the steps' frequencies, so a cut + can raise the terminal frequency (cutting the weak step). The reaction keeps + both frequencies. In the sweep that chose the fixture, endpoints did not + change the frequency. +- Y = A or B: removing A where B holds shows no reaction; with B disabled it + does; the randomized arms certify `Causes`. The chain replay is linear and + cannot express the OR, so the contingency is a mask over the unit population. + +## Falsifiers (disable-verified red) + +F1 SO reads the trial (`certify` instead of `certify_observational`); F2 SPO +certifies without evidence; F3 a load-bearing cut earns `Causes`; F4 the +counterfactual terminal edge written back; F5 hydration without bindings; F6 +confounding demotes `Causes`; F7 a null reaction demotes; F8 revision clears +the mantissa; F9 nondeterministic dispatch; F10 intervention receipts without +arms earn `Causes`; F11 bits 59..63 select the operator; F12 the mantissa +selects the operator. + +## Open + +- Production placement of `reason` / `revise` / `hydrate`. +- Whole-node removal of B (only route cuts run; in a two-step chain they are + B's only mechanisms). +- Contingency search beyond one disabled cause; no responsibility framework. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 749f257c2..8f6fc5d57 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,7 +25,7 @@ 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. -254 entries, 2026-08-06 .. 2026-10-07. +255 entries, 2026-08-06 .. 2026-10-07. | date | entry id | finding | file | |---|---|---|---| @@ -35,6 +35,7 @@ exactly one of them, never both. | 2026-10-07 | `D-PUZZLE-0-XWORD` | | [2026-10-07-puzzle-0-crossword-same-algebra.md](2026-10-07-puzzle-0-crossword-same-algebra.md) | | 2026-10-07 | `D-PUZZLE-0-XWORD-REAL` | | [2026-10-07-puzzle-0-crossword-real-words.md](2026-10-07-puzzle-0-crossword-real-words.md) | | 2026-10-07 | `D-PUZZLE-0-XWORD-MASK` | | [2026-10-07-puzzle-0-crossword-mask-propagation.md](2026-10-07-puzzle-0-crossword-mask-propagation.md) | +| 2026-10-07 | `D-PEARL-IO-0` | | [2026-10-07-pearl-io-0-ladder-in-and-out-of-ce64.md](2026-10-07-pearl-io-0-ladder-in-and-out-of-ce64.md) | | 2026-10-07 | `gated-gather-bit-schedule` | Production gather is 3.8× slow from a per-row branch; bit-gated visitation wins or ties at every density; ordinal vectors never win (R1 holds) | [2026-10-07-gated-gather-bit-schedule.md](2026-10-07-gated-gather-bit-schedule.md) | | 2026-10-07 | `D-EPI-MIG-INVENTORY` | | [2026-10-07-epi-mig-inventory.md](2026-10-07-epi-mig-inventory.md) | | 2026-10-07 | `D-EPI-CANON-0` | | [2026-10-07-epi-mig-0.md](2026-10-07-epi-mig-0.md) | diff --git a/README.md b/README.md index c9676f9b1..6ed84c5ea 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Rust Tests](https://github.com/AdaWorldAPI/lance-graph/actions/workflows/rust-test.yml/badge.svg)](https://github.com/AdaWorldAPI/lance-graph/actions/workflows/rust-test.yml) [![Style Check](https://github.com/AdaWorldAPI/lance-graph/actions/workflows/style.yml/badge.svg)](https://github.com/AdaWorldAPI/lance-graph/actions/workflows/style.yml) [![Build](https://github.com/AdaWorldAPI/lance-graph/actions/workflows/build.yml/badge.svg)](https://github.com/AdaWorldAPI/lance-graph/actions/workflows/build.yml) +[![CausalEdge64 semantic upper half: Pearl ladder, orientation, activation, entropy, belief-state update, epistemic state](docs/architecture/img/ce64-semantic-upper-half.png)](#the-causal-edge-register) + A Rust workspace for graph and columnar query execution over Arrow and Lance. It has: - a Cypher and SQL engine on DataFusion; @@ -62,6 +64,18 @@ What this means: and parses Gremlin and SPARQL into its own `Arena`. It does not execute queries. +## The causal edge register + +`CausalEdge64` (`crates/causal-edge`) packs one relation into 64 bits: the +S, P, O palette indices, frequency and confidence in bits 0..39, and the +reasoning fields in bits 40..63. + +The diagram at the top of this page is a working model of bits 40..63, not +the layout's definition. Bits 0..42 and 59..63 match the code. The code reads +bits 43..52 differently (direction triad, inference mantissa, plasticity), and +bits 53..58 hold one witness handle. The band-by-band comparison is in +[`docs/architecture/ce64-semantic-upper-half.md`](docs/architecture/ce64-semantic-upper-half.md). + ## Workspace 59 crate directories under `crates/`: 22 are workspace members and 37 are diff --git a/crates/cognitive-shader-driver/Cargo.toml b/crates/cognitive-shader-driver/Cargo.toml index e9bcc55dd..f5894478a 100644 --- a/crates/cognitive-shader-driver/Cargo.toml +++ b/crates/cognitive-shader-driver/Cargo.toml @@ -106,6 +106,14 @@ test = true name = "relational_certification_probe" test = true +# D-PEARL-IO-0: Pearl's ladder executed in and out of CausalEdge64 — bits +# 40..42 select the operator (P7a folds, P7a trial, dismech counterfactual +# replay, SO-vs-PO confounder check); the measured result revises bits 59..63. +[[example]] +name = "pearl_ladder_probe" +test = true +required-features = ["with-planner"] + # D-GSO-AFF-0 affordance-measurement probe; tests run under `cargo test`. [[example]] name = "affordance_measurement_probe" diff --git a/crates/cognitive-shader-driver/examples/README.md b/crates/cognitive-shader-driver/examples/README.md index 43d961211..0fba453ae 100644 --- a/crates/cognitive-shader-driver/examples/README.md +++ b/crates/cognitive-shader-driver/examples/README.md @@ -458,3 +458,17 @@ claim lane the shared three ways. German needs `DEREKO_PATH`. Board: cargo run --release -p cognitive-shader-driver --example crossword_mask_propagation_probe ``` +## pearl_ladder_probe.rs + +D-PEARL-IO-0. Pearl's ladder executed in and out of `CausalEdge64`. Bits +40..42 select an existing operator: SO runs the P7a observational folds, PO +the P7a executed randomized arms, SPO `dismech_counterfactual::counterfactual_replay` +with one route cut, SP the SO-vs-PO direction check. `revise` raises bits +59..63 only to what the operator earned; `hydrate` moves `IndirectUnknown` +to `IndirectKnown` only when both bindings of the path are sealed. + +```sh +cargo run -p cognitive-shader-driver --features with-planner --example pearl_ladder_probe +cargo test -p cognitive-shader-driver --features with-planner --example pearl_ladder_probe +``` + diff --git a/crates/cognitive-shader-driver/examples/pearl_ladder_probe.rs b/crates/cognitive-shader-driver/examples/pearl_ladder_probe.rs new file mode 100644 index 000000000..135f76f59 --- /dev/null +++ b/crates/cognitive-shader-driver/examples/pearl_ladder_probe.rs @@ -0,0 +1,1045 @@ +//! D-PEARL-IO-0: Pearl's ladder executed in and out of `CausalEdge64`. +//! +//! An edge enters with one `EpistemicState5`, the Pearl projection in bits +//! 40..42 selects which existing operator runs, the operator measures, and the +//! measurement — not the projection, not the caller — decides whether bits +//! 59..63 change. +//! +//! ```text +//! CausalEdge64 ──bits 40..42──► operator ──► Measured ──revise──► bits 59..63 +//! │ +//! SO ► P7a observational folds (certify_observational) +//! PO ► P7a executed randomized arms (Model::causes) +//! SPO ► dismech counterfactual replay (counterfactual_replay) +//! SP ► SO direction vs PO direction (CausalMask::simpsons_paradox_risk) +//! ``` +//! +//! # Nothing new underneath +//! +//! Every operator is existing code: the P7a sealed model and its folds +//! (`shared/certification_model.rs`, moved unchanged from +//! `relational_certification_probe`), `lance_graph_planner::dismech_counterfactual` +//! (one replay path for both arms), `CausalMask::simpsons_paradox_risk`, and +//! the canonical `EpistemicState5` declarations. What this probe adds is the +//! dispatch (`pearl::reason`), the write-back rule (`pearl::revise`) and the +//! topology gate (`pearl::hydrate`). +//! +//! # The write-back rule +//! +//! - **Certification only rises, and only to what the operator computed.** +//! SO can earn up to `CausalCandidate` (its folds do not include `causes`). +//! PO can earn `Causes`, from the executed arms. SPO and SP earn nothing: a +//! counterfactual reaction and a confounding diagnostic are evidence beside +//! the certification, not certifications. +//! - **Topology changes only through `hydrate`**, from `IndirectUnknown` to +//! `IndirectKnown`, when both bindings `A → B` and `B → Y` exist in the +//! sealed chain. Proposing a candidate B is not evidence. +//! - **Only bits 59..63 move.** The projection, mantissa, frequency and +//! confidence of the edge are inputs, never outputs. +//! +//! # What the measurements showed +//! +//! - The counterfactual replay's verdict is a NARS revision over the chain's +//! step frequencies, so a cut can move the truth in either direction: +//! cutting a weak step can RAISE the terminal frequency. The reaction is +//! therefore reported with both frequencies and its sign, never as "flipped" +//! alone. In the sweep that chose the fixture, the frequency did not depend +//! on the steps' endpoints. +//! - One-at-a-time removal is not the causal test. Under Y = A or B, removing +//! A where B holds changes nothing, removing A under the contingency "B +//! disabled" changes Y, and the randomized arms certify `Causes`. The +//! dismech chain replay is linear and cannot express the OR; the contingency +//! is a mask over the same unit population the P7a folds read. +//! +//! # Not decided here +//! +//! - Production placement of `reason` / `revise`. This is a probe. +//! - Whole-node removal of B: in a two-step chain B's only mechanisms are the +//! two routes, so route cuts are run and node removal is not. +//! - A responsibility framework beyond the single contingency. +//! +//! Run: `cargo run -p cognitive-shader-driver --features with-planner --example pearl_ladder_probe` +//! Tests: `cargo test -p cognitive-shader-driver --features with-planner --example pearl_ladder_probe` + +use causal_edge::edge::{CausalEdge64, InferenceType}; +use causal_edge::pearl::CausalMask; +use causal_edge::plasticity::PlasticityState; +use causal_edge::tables::NarsTables; +use lance_graph_contract::causal_audit::SupportBasis; +use lance_graph_contract::epistemic_state5::{Certification3, Topology2}; +use lance_graph_planner::dismech_counterfactual::{CutContext, DEFAULT_FREQUENCY_BAR}; +use lance_graph_planner::dismech_replay::{ChainStep, ComposeTables}; + +#[path = "shared/certification_model.rs"] +mod certification_model; +#[path = "shared/certification_reading.rs"] +mod certification_reading; + +use certification_model::*; +use certification_reading::*; + +// ── The operator layer ──────────────────────────────────────────────────── + +/// Dispatch, measurement and write-back. A separate module so that a +/// [`pearl::Measured`] can only be produced by running an operator: its +/// fields are private, so no caller outside this module can assert that an +/// intervention or a counterfactual passed. +mod pearl { + use super::certification_model::{compare, Model, NotGrounded}; + use super::certification_reading::{declarations, stamp, Contract, Refusal, CERT_CLASS, RAIL}; + use causal_edge::edge::CausalEdge64; + use causal_edge::pearl::CausalMask; + use lance_graph_contract::band_reading::EdgeProvenance; + use lance_graph_contract::epistemic_state5::{Certification3, Epi5Gen, Topology2}; + use lance_graph_planner::dismech_counterfactual::{counterfactual_replay, CutContext, Verdict}; + use lance_graph_planner::dismech_replay::ChainStep; + + /// The operator bits 40..42 select. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Operation { + /// `SO`: observational folds. + Association, + /// `PO`: executed randomized arms. + Intervention, + /// `SPO`: counterfactual replay with one route cut. + Counterfactual, + /// `SP`: observational direction against interventional direction. + ConfounderCheck, + /// A projection with no operator here (`None`, `O`, `P`, `S`). + NoOperator(CausalMask), + } + + impl Operation { + pub fn of(mask: CausalMask) -> Self { + match mask { + CausalMask::SO => Operation::Association, + CausalMask::PO => Operation::Intervention, + CausalMask::SPO => Operation::Counterfactual, + CausalMask::SP => Operation::ConfounderCheck, + other => Operation::NoOperator(other), + } + } + } + + /// Why an operator could not decide. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Ungrounded { + /// A P7a fold lacked what it needs. + Model(NotGrounded), + /// A counterfactual was asked for without a sealed chain. + NoChain, + /// A counterfactual was asked for without an edit. + NoEdit, + /// The edit names no step of the chain. + CutOutOfRange(usize), + /// The replay refused (sequence reservation). + Replay, + /// One direction of the confounder check is a tie. + NoDirection, + /// The projection selects no operator. + NoOperator, + } + + /// What one route cut did to the chain's answer, with both frequencies. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Reaction { + /// The verdict changed. + LoadBearing { factual: u8, counterfactual: u8 }, + /// The truth moved, the verdict did not. + TruthOnly { factual: u8, counterfactual: u8 }, + /// Nothing moved. + Inert { frequency: u8 }, + } + + /// A counterfactual edit. The base generation is never modified; the + /// operator replays the same chain with the edit applied. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Edit { + None, + /// Mask one route (one step) of the chain. + CutStep(usize), + } + + /// The sealed evidence an operator may read. Read-only. + pub struct Evidence<'a> { + pub model: &'a Model, + pub chain: Option>, + } + + /// A sealed chain for the counterfactual replay. + #[derive(Clone, Copy)] + pub struct Chain<'a> { + pub steps: &'a [ChainStep], + pub seed: CausalEdge64, + pub cx: CutContext<'a>, + } + + /// The result of running one operator. Only [`reason`] constructs it. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Measured { + operation: Operation, + earned: Option, + ungrounded: Option, + reaction: Option, + confounded: Option, + /// The counterfactual arm's terminal edge, kept as witness. Never + /// written back. + counterfactual_terminal: Option, + } + + impl Measured { + pub fn operation(&self) -> Operation { + self.operation + } + pub fn earned(&self) -> Option { + self.earned + } + pub fn ungrounded(&self) -> Option { + self.ungrounded + } + pub fn reaction(&self) -> Option { + self.reaction + } + pub fn confounded(&self) -> Option { + self.confounded + } + pub fn counterfactual_terminal(&self) -> Option { + self.counterfactual_terminal + } + + fn new(operation: Operation) -> Self { + Measured { + operation, + earned: None, + ungrounded: None, + reaction: None, + confounded: None, + counterfactual_terminal: None, + } + } + } + + /// Run the operator the edge's projection selects. Reads bits 40..42 and + /// nothing else of the edge: the epistemic state, mantissa and truth are + /// not inputs to the choice. + pub fn reason(edge: CausalEdge64, ev: &Evidence<'_>, edit: Edit) -> Measured { + let op = Operation::of(edge.causal_mask()); + let mut out = Measured::new(op); + let m = ev.model; + match op { + Operation::Association => match m.sourced() { + Err(e) => out.ungrounded = Some(Ungrounded::Model(e)), + Ok(()) => { + let c = m.certify_observational(); + if c != Contract::Open { + out.earned = Some(c.certification()); + } + } + }, + Operation::Intervention => match m.causes() { + Ok(true) => out.earned = Some(Certification3::Causes), + Ok(false) => {} + Err(e) => out.ungrounded = Some(Ungrounded::Model(e)), + }, + Operation::Counterfactual => { + let Some(chain) = ev.chain else { + out.ungrounded = Some(Ungrounded::NoChain); + return out; + }; + let Edit::CutStep(i) = edit else { + out.ungrounded = Some(Ungrounded::NoEdit); + return out; + }; + match counterfactual_replay(chain.steps, i, chain.seed, chain.cx) { + None => out.ungrounded = Some(Ungrounded::CutOutOfRange(i)), + Some(Err(_)) => out.ungrounded = Some(Ungrounded::Replay), + Some(Ok(cf)) => { + let f = cf.factual.last().map_or(0, |r| r.edge.frequency_u8()); + let c = cf + .counterfactual + .last() + .map_or(0, |r| r.edge.frequency_u8()); + out.reaction = Some(if cf.role.factual != cf.role.counterfactual { + Reaction::LoadBearing { + factual: f, + counterfactual: c, + } + } else if f != c || cf.factual.len() != cf.counterfactual.len() { + Reaction::TruthOnly { + factual: f, + counterfactual: c, + } + } else { + Reaction::Inert { frequency: f } + }); + out.counterfactual_terminal = cf.counterfactual.last().map(|r| r.edge); + debug_assert!(matches!( + cf.role.factual, + Verdict::Consistent | Verdict::Inconsistent + )); + } + } + } + Operation::ConfounderCheck => { + let obs = direction(m.outcome, m.universe & m.exposed, m.universe & !m.exposed); + let trial = direction(m.outcome, m.trial & m.assigned, m.trial & !m.assigned); + match (obs, trial) { + (Ok(Some(so)), Ok(Some(po))) => { + out.confounded = Some(CausalMask::simpsons_paradox_risk(so, po)); + } + (Err(e), _) | (_, Err(e)) => out.ungrounded = Some(Ungrounded::Model(e)), + _ => out.ungrounded = Some(Ungrounded::NoDirection), + } + } + Operation::NoOperator(_) => out.ungrounded = Some(Ungrounded::NoOperator), + } + out + } + + /// The direction triad `simpsons_paradox_risk` reads: bit 2 set when the + /// outcome is worse (here: rarer) in the exposed group. `None` on a tie. + fn direction(outcome: u64, a: u64, b: u64) -> Result, NotGrounded> { + if compare(outcome, a, b, true)? { + Ok(Some(0b000)) + } else if compare(outcome, b, a, true)? { + Ok(Some(0b100)) + } else { + Ok(None) + } + } + + /// Write a measurement back. Certification rises to what the operator + /// earned when that is strictly stronger; nothing else moves, and nothing + /// is ever lowered. + pub fn revise(edge: CausalEdge64, m: &Measured) -> Result { + let decl = declarations(); + let state = decl + .project_state5( + CERT_CLASS, + RAIL, + Epi5Gen::V1, + edge.epistemic_raw5(), + EdgeProvenance::V2Stamped, + ) + .map_err(Refusal::Reading)?; + let current = Contract::from_certification(state.certification()); + let next = match m.earned.map(Contract::from_certification) { + Some(c) if c != current && c.entails(current) => c, + _ => current, + }; + Ok(stamp(edge, next, state.topology())) + } + + /// What a candidate intermediate B turned out to be. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Hydration { + /// No binding of B to the path exists in the sealed chain. + ProposedOnly, + /// One of `A → B`, `B → Y` exists. + Partial, + /// Both bindings exist. + Hydrated, + } + + /// The topology gate. `IndirectUnknown` becomes `IndirectKnown` only when + /// both bindings `a → b` and `b → y` are present in the sealed chain. + /// Certification never moves here. + pub fn hydrate( + edge: CausalEdge64, + (a, b, y): (u8, u8, u8), + sealed: &[ChainStep], + ) -> Result<(CausalEdge64, Hydration), Refusal> { + let has = |s: u8, o: u8| sealed.iter().any(|(_, e)| e.s_idx() == s && e.o_idx() == o); + let found = match (has(a, b), has(b, y)) { + (true, true) => Hydration::Hydrated, + (false, false) => Hydration::ProposedOnly, + _ => Hydration::Partial, + }; + let state = declarations() + .project_state5( + CERT_CLASS, + RAIL, + Epi5Gen::V1, + edge.epistemic_raw5(), + EdgeProvenance::V2Stamped, + ) + .map_err(Refusal::Reading)?; + let topology = + if found == Hydration::Hydrated && state.topology() == Topology2::IndirectUnknown { + Topology2::IndirectKnown + } else { + state.topology() + }; + let contract = Contract::from_certification(state.certification()); + Ok((stamp(edge, contract, topology), found)) + } +} + +use pearl::*; + +// ── Fixtures ────────────────────────────────────────────────────────────── + +const A: u8 = 10; +const B: u8 = 20; +const Y: u8 = 30; +const PREDICATE: u8 = 0x91; + +fn edge(s: u8, o: u8, freq: u8, conf: u8, mask: CausalMask) -> CausalEdge64 { + CausalEdge64::pack( + s, + PREDICATE, + o, + freq, + conf, + mask, + 0b101, + InferenceType::Deduction, + PlasticityState::S_HOT, + 0, + ) +} + +/// The queried relation `A → Y` under projection `mask`, at a given state. +fn query(mask: CausalMask, contract: Contract, topology: Topology2) -> CausalEdge64 { + stamp(edge(A, Y, 200, 200, mask), contract, topology) +} + +fn with_mask(mut e: CausalEdge64, mask: CausalMask) -> CausalEdge64 { + e.set_causal_mask(mask); + e +} + +/// A → Y associated in the pooled population and after the robustness mask, +/// but lowered in stratum 1: observationally `Related`, not `Contributes`. +fn related_population() -> Builder { + let mut b = Builder::new(); + b.cell(0, true, 5, 4); + b.cell(0, false, 5, 1); + b.cell(1, true, 3, 1); + b.cell(1, false, 3, 2); + b.sources(SupportBasis::DirectlyObserved, &[1, 2]); + b +} + +/// The mediated chain `A → B → Y`. Measured with `counterfactual_replay`: +/// the weak first step and strong second step put the factual terminal +/// frequency above the bar; cutting `B → Y` drops below it, cutting `A → B` +/// raises it. +fn chain_steps() -> Vec { + vec![ + (PREDICATE, edge(A, B, 40, 220, CausalMask::SPO)), + (PREDICATE, edge(B, Y, 250, 220, CausalMask::SPO)), + ] +} + +fn compose_tables() -> [Box<[u8; 256 * 256]>; 3] { + let mut x: u64 = 0x9E37_79B9_7F4A_7C15; + core::array::from_fn(|_| { + let mut t = Box::new([0u8; 256 * 256]); + for v in t.iter_mut() { + x = x.wrapping_mul(6364136223846793005).wrapping_add(1); + *v = ((x >> 33) % 256) as u8; + } + t + }) +} + +/// Everything the replay needs, owned once. +struct Replay { + tables: NarsTables, + compose: [Box<[u8; 256 * 256]>; 3], + steps: Vec, +} + +impl Replay { + fn new() -> Self { + Replay { + tables: NarsTables::build(1), + compose: compose_tables(), + steps: chain_steps(), + } + } + + fn chain(&self) -> Chain<'_> { + Chain { + steps: &self.steps, + seed: edge(A, Y, 200, 200, CausalMask::SPO), + cx: CutContext { + tables: &self.tables, + compose: ComposeTables { + s: &self.compose[0], + p: &self.compose[1], + o: &self.compose[2], + }, + owner: 3, + base_seq: 100, + bar: DEFAULT_FREQUENCY_BAR, + }, + } + } +} + +/// Y = A or B, per unit, as unit masks. The counterfactual edit is a mask; +/// the base masks are never modified. +struct OrGate { + a: u64, + b: u64, +} + +impl OrGate { + fn y(a: u64, b: u64) -> u64 { + a | b + } + + /// Does removing A at `unit` change Y there, with B as it is? + fn but_for(&self, unit: u64) -> bool { + (Self::y(self.a, self.b) ^ Self::y(self.a & !unit, self.b)) & unit != 0 + } + + /// The same removal under the contingency `disabled` (B masked off). + fn but_for_given(&self, unit: u64, disabled: u64) -> bool { + let b = self.b & !disabled; + (Self::y(self.a, b) ^ Self::y(self.a & !unit, b)) & unit != 0 + } +} + +/// The randomized trial behind [`OrGate`]: A assigned in arms with and +/// without B, two intervention sources. +fn overdetermined_trial() -> (Model, OrGate) { + let mut b = Builder::new(); + let (a_and_b, _) = b.arm(true, 3, 3); + let (a_only, _) = b.arm(true, 3, 3); + let (b_only, _) = b.arm(false, 3, 3); + b.arm(false, 3, 0); + b.sources(SupportBasis::InterventionBacked, &[10, 11]); + ( + b.build(), + OrGate { + a: a_and_b | a_only, + b: a_and_b | b_only, + }, + ) +} + +fn certification(e: CausalEdge64) -> Certification3 { + let c = read( + &declarations(), + CERT_CLASS, + e, + lance_graph_contract::band_reading::EdgeProvenance::V2Stamped, + ) + .expect("declared class, valid code"); + c.certification() +} + +fn topology(e: CausalEdge64) -> Topology2 { + Topology2::from_ordinal(e.epistemic_raw5() & 0b11).expect("2-bit ordinal") +} + +// ── The script ──────────────────────────────────────────────────────────── + +fn main() { + let rp = Replay::new(); + let mut with_trial = related_population(); + add_positive_trial(&mut with_trial); + let model = with_trial.build(); + let ev = Evidence { + model: &model, + chain: Some(rp.chain()), + }; + let show = |label: &str, e: CausalEdge64| { + println!( + " {label:<46} {:?} x {:?} (bits 40..42 {:?}, raw5 {})", + topology(e), + certification(e), + e.causal_mask(), + e.epistemic_raw5() + ); + }; + println!("D-PEARL-IO-0: A -> Y through a candidate B (CUI BONO)"); + + let start = query( + CausalMask::SO, + Contract::Related, + Topology2::IndirectUnknown, + ); + show("START", start); + + let m = reason(start, &ev, Edit::None); + let e = revise(start, &m).unwrap(); + println!(" {:?} measured: earned {:?}", m.operation(), m.earned()); + show("after OBSERVATION", e); + + let (same, h) = hydrate(e, (A, 99, Y), &rp.steps).unwrap(); + println!(" candidate 99 proposed: {h:?}"); + show("after PROPOSING an unbound candidate", same); + + let (e, h) = hydrate(e, (A, B, Y), &rp.steps).unwrap(); + println!(" candidate B: {h:?}"); + show("after HYDRATING A -> B -> Y", e); + + let e = with_mask(e, CausalMask::PO); + let m = reason(e, &ev, Edit::None); + let e = revise(e, &m).unwrap(); + println!( + " PO measured: earned {:?}, ungrounded {:?}", + m.earned(), + m.ungrounded() + ); + show("after INTERVENTION", e); + + let e = with_mask(e, CausalMask::SPO); + for (cut, route) in [(1usize, "B -> Y"), (0, "A -> B")] { + let m = reason(e, &ev, Edit::CutStep(cut)); + let after = revise(e, &m).unwrap(); + println!( + " SPO mask {route}: reaction {:?}, earned {:?}, cf terminal mantissa {:?}", + m.reaction(), + m.earned(), + m.counterfactual_terminal().map(|t| t.inference_mantissa()) + ); + show("after COUNTERFACTUAL", after); + } + + let e = with_mask(e, CausalMask::SP); + let m = reason(e, &ev, Edit::None); + println!(" SP measured: confounded {:?}", m.confounded()); + + println!("redundancy, Y = A or B:"); + let (trial, gate) = overdetermined_trial(); + let unit = (gate.a & gate.b).isolate_lowest_one(); + println!( + " remove A at a unit with B: reaction {}; with B disabled: reaction {}", + gate.but_for(unit), + gate.but_for_given(unit, gate.b) + ); + let q = query(CausalMask::PO, Contract::Open, Topology2::Direct); + let m = reason( + q, + &Evidence { + model: &trial, + chain: None, + }, + Edit::None, + ); + show(" randomized arms, PO", revise(q, &m).unwrap()); +} + +#[cfg(test)] +mod tests { + use super::*; + use causal_edge::layout::EPISTEMIC_MASK; + + fn evidence<'a>(model: &'a Model, rp: &'a Replay) -> Evidence<'a> { + Evidence { + model, + chain: Some(rp.chain()), + } + } + + /// Run one operator and write it back. + fn step( + e: CausalEdge64, + mask: CausalMask, + ev: &Evidence<'_>, + edit: Edit, + ) -> (CausalEdge64, Measured) { + let e = with_mask(e, mask); + let m = reason(e, ev, edit); + (revise(e, &m).expect("declared"), m) + } + + /// The whole script, pinned: the edge leaves with a different state only + /// where an operator earned it. + #[test] + fn cui_bono_script_moves_the_state_only_where_evidence_earned_it() { + let rp = Replay::new(); + let mut b = related_population(); + add_positive_trial(&mut b); + let model = b.build(); + let ev = evidence(&model, &rp); + let start = query( + CausalMask::SO, + Contract::Related, + Topology2::IndirectUnknown, + ); + + let (e, m) = step(start, CausalMask::SO, &ev, Edit::None); + assert_eq!(m.earned(), Some(Certification3::Related)); + assert_eq!( + (topology(e), certification(e)), + (Topology2::IndirectUnknown, Certification3::Related) + ); + + let (e, h) = hydrate(e, (A, B, Y), &rp.steps).unwrap(); + assert_eq!(h, Hydration::Hydrated); + assert_eq!( + (topology(e), certification(e)), + (Topology2::IndirectKnown, Certification3::Related) + ); + + let (e, m) = step(e, CausalMask::PO, &ev, Edit::None); + assert_eq!(m.earned(), Some(Certification3::Causes)); + assert_eq!( + (topology(e), certification(e)), + (Topology2::IndirectKnown, Certification3::Causes) + ); + + let (after, m) = step(e, CausalMask::SPO, &ev, Edit::CutStep(1)); + assert_eq!( + m.reaction(), + Some(Reaction::LoadBearing { + factual: 185, + counterfactual: 120 + }) + ); + assert_eq!(after.epistemic_raw5(), e.epistemic_raw5()); + let (_, m) = step(e, CausalMask::SPO, &ev, Edit::CutStep(0)); + assert_eq!( + m.reaction(), + Some(Reaction::TruthOnly { + factual: 185, + counterfactual: 225 + }) + ); + } + + /// F1. Observation alone never reaches `Causes`, even when the sealed + /// model also holds a positive trial the SO operator could have read. + #[test] + fn f1_observation_alone_never_promotes_to_causes() { + let mut fixtures = vec![ + related_population(), + robust_association(), + simpson_population(), + ]; + let mut many = robust_association(); + many.sources(SupportBasis::DirectlyObserved, &(3..40).collect::>()); + fixtures.push(many); + let rp = Replay::new(); + for mut b in fixtures { + add_positive_trial(&mut b); + let model = b.build(); + assert_eq!( + model.certify(), + Contract::Causes, + "the trial is there to be ignored" + ); + let ev = evidence(&model, &rp); + let (e, m) = step( + query(CausalMask::SO, Contract::Open, Topology2::Direct), + CausalMask::SO, + &ev, + Edit::None, + ); + assert_ne!(m.earned(), Some(Certification3::Causes)); + assert_ne!(certification(e), Certification3::Causes); + assert!( + m.earned().is_some(), + "anti-vacuity: SO must earn something here" + ); + } + } + + /// F2. Setting `SPO` certifies nothing: with no chain the operator is not + /// grounded and the state is unchanged. + #[test] + fn f2_setting_the_projection_certifies_nothing() { + let model = related_population().build(); + let ev = Evidence { + model: &model, + chain: None, + }; + for c in Contract::ALL { + let q = query(CausalMask::SPO, c, Topology2::IndirectUnknown); + let m = reason(q, &ev, Edit::CutStep(1)); + assert_eq!(m.ungrounded(), Some(Ungrounded::NoChain)); + assert_eq!(revise(q, &m).unwrap(), q); + } + } + + /// F3. A load-bearing counterfactual reaction does not become `Causes`. + #[test] + fn f3_a_load_bearing_cut_is_not_causes() { + let rp = Replay::new(); + let model = related_population().build(); + let ev = evidence(&model, &rp); + for c in [Contract::Open, Contract::Related, Contract::CausalCandidate] { + let q = query(CausalMask::SPO, c, Topology2::IndirectKnown); + let m = reason(q, &ev, Edit::CutStep(1)); + assert!(matches!(m.reaction(), Some(Reaction::LoadBearing { .. }))); + assert_eq!(m.earned(), None); + assert_eq!(certification(revise(q, &m).unwrap()), c.certification()); + } + } + + /// F4. The counterfactual arm is tagged −6 and is never written back as + /// observed truth: the revised edge keeps its own frequency, confidence + /// and mantissa. + #[test] + fn f4_the_counterfactual_branch_is_not_written_back() { + let rp = Replay::new(); + let model = related_population().build(); + let ev = evidence(&model, &rp); + let q = query(CausalMask::SPO, Contract::Related, Topology2::IndirectKnown); + let m = reason(q, &ev, Edit::CutStep(1)); + let t = m.counterfactual_terminal().expect("replayed"); + assert_eq!( + t.inference_mantissa(), + InferenceType::Counterfactual.to_mantissa() + ); + assert_ne!( + t.frequency_u8(), + q.frequency_u8(), + "anti-vacuity: the arm differs" + ); + let out = revise(q, &m).unwrap(); + assert_eq!(out.frequency_u8(), q.frequency_u8()); + assert_eq!(out.confidence_u8(), q.confidence_u8()); + assert_eq!(out.inference_mantissa(), q.inference_mantissa()); + } + + /// F5. Proposing B changes nothing; half a path changes nothing; only both + /// bindings move the topology, and never the certification. + #[test] + fn f5_a_proposed_beneficiary_is_not_evidence() { + let rp = Replay::new(); + let q = query( + CausalMask::SO, + Contract::Related, + Topology2::IndirectUnknown, + ); + let (e, h) = hydrate(q, (A, 99, Y), &rp.steps).unwrap(); + assert_eq!((e, h), (q, Hydration::ProposedOnly)); + let half = &rp.steps[..1]; + let (e, h) = hydrate(q, (A, B, Y), half).unwrap(); + assert_eq!((e, h), (q, Hydration::Partial)); + let (e, h) = hydrate(q, (A, B, Y), &rp.steps).unwrap(); + assert_eq!(h, Hydration::Hydrated); + assert_eq!(topology(e), Topology2::IndirectKnown); + assert_eq!(certification(e), Certification3::Related); + } + + /// F6. Simpson: SO and PO disagree in direction, SP reports it, and the + /// diagnostic does not demote an intervention-certified `Causes`. + #[test] + fn f6_simpson_does_not_demote_intervention_causes() { + let rp = Replay::new(); + let mut b = Builder::new(); + // Pooled: exposed rarer outcome (4/10 vs 7/10); trial: exposed better. + b.cell(0, true, 8, 2); + b.cell(0, false, 2, 0); + b.cell(1, true, 2, 2); + b.cell(1, false, 8, 7); + b.sources(SupportBasis::DirectlyObserved, &[1, 2]); + add_positive_trial(&mut b); + let model = b.build(); + let ev = evidence(&model, &rp); + let q = query(CausalMask::PO, Contract::Open, Topology2::Direct); + let (e, _) = step(q, CausalMask::PO, &ev, Edit::None); + assert_eq!(certification(e), Certification3::Causes); + let (after, m) = step(e, CausalMask::SP, &ev, Edit::None); + assert_eq!(m.confounded(), Some(true)); + assert_eq!(certification(after), Certification3::Causes); + // And SO on the same population does not lower it either. + let (after, _) = step(after, CausalMask::SO, &ev, Edit::None); + assert_eq!(certification(after), Certification3::Causes); + } + + /// F6 silence twin: same-direction populations are not reported + /// confounded. + #[test] + fn f6_agreeing_directions_are_not_confounded() { + let rp = Replay::new(); + let mut b = robust_association(); + add_positive_trial(&mut b); + let model = b.build(); + let (_, m) = step( + query(CausalMask::SP, Contract::Open, Topology2::Direct), + CausalMask::SP, + &evidence(&model, &rp), + Edit::None, + ); + assert_eq!(m.confounded(), Some(false)); + } + + /// F7. Y = A or B: removing A where B holds shows no reaction, the + /// contingency "B disabled" shows one, and the trial certifies `Causes`. + /// A null removal never demotes. + #[test] + fn f7_one_at_a_time_removal_is_not_the_definition_of_causality() { + let (trial, gate) = overdetermined_trial(); + let unit = (gate.a & gate.b).isolate_lowest_one(); + assert_ne!(unit, 0); + assert!(!gate.but_for(unit), "B keeps Y: no reaction"); + assert!( + gate.but_for_given(unit, gate.b), + "with B disabled, removing A removes Y" + ); + let a_only = (gate.a & !gate.b).isolate_lowest_one(); + assert!( + gate.but_for(a_only), + "anti-vacuity: removal can react at all" + ); + + let rp = Replay::new(); + let ev = Evidence { + model: &trial, + chain: Some(rp.chain()), + }; + let (e, _) = step( + query(CausalMask::PO, Contract::Open, Topology2::Direct), + CausalMask::PO, + &ev, + Edit::None, + ); + assert_eq!(certification(e), Certification3::Causes); + // A counterfactual with no verdict change on this edge must not lower it. + let (after, m) = step(e, CausalMask::SPO, &ev, Edit::CutStep(0)); + assert!(matches!(m.reaction(), Some(Reaction::TruthOnly { .. }))); + assert_eq!(certification(after), Certification3::Causes); + } + + /// F8. Every operator writes bits 59..63 only. + #[test] + fn f8_only_bits_59_to_63_move() { + let rp = Replay::new(); + let mut b = related_population(); + add_positive_trial(&mut b); + let model = b.build(); + let ev = evidence(&model, &rp); + let mut moved = 0; + for v in 0u8..8 { + let mask = CausalMask::from_bits(v); + for mantissa in [-6i8, 0, 6] { + let q = query(mask, Contract::Open, Topology2::IndirectUnknown) + .with_inference_mantissa(mantissa); + let m = reason(q, &ev, Edit::CutStep(1)); + let out = revise(q, &m).unwrap(); + assert_eq!((out.0 ^ q.0) & !EPISTEMIC_MASK, 0, "{mask:?} {mantissa}"); + moved += usize::from(out != q); + } + } + assert!(moved > 0, "anti-vacuity: some operator must move the state"); + } + + /// F9. Same sealed evidence, same edit: same measurement, same bits. + #[test] + fn f9_replay_is_deterministic() { + let rp = Replay::new(); + let mut b = related_population(); + add_positive_trial(&mut b); + let model = b.build(); + let ev = evidence(&model, &rp); + for v in 0u8..8 { + let q = query( + CausalMask::from_bits(v), + Contract::Related, + Topology2::IndirectKnown, + ); + for edit in [Edit::None, Edit::CutStep(0), Edit::CutStep(1)] { + let (m1, m2) = (reason(q, &ev, edit), reason(q, &ev, edit)); + assert_eq!(m1, m2); + assert_eq!(revise(q, &m1).unwrap(), revise(q, &m2).unwrap()); + } + } + } + + /// F10. "The intervention passed" cannot be asserted: intervention + /// receipts without executed arms are not grounded, and executed arms + /// without an effect earn nothing. (`Measured` has private fields, so a + /// caller cannot construct a passing result either.) + #[test] + fn f10_a_pass_must_be_executed_not_claimed() { + let rp = Replay::new(); + let mut b = related_population(); + b.sources(SupportBasis::InterventionBacked, &[10, 11]); + let claimed = b.build(); + let (e, m) = step( + query(CausalMask::PO, Contract::Related, Topology2::Direct), + CausalMask::PO, + &evidence(&claimed, &rp), + Edit::None, + ); + assert_eq!( + m.ungrounded(), + Some(Ungrounded::Model(NotGrounded::EmptyArm)) + ); + assert_eq!(certification(e), Certification3::Related); + + let mut b = related_population(); + b.arm(true, 6, 3); + b.arm(false, 6, 3); + b.sources(SupportBasis::InterventionBacked, &[10, 11]); + let null = b.build(); + let (e, m) = step( + query(CausalMask::PO, Contract::Related, Topology2::Direct), + CausalMask::PO, + &evidence(&null, &rp), + Edit::None, + ); + assert_eq!((m.earned(), m.ungrounded()), (None, None)); + assert_eq!(certification(e), Certification3::Related); + } + + /// F11. Bits 59..63 do not select the operation and carry no reasoning + /// band: two edges differing only there get the same measurement. + #[test] + fn f11_bits_59_to_63_are_epistemic_output_not_a_reasoning_band() { + let rp = Replay::new(); + let mut b = related_population(); + add_positive_trial(&mut b); + let model = b.build(); + let ev = evidence(&model, &rp); + for v in 0u8..8 { + let mask = CausalMask::from_bits(v); + let base = reason( + query(mask, Contract::Open, Topology2::Direct), + &ev, + Edit::CutStep(1), + ); + for c in Contract::ALL { + for t in Topology2::ALL { + assert_eq!(reason(query(mask, c, t), &ev, Edit::CutStep(1)), base); + } + } + } + let src = include_str!("pearl_ladder_probe.rs"); + assert!(!src.contains(concat!("Reasoning", "Band"))); + } + + /// F12. Projection, mantissa and epistemic state are separate: the + /// operation follows the projection whatever the mantissa says, and + /// revision keeps both. + #[test] + fn f12_projection_mantissa_and_state_are_not_aliases() { + let rp = Replay::new(); + let mut b = related_population(); + add_positive_trial(&mut b); + let model = b.build(); + let ev = evidence(&model, &rp); + for v in 0u8..8 { + let mask = CausalMask::from_bits(v); + for mantissa in [ + InferenceType::Intervention.to_mantissa(), + InferenceType::Counterfactual.to_mantissa(), + 0, + ] { + let q = query(mask, Contract::Open, Topology2::Direct) + .with_inference_mantissa(mantissa); + let m = reason(q, &ev, Edit::CutStep(1)); + assert_eq!(m.operation(), Operation::of(mask)); + let out = revise(q, &m).unwrap(); + assert_eq!(out.causal_mask(), mask); + assert_eq!(out.inference_mantissa(), mantissa); + } + } + // The counterfactual mantissa on an SO edge still runs association. + let q = query(CausalMask::SO, Contract::Open, Topology2::Direct) + .with_inference_mantissa(InferenceType::Counterfactual.to_mantissa()); + assert_eq!( + reason(q, &ev, Edit::None).operation(), + Operation::Association + ); + } +} diff --git a/crates/cognitive-shader-driver/examples/relational_certification_probe.rs b/crates/cognitive-shader-driver/examples/relational_certification_probe.rs index 3158b4c0c..fff22e4ee 100644 --- a/crates/cognitive-shader-driver/examples/relational_certification_probe.rs +++ b/crates/cognitive-shader-driver/examples/relational_certification_probe.rs @@ -76,341 +76,19 @@ use causal_edge::edge::CausalEdge64; use causal_edge::layout::ReasoningBand; use lance_graph_contract::band_reading::EdgeProvenance; -use lance_graph_contract::causal_audit::{ - EvidenceSourceId, SupportBasis, SupportLedger, SupportReceipt, -}; -use lance_graph_contract::scheduler::DatasetVersion; +use lance_graph_contract::causal_audit::SupportBasis; +#[cfg(test)] +use lance_graph_contract::{causal_audit::EvidenceSourceId, scheduler::DatasetVersion}; #[path = "shared/certification_reading.rs"] mod certification_reading; use certification_reading::*; -// ── The sealed model and its satisfaction folds ─────────────────────────── - -/// Why a proposition could not be decided in the model. Not a third truth -/// value: the model lacks what the decision needs. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum NotGrounded { - EmptyArm, - TooFewSources, - NoIdentificationDesign, - NoDeclaredStratum, -} - -/// Satisfied (`Ok(true)`), refuted (`Ok(false)`), or not grounded. -type Sat = Result; - -const OBSERVATION: &[SupportBasis] = &[SupportBasis::DirectlyObserved, SupportBasis::TextAttested]; -const INTERVENTION: &[SupportBasis] = &[SupportBasis::InterventionBacked]; -/// Distinct sources a contract needs. Policy pin. -const MIN_SOURCES: usize = 2; - -/// A satisfaction fold over a sealed model. -type Evaluator = fn(&Model) -> Sat; - -/// One sealed model: unit masks over at most 64 units. -#[derive(Debug, Clone)] -struct Model { - seal: DatasetVersion, - universe: u64, - exposed: u64, - outcome: u64, - /// The declared comparison partition, fixed before the data are read. - strata: [u64; 4], - /// Units whose exposure precedes their outcome. - ordered: u64, - /// Units the robustness mask keeps. - clean: u64, - /// Randomized identification design. - trial: u64, - assigned: u64, - ledger: SupportLedger, - /// Which bases count as this model's evidence for the association - /// contracts. - bases: &'static [SupportBasis], -} - -fn count(m: u64) -> u64 { - u64::from(m.count_ones()) -} - -/// rate(Y | a) compared with rate(Y | b) by cross-multiplication. -fn compare(outcome: u64, a: u64, b: u64, strict: bool) -> Sat { - let (na, nb) = (count(a), count(b)); - if na == 0 || nb == 0 { - return Err(NotGrounded::EmptyArm); - } - let (lhs, rhs) = (count(outcome & a) * nb, count(outcome & b) * na); - Ok(if strict { lhs > rhs } else { lhs >= rhs }) -} - -/// Distinct sources among `bases`, counting only receipts recorded at or -/// before `seal`: a sealed model is certified from the evidence it held, so a -/// later receipt cannot change what an earlier seal replays to. -fn distinct_sources(ledger: &SupportLedger, bases: &[SupportBasis], seal: DatasetVersion) -> usize { - let mut seen: Vec = Vec::new(); - for r in ledger - .receipts() - .iter() - .filter(|r| r.at <= seal && bases.contains(&r.basis)) - { - if !seen.contains(&r.source) { - seen.push(r.source); - } - } - seen.len() -} - -/// Existential fold: satisfied if any grounded case is, refuted if every -/// grounded case is, not grounded if none is. -fn any(results: impl Iterator) -> Sat { - let mut grounded = false; - let mut first_err = NotGrounded::EmptyArm; - for r in results { - match r { - Ok(true) => return Ok(true), - Ok(false) => grounded = true, - Err(e) => first_err = e, - } - } - if grounded { - Ok(false) - } else { - Err(first_err) - } -} - -impl Model { - fn sourced(&self) -> Result<(), NotGrounded> { - if distinct_sources(&self.ledger, self.bases, self.seal) >= MIN_SOURCES { - Ok(()) - } else { - Err(NotGrounded::TooFewSources) - } - } - - fn assoc_in(&self, p: u64) -> Sat { - compare(self.outcome, p & self.exposed, p & !self.exposed, true) - } - - fn declared(&self) -> impl Iterator + '_ { - core::iter::once(self.universe).chain( - self.strata - .iter() - .filter(|s| **s != 0) - .map(move |s| s & self.universe), - ) - } - - fn associated(&self) -> Sat { - self.sourced()?; - any(self.declared().map(|p| self.assoc_in(p))) - } - - /// The marginal variant, kept to measure where the chain breaks. - fn associated_marginal(&self) -> Sat { - self.sourced()?; - self.assoc_in(self.universe) - } - - /// The robustness mask can only refute: the association must hold in the - /// full population and after the mask. - fn related(&self) -> Sat { - self.sourced()?; - any(self.declared().map(|p| { - let full = self.assoc_in(p)?; - let kept = self.assoc_in(p & self.clean)?; - Ok(full && kept) - })) - } - - fn contributes(&self) -> Sat { - self.sourced()?; - let mut strict = false; - let mut any_stratum = false; - for s in self.strata.iter().filter(|s| **s != 0) { - any_stratum = true; - let s = s & self.universe; - let (a, b) = (s & self.exposed, s & !self.exposed); - let (ka, kb) = (a & self.clean, b & self.clean); - if !compare(self.outcome, a, b, false)? || !compare(self.outcome, ka, kb, false)? { - return Ok(false); - } - strict |= compare(self.outcome, a, b, true)? && compare(self.outcome, ka, kb, true)?; - } - if !any_stratum { - return Err(NotGrounded::NoDeclaredStratum); - } - Ok(strict) - } - - fn causal_candidate(&self) -> Sat { - if !self.contributes()? { - return Ok(false); - } - let co = self.universe & self.exposed & self.outcome; - Ok(co & !self.ordered == 0) - } - - fn causes(&self) -> Sat { - match distinct_sources(&self.ledger, INTERVENTION, self.seal) { - 0 => return Err(NotGrounded::NoIdentificationDesign), - n if n < MIN_SOURCES => return Err(NotGrounded::TooFewSources), - _ => {} - } - compare( - self.outcome, - self.trial & self.assigned, - self.trial & !self.assigned, - true, - ) - } - - /// The randomized arms read as a population of their own. - fn trial_view(&self) -> Model { - Model { - universe: self.trial, - exposed: self.assigned, - strata: [self.trial, 0, 0, 0], - ordered: self.trial, - bases: INTERVENTION, - ..self.clone() - } - } - - /// The strongest contract the model satisfies. - fn certify(&self) -> Contract { - if self.causes() == Ok(true) { - return Contract::Causes; - } - let rungs: [(Contract, Evaluator); 4] = [ - (Contract::CausalCandidate, Model::causal_candidate), - (Contract::Contributes, Model::contributes), - (Contract::Related, Model::related), - (Contract::Associated, Model::associated), - ]; - rungs - .iter() - .find(|(_, f)| f(self) == Ok(true)) - .map_or(Contract::Open, |(c, _)| *c) - } -} - -// ── Fixture builder ─────────────────────────────────────────────────────── +#[path = "shared/certification_model.rs"] +mod certification_model; -struct Builder { - next: u32, - m: Model, -} - -impl Builder { - fn new() -> Self { - Builder { - next: 0, - m: Model { - seal: DatasetVersion(1), - universe: 0, - exposed: 0, - outcome: 0, - strata: [0; 4], - ordered: 0, - clean: u64::MAX, - trial: 0, - assigned: 0, - ledger: SupportLedger::new(), - bases: OBSERVATION, - }, - } - } - - fn units(&mut self, n: u32, hits: u32) -> (u64, u64) { - assert!(hits <= n && self.next + n <= 64, "fixture exceeds 64 units"); - let mut all = 0; - let mut hit = 0; - for i in 0..n { - let bit = 1u64 << (self.next + i); - all |= bit; - if i < hits { - hit |= bit; - } - } - self.next += n; - self.m.outcome |= hit; - (all, hit) - } - - /// Observational cell: `n` units in `stratum`, `hits` with the outcome. - fn cell(&mut self, stratum: usize, exposed: bool, n: u32, hits: u32) -> (u64, u64) { - let (all, hit) = self.units(n, hits); - self.m.universe |= all; - self.m.strata[stratum] |= all; - self.m.ordered |= all; - if exposed { - self.m.exposed |= all; - } - (all, hit) - } - - fn arm(&mut self, assigned: bool, n: u32, hits: u32) -> (u64, u64) { - let (all, hit) = self.units(n, hits); - self.m.trial |= all; - if assigned { - self.m.assigned |= all; - } - (all, hit) - } - - fn sources(&mut self, basis: SupportBasis, ids: &[u64]) -> &mut Self { - let at = self.m.seal; - self.sources_at(basis, ids, at) - } - - fn sources_at(&mut self, basis: SupportBasis, ids: &[u64], at: DatasetVersion) -> &mut Self { - for id in ids { - self.m.ledger.record(SupportReceipt { - basis, - source: EvidenceSourceId(*id), - at, - strength: 128, - }); - } - self - } - - fn build(&self) -> Model { - self.m.clone() - } -} - -/// A robust single-stratum association: 4/5 exposed vs 1/5 unexposed. -fn robust_association() -> Builder { - let mut b = Builder::new(); - b.cell(0, true, 5, 4); - b.cell(0, false, 5, 1); - b.sources(SupportBasis::DirectlyObserved, &[1, 2]); - b -} - -/// Within each stratum A raises Y; pooled, A lowers it (Simpson). -fn simpson_population() -> Builder { - let mut b = Builder::new(); - // stratum 0: mostly exposed, low base rate - b.cell(0, true, 8, 2); // 0.25 - b.cell(0, false, 2, 0); // 0.00 - // stratum 1: mostly unexposed, high base rate - b.cell(1, true, 2, 2); // 1.00 - b.cell(1, false, 8, 6); // 0.75 - b.sources(SupportBasis::DirectlyObserved, &[1, 2]); - b -} - -/// A positive randomized trial with two independent intervention sources. -fn add_positive_trial(b: &mut Builder) { - b.arm(true, 6, 5); - b.arm(false, 6, 1); - b.sources(SupportBasis::InterventionBacked, &[10, 11]); -} +use certification_model::*; fn main() { let decl = declarations(); diff --git a/crates/cognitive-shader-driver/examples/shared/certification_model.rs b/crates/cognitive-shader-driver/examples/shared/certification_model.rs new file mode 100644 index 000000000..e940ccc34 --- /dev/null +++ b/crates/cognitive-shader-driver/examples/shared/certification_model.rs @@ -0,0 +1,355 @@ +//! The P7a sealed model (D-GSO-7a, #1369): unit-mask populations, the +//! satisfaction folds that decide each certification contract, and the +//! fixture builder. Moved here unchanged from `relational_certification_probe` +//! (where it is tested) so that `pearl_ladder_probe` executes the same folds +//! instead of a copy. Items are `pub` only so the including examples can +//! reach them; each uses a subset, hence the `dead_code` allowance. +#![allow(dead_code)] + +use crate::certification_reading::Contract; +use lance_graph_contract::causal_audit::{ + EvidenceSourceId, SupportBasis, SupportLedger, SupportReceipt, +}; +use lance_graph_contract::scheduler::DatasetVersion; + +// ── The sealed model and its satisfaction folds ─────────────────────────── + +/// Why a proposition could not be decided in the model. Not a third truth +/// value: the model lacks what the decision needs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NotGrounded { + EmptyArm, + TooFewSources, + NoIdentificationDesign, + NoDeclaredStratum, +} + +/// Satisfied (`Ok(true)`), refuted (`Ok(false)`), or not grounded. +pub type Sat = Result; + +pub const OBSERVATION: &[SupportBasis] = + &[SupportBasis::DirectlyObserved, SupportBasis::TextAttested]; +pub const INTERVENTION: &[SupportBasis] = &[SupportBasis::InterventionBacked]; +/// Distinct sources a contract needs. Policy pin. +pub const MIN_SOURCES: usize = 2; + +/// A satisfaction fold over a sealed model. +pub type Evaluator = fn(&Model) -> Sat; + +/// One sealed model: unit masks over at most 64 units. +#[derive(Debug, Clone)] +pub struct Model { + pub seal: DatasetVersion, + pub universe: u64, + pub exposed: u64, + pub outcome: u64, + /// The declared comparison partition, fixed before the data are read. + pub strata: [u64; 4], + /// Units whose exposure precedes their outcome. + pub ordered: u64, + /// Units the robustness mask keeps. + pub clean: u64, + /// Randomized identification design. + pub trial: u64, + pub assigned: u64, + pub ledger: SupportLedger, + /// Which bases count as this model's evidence for the association + /// contracts. + pub bases: &'static [SupportBasis], +} + +pub fn count(m: u64) -> u64 { + u64::from(m.count_ones()) +} + +/// rate(Y | a) compared with rate(Y | b) by cross-multiplication. +pub fn compare(outcome: u64, a: u64, b: u64, strict: bool) -> Sat { + let (na, nb) = (count(a), count(b)); + if na == 0 || nb == 0 { + return Err(NotGrounded::EmptyArm); + } + let (lhs, rhs) = (count(outcome & a) * nb, count(outcome & b) * na); + Ok(if strict { lhs > rhs } else { lhs >= rhs }) +} + +/// Distinct sources among `bases`, counting only receipts recorded at or +/// before `seal`: a sealed model is certified from the evidence it held, so a +/// later receipt cannot change what an earlier seal replays to. +pub fn distinct_sources( + ledger: &SupportLedger, + bases: &[SupportBasis], + seal: DatasetVersion, +) -> usize { + let mut seen: Vec = Vec::new(); + for r in ledger + .receipts() + .iter() + .filter(|r| r.at <= seal && bases.contains(&r.basis)) + { + if !seen.contains(&r.source) { + seen.push(r.source); + } + } + seen.len() +} + +/// Existential fold: satisfied if any grounded case is, refuted if every +/// grounded case is, not grounded if none is. +pub fn any(results: impl Iterator) -> Sat { + let mut grounded = false; + let mut first_err = NotGrounded::EmptyArm; + for r in results { + match r { + Ok(true) => return Ok(true), + Ok(false) => grounded = true, + Err(e) => first_err = e, + } + } + if grounded { + Ok(false) + } else { + Err(first_err) + } +} + +impl Model { + pub fn sourced(&self) -> Result<(), NotGrounded> { + if distinct_sources(&self.ledger, self.bases, self.seal) >= MIN_SOURCES { + Ok(()) + } else { + Err(NotGrounded::TooFewSources) + } + } + + pub fn assoc_in(&self, p: u64) -> Sat { + compare(self.outcome, p & self.exposed, p & !self.exposed, true) + } + + pub fn declared(&self) -> impl Iterator + '_ { + core::iter::once(self.universe).chain( + self.strata + .iter() + .filter(|s| **s != 0) + .map(move |s| s & self.universe), + ) + } + + pub fn associated(&self) -> Sat { + self.sourced()?; + any(self.declared().map(|p| self.assoc_in(p))) + } + + /// The marginal variant, kept to measure where the chain breaks. + pub fn associated_marginal(&self) -> Sat { + self.sourced()?; + self.assoc_in(self.universe) + } + + /// The robustness mask can only refute: the association must hold in the + /// full population and after the mask. + pub fn related(&self) -> Sat { + self.sourced()?; + any(self.declared().map(|p| { + let full = self.assoc_in(p)?; + let kept = self.assoc_in(p & self.clean)?; + Ok(full && kept) + })) + } + + pub fn contributes(&self) -> Sat { + self.sourced()?; + let mut strict = false; + let mut any_stratum = false; + for s in self.strata.iter().filter(|s| **s != 0) { + any_stratum = true; + let s = s & self.universe; + let (a, b) = (s & self.exposed, s & !self.exposed); + let (ka, kb) = (a & self.clean, b & self.clean); + if !compare(self.outcome, a, b, false)? || !compare(self.outcome, ka, kb, false)? { + return Ok(false); + } + strict |= compare(self.outcome, a, b, true)? && compare(self.outcome, ka, kb, true)?; + } + if !any_stratum { + return Err(NotGrounded::NoDeclaredStratum); + } + Ok(strict) + } + + pub fn causal_candidate(&self) -> Sat { + if !self.contributes()? { + return Ok(false); + } + let co = self.universe & self.exposed & self.outcome; + Ok(co & !self.ordered == 0) + } + + pub fn causes(&self) -> Sat { + match distinct_sources(&self.ledger, INTERVENTION, self.seal) { + 0 => return Err(NotGrounded::NoIdentificationDesign), + n if n < MIN_SOURCES => return Err(NotGrounded::TooFewSources), + _ => {} + } + compare( + self.outcome, + self.trial & self.assigned, + self.trial & !self.assigned, + true, + ) + } + + /// The randomized arms read as a population of their own. + pub fn trial_view(&self) -> Model { + Model { + universe: self.trial, + exposed: self.assigned, + strata: [self.trial, 0, 0, 0], + ordered: self.trial, + bases: INTERVENTION, + ..self.clone() + } + } + + /// The strongest contract the model satisfies. + pub fn certify(&self) -> Contract { + if self.causes() == Ok(true) { + return Contract::Causes; + } + self.certify_observational() + } + + /// The strongest contract the observational folds alone satisfy. `causes` + /// is not in the list, so no amount of observation returns `Causes`. + pub fn certify_observational(&self) -> Contract { + let rungs: [(Contract, Evaluator); 4] = [ + (Contract::CausalCandidate, Model::causal_candidate), + (Contract::Contributes, Model::contributes), + (Contract::Related, Model::related), + (Contract::Associated, Model::associated), + ]; + rungs + .iter() + .find(|(_, f)| f(self) == Ok(true)) + .map_or(Contract::Open, |(c, _)| *c) + } +} + +// ── Fixture builder ─────────────────────────────────────────────────────── + +pub struct Builder { + pub next: u32, + pub m: Model, +} + +impl Builder { + pub fn new() -> Self { + Builder { + next: 0, + m: Model { + seal: DatasetVersion(1), + universe: 0, + exposed: 0, + outcome: 0, + strata: [0; 4], + ordered: 0, + clean: u64::MAX, + trial: 0, + assigned: 0, + ledger: SupportLedger::new(), + bases: OBSERVATION, + }, + } + } + + pub fn units(&mut self, n: u32, hits: u32) -> (u64, u64) { + assert!(hits <= n && self.next + n <= 64, "fixture exceeds 64 units"); + let mut all = 0; + let mut hit = 0; + for i in 0..n { + let bit = 1u64 << (self.next + i); + all |= bit; + if i < hits { + hit |= bit; + } + } + self.next += n; + self.m.outcome |= hit; + (all, hit) + } + + /// Observational cell: `n` units in `stratum`, `hits` with the outcome. + pub fn cell(&mut self, stratum: usize, exposed: bool, n: u32, hits: u32) -> (u64, u64) { + let (all, hit) = self.units(n, hits); + self.m.universe |= all; + self.m.strata[stratum] |= all; + self.m.ordered |= all; + if exposed { + self.m.exposed |= all; + } + (all, hit) + } + + pub fn arm(&mut self, assigned: bool, n: u32, hits: u32) -> (u64, u64) { + let (all, hit) = self.units(n, hits); + self.m.trial |= all; + if assigned { + self.m.assigned |= all; + } + (all, hit) + } + + pub fn sources(&mut self, basis: SupportBasis, ids: &[u64]) -> &mut Self { + let at = self.m.seal; + self.sources_at(basis, ids, at) + } + + pub fn sources_at( + &mut self, + basis: SupportBasis, + ids: &[u64], + at: DatasetVersion, + ) -> &mut Self { + for id in ids { + self.m.ledger.record(SupportReceipt { + basis, + source: EvidenceSourceId(*id), + at, + strength: 128, + }); + } + self + } + + pub fn build(&self) -> Model { + self.m.clone() + } +} + +/// A robust single-stratum association: 4/5 exposed vs 1/5 unexposed. +pub fn robust_association() -> Builder { + let mut b = Builder::new(); + b.cell(0, true, 5, 4); + b.cell(0, false, 5, 1); + b.sources(SupportBasis::DirectlyObserved, &[1, 2]); + b +} + +/// Within each stratum A raises Y; pooled, A lowers it (Simpson). +pub fn simpson_population() -> Builder { + let mut b = Builder::new(); + // stratum 0: mostly exposed, low base rate + b.cell(0, true, 8, 2); // 0.25 + b.cell(0, false, 2, 0); // 0.00 + // stratum 1: mostly unexposed, high base rate + b.cell(1, true, 2, 2); // 1.00 + b.cell(1, false, 8, 6); // 0.75 + b.sources(SupportBasis::DirectlyObserved, &[1, 2]); + b +} + +/// A positive randomized trial with two independent intervention sources. +pub fn add_positive_trial(b: &mut Builder) { + b.arm(true, 6, 5); + b.arm(false, 6, 1); + b.sources(SupportBasis::InterventionBacked, &[10, 11]); +} diff --git a/docs/architecture/ce64-semantic-upper-half.md b/docs/architecture/ce64-semantic-upper-half.md new file mode 100644 index 000000000..13f5594ab --- /dev/null +++ b/docs/architecture/ce64-semantic-upper-half.md @@ -0,0 +1,53 @@ +# CausalEdge64: the semantic upper half + +![CausalEdge64 semantic upper-half: Pearl ladder, orientation, activation, entropy, belief-state update, epistemic state](img/ce64-semantic-upper-half.png) + +The diagram reads bits 40..63 of the 64-bit edge register as a reasoning +loop: a Pearl question goes in, the edge records reaction, novelty and an +update breadcrumb, and its epistemic state says what the substrate may +currently claim. Pearl queries, ablation and EWA-style propagation are the +mechanics that change those states; they are not stored in the edge. + +**Status: WORKING-MODEL.** The diagram is a reading of the register, not its +definition. The layout is fixed by `crates/causal-edge/src/layout.rs` +(`_LAYOUT_COVERAGE` asserts all 64 bits are covered exactly once), and bits +59..63 by `lance_graph_contract::epistemic_state5`. Where the diagram names a +band differently from the code, the table below says so. Read the code's name +as current behaviour and the diagram's name as a proposed interpretation. + +## Band by band, against the code + +| bits | diagram | code today | agreement | +|---|---|---|---| +| 0..23 | opaque payload 8:8:8 | `s_idx`, `p_idx`, `o_idx`: three palette indices | same | +| 24..31 | F, frequency | `frequency_u8` | same | +| 32..39 | C, confidence | `confidence_u8` | same | +| 40..42 | Pearl ladder: asks the causal question | `CausalMask` (`pearl.rs`): `SO` association, `PO` intervention, `SPO` counterfactual, `SP` confounder detection, plus the marginals `None`, `O`, `P`, `S` | same. The diagram's "ablation" row has no mask of its own: ablation is the counterfactual edit (a cut), as the diagram's reasoning loop also says | +| 43..45 | orientation / syntax: which way the relation is read | `direction()`: a 3-bit triad, one "pathological" sign bit per S, P, O plane | different reading. Grammar, Gestalt, Moore and relation direction are not implemented on these bits | +| 46..49 | activation i4: +7 excitation … −8 inhibition | signed i4 inference mantissa: sign = chain direction, magnitude = NARS rule index; +6 Intervention, −6 Counterfactual | different reading. In code this field names the inference operation, not an excitation level | +| 50..52 | entropy / novelty: how unresolved or surprising | `PlasticityState`: one bit per S, P, O plane, hot = accepts palette reassignment under evidence pressure | different reading. Plasticity is related to how settled a plane is, but it is not an entropy measure | +| 53..58 | belief-state update: witness / G breadcrumb | `w_slot()`: witness corpus root handle, 0..=63, 0 = no anchor. The G-slot was dropped in v2 (L-3) | partly. The six bits hold one handle; the W1 → W2 → W3 history in the diagram is not stored in the edge | +| 59..63 | epistemic state: co-correlation → related / support → candidate → causality | `EpistemicState5 = Topology2 × Certification3`. Certification: 0 Open, 1 Associated, 2 Related, 3 Supports, 4 CausalCandidate, 5 Causes. Topology: Direct, IndirectKnown, IndirectUnknown, Unknown | same ladder. The diagram omits `Open` and the topology coordinate (bits 59..60) | + +## What the code already enforces from the diagram + +- **"Pearl asks the question; it does not certify the answer."** + `cognitive-shader-driver/examples/pearl_ladder_probe.rs` (D-PEARL-IO-0) + dispatches on bits 40..42 and lets only the measured result revise bits + 59..63. Only an executed intervention (`PO`) can earn `Causes`. +- **"Ablation and EWA are interrogation mechanics, not resident meaning."** + The counterfactual replay (`lance_graph_planner::dismech_counterfactual`) + tags its arm with the −6 mantissa and never writes it back as observed + truth. EWA-style propagation lives in `crates/jc/src/ewa_sandwich.rs` and + writes no edge field. +- **SPO alignment.** The payload, frequency and confidence feed learning + and calibration; certification is not derived from them. + +## Open + +- Whether bits 43..52 should be re-read as orientation, activation and + novelty. That would change the meaning of bits that existing code writes, + so it needs a version gate (`I-LEGACY-API-FEATURE-GATED`). It is not done + here. +- Where a belief-update history longer than one witness handle lives. It is + not in the edge. diff --git a/docs/architecture/img/ce64-semantic-upper-half.png b/docs/architecture/img/ce64-semantic-upper-half.png new file mode 100644 index 000000000..0eaf9ce39 Binary files /dev/null and b/docs/architecture/img/ce64-semantic-upper-half.png differ