From b0046b304d883b1ce549b174a5428b4a23dd0a37 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 18:28:04 +0000 Subject: [PATCH 01/23] =?UTF-8?q?quack:=20ruff=20taint=20probe=20=E2=80=94?= =?UTF-8?q?=20MeTTa=20lowerable=20verdict=20as=20presence-bit=20closure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recomputes ruff_metta_spo's interpreter-only fixpoint over the call graph as And(Plane(E), Plane(T_rep)) / Any per head extent through mask-risc, with ruff's verdict as the oracle. Hyperon 147 heads and PeTTa 1005 heads: 0 mismatches; zero steps disagree by exactly the call-tainted heads (9 / 71). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .../examples/ruff_taint_probe.rs | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 crates/lance-graph-quack/examples/ruff_taint_probe.rs diff --git a/crates/lance-graph-quack/examples/ruff_taint_probe.rs b/crates/lance-graph-quack/examples/ruff_taint_probe.rs new file mode 100644 index 000000000..b7d4b168b --- /dev/null +++ b/crates/lance-graph-quack/examples/ruff_taint_probe.rs @@ -0,0 +1,220 @@ +//! Dog-food probe: ruff's MeTTa "lowerable / interpreter-only" verdict, +//! recomputed as presence-bit algebra through quack and mask-risc. +//! +//! `ruff_metta_spo` classifies every rule head of a MeTTa corpus: a head is +//! interpreter-only when its own body uses a native (or a variable head), or +//! when it calls an interpreter-only head; the second rule runs to a fixpoint +//! over the call graph. ruff computes that fixpoint in Rust over maps. Here +//! the same fixpoint runs over our own substrate, and ruff's verdict is the +//! oracle. +//! +//! # Layout +//! +//! - Every rule head gets one coordinate: its index in sorted order. Names are +//! labels, read once at ingest and never seen by the algebra. +//! - The call graph is ONE presence plane `E` over `n · S` addresses: the bit +//! at `h · S + g` says head `h` calls head `g`. `S` is `n` rounded up to whole +//! words, so every row starts on a word. +//! - The taint plane `T` (one bit per head) is tiled into every row segment, +//! a word copy, giving `T_rep` with bit `h · S + g` = `T[g]`. +//! - One step: head `h` becomes tainted when `And(Plane(E), Plane(T_rep))` +//! has `Any` bit in row `h`'s extent. The callee bit and the taint bit of +//! callee `g` sit at the same coordinate, so the step reads nothing at a +//! position taken from another lane's values. +//! - Iterate until `T` stops changing. +//! +//! # Seed (stated, because it is borrowed) +//! +//! The direct taint (a head's own body uses a native) is not recomputed: it is +//! read from ruff's dump (`direct = 1`). What this probe tests is the CLOSURE. +//! The disable arm below proves the closure is load-bearing. +//! +//! # Input +//! +//! A directory written by +//! `cargo run --manifest-path crates/ruff_metta_spo/Cargo.toml --example metta_census -- --dump ` +//! in the ruff repository (`heads.tsv`, `calls.tsv`). +//! +//! ```text +//! cargo run -p lance-graph-quack --example ruff_taint_probe -- ... +//! ``` + +use std::collections::BTreeMap; +use std::path::Path; + +use lance_graph_mask_risc::exec::{execute_extent, Scratch}; +use lance_graph_mask_risc::{words_for, Foreign, Out, Planes, Program, Value}; +use lance_graph_quack::{lower, Agg, Filter, Mask, Query}; + +struct Dump { + /// Sorted head labels; the index is the coordinate. + labels: Vec, + direct: Vec, + lowerable: Vec, + /// `(caller, callee)` coordinates. + calls: Vec<(usize, usize)>, +} + +fn read_dump(dir: &Path) -> Dump { + let heads = std::fs::read_to_string(dir.join("heads.tsv")).expect("heads.tsv"); + let mut rows: Vec<(String, bool, bool)> = heads + .lines() + .skip(1) + .map(|l| { + let f: Vec<&str> = l.split('\t').collect(); + assert_eq!(f.len(), 3, "heads.tsv line {l:?}"); + (f[0].to_string(), f[1] == "1", f[2] == "1") + }) + .collect(); + rows.sort(); + let labels: Vec = rows.iter().map(|r| r.0.clone()).collect(); + let coord: BTreeMap<&str, usize> = labels + .iter() + .enumerate() + .map(|(i, l)| (l.as_str(), i)) + .collect(); + let calls_text = std::fs::read_to_string(dir.join("calls.tsv")).expect("calls.tsv"); + let calls = calls_text + .lines() + .skip(1) + .map(|l| { + let f: Vec<&str> = l.split('\t').collect(); + assert_eq!(f.len(), 2, "calls.tsv line {l:?}"); + (coord[f[0]], coord[f[1]]) + }) + .collect(); + Dump { + direct: rows.iter().map(|r| r.1).collect(), + lowerable: rows.iter().map(|r| r.2).collect(), + labels, + calls, + } +} + +fn set(words: &mut [u64], i: usize) { + words[i / 64] |= 1 << (i % 64); +} + +fn get(words: &[u64], i: usize) -> bool { + words[i / 64] >> (i % 64) & 1 == 1 +} + +/// The closure over `iterations` steps (`None` = to the fixpoint). Returns the +/// taint plane and the number of steps that changed it. +fn closure(d: &Dump, seed: &[bool], iterations: Option) -> (Vec, usize) { + let n = d.labels.len(); + let w = words_for(n); + let stride = w * 64; + let n_rows = n * stride; + + // E: the call presence plane. Built once, at ingest. + let mut e = vec![0u64; words_for(n_rows)]; + for &(h, g) in &d.calls { + set(&mut e, h * stride + g); + } + + let mut t = vec![0u64; w]; + for (i, &s) in seed.iter().enumerate() { + if s { + set(&mut t, i); + } + } + + // And(E, T_rep), Any — lowered once, run per row extent. + let program: Program = lower(&Query { + filter: Filter::And(vec![Filter::Plane(Mask(0)), Filter::Plane(Mask(1))]), + agg: Agg::Any, + }) + .expect("lowers"); + + let mut changed_steps = 0; + let mut t_rep = vec![0u64; words_for(n_rows)]; + loop { + if iterations.is_some_and(|k| changed_steps >= k) { + break; + } + // Tile T into every row segment: a positional word copy. + for h in 0..n { + t_rep[h * w..(h + 1) * w].copy_from_slice(&t); + } + let masks: [&[u64]; 2] = [&e, &t_rep]; + let planes = Planes { + n_rows, + masks: &masks, + lanes: &[], + }; + let mut next = t.clone(); + for h in 0..n { + if get(&t, h) { + continue; + } + let mut s = Scratch::for_program(&program, n_rows).expect("scratch"); + let v = execute_extent( + &program, + &planes, + &Foreign::NONE, + &mut s, + Out::None, + h * stride..(h + 1) * stride, + ) + .expect("runs"); + if matches!(v, Value::Bool(true)) { + set(&mut next, h); + } + } + if next == t { + break; + } + t = next; + changed_steps += 1; + } + (t, changed_steps) +} + +fn mismatches(d: &Dump, t: &[u64]) -> usize { + (0..d.labels.len()) + .filter(|&h| get(t, h) == d.lowerable[h]) + .count() +} + +fn main() { + let tier = if cfg!(target_feature = "avx512f") { + "v4 (avx512f)" + } else { + "not v4" + }; + println!("tier: {tier}"); + let dirs: Vec = std::env::args().skip(1).collect(); + assert!(!dirs.is_empty(), "pass one or more dump directories"); + for dir in dirs { + let d = read_dump(Path::new(&dir)); + let n = d.labels.len(); + let by_calls = (0..n).filter(|&h| !d.direct[h] && !d.lowerable[h]).count(); + + // 1. To the fixpoint: must equal ruff's verdict on every head. + let (t, steps) = closure(&d, &d.direct, None); + let tainted = (0..n).filter(|&h| get(&t, h)).count(); + let miss = mismatches(&d, &t); + println!( + "{dir}: heads {n}, call edges {}, seed {}, tainted at fixpoint {tainted} after {steps} steps, mismatches vs ruff {miss}", + d.calls.len(), + d.direct.iter().filter(|&&b| b).count() + ); + assert_eq!(miss, 0, "closure disagrees with ruff"); + + // 2. Disable: no closure. The heads tainted only through a call must + // now disagree — exactly those, and there must be some. + let (t0, _) = closure(&d, &d.direct, Some(0)); + let miss0 = mismatches(&d, &t0); + println!( + " disable (0 steps): mismatches {miss0}, heads tainted only through calls {by_calls}" + ); + assert!(by_calls > 0, "fixture cannot tell closure from seed"); + assert_eq!(miss0, by_calls); + + // 3. Silence: an empty seed taints nothing. + let (te, _) = closure(&d, &vec![false; n], None); + assert!((0..n).all(|h| !get(&te, h)), "closure invented taint"); + println!(" silence (empty seed): nothing tainted"); + } +} From b4a35521f91e18c17f0bfe41f7a731128604f6e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 18:44:16 +0000 Subject: [PATCH 02/23] =?UTF-8?q?quack:=20semantic-map=20probe=20=E2=80=94?= =?UTF-8?q?=20Haspelmath's=20grams=20as=20Cartesian=20presence=20planes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gram x function and function x function presence planes; contiguity of a gram is a closure of And(Plane(E), Plane(R_rep)) / Any kept inside the gram's row. All 18 grams of Figs. 8.1, 8.6, 8.9, 8.12 are contiguous; the (4) arrangement argument eliminates 4b (French a) and 4c (German zu); the Turkish old present (23) is caught as non-contiguous. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .../examples/semantic_map_probe.rs | 555 ++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 crates/lance-graph-quack/examples/semantic_map_probe.rs diff --git a/crates/lance-graph-quack/examples/semantic_map_probe.rs b/crates/lance-graph-quack/examples/semantic_map_probe.rs new file mode 100644 index 000000000..be3e702ba --- /dev/null +++ b/crates/lance-graph-quack/examples/semantic_map_probe.rs @@ -0,0 +1,555 @@ +//! Cartesian addressing of grammar: Haspelmath's semantic maps as two +//! presence planes, checked through quack and mask-risc. +//! +//! Source: M. Haspelmath, "The Geometry of Grammatical Meaning: Semantic Maps +//! and Cross-Linguistic Comparison", in Tomasello (ed.), *The New Psychology +//! of Language*, vol. 2, 2003, pp. 211–242. +//! +//! A semantic map is a graph over grammatical FUNCTIONS (direction, +//! recipient, purpose, …). Its one testable claim: every gram of every +//! language (English `to`, French `à`, …) covers a CONTIGUOUS region of the +//! map. A gram that covers a disconnected set falsifies the map. +//! +//! # Layout +//! +//! - Every function has one coordinate; every gram has one coordinate. Names +//! are labels, used only to build the planes and print results. +//! - `G` (gram × function): bit `g · S + f` = gram `g` is used in function `f`. +//! A parsed occurrence — an AST node of gram `g` in function `f` — sets +//! exactly that bit; the paper's numbered examples are such occurrences. +//! - `E` (function × function): bit `h · S + f` = the map links `h` and `f` +//! (both directions; the map is undirected). +//! - Contiguity of gram `g`: seed the lowest function of `g`'s row, then +//! repeatedly add every function `h` of the row with +//! `Any(And(Plane(E), Plane(R_rep)))` over row `h`, where `R` is the set +//! reached so far, tiled into every row segment. Contiguous iff the fixpoint +//! equals the gram's row. Every compared pair sits at one coordinate. +//! +//! ```text +//! cargo run -p lance-graph-quack --example semantic_map_probe +//! ``` + +use lance_graph_mask_risc::exec::{execute_extent, Scratch}; +use lance_graph_mask_risc::{words_for, Foreign, Out, Planes, Program, Value}; +use lance_graph_quack::{lower, Agg, Filter, Mask, Query}; + +fn set(words: &mut [u64], i: usize) { + words[i / 64] |= 1 << (i % 64); +} + +fn get(words: &[u64], i: usize) -> bool { + words[i / 64] >> (i % 64) & 1 == 1 +} + +/// One semantic map: function labels and its links. +struct Map { + name: &'static str, + functions: &'static [&'static str], + links: &'static [(&'static str, &'static str)], +} + +/// A gram: its language-particular label and the functions it covers. +struct Gram { + label: &'static str, + functions: &'static [&'static str], +} + +impl Map { + fn coord(&self, f: &str) -> usize { + self.functions + .iter() + .position(|x| *x == f) + .unwrap_or_else(|| panic!("{}: unknown function {f:?}", self.name)) + } +} + +/// The two planes for one map and a set of grams. +struct Space { + n: usize, + w: usize, + stride: usize, + e: Vec, + g: Vec, + program: Program, +} + +impl Space { + fn new(map: &Map, grams: &[Gram]) -> Self { + let n = map.functions.len(); + let w = words_for(n); + let stride = w * 64; + let mut e = vec![0u64; words_for(n * stride)]; + for &(a, b) in map.links { + let (a, b) = (map.coord(a), map.coord(b)); + set(&mut e, a * stride + b); + set(&mut e, b * stride + a); + } + let mut g = vec![0u64; words_for(grams.len() * stride).max(1)]; + for (gi, gram) in grams.iter().enumerate() { + for f in gram.functions { + set(&mut g, gi * stride + map.coord(f)); + } + } + let program = lower(&Query { + filter: Filter::And(vec![Filter::Plane(Mask(0)), Filter::Plane(Mask(1))]), + agg: Agg::Any, + }) + .expect("lowers"); + Space { + n, + w, + stride, + e, + g, + program, + } + } + + /// The gram's row, `w` words. + fn row(&self, gi: usize) -> Vec { + self.g[gi * self.w..(gi + 1) * self.w].to_vec() + } + + /// Reachable part of the gram's row from its lowest function, staying + /// inside the row. `steps = None` runs to the fixpoint. + fn reach(&self, gi: usize, steps: Option) -> Vec { + let row = self.row(gi); + let mut r = vec![0u64; self.w]; + if let Some(first) = (0..self.n).find(|&f| get(&row, f)) { + set(&mut r, first); + } + let n_rows = self.n * self.stride; + let mut r_rep = vec![0u64; words_for(n_rows)]; + let mut done = 0; + loop { + if steps.is_some_and(|k| done >= k) { + break; + } + for h in 0..self.n { + r_rep[h * self.w..(h + 1) * self.w].copy_from_slice(&r); + } + let masks: [&[u64]; 2] = [&self.e, &r_rep]; + let planes = Planes { + n_rows, + masks: &masks, + lanes: &[], + }; + let mut next = r.clone(); + for h in 0..self.n { + if !get(&row, h) || get(&r, h) { + continue; + } + let mut s = Scratch::for_program(&self.program, n_rows).expect("scratch"); + let v = execute_extent( + &self.program, + &planes, + &Foreign::NONE, + &mut s, + Out::None, + h * self.stride..(h + 1) * self.stride, + ) + .expect("runs"); + if matches!(v, Value::Bool(true)) { + set(&mut next, h); + } + } + if next == r { + break; + } + r = next; + done += 1; + } + r + } + + fn contiguous(&self, gi: usize) -> bool { + self.reach(gi, None) == self.row(gi) + } +} + +/// Returns the labels of the grams that are NOT contiguous on the map. +fn check(map: &Map, grams: &[Gram]) -> Vec<&'static str> { + let s = Space::new(map, grams); + grams + .iter() + .enumerate() + .filter(|(gi, _)| !s.contiguous(*gi)) + .map(|(_, g)| g.label) + .collect() +} + +// ── Fig. 8.1: typical dative functions ──────────────────────────────────── + +const DATIVE: Map = Map { + name: "dative (Fig. 8.1)", + functions: &[ + "direction", + "purpose", + "recipient", + "experiencer", + "predicative possessor", + "external possessor", + "beneficiary", + "judicantis", + ], + links: &[ + ("direction", "recipient"), + ("direction", "purpose"), + ("recipient", "experiencer"), + ("recipient", "predicative possessor"), + ("recipient", "beneficiary"), + ("predicative possessor", "external possessor"), + ("beneficiary", "external possessor"), + ("beneficiary", "judicantis"), + ], +}; + +const DATIVE_GRAMS: &[Gram] = &[ + // (2a–d), Fig. 8.1 + Gram { + label: "English to", + functions: &["direction", "recipient", "experiencer", "purpose"], + }, + // Fig. 8.2: no purpose, but predicative possession (Ce chien est à moi) + Gram { + label: "French à", + functions: &[ + "direction", + "recipient", + "experiencer", + "predicative possessor", + ], + }, + // Fig. 8.5 and its text: no direction, no predicative possession + Gram { + label: "French dative clitics", + functions: &[ + "recipient", + "experiencer", + "beneficiary", + "external possessor", + "judicantis", + ], + }, +]; + +// ── Fig. 8.6: indefinite pronoun functions ─────────────────────────────── + +const INDEFINITE: Map = Map { + name: "indefinites (Fig. 8.6)", + functions: &[ + "specific known", + "specific unknown", + "irrealis non-specific", + "question", + "conditional", + "indirect negation", + "comparative", + "direct negation", + "free choice", + ], + links: &[ + ("specific known", "specific unknown"), + ("specific unknown", "irrealis non-specific"), + ("irrealis non-specific", "question"), + ("irrealis non-specific", "conditional"), + ("question", "indirect negation"), + ("conditional", "comparative"), + ("indirect negation", "comparative"), + ("indirect negation", "direct negation"), + ("comparative", "free choice"), + ], +}; + +const INDEFINITE_GRAMS: &[Gram] = &[ + // Fig. 8.7 + Gram { + label: "English some-", + functions: &[ + "specific known", + "specific unknown", + "irrealis non-specific", + "question", + "conditional", + ], + }, + Gram { + label: "English any-", + functions: &[ + "question", + "conditional", + "indirect negation", + "comparative", + "direct negation", + "free choice", + ], + }, + // Fig. 8.8 and (8)–(11): -nibud' in irrealis, questions, conditionals; + // not in negation, not free choice + Gram { + label: "Russian -nibud'", + functions: &["irrealis non-specific", "question", "conditional"], + }, +]; + +// ── Fig. 8.9: reflexive and middle functions ───────────────────────────── + +const MIDDLE: Map = Map { + name: "reflexive/middle (Fig. 8.9)", + functions: &[ + "full reflexive", + "grooming/body motion", + "naturally reciprocal", + "anticausative", + "deobjective", + "potential passive", + "passive", + ], + links: &[ + ("full reflexive", "grooming/body motion"), + ("grooming/body motion", "naturally reciprocal"), + ("grooming/body motion", "anticausative"), + ("anticausative", "deobjective"), + ("anticausative", "potential passive"), + ("potential passive", "passive"), + ], +}; + +const MIDDLE_GRAMS: &[Gram] = &[ + // Fig. 8.10, (13) + Gram { + label: "French se", + functions: &[ + "full reflexive", + "grooming/body motion", + "naturally reciprocal", + "anticausative", + "potential passive", + ], + }, + // Fig. 8.10, (14)–(15) + Gram { + label: "Russian -sja", + functions: &[ + "grooming/body motion", + "naturally reciprocal", + "anticausative", + "deobjective", + "potential passive", + "passive", + ], + }, + // Fig. 8.11 + Gram { + label: "Turkish -In", + functions: &["grooming/body motion"], + }, + Gram { + label: "Turkish -Iş", + functions: &["naturally reciprocal"], + }, + Gram { + label: "Turkish -Il", + functions: &["anticausative", "potential passive", "passive"], + }, +]; + +// ── Fig. 8.12: instrumental and related functions ──────────────────────── + +const INSTRUMENTAL: Map = Map { + name: "instrumental (Fig. 8.12)", + functions: &[ + "recipient", + "beneficiary", + "cause", + "co-agent", + "conjunctive", + "comitative", + "instrumental", + "passive", + "source", + ], + links: &[ + ("recipient", "beneficiary"), + ("beneficiary", "cause"), + ("recipient", "co-agent"), + ("co-agent", "comitative"), + ("conjunctive", "comitative"), + ("comitative", "instrumental"), + ("instrumental", "passive"), + ("passive", "source"), + ("cause", "passive"), + ("cause", "source"), + ], +}; + +const INSTRUMENTAL_GRAMS: &[Gram] = &[ + // Fig. 8.13 + Gram { + label: "English with", + functions: &["co-agent", "comitative", "instrumental"], + }, + Gram { + label: "French par", + functions: &["passive", "cause"], + }, + Gram { + label: "German aus", + functions: &["source", "cause"], + }, + Gram { + label: "German von", + functions: &["passive", "source"], + }, + // Fig. 8.14, (21a–g) + Gram { + label: "Seychelles Creole ek", + functions: &[ + "instrumental", + "comitative", + "conjunctive", + "passive", + "source", + "cause", + "recipient", + "co-agent", + ], + }, + // Fig. 8.15 + Gram { + label: "Russian instrumental", + functions: &["instrumental", "passive"], + }, + Gram { + label: "Russian s", + functions: &["co-agent", "comitative", "conjunctive"], + }, +]; + +// ── (4): choosing between three arrangements of one sub-map ────────────── + +const SUB_FUNCTIONS: &[&str] = &["direction", "recipient", "purpose"]; + +const ARRANGE_A: Map = Map { + name: "(4a) purpose – direction – recipient", + functions: SUB_FUNCTIONS, + links: &[("purpose", "direction"), ("direction", "recipient")], +}; +const ARRANGE_B: Map = Map { + name: "(4b) direction – purpose – recipient", + functions: SUB_FUNCTIONS, + links: &[("direction", "purpose"), ("purpose", "recipient")], +}; +const ARRANGE_C: Map = Map { + name: "(4c) direction – recipient – purpose", + functions: SUB_FUNCTIONS, + links: &[("direction", "recipient"), ("recipient", "purpose")], +}; +/// Fig. 8.4: the vacuous map, every function linked to every other. +const VACUOUS: Map = Map { + name: "Fig. 8.4 vacuous", + functions: SUB_FUNCTIONS, + links: &[ + ("direction", "recipient"), + ("direction", "purpose"), + ("recipient", "purpose"), + ], +}; + +const SUB_GRAMS: &[Gram] = &[ + Gram { + label: "English to", + functions: &["direction", "recipient", "purpose"], + }, + // à Adam, à Leipzig; not purpose + Gram { + label: "French à", + functions: &["direction", "recipient"], + }, + // Anna ging zum Spielen; Ich gehe zu Anna; not recipient + Gram { + label: "German zu", + functions: &["direction", "purpose"], + }, +]; + +// ── (23): the Turkish old present as a doughnut ────────────────────────── + +const TENSE: Map = Map { + name: "(23) habitual – progressive – future", + functions: &["habitual", "progressive", "future"], + links: &[("habitual", "progressive"), ("progressive", "future")], +}; + +const TENSE_GRAMS: &[Gram] = &[ + Gram { + label: "German Present", + functions: &["habitual", "progressive", "future"], + }, + Gram { + label: "English Progressive", + functions: &["progressive", "future"], + }, + // ousted from progressive by -Iyor: the hole in the doughnut + Gram { + label: "Turkish old present", + functions: &["habitual", "future"], + }, +]; + +fn main() { + let tier = if cfg!(target_feature = "avx512f") { + "v4 (avx512f)" + } else { + "not v4" + }; + println!("tier: {tier}"); + + // 1. Every gram the chapter draws lies in one contiguous region. + for (map, grams) in [ + (&DATIVE, DATIVE_GRAMS), + (&INDEFINITE, INDEFINITE_GRAMS), + (&MIDDLE, MIDDLE_GRAMS), + (&INSTRUMENTAL, INSTRUMENTAL_GRAMS), + ] { + let broken = check(map, grams); + println!( + "{}: {} grams, non-contiguous {broken:?}", + map.name, + grams.len() + ); + assert!(broken.is_empty(), "{} is falsified by {broken:?}", map.name); + } + + // 2. The arrangement argument of (4): French and German eliminate (4b) + // and (4c); (4a) survives; the vacuous map admits everything. + let mut survivors = vec![]; + for map in [&ARRANGE_A, &ARRANGE_B, &ARRANGE_C, &VACUOUS] { + let broken = check(map, SUB_GRAMS); + println!("{}: non-contiguous {broken:?}", map.name); + if broken.is_empty() { + survivors.push(map.name); + } + } + assert_eq!(survivors, [ARRANGE_A.name, VACUOUS.name]); + assert_eq!(check(&ARRANGE_B, SUB_GRAMS), ["French à"]); + assert_eq!(check(&ARRANGE_C, SUB_GRAMS), ["German zu"]); + + // 3. A gram with a hole is caught: the Turkish old present. + let broken = check(&TENSE, TENSE_GRAMS); + println!("{}: non-contiguous {broken:?}", TENSE.name); + assert_eq!(broken, ["Turkish old present"]); + + // 4. Disable: with no closure steps only the seed is reached, so every + // gram that covers more than one function must read as broken. + let s = Space::new(&DATIVE, DATIVE_GRAMS); + let broken0 = (0..DATIVE_GRAMS.len()) + .filter(|&gi| s.reach(gi, Some(0)) != s.row(gi)) + .count(); + println!( + "disable (0 steps) on {}: {broken0} of {} broken", + DATIVE.name, + DATIVE_GRAMS.len() + ); + assert_eq!(broken0, DATIVE_GRAMS.len()); +} From e2b73883fb16db65c29efe7eafadf5715ca515c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 18:57:31 +0000 Subject: [PATCH 03/23] board: grammar-on-coordinates breadcrumb entry (annotations, TEKAMOLO x Antecedent loci, AriGraph episodic vs epistemic, CE64 42/52/59..63) Records the two probes, the haspelmath.rs prior art, gaps G1 (FSM relative antecedent not exported to Locus::Antecedent) and G2 (TEKAMOLO lanes vs loci 0..3, token vs event offsets), the CE64 50..52 reading conflict, and seven open decisions. Feeds the #1589 open-items ledger. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- ...6-grammar-coordinates-annotation-wiring.md | 131 ++++++++++++++++++ .claude/board/entries/README.md | 3 +- 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 .claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md diff --git a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md new file mode 100644 index 000000000..5630028da --- /dev/null +++ b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md @@ -0,0 +1,131 @@ +# 2026-10-16 — Grammar on coordinates: annotations, TEKAMOLO × Antecedent loci, AriGraph episodic vs epistemic, CE64 bits 42 / 52 / 59..63 + +**Status:** OPEN. A breadcrumb ledger for one design thread; it feeds the +loose ends of `2026-10-16-board-catchup-open-items.md` (#1589). It decides +nothing. Every claim cites the file it was read from (read in full, or through +a Haiku substrate map whose items were spot-checked against source); every +open point says what would close it. + +## 0. What landed in this thread (local commits, not pushed) + +| commit | what | measured (v4, tier printed by the program) | +|---|---|---| +| lance-graph `b0046b30` | `lance-graph-quack/examples/ruff_taint_probe.rs`: ruff_metta_spo's interpreter-only fixpoint recomputed as `Any(And(Plane(E), Plane(T_rep)))` per head extent | Hyperon 147 heads / PeTTa 1005 heads: 0 mismatches vs ruff; zero steps mismatch by exactly the call-tainted heads (9 / 71); dropping `T_rep` from the AND goes red | +| lance-graph `b4a35521` | `lance-graph-quack/examples/semantic_map_probe.rs`: Haspelmath (2003) grams as a gram × function plane over a function × function plane; contiguity = the same closure restricted to the gram's row | all 18 grams of Figs. 8.1/8.6/8.9/8.12 contiguous; (4b) broken by French à, (4c) by German zu; Turkish old present (23) caught; both disables red | +| ruff `4513c50af9` | `ruff_metta_spo/examples/metta_census.rs --dump` writes `heads.tsv` / `calls.tsv` | input of the taint probe | + +**Prior art missed, recorded so it is not repeated:** +`crates/deepnsm-v2/src/haspelmath.rs` already implements Fig. 8.1 continuity +(`continuity`, `FormAttestation` with `evidence_root`, `MapVerdict::Incomplete` +for incomplete coverage) at the FSM → hydrate → CE64 seam. The quack probe is +the same check on planes and covers Figs. 8.6–8.12 and (4)/(23), which +haspelmath.rs does not; it has no `Incomplete` verdict. OPEN: fold one into the +other (haspelmath.rs is the in-tree home; the probe's extra maps become its +fixtures) — not done. + +## 1. Datalog vs NAL vs ONA vs relalg as annotation readings of one fact + +| reading | where it lives (read) | what it adds to a fact at a coordinate | +|---|---|---| +| relalg / Datalog | `lance-graph-relalg/src/ir.rs` (`Rel`, `Atom`, `Measure`; no string by const assert); plan `relalg-v1.md` D-RA-3 (rules as algebra values), D-RA-4 (bounded μ) | set membership only; no truth, no stamp. μ and joins are "not built" (`lib.rs` doc, plan §4) | +| NAL | `lance-graph-nal/src/lib.rs`: key = NodeGuid read as `nal-statement-v1` `[classid, S, copula, P, id, 0, 0]` (lib.rs:7); f/c are value lanes beside the key; `HORIZON = 1` (lib.rs:216), `MAX_CONFIDENCE_CODE = 252` (lib.rs:220); revision refuses evidence without a stamp, `NalError::MissingProvenance` (lib.rs:515, nal.rs:298) | truth (f, c) + evidential stamp, revision gated on independence | +| ONA | oracle only (rules: no NARS clone); the NAL crate's constants are named after it | external answers to compare against | +| MeTTa | ruff `ruff_metta_spo` (harvest + classify) | which heads lower, call graph | + +Working model (not decided): one fact = one coordinate; each reading is an +**annotation plane or lane beside the key**, never a second store and never a +string. relalg supplies membership, NAL supplies (f, c, stamp) lanes, ONA and +Hyperon are oracles. Gap to watch: AriGraph's `TripletGraph::revise_with_evidence` +(`lance-graph/src/graph/arigraph/triplet_graph.rs:904`) revises on an exact +string match with **no stamp**, so it cannot tell independent evidence from a +repeat — the exact case the NAL crate refuses (and the rule "FirstOfRun is not +evidential independence"). + +## 2. TEKAMOLO × relativPronomen (Antecedent) — the 24 × i4 loci in DeepNSM-v2 + +Read in full: `deepnsm-v2/src/tekamolo.rs`, `deepnsm-v2/src/loci.rs`, +`lance-graph-contract/src/causal_witness.rs` (`Locus`), and the fsm.rs map. + +- The register: `CausalWitnessFacet`, 12 bytes = 24 signed i4 loci (−8..+7 + event offsets); 16 are named (`causal_witness.rs:116`): Temporal 0, Kausal 1, + Modal 2, Lokal 3, S/P/O meaning 4–6, **Antecedent 7** ("relativPronomen → its + antecedent"), BasinAnchor 8, SupportedBy 9, Supports 10, RunbookEvidence 11, + QualiaReference 12, MeaningLevel 13, Quorum 14, Contradiction 15. 8 slots are + unnamed. +- Who writes loci today: only `loci.rs` (`loci_from_triples`), and only + `Locus::Antecedent`, only for **pronoun subjects**, by recency or selectional + fit, refusing thin margins and anything beyond −8 (a basin edge, not an + overflow). A test pins that no other locus is ever written. +- **Gap G1 — relative-clause antecedent is computed but not exported.** The FSM's + one-level relative-clause sub-machine holds the antecedent in a register + (`fsm.rs:146` field `antecedent`, set at `fsm.rs:277`) and emits it inside the + SPO triple (object-relative: `Spo::new(es, verb, antecedent)`, `fsm.rs:232`). + The module doc calls it "the FSM-side feeder for the ±8 antecedent pointer in + `wave`" (`fsm.rs:32-33`, `:62-63`), but no code writes `Locus::Antecedent` + from it. Closing it: the parser reports, per emitted triple, the stream index + of the triple whose subject is the antecedent; loci writes `-(i - j)` when it + is within reach. +- **Gap G2 — TEKAMOLO lanes are filled, TEKAMOLO loci are not.** `tekamolo.rs` + writes the `ValueTenant::Tekamolo` facet (`LaneAddress {relation, rank}`, what + kind of circumstance); `LaneHypothesis.at` (`tekamolo.rs:162`) records the + TOKEN index that stated it. Loci 0–3 are EVENT offsets in the triple stream + (`loci.rs`: "one event per triple, at version = its stream index"). Unit + mismatch: a token index is not an event offset, so `at` cannot be copied into + a locus; it needs the token → triple-index map from the parse. +- The two meet: a relative clause ("der Mann, **der** gestern kam") carries + both an Antecedent pointer and its own TEKAMOLO lanes; the circumstance + belongs to the embedded event, the pointer to the matrix one. Which event a + lane attaches to is open. + +## 3. AriGraph episodic vs epistemic + +- **Episodic** exists: `EpisodicMemory` (`episodic.rs:64`), episodes with + `step`, observation text and `triplets: Vec` (`episodic.rs:50`) — + strings as content, against the no-strings rule; `EpisodicBasins::rail_rows` + (`episodic.rs:215`) moves basins onto `ValueTenant::EpisodicBasin` (D-ACR-6); + `episodic_search` = AriGraph Eq. 1. +- **Epistemic** has no AriGraph type. It lives in two places outside it: CE64 + bits 59..63 (`EpistemicState5`, `causal-edge/src/layout.rs:102-115`; meaning + per `(classid, rail, generation)` in `lance-graph-contract/src/epistemic_state5.rs`) + and the planner's read gate `EpistemicMode` (`lance-graph-planner/src/temporal.rs:77`, + Strict / Aware / Retro by rung). +- Working model: episodic = WHEN and IN WHICH EXPERIENCE (versions, steps, + basins); epistemic = HOW WELL KNOWN (topology × certification). Neither should + be inferred from the other. OPEN: AriGraph triplets carry `TruthValue` f32 and + a `timestamp`, but no epistemic state and no stamp. + +## 4. CE64 bits 42, 52, 59..63 (`causal-edge/src/layout.rs`, `pearl.rs`, `plasticity.rs`, `isa.rs`) + +| bits | field (code) | what the named bit means | +|---|---|---| +| 40..42 | Pearl `CausalMask`, 3 plane bits | **bit 42 = the S plane bit** (`CausalMask::S = 0b100`): set in S, SO (association), SP (confounder detection), SPO (counterfactual); clear in O, P, PO (intervention) | +| 50..52 | Plasticity, per plane hot/frozen (`isa.rs:360` `Field::Plasticity`) | **bit 52 = O-plane hot** (`PlasticityState::O_HOT = 0b100`) | +| 59..63 | `EpistemicState5`, one 5-bit field = topology (59-60) | certification << 2 (61-63) | D-EPI-CANON-0 | + +- **Two readings of 50..52 disagree.** The code names them plasticity; session + rule S7 (2026-10-12) records them as carrying the Staunen/witness ladder plus + a free-energy follow-up signal. #1589's N1 adds: `syllogize` writes bits 50..52 + as `0b111` and zeroes witness and epistemic bits. OPEN: which reading is the + contract; until decided, nothing new writes 50..52. +- `haspelmath.rs:10-13` keeps CE64 bits 24..63 at ZERO for grammar-only edges + ("syntax is not NARS confidence, Pearl causation, evidence independence or + epistemic maturity"). Any wiring of annotations into CE64 must keep that + boundary: a grammatical fit never sets Pearl, plasticity or epistemic bits. + +## 5. Open decisions (for the operator) + +| # | Decision | Recommendation | Why | Caveats | Needs from you | +|---|---|---|---|---|---| +| 1 | Annotation shape | lanes/planes beside the key, one per reading | one fact, many readings, no second store | relalg has no truth; NAL needs stamps the parser does not produce | OK on the model | +| 2 | G1 (relative antecedent → `Locus::Antecedent`) | build first, test-first, in deepnsm-v2 | the value is already computed; only the export is missing | needs triple-index provenance from `parse_readings` | go / no-go | +| 3 | G2 (TEKAMOLO → loci 0..3) | after G1, with the token → event map | lanes say what, loci say where | unit mismatch token vs event | go / no-go | +| 4 | CE64 bits 50..52 | settle the contract before any writer | code and rule S7 disagree; N1 shows a writer already clobbers it | — | ruling | +| 5 | Meaning of "42 52" in this request | treat as S-plane presence (42) and O-plane plasticity (52) per code | that is what the code says | may have meant a range | confirm | +| 6 | Haspelmath duplication | fold the quack probe's maps into `haspelmath.rs` fixtures | one home | haspelmath.rs is deepnsm-v2, the probe is quack | go / no-go | +| 7 | AriGraph revision without stamps | route revision through the NAL stamp gate | independence is otherwise unknown | AriGraph triplets are strings | go / no-go | + +## 6. Not searched + +ONA source (oracle only, not opened here); Hyperon's own grammar of +contexts beyond ruff's harvest; the 8 unnamed loci's intended use; any plan +that may already assign them (grep `Locus` in `.claude/plans` not run). diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 4cb428a34..fe5a4e894 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. -359 entries, 2026-08-06 .. 2026-10-16. +360 entries, 2026-08-06 .. 2026-10-16. | date | entry id | finding | file | |---|---|---|---| @@ -36,6 +36,7 @@ exactly one of them, never both. | 2026-10-16 | `D-ASM-1` | | [2026-10-16-metta-datalog-assimilation-council-v3.md](2026-10-16-metta-datalog-assimilation-council-v3.md) | | 2026-10-16 | `ledger-update-dedup-1579` | | [2026-10-16-ledger-update-dedup-1579.md](2026-10-16-ledger-update-dedup-1579.md) | | 2026-10-16 | `ledger-revision-after-1576` | | [2026-10-16-ledger-revision-after-1576.md](2026-10-16-ledger-revision-after-1576.md) | +| 2026-10-16 | `grammar-coordinates-annotation-wiring` | | [2026-10-16-grammar-coordinates-annotation-wiring.md](2026-10-16-grammar-coordinates-annotation-wiring.md) | | 2026-10-16 | `D-HOLO-H14d` | | [2026-10-16-d-holo-h14d-logic-mask-read.md](2026-10-16-d-holo-h14d-logic-mask-read.md) | | 2026-10-16 | `coresearch-wcoj-leapfrog` | | [2026-10-16-coresearch-wcoj-leapfrog.md](2026-10-16-coresearch-wcoj-leapfrog.md) | | 2026-10-16 | `board-catchup-open-items` | | [2026-10-16-board-catchup-open-items.md](2026-10-16-board-catchup-open-items.md) | From ef0aa59e5ddd5d7a22b032cdee244652367ce50d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:20:23 +0000 Subject: [PATCH 04/23] deepnsm-v2: wire every CE64 field of a parsed triple from its source wire.rs fills the semantic upper half of each parsed triple's edge: F/C from lance-graph-arm-discovery's arm_to_truth_u8 over independent verses; the Pearl rung from the TEKAMOLO Kausal lane (PO) or SO; the orientation code (43..45) from the traced clause path on a Keenan-Comrie syntactic-function map; activation (46..49) as signed semantic pressure pos - neg; the novelty -> wisdom ladder (50..52) as a per-statement Markov state; the W1/W2/W3 window of the last three belief-state updates (53..58); and EpistemicState5 (59..63) from the clause path and the belief state. Antecedent and TEKAMOLO loci land in the 24 x i4 facet. fsm.rs: parse_to_spo_traced reports each triple's clause path. tekamolo.rs: ClauseRead records the stating token per lane. causal-edge: novelty_wisdom / activation accessors over bits 50..52 and 46..49 (same bits as plasticity / inference_mantissa). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/causal-edge/src/edge.rs | 98 ++++ crates/deepnsm-v2/Cargo.toml | 3 + crates/deepnsm-v2/src/fsm.rs | 145 ++++++ crates/deepnsm-v2/src/lib.rs | 1 + crates/deepnsm-v2/src/tekamolo.rs | 11 + crates/deepnsm-v2/src/wire.rs | 747 ++++++++++++++++++++++++++++++ 6 files changed, 1005 insertions(+) create mode 100644 crates/deepnsm-v2/src/wire.rs diff --git a/crates/causal-edge/src/edge.rs b/crates/causal-edge/src/edge.rs index fee1b1cf4..9f9aab322 100644 --- a/crates/causal-edge/src/edge.rs +++ b/crates/causal-edge/src/edge.rs @@ -1103,6 +1103,54 @@ impl CausalEdge64 { Self((self.0 & !W_MASK) | (((w as u64) & BITS6_MASK) << W_SHIFT)) } + /// Rung on the novelty → wisdom ladder over bits 50..52: `0` = not on the + /// ladder (unaddressed, the zero-fallback), `1` = Staunen (first sighting, + /// unresolved) ..= `7` = wisdom (settled). A rung is climbed one step at a + /// time and only while the step below holds (the Maslow shape); the + /// semantic pressure in bits 46..49 ([`Self::activation`]) is what moves it. + /// + /// These are the same three bits [`Self::plasticity`] reads. The CE64 + /// semantic upper-half contract (2026-10-16: 40..42 Pearl ladder, 43..45 + /// orientation/syntax, 46..49 activation i4, 50..52 entropy/novelty, 53..58 + /// belief-state update witness, 59..63 epistemic state) names this field + /// entropy/novelty; per-plane plasticity is the older reading of it. One + /// field, two vocabularies, no bits moved. A writer picks one vocabulary + /// per class and says which. + #[inline(always)] + pub fn novelty_wisdom(self) -> u8 { + ((self.0 >> crate::layout::PLAST_SHIFT) & crate::layout::BITS3_MASK) as u8 + } + + /// Return new edge with the novelty → wisdom rung (bits 50..52) set. + /// Values above 7 saturate to 7 rather than wrapping to a lower rung. + #[inline] + pub fn with_novelty_wisdom(self, level: u8) -> Self { + use crate::layout::{BITS3_MASK, PLAST_MASK, PLAST_SHIFT}; + let level = u64::from(level.min(7)); + Self((self.0 & !PLAST_MASK) | ((level & BITS3_MASK) << PLAST_SHIFT)) + } + + /// Semantic pressure over bits 46..49, a signed i4 (`+7` excitation, `0` + /// neutral, `-8` inhibition): the net evidence pushing a statement up or + /// down the [`Self::novelty_wisdom`] ladder. + /// + /// These are the same four bits [`Self::inference_mantissa`] reads. The + /// semantic upper-half contract (2026-10-16) names the field activation; + /// the ISA reads it as a signed NARS rule index. One field, two + /// vocabularies: an edge written with a pressure must not be decoded as an + /// inference rule ([`Self::inference_type`]), and the reverse. + #[inline(always)] + pub fn activation(self) -> i8 { + self.inference_mantissa() + } + + /// Return new edge with the semantic pressure (bits 46..49) set, clamped + /// to `-8..=7` rather than wrapping to the opposite sign. + #[inline] + pub fn with_activation(self, pressure: i8) -> Self { + self.with_inference_mantissa(pressure.clamp(-8, 7)) + } + /// Return new edge with bits 59..63 set to the 5-bit `EpistemicState5` /// code `code` (`0..=31`). The ONE canonical writer of the field: all five /// bits move together, nothing else moves. @@ -1331,6 +1379,22 @@ impl CausalEdge64 { pub fn with_w_slot(self, _w: u8) -> Self { self } + #[inline(always)] + pub fn novelty_wisdom(self) -> u8 { + 0 + } + #[inline] + pub fn with_novelty_wisdom(self, _level: u8) -> Self { + self + } + #[inline(always)] + pub fn activation(self) -> i8 { + 0 + } + #[inline] + pub fn with_activation(self, _pressure: i8) -> Self { + self + } #[deprecated( since = "0.2.0", note = "writes bits 59..63 under a reading that is not the declared EpistemicState5 one (TrustTexture, historical ReasoningBand, or untyped spare); its bits would then be consumed as canonical without a declaration. Write a state from `lance_graph_contract::epistemic_state5` with `with_epistemic_raw5`, or the topology factor with `with_topology`" @@ -1414,6 +1478,40 @@ impl std::fmt::Debug for CausalEdge64 { mod tests { use super::*; + /// Novelty reads and writes bits 50..52 only, saturates at 7, and is the + /// same register plasticity reads (one field, two vocabularies). + #[cfg(feature = "causal-edge-v2-layout")] + #[test] + fn novelty_wisdom_owns_bits_50_to_52_only() { + let base = CausalEdge64(0xFFFF_FFFF_FFFF_FFFF ^ (0b111 << 50)); + for level in 0..=7u8 { + let e = base.with_novelty_wisdom(level); + assert_eq!(e.novelty_wisdom(), level); + assert_eq!(e.0 & !(0b111 << 50), base.0, "no other bit moved"); + assert_eq!(e.plasticity().bits(), level, "same three bits"); + } + assert_eq!( + CausalEdge64::ZERO.with_novelty_wisdom(200).novelty_wisdom(), + 7 + ); + } + + /// Activation reads and writes bits 46..49 only, as a signed i4, clamps + /// instead of wrapping, and is the register the inference mantissa reads. + #[cfg(feature = "causal-edge-v2-layout")] + #[test] + fn activation_owns_bits_46_to_49_only() { + let base = CausalEdge64(0xFFFF_FFFF_FFFF_FFFF ^ (0b1111 << 46)); + for p in -8..=7i8 { + let e = base.with_activation(p); + assert_eq!(e.activation(), p); + assert_eq!(e.0 & !(0b1111 << 46), base.0, "no other bit moved"); + assert_eq!(e.inference_mantissa(), p, "same four bits"); + } + assert_eq!(CausalEdge64::ZERO.with_activation(100).activation(), 7); + assert_eq!(CausalEdge64::ZERO.with_activation(-100).activation(), -8); + } + /// The canonical byte form is little-endian and round-trips every bit, /// including bits 59..63 (byte 7, bits 3..7). #[test] diff --git a/crates/deepnsm-v2/Cargo.toml b/crates/deepnsm-v2/Cargo.toml index fed14a490..80d2baf71 100644 --- a/crates/deepnsm-v2/Cargo.toml +++ b/crates/deepnsm-v2/Cargo.toml @@ -30,6 +30,9 @@ lance-graph-contract = { path = "../lance-graph-contract", features = ["guid-v3- # The Pearl 2³ ladder: `fsm::answered_questions` returns its `CausalMask`, the # same eight masks CausalEdge64 packs at bits [40:42]. Zero-dependency crate. causal-edge = { path = "../causal-edge" } +# F/C of wired edges: arm-discovery's `arm_to_truth_u8` (the OpenCog/learn- +# adjacent rule miner). Zero dependencies by default. +lance-graph-arm-discovery = { path = "../lance-graph-arm-discovery" } # `test = true` makes `cargo test` run the example's own `#[cfg(test)]` tests # (the D-LXC-1 tagger). Cargo only compiles an example by default; its `main()` diff --git a/crates/deepnsm-v2/src/fsm.rs b/crates/deepnsm-v2/src/fsm.rs index 3eff8e4ee..fe56b9666 100644 --- a/crates/deepnsm-v2/src/fsm.rs +++ b/crates/deepnsm-v2/src/fsm.rs @@ -371,6 +371,151 @@ pub fn answered_questions(tokens: &[Tagged]) -> Vec { out } +/// How a triple's slots came off the surface: the clause path that emitted it. +/// +/// This is what `CausalEdge64`'s orientation field (bits 43..45) reads — which +/// way the relation is read — and what the relative-clause antecedent pointer +/// (`Locus::Antecedent`) is derived from. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Origin { + /// Matrix clause, subject taken from the clause's own surface subject. + Main, + /// Matrix clause, subject carried over from the previous triple's object + /// (serial-verb chain: "gave **him** the charge"). + Carried, + /// Object-relative clause ("rat that cat bit"): the OBJECT is the + /// relativizer's antecedent, read before the embedded subject and verb. + ObjRelative, + /// Subject-relative clause ("dog that chased cat"): the SUBJECT is the + /// relativizer's antecedent. + SubjRelative, +} + +impl Origin { + /// Is the triple emitted inside a relative clause? + #[must_use] + pub const fn is_relative(self) -> bool { + matches!(self, Origin::ObjRelative | Origin::SubjRelative) + } +} + +/// One triple with the clause path that emitted it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Traced { + /// The triple, exactly as [`parse_to_spo`] emits it. + pub spo: Spo, + /// Which clause path emitted it. + pub origin: Origin, + /// Sentence ordinal within the input (incremented at every `Stop`). + pub sentence: u32, + /// Input index of the token that closed the triple. + pub closed_at: usize, +} + +/// [`parse_to_spo`] with each triple's [`Origin`]. Steps the SAME transition +/// table; the triples are identical to [`parse_to_spo`]'s, in the same order. +#[must_use] +pub fn parse_to_spo_traced(tokens: &[Tagged]) -> Vec { + let mut out = Vec::new(); + let mut core = Core::START; + let mut sentence = 0u32; + for (i, &t) in tokens.iter().enumerate() { + if t.pos == Pos::Stop { + core.state = State::Start; + core.rel = Rel::None; + sentence += 1; + continue; + } + // The path is decided by the registers BEFORE the closing token. + let origin = match core.rel { + Rel::ObjSubject(_) => Origin::ObjRelative, + Rel::HaveVerb { .. } => Origin::SubjRelative, + Rel::Open | Rel::None if core.subject_carried => Origin::Carried, + Rel::Open | Rel::None => Origin::Main, + }; + if let Some(spo) = core.step(t) { + out.push(Traced { + spo, + origin, + sentence, + closed_at: i, + }); + } + } + out +} + +#[cfg(test)] +mod traced_tests { + use super::*; + + fn n(id: WordId) -> Tagged { + Tagged::new(id, Pos::Noun) + } + fn v(id: WordId) -> Tagged { + Tagged::new(id, Pos::Verb) + } + fn det() -> Tagged { + Tagged::new(0, Pos::Det) + } + fn rel() -> Tagged { + Tagged::new(0, Pos::Rel) + } + fn stop() -> Tagged { + Tagged::new(0, Pos::Stop) + } + + /// The traced parse emits exactly `parse_to_spo`'s triples, in order, on + /// inputs that exercise all four paths. + #[test] + fn traced_triples_equal_parse_to_spo() { + let streams: Vec> = vec![ + vec![det(), n(1), rel(), det(), n(2), v(3), v(4), det(), n(5)], + vec![det(), n(1), rel(), v(2), det(), n(3), v(4), det(), n(5)], + vec![n(1), v(2), n(3), v(4), n(5), stop(), n(6), v(7), n(8)], + ]; + for s in streams { + let traced: Vec = parse_to_spo_traced(&s).iter().map(|t| t.spo).collect(); + assert_eq!(traced, parse_to_spo(&s)); + assert!(!traced.is_empty(), "anti-vacuity: the stream emits"); + } + } + + /// Each path is named: object-relative, subject-relative, main, carried. + /// Two-sided: every stream carries at least two different origins. + #[test] + fn each_clause_path_is_named() { + let obj = parse_to_spo_traced(&[det(), n(1), rel(), det(), n(2), v(3), v(4), det(), n(5)]); + assert_eq!( + obj.iter().map(|t| t.origin).collect::>(), + vec![Origin::ObjRelative, Origin::Main] + ); + assert_eq!( + obj[0].spo, + Spo::new(2, 3, 1), + "the antecedent is the object" + ); + + let subj = parse_to_spo_traced(&[det(), n(1), rel(), v(2), det(), n(3), v(4), det(), n(5)]); + assert_eq!( + subj.iter().map(|t| t.origin).collect::>(), + vec![Origin::SubjRelative, Origin::Main] + ); + assert_eq!(subj[0].spo.subject, 1, "the antecedent is the subject"); + + let chain = parse_to_spo_traced(&[n(1), v(2), n(3), v(4), n(5), stop(), n(6), v(7), n(8)]); + assert_eq!( + chain + .iter() + .map(|t| (t.origin, t.sentence)) + .collect::>(), + vec![(Origin::Main, 0), (Origin::Carried, 0), (Origin::Main, 1)] + ); + assert_eq!(chain[0].closed_at, 2); + assert_eq!(chain[2].closed_at, 8); + } +} + // ───────────────────────────────────────────────────────────────────────────── // Multi-reading input (D-LXC-2) // ───────────────────────────────────────────────────────────────────────────── diff --git a/crates/deepnsm-v2/src/lib.rs b/crates/deepnsm-v2/src/lib.rs index b0de36863..77ec24c1d 100644 --- a/crates/deepnsm-v2/src/lib.rs +++ b/crates/deepnsm-v2/src/lib.rs @@ -57,6 +57,7 @@ pub mod tekamolo; pub mod toc; pub mod vocab; pub mod wave; +pub mod wire; use lance_graph_contract::temporal_pov::{TemporalPov, VersionRange}; diff --git a/crates/deepnsm-v2/src/tekamolo.rs b/crates/deepnsm-v2/src/tekamolo.rs index ab0f08721..11e14c269 100644 --- a/crates/deepnsm-v2/src/tekamolo.rs +++ b/crates/deepnsm-v2/src/tekamolo.rs @@ -467,6 +467,10 @@ pub struct ClauseRead { pub abstained: usize, /// Lanes whose winner was an ambiguous form. pub ambiguous_wins: usize, + /// Per committed lane, the token index of the winning form — WHERE the + /// circumstance was stated, in Te-Ka-Mo-Lo order. `None` exactly where + /// [`Self::lanes`] is `None`. + pub at: [Option; 4], } /// Read ONE clause's tokens under `params`. @@ -546,6 +550,7 @@ pub fn read_clause(h: &GrammarHeuristics, tokens: &[&str], params: ReadParams) - out.ambiguous_wins += 1; } out.lanes[li] = Some(best.addr); + out.at[li] = Some(best.at); } out } @@ -653,6 +658,12 @@ mod tests { "right corner adjudicates to `damit` (rank 1) on the evidence" ); assert_ne!(left.lanes[k], right.lanes[k]); + // WHERE each corner found its Kausal form: `weil` at 0, `damit` at 3. + assert_eq!(left.at[k], Some(0)); + assert_eq!(right.at[k], Some(3)); + // Unaddressed lanes report no position. + let t = TekamoloRole::Temporal as usize; + assert_eq!((right.lanes[t], right.at[t]), (None, None)); } /// An ambiguous form may win only where the style admits it — and the diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs new file mode 100644 index 000000000..e411da266 --- /dev/null +++ b/crates/deepnsm-v2/src/wire.rs @@ -0,0 +1,747 @@ +//! `wire` — every `CausalEdge64` field of a parsed triple, filled from the one +//! source that grounds it, at the address [`crate::hydrate`] gives it. +//! +//! The field contract is the CE64 semantic upper half (2026-10-16). Each row of +//! the table names the field's source. A field whose source says nothing stays +//! at its zero-fallback and the [`WireReport`] counts it; nothing is guessed to +//! make a field non-zero. [`crate::haspelmath::run_harness`] still emits the +//! grammar-only edge (bits 24..63 zero). This module is the layer above it that +//! adds annotations, each from a named source and never from the grammatical +//! fit. +//! +//! | bits | field | source | +//! |---|---|---| +//! | 0..23 | S, P, O codes | `run_harness`'s frozen operand ABI | +//! | 24..31 | frequency | `lance-graph-arm-discovery` [`arm_to_truth_u8`], rule `(S, P) → O` over independent verses: `f = cooccur / antecedent` | +//! | 32..39 | confidence | the same: `c = cooccur / (cooccur + k)`, `k = 1` (the NAL horizon) | +//! | 40..42 | Pearl ladder | the TEKAMOLO read: a Kausal lane stated on this event → `PO` (the claim sits at the intervention rung); otherwise `SO` (association) | +//! | 43..45 | orientation / syntax | the clause path ([`Origin`]) as a code on [`ORIENTATION_MAP`]; `0` = untraced | +//! | 46..49 | activation i4 | semantic pressure `pos - neg`, clamped to `-8..=7` | +//! | 50..52 | novelty → wisdom | the statement's rung after this update, `1` Staunen ..= `7` wisdom | +//! | 53..58 | belief-state update | the statement's last three updates: `W1` prior (53..54), `W2` (55..56), `W3` newest (57..58) | +//! | 59..63 | epistemic state | `EpistemicState5`: topology from the clause path, certification from the belief state | +//! +//! # The belief state is a Markov chain per statement +//! +//! A statement `(S, P, O)` has one state: `pos`, its independent supporting +//! verses; `tot`, the independent verses that gave `(S, P)` at all (so +//! `neg = tot - pos` gave another object); its rung; and its window. Each +//! observation updates the states it touches and nothing else, so the next +//! state depends only on the current state and the observation: +//! +//! - an observation of `(S, P, O)` from a verse not yet counted for it is a +//! **corroboration** (`1`) of `(S, P, O)`; +//! - if that verse is also new for `(S, P)`, it is a **contest** (`2`) of every +//! other `(S, P, O')`; +//! - an observation from a verse already counted is **dependent** (`3`): no new +//! evidence, the NAL `DUPLICATE` / `OVERLAP` class. +//! +//! The window shifts the newest kind in at `W3`, and `W1` falls off. Each row +//! records the state of its own statement right after its own update, so the +//! rows of one statement, in stream order, are its time series. +//! +//! # The ladder (bits 50..52) +//! +//! A first sighting sits at rung 1 (Staunen). A corroboration climbs one rung +//! only while the observation is resolved and the pressure stays positive: a +//! rung is reached only when the one below holds. Resolved means single +//! readings, traced, no incomplete Haspelmath map, no TEKAMOLO abstention and, +//! for a relative clause, a bound antecedent. A contest drops the statement +//! back to rung 1. Six resolved corroborations after the first sighting, +//! without a contest, reach wisdom. +//! +//! # F/C against NAL revision +//! +//! `f` equals NAL revision of the unit observations (`pos` positive and `neg` +//! negative units at horizon 1). `c` equals it only for an uncontested +//! statement: arm-discovery counts confidence over the co-occurrence mass +//! (`c = pos / (pos + 1)`), NAL over all evidence (`c = tot / (tot + 1)`). +//! `f_and_c_against_nal_revision_of_units` pins both halves. +//! +//! # The orientation map (bits 43..45) +//! +//! A deterministic code on a semantic map of the syntactic functions the FSM +//! decides, in the Haspelmath sense: nodes are functions, and links say which +//! functions may share a form. The links follow the relativization +//! accessibility order (subject above object, Keenan & Comrie 1977): +//! `Main – Carried`, `Main – SubjRelative`, `SubjRelative – ObjRelative`. +//! Haspelmath's dative map is not encoded here: its eight functions would use +//! all eight codes and leave no unaddressed value, and [`crate::haspelmath`] +//! keeps its ordinals harness-local. + +use std::collections::{HashMap, HashSet}; + +use causal_edge::{CausalEdge64, CausalMask}; +use lance_graph_arm_discovery::{arm_to_truth_u8, CandidateRule, Item}; +use lance_graph_contract::causal_witness::{CausalWitnessFacet, Locus}; +use lance_graph_contract::epistemic_state5::{Certification3, Epi5Gen, EpistemicState5, Topology2}; +use lance_graph_contract::hhtl::NiblePath; + +use crate::fsm::{parse_readings, parse_to_spo_traced, Origin, Pos, Reading, Tagged, Traced}; +use crate::haspelmath::{ + continuity, run_harness, FormAttestation, FrozenOperandAbi, HarnessError, MapVerdict, +}; +use crate::spo::Spo; +use crate::tekamolo::{read_clause, GrammarHeuristics, LaneAddress, ReadParams}; + +/// The TEKAMOLO lane index of Kausal, in Te-Ka-Mo-Lo order. +pub const KAUSAL_LANE: usize = 1; + +/// The orientation code of a clause path (bits 43..45). `0` is reserved for +/// "untraced", so a zero field never claims a path. +#[must_use] +pub const fn orientation_code(origin: Origin) -> u8 { + match origin { + Origin::Main => 1, + Origin::Carried => 2, + Origin::SubjRelative => 3, + Origin::ObjRelative => 4, + } +} + +/// The syntactic-function map: for each code `0..=4`, its neighbours as a +/// bitmask over codes (bit `c` = code `c`). Code 0 is on no map. +pub const ORIENTATION_MAP: [u8; 5] = [ + 0, + (1 << 2) | (1 << 3), // Main: Carried, SubjRelative + 1 << 1, // Carried: Main + (1 << 1) | (1 << 4), // SubjRelative: Main, ObjRelative + 1 << 3, // ObjRelative: SubjRelative +]; + +/// Is a set of orientation codes (bitmask over codes `1..=4`) one connected +/// region of [`ORIENTATION_MAP`]? The empty set, and any set containing code +/// 0 or a code above 4, is not. +#[must_use] +pub fn orientation_contiguous(codes: u8) -> bool { + if codes == 0 || codes & 1 != 0 || codes >> 5 != 0 { + return false; + } + let mut seen = 1u8 << codes.trailing_zeros(); + loop { + let mut next = seen; + for (c, links) in ORIENTATION_MAP.iter().enumerate() { + if seen & (1 << c) != 0 { + next |= links & codes; + } + } + if next == seen { + return seen == codes; + } + seen = next; + } +} + +/// The kind of one belief-state update (a 2-bit witness). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Update { + /// An independent verse supports the statement. + Corroborate = 1, + /// An independent verse gave the same `(S, P)` with another object. + Contest = 2, + /// The verse was already counted: no new evidence. + Dependent = 3, +} + +/// Shift `kind` into a 6-bit window at `W3` (bits 4..5); `W1` falls off. +#[must_use] +pub const fn shift_window(window: u8, kind: Update) -> u8 { + ((window & 0x3F) >> 2) | ((kind as u8) << 4) +} + +/// The three updates of a window, `[W1 prior, W2, W3 newest]`; `0` = empty. +#[must_use] +pub const fn window_slots(window: u8) -> [u8; 3] { + [window & 0b11, (window >> 2) & 0b11, (window >> 4) & 0b11] +} + +/// One verse as the caller has it. +#[derive(Debug, Clone, Default)] +pub struct VerseIn<'a> { + /// The verse's tokens with their admissible readings (no trailing stop). + pub readings: Vec, + /// The surface forms, one per reading, for the TEKAMOLO read. Empty means + /// "no surface text": the TEKAMOLO lanes and loci stay unaddressed. + pub surface: Vec<&'a str>, + /// Haspelmath form attestations cited for this verse. + pub forms: Vec, +} + +/// One wired triple. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct WiredRow { + /// The verse node's address. + pub path: NiblePath, + /// Corpus-order index (the Markov index of [`crate::hydrate`]). + pub order: u32, + /// Which input verse it was read in. + pub verse: usize, + /// The triple. + pub spo: Spo, + /// The 64-bit edge, every field set from its source. + pub edge: CausalEdge64, + /// The 24 × i4 loci. + pub loci: CausalWitnessFacet, + /// TEKAMOLO lane addresses, on the event that stated them only. + pub tekamolo: [Option; 4], + /// The clause path, when the verse could be traced. + pub origin: Option, + /// Independent verses supporting the statement so far (`pos`). + pub support: u32, + /// Independent verses that gave `(S, P)` so far (`tot`). + pub antecedent: u32, +} + +/// What the wiring did, so a zero field is never mistaken for a reading. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct WireReport { + /// Rows written. + pub rows: usize, + /// Verses with a multi-reading token or a parse mismatch: orientation 0. + pub untraced_verses: usize, + /// Relative-clause rows whose antecedent event was found within reach. + pub antecedent_bound: usize, + /// Relative-clause rows whose antecedent event was not found. + pub antecedent_unbound: usize, + /// TEKAMOLO loci written (a pointer to another event of the verse). + pub tekamolo_loci: usize, + /// TEKAMOLO lanes committed on a stating event. + pub tekamolo_lanes: usize, + /// Rows at the intervention rung (a Kausal lane stated on the event). + pub intervention_rows: usize, + /// Contest updates applied to other statements. + pub contests: usize, + /// Dependent observations (a verse already counted). + pub dependent: usize, + /// Rows certified `CausalCandidate`. + pub causal_candidates: usize, +} + +const REACH_BACK: i64 = -8; +const REACH_FWD: i64 = 7; + +fn offset(from: usize, to: usize) -> Option { + let d = to as i64 - from as i64; + (d != 0 && (REACH_BACK..=REACH_FWD).contains(&d)).then_some(d as i8) +} + +/// The traced clause paths of one verse, or `None` when a token has more than +/// one reading or the traced parse disagrees with the multi-reading one. +fn trace(readings: &[Reading], certain: &[Spo]) -> Option> { + let mut tagged = Vec::with_capacity(readings.len() + 1); + for r in readings { + if r.pos.len() != 1 { + return None; + } + tagged.push(Tagged::new(r.id, r.pos.iter().next()?)); + } + tagged.push(Tagged::new(0, Pos::Stop)); + let traced = parse_to_spo_traced(&tagged); + let same = + traced.len() == certain.len() && traced.iter().zip(certain).all(|(t, c)| t.spo == *c); + same.then_some(traced) +} + +/// The state of one statement `(S, P, O)`. +#[derive(Debug, Clone, Default)] +struct StmtState { + verses: HashSet, + causal_verses: HashSet, + window: u8, + rung: u8, +} + +#[derive(Debug, Default)] +struct Group { + verses: HashSet, + objects: HashMap, +} + +/// The arm-discovery rule `(S, P) → O` over the counts so far. +fn rule(spo: Spo, pos: u32, tot: u32, window: u32) -> CandidateRule { + CandidateRule { + antecedent: vec![ + Item::new(0, u32::from(spo.subject)), + Item::new(1, u32::from(spo.predicate)), + ], + consequent: vec![Item::new(2, u32::from(spo.object))], + cooccur: pos, + antecedent_count: tot, + window, + } +} + +/// Wire every certain triple of the corpus. +/// +/// # Errors +/// +/// Exactly what [`run_harness`] refuses: unaligned verses, missing evidence +/// roots, a disconnected complete distribution, parser overflow, unaddressed +/// triples, operands outside the frozen ABI. +pub fn wire( + markers: &[(u16, u16)], + verses: &[VerseIn<'_>], + basin: u8, + abi: &FrozenOperandAbi, + heuristics: Option<&GrammarHeuristics>, + params: ReadParams, +) -> Result<(Vec, WireReport), HarnessError> { + let readings: Vec> = verses.iter().map(|v| v.readings.clone()).collect(); + let forms: Vec> = verses.iter().map(|v| v.forms.clone()).collect(); + let harness = run_harness(markers, &readings, &forms, basin, abi)?; + + let mut rep = WireReport::default(); + let mut rows: Vec = Vec::with_capacity(harness.encoded.len()); + let mut groups: HashMap<(u16, u16), Group> = HashMap::new(); + let mut next = 0usize; + + for (vi, verse) in verses.iter().enumerate() { + let mut input = verse.readings.clone(); + input.push(Reading::stop()); + let parsed = parse_readings(&input); + let n = parsed.certain.len(); + if n == 0 { + continue; + } + let traced = trace(&verse.readings, &parsed.certain); + if traced.is_none() { + rep.untraced_verses += 1; + } + let incomplete = verse + .forms + .iter() + .any(|f| continuity(f.functions, f.coverage_complete) == MapVerdict::Incomplete); + let ambiguous = !parsed.ambiguous.is_empty(); + + // TEKAMOLO: one read over the verse; each committed lane lands on the + // event whose clause holds the stating token. + let clause = match heuristics { + Some(h) if !verse.surface.is_empty() && verse.surface.len() == verse.readings.len() => { + Some(read_clause(h, &verse.surface, params)) + } + _ => None, + }; + let abstained = clause.is_some_and(|c| c.abstained > 0); + let stating: [Option; 4] = std::array::from_fn(|lane| { + let at = clause?.at[lane]?; + let t = traced.as_ref()?; + Some( + t.iter() + .position(|x| x.closed_at >= at) + .unwrap_or(t.len() - 1), + ) + }); + + let first = rows.len(); + for k in 0..n { + let h = harness.hydration.triples[next]; + let enc = harness.encoded[next]; + next += 1; + rows.push(WiredRow { + path: h.path, + order: h.order, + verse: vi, + spo: h.spo, + edge: enc.word, + loci: CausalWitnessFacet::ZERO, + tekamolo: [None; 4], + origin: traced.as_ref().map(|t| t[k].origin), + support: 0, + antecedent: 0, + }); + } + + // Relative-clause antecedent: the matrix event of the same sentence + // whose subject is the antecedent word. + let mut unbound = vec![false; n]; + if let Some(t) = traced.as_ref() { + for (k, tr) in t.iter().enumerate() { + if !tr.origin.is_relative() { + continue; + } + let ante_word = match tr.origin { + Origin::SubjRelative => tr.spo.subject, + _ => tr.spo.object, + }; + let matrix = t.iter().enumerate().skip(k + 1).find(|(_, m)| { + m.sentence == tr.sentence + && !m.origin.is_relative() + && m.spo.subject == ante_word + }); + match matrix.and_then(|(j, _)| offset(k, j)) { + Some(o) => { + rows[first + k].loci = rows[first + k].loci.with(Locus::Antecedent, o); + rep.antecedent_bound += 1; + } + None => { + unbound[k] = true; + rep.antecedent_unbound += 1; + } + } + } + } + + // TEKAMOLO lanes on the stating event, loci elsewhere in the verse. + for (lane, at) in stating.iter().enumerate() { + let (Some(j), Some(c)) = (*at, clause) else { + continue; + }; + rows[first + j].tekamolo[lane] = c.lanes[lane]; + rep.tekamolo_lanes += 1; + for k in 0..n { + if let Some(o) = offset(k, j) { + rows[first + k].loci = rows[first + k].loci.with(Locus::ALL[lane], o); + rep.tekamolo_loci += 1; + } + } + } + + // The belief-state update, row by row in stream order. + for (k, row) in rows[first..].iter_mut().enumerate() { + let spo = row.spo; + let causal = row.tekamolo[KAUSAL_LANE].is_some(); + let resolved = + traced.is_some() && !ambiguous && !incomplete && !abstained && !unbound[k]; + + let group = groups.entry((spo.subject, spo.predicate)).or_default(); + if group.verses.insert(vi) { + for (o, other) in &mut group.objects { + if *o != spo.object { + other.window = shift_window(other.window, Update::Contest); + if other.rung > 0 { + other.rung = 1; + } + rep.contests += 1; + } + } + } + let tot = group.verses.len() as u32; + let st = group.objects.entry(spo.object).or_default(); + let new_stmt = st.verses.is_empty(); + let own_fresh = st.verses.insert(vi); + if causal { + st.causal_verses.insert(vi); + } + let pos = st.verses.len() as u32; + let pressure = (i64::from(pos) - i64::from(tot - pos)).clamp(-8, 7) as i8; + let kind = if own_fresh { + Update::Corroborate + } else { + rep.dependent += 1; + Update::Dependent + }; + st.window = shift_window(st.window, kind); + if new_stmt { + st.rung = 1; + } else if kind == Update::Corroborate && resolved && pressure > 0 { + st.rung = (st.rung + 1).min(7); + } + + let truth = arm_to_truth_u8(&rule(spo, pos, tot, (vi + 1) as u32), 1); + let pearl = if causal { + rep.intervention_rows += 1; + CausalMask::PO + } else { + CausalMask::SO + }; + let topology = match row.origin { + None => Topology2::Unknown, + Some(Origin::Main) => Topology2::Direct, + Some(_) if unbound[k] => Topology2::IndirectUnknown, + Some(_) => Topology2::IndirectKnown, + }; + let certification = if pos >= 2 && pressure > 0 { + if st.causal_verses.len() >= 2 { + rep.causal_candidates += 1; + Certification3::CausalCandidate + } else { + Certification3::Related + } + } else { + Certification3::Associated + }; + let state = EpistemicState5::new(Epi5Gen::V1, topology, certification); + + let mut edge = row.edge; + edge.set_frequency_u8(truth.frequency); + edge.set_confidence_u8(truth.confidence); + edge.set_causal_mask(pearl); + edge.set_direction(row.origin.map_or(0, orientation_code)); + row.edge = edge + .with_activation(pressure) + .with_novelty_wisdom(st.rung) + .with_w_slot(st.window) + .with_epistemic_raw5(state.raw()); + row.support = pos; + row.antecedent = tot; + } + rep.rows = rows.len(); + } + Ok((rows, rep)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fsm::PosSet; + use lance_graph_contract::exploration::NarsTruth; + + const DOG: u16 = 10; + const MAN: u16 = 11; + const CAT: u16 = 12; + const BIT: u16 = 20; + const SAW: u16 = 21; + const THE: u16 = 30; + const WHO: u16 = 31; + + fn one(id: u16, pos: Pos) -> Reading { + Reading::new(id, PosSet::single(pos)) + } + + /// `the S V the O`. + fn svo(s: u16, v: u16, o: u16) -> Vec { + vec![ + one(THE, Pos::Det), + one(s, Pos::Noun), + one(v, Pos::Verb), + one(THE, Pos::Det), + one(o, Pos::Noun), + ] + } + + fn abi() -> FrozenOperandAbi { + FrozenOperandAbi::new(1, &[DOG, MAN, CAT, BIT, SAW, THE, WHO]).unwrap() + } + + fn run(verses: &[Vec]) -> (Vec, WireReport) { + let markers: Vec<(u16, u16)> = (0..verses.len() as u16).map(|i| (1, i + 1)).collect(); + let ins: Vec> = verses + .iter() + .map(|r| VerseIn { + readings: r.clone(), + ..VerseIn::default() + }) + .collect(); + wire(&markers, &ins, 1, &abi(), None, ReadParams::RIGHT_CORNER).unwrap() + } + + fn epi(e: CausalEdge64) -> EpistemicState5 { + EpistemicState5::decode(Epi5Gen::V1, e.epistemic_raw5()).unwrap() + } + + #[test] + fn orientation_map_is_contiguous_where_the_hierarchy_is() { + let b = |c: u8| 1u8 << c; + assert!(orientation_contiguous(b(1) | b(2))); + assert!(orientation_contiguous(b(1) | b(3) | b(4))); + assert!( + !orientation_contiguous(b(1) | b(4)), + "Main–ObjRelative skips SubjRelative" + ); + assert!( + !orientation_contiguous(b(2) | b(3)), + "Carried–SubjRelative only via Main" + ); + assert!(!orientation_contiguous(0)); + assert!(!orientation_contiguous(b(0) | b(1))); + } + + #[test] + fn window_shifts_newest_into_w3() { + let w = shift_window(0, Update::Corroborate); + assert_eq!(window_slots(w), [0, 0, 1]); + let w = shift_window(w, Update::Contest); + assert_eq!(window_slots(w), [0, 1, 2]); + let w = shift_window(w, Update::Dependent); + assert_eq!(window_slots(w), [1, 2, 3]); + let w = shift_window(w, Update::Corroborate); + assert_eq!(window_slots(w), [2, 3, 1], "W1 fell off"); + } + + /// Can fire: three independent verses climb `dog bit man` to rung 3 with + /// arm truth f=1, c=3/4, pressure +3, three corroborations, Related. + #[test] + fn independent_corroboration_climbs_the_ladder() { + let v = vec![svo(DOG, BIT, MAN); 3]; + let (rows, rep) = run(&v); + assert_eq!(rep.rows, 3); + let last = rows[2].edge; + assert_eq!(last.frequency_u8(), 255); + assert_eq!(last.confidence_u8(), (3 * 255 / 4) as u8); + assert_eq!(last.activation(), 3); + assert_eq!(last.novelty_wisdom(), 3); + assert_eq!(window_slots(last.w_slot()), [1, 1, 1]); + assert_eq!(epi(last).certification(), Certification3::Related); + assert_eq!(epi(last).topology(), Topology2::Direct); + assert_eq!(last.direction(), orientation_code(Origin::Main)); + assert_eq!(last.causal_mask(), CausalMask::SO); + // The rows of one statement are its time series. + let rungs: Vec = rows.iter().map(|r| r.edge.novelty_wisdom()).collect(); + assert_eq!(rungs, [1, 2, 3]); + assert_eq!( + epi(rows[0].edge).certification(), + Certification3::Associated + ); + } + + /// Stay silent: a repeat inside ONE verse is dependent — no new support, + /// no climb, no certification beyond Associated. + #[test] + fn a_repeat_in_one_verse_is_dependent() { + let mut v = svo(DOG, BIT, MAN); + v.push(Reading::stop()); + v.extend(svo(DOG, BIT, MAN)); + let (rows, rep) = run(&[v]); + assert_eq!(rows.len(), 2); + assert_eq!(rep.dependent, 1); + let e = rows[1].edge; + assert_eq!(rows[1].support, 1); + assert_eq!(e.novelty_wisdom(), 1, "no climb on dependent evidence"); + assert_eq!(window_slots(e.w_slot()), [0, 1, 3]); + assert_eq!(epi(e).certification(), Certification3::Associated); + } + + /// A contest drops the rival back to Staunen, and the pressure of a + /// contested statement reads the balance. + #[test] + fn a_contest_resets_the_rival_and_presses_down() { + let v = vec![ + svo(DOG, BIT, MAN), + svo(DOG, BIT, MAN), + svo(DOG, BIT, CAT), + svo(DOG, BIT, CAT), + svo(DOG, BIT, CAT), + ]; + let (rows, rep) = run(&v); + assert_eq!( + rep.contests, 3, + "each later verse contests the existing rival" + ); + let cat = rows[4].edge; + assert_eq!(cat.activation(), 1, "pos 3, neg 2"); + assert_eq!(cat.frequency_u8(), (3 * 255 / 5) as u8); + assert_eq!(rows[2].edge.activation(), -1, "pos 1 vs man's 2"); + assert_eq!(rows[2].edge.novelty_wisdom(), 1); + } + + /// F/C stays arm-discovery's law. `f` equals NAL revision of the unit + /// observations; `c` equals it only while uncontested, because + /// arm-discovery counts confidence over the co-occurrence mass. + #[test] + fn f_and_c_against_nal_revision_of_units() { + let pos_u = NarsTruth::new(1.0, 0.5); + let neg_u = NarsTruth::new(0.0, 0.5); + for (pos, neg) in [(1u32, 0u32), (3, 0), (3, 2), (1, 4)] { + let mut t = pos_u; + for _ in 1..pos { + t = t.revision(&pos_u); + } + for _ in 0..neg { + t = t.revision(&neg_u); + } + let arm = arm_to_truth_u8(&rule(Spo::new(1, 2, 3), pos, pos + neg, 1), 1); + let nal_f = (t.frequency * 255.0).floor() as i32; + assert!( + (i32::from(arm.frequency) - nal_f).abs() <= 1, + "f({pos},{neg}): arm {} nal {nal_f}", + arm.frequency + ); + let nal_c = (t.confidence * 255.0).floor() as i32; + let agree = (i32::from(arm.confidence) - nal_c).abs() <= 1; + assert_eq!(agree, neg == 0, "c agrees iff uncontested ({pos},{neg})"); + } + } + + /// A subject relative gets its orientation code and an Antecedent pointer + /// to the matrix event; topology IndirectKnown. + #[test] + fn subject_relative_points_at_its_matrix_event() { + // the dog who bit the man saw the cat + let v = vec![ + one(THE, Pos::Det), + one(DOG, Pos::Noun), + one(WHO, Pos::Rel), + one(BIT, Pos::Verb), + one(THE, Pos::Det), + one(MAN, Pos::Noun), + one(SAW, Pos::Verb), + one(THE, Pos::Det), + one(CAT, Pos::Noun), + ]; + let (rows, rep) = run(&[v]); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].origin, Some(Origin::SubjRelative)); + assert_eq!(rows[0].edge.direction(), 3); + assert_eq!(rows[0].loci.at(Locus::Antecedent), 1); + assert_eq!(rep.antecedent_bound, 1); + assert_eq!(epi(rows[0].edge).topology(), Topology2::IndirectKnown); + assert_eq!( + rows[1].loci.at(Locus::Antecedent), + 0, + "matrix event has no pointer" + ); + } + + /// An ambiguous token leaves the verse untraced: orientation 0, topology + /// Unknown, and no climb. + #[test] + fn an_untraced_verse_claims_no_path() { + // A token both readings skip: the triple is certain, the verse is not + // single-reading, so it cannot be traced. + let mut amb = vec![Reading::new(WHO, PosSet::single(Pos::Det).with(Pos::Other))]; + amb.extend(svo(DOG, BIT, MAN)); + let (rows, rep) = run(&[svo(DOG, BIT, MAN), amb]); + assert_eq!(rep.untraced_verses, 1); + let r = rows + .get(1) + .expect("the certain triple survives the ambiguity"); + assert_eq!(r.edge.direction(), 0); + assert_eq!(epi(r.edge).topology(), Topology2::Unknown); + assert_eq!(r.edge.novelty_wisdom(), 1, "unresolved: no climb"); + } + + /// Pearl ↔ TEKAMOLO: a Kausal lane stated in two independent verses puts + /// the event at the intervention rung and certifies CausalCandidate; the + /// same statement without the lane stays SO / Related. + #[test] + fn kausal_lane_sets_the_intervention_rung() { + let h = GrammarHeuristics::parse( + "# lane\tlemma\trelation\tcount\nKausal\tweil\tmark\t273\nTemporal\theute\tadvmod\t637\n", + ); + let surf = ["der", "hund", "biss", "den", "mann", "weil"]; + let mut tok = svo(DOG, BIT, MAN); + tok.push(one(THE, Pos::Other)); // `weil`, skipped by the FSM + let markers = [(1, 1), (1, 2)]; + let ins = vec![ + VerseIn { + readings: tok.clone(), + surface: surf.to_vec(), + forms: vec![], + }, + VerseIn { + readings: tok, + surface: surf.to_vec(), + forms: vec![], + }, + ]; + let (rows, rep) = wire( + &markers, + &ins, + 1, + &abi(), + Some(&h), + ReadParams::RIGHT_CORNER, + ) + .unwrap(); + assert_eq!(rep.intervention_rows, 2); + assert_eq!(rows[1].edge.causal_mask(), CausalMask::PO); + assert_eq!( + epi(rows[1].edge).certification(), + Certification3::CausalCandidate + ); + let (plain, _) = run(&[svo(DOG, BIT, MAN), svo(DOG, BIT, MAN)]); + assert_eq!(plain[1].edge.causal_mask(), CausalMask::SO); + assert_eq!(epi(plain[1].edge).certification(), Certification3::Related); + } +} From dc5c3f6c0ac39791f8feac85af56d56f225555b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:21:04 +0000 Subject: [PATCH 05/23] deepnsm-v2 wire: silence twins for the rival reset, zero pressure and a single causal verse Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/deepnsm-v2/src/wire.rs | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs index e411da266..a7ad3d455 100644 --- a/crates/deepnsm-v2/src/wire.rs +++ b/crates/deepnsm-v2/src/wire.rs @@ -623,6 +623,34 @@ mod tests { assert_eq!(cat.frequency_u8(), (3 * 255 / 5) as u8); assert_eq!(rows[2].edge.activation(), -1, "pos 1 vs man's 2"); assert_eq!(rows[2].edge.novelty_wisdom(), 1); + // pos 2 vs 2: zero pressure, no climb. + assert_eq!(rows[3].edge.activation(), 0); + assert_eq!( + rows[3].edge.novelty_wisdom(), + 1, + "no climb without pressure" + ); + } + + /// The contested rival restarts at Staunen: `man` reached rung 2, a `cat` + /// verse contests it, and the next `man` verse climbs from 1, not 2. + #[test] + fn a_contested_rival_restarts_at_staunen() { + let v = vec![ + svo(DOG, BIT, MAN), + svo(DOG, BIT, MAN), + svo(DOG, BIT, CAT), + svo(DOG, BIT, MAN), + ]; + let (rows, _) = run(&v); + assert_eq!(rows[1].edge.novelty_wisdom(), 2); + assert_eq!(rows[3].edge.activation(), 2, "pos 3, neg 1"); + assert_eq!( + rows[3].edge.novelty_wisdom(), + 2, + "reset to 1, then one climb" + ); + assert_eq!(window_slots(rows[3].edge.w_slot()), [1, 2, 1]); } /// F/C stays arm-discovery's law. `f` equals NAL revision of the unit @@ -740,6 +768,26 @@ mod tests { epi(rows[1].edge).certification(), Certification3::CausalCandidate ); + // One causal verse is not enough: Related, not CausalCandidate. + let mixed = vec![ + ins[0].clone(), + VerseIn { + readings: svo(DOG, BIT, MAN), + ..VerseIn::default() + }, + ]; + let (rows, rep) = wire( + &markers, + &mixed, + 1, + &abi(), + Some(&h), + ReadParams::RIGHT_CORNER, + ) + .unwrap(); + assert_eq!(rep.intervention_rows, 1); + assert_eq!(rep.causal_candidates, 0); + assert_eq!(epi(rows[1].edge).certification(), Certification3::Related); let (plain, _) = run(&[svo(DOG, BIT, MAN), svo(DOG, BIT, MAN)]); assert_eq!(plain[1].edge.causal_mask(), CausalMask::SO); assert_eq!(epi(plain[1].edge).certification(), Certification3::Related); From 9943fec294a46b8703655e1e2695aaf0525f1f09 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:25:21 +0000 Subject: [PATCH 06/23] =?UTF-8?q?quack:=20wired=5Freasoning=5Fprobe=20?= =?UTF-8?q?=E2=80=94=20wire=20field=20rules=20as=20Datalog=20over=20presen?= =?UTF-8?q?ce=20planes;=20board=20entry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- ...6-grammar-coordinates-annotation-wiring.md | 47 +++ crates/lance-graph-quack/Cargo.toml | 3 + .../examples/wired_reasoning_probe.rs | 290 ++++++++++++++++++ 3 files changed, 340 insertions(+) create mode 100644 crates/lance-graph-quack/examples/wired_reasoning_probe.rs diff --git a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md index 5630028da..a81fa5840 100644 --- a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md +++ b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md @@ -129,3 +129,50 @@ Read in full: `deepnsm-v2/src/tekamolo.rs`, `deepnsm-v2/src/loci.rs`, ONA source (oracle only, not opened here); Hyperon's own grammar of contexts beyond ruff's harvest; the 8 unnamed loci's intended use; any plan that may already assign them (grep `Locus` in `.claude/plans` not run). + +## 7. Wiring landed (2026-10-16, later the same day) + +`crates/deepnsm-v2/src/wire.rs` fills every field of the CE64 semantic +upper half on each parsed triple, each from one named source (module doc +has the table). `run_harness` is unchanged and still emits the grammar-only +edge; `wire` is the annotation layer above it. + +| bits | field | source | +|---|---|---| +| 24..39 | F/C | `lance-graph-arm-discovery::arm_to_truth_u8`, rule `(S,P) → O` over independent verses, k = 1 | +| 40..42 | Pearl | TEKAMOLO Kausal lane on the event → `PO`, else `SO` | +| 43..45 | orientation | clause path (`fsm::parse_to_spo_traced`): 1 Main, 2 Carried, 3 SubjRelative, 4 ObjRelative, 0 untraced | +| 46..49 | activation | pressure `pos − neg`, clamped i4 (`CausalEdge64::activation`, same bits as `inference_mantissa`) | +| 50..52 | novelty → wisdom | per-statement Markov rung, 1 Staunen ..= 7 wisdom (`novelty_wisdom`, same bits as plasticity) | +| 53..58 | belief update | W1/W2/W3 window of the last three updates: 1 corroborate, 2 contest, 3 dependent | +| 59..63 | epistemic | topology from the clause path; Associated → Related (≥2 independent, pressure > 0) → CausalCandidate (≥2 independent Kausal verses); never Causes | + +Closes G1 (relative antecedent → `Locus::Antecedent`) and G2 (TEKAMOLO → +loci 0..3, via `ClauseRead::at` and the triple's `closed_at`). + +Tests: 10 in `wire`, each guard disable-verified red on exactly its named +test (rival reset, dependent kind, resolved gate, pressure gate, two causal +verses, Kausal → PO, orientation code, antecedent locus, contest shift, map +link). `f_and_c_against_nal_revision_of_units` pins that arm `f` equals NAL +revision of unit evidence, and arm `c` equals it only when uncontested. + +Reasoning AST: `lance-graph-quack/examples/wired_reasoning_probe.rs` states +five field rules as Datalog (R1 `pearl_po ↔ kausal`, R2 `cert_rel ↔ +support2 ∧ press_pos`, R3 `candidate → cert_rel`, R4 `climbed → support2`, +R5 `direct ↔ main`) and runs each as a quack filter over presence planes of +541 wired rows: 0 violations, counts equal to a row oracle, every plane +non-trivial, one flipped output bit breaks exactly its rule. + +Not done, recorded so it is not lost: + +- The orientation map's contiguity check has no consumer: over the two + relative functions it is always true, so a per-relativizer probe would be + vacuous. +- `deepnsm_v2::belief::BeliefArena` and `lance-graph-nal` do not consume + wired rows yet. The arena's `Stamp` folds sources modulo 64 (false + overlap past 64 verses); `lance-graph-nal` has no relational copula for + `(S, verb, O)` (R10). +- AriGraph `revise_with_evidence` still revises without stamps. +- The 144 verbs (`contract/src/grammar/verb_table.rs`, 12×12 in a 16×16 + space with TEKAMOLO slot priors) are not connected to the P code or the + Pearl rung. diff --git a/crates/lance-graph-quack/Cargo.toml b/crates/lance-graph-quack/Cargo.toml index 590bb4178..8c512c885 100644 --- a/crates/lance-graph-quack/Cargo.toml +++ b/crates/lance-graph-quack/Cargo.toml @@ -25,3 +25,6 @@ lance-graph-arm-discovery = { path = "../lance-graph-arm-discovery" } # canonical `CausalEdge64` accessors, never against a re-derived bit layout. # Zero-dep, example-only. causal-edge = { path = "../causal-edge" } +# `examples/wired_reasoning_probe.rs`: the wired CE64 rows of deepnsm-v2, +# checked as Datalog rules over presence planes. Example-only. +deepnsm-v2 = { path = "../deepnsm-v2" } diff --git a/crates/lance-graph-quack/examples/wired_reasoning_probe.rs b/crates/lance-graph-quack/examples/wired_reasoning_probe.rs new file mode 100644 index 000000000..8bb7c10b4 --- /dev/null +++ b/crates/lance-graph-quack/examples/wired_reasoning_probe.rs @@ -0,0 +1,290 @@ +//! The field rules of `deepnsm_v2::wire` as Datalog over presence planes, +//! executed through quack and mask-risc, against a row oracle. +//! +//! One row = one wired triple. Every input fact and every output field is a +//! presence plane over the rows; a rule is a quack filter over those planes, +//! so the rule set is the reasoning AST and mask-risc executes it. Names are +//! labels used only to print. +//! +//! ```text +//! R1 pearl_po(E) <-> kausal(E) (Pearl rung from TEKAMOLO) +//! R2 cert_rel(E) <-> support2(E), press_pos(E) (Related or better) +//! R3 candidate(E) -> cert_rel(E) +//! R4 climbed(E) -> support2(E) (a rung above Staunen needs evidence) +//! R5 direct(E) <-> main(E) (topology from the clause path) +//! ``` +//! +//! An equivalence holds when `Any(Or(And(A, Not B), And(B, Not A)))` is false, +//! an implication when `Any(And(A, Not B))` is false. Each side is checked to +//! be non-trivial (some rows, not all) so no rule passes on an empty plane. +//! The falsifier arm corrupts one output bit and expects exactly that rule to +//! fail. +//! +//! ```text +//! cargo run -p lance-graph-quack --example wired_reasoning_probe +//! ``` + +use causal_edge::CausalMask; +use deepnsm_v2::fsm::{Origin, Pos, PosSet, Reading}; +use deepnsm_v2::haspelmath::FrozenOperandAbi; +use deepnsm_v2::tekamolo::{GrammarHeuristics, ReadParams}; +use deepnsm_v2::wire::{wire, VerseIn, WiredRow, KAUSAL_LANE}; +use lance_graph_contract::epistemic_state5::{Certification3, Epi5Gen, EpistemicState5, Topology2}; +use lance_graph_mask_risc::exec::{execute_extent, Scratch}; +use lance_graph_mask_risc::{words_for, Foreign, Out, Planes, Value}; +use lance_graph_quack::{lower, Agg, Filter, Mask, Query}; + +const NOUNS: [u16; 6] = [10, 11, 12, 13, 14, 15]; +const VERBS: [u16; 3] = [20, 21, 22]; +const THE: u16 = 30; +const WHO: u16 = 31; +const WEIL: u16 = 32; + +fn one(id: u16, pos: Pos) -> Reading { + Reading::new(id, PosSet::single(pos)) +} + +struct Lcg(u64); +impl Lcg { + fn next(&mut self, n: u64) -> u64 { + self.0 = self + .0 + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + (self.0 >> 33) % n + } +} + +/// A synthetic corpus: SVO and subject-relative clauses, a `weil` marker on +/// some verses, few objects per (S, P) so statements are both corroborated +/// and contested. +fn corpus(n: usize) -> (Vec>, Vec>) { + let mut g = Lcg(0x5EED); + let (mut rs, mut ss) = (Vec::new(), Vec::new()); + for _ in 0..n { + let s = NOUNS[g.next(2) as usize]; + let v = VERBS[g.next(2) as usize]; + let o = NOUNS[2 + g.next(2) as usize]; + let mut r = vec![one(THE, Pos::Det), one(s, Pos::Noun)]; + let mut w = vec!["der", "x"]; + if g.next(3) == 0 { + r.extend([ + one(WHO, Pos::Rel), + one(v, Pos::Verb), + one(THE, Pos::Det), + one(o, Pos::Noun), + ]); + w.extend(["der", "v", "den", "y"]); + let o2 = NOUNS[4 + g.next(2) as usize]; + r.extend([one(v, Pos::Verb), one(THE, Pos::Det), one(o2, Pos::Noun)]); + w.extend(["v", "den", "z"]); + } else { + r.extend([one(v, Pos::Verb), one(THE, Pos::Det), one(o, Pos::Noun)]); + w.extend(["v", "den", "y"]); + } + if g.next(2) == 0 { + r.push(one(WEIL, Pos::Other)); + w.push("weil"); + } + rs.push(r); + ss.push(w); + } + (rs, ss) +} + +fn set(w: &mut [u64], i: usize) { + w[i / 64] |= 1 << (i % 64); +} + +fn get(w: &[u64], i: usize) -> bool { + w[i / 64] >> (i % 64) & 1 == 1 +} + +const INPUTS: [&str; 5] = ["kausal", "support2", "press_pos", "main", "climbed"]; +const OUTPUTS: [&str; 4] = ["pearl_po", "cert_rel", "candidate", "direct"]; + +/// Plane index by label: inputs first, then outputs. +fn ix(label: &str) -> usize { + INPUTS + .iter() + .chain(OUTPUTS.iter()) + .position(|x| *x == label) + .unwrap() +} + +fn planes_of(rows: &[WiredRow]) -> Vec> { + let n = rows.len(); + let mut p = vec![vec![0u64; words_for(n)]; INPUTS.len() + OUTPUTS.len()]; + for (i, r) in rows.iter().enumerate() { + let e = r.edge; + let st = EpistemicState5::decode(Epi5Gen::V1, e.epistemic_raw5()).unwrap(); + let cert = st.certification(); + let facts = [ + ("kausal", r.tekamolo[KAUSAL_LANE].is_some()), + ("support2", r.support >= 2), + ("press_pos", e.activation() > 0), + ("main", r.origin == Some(Origin::Main)), + ("climbed", e.novelty_wisdom() > 1), + ("pearl_po", e.causal_mask() == CausalMask::PO), + ( + "cert_rel", + matches!( + cert, + Certification3::Related | Certification3::CausalCandidate + ), + ), + ("candidate", cert == Certification3::CausalCandidate), + ("direct", st.topology() == Topology2::Direct), + ]; + for (label, on) in facts { + if on { + set(&mut p[ix(label)], i); + } + } + } + p +} + +fn pl(label: &str) -> Filter { + Filter::Plane(Mask(ix(label) as u16)) +} + +fn not(f: Filter) -> Filter { + Filter::Not(Box::new(f)) +} + +fn and(a: Filter, b: Filter) -> Filter { + Filter::And(vec![a, b]) +} + +/// The violation filter of each rule: rows that break it. +fn rules() -> Vec<(&'static str, Filter)> { + let iff = + |a: Filter, b: Filter| Filter::Or(vec![and(a.clone(), not(b.clone())), and(b, not(a))]); + vec![ + ("R1 pearl_po <-> kausal", iff(pl("pearl_po"), pl("kausal"))), + ( + "R2 cert_rel <-> support2, press_pos", + iff(pl("cert_rel"), and(pl("support2"), pl("press_pos"))), + ), + ( + "R3 candidate -> cert_rel", + and(pl("candidate"), not(pl("cert_rel"))), + ), + ( + "R4 climbed -> support2", + and(pl("climbed"), not(pl("support2"))), + ), + ("R5 direct <-> main", iff(pl("direct"), pl("main"))), + ] +} + +fn run(filter: &Filter, agg: Agg, planes: &[Vec], n: usize) -> Value { + let program = lower(&Query { + filter: filter.clone(), + agg, + }) + .expect("lowers"); + let masks: Vec<&[u64]> = planes.iter().map(Vec::as_slice).collect(); + let p = Planes { + n_rows: n, + masks: &masks, + lanes: &[], + }; + let mut s = Scratch::for_program(&program, n).expect("scratch"); + execute_extent(&program, &p, &Foreign::NONE, &mut s, Out::None, 0..n).expect("runs") +} + +/// Row oracle: the violation count of each rule, by a plain loop. +fn oracle(planes: &[Vec], n: usize) -> Vec { + let b = |l: &str, i: usize| get(&planes[ix(l)], i); + let mut v = vec![0; 5]; + for i in 0..n { + v[0] += usize::from(b("pearl_po", i) != b("kausal", i)); + v[1] += usize::from(b("cert_rel", i) != (b("support2", i) && b("press_pos", i))); + v[2] += usize::from(b("candidate", i) && !b("cert_rel", i)); + v[3] += usize::from(b("climbed", i) && !b("support2", i)); + v[4] += usize::from(b("direct", i) != b("main", i)); + } + v +} + +fn check(planes: &[Vec], n: usize) -> Vec { + let want = oracle(planes, n); + let mut got = Vec::new(); + for ((name, f), w) in rules().iter().zip(&want) { + let Value::Count(c) = run(f, Agg::Count, planes, n) else { + panic!("{name}: count") + }; + let any = matches!(run(f, Agg::Any, planes, n), Value::Bool(true)); + assert_eq!(c, *w, "{name}: quack vs row oracle"); + assert_eq!(any, *w > 0, "{name}: Any vs Count"); + got.push(c); + } + got +} + +fn main() { + let n_verses = 400; + let (readings, surfaces) = corpus(n_verses); + let markers: Vec<(u16, u16)> = (0..n_verses) + .map(|i| (1 + (i / 50) as u16, 1 + (i % 50) as u16)) + .collect(); + let ins: Vec> = readings + .iter() + .zip(&surfaces) + .map(|(r, s)| VerseIn { + readings: r.clone(), + surface: s.clone(), + forms: vec![], + }) + .collect(); + let mut ops: Vec = NOUNS.to_vec(); + ops.extend(VERBS); + ops.extend([THE, WHO, WEIL]); + let abi = FrozenOperandAbi::new(1, &ops).unwrap(); + let h = GrammarHeuristics::parse("# lane\tlemma\trelation\tcount\nKausal\tweil\tmark\t273\n"); + let (rows, rep) = wire(&markers, &ins, 1, &abi, Some(&h), ReadParams::RIGHT_CORNER).unwrap(); + let n = rows.len(); + println!("rows {n} report {rep:?}"); + + let planes = planes_of(&rows); + for (k, label) in INPUTS.iter().chain(OUTPUTS.iter()).enumerate() { + let c = (0..n).filter(|&i| get(&planes[k], i)).count(); + println!(" {label:10} {c:5} / {n}"); + assert!( + c > 0 && c < n, + "{label}: plane must be non-trivial (got {c})" + ); + } + + let v = check(&planes, n); + for ((name, _), c) in rules().iter().zip(&v) { + println!("{name:40} violations {c}"); + } + assert!( + v.iter().all(|&c| c == 0), + "every rule holds on the wired rows" + ); + + // Falsifier: flip one output bit per rule and expect exactly that rule to + // fail (R1..R5 read pearl_po, cert_rel, candidate, climbed, direct). + let flips = ["pearl_po", "cert_rel", "candidate", "climbed", "direct"]; + for (r, label) in flips.iter().enumerate() { + let mut bad = planes.clone(); + let p = &mut bad[ix(label)]; + // A row where flipping must violate: for implications, a row where + // the consequent is false; otherwise any row. + let target = match *label { + "candidate" => (0..n).find(|&i| !get(&planes[ix("cert_rel")], i)), + "climbed" => (0..n).find(|&i| !get(&planes[ix("support2")], i)), + _ => Some(0), + } + .expect("a row to flip"); + p[target / 64] ^= 1 << (target % 64); + let v = check(&bad, n); + assert!(v[r] > 0, "flipping {label} must break rule {}", r + 1); + println!("flip {label:10} -> rule {} violations {}", r + 1, v[r]); + } + println!("ok"); +} From f97f4edb63868c30618e0a4f5c687e6752f36254 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:31:29 +0000 Subject: [PATCH 07/23] deepnsm-v2 wire: weil claims go through the belief arena Every wired row is a unit observation in a facts arena (S -Rel(P)-> O), and every Kausal claim a unit observation of cause => effect over event terms in a causes arena, both stamped with their verse. A cause event is CausalCandidate only when its implication rests on two or more disjoint verse stamps; the per-statement causal-verse counter is removed. The reasoning probe corpus gains two-clause weil verses. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/deepnsm-v2/src/wire.rs | 314 ++++++++++++++---- .../examples/wired_reasoning_probe.rs | 17 +- 2 files changed, 272 insertions(+), 59 deletions(-) diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs index a7ad3d455..669f8a90c 100644 --- a/crates/deepnsm-v2/src/wire.rs +++ b/crates/deepnsm-v2/src/wire.rs @@ -19,7 +19,26 @@ //! | 46..49 | activation i4 | semantic pressure `pos - neg`, clamped to `-8..=7` | //! | 50..52 | novelty → wisdom | the statement's rung after this update, `1` Staunen ..= `7` wisdom | //! | 53..58 | belief-state update | the statement's last three updates: `W1` prior (53..54), `W2` (55..56), `W3` newest (57..58) | -//! | 59..63 | epistemic state | `EpistemicState5`: topology from the clause path, certification from the belief state | +//! | 59..63 | epistemic state | `EpistemicState5`: topology from the clause path, certification from the belief state and the causes arena | +//! +//! # The belief arenas +//! +//! Every row is also offered to [`crate::belief::BeliefArena`] as one unit +//! observation, stamped with its verse: the stamp is the independence gate, +//! so disjoint verses pool and a repeat inside one verse is CHOICE, never +//! counted twice. Two arenas, two term spaces: +//! +//! - **facts**: `S –Rel(P)→ O` over word ids. `Rel` never transits. +//! - **causes**: `cause ⇒ effect` over event terms. The cause is the event +//! whose clause the Kausal marker (`weil`) opens; every matrix event of the +//! same verse is an effect. `Impl` does not auto-transit. +//! +//! A cause event is certified `CausalCandidate` only when its `cause ⇒ +//! effect` belief rests on two or more disjoint verse stamps (and the triple +//! itself is Related). Text never certifies `Causes`. The arena's stamps are +//! a 64-source bitset that folds by modulo: past 64 verses two sources can +//! collide, which turns a revision into a CHOICE (an undercount, never a +//! double count). //! //! # The belief state is a Markov chain per statement //! @@ -75,8 +94,10 @@ use causal_edge::{CausalEdge64, CausalMask}; use lance_graph_arm_discovery::{arm_to_truth_u8, CandidateRule, Item}; use lance_graph_contract::causal_witness::{CausalWitnessFacet, Locus}; use lance_graph_contract::epistemic_state5::{Certification3, Epi5Gen, EpistemicState5, Topology2}; +use lance_graph_contract::exploration::NarsTruth; use lance_graph_contract::hhtl::NiblePath; +use crate::belief::{BeliefArena, CStmt, Copula, ReviseOutcome, Stamp}; use crate::fsm::{parse_readings, parse_to_spo_traced, Origin, Pos, Reading, Tagged, Traced}; use crate::haspelmath::{ continuity, run_harness, FormAttestation, FrozenOperandAbi, HarnessError, MapVerdict, @@ -216,6 +237,36 @@ pub struct WireReport { pub dependent: usize, /// Rows certified `CausalCandidate`. pub causal_candidates: usize, + /// `cause ⇒ effect` observations offered to the causes arena. + pub implications: usize, + /// Of those, observations the arena pooled (disjoint stamps). + pub implications_revised: usize, + /// Events not interned because the u16 event-term space was full. + pub event_overflow: usize, +} + +/// What [`wire`] produced: the rows, the report, and the two belief arenas +/// the rows were fed into. +#[derive(Debug)] +pub struct Wired { + /// One row per certain triple, in stream order. + pub rows: Vec, + /// What happened. + pub report: WireReport, + /// Every row as `S –Rel(P)→ O`, one unit observation per verse, stamped + /// with the verse. `Rel` never transits. + pub facts: BeliefArena, + /// Every Kausal claim as `cause ⇒ effect` over EVENT terms (indices into + /// [`Self::events`]), one unit observation per verse, stamped with the + /// verse. `Impl` does not auto-transit (belief.rs V0). + pub causes: BeliefArena, + /// Event term `i` is the triple `events[i]`. + pub events: Vec, +} + +/// One unit of positive evidence at horizon 1. +fn unit() -> NarsTruth { + NarsTruth::new(1.0, 0.5) } const REACH_BACK: i64 = -8; @@ -247,7 +298,6 @@ fn trace(readings: &[Reading], certain: &[Spo]) -> Option> { #[derive(Debug, Clone, Default)] struct StmtState { verses: HashSet, - causal_verses: HashSet, window: u8, rung: u8, } @@ -286,7 +336,7 @@ pub fn wire( abi: &FrozenOperandAbi, heuristics: Option<&GrammarHeuristics>, params: ReadParams, -) -> Result<(Vec, WireReport), HarnessError> { +) -> Result { let readings: Vec> = verses.iter().map(|v| v.readings.clone()).collect(); let forms: Vec> = verses.iter().map(|v| v.forms.clone()).collect(); let harness = run_harness(markers, &readings, &forms, basin, abi)?; @@ -294,6 +344,10 @@ pub fn wire( let mut rep = WireReport::default(); let mut rows: Vec = Vec::with_capacity(harness.encoded.len()); let mut groups: HashMap<(u16, u16), Group> = HashMap::new(); + let mut facts = BeliefArena::new(); + let mut causes = BeliefArena::new(); + let mut events: Vec = Vec::new(); + let mut event_id: HashMap = HashMap::new(); let mut next = 0usize; for (vi, verse) in verses.iter().enumerate() { @@ -397,6 +451,49 @@ pub fn wire( } } + // Kausal claims: the stating event is the cause (the clause `weil` + // opens), every matrix event of the verse is an effect. Each pair is + // one unit observation of `cause ⇒ effect`, stamped with the verse, so + // the arena pools independent verses and never double counts one. + let mut cause_sources = vec![0u32; n]; + if let (Some(j), Some(t)) = (stating[KAUSAL_LANE], traced.as_ref()) { + let mut intern = |spo: Spo, rep: &mut WireReport| -> Option { + if let Some(&id) = event_id.get(&spo) { + return Some(id); + } + let Ok(id) = u16::try_from(events.len()) else { + rep.event_overflow += 1; + return None; + }; + events.push(spo); + event_id.insert(spo, id); + Some(id) + }; + for (e, tr) in t.iter().enumerate() { + if e == j || tr.origin != Origin::Main { + continue; + } + let (Some(c), Some(f)) = (intern(t[j].spo, &mut rep), intern(tr.spo, &mut rep)) + else { + continue; + }; + let stmt = CStmt { + s: c, + cop: Copula::Impl, + p: f, + }; + rep.implications += 1; + if matches!( + causes.observe(stmt, unit(), Stamp::source(vi as u32)), + ReviseOutcome::Revised { .. } + ) { + rep.implications_revised += 1; + } + let sources = causes.get(stmt).map_or(0, |b| b.stamp.0.count_ones()); + cause_sources[j] = cause_sources[j].max(sources); + } + } + // The belief-state update, row by row in stream order. for (k, row) in rows[first..].iter_mut().enumerate() { let spo = row.spo; @@ -420,9 +517,6 @@ pub fn wire( let st = group.objects.entry(spo.object).or_default(); let new_stmt = st.verses.is_empty(); let own_fresh = st.verses.insert(vi); - if causal { - st.causal_verses.insert(vi); - } let pos = st.verses.len() as u32; let pressure = (i64::from(pos) - i64::from(tot - pos)).clamp(-8, 7) as i8; let kind = if own_fresh { @@ -451,8 +545,17 @@ pub fn wire( Some(_) if unbound[k] => Topology2::IndirectUnknown, Some(_) => Topology2::IndirectKnown, }; + facts.observe( + CStmt { + s: spo.subject, + cop: Copula::Rel(spo.predicate), + p: spo.object, + }, + unit(), + Stamp::source(vi as u32), + ); let certification = if pos >= 2 && pressure > 0 { - if st.causal_verses.len() >= 2 { + if cause_sources[k] >= 2 { rep.causal_candidates += 1; Certification3::CausalCandidate } else { @@ -478,7 +581,13 @@ pub fn wire( } rep.rows = rows.len(); } - Ok((rows, rep)) + Ok(Wired { + rows, + report: rep, + facts, + causes, + events, + }) } #[cfg(test)] @@ -523,7 +632,8 @@ mod tests { ..VerseIn::default() }) .collect(); - wire(&markers, &ins, 1, &abi(), None, ReadParams::RIGHT_CORNER).unwrap() + let w = wire(&markers, &ins, 1, &abi(), None, ReadParams::RIGHT_CORNER).unwrap(); + (w.rows, w.report) } fn epi(e: CausalEdge64) -> EpistemicState5 { @@ -729,65 +839,157 @@ mod tests { assert_eq!(r.edge.novelty_wisdom(), 1, "unresolved: no climb"); } - /// Pearl ↔ TEKAMOLO: a Kausal lane stated in two independent verses puts - /// the event at the intervention rung and certifies CausalCandidate; the - /// same statement without the lane stays SO / Related. - #[test] - fn kausal_lane_sets_the_intervention_rung() { - let h = GrammarHeuristics::parse( - "# lane\tlemma\trelation\tcount\nKausal\tweil\tmark\t273\nTemporal\theute\tadvmod\t637\n", - ); - let surf = ["der", "hund", "biss", "den", "mann", "weil"]; - let mut tok = svo(DOG, BIT, MAN); - tok.push(one(THE, Pos::Other)); // `weil`, skipped by the FSM - let markers = [(1, 1), (1, 2)]; - let ins = vec![ - VerseIn { - readings: tok.clone(), - surface: surf.to_vec(), - forms: vec![], - }, - VerseIn { - readings: tok, - surface: surf.to_vec(), - forms: vec![], - }, + fn heuristics() -> GrammarHeuristics { + GrammarHeuristics::parse("# lane\tlemma\trelation\tcount\nKausal\tweil\tmark\t273\n") + } + + /// `der Hund biss den Mann. weil die Katze sah den Hund`: effect clause, + /// then the cause clause `weil` opens. + fn weil_verse() -> VerseIn<'static> { + let mut r = svo(DOG, BIT, MAN); + r.push(Reading::stop()); + r.push(one(WHO, Pos::Other)); // `weil`, skipped by the FSM + r.extend(svo(CAT, SAW, DOG)); + let surface = vec![ + "der", "hund", "biss", "den", "mann", ".", "weil", "die", "katze", "sah", "den", "hund", ]; - let (rows, rep) = wire( + VerseIn { + readings: r, + surface, + forms: vec![], + } + } + + fn run_in(ins: &[VerseIn<'_>]) -> Wired { + let markers: Vec<(u16, u16)> = (0..ins.len() as u16).map(|i| (1, i + 1)).collect(); + wire( &markers, - &ins, + ins, 1, &abi(), - Some(&h), + Some(&heuristics()), ReadParams::RIGHT_CORNER, ) - .unwrap(); - assert_eq!(rep.intervention_rows, 2); - assert_eq!(rows[1].edge.causal_mask(), CausalMask::PO); + .unwrap() + } + + fn impl_stmt(w: &Wired, cause: Spo, effect: Spo) -> CStmt { + let id = |x: Spo| w.events.iter().position(|e| *e == x).unwrap() as u16; + CStmt { + s: id(cause), + cop: Copula::Impl, + p: id(effect), + } + } + + /// Can fire: two independent `weil` verses pool `cause ⇒ effect` in the + /// causes arena (two disjoint stamps), the cause event sits at the + /// intervention rung and is certified CausalCandidate; the effect is not. + #[test] + fn weil_feeds_the_causes_arena() { + let w = run_in(&[weil_verse(), weil_verse()]); + assert_eq!(w.report.implications, 2); + assert_eq!(w.report.implications_revised, 1); + let (cause, effect) = (Spo::new(CAT, SAW, DOG), Spo::new(DOG, BIT, MAN)); + let b = w.causes.get(impl_stmt(&w, cause, effect)).unwrap(); + assert_eq!(b.stamp.0.count_ones(), 2); + assert!(b.truth.confidence > 0.6, "two units pool to c = 2/3"); + let row = |v: usize, x: Spo| w.rows.iter().find(|r| r.verse == v && r.spo == x).unwrap(); + assert_eq!(row(1, cause).edge.causal_mask(), CausalMask::PO); assert_eq!( - epi(rows[1].edge).certification(), + epi(row(1, cause).edge).certification(), Certification3::CausalCandidate ); - // One causal verse is not enough: Related, not CausalCandidate. - let mixed = vec![ - ins[0].clone(), + assert_eq!(row(1, effect).edge.causal_mask(), CausalMask::SO); + assert_eq!( + epi(row(1, effect).edge).certification(), + Certification3::Related + ); + assert_eq!(w.report.causal_candidates, 1); + } + + /// Stay silent: the cause clause seen twice, but the causal claim only + /// once — the cause is Related (two supports), not a candidate. + #[test] + fn one_causal_claim_is_not_a_candidate() { + let mut plain = svo(CAT, SAW, DOG); + plain.push(Reading::stop()); + plain.extend(svo(DOG, BIT, MAN)); + let w = run_in(&[ + weil_verse(), VerseIn { - readings: svo(DOG, BIT, MAN), + readings: plain, ..VerseIn::default() }, - ]; - let (rows, rep) = wire( - &markers, - &mixed, - 1, - &abi(), - Some(&h), - ReadParams::RIGHT_CORNER, - ) - .unwrap(); - assert_eq!(rep.intervention_rows, 1); - assert_eq!(rep.causal_candidates, 0); - assert_eq!(epi(rows[1].edge).certification(), Certification3::Related); + ]); + assert_eq!(w.report.implications, 1); + assert_eq!(w.report.implications_revised, 0); + assert_eq!(w.report.causal_candidates, 0); + let cause = w + .rows + .iter() + .find(|r| r.verse == 1 && r.spo == Spo::new(CAT, SAW, DOG)) + .unwrap(); + assert_eq!(epi(cause.edge).certification(), Certification3::Related); + assert_eq!(cause.edge.causal_mask(), CausalMask::SO); + } + + /// A `weil` clause with no matrix event states a cause of nothing: the + /// claim sits at the intervention rung, but no implication is observed. + #[test] + fn weil_without_an_effect_observes_no_implication() { + let mut r = svo(DOG, BIT, MAN); + r.push(one(WHO, Pos::Other)); + let v = VerseIn { + readings: r, + surface: vec!["der", "hund", "biss", "den", "mann", "weil"], + forms: vec![], + }; + let w = run_in(&[v.clone(), v]); + assert_eq!(w.report.intervention_rows, 2); + assert_eq!(w.report.implications, 0); + assert!(w.causes.entries().is_empty()); + assert_eq!(epi(w.rows[1].edge).certification(), Certification3::Related); + } + + /// The facts arena pools independent verses exactly as arm-discovery's + /// confidence does for an uncontested statement, and one verse repeated + /// is CHOICE, never pooled. + #[test] + fn facts_arena_agrees_with_arm_and_refuses_a_repeat() { + let (dog_bit_man, w) = ( + CStmt { + s: DOG, + cop: Copula::Rel(BIT), + p: MAN, + }, + run_in(&vec![ + VerseIn { + readings: svo(DOG, BIT, MAN), + ..VerseIn::default() + }; + 3 + ]), + ); + let b = w.facts.get(dog_bit_man).unwrap(); + let arm = w.rows[2].edge.confidence_u8(); + assert!(((b.truth.confidence * 255.0) as i32 - i32::from(arm)).abs() <= 1); + assert_eq!(b.stamp.0.count_ones(), 3); + + let mut twice = svo(DOG, BIT, MAN); + twice.push(Reading::stop()); + twice.extend(svo(DOG, BIT, MAN)); + let w = run_in(&[VerseIn { + readings: twice, + ..VerseIn::default() + }]); + let b = w.facts.get(dog_bit_man).unwrap(); + assert_eq!(b.stamp.0.count_ones(), 1, "one verse is one source"); + assert!((b.truth.confidence - 0.5).abs() < 1e-6, "not pooled"); + } + + #[test] + fn plain_rows_stay_at_association() { let (plain, _) = run(&[svo(DOG, BIT, MAN), svo(DOG, BIT, MAN)]); assert_eq!(plain[1].edge.causal_mask(), CausalMask::SO); assert_eq!(epi(plain[1].edge).certification(), Certification3::Related); diff --git a/crates/lance-graph-quack/examples/wired_reasoning_probe.rs b/crates/lance-graph-quack/examples/wired_reasoning_probe.rs index 8bb7c10b4..196b268d4 100644 --- a/crates/lance-graph-quack/examples/wired_reasoning_probe.rs +++ b/crates/lance-graph-quack/examples/wired_reasoning_probe.rs @@ -83,8 +83,18 @@ fn corpus(n: usize) -> (Vec>, Vec>) { w.extend(["v", "den", "y"]); } if g.next(2) == 0 { - r.push(one(WEIL, Pos::Other)); - w.push("weil"); + // `. weil `: the cause event the Kausal lane marks. + let (cs, co) = (NOUNS[g.next(2) as usize], NOUNS[2 + g.next(2) as usize]); + r.extend([ + Reading::stop(), + one(WEIL, Pos::Other), + one(THE, Pos::Det), + one(cs, Pos::Noun), + one(VERBS[2], Pos::Verb), + one(THE, Pos::Det), + one(co, Pos::Noun), + ]); + w.extend([".", "weil", "die", "x", "v", "den", "y"]); } rs.push(r); ss.push(w); @@ -244,7 +254,8 @@ fn main() { ops.extend([THE, WHO, WEIL]); let abi = FrozenOperandAbi::new(1, &ops).unwrap(); let h = GrammarHeuristics::parse("# lane\tlemma\trelation\tcount\nKausal\tweil\tmark\t273\n"); - let (rows, rep) = wire(&markers, &ins, 1, &abi, Some(&h), ReadParams::RIGHT_CORNER).unwrap(); + let w = wire(&markers, &ins, 1, &abi, Some(&h), ReadParams::RIGHT_CORNER).unwrap(); + let (rows, rep) = (w.rows, w.report); let n = rows.len(); println!("rows {n} report {rep:?}"); From 81f28a6e4c07ea31490db61b08fa93d1c9935f91 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:32:24 +0000 Subject: [PATCH 08/23] deepnsm-v2 wire: make the single-implication silence test bite Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/deepnsm-v2/src/wire.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs index 669f8a90c..dfad39e8c 100644 --- a/crates/deepnsm-v2/src/wire.rs +++ b/crates/deepnsm-v2/src/wire.rs @@ -915,12 +915,15 @@ mod tests { let mut plain = svo(CAT, SAW, DOG); plain.push(Reading::stop()); plain.extend(svo(DOG, BIT, MAN)); + // The plain verse first, so the weil verse's cause row already has two + // supports and a positive pressure: only the single implication + // source keeps it from CausalCandidate. let w = run_in(&[ - weil_verse(), VerseIn { readings: plain, ..VerseIn::default() }, + weil_verse(), ]); assert_eq!(w.report.implications, 1); assert_eq!(w.report.implications_revised, 0); @@ -930,8 +933,14 @@ mod tests { .iter() .find(|r| r.verse == 1 && r.spo == Spo::new(CAT, SAW, DOG)) .unwrap(); + assert_eq!(cause.support, 2); + assert!(cause.edge.activation() > 0); assert_eq!(epi(cause.edge).certification(), Certification3::Related); - assert_eq!(cause.edge.causal_mask(), CausalMask::SO); + assert_eq!( + cause.edge.causal_mask(), + CausalMask::PO, + "the claim is causal" + ); } /// A `weil` clause with no matrix event states a cause of nothing: the From ed7e7575cbc2b30945e458d8eb5feacb05df5b44 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:33:10 +0000 Subject: [PATCH 09/23] board: weil -> belief arena wired; qualia palette separation measured Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- ...16-grammar-coordinates-annotation-wiring.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md index a81fa5840..3257dc2d4 100644 --- a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md +++ b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md @@ -176,3 +176,21 @@ Not done, recorded so it is not lost: - The 144 verbs (`contract/src/grammar/verb_table.rs`, 12×12 in a 16×16 space with TEKAMOLO slot priors) are not connected to the P code or the Pearl rung. + +## 8. Kausal claims through the belief arena; qualia separation measured + +- `wire` now returns `Wired { rows, report, facts, causes, events }`. Every + row is a unit observation `S –Rel(P)→ O` in a facts `BeliefArena`; every + `weil` claim a unit observation `cause ⇒ effect` (event terms) in a causes + arena, both stamped `Stamp::source(verse)`. `CausalCandidate` now requires + the implication to rest on ≥2 disjoint stamps; the per-statement + causal-verse counter is gone. Five disables red (causes stamp, facts stamp, + two sources, no self-implication, cause = stating event); the two-sources + test was vacuous until its fixture put the plain verse first. +- Reasoning probe on 741 rows: 0 violations on R1–R5, 72 candidates, 201 + implications, 138 pooled. +- Qualia palette (#1571, 219 patches, PCS hundredths): nearest-neighbour L1 + min 25, p10 50, median 90, max 250; no duplicates; 95 / 219 nearest + neighbours in the same family. So any PCS reading within L1 12 of a patch + decodes to that patch uniquely. A "3σ" statement needs the noise σ of a + reading, which is the unmeasured A2B side (`ISS-QUALIA-A2B-CLUT-UNMEASURED`). From d2d2ba273ae77f4bbf311e0ecd3e7063765aac15 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:37:03 +0000 Subject: [PATCH 10/23] =?UTF-8?q?deepnsm-v2=20wire:=20qualia=20register=20?= =?UTF-8?q?=E2=80=94=20(operand=20:=20qualia)=20pairs=20for=20S,=20P,=20O?= =?UTF-8?q?=20and=20the=20144-cell=20verb=20byte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each wired row carries a FacetCascade read as six u8:u8 pairs: pairs 0..2 hold the S, P, O operand codes beside a qualia palette index (contract::qualia_profile), pair 3 the verb_table morton cell with a presence flag (cell 0 is a real verb, Becomes/Present). An adjective's qualia lands on the subject or object pair of its noun, an adverb's on the predicate pair of its event, so one index serves both and the pair is the distinction. The caller supplies the per-token index; no word -> qualia lexicon exists yet. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/deepnsm-v2/src/wire.rs | 329 ++++++++++++++++++ .../examples/wired_reasoning_probe.rs | 1 + 2 files changed, 330 insertions(+) diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs index dfad39e8c..bf9f9b7ca 100644 --- a/crates/deepnsm-v2/src/wire.rs +++ b/crates/deepnsm-v2/src/wire.rs @@ -95,6 +95,9 @@ use lance_graph_arm_discovery::{arm_to_truth_u8, CandidateRule, Item}; use lance_graph_contract::causal_witness::{CausalWitnessFacet, Locus}; use lance_graph_contract::epistemic_state5::{Certification3, Epi5Gen, EpistemicState5, Topology2}; use lance_graph_contract::exploration::NarsTruth; +use lance_graph_contract::facet::{CascadeShape, FacetCascade}; +use lance_graph_contract::grammar::verb_lexicon::classify_verb; +use lance_graph_contract::grammar::verb_table::morton_cell; use lance_graph_contract::hhtl::NiblePath; use crate::belief::{BeliefArena, CStmt, Copula, ReviseOutcome, Stamp}; @@ -187,6 +190,10 @@ pub struct VerseIn<'a> { pub surface: Vec<&'a str>, /// Haspelmath form attestations cited for this verse. pub forms: Vec, + /// Qualia palette index per token (`contract::qualia_profile`, `0` = + /// unnamed), aligned with `readings`; empty = none. Where the index comes + /// from is the caller's: this crate holds no word → qualia lexicon. + pub qualia: Vec, } /// One wired triple. @@ -212,6 +219,42 @@ pub struct WiredRow { pub support: u32, /// Independent verses that gave `(S, P)` so far (`tot`). pub antecedent: u32, + /// The qualia register, read under [`QUALIA_SHAPE`]: pairs 0..2 are + /// `(operand code : qualia)` for S, P, O; pair 3 is `(verb cell : 1)` when + /// the verb classifies, `(0 : 0)` when not. Class id 0 (default class). + pub qualia: FacetCascade, +} + +/// The carving the qualia register is read under: six `u8:u8` pairs. +pub const QUALIA_SHAPE: CascadeShape = CascadeShape::G6D2; + +/// Pair index of the subject in the qualia register. +pub const Q_SUBJECT: u8 = 0; +/// Pair index of the predicate. +pub const Q_PREDICATE: u8 = 1; +/// Pair index of the object. +pub const Q_OBJECT: u8 = 2; +/// Pair index of the verb cell (`verb_table::morton_cell`, flag byte). +pub const Q_VERB_CELL: u8 = 3; + +/// The qualia index on pair `pair` (level 1 of the `u8:u8`). +#[must_use] +pub fn qualia_at(f: FacetCascade, pair: u8) -> u8 { + f.cascade_byte(QUALIA_SHAPE, pair, 1) +} + +/// The operand code on pair `pair` (level 0 of the `u8:u8`). +#[must_use] +pub fn operand_at(f: FacetCascade, pair: u8) -> u8 { + f.cascade_byte(QUALIA_SHAPE, pair, 0) +} + +/// The verb cell, when the verb classified. Cell `0` is a real verb +/// (`Becomes`, `Present`), so presence is the flag byte, never the cell. +#[must_use] +pub fn verb_cell(f: FacetCascade) -> Option { + (f.cascade_byte(QUALIA_SHAPE, Q_VERB_CELL, 1) == 1) + .then(|| f.cascade_byte(QUALIA_SHAPE, Q_VERB_CELL, 0)) } /// What the wiring did, so a zero field is never mistaken for a reading. @@ -243,6 +286,20 @@ pub struct WireReport { pub implications_revised: usize, /// Events not interned because the u16 event-term space was full. pub event_overflow: usize, + /// Qualia tokens attached to a subject or object (adjectival). + pub qualia_adjectival: usize, + /// Qualia tokens attached to an event (adverbial). + pub qualia_adverbial: usize, + /// Qualia tokens with no slot to attach to (or in an untraced verse). + pub qualia_unattached: usize, + /// A second, different qualia for an already-qualified slot; the first is + /// kept. + pub qualia_conflicts: usize, + /// Triples whose verb surface classified to a 144-cell. + pub verb_classified: usize, + /// Triples whose verb did not classify (unknown, non-English, or a + /// homograph like `saw` the lexicon leaves unresolved). + pub verb_unclassified: usize, } /// What [`wire`] produced: the rows, the report, and the two belief arenas @@ -308,6 +365,120 @@ struct Group { objects: HashMap, } +/// Fill the qualia register of one traced verse's rows. +/// +/// - The operand codes come from the edge (bits 0..23). +/// - A qualia on an adjective attaches to the next noun of its sentence, on +/// every triple of that sentence where the noun is the subject or object. +/// - A qualia on any other skipped token (an adverb) attaches to an event: +/// the preceding triple of the sentence when no noun or verb stands +/// between its close and the token, otherwise the next triple, otherwise +/// the last of the sentence. +/// - The verb cell is `morton_cell(classify_verb(surface))` of the verb token +/// that closed into the triple's predicate. +fn attach_qualia(verse: &VerseIn<'_>, t: &[Traced], rows: &mut [WiredRow], rep: &mut WireReport) { + let r = &verse.readings; + let pos_of = |i: usize| r[i].pos.iter().next(); + let mut sentence = vec![0u32; r.len()]; + let mut s = 0; + for (i, slot) in sentence.iter_mut().enumerate() { + *slot = s; + if pos_of(i) == Some(Pos::Stop) { + s += 1; + } + } + // Pair g sits at bytes 4 + 2g (lo = qualia) and 5 + 2g (hi = operand). + let mut bytes: Vec<[u8; 16]> = rows + .iter() + .map(|row| { + let mut b = [0u8; 16]; + b[5] = row.edge.s_idx(); + b[7] = row.edge.p_idx(); + b[9] = row.edge.o_idx(); + b + }) + .collect(); + let content = |i: usize| matches!(pos_of(i), Some(Pos::Noun | Pos::Verb)); + for (i, &q) in verse.qualia.iter().enumerate() { + if q == 0 || i >= r.len() { + continue; + } + let sent = sentence[i]; + let in_sent: Vec = (0..t.len()).filter(|&k| t[k].sentence == sent).collect(); + let mut put = |k: usize, pair: u8| { + let at = 4 + 2 * pair as usize; + if bytes[k][at] != 0 && bytes[k][at] != q { + rep.qualia_conflicts += 1; + } else { + bytes[k][at] = q; + } + }; + let attached = match pos_of(i) { + Some(Pos::Adj) => { + let noun = (i + 1..r.len()) + .take_while(|&j| sentence[j] == sent) + .find(|&j| pos_of(j) == Some(Pos::Noun)); + let mut hit = false; + if let Some(n) = noun { + let w = r[n].id; + for &k in &in_sent { + if t[k].spo.subject == w { + put(k, Q_SUBJECT); + hit = true; + } + if t[k].spo.object == w { + put(k, Q_OBJECT); + hit = true; + } + } + } + hit.then_some(true) + } + Some(Pos::Other) => { + let prev = in_sent.iter().rev().copied().find(|&k| t[k].closed_at < i); + let next = in_sent.iter().copied().find(|&k| t[k].closed_at >= i); + let target = match (prev, next) { + (Some(p), _) if !(t[p].closed_at + 1..i).any(content) => Some(p), + (_, Some(n)) => Some(n), + (p, None) => p, + }; + target.map(|k| { + put(k, Q_PREDICATE); + false + }) + } + _ => None, + }; + match attached { + Some(true) => rep.qualia_adjectival += 1, + Some(false) => rep.qualia_adverbial += 1, + None => rep.qualia_unattached += 1, + } + } + for (k, tr) in t.iter().enumerate() { + let last = tr.closed_at.min(r.len().saturating_sub(1)); + let verb = (0..=last) + .rev() + .take_while(|&j| sentence[j] == tr.sentence) + .find(|&j| pos_of(j) == Some(Pos::Verb) && r[j].id == tr.spo.predicate); + let cell = verb + .and_then(|j| verse.surface.get(j)) + .and_then(|w| classify_verb(w)) + .map(|(f, tense)| morton_cell(f, tense)); + match cell { + Some(c) => { + bytes[k][10] = 1; + bytes[k][11] = c; + rep.verb_classified += 1; + } + None => rep.verb_unclassified += 1, + } + } + for (row, b) in rows.iter_mut().zip(bytes) { + row.qualia = FacetCascade::from_bytes(&b); + } +} + /// The arm-discovery rule `(S, P) → O` over the counts so far. fn rule(spo: Spo, pos: u32, tot: u32, window: u32) -> CandidateRule { CandidateRule { @@ -403,6 +574,7 @@ pub fn wire( origin: traced.as_ref().map(|t| t[k].origin), support: 0, antecedent: 0, + qualia: FacetCascade::default(), }); } @@ -451,6 +623,12 @@ pub fn wire( } } + if let Some(t) = traced.as_ref() { + attach_qualia(verse, t, &mut rows[first..], &mut rep); + } else { + rep.qualia_unattached += verse.qualia.iter().filter(|&&q| q != 0).count(); + } + // Kausal claims: the stating event is the cause (the clause `weil` // opens), every matrix event of the verse is an effect. Each pair is // one unit observation of `cause ⇒ effect`, stamped with the verse, so @@ -857,6 +1035,7 @@ mod tests { readings: r, surface, forms: vec![], + qualia: vec![], } } @@ -953,6 +1132,7 @@ mod tests { readings: r, surface: vec!["der", "hund", "biss", "den", "mann", "weil"], forms: vec![], + qualia: vec![], }; let w = run_in(&[v.clone(), v]); assert_eq!(w.report.intervention_rows, 2); @@ -997,6 +1177,155 @@ mod tests { assert!((b.truth.confidence - 0.5).abs() < 1e-6, "not pooled"); } + const QUICK: u8 = 42; + + /// One verse with surfaces and per-token qualia. + fn qverse(tokens: &[(u16, Pos, &'static str, u8)]) -> VerseIn<'static> { + VerseIn { + readings: tokens.iter().map(|&(id, p, _, _)| one(id, p)).collect(), + surface: tokens.iter().map(|&(_, _, w, _)| w).collect(), + forms: vec![], + qualia: tokens.iter().map(|&(_, _, _, q)| q).collect(), + } + } + + fn qrun(v: VerseIn<'static>) -> (WiredRow, WireReport) { + let w = wire(&[(1, 1)], &[v], 1, &abi(), None, ReadParams::RIGHT_CORNER).unwrap(); + (w.rows[0], w.report) + } + + /// The same qualia index is adjectival on the subject and adverbial on the + /// event: the pair carrying it is the distinction, not a tag. + #[test] + fn one_qualia_index_adjective_or_adverb_by_pair() { + let (adj, ra) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (WHO, Pos::Adj, "quick", QUICK), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + ])); + assert_eq!(adj.spo, Spo::new(DOG, BIT, MAN)); + assert_eq!(qualia_at(adj.qualia, Q_SUBJECT), QUICK); + assert_eq!(qualia_at(adj.qualia, Q_PREDICATE), 0); + assert_eq!(qualia_at(adj.qualia, Q_OBJECT), 0); + assert_eq!(ra.qualia_adjectival, 1); + + let (adv, rv) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + (WHO, Pos::Other, "quickly", QUICK), + ])); + assert_eq!(qualia_at(adv.qualia, Q_PREDICATE), QUICK); + assert_eq!(qualia_at(adv.qualia, Q_SUBJECT), 0); + assert_eq!(rv.qualia_adverbial, 1); + // The operand codes sit beside their qualia, equal to the edge's. + assert_eq!(operand_at(adv.qualia, Q_SUBJECT), adv.edge.s_idx()); + assert_eq!(operand_at(adv.qualia, Q_OBJECT), adv.edge.o_idx()); + } + + /// An adjective on the object lands on the object pair; an adverb before + /// the verb lands on the event; no qualia leaves every qualia byte 0. + #[test] + fn object_adjective_preverbal_adverb_and_silence() { + let (obj, _) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (WHO, Pos::Adj, "quick", QUICK), + (MAN, Pos::Noun, "man", 0), + ])); + assert_eq!(qualia_at(obj.qualia, Q_OBJECT), QUICK); + assert_eq!(qualia_at(obj.qualia, Q_SUBJECT), 0); + let (pre, _) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (WHO, Pos::Other, "quickly", QUICK), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + ])); + assert_eq!(qualia_at(pre.qualia, Q_PREDICATE), QUICK); + let (none, r) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + ])); + for pair in [Q_SUBJECT, Q_PREDICATE, Q_OBJECT] { + assert_eq!(qualia_at(none.qualia, pair), 0); + } + assert_eq!( + r.qualia_adjectival + r.qualia_adverbial + r.qualia_unattached, + 0 + ); + } + + /// An adverb after the first clause's object attaches to that clause, not + /// to the next one: no noun or verb stands between. + #[test] + fn a_trailing_adverb_stays_with_its_clause() { + let w = wire( + &[(1, 1)], + &[qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + (WHO, Pos::Other, "quickly", QUICK), + (THE, Pos::Other, "and", 0), + (THE, Pos::Det, "the", 0), + (CAT, Pos::Noun, "cat", 0), + (SAW, Pos::Verb, "saw", 0), + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + ])], + 1, + &abi(), + None, + ReadParams::RIGHT_CORNER, + ) + .unwrap(); + assert_eq!(w.rows.len(), 2); + assert_eq!(qualia_at(w.rows[0].qualia, Q_PREDICATE), QUICK); + assert_eq!(qualia_at(w.rows[1].qualia, Q_PREDICATE), 0); + } + + /// The verb cell is the 144-table byte; cell 0 (Becomes, Present) is real + /// and told apart from "unclassified" by the flag; `saw` stays + /// unclassified (a homograph the lexicon refuses). + #[test] + fn verb_cell_carries_a_presence_flag() { + use lance_graph_contract::grammar::role_keys::Tense; + use lance_graph_contract::grammar::verb_table::VerbFamily; + let verse = |verb: &'static str| { + qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, verb, 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + ]) + }; + let (caused, _) = qrun(verse("caused")); + assert_eq!( + verb_cell(caused.qualia), + Some(morton_cell(VerbFamily::Causes, Tense::Past)) + ); + let (becomes, _) = qrun(verse("becomes")); + assert_eq!(verb_cell(becomes.qualia), Some(0), "cell 0 is a real verb"); + let (saw, r) = qrun(verse("saw")); + assert_eq!(verb_cell(saw.qualia), None); + assert_eq!(r.verb_unclassified, 1); + } + #[test] fn plain_rows_stay_at_association() { let (plain, _) = run(&[svo(DOG, BIT, MAN), svo(DOG, BIT, MAN)]); diff --git a/crates/lance-graph-quack/examples/wired_reasoning_probe.rs b/crates/lance-graph-quack/examples/wired_reasoning_probe.rs index 196b268d4..331fb0c3c 100644 --- a/crates/lance-graph-quack/examples/wired_reasoning_probe.rs +++ b/crates/lance-graph-quack/examples/wired_reasoning_probe.rs @@ -247,6 +247,7 @@ fn main() { readings: r.clone(), surface: s.clone(), forms: vec![], + qualia: vec![], }) .collect(); let mut ops: Vec = NOUNS.to_vec(); From 7c9048fdd0091e07e53d68d33915b53daa6ecc71 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:37:48 +0000 Subject: [PATCH 11/23] deepnsm-v2 wire: an adjective does not cross a sentence (test) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/deepnsm-v2/src/wire.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs index bf9f9b7ca..3f96b4f66 100644 --- a/crates/deepnsm-v2/src/wire.rs +++ b/crates/deepnsm-v2/src/wire.rs @@ -1298,6 +1298,38 @@ mod tests { assert_eq!(qualia_at(w.rows[1].qualia, Q_PREDICATE), 0); } + /// An adjective with no noun after it in its sentence stays unattached; + /// it never reaches the next sentence's subject. + #[test] + fn an_adjective_does_not_cross_a_sentence() { + let w = wire( + &[(1, 1)], + &[qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + (WHO, Pos::Adj, "quick", QUICK), + (0, Pos::Stop, ".", 0), + (THE, Pos::Det, "the", 0), + (CAT, Pos::Noun, "cat", 0), + (SAW, Pos::Verb, "saw", 0), + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + ])], + 1, + &abi(), + None, + ReadParams::RIGHT_CORNER, + ) + .unwrap(); + assert_eq!(w.rows.len(), 2); + assert_eq!(qualia_at(w.rows[1].qualia, Q_SUBJECT), 0); + assert_eq!(w.report.qualia_unattached, 1); + assert_eq!(w.report.qualia_adjectival, 0); + } + /// The verb cell is the 144-table byte; cell 0 (Becomes, Present) is real /// and told apart from "unclassified" by the flag; `saw` stays /// unclassified (a homograph the lexicon refuses). From e1de6617cb9240245ed0f59eec51b6aa10809aa9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:38:13 +0000 Subject: [PATCH 12/23] deepnsm-v2 wire: sentence-bound adjective test bites (shared noun) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/deepnsm-v2/src/wire.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs index 3f96b4f66..7a261dfce 100644 --- a/crates/deepnsm-v2/src/wire.rs +++ b/crates/deepnsm-v2/src/wire.rs @@ -1312,11 +1312,13 @@ mod tests { (MAN, Pos::Noun, "man", 0), (WHO, Pos::Adj, "quick", QUICK), (0, Pos::Stop, ".", 0), + // The next sentence opens with `dog`, the subject of the + // first: an unbounded noun search would qualify it there. (THE, Pos::Det, "the", 0), - (CAT, Pos::Noun, "cat", 0), + (DOG, Pos::Noun, "dog", 0), (SAW, Pos::Verb, "saw", 0), (THE, Pos::Det, "the", 0), - (DOG, Pos::Noun, "dog", 0), + (CAT, Pos::Noun, "cat", 0), ])], 1, &abi(), @@ -1325,7 +1327,9 @@ mod tests { ) .unwrap(); assert_eq!(w.rows.len(), 2); - assert_eq!(qualia_at(w.rows[1].qualia, Q_SUBJECT), 0); + for row in &w.rows { + assert_eq!(qualia_at(row.qualia, Q_SUBJECT), 0); + } assert_eq!(w.report.qualia_unattached, 1); assert_eq!(w.report.qualia_adjectival, 0); } From 5e8a56e7fa68944d798d9b9c3610f93d07546fce Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:38:30 +0000 Subject: [PATCH 13/23] board: qualia register wired; word -> qualia source deferred Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- ...6-grammar-coordinates-annotation-wiring.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md index 3257dc2d4..9c33ee399 100644 --- a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md +++ b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md @@ -194,3 +194,25 @@ Not done, recorded so it is not lost: neighbours in the same family. So any PCS reading within L1 12 of a patch decodes to that patch uniquely. A "3σ" statement needs the noise σ of a reading, which is the unmeasured A2B side (`ISS-QUALIA-A2B-CLUT-UNMEASURED`). + +## 9. Qualia register (approved proposals #1, #2, #4) + +- `WiredRow::qualia` is a `FacetCascade` (class id 0) read `G6D2`, six + `u8:u8` pairs: 0..2 = `(operand code : qualia index)` for S, P, O; pair 3 = + `(verb_table::morton_cell : 1)`, or `(0 : 0)` when the verb does not + classify. An adjective's index lands on its noun's S/O pair, an adverb's + on its event's P pair: one index, two meanings, told apart by the pair. +- Deviation from the proposal, found while building: `morton_cell(Becomes, + Present) == 0`, a real verb, so the cell cannot share a pair whose zero + means "absent", and a reserved cell may not be borrowed as a sentinel. The + cell got its own pair with a presence byte. +- `saw` stays unclassified (`verb_lexicon` refuses the homograph); German + surfaces never classify (the lexicon is English). +- Five disables red. Two tests were vacuous on their first fixture and were + fixed until the disable bit: the single-implication case (plain verse + first) and the sentence-bound adjective (only bites when the next + sentence's noun also occurs in the current sentence's triples). +- OPEN (#3, deferred by the operator): the word → qualia index source. + Candidate for later: WordNet senses measured against the 219 patches' + Jina embeddings (`data/qualia/calibrated_219.bin1024`). Until then the + caller supplies indices and every index defaults to 0. From e31cd087386ad6760e131af4dbd309a75b98ad74 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:45:45 +0000 Subject: [PATCH 14/23] wire: W is the prior state's witness slot; activation is the update's frequency change W (53..58) now indexes a per-cohort WitnessTable<64>; each row's W points at the witness entry of the prior state of the same statement, so the W chain is the statement's Markov arc in reading order. A prior in another cohort starts a new arc and is counted (arc_breaks). Activation (46..49) is the change the sentence makes to the statement's arm frequency over the prior state, in eighths. The ladder and certification read the support balance (pos > neg) instead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/deepnsm-v2/src/wire.rs | 220 +++++++++++++----- .../examples/wired_reasoning_probe.rs | 2 +- 2 files changed, 165 insertions(+), 57 deletions(-) diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs index 7a261dfce..8d151aa07 100644 --- a/crates/deepnsm-v2/src/wire.rs +++ b/crates/deepnsm-v2/src/wire.rs @@ -16,9 +16,9 @@ //! | 32..39 | confidence | the same: `c = cooccur / (cooccur + k)`, `k = 1` (the NAL horizon) | //! | 40..42 | Pearl ladder | the TEKAMOLO read: a Kausal lane stated on this event → `PO` (the claim sits at the intervention rung); otherwise `SO` (association) | //! | 43..45 | orientation / syntax | the clause path ([`Origin`]) as a code on [`ORIENTATION_MAP`]; `0` = untraced | -//! | 46..49 | activation i4 | semantic pressure `pos - neg`, clamped to `-8..=7` | +//! | 46..49 | activation i4 | what this sentence's update caused: the change of the statement's arm frequency over the prior state, in eighths, clamped to `-8..=7`; `0` when the update changes nothing | //! | 50..52 | novelty → wisdom | the statement's rung after this update, `1` Staunen ..= `7` wisdom | -//! | 53..58 | belief-state update | the statement's last three updates: `W1` prior (53..54), `W2` (55..56), `W3` newest (57..58) | +//! | 53..58 | belief-state update | W slot into the cohort's [`WitnessTable`]: the entry that witnessed this statement's PRIOR state; `0` = first sighting or the prior is in another cohort | //! | 59..63 | epistemic state | `EpistemicState5`: topology from the clause path, certification from the belief state and the causes arena | //! //! # The belief arenas @@ -44,7 +44,7 @@ //! //! A statement `(S, P, O)` has one state: `pos`, its independent supporting //! verses; `tot`, the independent verses that gave `(S, P)` at all (so -//! `neg = tot - pos` gave another object); its rung; and its window. Each +//! `neg = tot - pos` gave another object); and its rung. Each //! observation updates the states it touches and nothing else, so the next //! state depends only on the current state and the observation: //! @@ -55,14 +55,22 @@ //! - an observation from a verse already counted is **dependent** (`3`): no new //! evidence, the NAL `DUPLICATE` / `OVERLAP` class. //! -//! The window shifts the newest kind in at `W3`, and `W1` falls off. Each row -//! records the state of its own statement right after its own update, so the -//! rows of one statement, in stream order, are its time series. +//! Reading order is the chain. Each row records its statement's state right +//! after its own update, and registers one [`WitnessEntry`] (its verse, its +//! facts-arena belief) in the cohort's [`WitnessTable`]. Its W slot points at +//! the entry of the statement's PREVIOUS row, as the contract says +//! (`witness_table`: "each edge's W-slot resolves to the entry that witnessed +//! the prior state"). Walking W from a row and resolving each entry back to +//! its row walks the statement's updates newest → oldest: W1 prior, W2, W3 are +//! the last three hops. A cohort is 63 consecutive rows in reading order +//! (slots `1..=63`, slot 0 = none); a prior in an earlier cohort cannot be +//! addressed by 6 bits, so W is 0 there and the report counts an arc break. //! //! # The ladder (bits 50..52) //! //! A first sighting sits at rung 1 (Staunen). A corroboration climbs one rung -//! only while the observation is resolved and the pressure stays positive: a +//! only while the observation is resolved and the support balance stays +//! positive (`pos > neg`): a //! rung is reached only when the one below holds. Resolved means single //! readings, traced, no incomplete Haspelmath map, no TEKAMOLO abstention and, //! for a relative clause, a bound antecedent. A contest drops the statement @@ -99,6 +107,7 @@ use lance_graph_contract::facet::{CascadeShape, FacetCascade}; use lance_graph_contract::grammar::verb_lexicon::classify_verb; use lance_graph_contract::grammar::verb_table::morton_cell; use lance_graph_contract::hhtl::NiblePath; +use lance_graph_contract::witness_table::{WitnessEntry, WitnessTable}; use crate::belief::{BeliefArena, CStmt, Copula, ReviseOutcome, Stamp}; use crate::fsm::{parse_readings, parse_to_spo_traced, Origin, Pos, Reading, Tagged, Traced}; @@ -168,17 +177,8 @@ pub enum Update { Dependent = 3, } -/// Shift `kind` into a 6-bit window at `W3` (bits 4..5); `W1` falls off. -#[must_use] -pub const fn shift_window(window: u8, kind: Update) -> u8 { - ((window & 0x3F) >> 2) | ((kind as u8) << 4) -} - -/// The three updates of a window, `[W1 prior, W2, W3 newest]`; `0` = empty. -#[must_use] -pub const fn window_slots(window: u8) -> [u8; 3] { - [window & 0b11, (window >> 2) & 0b11, (window >> 4) & 0b11] -} +/// Rows per witness cohort: slots `1..=63` of a `WitnessTable<64>`. +pub const COHORT_ROWS: usize = 63; /// One verse as the caller has it. #[derive(Debug, Clone, Default)] @@ -219,6 +219,8 @@ pub struct WiredRow { pub support: u32, /// Independent verses that gave `(S, P)` so far (`tot`). pub antecedent: u32, + /// The witness cohort this row's W slot is read in. + pub cohort: u32, /// The qualia register, read under [`QUALIA_SHAPE`]: pairs 0..2 are /// `(operand code : qualia)` for S, P, O; pair 3 is `(verb cell : 1)` when /// the verb classifies, `(0 : 0)` when not. Class id 0 (default class). @@ -284,6 +286,8 @@ pub struct WireReport { pub implications: usize, /// Of those, observations the arena pooled (disjoint stamps). pub implications_revised: usize, + /// Rows whose prior update sits in an earlier witness cohort (W = 0). + pub arc_breaks: usize, /// Events not interned because the u16 event-term space was full. pub event_overflow: usize, /// Qualia tokens attached to a subject or object (adjectival). @@ -319,6 +323,9 @@ pub struct Wired { pub causes: BeliefArena, /// Event term `i` is the triple `events[i]`. pub events: Vec, + /// One `WitnessTable<64>` per cohort of [`COHORT_ROWS`] rows, in reading + /// order. Entry = `(verse + 1, Some(facts-arena belief id))`. + pub witnesses: Vec>, } /// One unit of positive evidence at horizon 1. @@ -355,8 +362,9 @@ fn trace(readings: &[Reading], certain: &[Spo]) -> Option> { #[derive(Debug, Clone, Default)] struct StmtState { verses: HashSet, - window: u8, rung: u8, + /// `(cohort, slot)` of this statement's latest row. + last: Option<(u32, u8)>, } #[derive(Debug, Default)] @@ -479,6 +487,17 @@ fn attach_qualia(verse: &VerseIn<'_>, t: &[Traced], rows: &mut [WiredRow], rep: } } +/// A frequency change in `u8` codes (`-255..=255`) as eighths, rounded half +/// away from zero, clamped to the i4 range. +fn eighths(delta: i32) -> i8 { + let r = if delta >= 0 { + (delta * 8 + 127) / 255 + } else { + (delta * 8 - 127) / 255 + }; + r.clamp(-8, 7) as i8 +} + /// The arm-discovery rule `(S, P) → O` over the counts so far. fn rule(spo: Spo, pos: u32, tot: u32, window: u32) -> CandidateRule { CandidateRule { @@ -519,6 +538,7 @@ pub fn wire( let mut causes = BeliefArena::new(); let mut events: Vec = Vec::new(); let mut event_id: HashMap = HashMap::new(); + let mut witnesses: Vec> = Vec::new(); let mut next = 0usize; for (vi, verse) in verses.iter().enumerate() { @@ -574,6 +594,7 @@ pub fn wire( origin: traced.as_ref().map(|t| t[k].origin), support: 0, antecedent: 0, + cohort: 0, qualia: FacetCascade::default(), }); } @@ -680,10 +701,10 @@ pub fn wire( traced.is_some() && !ambiguous && !incomplete && !abstained && !unbound[k]; let group = groups.entry((spo.subject, spo.predicate)).or_default(); + let tot_prior = group.verses.len() as u32; if group.verses.insert(vi) { for (o, other) in &mut group.objects { if *o != spo.object { - other.window = shift_window(other.window, Update::Contest); if other.rung > 0 { other.rung = 1; } @@ -693,20 +714,28 @@ pub fn wire( } let tot = group.verses.len() as u32; let st = group.objects.entry(spo.object).or_default(); - let new_stmt = st.verses.is_empty(); + let pos_prior = st.verses.len() as u32; + let new_stmt = pos_prior == 0; let own_fresh = st.verses.insert(vi); let pos = st.verses.len() as u32; - let pressure = (i64::from(pos) - i64::from(tot - pos)).clamp(-8, 7) as i8; + // The support balance: more independent verses for this object than + // for every other object of the same (S, P). + let balance_pos = 2 * pos > tot; + // The activation this sentence causes: the change of the arm + // frequency over the prior state (unknown prior = arm's own 128). + let f_prior = + arm_to_truth_u8(&rule(spo, pos_prior, tot_prior, vi as u32), 1).frequency; + let f_now = arm_to_truth_u8(&rule(spo, pos, tot, (vi + 1) as u32), 1).frequency; + let activation = eighths(i32::from(f_now) - i32::from(f_prior)); let kind = if own_fresh { Update::Corroborate } else { rep.dependent += 1; Update::Dependent }; - st.window = shift_window(st.window, kind); if new_stmt { st.rung = 1; - } else if kind == Update::Corroborate && resolved && pressure > 0 { + } else if kind == Update::Corroborate && resolved && balance_pos { st.rung = (st.rung + 1).min(7); } @@ -723,7 +752,7 @@ pub fn wire( Some(_) if unbound[k] => Topology2::IndirectUnknown, Some(_) => Topology2::IndirectKnown, }; - facts.observe( + let fact = match facts.observe( CStmt { s: spo.subject, cop: Copula::Rel(spo.predicate), @@ -731,8 +760,37 @@ pub fn wire( }, unit(), Stamp::source(vi as u32), - ); - let certification = if pos >= 2 && pressure > 0 { + ) { + ReviseOutcome::Admitted { id } + | ReviseOutcome::Revised { id, .. } + | ReviseOutcome::Chosen { id, .. } => id, + }; + // Register this row's witness; W points at the prior row's entry. + let row_ix = first + k; + let (cohort, slot) = ((row_ix / COHORT_ROWS) as u32, (1 + row_ix % COHORT_ROWS) as u8); + if witnesses.len() <= cohort as usize { + witnesses.push(WitnessTable::new()); + } + witnesses[cohort as usize] + .set( + slot, + WitnessEntry { + mailbox_ref: vi as u32 + 1, + spo_fact_ref: Some(u64::from(fact)), + }, + ) + .expect("slot is 1..=63"); + let w_prior = match st.last { + Some((c, s)) if c == cohort => s, + Some(_) => { + rep.arc_breaks += 1; + 0 + } + None => 0, + }; + st.last = Some((cohort, slot)); + row.cohort = cohort; + let certification = if pos >= 2 && balance_pos { if cause_sources[k] >= 2 { rep.causal_candidates += 1; Certification3::CausalCandidate @@ -750,9 +808,9 @@ pub fn wire( edge.set_causal_mask(pearl); edge.set_direction(row.origin.map_or(0, orientation_code)); row.edge = edge - .with_activation(pressure) + .with_activation(activation) .with_novelty_wisdom(st.rung) - .with_w_slot(st.window) + .with_w_slot(w_prior) .with_epistemic_raw5(state.raw()); row.support = pos; row.antecedent = tot; @@ -765,6 +823,7 @@ pub fn wire( facts, causes, events, + witnesses, }) } @@ -835,20 +894,9 @@ mod tests { assert!(!orientation_contiguous(b(0) | b(1))); } - #[test] - fn window_shifts_newest_into_w3() { - let w = shift_window(0, Update::Corroborate); - assert_eq!(window_slots(w), [0, 0, 1]); - let w = shift_window(w, Update::Contest); - assert_eq!(window_slots(w), [0, 1, 2]); - let w = shift_window(w, Update::Dependent); - assert_eq!(window_slots(w), [1, 2, 3]); - let w = shift_window(w, Update::Corroborate); - assert_eq!(window_slots(w), [2, 3, 1], "W1 fell off"); - } - /// Can fire: three independent verses climb `dog bit man` to rung 3 with - /// arm truth f=1, c=3/4, pressure +3, three corroborations, Related. + /// arm truth f=1, c=3/4, Related. Only the first verse moves `f` (from the + /// unknown 128 to 255), so only it activates. #[test] fn independent_corroboration_climbs_the_ladder() { let v = vec![svo(DOG, BIT, MAN); 3]; @@ -857,9 +905,9 @@ mod tests { let last = rows[2].edge; assert_eq!(last.frequency_u8(), 255); assert_eq!(last.confidence_u8(), (3 * 255 / 4) as u8); - assert_eq!(last.activation(), 3); + let acts: Vec = rows.iter().map(|r| r.edge.activation()).collect(); + assert_eq!(acts, [4, 0, 0], "a repeat of a settled belief changes nothing"); assert_eq!(last.novelty_wisdom(), 3); - assert_eq!(window_slots(last.w_slot()), [1, 1, 1]); assert_eq!(epi(last).certification(), Certification3::Related); assert_eq!(epi(last).topology(), Topology2::Direct); assert_eq!(last.direction(), orientation_code(Origin::Main)); @@ -886,12 +934,12 @@ mod tests { let e = rows[1].edge; assert_eq!(rows[1].support, 1); assert_eq!(e.novelty_wisdom(), 1, "no climb on dependent evidence"); - assert_eq!(window_slots(e.w_slot()), [0, 1, 3]); + assert_eq!(e.activation(), 0, "dependent evidence moves no frequency"); assert_eq!(epi(e).certification(), Certification3::Associated); } - /// A contest drops the rival back to Staunen, and the pressure of a - /// contested statement reads the balance. + /// A contest drops the rival back to Staunen; the activation is the change + /// of the row's own frequency, and the climb waits for a positive balance. #[test] fn a_contest_resets_the_rival_and_presses_down() { let v = vec![ @@ -907,17 +955,18 @@ mod tests { "each later verse contests the existing rival" ); let cat = rows[4].edge; - assert_eq!(cat.activation(), 1, "pos 3, neg 2"); assert_eq!(cat.frequency_u8(), (3 * 255 / 5) as u8); - assert_eq!(rows[2].edge.activation(), -1, "pos 1 vs man's 2"); + let acts: Vec = rows.iter().map(|r| r.edge.activation()).collect(); + // man 128→255; man stays; cat 0→85; 85→127; 127→153. + assert_eq!(acts, [4, 0, 3, 1, 1]); assert_eq!(rows[2].edge.novelty_wisdom(), 1); - // pos 2 vs 2: zero pressure, no climb. - assert_eq!(rows[3].edge.activation(), 0); + // pos 2 vs 2: no positive balance, no climb. assert_eq!( rows[3].edge.novelty_wisdom(), 1, - "no climb without pressure" + "no climb without a positive balance" ); + assert_eq!(rows[4].edge.novelty_wisdom(), 2, "3 vs 2 climbs"); } /// The contested rival restarts at Staunen: `man` reached rung 2, a `cat` @@ -932,13 +981,72 @@ mod tests { ]; let (rows, _) = run(&v); assert_eq!(rows[1].edge.novelty_wisdom(), 2); - assert_eq!(rows[3].edge.activation(), 2, "pos 3, neg 1"); + assert_eq!(rows[3].edge.activation(), 1, "170 → 191"); assert_eq!( rows[3].edge.novelty_wisdom(), 2, "reset to 1, then one climb" ); - assert_eq!(window_slots(rows[3].edge.w_slot()), [1, 2, 1]); + // The man arc skips the cat row: W points at the man row of verse 1. + assert_eq!(rows[3].edge.w_slot(), 2); + } + + fn run_wired(verses: &[Vec]) -> Wired { + let markers: Vec<(u16, u16)> = (0..verses.len() as u16).map(|i| (1, i + 1)).collect(); + let ins: Vec> = verses + .iter() + .map(|r| VerseIn { + readings: r.clone(), + ..VerseIn::default() + }) + .collect(); + wire(&markers, &ins, 1, &abi(), None, ReadParams::RIGHT_CORNER).unwrap() + } + + /// The W slot is a Markov arc: from the newest row of a statement, each + /// W resolves to the witness of the prior state of the SAME statement, + /// skipping the rival's rows, and the walk ends at W = 0. + #[test] + fn w_walks_the_statement_arc_backwards() { + let v = vec![ + svo(DOG, BIT, MAN), + svo(DOG, BIT, CAT), + svo(DOG, BIT, MAN), + svo(DOG, BIT, CAT), + svo(DOG, BIT, MAN), + ]; + let w = run_wired(&v); + let table = &w.witnesses[0]; + let mut verses = vec![w.rows[4].verse]; + let mut facts = Vec::new(); + let mut slot = w.rows[4].edge.w_slot(); + while slot != 0 { + let e = table.get(slot).unwrap(); + let vi = (e.mailbox_ref - 1) as usize; + verses.push(vi); + facts.push(e.spo_fact_ref.unwrap()); + let prior = w.rows.iter().find(|r| r.verse == vi && r.spo.object == MAN).unwrap(); + slot = prior.edge.w_slot(); + } + assert_eq!(verses, [4, 2, 0], "newest to oldest, cat rows skipped"); + assert!(facts.windows(2).all(|p| p[0] == p[1]), "one statement"); + let cat_fact = table.get(w.rows[3].edge.w_slot()).unwrap().spo_fact_ref.unwrap(); + assert_ne!(cat_fact, facts[0], "the rival is its own arc"); + assert_eq!(w.report.arc_breaks, 0); + } + + /// A cohort holds 63 rows; a statement whose prior lies in the previous + /// cohort starts a new arc (W = 0) and the break is counted. + #[test] + fn a_cohort_boundary_breaks_the_arc() { + let v = vec![svo(DOG, BIT, MAN); COHORT_ROWS + 2]; + let w = run_wired(&v); + assert_eq!(w.witnesses.len(), 2); + assert_eq!(w.rows[COHORT_ROWS - 1].edge.w_slot(), (COHORT_ROWS - 1) as u8); + assert_eq!(w.rows[COHORT_ROWS].cohort, 1); + assert_eq!(w.rows[COHORT_ROWS].edge.w_slot(), 0, "prior is in cohort 0"); + assert_eq!(w.rows[COHORT_ROWS + 1].edge.w_slot(), 1); + assert_eq!(w.report.arc_breaks, 1); } /// F/C stays arm-discovery's law. `f` equals NAL revision of the unit @@ -1095,7 +1203,7 @@ mod tests { plain.push(Reading::stop()); plain.extend(svo(DOG, BIT, MAN)); // The plain verse first, so the weil verse's cause row already has two - // supports and a positive pressure: only the single implication + // supports and a positive balance: only the single implication // source keeps it from CausalCandidate. let w = run_in(&[ VerseIn { @@ -1113,7 +1221,7 @@ mod tests { .find(|r| r.verse == 1 && r.spo == Spo::new(CAT, SAW, DOG)) .unwrap(); assert_eq!(cause.support, 2); - assert!(cause.edge.activation() > 0); + assert!(2 * cause.support > cause.antecedent, "positive balance"); assert_eq!(epi(cause.edge).certification(), Certification3::Related); assert_eq!( cause.edge.causal_mask(), diff --git a/crates/lance-graph-quack/examples/wired_reasoning_probe.rs b/crates/lance-graph-quack/examples/wired_reasoning_probe.rs index 331fb0c3c..34b7222db 100644 --- a/crates/lance-graph-quack/examples/wired_reasoning_probe.rs +++ b/crates/lance-graph-quack/examples/wired_reasoning_probe.rs @@ -132,7 +132,7 @@ fn planes_of(rows: &[WiredRow]) -> Vec> { let facts = [ ("kausal", r.tekamolo[KAUSAL_LANE].is_some()), ("support2", r.support >= 2), - ("press_pos", e.activation() > 0), + ("press_pos", 2 * r.support > r.antecedent), ("main", r.origin == Some(Origin::Main)), ("climbed", e.novelty_wisdom() > 1), ("pearl_po", e.causal_mask() == CausalMask::PO), From ceecd2d641514d8dd80ceaf49a4fab77ae93b4e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:48:02 +0000 Subject: [PATCH 15/23] board: W arc and update-caused activation Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- ...6-grammar-coordinates-annotation-wiring.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md index 9c33ee399..78eee488d 100644 --- a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md +++ b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md @@ -216,3 +216,25 @@ Not done, recorded so it is not lost: Candidate for later: WordNet senses measured against the 219 patches' Jina embeddings (`data/qualia/calibrated_219.bin1024`). Until then the caller supplies indices and every index defaults to 0. + +## 10. W is the Markov arc; activation is what the update caused (`e31cd087`) + +- W (53..58) indexes a per-cohort `WitnessTable<64>` (63 rows per cohort, + slot 0 = no prior). Each row's entry is `(verse + 1, facts-arena id)`; its + W points at the entry of the PRIOR state of the same statement, so walking + W from the newest row visits that statement's verses newest to oldest and + skips rival rows. A prior in an earlier cohort starts a new arc + (`arc_breaks`). The earlier 3-slot window (`shift_window`) is gone: it + ignored the existing `WitnessTable` contract. +- Activation (46..49) = the change of the statement's arm frequency over the + prior state, in eighths (`128` = unknown prior). A repeat of a settled + belief activates 0. The ladder and certification read the support balance + (`pos > neg`) instead. +- Tests: `w_walks_the_statement_arc_backwards`, + `a_cohort_boundary_breaks_the_arc`, plus re-pinned activation values. Four + disables red (activation as pos − neg, cohort check removed, balance + always true, W = previous row). +- Probe R2 now reads `2·support > antecedent`; 741 rows, 0 violations. +- OPEN: bits 46..49 are also `InferenceType`'s mantissa, and + `contract::counterfactual::deposit_counterfactual` writes −6 there to mark + the road not taken. Activation and that marker cannot share the bits. From 18c2363e0f1906024c0dcec9dfc669ea3510804e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:54:50 +0000 Subject: [PATCH 16/23] wire: the Gadamer gate decides each update; contradiction is kept Every observation of (S, P, O) is an encounter with its (S, P) group's interpretive horizon (claims = objects met, roots = independent verses). GadamerRevision's verdict decides: only IncreaseEligible observations are counted, reach the facts arena, or climb the ladder. An echo counts nothing; a rival from a verse already counted is ContradictionPreserved (suspended). Claims are never withdrawn and the tension only grows, so a contest no longer resets the rival's rung. Rows carry the gate's kind and whether the object is contested. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/deepnsm-v2/src/wire.rs | 272 ++++++++++++++++++++++++++-------- 1 file changed, 209 insertions(+), 63 deletions(-) diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs index 8d151aa07..846a6bd77 100644 --- a/crates/deepnsm-v2/src/wire.rs +++ b/crates/deepnsm-v2/src/wire.rs @@ -46,14 +46,25 @@ //! verses; `tot`, the independent verses that gave `(S, P)` at all (so //! `neg = tot - pos` gave another object); and its rung. Each //! observation updates the states it touches and nothing else, so the next -//! state depends only on the current state and the observation: +//! state depends only on the current state and the observation. //! -//! - an observation of `(S, P, O)` from a verse not yet counted for it is a -//! **corroboration** (`1`) of `(S, P, O)`; -//! - if that verse is also new for `(S, P)`, it is a **contest** (`2`) of every -//! other `(S, P, O')`; -//! - an observation from a verse already counted is **dependent** (`3`): no new -//! evidence, the NAL `DUPLICATE` / `OVERLAP` class. +//! # The Gadamer gate decides; counting follows +//! +//! Each `(S, P)` group is an interpretive horizon +//! (`contract::revision::GadamerRevision`): its claims are the objects met, +//! its roots the independent verses. Every observation is an encounter, and +//! the gate's verdict decides whether anything is counted: +//! +//! - a new verse for the only object so far is `IndependentConfirmation` (or +//! `HorizonExpansion` for a first sighting): counted; +//! - a new verse while another object is in view is `HorizonFusion`: counted, +//! and both objects join the group's tension; +//! - a verse already counted is `Echo`: nothing counted; +//! - a rival object from a verse already counted is `ContradictionPreserved`: +//! suspended, nothing counted, the tension kept. +//! +//! Claims are never withdrawn and the tension only grows (G3): a contest +//! never resets the rival. Only admitted observations reach the facts arena. //! //! Reading order is the chain. Each row records its statement's state right //! after its own update, and registers one [`WitnessEntry`] (its verse, its @@ -73,9 +84,9 @@ //! positive (`pos > neg`): a //! rung is reached only when the one below holds. Resolved means single //! readings, traced, no incomplete Haspelmath map, no TEKAMOLO abstention and, -//! for a relative clause, a bound antecedent. A contest drops the statement -//! back to rung 1. Six resolved corroborations after the first sighting, -//! without a contest, reach wisdom. +//! for a relative clause, a bound antecedent. A contest leaves the rival's +//! rung as it is. Six resolved, admitted observations with a positive balance +//! after the first sighting reach wisdom. //! //! # F/C against NAL revision //! @@ -107,6 +118,11 @@ use lance_graph_contract::facet::{CascadeShape, FacetCascade}; use lance_graph_contract::grammar::verb_lexicon::classify_verb; use lance_graph_contract::grammar::verb_table::morton_cell; use lance_graph_contract::hhtl::NiblePath; +use lance_graph_contract::revision::{ + BasisView, CodebookId, EncounterEvidence, EvidenceMask, EvidentialEffect, GadamerRevision, + GrammarId, HorizonId, InterpretiveHorizon, LanguageId, LensId, QuestionId, RevisionDelta, + RevisionKind, RevisionPolicy, +}; use lance_graph_contract::witness_table::{WitnessEntry, WitnessTable}; use crate::belief::{BeliefArena, CStmt, Copula, ReviseOutcome, Stamp}; @@ -165,18 +181,6 @@ pub fn orientation_contiguous(codes: u8) -> bool { } } -/// The kind of one belief-state update (a 2-bit witness). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum Update { - /// An independent verse supports the statement. - Corroborate = 1, - /// An independent verse gave the same `(S, P)` with another object. - Contest = 2, - /// The verse was already counted: no new evidence. - Dependent = 3, -} - /// Rows per witness cohort: slots `1..=63` of a `WitnessTable<64>`. pub const COHORT_ROWS: usize = 63; @@ -221,6 +225,12 @@ pub struct WiredRow { pub antecedent: u32, /// The witness cohort this row's W slot is read in. pub cohort: u32, + /// The object is part of its `(S, P)` group's preserved tension: another + /// object was met for the same `(S, P)` and neither was withdrawn. + pub contested: bool, + /// What the Gadamer gate made of this observation (`None`: the group ran + /// out of gate bits and the observation was not admitted). + pub revision: Option, /// The qualia register, read under [`QUALIA_SHAPE`]: pairs 0..2 are /// `(operand code : qualia)` for S, P, O; pair 3 is `(verb cell : 1)` when /// the verb classifies, `(0 : 0)` when not. Class id 0 (default class). @@ -280,6 +290,12 @@ pub struct WireReport { pub contests: usize, /// Dependent observations (a verse already counted). pub dependent: usize, + /// Observations the gate suspended: a contradiction with no new + /// independent root (`ContradictionPreserved`). Nothing is counted. + pub suspended: usize, + /// Observations not admitted because a group had more than 256 distinct + /// verses or objects. + pub gate_overflow: usize, /// Rows certified `CausalCandidate`. pub causal_candidates: usize, /// `cause ⇒ effect` observations offered to the causes arena. @@ -367,10 +383,102 @@ struct StmtState { last: Option<(u32, u8)>, } -#[derive(Debug, Default)] +/// The gate's mask width: group-local roots and claims, 256 each. +type GateMask = [u64; 4]; + +/// Bit `i` of a [`GateMask`]. +fn gate_bit(i: u16) -> GateMask { + let mut m = [0u64; 4]; + m[usize::from(i / 64)] = 1 << (i % 64); + m +} + +const GATE_BITS: u16 = 256; + struct Group { verses: HashSet, objects: HashMap, + /// The `(S, P)` interpretive horizon: claims are this group's objects, + /// roots its independent verses (group-local bit indices). + horizon: InterpretiveHorizon<(), GateMask>, + roots: HashMap, + claims: HashMap, +} + +impl Default for Group { + fn default() -> Self { + let empty = GateMask::empty(); + Self { + verses: HashSet::new(), + objects: HashMap::new(), + horizon: InterpretiveHorizon { + id: HorizonId(0), + awareness: (), + question: QuestionId(0), + language: LanguageId(0), + grammar: GrammarId(0), + codebook: CodebookId(0), + lens: LensId(0), + projected_claims: empty, + independent_roots: empty, + inherited_roots: empty, + unresolved_tension: empty, + revision_index: 0, + }, + roots: HashMap::new(), + claims: HashMap::new(), + } + } +} + +impl Group { + /// Pass one observation of `(S, P, object)` from verse `vi` through the + /// Gadamer gate and adopt the resulting horizon. `None` when the group has + /// run out of root or claim bits (the observation is not admitted). + fn encounter(&mut self, vi: usize, object: u16) -> Option> { + let next_root = self.roots.len() as u16; + let next_claim = self.claims.len() as u16; + let root = *self.roots.entry(vi).or_insert(next_root); + let claim = *self.claims.entry(object).or_insert(next_claim); + if root >= GATE_BITS || claim >= GATE_BITS { + return None; + } + let prior = &self.horizon; + let own = gate_bit(claim); + // Claims are never withdrawn: the working whole keeps every object + // the group has met, so a rival stays in view as live tension. + let proposed = prior.projected_claims.union(&own); + let others = prior.projected_claims.difference(&own); + let contradictions = if others.is_empty() { + GateMask::empty() + } else { + others.union(&own) + }; + let encounter = EncounterEvidence { + proposed_claims: proposed, + independent_roots: gate_bit(root), + inherited_roots: GateMask::empty(), + resistance: GateMask::empty(), + contradictions, + affected_parts: contradictions, + }; + let ancestry = BasisView { + ancestry_independent_roots: prior.independent_roots, + ancestry_derived_roots: GateMask::empty(), + ancestor_claims: prior.projected_claims, + closes_cycle: false, + }; + let delta = GadamerRevision.revise(prior, &encounter, &ancestry); + self.horizon = delta.resulting.clone(); + Some(delta) + } + + /// Whether `object` is part of this group's preserved tension. + fn contested(&self, object: u16) -> bool { + self.claims + .get(&object) + .is_some_and(|&c| c < GATE_BITS && self.horizon.unresolved_tension.intersects(&gate_bit(c))) + } } /// Fill the qualia register of one traced verse's rows. @@ -535,6 +643,7 @@ pub fn wire( let mut rows: Vec = Vec::with_capacity(harness.encoded.len()); let mut groups: HashMap<(u16, u16), Group> = HashMap::new(); let mut facts = BeliefArena::new(); + let mut fact_ids: HashMap = HashMap::new(); let mut causes = BeliefArena::new(); let mut events: Vec = Vec::new(); let mut event_id: HashMap = HashMap::new(); @@ -595,6 +704,8 @@ pub fn wire( support: 0, antecedent: 0, cohort: 0, + contested: false, + revision: None, qualia: FacetCascade::default(), }); } @@ -701,22 +812,30 @@ pub fn wire( traced.is_some() && !ambiguous && !incomplete && !abstained && !unbound[k]; let group = groups.entry((spo.subject, spo.predicate)).or_default(); + // The gate decides whether and in which sense this observation + // revises the (S, P) horizon; counting runs only if it admits. + let delta = group.encounter(vi, spo.object); + let effect = delta.as_ref().map(|d| d.evidential_effect); + match delta.as_ref().map(|d| d.kind) { + None => rep.gate_overflow += 1, + Some(RevisionKind::HorizonFusion) => rep.contests += 1, + Some(RevisionKind::ContradictionPreserved) => rep.suspended += 1, + Some(RevisionKind::Echo) => rep.dependent += 1, + Some(_) => {} + } + let admitted = effect == Some(EvidentialEffect::IncreaseEligible); let tot_prior = group.verses.len() as u32; - if group.verses.insert(vi) { - for (o, other) in &mut group.objects { - if *o != spo.object { - if other.rung > 0 { - other.rung = 1; - } - rep.contests += 1; - } - } + if admitted { + group.verses.insert(vi); } let tot = group.verses.len() as u32; + let contested = group.contested(spo.object); let st = group.objects.entry(spo.object).or_default(); let pos_prior = st.verses.len() as u32; - let new_stmt = pos_prior == 0; - let own_fresh = st.verses.insert(vi); + let new_stmt = st.rung == 0; + if admitted { + st.verses.insert(vi); + } let pos = st.verses.len() as u32; // The support balance: more independent verses for this object than // for every other object of the same (S, P). @@ -727,15 +846,9 @@ pub fn wire( arm_to_truth_u8(&rule(spo, pos_prior, tot_prior, vi as u32), 1).frequency; let f_now = arm_to_truth_u8(&rule(spo, pos, tot, (vi + 1) as u32), 1).frequency; let activation = eighths(i32::from(f_now) - i32::from(f_prior)); - let kind = if own_fresh { - Update::Corroborate - } else { - rep.dependent += 1; - Update::Dependent - }; if new_stmt { st.rung = 1; - } else if kind == Update::Corroborate && resolved && balance_pos { + } else if admitted && resolved && balance_pos { st.rung = (st.rung + 1).min(7); } @@ -752,18 +865,24 @@ pub fn wire( Some(_) if unbound[k] => Topology2::IndirectUnknown, Some(_) => Topology2::IndirectKnown, }; - let fact = match facts.observe( - CStmt { - s: spo.subject, - cop: Copula::Rel(spo.predicate), - p: spo.object, - }, - unit(), - Stamp::source(vi as u32), - ) { - ReviseOutcome::Admitted { id } - | ReviseOutcome::Revised { id, .. } - | ReviseOutcome::Chosen { id, .. } => id, + // The facts arena counts, so it sees only admitted observations; + // a suspended or echoed row points at the fact as it stands (none + // for a claim never admitted). + let stmt = CStmt { + s: spo.subject, + cop: Copula::Rel(spo.predicate), + p: spo.object, + }; + let fact = if admitted { + let id = match facts.observe(stmt, unit(), Stamp::source(vi as u32)) { + ReviseOutcome::Admitted { id } + | ReviseOutcome::Revised { id, .. } + | ReviseOutcome::Chosen { id, .. } => id, + }; + fact_ids.insert(stmt, id); + Some(id) + } else { + fact_ids.get(&stmt).copied() }; // Register this row's witness; W points at the prior row's entry. let row_ix = first + k; @@ -776,7 +895,7 @@ pub fn wire( slot, WitnessEntry { mailbox_ref: vi as u32 + 1, - spo_fact_ref: Some(u64::from(fact)), + spo_fact_ref: fact.map(u64::from), }, ) .expect("slot is 1..=63"); @@ -814,6 +933,8 @@ pub fn wire( .with_epistemic_raw5(state.raw()); row.support = pos; row.antecedent = tot; + row.contested = contested; + row.revision = delta.map(|d| d.kind); } rep.rows = rows.len(); } @@ -969,28 +1090,53 @@ mod tests { assert_eq!(rows[4].edge.novelty_wisdom(), 2, "3 vs 2 climbs"); } - /// The contested rival restarts at Staunen: `man` reached rung 2, a `cat` - /// verse contests it, and the next `man` verse climbs from 1, not 2. + /// A contest does not null the rival: `man` reached rung 2, a `cat` verse + /// contests it, and the next `man` verse climbs on to 3. Both objects stay + /// in the group's preserved tension. #[test] - fn a_contested_rival_restarts_at_staunen() { + fn a_contested_rival_keeps_its_climb() { let v = vec![ svo(DOG, BIT, MAN), svo(DOG, BIT, MAN), svo(DOG, BIT, CAT), svo(DOG, BIT, MAN), ]; - let (rows, _) = run(&v); + let (rows, rep) = run(&v); assert_eq!(rows[1].edge.novelty_wisdom(), 2); assert_eq!(rows[3].edge.activation(), 1, "170 → 191"); - assert_eq!( - rows[3].edge.novelty_wisdom(), - 2, - "reset to 1, then one climb" - ); + assert_eq!(rows[3].edge.novelty_wisdom(), 3, "no reset, one climb"); + assert_eq!(rows[2].revision, Some(RevisionKind::HorizonFusion)); + assert_eq!(rows[3].revision, Some(RevisionKind::HorizonFusion)); + assert!(!rows[1].contested, "no rival yet"); + assert!(rows[2].contested && rows[3].contested); + assert_eq!(rep.contests, 2); // The man arc skips the cat row: W points at the man row of verse 1. assert_eq!(rows[3].edge.w_slot(), 2); } + /// A contradiction with no new independent root is suspended, not + /// counted: one verse states `dog bit man` and `dog bit cat`. The cat row + /// earns no support and no fact; the tension is kept. + #[test] + fn a_contradiction_from_one_source_is_suspended() { + let mut v = svo(DOG, BIT, MAN); + v.push(Reading::stop()); + v.extend(svo(DOG, BIT, CAT)); + let w = run_wired(&[v, svo(DOG, BIT, MAN)]); + assert_eq!(w.report.suspended, 1); + let cat = &w.rows[1]; + assert_eq!(cat.revision, Some(RevisionKind::ContradictionPreserved)); + assert_eq!(cat.support, 0); + assert_eq!(cat.antecedent, 1, "the group counts its one source once"); + assert!(cat.contested); + assert_eq!(w.witnesses[0].get(2).unwrap().spo_fact_ref, None); + let cat_stmt = CStmt { s: DOG, cop: Copula::Rel(BIT), p: CAT }; + assert!(w.facts.get(cat_stmt).is_none(), "the arena never counted it"); + // A later independent man verse fuses (the tension is still live). + assert_eq!(w.rows[2].revision, Some(RevisionKind::HorizonFusion)); + assert_eq!(w.rows[2].support, 2); + } + fn run_wired(verses: &[Vec]) -> Wired { let markers: Vec<(u16, u16)> = (0..verses.len() as u16).map(|i| (1, i + 1)).collect(); let ins: Vec> = verses From c9a3e13b227559811fffc17fdcbb6656b31a6b2b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 20:04:53 +0000 Subject: [PATCH 17/23] wire: 43..45 is the Haspelmath function; antecedents both ways; quorum and contradiction loci Orientation (43..45) is now the dative-map function the relation is read in (Haspelmath 2003, Fig. 8.1): 0 = plain core argument, 1..7 = Direction, Recipient, Experiencer, Purpose, Beneficiary, Predicative possessor, External possessor. The functions come from the forms attested inside the clause span (the morphology signal), narrowed by the row's TEKAMOLO lanes (Kausal -> Purpose, Lokal -> Direction). A form still allowing several functions leaves 0 and the row unresolved. The clause path moves to the topology only. The relative-clause antecedent is tested in both directions within the i4 reach, nearest first, as matrix subject or object. Every row also points at its nearest agreeing (Quorum) and disagreeing (Contradiction) peer within -8..+7 of the whole row stream, so a contradiction stays as a pointer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/deepnsm-v2/src/haspelmath.rs | 12 + crates/deepnsm-v2/src/wire.rs | 453 +++++++++++++++++++++++----- 2 files changed, 384 insertions(+), 81 deletions(-) diff --git a/crates/deepnsm-v2/src/haspelmath.rs b/crates/deepnsm-v2/src/haspelmath.rs index 60cce105e..4280f49ad 100644 --- a/crates/deepnsm-v2/src/haspelmath.rs +++ b/crates/deepnsm-v2/src/haspelmath.rs @@ -36,6 +36,18 @@ pub enum DativeFunction { } impl DativeFunction { + /// All eight functions in ordinal order. + pub const ALL: [Self; 8] = [ + Self::Direction, + Self::Recipient, + Self::Experiencer, + Self::Purpose, + Self::Beneficiary, + Self::PredicativePossessor, + Self::ExternalPossessor, + Self::Judicantis, + ]; + #[must_use] pub const fn bit(self) -> u8 { 1 << (self as u8) diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs index 846a6bd77..244e2d019 100644 --- a/crates/deepnsm-v2/src/wire.rs +++ b/crates/deepnsm-v2/src/wire.rs @@ -15,7 +15,7 @@ //! | 24..31 | frequency | `lance-graph-arm-discovery` [`arm_to_truth_u8`], rule `(S, P) → O` over independent verses: `f = cooccur / antecedent` | //! | 32..39 | confidence | the same: `c = cooccur / (cooccur + k)`, `k = 1` (the NAL horizon) | //! | 40..42 | Pearl ladder | the TEKAMOLO read: a Kausal lane stated on this event → `PO` (the claim sits at the intervention rung); otherwise `SO` (association) | -//! | 43..45 | orientation / syntax | the clause path ([`Origin`]) as a code on [`ORIENTATION_MAP`]; `0` = untraced | +//! | 43..45 | orientation / syntax | the Haspelmath function the relation is read in ([`orientation_code`]): the dative-map functions the clause's attested forms allow, narrowed by its TEKAMOLO lanes; `0` = plain core argument or undecided. The clause path ([`Origin`]) goes to the topology in 59..63 | //! | 46..49 | activation i4 | what this sentence's update caused: the change of the statement's arm frequency over the prior state, in eighths, clamped to `-8..=7`; `0` when the update changes nothing | //! | 50..52 | novelty → wisdom | the statement's rung after this update, `1` Staunen ..= `7` wisdom | //! | 53..58 | belief-state update | W slot into the cohort's [`WitnessTable`]: the entry that witnessed this statement's PRIOR state; `0` = first sighting or the prior is in another cohort | @@ -128,7 +128,8 @@ use lance_graph_contract::witness_table::{WitnessEntry, WitnessTable}; use crate::belief::{BeliefArena, CStmt, Copula, ReviseOutcome, Stamp}; use crate::fsm::{parse_readings, parse_to_spo_traced, Origin, Pos, Reading, Tagged, Traced}; use crate::haspelmath::{ - continuity, run_harness, FormAttestation, FrozenOperandAbi, HarnessError, MapVerdict, + continuity, run_harness, DativeFunction, FormAttestation, FrozenOperandAbi, HarnessError, + MapVerdict, }; use crate::spo::Spo; use crate::tekamolo::{read_clause, GrammarHeuristics, LaneAddress, ReadParams}; @@ -136,51 +137,49 @@ use crate::tekamolo::{read_clause, GrammarHeuristics, LaneAddress, ReadParams}; /// The TEKAMOLO lane index of Kausal, in Te-Ka-Mo-Lo order. pub const KAUSAL_LANE: usize = 1; -/// The orientation code of a clause path (bits 43..45). `0` is reserved for -/// "untraced", so a zero field never claims a path. +/// The orientation code of a Haspelmath function (bits 43..45): the +/// function the relation is read in, on the dative semantic map +/// (Haspelmath 2003, Fig. 8.1). `0` is the plain core argument (no dative-map +/// function); `1..=7` are the seven functions below. `Judicantis` has no code. #[must_use] -pub const fn orientation_code(origin: Origin) -> u8 { - match origin { - Origin::Main => 1, - Origin::Carried => 2, - Origin::SubjRelative => 3, - Origin::ObjRelative => 4, +pub const fn orientation_code(f: DativeFunction) -> Option { + match f { + DativeFunction::Direction => Some(1), + DativeFunction::Recipient => Some(2), + DativeFunction::Experiencer => Some(3), + DativeFunction::Purpose => Some(4), + DativeFunction::Beneficiary => Some(5), + DativeFunction::PredicativePossessor => Some(6), + DativeFunction::ExternalPossessor => Some(7), + DativeFunction::Judicantis => None, } } -/// The syntactic-function map: for each code `0..=4`, its neighbours as a -/// bitmask over codes (bit `c` = code `c`). Code 0 is on no map. -pub const ORIENTATION_MAP: [u8; 5] = [ - 0, - (1 << 2) | (1 << 3), // Main: Carried, SubjRelative - 1 << 1, // Carried: Main - (1 << 1) | (1 << 4), // SubjRelative: Main, ObjRelative - 1 << 3, // ObjRelative: SubjRelative -]; - -/// Is a set of orientation codes (bitmask over codes `1..=4`) one connected -/// region of [`ORIENTATION_MAP`]? The empty set, and any set containing code -/// 0 or a code above 4, is not. +/// The function an orientation code names; `None` for `0` (core argument) +/// and anything above 7. #[must_use] -pub fn orientation_contiguous(codes: u8) -> bool { - if codes == 0 || codes & 1 != 0 || codes >> 5 != 0 { - return false; - } - let mut seen = 1u8 << codes.trailing_zeros(); - loop { - let mut next = seen; - for (c, links) in ORIENTATION_MAP.iter().enumerate() { - if seen & (1 << c) != 0 { - next |= links & codes; - } - } - if next == seen { - return seen == codes; - } - seen = next; +pub const fn orientation_function(code: u8) -> Option { + match code { + 1 => Some(DativeFunction::Direction), + 2 => Some(DativeFunction::Recipient), + 3 => Some(DativeFunction::Experiencer), + 4 => Some(DativeFunction::Purpose), + 5 => Some(DativeFunction::Beneficiary), + 6 => Some(DativeFunction::PredicativePossessor), + 7 => Some(DativeFunction::ExternalPossessor), + _ => None, } } +/// TEKAMOLO lanes that select a dative-map function when a form is +/// multifunctional: Kausal (final reading) → Purpose, Lokal → Direction. +const LANE_FUNCTION: [u8; 4] = [ + 0, + DativeFunction::Purpose.bit(), + 0, + DativeFunction::Direction.bit(), +]; + /// Rows per witness cohort: slots `1..=63` of a `WitnessTable<64>`. pub const COHORT_ROWS: usize = 63; @@ -290,6 +289,16 @@ pub struct WireReport { pub contests: usize, /// Dependent observations (a verse already counted). pub dependent: usize, + /// Rows whose orientation names a dative-map function. + pub orientation_coded: usize, + /// Rows left at 0 because the forms allow several functions. + pub orientation_ambiguous: usize, + /// Rows whose one function has no code (Judicantis). + pub orientation_unrepresented: usize, + /// Rows whose Quorum locus points at an agreeing row. + pub quorum_bound: usize, + /// Rows whose Contradiction locus points at a disagreeing row. + pub contradiction_bound: usize, /// Observations the gate suspended: a contradiction with no new /// independent root (`ContradictionPreserved`). Nothing is counted. pub suspended: usize, @@ -475,9 +484,9 @@ impl Group { /// Whether `object` is part of this group's preserved tension. fn contested(&self, object: u16) -> bool { - self.claims - .get(&object) - .is_some_and(|&c| c < GATE_BITS && self.horizon.unresolved_tension.intersects(&gate_bit(c))) + self.claims.get(&object).is_some_and(|&c| { + c < GATE_BITS && self.horizon.unresolved_tension.intersects(&gate_bit(c)) + }) } } @@ -722,11 +731,21 @@ pub fn wire( Origin::SubjRelative => tr.spo.subject, _ => tr.spo.object, }; - let matrix = t.iter().enumerate().skip(k + 1).find(|(_, m)| { - m.sentence == tr.sentence - && !m.origin.is_relative() - && m.spo.subject == ante_word - }); + // Test every matrix event of the sentence within the i4 reach, + // before or after the relative clause, nearest first (the + // earlier one on a tie); the antecedent noun may be the + // matrix subject or object. + let matrix = t + .iter() + .enumerate() + .filter(|(j, m)| { + *j != k + && m.sentence == tr.sentence + && !m.origin.is_relative() + && (m.spo.subject == ante_word || m.spo.object == ante_word) + && offset(k, *j).is_some() + }) + .min_by_key(|(j, _)| (j.abs_diff(k), *j > k)); match matrix.and_then(|(j, _)| offset(k, j)) { Some(o) => { rows[first + k].loci = rows[first + k].loci.with(Locus::Antecedent, o); @@ -755,6 +774,69 @@ pub fn wire( } } + // Orientation: the dative-map functions of the forms attested inside + // each traced clause span, narrowed by the row's TEKAMOLO lanes. + let mut undecided = vec![false; n]; + if let Some(t) = traced.as_ref() { + for (k, tr) in t.iter().enumerate() { + // The clause span: after the previous triple of the same + // sentence, never before the sentence's own start. + let sentence_start = verse.readings[..tr.closed_at] + .iter() + .rposition(|r| r.pos.contains(Pos::Stop)) + .map_or(0, |i| i + 1); + let start = t[..k] + .iter() + .rev() + .find(|p| p.sentence == tr.sentence) + .map_or(sentence_start, |p| (p.closed_at + 1).max(sentence_start)); + let span = &verse.readings + [start.min(tr.closed_at)..=tr.closed_at.min(verse.readings.len() - 1)]; + let mut f = 0u8; + for r in span { + for a in verse.forms.iter().filter(|a| a.word == r.id) { + f |= a.functions; + } + } + let lanes = rows[first + k] + .tekamolo + .iter() + .enumerate() + .fold( + 0u8, + |m, (l, a)| { + if a.is_some() { + m | LANE_FUNCTION[l] + } else { + m + } + }, + ); + if f & lanes != 0 { + f &= lanes; + } + let code = match f.count_ones() { + 0 => 0, + 1 => { + let func = DativeFunction::ALL[f.trailing_zeros() as usize]; + orientation_code(func).unwrap_or_else(|| { + rep.orientation_unrepresented += 1; + 0 + }) + } + _ => { + rep.orientation_ambiguous += 1; + undecided[k] = true; + 0 + } + }; + if code != 0 { + rep.orientation_coded += 1; + } + rows[first + k].edge.set_direction(code); + } + } + if let Some(t) = traced.as_ref() { attach_qualia(verse, t, &mut rows[first..], &mut rep); } else { @@ -808,8 +890,12 @@ pub fn wire( for (k, row) in rows[first..].iter_mut().enumerate() { let spo = row.spo; let causal = row.tekamolo[KAUSAL_LANE].is_some(); - let resolved = - traced.is_some() && !ambiguous && !incomplete && !abstained && !unbound[k]; + let resolved = traced.is_some() + && !ambiguous + && !incomplete + && !abstained + && !unbound[k] + && !undecided[k]; let group = groups.entry((spo.subject, spo.predicate)).or_default(); // The gate decides whether and in which sense this observation @@ -842,8 +928,7 @@ pub fn wire( let balance_pos = 2 * pos > tot; // The activation this sentence causes: the change of the arm // frequency over the prior state (unknown prior = arm's own 128). - let f_prior = - arm_to_truth_u8(&rule(spo, pos_prior, tot_prior, vi as u32), 1).frequency; + let f_prior = arm_to_truth_u8(&rule(spo, pos_prior, tot_prior, vi as u32), 1).frequency; let f_now = arm_to_truth_u8(&rule(spo, pos, tot, (vi + 1) as u32), 1).frequency; let activation = eighths(i32::from(f_now) - i32::from(f_prior)); if new_stmt { @@ -886,7 +971,10 @@ pub fn wire( }; // Register this row's witness; W points at the prior row's entry. let row_ix = first + k; - let (cohort, slot) = ((row_ix / COHORT_ROWS) as u32, (1 + row_ix % COHORT_ROWS) as u8); + let (cohort, slot) = ( + (row_ix / COHORT_ROWS) as u32, + (1 + row_ix % COHORT_ROWS) as u8, + ); if witnesses.len() <= cohort as usize { witnesses.push(WitnessTable::new()); } @@ -925,7 +1013,6 @@ pub fn wire( edge.set_frequency_u8(truth.frequency); edge.set_confidence_u8(truth.confidence); edge.set_causal_mask(pearl); - edge.set_direction(row.origin.map_or(0, orientation_code)); row.edge = edge .with_activation(activation) .with_novelty_wisdom(st.rung) @@ -938,6 +1025,35 @@ pub fn wire( } rep.rows = rows.len(); } + // Quorum and Contradiction loci over the whole row stream (a text is one + // Markov chain): the nearest row within the i4 reach with the same + // (S, P) and the same object agrees, with another object disagrees. The + // contradiction is kept as a pointer, never dropped. + for i in 0..rows.len() { + let me = rows[i].spo; + let near = |same: bool| { + (i.saturating_sub(8)..(i + 8).min(rows.len())) + .filter(|&j| j != i && offset(i, j).is_some()) + .filter(|&j| { + let o = rows[j].spo; + o.subject == me.subject + && o.predicate == me.predicate + && (o.object == me.object) == same + }) + .min_by_key(|&j| (j.abs_diff(i), j > i)) + }; + let (agree, disagree) = (near(true), near(false)); + if let Some(j) = agree { + rows[i].loci = rows[i].loci.with(Locus::Quorum, offset(i, j).unwrap()); + rep.quorum_bound += 1; + } + if let Some(j) = disagree { + rows[i].loci = rows[i] + .loci + .with(Locus::Contradiction, offset(i, j).unwrap()); + rep.contradiction_bound += 1; + } + } Ok(Wired { rows, report: rep, @@ -961,6 +1077,8 @@ mod tests { const SAW: u16 = 21; const THE: u16 = 30; const WHO: u16 = 31; + const TO: u16 = 32; + const THERE: u16 = 33; fn one(id: u16, pos: Pos) -> Reading { Reading::new(id, PosSet::single(pos)) @@ -978,7 +1096,7 @@ mod tests { } fn abi() -> FrozenOperandAbi { - FrozenOperandAbi::new(1, &[DOG, MAN, CAT, BIT, SAW, THE, WHO]).unwrap() + FrozenOperandAbi::new(1, &[DOG, MAN, CAT, BIT, SAW, THE, WHO, TO, THERE]).unwrap() } fn run(verses: &[Vec]) -> (Vec, WireReport) { @@ -998,26 +1116,28 @@ mod tests { EpistemicState5::decode(Epi5Gen::V1, e.epistemic_raw5()).unwrap() } + /// The seven listed functions round-trip through codes 1..=7; code 0 is + /// the plain core argument; Judicantis has no code. #[test] - fn orientation_map_is_contiguous_where_the_hierarchy_is() { - let b = |c: u8| 1u8 << c; - assert!(orientation_contiguous(b(1) | b(2))); - assert!(orientation_contiguous(b(1) | b(3) | b(4))); - assert!( - !orientation_contiguous(b(1) | b(4)), - "Main–ObjRelative skips SubjRelative" - ); - assert!( - !orientation_contiguous(b(2) | b(3)), - "Carried–SubjRelative only via Main" + fn orientation_codes_round_trip() { + for f in DativeFunction::ALL { + match orientation_code(f) { + Some(c) => assert_eq!(orientation_function(c), Some(f)), + None => assert_eq!(f, DativeFunction::Judicantis), + } + } + let codes: Vec = DativeFunction::ALL + .iter() + .filter_map(|&f| orientation_code(f)) + .collect(); + assert_eq!( + codes, + [1, 2, 3, 4, 5, 6, 7], + "all seven non-zero states used" ); - assert!(!orientation_contiguous(0)); - assert!(!orientation_contiguous(b(0) | b(1))); + assert_eq!(orientation_function(0), None); } - /// Can fire: three independent verses climb `dog bit man` to rung 3 with - /// arm truth f=1, c=3/4, Related. Only the first verse moves `f` (from the - /// unknown 128 to 255), so only it activates. #[test] fn independent_corroboration_climbs_the_ladder() { let v = vec![svo(DOG, BIT, MAN); 3]; @@ -1027,11 +1147,15 @@ mod tests { assert_eq!(last.frequency_u8(), 255); assert_eq!(last.confidence_u8(), (3 * 255 / 4) as u8); let acts: Vec = rows.iter().map(|r| r.edge.activation()).collect(); - assert_eq!(acts, [4, 0, 0], "a repeat of a settled belief changes nothing"); + assert_eq!( + acts, + [4, 0, 0], + "a repeat of a settled belief changes nothing" + ); assert_eq!(last.novelty_wisdom(), 3); assert_eq!(epi(last).certification(), Certification3::Related); assert_eq!(epi(last).topology(), Topology2::Direct); - assert_eq!(last.direction(), orientation_code(Origin::Main)); + assert_eq!(last.direction(), 0, "no attested form: plain core argument"); assert_eq!(last.causal_mask(), CausalMask::SO); // The rows of one statement are its time series. let rungs: Vec = rows.iter().map(|r| r.edge.novelty_wisdom()).collect(); @@ -1062,7 +1186,7 @@ mod tests { /// A contest drops the rival back to Staunen; the activation is the change /// of the row's own frequency, and the climb waits for a positive balance. #[test] - fn a_contest_resets_the_rival_and_presses_down() { + fn a_contest_is_counted_and_the_balance_decides_the_climb() { let v = vec![ svo(DOG, BIT, MAN), svo(DOG, BIT, MAN), @@ -1130,8 +1254,15 @@ mod tests { assert_eq!(cat.antecedent, 1, "the group counts its one source once"); assert!(cat.contested); assert_eq!(w.witnesses[0].get(2).unwrap().spo_fact_ref, None); - let cat_stmt = CStmt { s: DOG, cop: Copula::Rel(BIT), p: CAT }; - assert!(w.facts.get(cat_stmt).is_none(), "the arena never counted it"); + let cat_stmt = CStmt { + s: DOG, + cop: Copula::Rel(BIT), + p: CAT, + }; + assert!( + w.facts.get(cat_stmt).is_none(), + "the arena never counted it" + ); // A later independent man verse fuses (the tension is still live). assert_eq!(w.rows[2].revision, Some(RevisionKind::HorizonFusion)); assert_eq!(w.rows[2].support, 2); @@ -1171,12 +1302,20 @@ mod tests { let vi = (e.mailbox_ref - 1) as usize; verses.push(vi); facts.push(e.spo_fact_ref.unwrap()); - let prior = w.rows.iter().find(|r| r.verse == vi && r.spo.object == MAN).unwrap(); + let prior = w + .rows + .iter() + .find(|r| r.verse == vi && r.spo.object == MAN) + .unwrap(); slot = prior.edge.w_slot(); } assert_eq!(verses, [4, 2, 0], "newest to oldest, cat rows skipped"); assert!(facts.windows(2).all(|p| p[0] == p[1]), "one statement"); - let cat_fact = table.get(w.rows[3].edge.w_slot()).unwrap().spo_fact_ref.unwrap(); + let cat_fact = table + .get(w.rows[3].edge.w_slot()) + .unwrap() + .spo_fact_ref + .unwrap(); assert_ne!(cat_fact, facts[0], "the rival is its own arc"); assert_eq!(w.report.arc_breaks, 0); } @@ -1188,7 +1327,10 @@ mod tests { let v = vec![svo(DOG, BIT, MAN); COHORT_ROWS + 2]; let w = run_wired(&v); assert_eq!(w.witnesses.len(), 2); - assert_eq!(w.rows[COHORT_ROWS - 1].edge.w_slot(), (COHORT_ROWS - 1) as u8); + assert_eq!( + w.rows[COHORT_ROWS - 1].edge.w_slot(), + (COHORT_ROWS - 1) as u8 + ); assert_eq!(w.rows[COHORT_ROWS].cohort, 1); assert_eq!(w.rows[COHORT_ROWS].edge.w_slot(), 0, "prior is in cohort 0"); assert_eq!(w.rows[COHORT_ROWS + 1].edge.w_slot(), 1); @@ -1223,8 +1365,8 @@ mod tests { } } - /// A subject relative gets its orientation code and an Antecedent pointer - /// to the matrix event; topology IndirectKnown. + /// A subject relative gets an Antecedent pointer to the matrix event; + /// topology IndirectKnown. With no attested form, orientation stays 0. #[test] fn subject_relative_points_at_its_matrix_event() { // the dog who bit the man saw the cat @@ -1242,7 +1384,7 @@ mod tests { let (rows, rep) = run(&[v]); assert_eq!(rows.len(), 2); assert_eq!(rows[0].origin, Some(Origin::SubjRelative)); - assert_eq!(rows[0].edge.direction(), 3); + assert_eq!(rows[0].edge.direction(), 0); assert_eq!(rows[0].loci.at(Locus::Antecedent), 1); assert_eq!(rep.antecedent_bound, 1); assert_eq!(epi(rows[0].edge).topology(), Topology2::IndirectKnown); @@ -1253,6 +1395,155 @@ mod tests { ); } + /// Right corner: `the cat bit the man who saw the dog`. The antecedent + /// `man` is the matrix OBJECT and the matrix event comes FIRST, so the + /// pointer goes back (-1). + #[test] + fn an_antecedent_behind_the_relative_is_found() { + let v = vec![ + one(THE, Pos::Det), + one(CAT, Pos::Noun), + one(BIT, Pos::Verb), + one(THE, Pos::Det), + one(MAN, Pos::Noun), + one(WHO, Pos::Rel), + one(SAW, Pos::Verb), + one(THE, Pos::Det), + one(DOG, Pos::Noun), + ]; + let (rows, rep) = run(&[v]); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].spo, Spo::new(CAT, BIT, MAN)); + assert!(rows[1].origin.is_some_and(Origin::is_relative)); + assert_eq!(rows[1].loci.at(Locus::Antecedent), -1); + assert_eq!((rep.antecedent_bound, rep.antecedent_unbound), (1, 0)); + } + + fn form(word: u16, fs: &[DativeFunction]) -> FormAttestation { + FormAttestation { + word, + functions: fs.iter().fold(0, |m, f| m | f.bit()), + coverage_complete: true, + evidence_root: 7, + } + } + + /// `the dog gave to the man`: the form `to` inside the clause decides the + /// orientation when it allows one function, abstains (0, undecided) when + /// it allows several, and the code stays out of a clause that lacks it. + #[test] + fn the_attested_form_sets_the_orientation() { + let gave_to = |extra: Vec| { + let mut v = vec![ + one(THE, Pos::Det), + one(DOG, Pos::Noun), + one(BIT, Pos::Verb), + one(TO, Pos::Other), + ]; + v.extend([one(THE, Pos::Det), one(MAN, Pos::Noun)]); + v.extend(extra); + v + }; + let run_forms = |v: Vec, forms: Vec| { + wire( + &[(1, 1)], + &[VerseIn { + readings: v, + forms, + ..VerseIn::default() + }], + 1, + &abi(), + None, + ReadParams::RIGHT_CORNER, + ) + .unwrap() + }; + let w = run_forms( + gave_to(vec![]), + vec![form(TO, &[DativeFunction::Recipient])], + ); + assert_eq!(w.rows[0].edge.direction(), 2, "Recipient"); + assert_eq!(w.report.orientation_coded, 1); + let w = run_forms( + gave_to(vec![]), + vec![form( + TO, + &[DativeFunction::Direction, DativeFunction::Recipient], + )], + ); + assert_eq!(w.rows[0].edge.direction(), 0); + assert_eq!(w.report.orientation_ambiguous, 1); + // A second clause without the form stays at 0. + let mut second = vec![Reading::stop()]; + second.extend(svo(CAT, SAW, DOG)); + let w = run_forms( + gave_to(second), + vec![form(TO, &[DativeFunction::Recipient])], + ); + assert_eq!(w.rows.len(), 2); + assert_eq!( + w.rows[1].edge.direction(), + 0, + "the form is in the other clause" + ); + } + + /// A Lokal lane on the clause narrows a multifunctional form to + /// Direction; without the lane the same form stays undecided. + #[test] + fn a_lokal_lane_narrows_the_form_to_direction() { + let h = + GrammarHeuristics::parse("# lane\tlemma\trelation\tcount\nLokal\tdort\tadvmod\t10\n"); + let readings = vec![ + one(THE, Pos::Det), + one(DOG, Pos::Noun), + one(BIT, Pos::Verb), + one(TO, Pos::Other), + one(THE, Pos::Det), + one(MAN, Pos::Noun), + one(THERE, Pos::Other), + ]; + let surface = vec!["the", "dog", "bit", "to", "the", "man", "dort"]; + let forms = vec![form( + TO, + &[DativeFunction::Direction, DativeFunction::Recipient], + )]; + let ins = [VerseIn { + readings, + surface, + forms, + ..VerseIn::default() + }]; + let w = wire( + &[(1, 1)], + &ins, + 1, + &abi(), + Some(&h), + ReadParams::RIGHT_CORNER, + ) + .unwrap(); + assert!(w.rows[0].tekamolo[3].is_some(), "the Lokal lane landed"); + assert_eq!(w.rows[0].edge.direction(), 1, "Direction"); + let w = wire(&[(1, 1)], &ins, 1, &abi(), None, ReadParams::RIGHT_CORNER).unwrap(); + assert_eq!(w.rows[0].edge.direction(), 0, "no lane: undecided"); + } + + /// Quorum points at the nearest agreeing row, Contradiction at the + /// nearest disagreeing one; across verses, the earlier one on a tie. + #[test] + fn quorum_and_contradiction_point_at_peers() { + let v = vec![svo(DOG, BIT, MAN), svo(DOG, BIT, CAT), svo(DOG, BIT, MAN)]; + let (rows, rep) = run(&v); + assert_eq!(rows[2].loci.at(Locus::Quorum), -2); + assert_eq!(rows[2].loci.at(Locus::Contradiction), -1); + assert_eq!(rows[1].loci.at(Locus::Contradiction), -1, "tie: the prior"); + assert_eq!(rows[1].loci.at(Locus::Quorum), 0, "no other cat row"); + assert_eq!(rows[0].loci.at(Locus::Quorum), 2); + assert_eq!((rep.quorum_bound, rep.contradiction_bound), (2, 3)); + } + /// An ambiguous token leaves the verse untraced: orientation 0, topology /// Unknown, and no climb. #[test] From fe0df60b48c18036c8f180220d640a55e3b74834 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 20:05:41 +0000 Subject: [PATCH 18/23] board: Gadamer gate, Haspelmath orientation, peer loci Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- ...6-grammar-coordinates-annotation-wiring.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md index 78eee488d..91e39038a 100644 --- a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md +++ b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md @@ -238,3 +238,38 @@ Not done, recorded so it is not lost: - OPEN: bits 46..49 are also `InferenceType`'s mantissa, and `contract::counterfactual::deposit_counterfactual` writes −6 there to mark the road not taken. Activation and that marker cannot share the bits. + +## 11. Gadamer gate, Haspelmath orientation, peer loci (`18c2363e`, `c9a3e13b`) + +- **Gate first (G5).** Each `(S, P)` group is an `InterpretiveHorizon` + (claims = objects, roots = independent verses, 256 bits each). Every + observation is passed through `GadamerRevision`; only `IncreaseEligible` + observations are counted, reach the facts arena, or climb. Claims are never + withdrawn, so a contest no longer resets the rival's rung (G3). A rival from + a verse already counted is `ContradictionPreserved`: suspended, no fact. + Rows carry the gate's kind and `contested`. +- **43..45 = Haspelmath.** The dative-map function the relation is read in + (Fig. 8.1): 0 core argument, 1..7 Direction, Recipient, Experiencer, + Purpose, Beneficiary, Predicative possessor, External possessor. Source: + the forms attested in the clause span (bounded by the sentence start), + narrowed by TEKAMOLO lanes (Kausal → Purpose, Lokal → Direction). Several + functions left → 0 and the row is unresolved. Judicantis has no code. The + clause path now feeds only the topology. The earlier clause-path code was + wrong. +- **Loci.** The antecedent is tested in both directions within −8..+7, + nearest first, as matrix subject or object (right-corner relatives were + missed before). Every row points at its nearest Quorum and Contradiction + peer in the whole row stream. +- Probe corpus (741 rows): 593 fusions, 8 suspended, 132 echoes, 423 quorum + and 609 contradiction pointers, 0 violations. `arc_breaks` 196: cohorts of + 63 rows cut many arcs. +- Nine disables red (four gate, five orientation/loci). +- Measured (DeReKo-2014, local only, CC BY-NC): the relative-pronoun reading + is a minority of every article form (`der` 4.6 %, `die` 13 %, `das` 13 %, + `den` 1.2 %); COCA has no relativizer tag at all (`that` = c 8.3 M / d + 5.0 M). Frequency gives a default only; the antecedent test promotes. +- OPEN: frequency-ranked promotion of an alternative reading in the parser; + case morphology for argument order (*Den Hund biss der Mann*); the + counterfactual lanes for `CausalCandidate` → `Causes`; concessive markers + as resistance; 32 × i4 loci over the full 16-byte lane; S/P/O as 16-byte + identities (8 × u8:u8) instead of the u8 palette. From f9b730ed1c87776205e4cc978440214f85f2e49a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 20:12:01 +0000 Subject: [PATCH 19/23] plan: ce64-grammar-wiring-v1 (D-CGW-0..12); board rows, plan index, latest state Maps the open items after the wire.rs field map: the versioned reading binding that must exist before wire edges leave the probe (43..45 and 46..49 are read as the pathology triad and the inference mantissa elsewhere), frequency-ranked defaults with antecedent-tested promotion (DeReKo / COCA shares measured), case-driven argument order, counterfactual lanes to Causes, concessive resistance, cross-cohort arcs, and the layout widenings. Corrects the das relative-pronoun share to 9.6 %. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/board/INTEGRATION_PLANS.md | 8 + .claude/board/LATEST_STATE.md | 5 + .claude/board/STATUS_BOARD.md | 20 ++ ...6-grammar-coordinates-annotation-wiring.md | 2 +- .claude/plans/ce64-grammar-wiring-v1.md | 230 ++++++++++++++++++ 5 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 .claude/plans/ce64-grammar-wiring-v1.md diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index dace6bf80..87cff79aa 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -1,3 +1,11 @@ +## 2026-10-16 (5) — ce64-grammar-wiring-v1 — every CE64 upper-half field of a parsed triple from its source → `.claude/plans/ce64-grammar-wiring-v1.md` + +**Status:** D-CGW-0 in PR; D-CGW-1..12 PROPOSAL, test-first. + +- Ships the `deepnsm-v2::wire` field map, the Gadamer gate in front of counting, the W arc, the Haspelmath orientation and the peer loci. +- First gate: wire edges carry readings of 43..45 and 46..49 that existing readers interpret differently (pathology triad, inference mantissa); they stay in the probe until a versioned binding exists (D-CGW-1). +- Then: frequency default with antecedent-tested promotion (D-CGW-2), case-driven argument order (D-CGW-3), concessive resistance (D-CGW-5), counterfactual lanes to `Causes` (D-CGW-4), cross-cohort arcs (D-CGW-6), layout widenings behind the envelope audit (D-CGW-7/8). + ## 2026-10-16 (4) — catch-up: 14 plans of 2026-10-12..16 and 59 imported plans had no index entry **Status:** INDEX CATCH-UP; no plan status changes. The 14 dated plans below were read at their header and status line; the 59 imported ones were not read. diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index bbf615eb6..8f0da59c2 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,8 @@ +## 2026-10-16 — CE64 grammar wiring (branch `ccr-9f3b8075-74u47l-ce64-grammar-wiring`, in PR) + +- `deepnsm-v2::wire` writes every CausalEdge64 field of a parsed triple from a named source, with the Gadamer gate (`contract::revision`) deciding before anything is counted; W is the statement's Markov arc in a `WitnessTable<64>`; 43..45 is the Haspelmath dative-map function; loci carry Antecedent, TEKAMOLO, Quorum and Contradiction pointers. Probe: `lance-graph-quack/examples/wired_reasoning_probe.rs`. Plan: `ce64-grammar-wiring-v1.md` (D-CGW-0..12). +- Contract inventory delta: none (consumes `revision`, `witness_table`, `causal_witness`, `epistemic_state5`). `deepnsm-v2` gains `wire` and `DativeFunction::ALL`; `causal-edge` gains `activation` / `novelty_wisdom` accessors. + ## 2026-10-16 — board catch-up for merged PRs #1388–#1588 (in PR) - **`PR_ARC_INVENTORY.md`:** 115 merged PRs between #1388 and #1588 had no entry; each now has one, written from its description and file list, in date order. Hygiene-only PRs (#1527, #1528, #1563, #1568, #1583) get none, per the termination clause. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 03d0b7978..0e287492f 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -1,3 +1,23 @@ +## D-CGW — CE64 grammar wiring: every upper-half field of a parsed triple from its source (2026-10-16) + +Plan: `.claude/plans/ce64-grammar-wiring-v1.md`. Entry: `entries/2026-10-16-grammar-coordinates-annotation-wiring.md` §7–§11. Related: D-SMD-2 (grammar topology from Haspelmath maps), D-SAG-CF-1 (counterfactual contract), D-LXC-29 (8 rails), EW64 OD-5. + +| D-id | scope | status | gate / falsifier | +|---|---|---|---| +| **D-CGW-0** | `deepnsm-v2::wire`: CE64 0..63 from named sources; Gadamer gate before counting; W = Markov arc in `WitnessTable<64>`; activation = update-caused frequency change; 43..45 = Haspelmath function; Antecedent / TEKAMOLO / Quorum / Contradiction loci; qualia register; `wired_reasoning_probe` | In PR | 229 tests; probe 741 rows, 5 rules, 0 violations; disable runs red per entry §7–§11 | +| **D-CGW-1** | versioned reading binding for 43..45 and 46..49; triad and mantissa readers refuse a grammar-bound edge | Queued | refuse/accept pair per reader; field isolation 43..49. **Gate:** wire edges stay in the probe until this lands | +| **D-CGW-2** | frequency-ranked default reading (COCA, DeReKo) + antecedent-tested promotion of the alternative (relative pronoun) with agreement | Queued | `der Mann, der …` promoted; `der Mann gab der Frau …` silent; English `that` promoted without a COCA Rel tag | +| **D-CGW-3** | case morphology decides argument order in the parser (`Den Hund biss der Mann`) | Queued | OVS flips, SVO unchanged, syncretic case unresolved; UD German GSD dev measured | +| **D-CGW-4** | counterfactual lanes: `CausalCandidate` → `Causes` only with `IncreaseEligible` ∧ `Necessary` | Queued | necessary / dispensable / not-run triple | +| **D-CGW-5** | concessive markers as gate resistance and refuted implication | Queued | marked vs unmarked contest | +| **D-CGW-6** | witness arcs across 63-row cohorts (196/741 probe rows broke) | Queued | arc walk across cohorts; `arc_breaks` 0 for the fixture | +| **D-CGW-7** | loci 24 → 32 × i4 over the full 16-byte lane | Queued | envelope audit; field isolation over 32 slots | +| **D-CGW-8** | S/P/O identity as 8 × u8:u8 rails beside the u8 palette | Queued | decision: palette stays in 0..23 | +| **D-CGW-9** | bits 46..49 ownership (activation vs inference mantissa) | Queued | `deposit_counterfactual` refused on a grammar-bound edge | +| **D-CGW-10** | `ce64-semantic-upper-half.md`: the grammar reading of 43..45 declared beside the triad | Queued | doc only | +| **D-CGW-11** | Judicantis and multifunctional forms without a lane: measure before adding context rules | Queued | measurement on a corpus with attested forms | +| **D-CGW-12** | word → qualia index source (WordNet × Jina) | Deferred (operator) | — | + ## Catch-up (2026-10-16) — D-ids named by merged PRs that had no row Found by the PR-arc catch-up over #1388–#1588. HubSPO-rs D-ids (D-IAM-2, D-IAM-3, D-QRY-1) belong to that repo's board; D-K16 and D-V3M were renamed in their own PRs (to D-KEY16, D-HPR). diff --git a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md index 91e39038a..5732a4920 100644 --- a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md +++ b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md @@ -265,7 +265,7 @@ Not done, recorded so it is not lost: 63 rows cut many arcs. - Nine disables red (four gate, five orientation/loci). - Measured (DeReKo-2014, local only, CC BY-NC): the relative-pronoun reading - is a minority of every article form (`der` 4.6 %, `die` 13 %, `das` 13 %, + is a minority of every article form (`der` 4.6 %, `die` 13 %, `das` 9.6 %, `den` 1.2 %); COCA has no relativizer tag at all (`that` = c 8.3 M / d 5.0 M). Frequency gives a default only; the antecedent test promotes. - OPEN: frequency-ranked promotion of an alternative reading in the parser; diff --git a/.claude/plans/ce64-grammar-wiring-v1.md b/.claude/plans/ce64-grammar-wiring-v1.md new file mode 100644 index 000000000..73525f1a4 --- /dev/null +++ b/.claude/plans/ce64-grammar-wiring-v1.md @@ -0,0 +1,230 @@ +# CE64 grammar wiring v1 — every upper-half field of a parsed triple from its source + +**Status:** D-CGW-0 SHIPPED IN PR (this branch); D-CGW-1..12 PROPOSAL, test-first, +nothing built. Board entry: `entries/2026-10-16-grammar-coordinates-annotation-wiring.md` +§7–§11 (what landed, with measurements and disable runs). + +**Scope.** `crates/deepnsm-v2/src/wire.rs` and the readers of the edges it writes. +A parsed verse becomes rows; each row's `CausalEdge64` bits 0..63 are written from +one named source. The probe `crates/lance-graph-quack/examples/wired_reasoning_probe.rs` +checks the field rules as Datalog over presence planes against a row oracle. + +**Reading order.** The CE64 field meanings follow +`docs/architecture/ce64-semantic-upper-half.md` (WORKING-MODEL) and the frozen +revision invariants G1–G7 (`entries/2026-10-14-revision-layer-invariants.md`). +Where this plan and those disagree, they win and this plan is corrected. + +## 0. What D-CGW-0 delivers (this PR) + +| bits | field | source in `wire.rs` | +|---|---|---| +| 0..23 | S, P, O | the frozen operand ABI of `run_harness` | +| 24..31, 32..39 | F, C | `arm_to_truth_u8`, rule `(S, P) → O`, counted only after the Gadamer gate admits | +| 40..42 | Pearl ladder | `PO` when a Kausal lane is stated on the event, else `SO` | +| 43..45 | orientation | the Haspelmath dative-map function (0 core argument, 1..7 Direction, Recipient, Experiencer, Purpose, Beneficiary, Predicative possessor, External possessor) from the forms attested in the clause span, narrowed by TEKAMOLO lanes | +| 46..49 | activation | the change of the statement's arm frequency this sentence caused, in eighths | +| 50..52 | novelty → wisdom | rung 1 Staunen ..= 7; climbs on an admitted, resolved observation with positive support balance; a contest never resets it | +| 53..58 | W | slot into the cohort's `WitnessTable<64>`: the witness of the same statement's prior state (the Markov arc) | +| 59..63 | epistemic state | topology from the clause path, certification from support balance and the causes arena | + +Besides the edge, every row carries `CausalWitnessFacet` loci (Antecedent, TEKAMOLO, +Quorum, Contradiction, all −8..+7 over the row stream), the qualia register +(`FacetCascade`, `G6D2`), the gate's `RevisionKind` and `contested`. + +Verification: 229 deepnsm-v2 tests; clippy `-D warnings` on the lib; the probe +(741 rows, 5 rules, 0 violations, each rule broken by a one-bit flip); disable runs +red for every guard listed in the board entry (§7–§11). + +## 1. Gate: nothing below leaves the probe until D-CGW-1 lands + +Two fields written by `wire.rs` already have other readers with other meanings: + +| bits | wire writes | existing readers read | +|---|---|---| +| 43..45 | Haspelmath function code | the per-plane pathology triad: `CausalEdge64::{s,p,o}_pathological`, `concern_level`, `network.rs` Simpson's-paradox detection, `compose` (`edge.rs:808`), `moore_nars16_probe`, `pearl_ladder_probe`, `witness_angle_probe` | +| 46..49 | activation (signed change) | the inference mantissa (+6 Intervention, −6 Counterfactual, NARS rule index): `pearl.rs`, `chain_counterfactual.rs`, `contract::counterfactual::deposit_counterfactual`, `cognitive-shader-driver/src/mailbox_soa.rs` (production) | + +So a wire edge handed to `CausalNetwork` would read code 1 (Direction) as +"S pathological", and one handed to the mailbox would read activation +4 as a NARS +rule. Today wire edges go only to the probe. **They must not go anywhere else +before D-CGW-1.** + +## 2. Work items + +Each item lists: goal; tests to write first (can fire + stays silent, non-trivial +inputs); files; dependencies; the decision it needs, if any. + +### D-CGW-1 — versioned reading binding for 43..45 and 46..49 + +- **Goal.** A wire edge declares which reading its bits carry, and every reader + refuses a reading it does not implement. `ce64-semantic-upper-half.md` + §"Encoding, reading, operation": readings are told apart by a versioned binding, + never inferred. `I-LEGACY-API-FEATURE-GATED` applies. +- **Tests first.** + - A triad reader (`s_pathological`, `CausalNetwork`) given an edge bound to the + grammar reading refuses it (error / `None`), and still reads a triad-bound edge. + - A mantissa reader (`pearl.rs` dispatch, `chain_counterfactual`, `mailbox_soa`) + refuses a grammar-bound edge; the −6 counterfactual tag still round-trips on a + mantissa-bound edge. + - Field-isolation matrix over bits 43..49 for both readings. +- **Files.** `causal-edge/src/{edge.rs,layout.rs,v2_layout_tests.rs}`; the binding + site (where an edge's reading is known: the tenant's ClassView, not a new bit); + `docs/architecture/ce64-semantic-upper-half.md` (the "code today" column). +- **Decision.** Where the binding lives. Options: (a) the tenant / ClassView of the + row that holds the edge; (b) a field outside the edge. A bit inside the edge is + excluded (no free bit). Recommendation: (a), because a reading is a property of + the class that holds the register. + +### D-CGW-2 — frequency-ranked default, antecedent-tested promotion of the alternative reading + +- **Goal.** Corpus frequency gives each ambiguous token a default reading (a + best-effort median, never a decision). When the readings contradict, the + alternative reading is promoted as a constituent of a different parse if the + grammar test passes: the relative pronoun's antecedent within −8..+7, with + morphological agreement. +- **Measured basis (2026-10-16, DeReKo-2014 local only, CC BY-NC, never committed).** + The relative-pronoun reading is a minority of every article form: `der` ART + 241.4 M / PRELS 11.5 M (4.6 %), `die` 13 %, `das` PDS 11.6 M / PRELS 6.3 M (9.6 %), `den` + 1.2 %, `dem` 3.3 %; `welche` PRELS dominant. COCA has no relativizer tag: `that` + c 8.3 M / d 5.0 M / r 0.1 M, `which` d only, `who` p only. So the most frequent + reading is wrong exactly at a relative clause, and in English frequency cannot + even propose it. +- **Tests first.** + - `der Mann, der den Hund sah, ...`: the second `der` (frequency default ART) + is promoted to Rel because an antecedent noun of matching gender/number lies + within −8..−1 and the following clause lacks a subject. Stays silent: `der + Mann gab der Frau das Buch` (no comma clause, no gap) keeps ART. + - English `the man that saw the dog bit the cat`: `that` promoted to Rel by the + antecedent test although COCA has no Rel reading; silent on `he said that the + dog bit the cat` (no antecedent noun directly before). + - A promotion is recorded as a contradiction resolved by the grammar (the gate's + resistance), never as a silent re-tag. +- **Files.** `deepnsm-v2/src/{fsm.rs,lexical.rs,coca.rs}`; a DeReKo adapter beside + `coca.rs` (reads a local path, like `examples/dereko_profile.rs`; nothing + committed); `wire.rs` only consumes the result. +- **Dependencies.** None for English; the DeReKo adapter for German. +- **Decision.** Agreement features: STTS has no gender/case, so agreement needs UD + features (`Gender`, `Number`, `Case`) or a small article table. Recommendation: + UD features when present, else the closed German article paradigm as a table. + +### D-CGW-3 — case morphology decides argument order + +- **Goal.** `Den Hund biss der Mann` yields `SPO(Mann, biss, Hund)`: when case + contradicts position, case wins, and the triple is built from it in the parser + (the role decision is the FSM's, not a post-hoc swap in `wire.rs`). +- **Tests first.** OVS with unambiguous case flips roles; SVO with agreeing case is + unchanged; syncretic case (`die Frau sah die Katze`) keeps position and marks the + token ambiguous (the row is not resolved, nothing climbs). +- **Files.** `deepnsm-v2/src/fsm.rs` (`Reading` gains an optional case reading); + `wire.rs` (none beyond reading the parse). +- **Measure.** On UD German GSD dev (fetched locally, 15,041 lines, 5,604 `Case=` + features): share of transitive clauses where case and position disagree; parse + agreement with UD `nsubj`/`obj` before and after. +- **Dependency.** D-CGW-2's agreement source. + +### D-CGW-4 — counterfactual lanes: `CausalCandidate` → `Causes` + +- **Goal.** A row is certified `Causes` only when `RevisionVerdict::is_acceptable` + holds: the gate said `IncreaseEligible` AND a counterfactual said `Necessary`. + The counterfactual removes the cause's contributing verses from the causes arena + and the effect's arc and re-evaluates the effect's support (W0 vs W_X, D-SAG-CF-1 + §0). Candidates run in parallel, one per hypothesis lane (64-lane budget per + batch; 72 candidates in the probe corpus → two batches). +- **Tests first.** A cause stated by two independent verses whose effect is + supported only through it → `Necessary` → `Causes`; the same cause where the + effect has an independent third source → `Dispensable` → stays `CausalCandidate`; + a candidate never attacked stays `CausalCandidate` (`NotRun`, G6). +- **Files.** `wire.rs` (record contributors per candidate); the replay over the + arenas; `contract::revision` (no change; consumes `CounterfactualVerdict`). +- **Constraints.** A counterfactual belief is never written as observed truth; the + −6 tag does not go into 46..49 of a grammar-bound edge (D-CGW-1). Single cuts + miss overdetermination (D-SAG-CF-1 §1): joint cuts are a follow-up. + +### D-CGW-5 — concessive markers as resistance and as a refuted implication + +- **Goal.** `obwohl X, Y` / `X, aber Y`: the writer marks a contradiction on + purpose. The clash enters the gate as `resistance` (it ranks above an unmarked + one) and `obwohl X, Y` seeds the counterfactual test `X ⇒ ¬Y` refuted. +- **Tests first.** Marked contest → resistance set, kind unchanged where G1–G7 + decide it; unmarked contest → no resistance. `obwohl` adds one refuted-implication + observation to the causes arena with the verse stamp. +- **Files.** `tekamolo.rs` (`GrammarHeuristics` gains a concessive row class), + `wire.rs` (`Group::encounter` passes resistance). +- **Decision.** Concessive is not one of the four TEKAMOLO lanes. Options: a fifth + lane in the heuristics table, or a separate marker table. Recommendation: a + separate marker table, so the four lane addresses stay unchanged. + +### D-CGW-6 — witness arcs across cohorts + +- **Problem.** A cohort is 63 rows, so 196 of 741 probe rows started a new arc + (`arc_breaks`). Whole books break arcs at every boundary. +- **Options to measure.** (a) the first row of a statement in a new cohort writes a + back-link entry whose `spo_fact_ref` names the same facts-arena id, so the arc is + continued by fact identity, not by slot; (b) cohorts per statement group instead + of per reading position. +- **Tests first.** A statement seen in rows 10, 70, 130 walks back 130 → 70 → 10 + under the chosen option; `arc_breaks` = 0 for that statement. +- **Measure.** `arc_breaks` on the probe corpus and on a KJV book. + +### D-CGW-7 — loci: 24 → 32 × i4 over the full 16-byte lane + +- **Goal.** Tenant 14 reads `G24N4` (24 loci in the 12-byte payload of a 4+12 + facet, slots 16..23 reserved). With the classid already in the row key + (D-LXC-29), all 16 bytes can hold 32 loci. +- **Needs.** `v3-envelope-auditor` review; `ENVELOPE_LAYOUT_VERSION` decision; + EW64 OD-5 (`ew64-witness-unification-v1.md`). +- **Tests first.** Field isolation over 32 slots; slots 16..31 read 0 until set; + the existing 16 named loci keep their offsets. + +### D-CGW-8 — S/P/O as 16-byte identities (8 × u8:u8) instead of the u8 palette + +- **Goal.** The frozen operand ABI caps a verse at 256 operands (8:8:8 in bits + 0..23). The row already has a 16-byte identity and a 16-byte edge facet; reading + them as 8 rails of `u8:u8` (D-LXC-29 measured 8 rails: one tenant fits 99.41 % of + KJV verses vs 95.66 % with 6) removes the cap. +- **Decision.** Whether the edge's 0..23 stays the palette (hot, 3 bytes) with the + identity beside it, or the identity replaces it. Recommendation: keep 0..23 as the + palette and resolve through the identity rails; changing 0..23 breaks every ISA + reader. + +### D-CGW-9 — bits 46..49 ownership (activation vs inference mantissa) + +- **Goal.** Settle D-CGW-1's second half: either activation keeps 46..49 under the + grammar binding and counterfactuals live only in their own lanes, or activation + moves. Approved direction (2026-10-16 table, row 6): activation owns the bits for + grammar-bound edges; the counterfactual lane is separate by design. +- **Tests first.** `deposit_counterfactual` on a grammar-bound edge is refused; + on a mantissa-bound edge unchanged. + +### D-CGW-10 — the clause-path → orientation correction downstream + +- **Goal.** The board entry §7 table and the plan text that named 43..45 as the + clause path are corrected (done in §11 of the entry); `ce64-semantic-upper-half.md` + gains the grammar reading of 43..45 as a declared reading beside the triad. +- **Files.** `docs/architecture/ce64-semantic-upper-half.md` only. + +### D-CGW-11 — Judicantis and multifunctional forms + +- **Open.** Judicantis has no code (seven states named, one core-argument state). + A form allowing several functions with no TEKAMOLO lane leaves 0 and the row + unresolved. Measure how often on a real corpus with attested forms before + deciding whether a context rule beyond Kausal → Purpose / Lokal → Direction is + needed. + +### D-CGW-12 — word → qualia index source (deferred) + +- **Deferred by the operator.** Candidate: WordNet senses measured against the 219 + patches' Jina embeddings (`data/qualia/calibrated_219.bin1024`). Until then the + caller supplies indices. + +## 3. Order + +D-CGW-1 → D-CGW-9 (same binding) → D-CGW-2 → D-CGW-3 → D-CGW-5 → D-CGW-4 → +D-CGW-6 → D-CGW-7 / D-CGW-8 (layout, need the envelope audit) → D-CGW-10 / 11. +D-CGW-12 waits for the operator. + +## 4. Not in scope + +- No new opcode, interpreter or executor; the probe's rules run as quack filters. +- No production write path for wire rows. +- No change to `contract::revision` (G1–G7 are frozen). From 443c33034b1709fb82b8c764c2c257019d0a7ee6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 20:18:22 +0000 Subject: [PATCH 20/23] board: #1590 arc entry Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/board/PR_ARC_INVENTORY.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index bfa9aba7c..c36037185 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -1,3 +1,10 @@ +## 2026-10-16 — lance-graph PR #1590 (merged `2a6d502d`, branch `ccr-9f3b8075-74u47l-violation-banners`, head `d43a6ac5`, 1 commit) — board: mark the gather designs as violations; point at D-HOLO H14c/H14d + +- **Added:** capital-letter VIOLATION banners on plans `wcoj-closing-atoms-v1`, `quack-multitable-bind-v1`, `metta-datalog-assimilation-v1-council-v3`, the D-CMR-5 row, the joins line of `relalg-v1`; entries `wcoj-closing-atoms-council`, `wcoj-probes-1-3`, `coresearch-wcoj-leapfrog`, `asm-accepted-proposals`; STATUS rows D-QMB-0..5, D-ASM-11/11A/11B, D-CMR-5, D-RA-4; both 2026-10-16 INTEGRATION_PLANS entries; `TD-NO-BOUNDED-U32-GATHER-1`. Each names `d-holo-h14c-thought-first-v1.md` and `d-holo-h14d-logic-mask-read-v1.md` as the correct plans. +- **Locked:** a read at a position taken from another lane's values (gather, foreign lanes, the preimage hop) is a violation; nothing is deleted. +- **Deferred / open:** the gather surface that already exists in code is a separate change. +- **Confidence:** n/a (board). + ## 2026-10-16 — lance-graph PR #1588 (merged `399c8558`, branch `ccr-9f3b8075-74u47l-cats-validity`, head `ba4d0b19`, 3 commits) — sap: optional CATS code fields bind three-valued through validity planes - **Added:** `CatsBatch::bind` builds one validity plane per optional `U32` field (`OPTIONAL_CODE_FIELDS = [6, 7, 8, 16, 20, 21]`, unit test pins it against `FIELDS`); `CatsBatch::masks(live)` returns live first, then the validity planes; `CatsBinder::field` binds optional fields with `validity: Some(validity_mask(..))`. Files: `lance-graph-sap` `bind.rs`, `binder.rs`, `tests/binder.rs`; two hypothesis notes in `lance-graph-glove-parity` `basis.rs`; `LATEST_STATE.md`, `PR_ARC_INVENTORY.md`. From 2898f124306b67fd662da83a53955af7ddd8d413 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 20:22:44 +0000 Subject: [PATCH 21/23] wire: count Kausal claims after the gate; Pos::Adv is adverbial Codex review on #1591: - The causes arena was fed before the per-row gate ran, so a suspended cause row still added its verse stamp and could reach CausalCandidate. The Kausal block now runs after the gate and observes a pair only when both the cause row and the effect row were admitted; CausalCandidate is promoted from Related afterwards. - attach_qualia matched only Pos::Other for adverbs; COCA r maps to Pos::Adv, so real adverbs were left unattached. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- crates/deepnsm-v2/src/wire.rs | 166 ++++++++++++++++++++++++---------- 1 file changed, 116 insertions(+), 50 deletions(-) diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs index 244e2d019..c79162a15 100644 --- a/crates/deepnsm-v2/src/wire.rs +++ b/crates/deepnsm-v2/src/wire.rs @@ -559,7 +559,7 @@ fn attach_qualia(verse: &VerseIn<'_>, t: &[Traced], rows: &mut [WiredRow], rep: } hit.then_some(true) } - Some(Pos::Other) => { + Some(Pos::Adv | Pos::Other) => { let prev = in_sent.iter().rev().copied().find(|&k| t[k].closed_at < i); let next = in_sent.iter().copied().find(|&k| t[k].closed_at >= i); let target = match (prev, next) { @@ -843,50 +843,8 @@ pub fn wire( rep.qualia_unattached += verse.qualia.iter().filter(|&&q| q != 0).count(); } - // Kausal claims: the stating event is the cause (the clause `weil` - // opens), every matrix event of the verse is an effect. Each pair is - // one unit observation of `cause ⇒ effect`, stamped with the verse, so - // the arena pools independent verses and never double counts one. - let mut cause_sources = vec![0u32; n]; - if let (Some(j), Some(t)) = (stating[KAUSAL_LANE], traced.as_ref()) { - let mut intern = |spo: Spo, rep: &mut WireReport| -> Option { - if let Some(&id) = event_id.get(&spo) { - return Some(id); - } - let Ok(id) = u16::try_from(events.len()) else { - rep.event_overflow += 1; - return None; - }; - events.push(spo); - event_id.insert(spo, id); - Some(id) - }; - for (e, tr) in t.iter().enumerate() { - if e == j || tr.origin != Origin::Main { - continue; - } - let (Some(c), Some(f)) = (intern(t[j].spo, &mut rep), intern(tr.spo, &mut rep)) - else { - continue; - }; - let stmt = CStmt { - s: c, - cop: Copula::Impl, - p: f, - }; - rep.implications += 1; - if matches!( - causes.observe(stmt, unit(), Stamp::source(vi as u32)), - ReviseOutcome::Revised { .. } - ) { - rep.implications_revised += 1; - } - let sources = causes.get(stmt).map_or(0, |b| b.stamp.0.count_ones()); - cause_sources[j] = cause_sources[j].max(sources); - } - } - // The belief-state update, row by row in stream order. + let mut admitted_rows = vec![false; n]; for (k, row) in rows[first..].iter_mut().enumerate() { let spo = row.spo; let causal = row.tekamolo[KAUSAL_LANE].is_some(); @@ -997,13 +955,9 @@ pub fn wire( }; st.last = Some((cohort, slot)); row.cohort = cohort; + // CausalCandidate is decided after the Kausal claims are counted. let certification = if pos >= 2 && balance_pos { - if cause_sources[k] >= 2 { - rep.causal_candidates += 1; - Certification3::CausalCandidate - } else { - Certification3::Related - } + Certification3::Related } else { Certification3::Associated }; @@ -1022,7 +976,67 @@ pub fn wire( row.antecedent = tot; row.contested = contested; row.revision = delta.map(|d| d.kind); + admitted_rows[k] = admitted; + } + + // Kausal claims: the stating event is the cause (the clause `weil` + // opens), every matrix event of the verse is an effect. Each pair is + // one unit observation of `cause ⇒ effect`, stamped with the verse, so + // the arena pools independent verses and never double counts one. + // It counts, so it runs after the gate: a pair is observed only when + // both the cause row and the effect row were admitted (G5). + if let (Some(j), Some(t)) = (stating[KAUSAL_LANE], traced.as_ref()) { + let mut cause_sources = 0u32; + let mut intern = |spo: Spo, rep: &mut WireReport| -> Option { + if let Some(&id) = event_id.get(&spo) { + return Some(id); + } + let Ok(id) = u16::try_from(events.len()) else { + rep.event_overflow += 1; + return None; + }; + events.push(spo); + event_id.insert(spo, id); + Some(id) + }; + for (e, tr) in t.iter().enumerate() { + if e == j || tr.origin != Origin::Main || !admitted_rows[j] || !admitted_rows[e] { + continue; + } + let (Some(c), Some(f)) = (intern(t[j].spo, &mut rep), intern(tr.spo, &mut rep)) + else { + continue; + }; + let stmt = CStmt { + s: c, + cop: Copula::Impl, + p: f, + }; + rep.implications += 1; + if matches!( + causes.observe(stmt, unit(), Stamp::source(vi as u32)), + ReviseOutcome::Revised { .. } + ) { + rep.implications_revised += 1; + } + cause_sources = + cause_sources.max(causes.get(stmt).map_or(0, |b| b.stamp.0.count_ones())); + } + // Two independent implication sources promote a Related cause. + let row = &mut rows[first + j]; + let state = EpistemicState5::decode(Epi5Gen::V1, row.edge.epistemic_raw5()) + .expect("written above as V1"); + if cause_sources >= 2 && state.certification() == Certification3::Related { + rep.causal_candidates += 1; + let up = EpistemicState5::new( + Epi5Gen::V1, + state.topology(), + Certification3::CausalCandidate, + ); + row.edge = row.edge.with_epistemic_raw5(up.raw()); + } } + rep.rows = rows.len(); } // Quorum and Contradiction loci over the whole row stream (a text is one @@ -1773,6 +1787,58 @@ mod tests { assert_eq!(operand_at(adv.qualia, Q_OBJECT), adv.edge.o_idx()); } + /// A token the lexicon tags as an adverb (`Pos::Adv`, COCA `r`) is + /// adverbial like a skipped `Other` token (Codex review on #1591). + #[test] + fn a_pos_adv_token_is_adverbial() { + let (adv, rv) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + (WHO, Pos::Adv, "quickly", QUICK), + ])); + assert_eq!(qualia_at(adv.qualia, Q_PREDICATE), QUICK); + assert_eq!((rv.qualia_adverbial, rv.qualia_unattached), (1, 0)); + } + + /// A Kausal claim whose cause row the gate suspended is not counted: the + /// verse states `dog bit man` and then `weil dog bit cat`, a rival from a + /// source already counted. Twice. Nothing reaches the causes arena (Codex + /// review on #1591). + #[test] + fn a_suspended_cause_is_not_counted() { + let mut r = svo(DOG, BIT, MAN); + r.push(Reading::stop()); + r.push(one(WHO, Pos::Other)); + r.extend(svo(DOG, BIT, CAT)); + let surface = vec![ + "der", "hund", "biss", "den", "mann", ".", "weil", "der", "hund", "biss", "die", + "katze", + ]; + let v = VerseIn { + readings: r, + surface, + forms: vec![], + qualia: vec![], + }; + let w = run_in(&[v.clone(), v]); + let cause = w + .rows + .iter() + .find(|x| x.spo == Spo::new(DOG, BIT, CAT)) + .unwrap(); + assert!( + cause.tekamolo[KAUSAL_LANE].is_some(), + "the Kausal lane landed" + ); + assert_eq!(cause.revision, Some(RevisionKind::ContradictionPreserved)); + assert_eq!(w.report.implications, 0); + assert!(w.causes.entries().is_empty()); + assert_eq!(w.report.causal_candidates, 0); + } + /// An adjective on the object lands on the object pair; an adverb before /// the verb lands on the event; no qualia leaves every qualia byte 0. #[test] From 7e81bd6027787bed06067fec2f1902099a4adbe5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 20:24:31 +0000 Subject: [PATCH 22/23] plan: CE64 is a contract with one global meaning per field (D-CGW-1, D-CGW-9 corrected) The versioned per-tenant reading binding is withdrawn: CE64 is an ISA wire format like TCP/IP or BF16, the fixed point readings are measured against. 43..45 is amended in the contract to orientation with all triad readers migrated; 46..49 stays the inference mantissa, so activation moves out of the edge until the contract gives it a field. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/board/STATUS_BOARD.md | 4 +- .claude/plans/ce64-grammar-wiring-v1.md | 82 ++++++++++++++++--------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index e4be68f43..b3606c6ab 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -5,7 +5,7 @@ Plan: `.claude/plans/ce64-grammar-wiring-v1.md`. Entry: `entries/2026-10-16-gram | D-id | scope | status | gate / falsifier | |---|---|---|---| | **D-CGW-0** | `deepnsm-v2::wire`: CE64 0..63 from named sources; Gadamer gate before counting; W = Markov arc in `WitnessTable<64>`; activation = update-caused frequency change; 43..45 = Haspelmath function; Antecedent / TEKAMOLO / Quorum / Contradiction loci; qualia register; `wired_reasoning_probe` | In PR | 229 tests; probe 741 rows, 5 rules, 0 violations; disable runs red per entry §7–§11 | -| **D-CGW-1** | versioned reading binding for 43..45 and 46..49; triad and mantissa readers refuse a grammar-bound edge | Queued | refuse/accept pair per reader; field isolation 43..49. **Gate:** wire edges stay in the probe until this lands | +| **D-CGW-1** | one global meaning per CE64 field, held in the `causal-edge` contract (no per-tenant readings; superseded the same day); amend 43..45 to orientation with all triad readers migrated | Queued | one code table shared by accessor, writer and readers; field isolation 43..49. **Gate:** wire edges stay in the probe until this lands | | **D-CGW-2** | frequency-ranked default reading (COCA, DeReKo) + antecedent-tested promotion of the alternative (relative pronoun) with agreement | Queued | `der Mann, der …` promoted; `der Mann gab der Frau …` silent; English `that` promoted without a COCA Rel tag | | **D-CGW-3** | case morphology decides argument order in the parser (`Den Hund biss der Mann`) | Queued | OVS flips, SVO unchanged, syncretic case unresolved; UD German GSD dev measured | | **D-CGW-4** | counterfactual lanes: `CausalCandidate` → `Causes` only with `IncreaseEligible` ∧ `Necessary` | Queued | necessary / dispensable / not-run triple | @@ -13,7 +13,7 @@ Plan: `.claude/plans/ce64-grammar-wiring-v1.md`. Entry: `entries/2026-10-16-gram | **D-CGW-6** | witness arcs across 63-row cohorts (196/741 probe rows broke) | Queued | arc walk across cohorts; `arc_breaks` 0 for the fixture | | **D-CGW-7** | loci 24 → 32 × i4 over the full 16-byte lane | Queued | envelope audit; field isolation over 32 slots | | **D-CGW-8** | S/P/O identity as 8 × u8:u8 rails beside the u8 palette | Queued | decision: palette stays in 0..23 | -| **D-CGW-9** | bits 46..49 ownership (activation vs inference mantissa) | Queued | `deposit_counterfactual` refused on a grammar-bound edge | +| **D-CGW-9** | 46..49 stays the ISA inference mantissa (law); activation moves out of the edge until the contract gives it a field | Queued | every wire edge decodes through the mantissa decoder as the inference it states | | **D-CGW-10** | `ce64-semantic-upper-half.md`: the grammar reading of 43..45 declared beside the triad | Queued | doc only | | **D-CGW-11** | Judicantis and multifunctional forms without a lane: measure before adding context rules | Queued | measurement on a corpus with attested forms | | **D-CGW-12** | word → qualia index source (WordNet × Jina) | Deferred (operator) | — | diff --git a/.claude/plans/ce64-grammar-wiring-v1.md b/.claude/plans/ce64-grammar-wiring-v1.md index 73525f1a4..365efbeb4 100644 --- a/.claude/plans/ce64-grammar-wiring-v1.md +++ b/.claude/plans/ce64-grammar-wiring-v1.md @@ -47,33 +47,49 @@ Two fields written by `wire.rs` already have other readers with other meanings: So a wire edge handed to `CausalNetwork` would read code 1 (Direction) as "S pathological", and one handed to the mailbox would read activation +4 as a NARS rule. Today wire edges go only to the probe. **They must not go anywhere else -before D-CGW-1.** +before D-CGW-1.** Under the contract rule (D-CGW-1) this is not a reading mismatch +to bind around: these are contract violations by `wire.rs`, to be fixed by +amending the contract (43..45) or by `wire.rs` conforming (46..49). ## 2. Work items Each item lists: goal; tests to write first (can fire + stays silent, non-trivial inputs); files; dependencies; the decision it needs, if any. -### D-CGW-1 — versioned reading binding for 43..45 and 46..49 - -- **Goal.** A wire edge declares which reading its bits carry, and every reader - refuses a reading it does not implement. `ce64-semantic-upper-half.md` - §"Encoding, reading, operation": readings are told apart by a versioned binding, - never inferred. `I-LEGACY-API-FEATURE-GATED` applies. -- **Tests first.** - - A triad reader (`s_pathological`, `CausalNetwork`) given an edge bound to the - grammar reading refuses it (error / `None`), and still reads a triad-bound edge. - - A mantissa reader (`pearl.rs` dispatch, `chain_counterfactual`, `mailbox_soa`) - refuses a grammar-bound edge; the −6 counterfactual tag still round-trips on a - mantissa-bound edge. - - Field-isolation matrix over bits 43..49 for both readings. -- **Files.** `causal-edge/src/{edge.rs,layout.rs,v2_layout_tests.rs}`; the binding - site (where an edge's reading is known: the tenant's ClassView, not a new bit); - `docs/architecture/ce64-semantic-upper-half.md` (the "code today" column). -- **Decision.** Where the binding lives. Options: (a) the tenant / ClassView of the - row that holds the edge; (b) a field outside the edge. A bit inside the edge is - excluded (no free bit). Recommendation: (a), because a reading is a property of - the class that holds the register. +### D-CGW-1 — one global meaning per CE64 field, held in the contract + +> ⊘ **Superseded 2026-10-16, same day:** the first version of this item was a +> per-tenant "versioned reading binding" for 43..45 and 46..49. That is wrong. +> CE64 is an ISA wire format, like TCP/IP or BF16: every field has one meaning +> everywhere, fixed by the contract, and nothing interprets it per tenant or per +> edge. The mantissa bits are law. Like GMT, the edge is the fixed point every +> reading is measured against, so it carries no qualia of its own: qualia and +> other readings sit beside it (the row's qualia register), never inside it. + +- **Rule.** The meaning of each field lives once, in the `causal-edge` contract + (`layout.rs`, the `edge.rs` accessors, and `docs/architecture/ce64-semantic-upper-half.md`). + Every writer conforms to it, `wire.rs` included. A field changes meaning only by + amending the contract once, with every reader migrated in the same change + (`I-LEGACY-API-FEATURE-GATED`). +- **Where `wire.rs` breaks it today.** + - 43..45: the contract says S/P/O pathology triad; `wire.rs` writes the + Haspelmath function. The operator ruled 43..45 = orientation / syntax + (Haspelmath for text). So the contract is amended to orientation, and the + triad's readers (`*_pathological`, `concern_level`, `network.rs` Simpson + check, `compose`) migrate or move their signal elsewhere. + - 46..49: the contract says inference mantissa, which the ISA executes + (+6 Intervention, −6 Counterfactual, NARS rule index). `wire.rs` writes + activation there. Because the mantissa is law, activation cannot share these + bits; see D-CGW-9. +- **Tests first.** For each amended field: the contract accessor, a writer and + every migrated reader agree on one table of codes; the field-isolation matrix + over 43..49; a reader of the old meaning that is not migrated fails to compile + (the accessor is renamed), never silently misreads. +- **Files.** `causal-edge/src/{layout.rs,edge.rs,network.rs,v2_layout_tests.rs}`, + the readers listed in §1, `ce64-semantic-upper-half.md`. +- **Open.** Moore for non-verbal Cartesian data and Haspelmath for text are two + code tables for 43..45. Under one global meaning they must be one table, or + orientation needs a code space both share. ### D-CGW-2 — frequency-ranked default, antecedent-tested promotion of the alternative reading @@ -187,14 +203,22 @@ inputs); files; dependencies; the decision it needs, if any. palette and resolve through the identity rails; changing 0..23 breaks every ISA reader. -### D-CGW-9 — bits 46..49 ownership (activation vs inference mantissa) - -- **Goal.** Settle D-CGW-1's second half: either activation keeps 46..49 under the - grammar binding and counterfactuals live only in their own lanes, or activation - moves. Approved direction (2026-10-16 table, row 6): activation owns the bits for - grammar-bound edges; the counterfactual lane is separate by design. -- **Tests first.** `deposit_counterfactual` on a grammar-bound edge is refused; - on a mantissa-bound edge unchanged. +### D-CGW-9 — bits 46..49: the inference mantissa is law; where does activation go? + +- **Contract.** 46..49 is the inference mantissa the ISA executes. It keeps that + meaning globally (D-CGW-1). +- **Consequence for `wire.rs`.** Writing activation into 46..49 contradicts the + contract. Until the contract gives activation a field, `wire.rs` must write the + mantissa (or nothing) there and keep activation outside the edge (the row, or a + lane beside it). +- **Supersedes** the row-6 direction of the 2026-10-16 table ("activation owns the + bits for grammar-bound edges"), which assumed per-tenant readings. +- **Tests first.** Every edge `wire.rs` emits decodes through the ISA mantissa + decoder as the inference it states; activation is read from its own home and + equals the frequency change. +- **Decision.** Where activation lives: a row field beside the edge, or an amended + contract field. Recommendation: a row field until the contract is amended, + because no 4-bit field of the edge is free. ### D-CGW-10 — the clause-path → orientation correction downstream From cd1b1b3cacd1d1225f3091736f9fe4d42961a3b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 20:28:03 +0000 Subject: [PATCH 23/23] plan: D-CGW-13 cross-oracle readout on CE64, D-CGW-14 OpenPSI as a qualia device Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/board/STATUS_BOARD.md | 2 ++ .claude/plans/ce64-grammar-wiring-v1.md | 47 +++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index b3606c6ab..8dfda1d48 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -17,6 +17,8 @@ Plan: `.claude/plans/ce64-grammar-wiring-v1.md`. Entry: `entries/2026-10-16-gram | **D-CGW-10** | `ce64-semantic-upper-half.md`: the grammar reading of 43..45 declared beside the triad | Queued | doc only | | **D-CGW-11** | Judicantis and multifunctional forms without a lane: measure before adding context rules | Queued | measurement on a corpus with attested forms | | **D-CGW-12** | word → qualia index source (WordNet × Jina) | Deferred (operator) | — | +| **D-CGW-13** | all oracles (Java OpenNARS, ONA, MeTTa, OpenCog, MORK) read out on the same CE64 fields | Queued | D-ORACLE readout | +| **D-CGW-14** | OpenPSI modulators as a qualia device profile into the #1571 PCS (demands stay goals) | Queued | — | ## Catch-up (2026-10-16) — D-ids named by merged PRs that had no row diff --git a/.claude/plans/ce64-grammar-wiring-v1.md b/.claude/plans/ce64-grammar-wiring-v1.md index 365efbeb4..4498f405e 100644 --- a/.claude/plans/ce64-grammar-wiring-v1.md +++ b/.claude/plans/ce64-grammar-wiring-v1.md @@ -241,11 +241,54 @@ inputs); files; dependencies; the decision it needs, if any. patches' Jina embeddings (`data/qualia/calibrated_219.bin1024`). Until then the caller supplies indices. +### D-CGW-13 — every oracle reads out on the same CE64 fields + +- **Why now.** Because each CE64 field has one global meaning (D-CGW-1), the + answers of independent reasoners can be compared on the same register: F and C + (24..39), the Pearl question (40..42), orientation (43..45), the inference + mantissa (46..49) and the certification ladder (59..63). This is the readout of + the existing oracle plan `d-oracle-fidelity-matrix-v1.md` (D-ORACLE), not a new + harness: Java OpenNARS (Pei Wang's lineage, `opennars/opennars`), ONA, MeTTa + (Hyperon and PeTTa, golden corpus #48), OpenCog (temporary oracle only, entry + `2026-10-16-opencog-oracle-scope.md`) and MORK (D-ASM-10). +- **Tests first.** One fixture, all oracles, each native answer recorded + verbatim; then a per-oracle projection onto the CE64 fields, where a field the + oracle has no notion of is written as "not comparable", never as 0. Falsifier: + two oracles that disagree in their native output must not project to equal + edges unless the disagreement lies in a field marked not comparable. +- **Fences.** Native outputs stay separate (D-ORACLE F9: non-parity does not mean + one is wrong). Oracle code runs outside the workspace; only inputs we generated + and recorded answers are committed. No blended score. +- **Needs.** D-CGW-1 (the contract amendment), D-ORACLE runners pinned. + +### D-CGW-14 — OpenPSI motivation as a qualia device (#1571) + +- **Shape.** OpenPSI is another *device* in the ICC sense of + `contract::qualia_profile` (#1571, D-QPROF-0), beside the engine telemetry and + the Ekman observer: its modulators (activation/arousal, resolution level, + selection threshold, securing rate) get a device → PCS profile, measured, not + declared. Arousal and resolution meet the PCS axes `arousal`, + `resolution_hunger` and `clarity` by name; every other mapping is fitted from + probe pairs, like the A2B CLUT the module says it does not ship yet. +- **What does not move.** The edge carries no qualia (D-CGW-1: the edge is the + fixed point). OpenPSI *demands* are goals, not felt states: they are never + revised by counting and stay with the Rubikon planning/evaluation path + (revision invariants G1–G7); only the modulators enter the qualia profile. + D-ORACLE row D already marks the OpenPSI ↔ Rubikon correspondence as a + hypothesis. +- **Tests first.** Drive one OpenPSI scenario as an oracle (outside the + workspace), record its modulator trace, fit the shared-axis transform, and + check it with a held-out trace; a guard that maps every trace to the same + palette index fails. +- **Source.** Written from the published PSI/MicroPsi model and OpenPSI's + observed behaviour, never from its source. + ## 3. Order -D-CGW-1 → D-CGW-9 (same binding) → D-CGW-2 → D-CGW-3 → D-CGW-5 → D-CGW-4 → +D-CGW-1 → D-CGW-9 (same contract amendment) → D-CGW-2 → D-CGW-3 → D-CGW-5 → D-CGW-4 → D-CGW-6 → D-CGW-7 / D-CGW-8 (layout, need the envelope audit) → D-CGW-10 / 11. -D-CGW-12 waits for the operator. +D-CGW-12 waits for the operator. D-CGW-13 follows D-CGW-1; D-CGW-14 follows +D-CGW-13's OpenPSI runner. ## 4. Not in scope