diff --git a/.claude/board/entries/2026-10-09-d-rpf-conf-0-confidence-surface.md b/.claude/board/entries/2026-10-09-d-rpf-conf-0-confidence-surface.md new file mode 100644 index 000000000..07b6bd037 --- /dev/null +++ b/.claude/board/entries/2026-10-09-d-rpf-conf-0-confidence-surface.md @@ -0,0 +1,201 @@ +# 2026-10-09 — D-RPF-CONF-0: the confidence revision surface, measured before any policy + +**Status:** MEASURED. Tests only; no production semantics changed, no policy +chosen, no quantizer unified, no contract declaration added (T5 stays blocked). +**Harnesses:** `crates/causal-edge/tests/d_rpf_conf_0.rs` (T1, singular, T3, +R1, NarsTables invariance) and `crates/lance-graph-planner/tests/d_rpf_conf_0_r3.rs` +(R3). Both run in under a second in release. +**Parent:** `2026-10-08-coresearch-ce64-bf16-for-thinking.md` (T1, T3, R1, R3, +G4 items of its TO-DO / RESEARCH / GAP list). + +Rule held throughout: defined somewhere ≠ live ≠ numerically distinct. + +## T1 — Rust confidence surface vs the exact integer form + +Reference: `c = 255·N/D`, `N = c1(255−c2) + c2(255−c1)`, `D = 255² − c1·c2`, +rounded half up, computed in integers. 65,535 ordinary cells compared (the +singular cell excluded, see below). + +- **3 deviations, all by 1 code:** (45,45) Rust 76 vs exact 77, and + (146,148)/(148,146) Rust 187 vs exact 186. +- The numpy hypothesis `{(45,45),(85,165),(165,85)}` was right on one cell of + three. (85,165) is an exact tie that Rust rounds correctly; (146,148) is a + near-tie (exact 186.499988) that f32 rounds the wrong way. +- **8 exact ties** on the grid, pinned: (3,75), (45,45), (75,3), (85,85), + (85,165), (153,225), (165,85), (225,153). One of them, (45,45), is a deviation. +- Monotonicity violations 0; symmetry violations 0. +- Confidence output does not depend on frequency (all 256 f pairs tested). +- f side, sampled on c ∈ {1,64,128,200,254}²: 6,932 deviations of 1 code. All + 6,932 sit on exact .5 ties (163,840 ties in the sample). There is no + deviation away from a tie. + +Pinned in `T1_DEVIATIONS` / `T1_TIES`. The falsifier flips one reference cell +and the deviation set changes. + +## Singular cell (255,255) + +Kept out of the ordinary domain. `w = f32::MAX` on both sides, `ws = inf`, the +pooled frequency is `inf/inf = NaN` and the confidence is `inf/inf = NaN`; both +encode to 0. **Measured: (255,255) → (0,0) for every f pair.** The test is named +`t1_singular_cell_is_recorded_not_endorsed`: this is what the code does, not a +value anyone chose. + +## T3 — zero-weight identity and null operands + +Over all (f,c), with the other operand g ∈ {0,128,255}, in both orders: + +- **255 drift cells**, all with c = 0 and f ≠ 128. Each becomes (128, 0). + max delta 128. +- Neither g nor operand order leaks into the result. +- There is no drift for c > 0: revision with a zero-weight partner returns the + input exactly. + +So the drift is semantic, not arithmetic: no evidence on either side resolves +to (0.5, 0), and the original frequency is dropped. `forward` with both weights +zero faults with `IsaFault::Unsupported { mantissa: 0 }`. + +## R1 — c=255 policy candidates, side by side (no choice made) + +Columns: NaN on the c surface / NaN on the f boundary (c ∈ 253..255) / +monotonicity violations (singular included) / symmetry / c cells off the exact +form / their max error / cells changed vs status quo / the singular cell / +one-sided f `c1=255, c2<255`: off exact, max error, changed. + +| policy | NaN c | NaN f | mono | sym | off exact | max | changed | (255,255) | 1-sided off | max | changed | +|---|---|---|---|---|---|---|---|---|---|---|---| +| status quo | 1 | 65,536 | 1 | 0 | 3 | 1 | 0 | (0,0) | 0 | 0 | 0 | +| cap before weight (0.9999) | 0 | 0 | 0 | 0 | 3 | 1 | 1 | (128,255) | 101,900 | 6 | 101,900 | +| cap input at 254 | 0 | 0 | 0 | 0 | 513 | 1 | 511 | (128,254) | 194,671 | 128 | 194,671 | +| saturate at 254 | 0 | 0 | 0 | 0 | 513 | 1 | 511 | (128,254) | 194,671 | 128 | 194,671 | +| singular only | 0 | 0 | 0 | 0 | 3 | 1 | 1 | (128,255) | 0 | 0 | 0 | + +Reading, without a recommendation: +- The status quo is **exact on the one-sided boundary** (one dogmatic premise + wins outright). Every cap or saturate candidate breaks that: cap-before-weight + by up to 6 codes on 101,900 cells, the 254 variants by up to 128 codes on + 194,671 cells. +- "Singular only" (status quo except (255,255) → (mean f, 255)) removes all NaN + and the one monotonicity violation and changes exactly one cell. That makes + G1 a single, explicit decision about one cell, not a policy over a surface. +- The f-boundary NaN count (65,536) is the f surface at (255,255) — every f + pair there is NaN. + +The candidates are replicas of the production f32 path; `r1_replica_of_status_quo_is_faithful` +checks the status-quo replica against `CausalEdge64::revision` on every cell. + +## G4 — census of revision laws + +Closed search: `fn (revise|revision)\b` over `crates/**/*.rs`. **N = 24 +definitions in 20 files.** (The earlier "37" counted files containing the words, +not definitions.) Every call site used for a live/not-live claim was read. + +- **9 homonyms, not NARS revision:** Gadamer interpretive revision + (`contract/revision.rs:308`, `:327` and three example wrappers), kanban column + routing (`kanban.rs:231`), a counterfactual trait declaration and its test + stub, `planner/pearl.rs:373`. +- **Delegations, not new laws:** arigraph orchestrator → `spo::TruthValue`; + `CausalEdge64::revision` → `isa::truth::revision`; + `GrammarStyleAwareness::revise` → `revise_truth`. +- **Distinct laws reaching production in some crate:** A (ISA f32), B(L) + (NarsTables), C (ndarray evidence split), D (planner `TruthValue`), E + (contract `NarsTruth`, cap 0.99), F (`spo::TruthValue`, `+EPS`), G (holograph + u16), J (`PathTruth`, raw weight argument), LIN (c used as weight; three + copies). H (`learning`) is private behind `wip` and cannot be called. +- **Planner path, read in code:** `NarsEngine::new` builds + `NarsTables::build(1)` (`cache/nars_engine.rs:451`). `replay_step` + (`chain_replay.rs:196-207`) calls `tables.revise` and **overwrites** the + truth `forward` computed with law A. That chain is reached through + `replay_chain` ← `counterfactual_replay` ← `pearl::reason`, all public + production functions. Every in-tree caller passes `build(1)`, except + `tests/chain_confidence.rs` (16). So on the replay/counterfactual path the + emitted truth is **B(1)**. +- Not in the census, found while verifying it: `contract/high_heel.rs:240` + `revise_truth` is another LIN copy with a 0.99 cap. The census sweep of + differently named pooling functions was incidental, not exhaustive, so this + is unsurprising; it is recorded so the count is not read as closed. + +**NarsTables `build(1)` is confidence-inert, measured:** one distinct output per +f pair over the whole c grid; c is always 170 and f is the mean. `build(16)` +varies (92 distinct c outputs at f = (128,128)). + +## R3 — disagreement matrix over the distinct live laws only + +Laws: A, B1, B16, C, D, E, LIN, each through its real entry point. F and G are +off the planner path and unreachable from it; J needs a declared c→weight rule +that no caller fixes; H is not callable. f32 laws are quantized with the +`set_*_u8` rule; A and B keep their native encoding. + +Domain: f ∈ {0,51,128,204,255}², c over the full 256×256 grid (1,638,400 cells). +"Broad" = neither c in {0, 254, 255}. + +Distinct output c at f = (128,128): A 256, B1 **1**, B16 92, C 256, D 256, E 253, LIN 171. + +| pair | cells differing | of those broad | broad max dF | broad max dC | mean dC | +|---|---|---|---|---|---| +| A–D | 0 | 0 | 0 | 0 | 0 | +| A–C | 391 | 130 | 1 | 1 | 0.004 | +| C–D | 391 | 130 | 1 | 1 | 0.004 | +| A–E | 38,897 | 13,327 | 15 | 2 | 0.051 | +| C–E | 39,025 | 13,455 | 15 | 2 | 0.047 | +| A–B16 | 1,585,273 | 1,547,346 | 113 | 14 | 3.27 | +| A–B1 | 1,636,550 | 1,598,385 | 128 | 168 | 47.6 | +| A–LIN | 1,636,238 | 1,598,618 | 107 | 126 | 59.6 | +| B1–B16 | 1,630,720 | 1,592,545 | 128 | 155 | 47.3 | +| E–LIN | 1,636,218 | 1,598,618 | 105 | 125 | 59.6 | + +(Full 21-pair matrix pinned in `MATRIX`.) + +The falsifier: a pair whose broad cells never differ by more than one code is a +cosmetic difference. Measured, and pinned from the measurement: + +- **Cosmetic: A–C, A–D, C–D.** A and D are byte-identical on this domain (D's + `MAX` weight is reached only at c = 1.0, which on the u8 grid is code 255, + the same single code as A's `c ≥ 0.999`). C differs from both only at the + boundary and by one code in the broad domain. Unifying A, C and D would be + cosmetic in the broad domain; their real difference is the c = 255 policy. +- **E differs only through its 0.99 cap:** broad dC ≤ 2; the dF of 15 comes + from c = 253, which the cap moves from w ≈ 126 to 99. +- **Not cosmetic: B1, B16, LIN** against everything. B1 differs from A on 99.9% + of cells by up to 168 confidence codes. **B1 is the law the replay and + counterfactual path emits.** + +So M = 7 laws compared, which collapses to 4 numerical behaviours on the u8 +grid away from the boundary: {A, C, D} (E is this one plus a cap), B16, B1, LIN. + +## Disable runs (all red as required) + +- T1: exact rounding changed to truncation → pins fail. +- R1: "singular only" singular c changed to 254 → table pin fails. +- NarsTables: `build(1)` changed to `build(2)` → invariance pin fails. +- T3: identity measured with `rev(f,c,0,1)` → drift pin fails. +- R3: law D replaced by a constant-c bucket → distinct-c, matrix and cosmetic + pins fail. +- R3: the broad/boundary split disabled → matrix pin fails. + +One test was vacuous and was fixed: the first `r3_cosmetic_pairs_are_pinned` +filtered the pinned `MATRIX` constant, so it passed under the law-D disable. It +now filters the measured matrix and fails under that disable. + +## What this does not decide + +- G1 (the (255,255) cell) and G2 (whether replay should emit B1) are operator + decisions. This entry gives each one its measured cost. +- T5 (rounding/saturation declarations in `isa::contracts`) stays blocked. +- No law is declared canonical. + +``` +STATUS: measured | OUTCOME: T1 3 one-code f32 deviations; (255,255)->(0,0); +T3 drift only at c=0; R1 table (singular-only changes 1 cell, caps change +up to 194,671); G4 24 defs -> 7 compared laws -> 4 broad behaviours; +replay path emits confidence-inert B(1) | OPEN: G1, G2, T5; J needs a c->w +rule; F/G unmeasured on the u8 grid +``` + +MIRROR +- BLIND SPOT: R3 samples five f values; the f side of the matrix is not + exhaustive, and the "broad" boundary at {0,254,255} is a chosen cut. +- BIAS CHECK: did the numpy prediction anchor the T1 search? The Rust run was + compared against the integer form, not against the prediction, and the + prediction was wrong on two of three cells. +- STILL OPEN: whether B1 on the replay path was intended (a fast path) or an + accident; nothing in the code says which. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 54eb2436f..b65fce9a6 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,10 +25,11 @@ 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. -279 entries, 2026-08-06 .. 2026-10-08. +280 entries, 2026-08-06 .. 2026-10-09. | date | entry id | finding | file | |---|---|---|---| +| 2026-10-09 | `D-RPF-CONF-0` | | [2026-10-09-d-rpf-conf-0-confidence-surface.md](2026-10-09-d-rpf-conf-0-confidence-surface.md) | | 2026-10-08 | `register128-signed-readings` | | [2026-10-08-register128-signed-readings.md](2026-10-08-register128-signed-readings.md) | | 2026-10-08 | `D-ART-2` | | [2026-10-08-recipe-probe-phaselut-foldcost.md](2026-10-08-recipe-probe-phaselut-foldcost.md) | | 2026-10-08 | `rbac-nested-scope-path` | | [2026-10-08-rbac-nested-scope-path.md](2026-10-08-rbac-nested-scope-path.md) | diff --git a/crates/causal-edge/tests/d_rpf_conf_0.rs b/crates/causal-edge/tests/d_rpf_conf_0.rs new file mode 100644 index 000000000..e9efc3b06 --- /dev/null +++ b/crates/causal-edge/tests/d_rpf_conf_0.rs @@ -0,0 +1,721 @@ +//! D-RPF-CONF-0 — the exhaustive confidence surface of CE64 revision. +//! +//! Measurement only. Production semantics are not changed here and no +//! confidence policy is chosen; the policy candidates below are probe +//! variants that live in this file. +//! +//! - **T1**: `CausalEdge64::revision` over all 65,536 `(c1, c2)` against the +//! exact integer form. With integer codes `c ∈ 0..=255` and +//! `w = c / (255 − c)`, the pooled confidence is +//! `c_out·255 = 255·N/D` with `N = c1(255−c2) + c2(255−c1)` and +//! `D = 255² − c1·c2`. It is defined on 65,535 cells; `(255, 255)` gives +//! `0/0` and is the singular cell, kept out of the ordinary comparison. +//! - **T3**: revision against a zero-weight operand (confidence code 0), +//! over every representable `(f, c)`, in both operand orders. +//! - **R1**: four c=255 policy candidates measured side by side. +//! - **NarsTables**: whether the table path's output depends on confidence. +//! +//! Every pinned set below was discovered by running this file against the +//! Rust code, then written down. The numpy model that preceded it is not the +//! source of any constant here. +#![cfg(feature = "causal-edge-v2-layout")] + +use std::collections::BTreeSet; + +use causal_edge::tables::{unpack_c, unpack_f, NarsTables}; +use causal_edge::{CausalEdge64, IsaFault}; + +// ─── the production path ──────────────────────────────────────────────── + +fn edge(f: u8, c: u8) -> CausalEdge64 { + let mut e = CausalEdge64(0); + e.set_frequency_u8(f); + e.set_confidence_u8(c); + e +} + +/// `(f, c)` codes of `edge(f1, c1).revision(edge(f2, c2))`. +fn prod(f1: u8, c1: u8, f2: u8, c2: u8) -> (u8, u8) { + let r = edge(f1, c1).revision(edge(f2, c2)); + (r.frequency_u8(), r.confidence_u8()) +} + +// ─── the exact integer reference ──────────────────────────────────────── + +/// The exact pooled confidence code, rounded half away from zero (the +/// production rounding of a non-negative value), and whether the exact value +/// sits on a `.5` tie. `None` at `(255, 255)`, where `N = D = 0`. +fn exact_c(c1: u8, c2: u8) -> Option<(u8, bool)> { + let (c1, c2) = (u64::from(c1), u64::from(c2)); + let n = c1 * (255 - c2) + c2 * (255 - c1); + let d = 255 * 255 - c1 * c2; + if d == 0 { + return None; + } + let code = (2 * 255 * n + d) / (2 * d); + let tie = (2 * 255 * n) % (2 * d) == d; + Some((code as u8, tie)) +} + +/// The exact pooled frequency code, and whether it is a `.5` tie. `None` +/// when neither side carries evidence (`N = 0`), which includes `(255, 255)`. +fn exact_f(f1: u8, c1: u8, f2: u8, c2: u8) -> Option<(u8, bool)> { + let (f1, c1, f2, c2) = (u64::from(f1), u64::from(c1), u64::from(f2), u64::from(c2)); + let a = c1 * (255 - c2); + let b = c2 * (255 - c1); + let n = a + b; + if n == 0 { + return None; + } + let num = f1 * a + f2 * b; + let code = (2 * num + n) / (2 * n); + let tie = (2 * num) % (2 * n) == n; + Some((code as u8, tie)) +} + +// ─── T1 ───────────────────────────────────────────────────────────────── + +/// One cell `(c1, c2, got, want)` where a confidence surface disagrees with +/// its reference. +type Dev = (u8, u8, u8, u8); + +#[derive(Debug, PartialEq, Eq)] +struct Surface { + compared: usize, + deviations: Vec, + max_delta: u8, + ties: Vec<(u8, u8)>, + monotonicity_violations: Vec<(u8, u8)>, + symmetry_violations: usize, + singular: (u8, u8), +} + +/// Measures a confidence surface `c(c1, c2)` against a reference. The +/// surface is evaluated at frequency 128 on both sides; `t1_confidence_*` +/// checks that it does not depend on frequency. +fn measure_c( + surface: impl Fn(u8, u8) -> u8, + reference: impl Fn(u8, u8) -> Option<(u8, bool)>, +) -> Surface { + let mut grid = vec![[0u8; 256]; 256]; + for c1 in 0..=255u8 { + for c2 in 0..=255u8 { + grid[c1 as usize][c2 as usize] = surface(c1, c2); + } + } + let mut s = Surface { + compared: 0, + deviations: Vec::new(), + max_delta: 0, + ties: Vec::new(), + monotonicity_violations: Vec::new(), + symmetry_violations: 0, + singular: (0, 0), + }; + for c1 in 0..=255u8 { + for c2 in 0..=255u8 { + let got = grid[c1 as usize][c2 as usize]; + if grid[c2 as usize][c1 as usize] != got { + s.symmetry_violations += 1; + } + // Non-decreasing in c1 for fixed c2: more evidence never lowers + // the pooled confidence. + // The singular cell is kept out: it has no ordinary neighbour + // relation and is reported on its own. + if c1 > 0 + && reference(c1, c2).is_some() + && reference(c1 - 1, c2).is_some() + && got < grid[c1 as usize - 1][c2 as usize] + { + s.monotonicity_violations.push((c1, c2)); + } + match reference(c1, c2) { + None => s.singular = (c1, c2), + Some((want, tie)) => { + s.compared += 1; + if tie { + s.ties.push((c1, c2)); + } + if got != want { + s.deviations.push((c1, c2, got, want)); + s.max_delta = s.max_delta.max(got.abs_diff(want)); + } + } + } + } + } + s +} + +/// What the current Rust revision does, discovered by running `t1_report` +/// and then written down. +/// +/// The numpy f32 model that preceded this file predicted +/// `{(45,45), (85,165), (165,85)}`. Rust disagrees on two of the three: +/// `(85,165)` is an exact tie that Rust rounds correctly, and +/// `(146,148)`/`(148,146)` are not ties at all. Their exact value is +/// `8097270/43417 ≈ 186.499988`; the f32 path lands just above `.5` and rounds +/// up. `(45,45)` is an exact tie (`76.5`) that f32 lands just below. +const T1_DEVIATIONS: &[Dev] = &[(45, 45, 76, 77), (146, 148, 187, 186), (148, 146, 187, 186)]; + +/// The ordinary cells whose exact value is a `.5` tie. +const T1_TIES: &[(u8, u8)] = &[ + (3, 75), + (45, 45), + (75, 3), + (85, 85), + (85, 165), + (153, 225), + (165, 85), + (225, 153), +]; + +fn prod_c(c1: u8, c2: u8) -> u8 { + prod(128, c1, 128, c2).1 +} + +#[test] +fn t1_report() { + let s = measure_c(prod_c, exact_c); + eprintln!("T1 ordinary cells compared: {}", s.compared); + eprintln!("T1 deviations: {:?}", s.deviations); + eprintln!("T1 max delta: {}", s.max_delta); + eprintln!("T1 ties: {:?}", s.ties); + eprintln!( + "T1 monotonicity violations: {:?}", + s.monotonicity_violations + ); + eprintln!("T1 symmetry violations: {}", s.symmetry_violations); + let (c1, c2) = s.singular; + eprintln!( + "T1 singular ({c1},{c2}): current -> {:?}", + prod(128, c1, 128, c2) + ); +} + +#[test] +fn t1_confidence_surface_is_pinned() { + let s = measure_c(prod_c, exact_c); + assert_eq!(s.compared, 65_535, "ordinary domain"); + assert_eq!(s.singular, (255, 255), "the one undefined cell"); + assert_eq!(s.deviations, T1_DEVIATIONS); + assert_eq!(s.ties, T1_TIES); + assert!(s.monotonicity_violations.is_empty()); + assert_eq!(s.symmetry_violations, 0); +} + +#[test] +fn t1_confidence_does_not_depend_on_frequency() { + // `ws` never reads f, so c(c1, c2) must be the same for every frequency + // pair. A probe that silently depended on f = 128 would hide a defect. + for &(f1, f2) in &[(0u8, 0u8), (255, 255), (0, 255), (77, 200)] { + for c1 in 0..=255u8 { + for c2 in 0..=255u8 { + if (c1, c2) == (255, 255) { + continue; // NaN path: f and c both collapse; see t1_singular + } + assert_eq!( + prod(f1, c1, f2, c2).1, + prod_c(c1, c2), + "({f1},{c1},{f2},{c2})" + ); + } + } + } +} + +#[test] +fn t1_singular_cell_is_recorded_not_endorsed() { + // Current behaviour only: both operands at 255 pool two f32::MAX weights, + // overflow to infinity, compute inf/inf = NaN, and the saturating cast + // stores 0 for both fields. That 0 is not claimed to be correct. + for &(f1, f2) in &[(0u8, 0u8), (128, 128), (255, 255), (10, 250)] { + assert_eq!(prod(f1, 255, f2, 255), (0, 0), "f1={f1} f2={f2}"); + } + assert_eq!(exact_c(255, 255), None); + assert_eq!(exact_f(0, 255, 0, 255), None); +} + +#[test] +fn t1_falsifier_a_perturbed_reference_changes_the_set() { + // Anti-vacuity: if the comparison were blind (e.g. both sides computed by + // the same function), corrupting the reference would not move the result. + let perturbed = |c1: u8, c2: u8| { + exact_c(c1, c2).map(|(v, t)| { + if (c1, c2) == (100, 37) { + (v ^ 1, t) + } else { + (v, t) + } + }) + }; + let s = measure_c(prod_c, perturbed); + assert_ne!(s.deviations, T1_DEVIATIONS); + assert!(s.deviations.iter().any(|d| (d.0, d.1) == (100, 37))); +} + +#[test] +fn t1_frequency_side_against_exact_on_a_confidence_sample() { + // The full f-side is 2^32 cells; this samples the confidence plane and + // takes every (f1, f2). Reported, then pinned. + let cs = [1u8, 64, 128, 200, 254]; + let mut dev = 0usize; + let mut ties = 0usize; + let mut tie_dev = 0usize; + let mut max_delta = 0u8; + let mut compared = 0usize; + for &c1 in &cs { + for &c2 in &cs { + for f1 in 0..=255u8 { + for f2 in 0..=255u8 { + let (want, tie) = exact_f(f1, c1, f2, c2).expect("evidence on both sides"); + let got = prod(f1, c1, f2, c2).0; + compared += 1; + ties += usize::from(tie); + if got != want { + dev += 1; + tie_dev += usize::from(tie); + max_delta = max_delta.max(got.abs_diff(want)); + } + } + } + } + } + eprintln!( + "T1-f compared {compared} deviations {dev} (of which ties {tie_dev}) ties {ties} max delta {max_delta}" + ); + assert_eq!(compared, 25 * 65_536); + // Every f-side deviation on this sample is an exact .5 tie that f32 lands + // just below; no non-tie cell deviates. + assert_eq!((dev, tie_dev, ties, max_delta), (6_932, 6_932, 163_840, 1)); +} + +// ─── T3 ───────────────────────────────────────────────────────────────── + +#[derive(Debug, PartialEq, Eq)] +struct Identity { + drift: Vec<(u8, u8, u8, u8)>, // (f, c, got_f, got_c) + max_delta: u8, +} + +/// Revision of `x = (f, c)` against a zero-weight operand `(g, 0)`, both +/// orders, every representable `x`, three values of `g`. +fn measure_identity(rev: impl Fn(u8, u8, u8, u8) -> (u8, u8)) -> Identity { + let mut out = Identity { + drift: Vec::new(), + max_delta: 0, + }; + for f in 0..=255u8 { + for c in 0..=255u8 { + let first = rev(f, c, 0, 0); + for &g in &[0u8, 128, 255] { + let l = rev(f, c, g, 0); + let r = rev(g, 0, f, c); + // The zero-weight operand's frequency must not matter, and + // the operand order must not matter. + assert_eq!(l, first, "g leaks into ({f},{c})"); + assert_eq!(r, first, "order leaks into ({f},{c})"); + } + if first != (f, c) { + out.drift.push((f, c, first.0, first.1)); + out.max_delta = out + .max_delta + .max(first.0.abs_diff(f)) + .max(first.1.abs_diff(c)); + } + } + } + out +} + +#[test] +fn t3_report() { + let id = measure_identity(prod); + let mut by_c: BTreeSet = BTreeSet::new(); + for d in &id.drift { + by_c.insert(d.1); + } + eprintln!("T3 drift cells: {}", id.drift.len()); + eprintln!("T3 max delta: {}", id.max_delta); + eprintln!("T3 confidence codes with drift: {:?}", by_c); + eprintln!( + "T3 first drift cells: {:?}", + &id.drift[..id.drift.len().min(12)] + ); +} + +#[test] +fn t3_zero_weight_identity_is_pinned() { + let id = measure_identity(prod); + // The only drift is semantic, not quantisation: with confidence 0 on both + // sides there is no evidence, revision returns unknown `(0.5, 0.0)`, and + // every frequency except 128 moves to 128. For every c > 0 the zero-weight + // operand is an exact identity (0 off-by-one cells). + assert_eq!(id.drift.len(), 255); + assert!(id + .drift + .iter() + .all(|&(f, c, gf, gc)| c == 0 && f != 128 && (gf, gc) == (128, 0))); + assert_eq!(id.max_delta, 128); +} + +#[test] +fn t3_falsifier_an_injected_drift_is_detected() { + let base = measure_identity(prod); + let drifting = |f1: u8, c1: u8, f2: u8, c2: u8| { + let (f, c) = prod(f1, c1, f2, c2); + // One code of drift on one ordinary cell, whichever operand it is. + if (f1, c1) == (90, 40) || (f2, c2) == (90, 40) { + (f.wrapping_add(1), c) + } else { + (f, c) + } + }; + let id = measure_identity(drifting); + assert_eq!(id.drift.len(), base.drift.len() + 1); + assert!(id.drift.iter().any(|d| (d.0, d.1) == (90, 40))); +} + +#[test] +fn t3_forward_with_an_all_zero_weight_faults() { + // The documented null operand of `forward`: mantissa 0 decodes to no + // instruction, so it must refuse rather than run something. + let tab = Box::new([0u8; 256 * 256]); + let got = edge(200, 200).forward(CausalEdge64(0), &tab, &tab, &tab); + assert_eq!(got, Err(IsaFault::Unsupported { mantissa: 0 })); +} + +// ─── R1: c=255 policy candidates (probe variants, not production) ─────── + +/// Candidate policies for confidence code 255. None of them is adopted here. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Policy { + /// A: the current code path, replicated step by step. + StatusQuo, + /// B: cap the decoded confidence at 0.9999 and compute `c / (1 − c)` + /// directly, as the f64 reference in `ce64_isa_golden.rs` does. (Capping + /// first and then calling `evidence_weight` would change nothing, because + /// 0.9999 is still above its 0.999 saturation threshold.) + CapBeforeWeight, + /// B2: read input code 255 as 254 before the weight; outputs unchanged. + CapInputAt254, + /// C: 255 is not a representable confidence: inputs read 255 as 254 and + /// outputs are clamped to 254. + Saturate254, + /// D: status quo everywhere except the singular cell, which gets a + /// declared answer: `c = 255` and `f` = the mean of the two frequencies. + /// Added after the first R1 run showed that every cap candidate changes + /// the one-sided boundary, where the status quo is already exact. It + /// isolates "define one cell" from "change the c→w map". Not adopted. + SingularOnly, +} + +const POLICIES: [Policy; 5] = [ + Policy::StatusQuo, + Policy::CapBeforeWeight, + Policy::CapInputAt254, + Policy::Saturate254, + Policy::SingularOnly, +]; + +/// `(f_code, c_code, produced_nan)` for one revision under `pol`, in the same +/// f32 arithmetic and the same order as `isa::truth::revision` + `set_*`. +fn revise_with(pol: Policy, f1: u8, c1: u8, f2: u8, c2: u8) -> (u8, u8, bool) { + if pol == Policy::SingularOnly && (c1, c2) == (255, 255) { + let f = (f32::from(f1) / 255.0 + f32::from(f2) / 255.0) / 2.0; + return ((f * 255.0).round() as u8, 255, false); + } + let read = |c: u8| match pol { + Policy::CapInputAt254 | Policy::Saturate254 => c.min(254), + _ => c, + }; + let (f1, f2) = (f32::from(f1) / 255.0, f32::from(f2) / 255.0); + let (c1, c2) = (f32::from(read(c1)) / 255.0, f32::from(read(c2)) / 255.0); + let weight = |c: f32| match pol { + Policy::CapBeforeWeight => { + let c = c.min(0.9999); + c / (1.0 - c) + } + _ => causal_edge::isa::truth::evidence_weight(c), + }; + let (w1, w2) = (weight(c1), weight(c2)); + let ws = w1 + w2; + let (f, c) = if ws > f32::EPSILON { + ((f1 * w1 + f2 * w2) / ws, ws / (ws + 1.0)) + } else { + (0.5, 0.0) + }; + let nan = f.is_nan() || c.is_nan(); + let enc = |x: f32| (x.clamp(0.0, 1.0) * 255.0).round() as u8; + let (fo, mut co) = (enc(f), enc(c)); + if pol == Policy::Saturate254 { + co = co.min(254); + } + (fo, co, nan) +} + +#[derive(Debug)] +struct PolicyRow { + nan_c_surface: usize, + nan_f_boundary: usize, + monotonicity: usize, + symmetry: usize, + exact_mismatch: usize, + exact_max_err: u8, + changed_vs_status_quo: usize, + singular: (u8, u8), + /// f-side on the one-sided boundary `c1 = 255`, `c2 < 255` (ordinary + /// cells: the exact limit is `f = f1`): mismatches against the exact + /// form, max error, and cells changed against the status quo. + f_onesided_exact_mismatch: usize, + f_onesided_max_err: u8, + f_onesided_changed: usize, +} + +fn measure_policy(rev: impl Fn(u8, u8, u8, u8) -> (u8, u8, bool)) -> PolicyRow { + let mut grid = vec![[0u8; 256]; 256]; + let mut row = PolicyRow { + nan_c_surface: 0, + nan_f_boundary: 0, + monotonicity: 0, + symmetry: 0, + exact_mismatch: 0, + exact_max_err: 0, + changed_vs_status_quo: 0, + singular: (0, 0), + f_onesided_exact_mismatch: 0, + f_onesided_max_err: 0, + f_onesided_changed: 0, + }; + for c1 in 0..=255u8 { + for c2 in 0..=255u8 { + let (_, c, nan) = rev(128, c1, 128, c2); + grid[c1 as usize][c2 as usize] = c; + row.nan_c_surface += usize::from(nan); + if c != prod_c(c1, c2) { + row.changed_vs_status_quo += 1; + } + match exact_c(c1, c2) { + Some((want, _)) if c != want => { + row.exact_mismatch += 1; + row.exact_max_err = row.exact_max_err.max(c.abs_diff(want)); + } + Some(_) => {} + None => row.singular = (rev(128, c1, 128, c2).0, c), + } + } + } + #[allow(clippy::needless_range_loop)] // both (c1,c2) and (c2,c1) index the grid + for c1 in 0..=255usize { + for c2 in 0..=255usize { + // Here the singular cell IS included: a policy is judged on the + // whole surface, and a dip at (255,255) is exactly what it must fix. + if c1 > 0 && grid[c1][c2] < grid[c1 - 1][c2] { + row.monotonicity += 1; + } + if grid[c1][c2] != grid[c2][c1] { + row.symmetry += 1; + } + } + } + for c1 in 253..=255u8 { + for c2 in 253..=255u8 { + for f1 in 0..=255u8 { + for f2 in 0..=255u8 { + row.nan_f_boundary += usize::from(rev(f1, c1, f2, c2).2); + } + } + } + } + for c2 in [0u8, 1, 64, 127, 200, 253, 254] { + for f1 in 0..=255u8 { + for f2 in 0..=255u8 { + let got = rev(f1, 255, f2, c2).0; + let want = exact_f(f1, 255, f2, c2).expect("one-sided evidence").0; + if got != want { + row.f_onesided_exact_mismatch += 1; + row.f_onesided_max_err = row.f_onesided_max_err.max(got.abs_diff(want)); + } + row.f_onesided_changed += usize::from(got != prod(f1, 255, f2, c2).0); + } + } + } + row +} + +#[test] +fn r1_replica_of_status_quo_is_faithful() { + // The policy variants are only meaningful if the replica they modify IS + // the production path: every (f1,c1,f2,c2) on the boundary block and the + // whole c-surface must agree bit for bit. + for c1 in 0..=255u8 { + for c2 in 0..=255u8 { + let (f, c, _) = revise_with(Policy::StatusQuo, 128, c1, 128, c2); + assert_eq!((f, c), prod(128, c1, 128, c2), "({c1},{c2})"); + } + } + for c1 in 250..=255u8 { + for c2 in 250..=255u8 { + for f1 in (0..=255u8).step_by(5) { + for f2 in (0..=255u8).step_by(3) { + let (f, c, _) = revise_with(Policy::StatusQuo, f1, c1, f2, c2); + assert_eq!((f, c), prod(f1, c1, f2, c2)); + } + } + } + } +} + +#[test] +fn r1_report() { + eprintln!("R1 policy NaN(c) NaN(f@253-255) mono sym exact-mismatch max-err changed singular | f@c1=255: exact-mismatch max-err changed"); + for pol in POLICIES { + let r = measure_policy(|a, b, c, d| revise_with(pol, a, b, c, d)); + eprintln!( + "R1 {:<18} {:>6} {:>14} {:>4} {:>3} {:>14} {:>7} {:>7} {:?} | {:>6} {:>4} {:>6}", + format!("{pol:?}"), + r.nan_c_surface, + r.nan_f_boundary, + r.monotonicity, + r.symmetry, + r.exact_mismatch, + r.exact_max_err, + r.changed_vs_status_quo, + r.singular, + r.f_onesided_exact_mismatch, + r.f_onesided_max_err, + r.f_onesided_changed + ); + } + for pol in POLICIES { + let mut line = format!("R1 boundary {:<18}", format!("{pol:?}")); + for c1 in [253u8, 254, 255] { + for c2 in [0u8, 1, 127, 253, 254, 255] { + line += &format!(" {c1}/{c2}={}", revise_with(pol, 128, c1, 128, c2).1); + } + } + eprintln!("{line}"); + } +} + +#[test] +fn r1_falsifier_an_invalid_boundary_is_detected() { + // A candidate that lowers confidence when an operand reaches 255 must be + // caught by the monotonicity gate; one that returns NaN must be counted. + let dip = measure_policy(|f1, c1, f2, c2| { + let (f, c, n) = revise_with(Policy::CapInputAt254, f1, c1, f2, c2); + if c1 == 255 && c2 == 10 { + (f, 0, n) + } else { + (f, c, n) + } + }); + assert!(dip.monotonicity > 0); + let nan = measure_policy(|f1, c1, f2, c2| { + let (f, c, _) = revise_with(Policy::CapInputAt254, f1, c1, f2, c2); + (f, c, c1 == 254 && c2 == 254) + }); + assert!(nan.nan_c_surface > 0 && nan.nan_f_boundary > 0); +} + +// ─── NarsTables: does the table path read confidence? ─────────────────── + +/// Distinct `(f, c)` outputs over all 65,536 `(c1, c2)` for fixed `(f1, f2)`. +fn distinct_over_confidence(rev: impl Fn(u8, u8, u8, u8) -> (u8, u8), f1: u8, f2: u8) -> usize { + let mut seen = BTreeSet::new(); + for c1 in 0..=255u8 { + for c2 in 0..=255u8 { + seen.insert(rev(f1, c1, f2, c2)); + } + } + seen.len() +} + +/// The table path as a revision function on codes. +fn table_rev(t: &NarsTables) -> impl Fn(u8, u8, u8, u8) -> (u8, u8) + '_ { + move |f1, c1, f2, c2| { + let p = t.revise(f1, c1, f2, c2); + (unpack_f(p), unpack_c(p)) + } +} + +#[test] +fn nars_tables_report_and_pin() { + let t1 = NarsTables::build(1); + let t16 = NarsTables::build(16); + let via = table_rev; + let pairs = [(0u8, 255u8), (64, 192), (200, 30), (128, 128)]; + for &(f1, f2) in &pairs { + let d1 = distinct_over_confidence(via(&t1), f1, f2); + let d16 = distinct_over_confidence(via(&t16), f1, f2); + let dp = distinct_over_confidence(prod, f1, f2); + eprintln!("NarsTables f=({f1},{f2}): build(1) {d1} | build(16) {d16} | isa revision {dp}"); + // build(1): every confidence lands in bucket 0, so the output is one + // constant pair for any (c1, c2). Confidence is inert. + assert_eq!(d1, 1, "build(1) must be confidence-blind"); + assert!(d16 > 1 && dp > d16, "confidence-aware paths must vary"); + } + // What the one constant pair is: both representative confidences are 0.5, + // so c = 2/3 -> 170 and f = the mean of the two codes. + let (f, c) = via(&t1)(0, 7, 255, 250); + assert_eq!((f, c), (128, 170)); +} + +#[test] +fn r1_table_is_pinned() { + // (NaN c-surface, NaN f-boundary, monotonicity, symmetry, exact mismatch, + // exact max err, changed cells, singular (f,c), one-sided f: exact + // mismatch, max err, changed). Measured; no policy is chosen by this. + type Row = ( + usize, + usize, + usize, + usize, + usize, + u8, + usize, + (u8, u8), + usize, + u8, + usize, + ); + let want: [(Policy, Row); 5] = [ + ( + Policy::StatusQuo, + (1, 65_536, 1, 0, 3, 1, 0, (0, 0), 0, 0, 0), + ), + ( + Policy::CapBeforeWeight, + (0, 0, 0, 0, 3, 1, 1, (128, 255), 101_900, 6, 101_900), + ), + ( + Policy::CapInputAt254, + (0, 0, 0, 0, 513, 1, 511, (128, 254), 194_671, 128, 194_671), + ), + ( + Policy::Saturate254, + (0, 0, 0, 0, 513, 1, 511, (128, 254), 194_671, 128, 194_671), + ), + ( + Policy::SingularOnly, + (0, 0, 0, 0, 3, 1, 1, (128, 255), 0, 0, 0), + ), + ]; + for (pol, w) in want { + let r = measure_policy(|a, b, c, d| revise_with(pol, a, b, c, d)); + let got = ( + r.nan_c_surface, + r.nan_f_boundary, + r.monotonicity, + r.symmetry, + r.exact_mismatch, + r.exact_max_err, + r.changed_vs_status_quo, + r.singular, + r.f_onesided_exact_mismatch, + r.f_onesided_max_err, + r.f_onesided_changed, + ); + assert_eq!(got, w, "{pol:?}"); + } +} diff --git a/crates/lance-graph-planner/tests/d_rpf_conf_0_r3.rs b/crates/lance-graph-planner/tests/d_rpf_conf_0_r3.rs new file mode 100644 index 000000000..8173993c5 --- /dev/null +++ b/crates/lance-graph-planner/tests/d_rpf_conf_0_r3.rs @@ -0,0 +1,330 @@ +//! D-RPF-CONF-0, R3: the disagreement matrix over the DISTINCT LIVE revision +//! laws, measured on the u8 truth grid. +//! +//! The law list is the G4 census result +//! (`.claude/board/entries/2026-10-09-d-rpf-conf-0-confidence-surface.md`), not +//! "every function named revise". Each law is called through its real public +//! entry point; nothing is reimplemented here. Laws that compute in f32 are +//! quantized with ONE declared quantizer, `q` below, which is the same rule +//! `CausalEdge64::set_*_u8` applies (clamp, `*255`, round half away from zero). +//! A and B carry their own native u8 encoding and are not re-quantized. +//! +//! Laws compared (letters from the census): +//! - A `CausalEdge64::revision` (ISA f32, k=1, `w = MAX` at c >= 0.999) +//! - B1 `NarsTables::build(1).revise` (the planner's replay path) +//! - B16 `NarsTables::build(16).revise` +//! - C `ndarray::hpc::nars::nars_revision` (evidence split, c clamped to 0.9999) +//! - D planner `nars::truth::TruthValue::revise` (`w = MAX` only at c >= 1) +//! - E contract `exploration::NarsTruth::revision` (`+1e-9`, cap 0.99) +//! - LIN contract `grammar::revise_truth` (c used directly as the weight) +//! +//! Not compared, with the reason recorded in the board entry: F and G (not +//! reachable from the planner and not on its path), H (private, `wip`), +//! J (`PathTruth::revise` takes a raw weight, so comparing it needs a declared +//! c->w rule that no caller fixes). + +use causal_edge::edge::CausalEdge64; +use causal_edge::tables::{unpack_c, unpack_f, NarsTables}; +use lance_graph_contract::crystal::TruthValue as CrystalTruth; +use lance_graph_contract::exploration::NarsTruth as ContractTruth; +use lance_graph_contract::grammar::revise_truth; +use lance_graph_planner::nars::truth::TruthValue as PlannerTruth; +use ndarray::hpc::nars::{nars_revision, NarsTruth as NdTruth}; + +/// The declared quantizer for f32 laws: the rule `CausalEdge64::set_*_u8` uses. +fn q(x: f32) -> u8 { + (x.clamp(0.0, 1.0) * 255.0).round() as u8 +} + +fn d(x: u8) -> f32 { + x as f32 / 255.0 +} + +type Law<'a> = Box (u8, u8) + 'a>; + +fn edge(f: u8, c: u8) -> CausalEdge64 { + let mut e = CausalEdge64(0); + e.set_frequency_u8(f); + e.set_confidence_u8(c); + e +} + +fn law_a() -> Law<'static> { + Box::new(|f1, c1, f2, c2| { + let r = edge(f1, c1).revision(edge(f2, c2)); + (r.frequency_u8(), r.confidence_u8()) + }) +} + +fn law_table(t: &NarsTables) -> Law<'_> { + Box::new(move |f1, c1, f2, c2| { + let p = t.revise(f1, c1, f2, c2); + (unpack_f(p), unpack_c(p)) + }) +} + +fn law_c() -> Law<'static> { + Box::new(|f1, c1, f2, c2| { + let r = nars_revision(NdTruth::new(d(f1), d(c1)), NdTruth::new(d(f2), d(c2))); + (q(r.frequency), q(r.confidence)) + }) +} + +fn law_d() -> Law<'static> { + Box::new(|f1, c1, f2, c2| { + let r = PlannerTruth::new(d(f1), d(c1)).revise(&PlannerTruth::new(d(f2), d(c2))); + (q(r.frequency), q(r.confidence)) + }) +} + +fn law_e() -> Law<'static> { + Box::new(|f1, c1, f2, c2| { + let r = ContractTruth::new(d(f1), d(c1)).revision(&ContractTruth::new(d(f2), d(c2))); + (q(r.frequency), q(r.confidence)) + }) +} + +fn law_lin() -> Law<'static> { + Box::new(|f1, c1, f2, c2| { + let r = revise_truth(CrystalTruth::new(d(f1), d(c1)), d(f2), d(c2)); + (q(r.frequency), q(r.confidence)) + }) +} + +/// Frequency pairs sampled; confidence is swept over the full 256 x 256 grid. +const FS: [u8; 5] = [0, 51, 128, 204, 255]; + +/// A confidence code at the edge of the scale, where laws differ by policy +/// rather than by arithmetic. +fn is_boundary(c: u8) -> bool { + c == 0 || c >= 254 +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +struct Pair { + differ: u64, + differ_broad: u64, + max_df: u8, + max_dc: u8, + /// The same maxima over broad cells only (no c in {0, 254, 255}). + max_df_broad: u8, + max_dc_broad: u8, + /// Sums of |dF| and |dC| over every compared cell (mean = sum / CELLS). + sum_df: u64, + sum_dc: u64, +} + +const CELLS: u64 = (FS.len() * FS.len() * 256 * 256) as u64; + +/// One law's outputs over the whole sampled domain, in a fixed order. +fn surface(law: &Law<'_>) -> Vec<(u8, u8)> { + let mut v = Vec::with_capacity(CELLS as usize); + for &f1 in &FS { + for &f2 in &FS { + for c1 in 0..=255u8 { + for c2 in 0..=255u8 { + v.push(law(f1, c1, f2, c2)); + } + } + } + } + v +} + +fn compare(a: &[(u8, u8)], b: &[(u8, u8)]) -> Pair { + let mut p = Pair::default(); + let mut i = 0; + for _f1 in &FS { + for _f2 in &FS { + for c1 in 0..=255u8 { + for c2 in 0..=255u8 { + let ((fa, ca), (fb, cb)) = (a[i], b[i]); + i += 1; + let df = fa.abs_diff(fb); + let dc = ca.abs_diff(cb); + p.sum_df += df as u64; + p.sum_dc += dc as u64; + p.max_df = p.max_df.max(df); + p.max_dc = p.max_dc.max(dc); + if df != 0 || dc != 0 { + p.differ += 1; + if !is_boundary(c1) && !is_boundary(c2) { + p.differ_broad += 1; + p.max_df_broad = p.max_df_broad.max(df); + p.max_dc_broad = p.max_dc_broad.max(dc); + } + } + } + } + } + } + p +} + +/// How many distinct output confidences a law produces over the c grid, +/// with f fixed. 1 means the law ignores confidence. +fn distinct_c(law: &Law<'_>) -> usize { + let mut seen = [false; 256]; + for c1 in 0..=255u8 { + for c2 in 0..=255u8 { + seen[law(128, c1, 128, c2).1 as usize] = true; + } + } + seen.iter().filter(|s| **s).count() +} + +const NAMES: [&str; 7] = ["A", "B1", "B16", "C", "D", "E", "LIN"]; + +fn all_surfaces() -> Vec> { + let t1 = NarsTables::build(1); + let t16 = NarsTables::build(16); + let laws: Vec> = vec![ + law_a(), + law_table(&t1), + law_table(&t16), + law_c(), + law_d(), + law_e(), + law_lin(), + ]; + laws.iter().map(surface).collect() +} + +fn matrix(s: &[Vec<(u8, u8)>]) -> Vec<(usize, usize, Pair)> { + let mut out = Vec::new(); + for i in 0..s.len() { + for j in (i + 1)..s.len() { + out.push((i, j, compare(&s[i], &s[j]))); + } + } + out +} + +/// Distinct output confidences at f = (128, 128), per law in `NAMES` order. +const DISTINCT_C: [usize; 7] = [256, 1, 92, 256, 256, 253, 171]; + +/// Per pair (i, j): (cells differing, of those broad, broad max dF, broad max dC). +/// Domain: f in `FS` x `FS`, c over the full 256 x 256 grid (1,638,400 cells). +#[rustfmt::skip] +const MATRIX: [(usize, usize, u64, u64, u8, u8); 21] = [ + (0, 1, 1636550, 1598385, 128, 168), + (0, 2, 1585273, 1547346, 113, 14), + (0, 3, 391, 130, 1, 1), + (0, 4, 0, 0, 0, 0), + (0, 5, 38897, 13327, 15, 2), + (0, 6, 1636238, 1598618, 107, 126), + (1, 2, 1630720, 1592545, 128, 155), + (1, 3, 1636552, 1598387, 128, 168), + (1, 4, 1636550, 1598385, 128, 168), + (1, 5, 1636550, 1598385, 128, 168), + (1, 6, 1638119, 1600200, 127, 168), + (2, 3, 1585277, 1547350, 113, 14), + (2, 4, 1585273, 1547346, 113, 14), + (2, 5, 1585273, 1547346, 113, 14), + (2, 6, 1637830, 1599655, 112, 123), + (3, 4, 391, 130, 1, 1), + (3, 5, 39025, 13455, 15, 2), + (3, 6, 1636240, 1598620, 107, 126), + (4, 5, 38897, 13327, 15, 2), + (4, 6, 1636238, 1598618, 107, 126), + (5, 6, 1636218, 1598618, 105, 125), +]; + +#[test] +fn r3_report() { + let t1 = NarsTables::build(1); + let t16 = NarsTables::build(16); + let laws: Vec> = vec![ + law_a(), + law_table(&t1), + law_table(&t16), + law_c(), + law_d(), + law_e(), + law_lin(), + ]; + for (n, l) in NAMES.iter().zip(&laws) { + println!("distinct_c {n}: {}", distinct_c(l)); + } + let s: Vec<_> = laws.iter().map(surface).collect(); + for (i, j, p) in matrix(&s) { + println!( + "{:>3} vs {:<3} differ={:>7} broad={:>7} max_dF={:>3} max_dC={:>3} \ + broad_max=({},{}) mean_dF={:.4} mean_dC={:.4}", + NAMES[i], + NAMES[j], + p.differ, + p.differ_broad, + p.max_df, + p.max_dc, + p.max_df_broad, + p.max_dc_broad, + p.sum_df as f64 / CELLS as f64, + p.sum_dc as f64 / CELLS as f64 + ); + } +} + +#[test] +fn r3_distinct_confidence_is_pinned() { + let t1 = NarsTables::build(1); + let t16 = NarsTables::build(16); + let laws: Vec> = vec![ + law_a(), + law_table(&t1), + law_table(&t16), + law_c(), + law_d(), + law_e(), + law_lin(), + ]; + let got: Vec = laws.iter().map(distinct_c).collect(); + assert_eq!(got, DISTINCT_C); +} + +#[test] +fn r3_matrix_is_pinned() { + let got: Vec<_> = matrix(&all_surfaces()) + .into_iter() + .map(|(i, j, p)| { + ( + i, + j, + p.differ, + p.differ_broad, + p.max_df_broad, + p.max_dc_broad, + ) + }) + .collect(); + assert_eq!(got, MATRIX.to_vec()); +} + +/// The falsifier the matrix answers: a pair is a cosmetic difference when no +/// broad cell differs by more than one code. Pinned so a change to any law +/// that moves a pair across that line is loud. +#[test] +fn r3_cosmetic_pairs_are_pinned() { + // Read from the measurement, not from `MATRIX`: a check over the pinned + // constant could never fail. + let cosmetic: Vec<(&str, &str)> = matrix(&all_surfaces()) + .into_iter() + .filter(|(_, _, p)| p.max_df_broad <= 1 && p.max_dc_broad <= 1) + .map(|(i, j, _)| (NAMES[i], NAMES[j])) + .collect(); + assert_eq!(cosmetic, vec![("A", "C"), ("A", "D"), ("C", "D")]); +} + +/// Anti-vacuity: the measure is not blind. A law compared with itself is +/// zero everywhere, and a confidence-inert law (B1) is detected as far from A +/// in the broad domain. +#[test] +fn r3_falsifier_the_measure_separates_identical_from_inert() { + let a = surface(&law_a()); + assert_eq!(compare(&a, &a), Pair::default()); + let t1 = NarsTables::build(1); + let b1 = surface(&law_table(&t1)); + let p = compare(&a, &b1); + assert!(p.differ_broad * 2 > CELLS, "{p:?}"); + assert!(p.max_dc_broad > 100, "{p:?}"); +}