diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index f4c8e51a0..ccb470927 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -34,7 +34,8 @@ Plan: `.claude/plans/2026-10-06-global-sudoku-replayable-orchestration-v1.md`. O | **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, #1362, #1365); `observations_pending` + `frontier_bounded` wired, `new_encounter` → `revision_pending`: 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. Follow-up: `new_encounter` is derived from the world (revising would still change a horizon mask; equals `revise` changing it on a 2-bit universe; rootless revisions included) and the `Revision` recipe writes `GadamerRevision`'s `delta.resulting`; dropping that write never rests; `unresolved_tension` cannot drive `local_disagreement` (never cleared); the other three facts are still stand-ins; 14 tests, 10 disable runs red. Follow-up 2: `local_disagreement` = `unresolved_tension \ interrogated`, where `interrogated` is the coverage `MooreInterrogation` writes (tension itself never cleared); a revision adding a new contradiction reopens it; dropping the interrogation write never rests; `observations_pending` and `frontier_bounded` remain stand-ins; 17 tests, 15 disable runs red. Follow-up 3: `new_encounter` renamed `revision_pending` (revising would change the horizon); `observations_pending` = arrived verdicts > verdicts the `Quorum` counted (silence included; `speaking()` would never rest on a silent source); dropping the fold write never rests; coverage stays per tension bit (new evidence on a covered bit does not reopen); `frontier_bounded` = a recorded `Quad8::fold_product` frontier exists for the current candidate space (a changed space reopens it; an empty frontier is bounded); no stand-in fact remains in the wired cycle; 25 tests, 25 disable runs red | | **D-GSO-7** | P7: reasoning-band earning and downgrade | Shipped (#1360) | `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-7a** | P7a: the reasoning band as a relational certification (supersedes the meaning of D-GSO-7's rungs; #1360 unchanged) | In PR | `cognitive-shader-driver/examples/relational_certification_probe.rs`: a reading declared per class over bits 61..63 (`0 Open, 1 Associated, 2 Related, 3 Contributes, 4 CausalCandidate, 5 Causes`, 6..7 refuse; a class declared under the historical reading refuses the same bits); contracts are integer folds over unit masks + `causal_audit::SupportLedger` (2 distinct sources; the robustness mask only refutes; `Causes` = 2 distinct `InterventionBacked` sources + executed randomized arms); `entails` is an explicit table equal to `code >=` on 0..=5 only; repetition, observational effect size, the inference mantissa and confounded observational data never raise or demote past their contract; exhaustive 2,500-model family: 0 chain violations with scoped association, 10 with marginal (Simpson); sibling specificity and removal (overdetermination) are shown not to be rungs; only receipts recorded at or before the model's seal count, so a later receipt cannot change an earlier seal's replay; 13 tests, 12 disable runs red | +| **D-GSO-7a** | P7a: the reasoning band as a relational certification (supersedes the meaning of D-GSO-7's rungs; #1360 unchanged) | Shipped (#1369) | `cognitive-shader-driver/examples/relational_certification_probe.rs`: a reading declared per class over bits 61..63 (`0 Open, 1 Associated, 2 Related, 3 Contributes, 4 CausalCandidate, 5 Causes`, 6..7 refuse; a class declared under the historical reading refuses the same bits); contracts are integer folds over unit masks + `causal_audit::SupportLedger` (2 distinct sources; the robustness mask only refutes; `Causes` = 2 distinct `InterventionBacked` sources + executed randomized arms); `entails` is an explicit table equal to `code >=` on 0..=5 only; repetition, observational effect size, the inference mantissa and confounded observational data never raise or demote past their contract; exhaustive 2,500-model family: 0 chain violations with scoped association, 10 with marginal (Simpson); sibling specificity and removal (overdetermination) are shown not to be rungs; only receipts recorded at or before the model's seal count, so a later receipt cannot change an earlier seal's replay; 13 tests, 12 disable runs red | +| **D-GSO-AFF-0** | Affordance measurement: `CausalEdge64 × RecipeLaw[0..63] → EligibleRecipes: u64` | In PR | `cognitive-shader-driver/examples/affordance_measurement_probe.rs`: bits 59..63 read as one EpistemicState5 code (`spare() << 2 \| truth_raw()`, 10 declared codes, non-monotone, 22 refuse) + Pearl3; 7 recipes as `requires`/`forbids` facts + Pearl planes, compiled at build time to `[u64; 32]` × `[u64; 8]`; measure = 2 lookups + AND, 0 allocations; F1 determinism and law-generation dependence, F2 relevant factor flips its bit, F3 S/P/O/F/C/43..45/mantissa/plasticity/witness swept with no change, F4/F5 tables agree bit-for-bit with an independent rule re-derivation that names every refusal, F6/F7 preference only clears, half-field writes to 59..60 yield refusing codes; v1 / unknown provenance refuses (a v1 row with temporal 128 would read as `Causes`); no scheduler/task/capability object; 9 tests, 8 disable runs red | | **D-GSO-8** | P8: seal boundary | Shipped (#1363) | `lance-graph-planner/examples/seal_boundary_probe.rs`: internal events change only working rows; a declared `Boundary` event freezes the changed rows with the shipped `DetachedCycleBatch::freeze` into a local ledger (no `WalSink`); an unchanged boundary writes nothing; replaying the persisted events from each prior seal reproduces the next seal exactly (frame, landings, image, hash); wrong base, dropped event and swapped order each change the seal; tie limit handled by plan option 1: the key is the event's own durable unique `seq`, a tied batch is refused, and a test shows on the real `freeze` that tied keys make the seal arrival-dependent; 6 tests, 5 disable runs red. Follow-up: `apply` refuses a `seq` not greater than the last (a reuse on one row or across seals escaped `first_tie`), `resume_after` restores the top persisted key; 7 tests, 2 more disable runs red | ## D-PLX — Population-law cross-check (2026-10-03) diff --git a/.claude/board/entries/2026-10-06-affordance-measurement.md b/.claude/board/entries/2026-10-06-affordance-measurement.md new file mode 100644 index 000000000..7dbd4eab8 --- /dev/null +++ b/.claude/board/entries/2026-10-06-affordance-measurement.md @@ -0,0 +1,60 @@ +# 2026-10-06 — D-GSO-AFF-0: recipe eligibility is a measurement over the edge + +## MEASURED + +`crates/cognitive-shader-driver/examples/affordance_measurement_probe.rs`: +9 tests, 8 disable runs red. + +- `CausalEdge64 × RecipeLaw → EligibleRecipes: u64` is two const-table + lookups and one AND, with 0 allocations and no scheduler, task, capability + object or recipe list. The tables are compiled at build time from per-recipe + `requires` / `forbids` facts and Pearl planes. +- Bits 59..63 read as one EpistemicState5 code through the shipped accessors + (`spare() << 2 | truth_raw()`). Ten codes are declared, ordered without regard + to strength; 22 refuse. Code 17 = Indirect × IntermediateKnown × Causes. +- F3: S/P/O bytes, F/C, bits 43..45, the mantissa, plasticity and the witness + slot were swept over their full ranges with no bit change. Pearl moves only + the one recipe that reads it. +- F4/F5: the compiled tables match an independent re-derivation from the rules + on every code × projection × recipe, and every refusal names its obligation. +- Bits 59..63 are read only under asserted v2/V3 provenance (codex review): + a v1 row whose old `temporal` is 128 has bit 59 set and would read as code + 1 (`Causes`); v1 and unknown provenance refuse. +- Rewriting only bits 59..60 with the shipped `with_topology` turns code 17 + into 18, which refuses. + +## Inventory (before the probe) + +- Bits 43..45 (`direction`): no reader outside `causal-edge`. Inside it, + `*_pathological`, `concern_level`, `CausalNetwork::detect_simpsons_paradox`, + `compose` (inherits the weight's triad) and the V3 lift read it as the + per-plane pathology triad. +- Witness (bits 53..58): `MailboxSoA::apply_edges` (production) drops every + edge whose `w_slot` differs from the mailbox's. It is compared by equality + only; a reading that varies it per evidence class would drop edges there. + Width 6 is assumed by `MailboxSoA::new` (`< 64`), `WitnessTable<64>`, the V3 + byte 8 packing and `with_routing`. +- Bits 59..63: shipped readers split them. `truth`/`topology` read 59..60; + `ReasoningBand`/`spare` read 61..63; `band_reading` declares and projects the + two separately; V3 stores them in different bytes (raw copies, so the joint + code survives the lift). External readers: `dismech_counterfactual::EdgeRole` + copies both; `entropy_topology_probe` reads 59..60. +- S/P/O: typed by name only (`s_idx`, "palette index"); `compose` indexes + caller-supplied 256×256 tables as `left * 256 + right`, the same orientation + as `PairAddress`. +- #1336/#1337: `Quad8([u8; 4])` holds blind bytes; `palette_pairs` wraps them + in `PaletteState` for `PairAddress::new`, which is `(left << 8) | right`. No + code connects Quad8 to CE64's S/P/O bytes. Both modules call the 8×8×8×8 + product "Cartesian". +- `StepMask(u64)` exists for template step positions, a different index space + from recipe ordinals, so the probe uses a plain `u64`. `recipe_dispatch::ladder` + returns `Vec`. + +## OPEN + +- A joint 5-bit reading needs its own `band_reading` declaration, and the + half-field writers (`with_topology`, `with_truth`, `with_reasoning_band`) + can produce undeclared codes. +- Witness cannot be read as an evidence class while `MailboxSoA` routes on it. +- The codebook, facts and recipes are probe pins; dynamic gates, preference + and execution are not built. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 7081a13e3..483c37fbc 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. -230 entries, 2026-08-06 .. 2026-10-06. +231 entries, 2026-08-06 .. 2026-10-06. | date | entry id | finding | file | |---|---|---|---| @@ -40,6 +40,7 @@ exactly one of them, never both. | 2026-10-06 | `diff-plateau-as-expression-round5` | | [2026-10-06-diff-plateau-as-expression-round5.md](2026-10-06-diff-plateau-as-expression-round5.md) | | 2026-10-06 | `dav-resident-reading-round2` | | [2026-10-06-dav-resident-reading-round2.md](2026-10-06-dav-resident-reading-round2.md) | | 2026-10-06 | `dav-carrier-invariance-quack` | | [2026-10-06-dav-carrier-invariance-quack.md](2026-10-06-dav-carrier-invariance-quack.md) | +| 2026-10-06 | `D-GSO-AFF-0` | | [2026-10-06-affordance-measurement.md](2026-10-06-affordance-measurement.md) | | 2026-10-05 | `text-to-numeric-boundary-inventory` | | [2026-10-05-text-to-numeric-boundary-inventory.md](2026-10-05-text-to-numeric-boundary-inventory.md) | | 2026-10-05 | `report-pair-key-and-stack-recon` | | [2026-10-05-report-pair-key-and-stack-recon.md](2026-10-05-report-pair-key-and-stack-recon.md) | | 2026-10-05 | `quack-two-world-frontend` | | [2026-10-05-quack-two-world-frontend.md](2026-10-05-quack-two-world-frontend.md) | diff --git a/crates/cognitive-shader-driver/Cargo.toml b/crates/cognitive-shader-driver/Cargo.toml index 2f75f83e8..a734260c7 100644 --- a/crates/cognitive-shader-driver/Cargo.toml +++ b/crates/cognitive-shader-driver/Cargo.toml @@ -102,6 +102,11 @@ test = true name = "relational_certification_probe" test = true +# D-GSO-AFF-0 affordance-measurement probe; tests run under `cargo test`. +[[example]] +name = "affordance_measurement_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 22918c340..2ec7214da 100644 --- a/crates/cognitive-shader-driver/examples/README.md +++ b/crates/cognitive-shader-driver/examples/README.md @@ -198,6 +198,22 @@ thresholds are policy pins. cargo run -p cognitive-shader-driver --example relational_certification_probe ``` +## affordance_measurement_probe.rs + +D-GSO-AFF-0. Recipe eligibility as a measurement: `CausalEdge64 × RecipeLaw → +EligibleRecipes: u64`. Bits 59..63 are read as one EpistemicState5 code (ten +declared codes, deliberately not ordered by strength; the rest refuse), and +bits 40..42 under their Pearl reading. Seven recipes declare `requires` / +`forbids` facts and Pearl planes; the rules compile at build time into +`[u64; 32]` and `[u64; 8]`, so a measurement is two lookups and an AND with no +allocation. Fields no recipe reads (S/P/O, F/C, bits 43..45, mantissa, +plasticity, witness) are swept and change nothing; preference can only clear +bits. Two law generations show the generation is part of the reading. + +```bash +cargo run -p cognitive-shader-driver --example affordance_measurement_probe +``` + ## ewa_anisotropic_probe.rs D-CTX-3. Anisotropic EWA: the footprint is each virtual surfel's normalised diff --git a/crates/cognitive-shader-driver/examples/affordance_measurement_probe.rs b/crates/cognitive-shader-driver/examples/affordance_measurement_probe.rs new file mode 100644 index 000000000..20c0ba0c5 --- /dev/null +++ b/crates/cognitive-shader-driver/examples/affordance_measurement_probe.rs @@ -0,0 +1,749 @@ +//! D-GSO-AFF-0: recipe eligibility is a measurement over resident edge state. +//! +//! Claim under test: +//! +//! ```text +//! CausalEdge64 × RecipeLaw[0..63] → EligibleRecipes : u64 +//! ``` +//! +//! falls out of the existing substrate as two const-table lookups and one AND, +//! with no scheduler, task, capability object or heap allocation. The `u64` is +//! transient: it is measured from the edge each time and never written back. +//! +//! # The readings the law declares (law generation V1 / V2) +//! +//! - **EpistemicState5.** Bits 59..63 are read as ONE 5-bit code, +//! `raw5 = spare() << 2 | truth_raw()`, through the existing accessors. The +//! law maps a code to a set of semantic facts (`DIRECT`, `INDIRECT`, +//! `INTERMEDIATE_*`, `OBSERVED`, `ASSOCIATED`, `RELATED`, `SUPPORTS`, +//! `CAUSES`). Only ten codes are valid; the rest refuse. The codes are +//! deliberately not ordered by strength: `raw_a > raw_b` means nothing, and +//! "or higher" is written into each code's fact set by the law. +//! Bits 59..63 are read only under an asserted v2-stamped or V3-register +//! provenance (the `band_reading` rule): on a v1 row they are old +//! `temporal` bits, and v1 or unknown provenance refuses. +//! - **Pearl3.** Bits 40..42 under their shipped reading (which S/P/O planes +//! participate). Only one recipe reads it. +//! +//! Every other field (S/P/O bytes, F/C, bits 43..45, the inference mantissa, +//! plasticity, the witness slot) is irrelevant to every recipe here, and a test +//! sweeps each of them to prove it. +//! +//! # The recipe law +//! +//! | bit | recipe | requires | forbids | Pearl planes | +//! |---|---|---|---|---| +//! | 0 | `HYDRATE_INTERMEDIATE` | `INDIRECT`, `INTERMEDIATE_UNKNOWN` | `INTERMEDIATE_KNOWN` | — | +//! | 1 | `MECHANISM_FOLD` | `INDIRECT`, `INTERMEDIATE_KNOWN` | — | — | +//! | 2 | `STRATIFY` | `ASSOCIATED` | (V2: `CAUSES`) | — | +//! | 3 | `ROBUSTNESS_TEST` | `RELATED` | — | — | +//! | 4 | `CAUSAL_IDENTIFICATION` | `SUPPORTS` | `INTERMEDIATE_UNKNOWN`, `CAUSES` | — | +//! | 5 | `OBSERVE_FOLD` | — | — | — | +//! | 6 | `COUNTERFACTUAL_PROBE` | `CAUSES` | — | S, P and O | +//! | 7..63 | not in the law | | | | +//! +//! `CAUSAL_IDENTIFICATION` only makes the attempt legal. Certifying `Causes` +//! stays the job of the certification obligations (D-GSO-7a); nothing here +//! writes the edge. +//! +//! The rules are compiled at build time into `[u64; 32]` (per code) and +//! `[u64; 8]` (per Pearl projection). At run time the facts disappear: +//! `eligible = STATE[raw5] & PEARL[pearl3]`. +//! +//! # Layers kept apart +//! +//! 1. facts: what the code means (the law's fact table); +//! 2. static affordances: the compiled tables; +//! 3. dynamic gates: not in this probe (D-GSO-AFF-1); +//! 4. preference: `eligible & preference`. It can only clear bits. +//! +//! # Not decided here +//! +//! - The codebook, the fact vocabulary and the recipe set are probe pins. +//! - Bits 59..63 have shipped split readings (`truth`/`topology` on 59..60, +//! `ReasoningBand` on 61..63, read separately by `band_reading`). A joint +//! 5-bit reading needs its own declaration there before it can be relied on; +//! this probe declares it locally. +//! - Dynamic gates, Shannon preference and executing a recipe are out of scope. +//! +//! Run: `cargo run -p cognitive-shader-driver --example affordance_measurement_probe` +//! Tests: `cargo test -p cognitive-shader-driver --example affordance_measurement_probe` + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::cell::Cell; + +use causal_edge::edge::CausalEdge64; +use causal_edge::pearl::CausalMask; +use causal_edge::PlasticityState; +use lance_graph_contract::band_reading::EdgeProvenance; + +// ── allocation counter (the recipe_quartet_probe pattern) ───────────────── + +/// Counts allocations on the current thread; test threads run in parallel. +struct CountingAlloc; + +thread_local! { + static ALLOCATIONS: Cell = const { Cell::new(0) }; +} + +// SAFETY: forwards every call to `System` unchanged; it only adds a counter. +unsafe impl GlobalAlloc for CountingAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let _ = ALLOCATIONS.try_with(|c| c.set(c.get() + 1)); + unsafe { System.alloc(layout) } + } + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + unsafe { System.dealloc(ptr, layout) } + } + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + let _ = ALLOCATIONS.try_with(|c| c.set(c.get() + 1)); + unsafe { System.realloc(ptr, layout, new_size) } + } +} + +#[global_allocator] +static GLOBAL: CountingAlloc = CountingAlloc; + +/// Run `f` and return its result with the allocations it made on this thread. +fn counting(f: impl FnOnce() -> T) -> (T, usize) { + let before = ALLOCATIONS.with(Cell::get); + let out = f(); + (out, ALLOCATIONS.with(Cell::get) - before) +} + +// ── facts (the EpistemicState5 reading) ──────────────────────────────────── + +const DIRECT: u32 = 1 << 0; +const INDIRECT: u32 = 1 << 1; +const INTERMEDIATE_PRESENT: u32 = 1 << 2; +const INTERMEDIATE_UNKNOWN: u32 = 1 << 3; +const INTERMEDIATE_KNOWN: u32 = 1 << 4; +const OBSERVED: u32 = 1 << 5; +const ASSOCIATED: u32 = 1 << 6; +const RELATED: u32 = 1 << 7; +const SUPPORTS: u32 = 1 << 8; +const CAUSES: u32 = 1 << 9; + +const FACT_NAMES: [&str; 10] = [ + "DIRECT", + "INDIRECT", + "INTERMEDIATE_PRESENT", + "INTERMEDIATE_UNKNOWN", + "INTERMEDIATE_KNOWN", + "OBSERVED", + "ASSOCIATED", + "RELATED", + "SUPPORTS", + "CAUSES", +]; + +const IND_UNKNOWN: u32 = INDIRECT | INTERMEDIATE_PRESENT | INTERMEDIATE_UNKNOWN; +const IND_KNOWN: u32 = INDIRECT | INTERMEDIATE_PRESENT | INTERMEDIATE_KNOWN; +const UP_TO_RELATED: u32 = ASSOCIATED | RELATED; +const UP_TO_SUPPORTS: u32 = UP_TO_RELATED | SUPPORTS; + +/// EpistemicState5 law: code → facts. Ten valid codes, deliberately not ordered +/// by strength (`Causes` sits at 1, `Open` at 0, `Related` at 20, …). +const EPI_LAW: [Option; 32] = { + let mut t = [None; 32]; + t[0] = Some(0); // Open + t[3] = Some(DIRECT | OBSERVED); + t[7] = Some(DIRECT | OBSERVED | ASSOCIATED); + t[12] = Some(DIRECT | ASSOCIATED); + t[20] = Some(DIRECT | UP_TO_RELATED); + t[25] = Some(IND_UNKNOWN | UP_TO_RELATED); + t[5] = Some(IND_KNOWN | UP_TO_RELATED); + t[30] = Some(IND_KNOWN | UP_TO_SUPPORTS); + t[1] = Some(DIRECT | UP_TO_SUPPORTS | CAUSES); + t[17] = Some(IND_KNOWN | UP_TO_SUPPORTS | CAUSES); + t +}; + +// ── the recipe law ────────────────────────────────────────────────────────── + +/// One recipe's static obligations. `pearl` names the planes that must +/// participate (S = 0b100, P = 0b010, O = 0b001). +#[derive(Clone, Copy)] +struct Rule { + active: bool, + requires: u32, + forbids: u32, + pearl: u8, +} + +const NOT_IN_LAW: Rule = Rule { + active: false, + requires: 0, + forbids: 0, + pearl: 0, +}; + +const fn rule(requires: u32, forbids: u32, pearl: u8) -> Rule { + Rule { + active: true, + requires, + forbids, + pearl, + } +} + +const HYDRATE_INTERMEDIATE: u8 = 0; +const MECHANISM_FOLD: u8 = 1; +const STRATIFY: u8 = 2; +const ROBUSTNESS_TEST: u8 = 3; +const CAUSAL_IDENTIFICATION: u8 = 4; +const OBSERVE_FOLD: u8 = 5; +const COUNTERFACTUAL_PROBE: u8 = 6; +const RECIPE_NAMES: [&str; 7] = [ + "HYDRATE_INTERMEDIATE", + "MECHANISM_FOLD", + "STRATIFY", + "ROBUSTNESS_TEST", + "CAUSAL_IDENTIFICATION", + "OBSERVE_FOLD", + "COUNTERFACTUAL_PROBE", +]; + +const fn law(stratify_forbids: u32) -> [Rule; 64] { + let mut t = [NOT_IN_LAW; 64]; + t[HYDRATE_INTERMEDIATE as usize] = rule(INDIRECT | INTERMEDIATE_UNKNOWN, INTERMEDIATE_KNOWN, 0); + t[MECHANISM_FOLD as usize] = rule(INDIRECT | INTERMEDIATE_KNOWN, 0, 0); + t[STRATIFY as usize] = rule(ASSOCIATED, stratify_forbids, 0); + t[ROBUSTNESS_TEST as usize] = rule(RELATED, 0, 0); + t[CAUSAL_IDENTIFICATION as usize] = rule(SUPPORTS, INTERMEDIATE_UNKNOWN | CAUSES, 0); + t[OBSERVE_FOLD as usize] = rule(0, 0, 0); + t[COUNTERFACTUAL_PROBE as usize] = rule(CAUSES, 0, 0b111); + t +} + +/// The recipe law in force. A generation names the whole declared reading: +/// the EpistemicState5 codebook and the recipe rules together. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum LawGen { + V1, + /// V2 forbids `STRATIFY` once `CAUSES` is certified. + V2, +} + +const LAW_V1: [Rule; 64] = law(0); +const LAW_V2: [Rule; 64] = law(CAUSES); + +/// Compiled static affordances for one law generation. +struct Tables { + /// Per EpistemicState5 code: recipes whose fact obligations hold. Invalid + /// codes have no entry here; they refuse before lookup. + state: [u64; 32], + /// Per Pearl projection: recipes whose plane obligations hold. + pearl: [u64; 8], +} + +const fn compile(law: &[Rule; 64]) -> Tables { + let mut state = [0u64; 32]; + let mut code = 0; + while code < 32 { + if let Some(facts) = EPI_LAW[code] { + let mut r = 0; + while r < 64 { + let rl = law[r]; + if rl.active && facts & rl.requires == rl.requires && facts & rl.forbids == 0 { + state[code] |= 1 << r; + } + r += 1; + } + } + code += 1; + } + let mut pearl = [0u64; 8]; + let mut p = 0; + while p < 8 { + let mut r = 0; + while r < 64 { + let rl = law[r]; + if rl.active && (p as u8) & rl.pearl == rl.pearl { + pearl[p] |= 1 << r; + } + r += 1; + } + p += 1; + } + Tables { state, pearl } +} + +static TABLES_V1: Tables = compile(&LAW_V1); +static TABLES_V2: Tables = compile(&LAW_V2); + +impl LawGen { + fn tables(self) -> &'static Tables { + match self { + LawGen::V1 => &TABLES_V1, + LawGen::V2 => &TABLES_V2, + } + } + fn rules(self) -> &'static [Rule; 64] { + match self { + LawGen::V1 => &LAW_V1, + LawGen::V2 => &LAW_V2, + } + } +} + +// ── the measurement ───────────────────────────────────────────────────────── + +/// Why an edge cannot be measured under this law. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Refusal { + /// The caller did not assert that bits 59..63 were written under the v2 + /// layout. On a v1 row they are old `temporal` bits: `temporal = 128` + /// sets bit 59 and would read as code 1 (`Causes`). Same rule as + /// `band_reading`: v1 and unknown provenance refuse. + Provenance(EdgeProvenance), + /// Bits 59..63 do not form a code this law declares. + Undeclared(u8), +} + +/// Only an asserted v2-stamped edge or a clean V3 register is readable. +fn admitted(provenance: EdgeProvenance) -> bool { + matches!( + provenance, + EdgeProvenance::V2Stamped | EdgeProvenance::V3Register + ) +} + +/// Bits 59..63 as one code, through the shipped accessors. +fn raw5(edge: CausalEdge64) -> u8 { + (edge.spare() << 2) | edge.truth_raw() +} + +/// `CausalEdge64 × RecipeLaw → EligibleRecipes`. Two lookups and one AND. +fn measure(law: LawGen, edge: CausalEdge64, provenance: EdgeProvenance) -> Result { + if !admitted(provenance) { + return Err(Refusal::Provenance(provenance)); + } + let code = raw5(edge); + if EPI_LAW[code as usize].is_none() { + return Err(Refusal::Undeclared(code)); + } + let t = law.tables(); + Ok(t.state[code as usize] & t.pearl[edge.causal_mask() as usize]) +} + +/// Preference may only remove legal recipes. +fn prefer(eligible: u64, preference: u64) -> u64 { + eligible & preference +} + +/// One deterministic ordinal from a population: the lowest set bit. +fn select(population: u64) -> Option { + (population != 0).then(|| population.trailing_zeros() as u8) +} + +// ── cold-path explanation (independent of the compiled tables) ────────────── + +/// Why a recipe is or is not legal. Diagnostics only; never on the hot path. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Verdict { + Eligible, + UntrustedProvenance, + Unreadable, + NotInLaw, + MissingFact(u32), + ForbiddenFact(u32), + PearlPlanesMissing(u8), +} + +/// Re-derive one recipe's legality from the rules, not from the tables. +fn explain(law: LawGen, edge: CausalEdge64, provenance: EdgeProvenance, recipe: u8) -> Verdict { + if !admitted(provenance) { + return Verdict::UntrustedProvenance; + } + let Some(facts) = EPI_LAW[raw5(edge) as usize] else { + return Verdict::Unreadable; + }; + let rl = law.rules()[recipe as usize]; + if !rl.active { + return Verdict::NotInLaw; + } + let missing = rl.requires & !facts; + if missing != 0 { + return Verdict::MissingFact(missing); + } + let forbidden = rl.forbids & facts; + if forbidden != 0 { + return Verdict::ForbiddenFact(forbidden); + } + let planes = rl.pearl & !(edge.causal_mask() as u8); + if planes != 0 { + return Verdict::PearlPlanesMissing(planes); + } + Verdict::Eligible +} + +// ── fixtures ──────────────────────────────────────────────────────────────── + +/// Measure an edge this probe stamped itself under the v2 layout. +fn stamped(law: LawGen, edge: CausalEdge64) -> Result { + measure(law, edge, EdgeProvenance::V2Stamped) +} + +/// Explain a recipe on an edge this probe stamped itself. +fn explain_stamped(law: LawGen, edge: CausalEdge64, recipe: u8) -> Verdict { + explain(law, edge, EdgeProvenance::V2Stamped, recipe) +} + +/// Stamp an EpistemicState5 code through the shipped writers. +fn with_code(edge: CausalEdge64, code: u8) -> CausalEdge64 { + use causal_edge::layout::TrustTexture; + edge.with_spare(code >> 2) + .with_truth(TrustTexture::from_bits_2(code & 0b11)) +} + +/// An edge with every irrelevant field non-zero. +fn busy_edge(pearl: CausalMask) -> CausalEdge64 { + CausalEdge64::pack_v2( + 0xA5, + 0x3C, + 0x7E, + 200, + 150, + pearl, + 0b101, + PlasticityState::from_bits(0b011), + ) + .with_inference_mantissa(-6) + .with_w_slot(41) +} + +fn fact_names(mask: u32) -> String { + (0..10) + .filter(|i| mask & (1 << i) != 0) + .map(|i| FACT_NAMES[i]) + .collect::>() + .join("|") +} + +fn main() { + println!("EligibleRecipes per EpistemicState5 code (law V1, Pearl SPO):"); + for code in 0u8..32 { + let Some(facts) = EPI_LAW[code as usize] else { + continue; + }; + let e = stamped(LawGen::V1, with_code(busy_edge(CausalMask::SPO), code)).unwrap(); + let names: Vec<&str> = (0..7) + .filter(|r| e & (1 << r) != 0) + .map(|r| RECIPE_NAMES[r]) + .collect(); + println!( + " {code:>2} {:<70} -> {:#04x} {:?}", + fact_names(facts), + e, + names + ); + } + let unreadable = (0u8..32).filter(|c| EPI_LAW[*c as usize].is_none()).count(); + println!(" {unreadable} codes refuse (not declared by the law)"); + let edge = with_code(busy_edge(CausalMask::SO), 25); + let (e, allocs) = counting(|| stamped(LawGen::V1, edge)); + let eligible = e.unwrap_or(0); + println!( + "Indirect × IntermediateUnknown × Related under SO: {eligible:#04x}, {allocs} allocations" + ); + for r in 0..7u8 { + if eligible & 1 << r == 0 { + println!( + " refused {:<22} {:?}", + RECIPE_NAMES[r as usize], + explain_stamped(LawGen::V1, edge, r) + ); + } + } + let cheap = 1 << OBSERVE_FOLD | 1 << STRATIFY | 1 << COUNTERFACTUAL_PROBE; + let preferred = prefer(eligible, cheap); + println!( + " preference {cheap:#04x} -> {preferred:#04x}, first recipe {:?} (COUNTERFACTUAL_PROBE stays illegal)", + select(preferred).map(|r| RECIPE_NAMES[r as usize]) + ); + let causes = with_code(busy_edge(CausalMask::SPO), 17); + println!( + "Causes code under V1 {:#04x}, under V2 {:#04x} (V2 forbids STRATIFY once CAUSES holds)", + stamped(LawGen::V1, causes).unwrap(), + stamped(LawGen::V2, causes).unwrap() + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + const ALL_PEARL: [CausalMask; 8] = [ + CausalMask::None, + CausalMask::O, + CausalMask::P, + CausalMask::PO, + CausalMask::S, + CausalMask::SO, + CausalMask::SP, + CausalMask::SPO, + ]; + const ACTIVE: u64 = 0b111_1111; + + fn valid_codes() -> impl Iterator { + (0u8..32).filter(|c| EPI_LAW[*c as usize].is_some()) + } + + /// F1 + F8: same bits and same law generation give the same population, on + /// every valid code and projection, and the generation itself matters. + #[test] + fn f1_same_bits_and_generation_give_the_same_population() { + let mut generations_differ = false; + for law in [LawGen::V1, LawGen::V2] { + for code in valid_codes() { + for p in ALL_PEARL { + let e = with_code(busy_edge(p), code); + let a = stamped(law, e); + assert_eq!(a, stamped(law, e)); + assert_eq!(a, stamped(law, CausalEdge64(e.0)), "replay from raw bits"); + if law == LawGen::V1 && a != stamped(LawGen::V2, e) { + generations_differ = true; + } + } + } + } + assert!(generations_differ, "V2 must change at least one population"); + } + + /// F2: changing a relevant factor flips the affected bit. + #[test] + fn f2_a_relevant_factor_flips_the_affected_bit() { + let unknown = stamped(LawGen::V1, with_code(busy_edge(CausalMask::SO), 25)).unwrap(); + let known = stamped(LawGen::V1, with_code(busy_edge(CausalMask::SO), 5)).unwrap(); + assert_ne!(unknown & 1 << HYDRATE_INTERMEDIATE, 0); + assert_eq!(unknown & 1 << MECHANISM_FOLD, 0); + assert_eq!(known & 1 << HYDRATE_INTERMEDIATE, 0); + assert_ne!(known & 1 << MECHANISM_FOLD, 0); + + let causes_so = stamped(LawGen::V1, with_code(busy_edge(CausalMask::SO), 17)).unwrap(); + let causes_spo = stamped(LawGen::V1, with_code(busy_edge(CausalMask::SPO), 17)).unwrap(); + assert_eq!(causes_so & 1 << COUNTERFACTUAL_PROBE, 0); + assert_ne!(causes_spo & 1 << COUNTERFACTUAL_PROBE, 0); + assert_eq!( + causes_so ^ causes_spo, + 1 << COUNTERFACTUAL_PROBE, + "only that bit moves" + ); + } + + /// F3: every field no recipe reads leaves every bit alone; the Pearl + /// projection moves only the one recipe that reads it. + #[test] + fn f3_irrelevant_fields_change_no_bit() { + for code in valid_codes() { + let base = with_code(busy_edge(CausalMask::SPO), code); + let want = stamped(LawGen::V1, base).unwrap(); + let same = + |e: CausalEdge64| assert_eq!(stamped(LawGen::V1, e).unwrap(), want, "code {code}"); + for v in 0..=255u8 { + let (mut s, mut p, mut o) = (base, base, base); + s.set_s_idx(v); + p.set_p_idx(v); + o.set_o_idx(v); + same(s); + same(p); + same(o); + let fc = CausalEdge64::pack_v2( + 0xA5, + 0x3C, + 0x7E, + v, + 255 - v, + CausalMask::SPO, + 0b101, + PlasticityState::from_bits(0b011), + ); + same(with_code( + fc.with_inference_mantissa(-6).with_w_slot(41), + code, + )); + } + for v in 0..8u8 { + let mut d = base; + d.set_direction(v); + same(d); + let mut pl = base; + pl.set_plasticity(PlasticityState::from_bits(v)); + same(pl); + } + for m in -8i8..=7 { + same(base.with_inference_mantissa(m)); + } + for w in 0..64u8 { + same(base.with_w_slot(w)); + } + for p in ALL_PEARL { + let moved = want ^ stamped(LawGen::V1, with_code(busy_edge(p), code)).unwrap(); + assert_eq!( + moved & !(1 << COUNTERFACTUAL_PROBE), + 0, + "Pearl moved another recipe" + ); + } + } + } + + /// F4 + F5: the compiled tables agree, bit for bit, with an independent + /// re-derivation from the rules; every refused recipe names its failed + /// obligation; and every active recipe both fires and stays silent. + #[test] + fn f4_f5_every_bit_matches_its_named_obligation() { + for law in [LawGen::V1, LawGen::V2] { + let (mut fired, mut silent) = (0u64, 0u64); + for code in valid_codes() { + for p in ALL_PEARL { + let e = with_code(busy_edge(p), code); + let pop = stamped(law, e).unwrap(); + for r in 0..64u8 { + let v = explain_stamped(law, e, r); + if pop & 1 << r != 0 { + assert_eq!(v, Verdict::Eligible, "{law:?} code {code} recipe {r}"); + fired |= 1 << r; + } else { + assert!( + matches!( + v, + Verdict::NotInLaw + | Verdict::MissingFact(_) + | Verdict::ForbiddenFact(_) + | Verdict::PearlPlanesMissing(_) + ), + "{law:?} code {code} recipe {r}: refused without a reason ({v:?})" + ); + silent |= 1 << r; + } + } + } + } + assert_eq!( + fired, ACTIVE, + "{law:?}: every recipe in the law fires somewhere" + ); + assert_eq!( + silent & ACTIVE & !(1 << OBSERVE_FOLD), + ACTIVE & !(1 << OBSERVE_FOLD) + ); + assert_eq!(fired & !ACTIVE, 0, "recipes outside the law never fire"); + } + } + + /// F6 + F7: preference only removes bits, never adds one, and dropping it + /// leaves legality unchanged. Selection stays inside the legal set. + #[test] + fn f6_f7_preference_can_only_remove() { + let mut lcg: u64 = 0x9E37_79B9_7F4A_7C15; + for code in valid_codes() { + for p in ALL_PEARL { + let eligible = stamped(LawGen::V1, with_code(busy_edge(p), code)).unwrap(); + assert_eq!( + prefer(eligible, u64::MAX), + eligible, + "no preference = legality" + ); + for _ in 0..64 { + lcg = lcg.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1); + let preferred = prefer(eligible, lcg); + assert_eq!(preferred & !eligible, 0, "preference set an illegal bit"); + if let Some(r) = select(preferred) { + assert_ne!(eligible & 1 << r, 0); + } + } + } + } + } + + /// The raw code is not a strength: code 1 (`Causes`) carries strictly more + /// facts than code 20 (`Related`), so comparing codes would admit wrongly. + #[test] + fn raw_code_order_is_not_epistemic_order() { + let (causes, related) = (EPI_LAW[1].unwrap(), EPI_LAW[20].unwrap()); + assert_eq!(causes & related, related); + assert_ne!(causes, related); + // `ROBUSTNESS_TEST` is legal at code 1 (Causes) and refused at code 12 + // (Associated only), although 12 > 1. + let at = |c: u8| stamped(LawGen::V1, with_code(busy_edge(CausalMask::SPO), c)).unwrap(); + assert_ne!(at(1) & 1 << ROBUSTNESS_TEST, 0); + assert_eq!(at(12) & 1 << ROBUSTNESS_TEST, 0); + } + + /// Undeclared codes refuse instead of reading as a weak state, including a + /// code produced by a writer that touches only half of bits 59..63. + #[test] + fn undeclared_codes_refuse_including_half_field_writes() { + for code in (0u8..32).filter(|c| EPI_LAW[*c as usize].is_none()) { + let e = with_code(busy_edge(CausalMask::SPO), code); + assert_eq!(stamped(LawGen::V1, e), Err(Refusal::Undeclared(code))); + assert_eq!( + explain_stamped(LawGen::V1, e, OBSERVE_FOLD), + Verdict::Unreadable + ); + } + // 17 = 0b100_01; rewriting only bits 59..60 through the shipped + // topology writer leaves bits 61..63 alone and yields 0b100_10 = 18. + let causes = with_code(busy_edge(CausalMask::SPO), 17); + let half = causes.with_topology(causal_edge::layout::CausalTopology::from_bits_2(0b10)); + assert_eq!(raw5(half), 18); + assert_eq!(stamped(LawGen::V1, half), Err(Refusal::Undeclared(18))); + } + + /// Bits 59..63 are only read under an asserted v2/V3 provenance. A v1 row + /// whose old `temporal` was 128 has bit 59 set, which would read as code 1 + /// (`Causes`) and make `COUNTERFACTUAL_PROBE` eligible; it refuses instead. + #[test] + fn v1_and_unknown_provenance_refuse() { + let v1_row = CausalEdge64((128u64 << 52) | (CausalMask::SPO as u64) << 40); + assert_eq!( + raw5(v1_row), + 1, + "the old temporal bit lands on a declared code" + ); + for prov in [EdgeProvenance::V1Legacy, EdgeProvenance::Unknown] { + assert_eq!( + measure(LawGen::V1, v1_row, prov), + Err(Refusal::Provenance(prov)) + ); + assert_eq!( + explain(LawGen::V1, v1_row, prov, COUNTERFACTUAL_PROBE), + Verdict::UntrustedProvenance + ); + } + // Can fire: the same bits under an asserted v2 stamp do measure. + for prov in [EdgeProvenance::V2Stamped, EdgeProvenance::V3Register] { + let e = measure(LawGen::V1, v1_row, prov).unwrap(); + assert_ne!(e & 1 << COUNTERFACTUAL_PROBE, 0); + } + } + + /// F9: the measurement allocates nothing, over every valid code and + /// projection. The output is a `u64`; there is no other object. + #[test] + fn f9_the_measurement_allocates_nothing() { + let edges: [CausalEdge64; 80] = { + let mut a = [CausalEdge64::ZERO; 80]; + let mut i = 0; + for code in valid_codes() { + for p in ALL_PEARL { + a[i] = with_code(busy_edge(p), code); + i += 1; + } + } + assert_eq!(i, 80); + a + }; + let (acc, allocs) = counting(|| { + edges.iter().fold(0u64, |acc, e| { + acc ^ stamped(LawGen::V1, *e) + .unwrap() + .rotate_left(acc as u32 & 63) + }) + }); + assert_eq!(allocs, 0); + assert_ne!(acc, 0); + } +}