diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index ad722234e..57f55dc83 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,35 @@ +## 2026-07-21 — E-LADDER-UNSHADOWS-SELECTOR-1 — the measured 8/34 selector-reachability gap (`E-RECIPE-SELECTOR-REACHABILITY-1`, `66f5a73`) closes by MODE COMPOSITION, not selector retuning: `select_tactic` = the foveated SACCADE (one tactic per surprise cycle), `recipe_dispatch::ladder` = the systematic SWEEP (all 34 in rung order, each gated only by its own NaN checklist) — two dispatch modes, one catalogue + +**Status:** FINDING (this session; reconciles PR #780's `recipe_dispatch` with the canonical `docs/NARS_RECIPES_DISPATCH.md` shipped in `66f5a73`). **Confidence:** High — both sides are shipped + measured code; the reconciliation cross-ref is in `recipe_dispatch.rs`'s module doc. + +**The two modes.** `materialize::select_tactic` (canonical doc, measured): only **8/34 reachable** (#1,3,4,5,6,10,17,28), all 14 Infrastructure recipes never win, ICR #31 permanently shadowed by RCR #4 on the lowest-id tie, dissonance tie-weight inert. `recipe_dispatch::ladder` (PR #780): ALL 34 walked in rung-ascending order (Tier base + inference escalation delta), each recipe gated only by its own `requires()` NaN checklist. Under the ladder: 34/34 reachable, CrossTier/Infrastructure fire EARLY (rung base 1 — infrastructure runs everywhere, which is what Infrastructure means), ICR #31 fires unshadowed at the deepest rung (counterfactual +5). **The selector's three measured gaps are exactly what the sweep covers — and the sweep's cost (34 firings) is exactly what the saccade avoids.** Foveated saccade for the hot path, systematic sweep for the deliberate pass: Kahneman System-1/System-2 as two dispatch functions over ONE catalogue spine (no recipe duplicated, no new vocabulary). + +**Cross-ref:** `E-RECIPE-SELECTOR-REACHABILITY-1` (the gap measurement), `E-RECIPE-SUBSTRATE-WIRING-1` (below — the ladder + tenants), `E-FOVEATED-AWARENESS-1` (saccade framing), `docs/NARS_RECIPES_DISPATCH.md` (the canonical doc; its "open edges" list now has this ladder as the systematic-mode answer). Peer-session tie-in: the `advance_on_gate` self-reasoning kanban wire (`66f5a73`) fires atoms as KanbanMoves — the ladder supplies the ESCALATION ORDER those moves walk, the A9 loci supply the CONTEXT they read. + +## 2026-07-21 — E-THINKING-SKILLS-ARE-RUNG-4-MACROS-1 — the cc-thinking-skills catalogue (47+ session-level methodologies, `tjboudreaux`/`AdaWorldAPI/cc-thinking-skills`) is a RUNG-4 MACRO catalogue over the rung-3 recipes: ~2/3 map onto existing 34-recipe compositions, and the UNMAPPED third is the expansion surface — new thinking lands as rung-4 StyleFamily macros composing rung-3 atoms, NEVER as new rung-3 entries (the 34 stays operator-locked) + +**Status:** CONJECTURE→mapping (directory-level survey via WebFetch; per-skill SKILL.md contents not yet read). **Confidence:** Medium-high for the mapping direction; the per-skill correspondences below are structural, from the skill names + known frameworks. **Anti-conflation holds:** per `persona-vs-rung-ladder.md`, rung 3 = the 34 recipes (locked), rung 4 = StyleFamily macros + autopoiesis. The skills are METHODOLOGIES (macro-compositions of inference steps) — rung-4 shaped, not rung-3. + +**Mapped pairs (skill ≈ rung-3 recipe/organ it composes):** inversion≈RCR(4 reverse-causality); red-team≈ASC(7); pre-mortem + thought-experiment≈ICR(31 counterfactual); socratic≈MCP(10 metacognition); dual-process≈CDT(13 convergent/divergent) — and the saccade/sweep pair of E-LADDER-UNSHADOWS-SELECTOR-1 IS dual-process as dispatch; model-router + model-selection≈DTMF(33 meta-frame switch); via-negativa≈TCP(5 pruning); steel-manning≈CR(11)+CDI(17 contradiction preserved/induced); scientific-method≈SSR(21)+SDD(32 validate); model-combination≈HKF(34)/ZCF(24 fusion); feedback-loops≈AMP(23 TD-adapt); second-order≈RTE(1 recursive expansion); first-principles≈HTD(2)/ETD(22 decomposition); five-whys-plus≈the KAUSAL witness-edge chain walked recursively; OODA≈the ladder loop itself (observe=project, orient=NaN-gate, decide=dispatch, act=fire); bayesian/probabilistic≈NARS truth revision (the substrate); systems + leverage-points≈the supported_by/supports loci (basin chains); circle-of-competence + bounded-rationality≈MUL DK; map-territory≈**I-VSA-IDENTITIES itself** (pointer vs content — the catalogue independently reproduces the workspace's iron rule); archetypes≈the persona-36 (the SEPARATE off-ladder storyline — the catalogue even reproduces our anti-conflation). + +**The expansion surface (no current rung-3 composition):** fermi-estimation (order-of-magnitude priors), opportunity-cost + regret-minimization (counterfactual VALUE comparison — ICR gives the world, not the pricing), margin-of-safety (threshold slack — Jirak-derived bounds are the natural substrate), lindy-effect (age-weighted truth — a NARS confidence decay/anti-decay prior), cynefin (domain classification BEFORE strategy — a pre-ladder gate choosing saccade vs sweep vs escalate), reversibility (one-way-door detection — pairs with the kanban advance gate), effectuation, jobs-to-be-done, kepner-tregoe, TRIZ. Each would land as a **rung-4 macro** (a StyleFamily composing rung-3 atoms + MUL + witness loci) — the catalogue is a GAP DETECTOR for macros, never a source of new rung-3 entries. + +**Cross-ref:** `persona-vs-rung-ladder.md` (the rung-content ladder + anti-conflation), `E-LADDER-UNSHADOWS-SELECTOR-1` (dual-process as dispatch), `style_family` (the rung-4 macro home), `I-VSA-IDENTITIES` (map-territory), MUL (`circle-of-competence`). Next probe when wanted: read the per-skill SKILL.md files and score the mapping table the way `loci_recipe_relevance` scored loci×recipes. + +## 2026-07-21 — E-RECIPE-SUBSTRATE-WIRING-1 — the 34 NARS recipes MEASURE THEIR CLAIMS but on a scalar PROXY, not the real organ (28/34 sound on the proxy; the weakness is SUBSTRATE-WIRING, not recipe composition) — so the three real tenants are wired in: A9 CausalWitnessFacet (24 loci = third #729 register reading) + SPO + qualia → a rung-ordered, NaN-gated CAUSAL LADDER keyed by inference type + +**Status:** FINDING + WIRING (this session; operator-steered live). **Confidence:** High for the audit (it exercises the REAL shipped `recipe_kernels::kernel(id)`, not a reimpl); the projection metrics (pair_similarity) are a documented zero-dep stand-in for the ndarray 256×256 CAM-PQ table (follow-up). Deliverables: `causal_witness.rs`, `recipe_substrate.rs`, `recipe_dispatch.rs` (3 new contract modules, +18 tests → 970 green, clippy clean) + 3 probes (`recipe_claim_audit`, `loci_recipe_relevance`, `recipe_ladder_over_substrate`, all gates green). Gates registered before each run. + +**The root-cause audit (`recipe_claim_audit`, real kernels on positive vs control ctx).** Two measured axes. Axis A (recipe-side realization): **12 MEASURES + 16 REALIZES = 28/34 sound** on the proxy; **2 INERT** (CAS #8, ETD #22 — compute-then-DISCARD bugs: `_level`/clone never written back) + **4 CONSTANT** (ARE/ZCF/ICR/HKF — exact algebraic identities but INPUT-INDEPENDENT, hardcoded `u32`). Axis B (substrate grounding): **34/34 kernels consume ONLY the 8-field `ThoughtField` scalar-proxy basis — 0 read a real organ**, exactly as the module doc concedes ("richer substrate slots in behind the same trait later"). **Verdict to the operator's question:** the recipe *compositions* are sound; the dominant weakness is SUBSTRATE-side — the recipes run on hand-set scalars, never the SPO 2³ / CAM-PQ 4096² / temporal.rs Markov / VSA / deepnsm organs they name. The gap is WIRING. + +**The wiring (closes the gap).** (1) **`causal_witness::CausalWitnessFacet`** — the A9 "24 edges": a reading of the 12-byte content-blind register as 24 signed `i4` loci (`G24N4`), each a context POINTER (`∈[−8,+7]`, `0`=unbound) into the ±8 Markov window. It is the **THIRD ClassView reading** of the SAME register #729 shipped two of (FROZEN `12×u8`, Orchestration `6×(8:8)`) — a reading, no stored bytes, envelope-safe. 16 named loci (§2.9) + 8 reserved-empty (never padded). (2) **`recipe_substrate::SubstrateView`** projects SPO + witness + qualia into the recipe input basis. (3) **`recipe_dispatch`** orders the 34 into a rung-gated causal ladder keyed by NARS `InferenceType`. + +**Operator design-locks (each enforced by a green gate).** • **Qualia is ADDITIVE and STAKES-only** — it adds to the affective `temperature` (arousal/tension/groundedness), never multiplied/gating, and NEVER touches a logical marker: swapping the qualia register moves ONLY temperature, confidence/surprise/dissonance/candidates are byte-identical ("you do not describe an epiphany's logical quality with a qualia about how it feels"; qualia "might describe stakes"). • **Causality is NOT a qualia** — the cause is the witness `Locus::Kausal` edge (logical, A9). • **Logic ← SPO + 24 witness edges; stakes ← qualia.** • **Thinking is an act of cause-and-effect** — the dispatch is a causal chain (fire→next state→next fire); the **Versuchsleitereffekt IS the Heisenberg observer effect** (measuring disturbs → each step logs its triggering cause); dispatch is a **deterministic Schrödinger collapse** (superposed awareness, recoverable by role key, no randomness); a **NaN disqualifier is an unmeasurable conjugate** (a required input the tenants can't ground → recipe skipped, `E-RELIABILITY-IS-CHECKLIST-COVERAGE` as a runtime gate); the `N≤√d/4≈32` bundle bound is the substrate's uncertainty relation, and the 24-locus witness sits safely under it. + +**External validation anchors (operator-provided, NOT yet wired — out of current repo scope):** `AdaWorldAPI/NARS-Swift` (a real NARS implementation — the inference-rule oracle to validate the deduction/induction/abduction/revision/counterfactual classification against) and `InternScience`/`AdaWorldAPI/MME-Reasoning` (arXiv 2505.21327 — a multimodal-reasoning benchmark, inductive/deductive/abductive taxonomy — the eventual empirical test bed for the 34-recipe ladder). Flagged for a future `add_repo` + a benchmark probe; the inference-type map in `recipe_dispatch::inference` is the hypothesis those repos would falsify. **Theory anchor:** **PCRLLM** (arXiv 2511.08392, Tangrui Li, **Pei Wang** [NARS], Hahm et al., Temple — "Proof-Carrying Reasoning with LLMs under Stepwise Logical Constraints") is the formal frame the ladder instantiates: reasoning constrained to **single-step inferences** with explicit **premises + rule + conclusion**, verifiable against a target logic, composable across experts. A `RecipeStep` = one such proof-carrying step (its `inference` = the rule, its `trigger` = the grounded premises, the `nan_disqualifier` = the verification gate that refuses an unsupported step). **Causal-formalism anchor:** **Causal ML survey** (arXiv 2206.15475, Kaddour/Lynch/Liu/Kusner/Silva — "Causal Machine Learning: A Survey and Open Problems") formalizes the structural-causal-model ladder the witness edges instantiate: SCM structure = the `Locus::Kausal` edge, **intervention** = Pearl rung-2, **counterfactual** = rung-3 = `RecipeInference::Counterfactual` (ICR #31, SPO=0b111 apex, causal-edge `−6` mantissa). "Thinking finds cause and effect" = the survey's interventional/counterfactual reasoning read off the witness loci. + +**Cross-ref:** #729 "one register, N readings" (`E-H268-REPLAYABLE-TILE-1`; A9 = the third), M20 plan §2.9 (the L9 schema this ships as a type), `E-L9-REAL-TEXT-1` (the loci validated on real text), `E-AWARENESS-TENANTS-EVOLVE-NOT-COLLAPSE-1` (the tenants this reads), `contract::nars::InferenceType` (the `to_mantissa` bridge to CausalEdge64), `E-CE64-MB-4` (ractor ownership — the write-back the ladder feeds), `I-VSA-IDENTITIES` (Test 1 = the Heisenberg bound; loci are identity pointers not content). Honest bound: `pair_similarity` is a zero-dep stand-in for the ndarray CAM-PQ table; INERT/CONSTANT tiers are input-choice-sensitive but the tiers themselves (compute-then-discard, input-independent) are structural. + ## 2026-07-21 — E-PARTOF-ISA-vs-PALETTE256-1 — the two sanctioned `6×(8:8)` readings of the SAME 12 bytes are NEAR-ORTHOGONAL on real substrate (cross-reading ρ 0.035, ICC 0.006): `part_of:is_a` holds exact TAXONOMY (+0.346 same-kind separation), `palette256²` holds frequency-centroid SIMILARITY (+0.001, taxonomy-blind) — the catalogue's L1-vs-L4 split is load-bearing, not cosmetic **Status:** FINDING — real substrate: the fable content nouns (merged `l9_loci_real_text` corpus) with real COCA-5k ranks (`palette256²` = `(rank>>8, rank&0xFF)`, freq-is-cosine distance) vs objectively-true English taxonomy (`part_of:is_a` = is_a-graph path length, the copula facts `deepnsm::spo_markov_kg` extracts via `"is"→"is_a"`). **Confidence:** High for the orthogonality direction; small vocabulary (11/16 nouns in COCA-5k) + one hand-verified real taxonomy bound the magnitudes. Deliverable: `crates/jc/examples/partof_isa_vs_palette256.rs`. Gates registered before the first run. diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index ed9e43699..bb385a0a9 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,10 @@ +## 2026-07-21 — branch `claude/x265-x266-plans-review-h9osnl` — recipes wired to real tenants: `causal_witness` (A9 24 loci) + `recipe_substrate` + `recipe_dispatch` (additive, zero-dep, nothing existing touched) + +### Current Contract Inventory — new entry +- `lance_graph_contract::causal_witness::{CausalWitnessFacet, Locus, WITNESS_LOCI(24), NAMED_LOCI(16), LOCUS_LABELS, WITNESS_REGISTER_BYTES(12)}` — the **A9 CausalWitnessFacet**: the L9 `TekamoloWindowBinding` reading of the 12-byte content-blind register as **24 signed `i4` loci** (`G24N4`), each a context POINTER (`∈[−8,+7]`, `0`=unbound, sign=orientation) into the ±8 `temporal.rs` Markov window. The **THIRD ClassView reading** of the same register #729 shipped two of (FROZEN `12×u8` + Orchestration `6×(8:8)`) — a reading, NOT a layout (no stored bytes, mirrors `awareness_facet::SpoFacet`). 16 named loci (§2.9) + 8 reserved-empty (RESERVE-DON'T-RECLAIM). `get`/`set`/`with`/`at`/`is_bound`/`bound_count`/`resolves_to`/`agrees_at`/`agreement_count`/`quorum`/`contradiction`/`cause`/`antecedent`. 8 tests + doctest. Zero deps. +- `lance_graph_contract::recipe_substrate::{SubstrateView, affective_temperature, pair_similarity}` — wires the 3 real tenants (SPO `SpoFacet` + witness `CausalWitnessFacet` + qualia `QualiaI4_16D`) into the `recipe_kernels::ThoughtCtx` input via `project()`. **Qualia is ADDITIVE + STAKES-only** (temperature only, never a logical marker); **logic + causality ← SPO + 24 witness edges** (`logical_confidence`/`logical_surprise`/`logical_dissonance`/`logical_candidates`/`logical_rung`/`logical_beliefs`). A marker a MISSING tenant can't ground → **NaN/empty** (the NaN-disqualifier signal). 4 tests. +- `lance_graph_contract::recipe_dispatch::{RecipeInference, inference, rung, dispatch_order, nan_disqualifier, RecipeStep, ladder}` — the 34 recipes as a **rung-ordered, NaN-gated causal ladder** keyed by NARS inference (deduction/induction=fanout/abduction/revision/counterfactual; bridged to `nars::InferenceType` by `to_mantissa` value). `rung` = Tier base + inference escalation delta; `nan_disqualifier` = the runtime `requires()`-coverage gate (a NaN required input disqualifies); `ladder` records each step's triggering cause (Versuchsleitereffekt audit). 6 tests. Deliverable D-REC-WIRE-1; finding E-RECIPE-SUBSTRATE-WIRING-1. + ## 2026-07-20 — branch `claude/ogar-docir-architecture-jjzlig` — GraphQL ergonomics as mask algebra: `selection` + `standing_mask` land in the contract (additive, zero-dep, nothing existing touched) ### Current Contract Inventory — new entry diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 00be3082e..a57701359 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -4,6 +4,7 @@ Plan: `.claude/plans/causal-rung-standing-wave-v1.md`. Consumes M20 A1 (shipped) | D-id | Title | Repo | Status | Evidence | |---|---|---|---|---| +| D-REC-WIRE-1 | Recipe claim-audit (34 kernels measure on a scalar proxy, not the real organ) → wire the 3 real tenants (A9 CausalWitnessFacet 24 loci + SPO + qualia) into a rung-ordered, NaN-gated causal ladder keyed by NARS inference type | lance-graph | **In PR** (branch, this session) — 3 contract modules + 3 probes, 970 tests green, clippy clean, all example gates green | `causal_witness.rs`/`recipe_substrate.rs`/`recipe_dispatch.rs`; probes `recipe_claim_audit`/`loci_recipe_relevance`/`recipe_ladder_over_substrate`; E-RECIPE-SUBSTRATE-WIRING-1 | | D-CSW-0 | Plan doc + O1 decision (canonical ladder masks + per-class facet election) | lance-graph | In PR #777; **O1 DECIDED (operator 2026-07-21: canonical masks)** | plan §2 | | D-CSW-1 | Standing-wave probe: per-rung persistence over an ordered stream separates causal from coincidental vs single-cycle + p64 3×u8 baseline; escalation cascade prunes at zero separation cost | lance-graph | **v5 SPLIT VERDICT** — CORE standing-wave claim GREEN (auc_wave .997 vs single .878, vs p64-**wave** .500 [M3], vs **reverse .000** [M2 orientation control, +.997]; cascade .997); the SEPARATE §0.5 escalation-ECONOMICS gate 3a **KILLS** (pruned .333 < .40 registered — reported not retuned; the M1 witness correction dropped it from v4's .458) → DEFERRED. v1 fixture-ceiling + v2 mean−std KILLs recorded on the way; leg 2 (real temporal.rs/Lance versions, wild corpora) OPEN. _(v3 historical: .972/.875/.375/.458 — pre M1/M2/M3 fixes.)_ | probe `deepnsm/examples/probe_dcsw1_standing_wave.rs` (v5); E-DCSW1-V5-SPLIT-VERDICT / E-CAUSAL-TISSUE-ALREADY-SHIPS-1 / E-DCSW1-LEG1-GREEN; plan §6.5 | | D-CSW-2 | Basin→causal-edge candidate probe: co-occupancy + rung survival vs basin-only / rung-only ablations | lance-graph | Queued (gated on D-CSW-1) | plan §5a/§6 | diff --git a/crates/lance-graph-contract/examples/loci_recipe_relevance.rs b/crates/lance-graph-contract/examples/loci_recipe_relevance.rs new file mode 100644 index 000000000..6a2261fbf --- /dev/null +++ b/crates/lance-graph-contract/examples/loci_recipe_relevance.rs @@ -0,0 +1,507 @@ +//! `loci_recipe_relevance` — the 24 L9 loci × 34 NARS recipes relevance matrix. +//! +//! The L9 `TekamoloWindowBinding` facet (plan §2.9) is 24 signed nibbles, each a +//! **locus**: a signed context pointer that places ONE awareness dimension's +//! filler in the ±8 `temporal.rs` Markov window. 16 of the 24 are the +//! operator-named dimensions (grounded in shipped organs); 8 stay reserved-empty +//! (RESERVE-DON'T-RECLAIM — never padded to hit a count). +//! +//! This example answers the operator's question — *"the 24 pointers seem to have +//! a very high relative relevance"* — by **deriving**, per (locus, recipe) cell, +//! how relevant that awareness pointer is to that reasoning runbook. The score is +//! a DOCUMENTED STRUCTURAL HEURISTIC over the REAL [`RECIPES`] metadata (the +//! `substrate` / `name` strings + `mechanism` / `spo2cubed` / `bucket` / `tier` +//! fields) — a design-time relevance map, NOT a runtime measurement. Every rule +//! is spelled out in [`keyword_hits`] and [`field_bonus`] so the derivation is +//! auditable, not a guess. +//! +//! Row-sums rank the 24 loci by their spread across the 34 runbooks (which +//! pointers earn their nibble); column-sums rank the 34 runbooks by how much +//! window-awareness they consume. Both feed the rung-escalation order: a runbook +//! that lights many loci is one the escalation ladder must hand a rich A9 view. +//! +//! ```sh +//! cargo run -p lance-graph-contract --example loci_recipe_relevance +//! ``` + +use lance_graph_contract::recipes::{Bucket, Coverage, Mechanism, Recipe, Tier, RECIPES}; + +/// One L9 locus dimension — a named awareness pointer the A9 register can bind. +struct Locus { + /// Slot index 0..24 in the `TekamoloWindowBinding` register. + slot: usize, + /// Operator-named dimension (plan §2.9), or `""` for a reserved-empty slot. + name: &'static str, + /// Which shipped organ grounds this pointer (§2.9 "Grounded in" column). + grounded_in: &'static str, + /// Lowercase substrings that, when present in a recipe's `substrate`/`name`, + /// signal this locus is load-bearing for that runbook. Documented rule set. + keywords: &'static [&'static str], +} + +/// The 24-slot L9 catalogue: 16 named (plan §2.9 table) + 8 reserved-empty. +/// Reserved slots carry NO keywords and score 0 everywhere — they are held open +/// (RESERVE-DON'T-RECLAIM), never padded with a construct to reach 24. +const LOCI: [Locus; 24] = [ + Locus { + slot: 0, + name: "TEMPORAL", + grounded_in: "role_keys::TEMPORAL_KEY", + keywords: &["temporal", "markov", "granger", "time", "±5", "cascade"], + }, + Locus { + slot: 1, + name: "KAUSAL", + grounded_in: "role_keys::KAUSAL_KEY", + keywords: &[ + "causal", + "cause", + "abduction", + "reverse", + "counterfactual", + "granger", + "intervention", + "reason", + ], + }, + Locus { + slot: 2, + name: "MODAL", + grounded_in: "role_keys::MODAL_KEY", + keywords: &[ + "uncertain", + "confidence", + "calibration", + "brier", + "possibility", + "meta-cognition", + "skeptic", + ], + }, + Locus { + slot: 3, + name: "LOKAL", + grounded_in: "role_keys::LOKAL_KEY", + keywords: &[ + "context", + "window", + "bindspace", + "episodic", + "latent space", + "cluster", + "scaffold", + ], + }, + Locus { + slot: 4, + name: "S-meaning", + grounded_in: "SPO plane (A1)", + keywords: &["spo", "subject", "agent", "s_o", "0b111", "triple"], + }, + Locus { + slot: 5, + name: "P-meaning", + grounded_in: "SPO plane (A1)", + keywords: &["spo", "predicate", "action", "relation", "rel", "bind"], + }, + Locus { + slot: 6, + name: "O-meaning", + grounded_in: "SPO plane (A1)", + keywords: &["spo", "object", "patient", "s_o", "0b111"], + }, + Locus { + slot: 7, + name: "antecedent", + grounded_in: "MODIFIER/CONTEXT keys", + keywords: &[ + "reframe", + "analog", + "mapping", + "reference", + "intent", + "roleplay", + ], + }, + Locus { + slot: 8, + name: "basin-anchor", + grounded_in: "part_of:is_a (L1)", + keywords: &[ + "clam", + "cluster", + "decompos", + "hierarch", + "abstraction", + "knowledge", + "is_a", + ], + }, + Locus { + slot: 9, + name: "supported-by", + grounded_in: "hi_chain", + keywords: &[ + "cascade", + "hierarch", + "decompos", + "evidence", + "scaffold", + "abstraction", + "coarse", + ], + }, + Locus { + slot: 10, + name: "supports", + grounded_in: "lo_chain", + keywords: &[ + "fusion", + "compose", + "synthesis", + "aggregate", + "bundle", + "expansion", + "fuse", + ], + }, + Locus { + slot: 11, + name: "runbook-evidence", + grounded_in: "RECIPES[34] / A8", + keywords: &[ + "template", "prompt", "scaffold", "style", "td-learn", "q-value", "slots", + ], + }, + Locus { + slot: 12, + name: "qualia-reference", + grounded_in: "QualiaColumn / i4-qualia", + keywords: &[ + "staunen", + "qualia", + "temperature", + "perturb", + "noise", + "wisdom", + "texture", + ], + }, + Locus { + slot: 13, + name: "meaning-level", + grounded_in: "rung-content ladder 0–4", + keywords: &[ + "rung", + "depth", + "abstraction", + "expand", + "compress", + "scaling", + "level", + "meta", + ], + }, + Locus { + slot: 14, + name: "quorum", + grounded_in: "NARS freq·conf (A3)", + keywords: &[ + "agreement", + "vote", + "consensus", + "majority", + "debate", + "council", + "raid", + "ecc", + "independent", + "corrob", + ], + }, + Locus { + slot: 15, + name: "contradiction", + grounded_in: "Staunen×Wisdom depth", + keywords: &[ + "contradiction", + "dissonance", + "opposing", + "adversar", + "critique", + "skeptic", + "distortion", + "reciprocal", + "challenge", + "negation", + ], + }, + // ── reserved-empty (RESERVE-DON'T-RECLAIM; never padded) ── + Locus { + slot: 16, + name: "", + grounded_in: "reserved", + keywords: &[], + }, + Locus { + slot: 17, + name: "", + grounded_in: "reserved", + keywords: &[], + }, + Locus { + slot: 18, + name: "", + grounded_in: "reserved", + keywords: &[], + }, + Locus { + slot: 19, + name: "", + grounded_in: "reserved", + keywords: &[], + }, + Locus { + slot: 20, + name: "", + grounded_in: "reserved", + keywords: &[], + }, + Locus { + slot: 21, + name: "", + grounded_in: "reserved", + keywords: &[], + }, + Locus { + slot: 22, + name: "", + grounded_in: "reserved", + keywords: &[], + }, + Locus { + slot: 23, + name: "", + grounded_in: "reserved", + keywords: &[], + }, +]; + +/// Documented rule #1 — keyword hits. Count DISTINCT locus keywords present in +/// the recipe's `substrate` + `name` (lowercased). Capped at 2 so no single +/// verbose substrate string dominates. +fn keyword_hits(locus: &Locus, r: &Recipe) -> u8 { + if locus.keywords.is_empty() { + return 0; + } + let hay = format!("{} {}", r.substrate.to_lowercase(), r.name.to_lowercase()); + let hits = locus.keywords.iter().filter(|k| hay.contains(**k)).count(); + hits.min(2) as u8 +} + +/// Documented rule #2 — structural field bonus (+1 each, from the recipe's typed +/// metadata, NOT string matching). Encodes the substrate invariants: SPO-2³ +/// coverage makes the causality + S/P/O loci load-bearing; TruthAwareInference +/// makes the quorum/contradiction peers load-bearing; a Control bucket is where +/// the escalation-facing loci (meaning-level, runbook-evidence) matter. +fn field_bonus(locus: &Locus, r: &Recipe) -> u8 { + let mut b = 0u8; + match locus.name { + "KAUSAL" | "S-meaning" | "P-meaning" | "O-meaning" => { + b += match r.spo2cubed { + Coverage::Covered => 2, + Coverage::Partial => 1, + Coverage::NotCovered => 0, + }; + } + "quorum" => { + b += u8::from(matches!( + r.mechanism, + Mechanism::ParallelIndependence | Mechanism::TruthAwareInference + )); + } + "contradiction" => { + b += u8::from(matches!( + r.mechanism, + Mechanism::TruthAwareInference | Mechanism::StructuralDivergence + )); + } + "meaning-level" | "runbook-evidence" => { + b += u8::from(matches!(r.bucket, Bucket::Control)); + } + "qualia-reference" => { + // Staunen (surprise/temperature) rides the Gate bucket + the hardest tiers. + b += + u8::from(matches!(r.bucket, Bucket::Gate) || matches!(r.tier, Tier::ExtremelyHard)); + } + _ => {} + } + b +} + +/// Cell relevance 0..=3 = clamp(keyword_hits + field_bonus). +fn cell(locus: &Locus, r: &Recipe) -> u8 { + (keyword_hits(locus, r) + field_bonus(locus, r)).min(3) +} + +fn main() { + // slot integrity: every LOCI entry's declared slot equals its array index. + for (i, l) in LOCI.iter().enumerate() { + assert_eq!( + l.slot, i, + "locus '{}' slot must equal its register index", + l.name + ); + } + + // ── the 24 × 34 matrix ── + let mut matrix = [[0u8; 34]; 24]; + for (li, locus) in LOCI.iter().enumerate() { + for (ri, r) in RECIPES.iter().enumerate() { + matrix[li][ri] = cell(locus, r); + } + } + + // ── header ── + println!("L9 loci × 34 NARS recipes — structural relevance matrix"); + println!( + "(cell 0..3 = documented keyword hits + typed field bonus over REAL RECIPES metadata)" + ); + println!(); + print!("{:<18}", "locus \\ recipe"); + for r in RECIPES.iter() { + print!("{:>4}", r.code); + } + println!("{:>7}", "ΣROW"); + println!("{}", "─".repeat(18 + 34 * 4 + 7)); + + // ── rows + row-sums ── + let mut row_sum = [0u32; 24]; + for (li, locus) in LOCI.iter().enumerate() { + let disp = if locus.name.is_empty() { + "· reserved ·" + } else { + locus.name + }; + print!("{:<18}", disp); + for &v in &matrix[li] { + row_sum[li] += v as u32; + print!( + "{:>4}", + if v == 0 { + ".".to_string() + } else { + v.to_string() + } + ); + } + println!("{:>7}", row_sum[li]); + } + + // ── column-sums ── + println!("{}", "─".repeat(18 + 34 * 4 + 7)); + print!("{:<18}", "ΣCOL"); + let mut col_sum = [0u32; 34]; + for (ri, cs) in col_sum.iter_mut().enumerate() { + for row in &matrix { + *cs += row[ri] as u32; + } + print!("{:>4}", *cs); + } + let grand: u32 = row_sum.iter().sum(); + println!("{:>7}", grand); + + // ── locus ranking by row-sum (the "which pointers are high relative relevance") ── + println!(); + println!("── Loci ranked by relative relevance (row-sum across all 34 runbooks) ──"); + let mut ranked: Vec<(usize, u32)> = (0..24).map(|i| (i, row_sum[i])).collect(); + ranked.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0))); + for (rank, (li, s)) in ranked.iter().enumerate() { + if *s == 0 { + continue; // reserved-empty loci omitted from the ranking + } + let l = &LOCI[*li]; + println!( + " {:>2}. {:<17} Σ={:>3} ← {}", + rank + 1, + l.name, + s, + l.grounded_in + ); + } + + // ── recipe ranking by column-sum (which runbooks consume the most window-awareness) ── + println!(); + println!("── Runbooks ranked by window-awareness consumed (col-sum over 24 loci) ──"); + let mut rcol: Vec<(usize, u32)> = (0..34).map(|i| (i, col_sum[i])).collect(); + rcol.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0))); + for (rank, (ri, s)) in rcol.iter().take(10).enumerate() { + let r = &RECIPES[*ri]; + println!(" {:>2}. {:<5} Σ={:>3} {}", rank + 1, r.code, s, r.name); + } + + // ═══ registered gates ═══ + println!(); + println!("── gates ──"); + let mut all_green = true; + + // Gate 1: every one of the 16 NAMED loci is load-bearing for ≥1 runbook + // (no dead pointer — each named dimension earns its nibble). + let named_all_live = (0..16).all(|i| row_sum[i] > 0); + println!( + "[{}] G1 every named locus is relevant to ≥1 runbook (no dead pointer)", + if named_all_live { "PASS" } else { "FAIL" } + ); + all_green &= named_all_live; + + // Gate 2: every RESERVED locus scores exactly 0 (held empty, never padded). + let reserved_empty = (16..24).all(|i| row_sum[i] == 0); + println!( + "[{}] G2 all 8 reserved loci score 0 (RESERVE-DON'T-RECLAIM, not padded)", + if reserved_empty { "PASS" } else { "FAIL" } + ); + all_green &= reserved_empty; + + // Gate 3: the causality-trajectory minimum features (KAUSAL + the 3 SPO + // meaning loci) rank in the top half of the 16 named loci — the + // TEKAMOLO causality core must be high-relevance, not incidental. + let named_sorted: Vec = { + let mut v: Vec<(usize, u32)> = (0..16).map(|i| (i, row_sum[i])).collect(); + v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0))); + v.into_iter().map(|(i, _)| i).collect() + }; + let top_half: std::collections::HashSet = named_sorted.iter().take(8).copied().collect(); + // slots: 1=KAUSAL, 4=S, 5=P, 6=O + let core = [1usize, 4, 5, 6]; + let core_in_top = core.iter().filter(|s| top_half.contains(s)).count(); + let core_ok = core_in_top >= 3; + println!( + "[{}] G3 causality core (KAUSAL+S/P/O) mostly top-half: {}/4 in top-8", + if core_ok { "PASS" } else { "FAIL" }, + core_in_top + ); + all_green &= core_ok; + + // Gate 4: the matrix is not degenerate — mean named-cell relevance in a sane + // band (neither all-zero nor saturated). Sanity, not a target. + let named_cells: u32 = (0..16).map(|i| row_sum[i]).sum(); + let mean = named_cells as f64 / (16.0 * 34.0); + let band_ok = (0.10..=1.50).contains(&mean); + println!( + "[{}] G4 mean named-cell relevance in band [0.10,1.50]: {:.3}", + if band_ok { "PASS" } else { "FAIL" }, + mean + ); + all_green &= band_ok; + + println!(); + println!( + "{}", + if all_green { + "ALL GATES GREEN" + } else { + "GATE FAILURE" + } + ); + assert!(all_green, "loci×recipe relevance gates failed"); +} diff --git a/crates/lance-graph-contract/examples/recipe_claim_audit.rs b/crates/lance-graph-contract/examples/recipe_claim_audit.rs new file mode 100644 index 000000000..e349e8613 --- /dev/null +++ b/crates/lance-graph-contract/examples/recipe_claim_audit.rs @@ -0,0 +1,764 @@ +//! `recipe_claim_audit` — do the 34 NARS recipes MEASURE WHAT THEY CLAIM, and is +//! each weakness in the **recipe** (the composition) or in the **substrate** (the +//! organ it runs on)? Operator's question, answered with two MEASURED axes over +//! the REAL shipped kernels (`recipe_kernels::kernel(id)`) — never a reimpl. +//! +//! ## Axis A — recipe-side realization (measured by exercising the real kernel) +//! +//! Each recipe's `substrate` string makes a claim (detect X / transform Y / +//! invert Z). This probe feeds the ACTUAL kernel a **positive** and a **control** +//! [`ThoughtCtx`] and asks whether its observable output (Outcome branch, sign of +//! `delta_conf`, or the ctx field it writes) is a FUNCTION OF THE INPUT that +//! reflects the claim. Four tiers: +//! * `MEASURES` — a detector claim whose output flips between positive & control. +//! * `REALIZES` — a transform claim whose ctx change matches the claimed op. +//! * `INERT` — computes then DISCARDS: no observable effect (recipe bug). +//! * `CONSTANT` — an exact algebraic identity, but INPUT-INDEPENDENT (measures +//! an algebra, not the thought context). +//! +//! `INERT` and `CONSTANT` are the recipe-side weaknesses; the first is a real bug, +//! the second is correct-but-untethered. +//! +//! ## Axis B — substrate-side grounding (measured, structural) +//! +//! Every kernel's declared input checklist (`requires()`) draws ONLY from the +//! 8-field `ThoughtField` scalar basis (sd / free_energy / dissonance / +//! temperature / confidence / rung / candidates / beliefs). **No `ThoughtField` +//! names a real organ** — not the SPO 2³ plane, not the CAM-PQ 4096² table, not +//! the `temporal.rs` Markov stream, not a VSA 16k fingerprint. The organs the +//! `substrate` STRINGS name ship elsewhere in the workspace (verified paths in +//! [`ORGAN_SHIPS`]) but are not in the kernel's input basis. So every recipe +//! measures its claim against a lightweight **proxy**, never the real substrate — +//! exactly as the module doc concedes ("richer substrate … slots in behind the +//! same trait later"). +//! +//! ## The verdict the operator asked for +//! +//! Where a recipe cannot yet measure its claim on real data, the fault is almost +//! always SUBSTRATE-side (organ shipped-but-unwired), not RECIPE-side: the +//! compositions are sound on the proxy; the gap is the wiring. The narrow +//! recipe-side weaknesses (INERT + CONSTANT) are named explicitly. +//! +//! ```sh +//! cargo run -p lance-graph-contract --example recipe_claim_audit +//! ``` + +use lance_graph_contract::recipe_kernels::{kernel, ThoughtCtx}; +use lance_graph_contract::recipes::RECIPES; + +/// Axis-A recipe-side realization tier. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Realize { + /// Detector: output flips between positive and control input. + Measures, + /// Transform: ctx change faithfully matches the claimed operation. + Realizes, + /// Compute-then-discard: no observable effect (recipe-side bug). + Inert, + /// Exact algebraic identity, but input-independent (measures an algebra). + Constant, +} + +impl Realize { + fn tag(self) -> &'static str { + match self { + Realize::Measures => "MEASURES", + Realize::Realizes => "REALIZES", + Realize::Inert => "INERT", + Realize::Constant => "CONSTANT", + } + } +} + +/// Run the REAL shipped kernel on a ctx, returning (fired, note, delta_conf, post-ctx). +fn run(id: u8, mut c: ThoughtCtx) -> (bool, &'static str, f32, ThoughtCtx) { + let k = kernel(id).expect("kernel id present"); + let before = c.confidence; + let out = k.run(&mut c); + // delta relative to pre-run confidence (run() clamps ctx.confidence in place). + (out.fired, out.note, c.confidence - before, c) +} + +/// A neutral ctx with the given candidate scores and a non-FLOW gate (so Gate-bucket +/// recipes can fire when we want them to). +fn ctx(cands: &[f32]) -> ThoughtCtx { + let mut c = ThoughtCtx::new(cands.to_vec()); + c.sd = 0.25; // HOLD — not FLOW, so Gate recipes are eligible + c +} + +/// Axis A: exercise the real kernel and classify its realization tier. +/// Every arm drives `kernel(id)` (the shipped code), never a reimplementation. +fn axis_a(id: u8) -> (Realize, String) { + match id { + // ── Detectors: output must FLIP between positive & control ── + 3 => { + // SMAD: tight spread → converged(+); wide spread → split(−). + let (_, np, dp, _) = run(3, ctx(&[0.50, 0.52, 0.49, 0.51])); + let (_, nc, dc, _) = run(3, ctx(&[0.95, 0.10, 0.80, 0.05])); + flip("converged↔split", np, dp, nc, dc) + } + 7 => { + // ASC: confidence>0.6 survives(+); else weakens(−). + let mut p = ctx(&[0.5]); + p.confidence = 0.9; + let (_, np, dp, _) = run(7, p); + let mut q = ctx(&[0.5]); + q.confidence = 0.3; + let (_, nc, dc, _) = run(7, q); + flip("survive↔fail", np, dp, nc, dc) + } + 10 => { + // MCP: confident+high-FE (miscalibrated) → down; else flat. + let mut p = ctx(&[0.5]); + p.confidence = 0.9; + p.free_energy = 0.8; + let (_, np, dp, _) = run(10, p); + let mut q = ctx(&[0.5]); + q.confidence = 0.5; + q.free_energy = 0.2; + let (_, nc, dc, _) = run(10, q); + flip("miscalib↔ok", np, dp, nc, dc) + } + 11 => { + // CR: same-topic opposing beliefs → detected(−); else coherent. + let mut p = ctx(&[0.5]); + p.beliefs = vec![(7, 0.9, 0.8), (7, 0.1, 0.7)]; + let (_, np, dp, _) = run(11, p); + let mut q = ctx(&[0.5]); + q.beliefs = vec![(7, 0.9, 0.8), (9, 0.85, 0.7)]; + let (_, nc, dc, _) = run(11, q); + flip("contradiction↔coherent", np, dp, nc, dc) + } + 13 => { + // CDT: hot→divergent(spread); cold→convergent(collapse to 1). + let mut p = ctx(&[0.2, 0.4, 0.6, 0.8]); + p.temperature = 0.9; + let (_, np, _, cp) = run(13, p); + let mut q = ctx(&[0.2, 0.4, 0.6, 0.8]); + q.temperature = 0.1; + let (_, nc, _, cq) = run(13, q); + let discriminates = np != nc && cp.candidates.len() != cq.candidates.len(); + ( + if discriminates { + Realize::Measures + } else { + Realize::Inert + }, + format!( + "hot→{} / cold→{} (len {}↔{})", + np, + nc, + cp.candidates.len(), + cq.candidates.len() + ), + ) + } + 23 => { + // AMP: high FE → rung+1; else no rung change. + let mut p = ctx(&[0.5]); + p.free_energy = 0.8; + let (_, _, _, cp) = run(23, p); + let mut q = ctx(&[0.5]); + q.free_energy = 0.2; + let (_, _, _, cq) = run(23, q); + let discriminates = cp.rung > cq.rung; + ( + if discriminates { + Realize::Measures + } else { + Realize::Inert + }, + format!("rung {}↔{} on free_energy", cp.rung, cq.rung), + ) + } + 28 => { + // SSAM: low sd (close cluster) → +analogy; high sd → −. + let mut p = ctx(&[0.5]); + p.sd = 0.1; + let (_, _, dp, _) = run(28, p); + let mut q = ctx(&[0.5]); + q.sd = 0.9; + let (_, _, dc, _) = run(28, q); + let discriminates = dp.signum() != dc.signum() || (dp - dc).abs() > 1e-4; + ( + if discriminates { + Realize::Measures + } else { + Realize::Inert + }, + format!("Δconf {:+.3}↔{:+.3} on cluster sim", dp, dc), + ) + } + 30 => { + // SPP: shadow paths agree(+) vs diverge(−). Control must be ASYMMETRIC + // (a symmetric spread collapses path_b=midrange to the mean → false agree). + let (_, np, dp, _) = run(30, ctx(&[0.50, 0.50, 0.50])); + let (_, nc, dc, _) = run(30, ctx(&[0.90, 0.80, 0.10])); + flip("agree↔diverge", np, dp, nc, dc) + } + 32 => { + // SDD: mean far from 0.5 → distortion flagged; near → within floor. + let (_, np, _, _) = run(32, ctx(&[0.95, 0.90, 0.99])); + let (_, nc, _, _) = run(32, ctx(&[0.50, 0.50, 0.50])); + ( + if np != nc { + Realize::Measures + } else { + Realize::Inert + }, + format!("{} ↔ {}", np, nc), + ) + } + 33 => { + // DTMF: BLOCK frame → switched (temp+0.3); else held. + let mut p = ctx(&[0.5]); + p.sd = 0.5; // BLOCK + let (_, np, _, cp) = run(33, p); + let mut q = ctx(&[0.5]); + q.sd = 0.2; // HOLD + let (_, nc, _, cq) = run(33, q); + let discriminates = np != nc && (cp.temperature - cq.temperature).abs() > 1e-4; + ( + if discriminates { + Realize::Measures + } else { + Realize::Inert + }, + format!("{} ↔ {}", np, nc), + ) + } + 1 => { + // RTE: deepen rung while free_energy > noise floor. + let mut p = ctx(&[0.5]); + p.free_energy = 0.9; + let (_, _, _, cp) = run(1, p); + let mut q = ctx(&[0.5]); + q.free_energy = 0.005; + let (_, _, _, cq) = run(1, q); + let discriminates = cp.rung > cq.rung; + ( + if discriminates { + Realize::Measures + } else { + Realize::Inert + }, + format!("rung {}↔{} on surprise", cp.rung, cq.rung), + ) + } + 21 => { + // SSR: challenge intensity = (confidence − free_energy)⁺ → −delta. + let mut p = ctx(&[0.5]); + p.confidence = 0.9; + p.free_energy = 0.1; + let (_, _, dp, _) = run(21, p); + let mut q = ctx(&[0.5]); + q.confidence = 0.2; + q.free_energy = 0.9; + let (_, _, dc, _) = run(21, q); + let discriminates = (dp - dc).abs() > 1e-4; + ( + if discriminates { + Realize::Measures + } else { + Realize::Inert + }, + format!("Δconf {:+.3}↔{:+.3} on (conf−fe)", dp, dc), + ) + } + + // ── Transforms: ctx change must faithfully match the claimed op ── + 2 => { + // HTD: bipolar split — candidates become hi(≥mean)++lo(() / src.len() as f32; + let (_, _, _, c) = run(2, ctx(&src)); + let split = c + .candidates + .iter() + .position(|&v| v < m) + .unwrap_or(c.candidates.len()); + let ok = c.candidates[..split].iter().all(|&v| v >= m) + && c.candidates[split..].iter().all(|&v| v < m); + realize(ok, "bipolar split hi++lo") + } + 4 => { + // RCR: reverse the chain (effect→cause). + let src = [0.1, 0.2, 0.3, 0.4]; + let (_, _, _, c) = run(4, ctx(&src)); + let mut want = src.to_vec(); + want.reverse(); + realize(c.candidates == want, "chain reversed") + } + 5 => { + // TCP: prune below an SD-derived floor → fewer candidates. + let (_, _, _, c) = run(5, ctx(&[0.9, 0.85, 0.05, 0.02])); + realize(c.candidates.len() < 4, "low branches pruned") + } + 6 => { + // TR: temperature-scaled perturbation changes candidates. + let src = [0.5, 0.5, 0.5, 0.5]; + let mut p = ctx(&src); + p.temperature = 0.9; + let (_, _, _, c) = run(6, p); + realize(c.candidates != src.to_vec(), "candidates perturbed") + } + 9 => { + // IRS: persona modulation scales candidates. + let src = [0.4, 0.6]; + let mut p = ctx(&src); + p.temperature = 0.9; + let (_, _, _, c) = run(9, p); + realize(c.candidates != src.to_vec(), "candidates modulated") + } + 12 => { + // TCA: Granger lag = rotate_right(1). + let src = [0.1, 0.2, 0.3, 0.4]; + let (_, _, _, c) = run(12, ctx(&src)); + let mut want = src.to_vec(); + want.rotate_right(1); + realize(c.candidates == want, "temporal lag applied") + } + 14 => { + // MCT: unify modalities → [mean]. + let src = [0.2, 0.4, 0.6]; + let m = src.iter().sum::() / src.len() as f32; + let (_, _, _, c) = run(14, ctx(&src)); + realize( + c.candidates.len() == 1 && (c.candidates[0] - m).abs() < 1e-6, + "unified to mean", + ) + } + 15 => { + // LSI: introspect distribution → writes ctx.sd = std. + let src = [0.2, 0.8]; + let m = 0.5f32; + let want = (((0.2 - m).powi(2) + (0.8 - m).powi(2)) / 2.0f32).sqrt(); + let (_, _, _, c) = run(15, ctx(&src)); + realize((c.sd - want).abs() < 1e-6, "sd = cluster std") + } + 16 => { + // PSO: scaffold = sort descending. + let (_, _, _, c) = run(16, ctx(&[0.3, 0.9, 0.1, 0.6])); + let sorted = c.candidates.windows(2).all(|w| w[0] >= w[1]); + realize(sorted, "ordered descending") + } + 17 => { + // CDI: inject counter-belief + raise dissonance. + let mut p = ctx(&[0.5]); + p.beliefs = vec![(7, 0.9, 0.8)]; + let before = p.dissonance; + let (_, _, _, c) = run(17, p); + realize( + c.beliefs.len() == 2 && c.dissonance > before, + "dissonance induced", + ) + } + 18 => { + // CWS: checkpoint best into persistent belief set. + let (_, _, _, c) = run(18, ctx(&[0.3, 0.9, 0.1])); + realize(c.beliefs.len() == 1, "state checkpointed") + } + 20 => { + // TCF: filter N strategies to their median. + let (_, _, _, c) = run(20, ctx(&[0.1, 0.5, 0.9])); + realize(c.candidates == vec![0.5], "filtered to agreement") + } + 25 => { + // HPM: match nearest to the 0.5 query target. + let (_, _, _, c) = run(25, ctx(&[0.1, 0.55, 0.95])); + realize(c.candidates == vec![0.55], "nearest pattern matched") + } + 26 => { + // CUR: coarse→fine reduce (candidates shrink). + let (_, _, _, c) = run(26, ctx(&[0.9, 0.8, 0.2, 0.1])); + realize(c.candidates.len() < 4, "uncertainty reduced") + } + 27 => { + // MPC: compress perspectives to consensus (mean). + let src = [0.2, 0.8]; + let (_, _, _, c) = run(27, ctx(&src)); + realize(c.candidates == vec![0.5], "compressed to consensus") + } + 29 => { + // IDR: reframe to dominant intent (max). + let (_, _, _, c) = run(29, ctx(&[0.3, 0.9, 0.1])); + realize(c.candidates == vec![0.9], "reframed to dominant") + } + + // ── Compute-then-discard: recipe-side INERT bugs ── + 8 => { + // CAS: computes `_level` from rung, then DROPS it — no ctx change, fixed note. + let src = ctx(&[0.4, 0.6]); + let before = src.clone(); + let (_, _, dconf, c) = run(8, src); + let unchanged = + c.candidates == before.candidates && c.rung == before.rung && dconf == 0.0; + ( + if unchanged { + Realize::Inert + } else { + Realize::Realizes + }, + "abstraction level computed then discarded — no observable effect".to_string(), + ) + } + 22 => { + // ETD: sorts a CLONE `v`, never writes back — ctx unchanged, fixed note. + let src = ctx(&[0.3, 0.9, 0.1]); + let before = src.clone(); + let (_, _, dconf, c) = run(22, src); + let unchanged = c.candidates == before.candidates && dconf == 0.0; + ( + if unchanged { + Realize::Inert + } else { + Realize::Realizes + }, + "cluster split computed on a clone, never applied — no observable effect" + .to_string(), + ) + } + + // ── Exact algebraic identities: correct but INPUT-INDEPENDENT ── + 19 | 24 | 31 | 34 => { + // ARE/ZCF/ICR/HKF: hardcoded u32 constants; output identical for any ctx. + let (_, n1, d1, _) = run(id, ctx(&[0.1])); + let (_, n2, d2, _) = run(id, ctx(&[0.9, 0.9, 0.9])); + let input_independent = n1 == n2 && (d1 - d2).abs() < 1e-9; + ( + if input_independent { + Realize::Constant + } else { + Realize::Realizes + }, + "exact algebraic inverse holds, but ignores the thought context (constant)" + .to_string(), + ) + } + + _ => (Realize::Inert, "unhandled id".to_string()), + } +} + +/// Detector helper: PASS(Measures) iff (note OR sign of Δconf) differs pos↔ctrl. +fn flip(label: &str, np: &str, dp: f32, nc: &str, dc: f32) -> (Realize, String) { + let discriminates = np != nc || dp.signum() != dc.signum(); + ( + if discriminates { + Realize::Measures + } else { + Realize::Inert + }, + format!("{label}: '{np}'({dp:+.2}) ↔ '{nc}'({dc:+.2})"), + ) +} + +/// Transform helper: PASS(Realizes) iff the claimed post-condition holds. +fn realize(ok: bool, what: &str) -> (Realize, String) { + ( + if ok { + Realize::Realizes + } else { + Realize::Inert + }, + format!("{what}: {}", if ok { "held" } else { "FAILED" }), + ) +} + +/// Axis B — the real organ each recipe's `substrate` string names, and whether it +/// ships anywhere in the workspace (path verified by grep this session). The kernel +/// itself consumes NONE of these — it reads the ThoughtCtx scalar proxy. `""` path +/// = the named organ is a proxy scalar with no distinct real organ. +const ORGAN_SHIPS: [(u8, &str, &str); 34] = [ + ( + 1, + "rung depth ladder", + "recipe_kernels ThoughtCtx.rung (proxy scalar)", + ), + ( + 2, + "CLAM bipolar split", + "graph/neighborhood CLAM (ndarray) — unwired", + ), + ( + 3, + "a2a_blackboard InnerCouncil", + "planner/mul/escalation.rs — unwired", + ), + ( + 4, + "SPO 2³ backward S_O", + "graph/spo + deepnsm SPO — unwired", + ), + ( + 5, + "CollapseGate SD BLOCK", + "recipe_kernels SD_FLOW/SD_BLOCK (proxy scalar)", + ), + ( + 6, + "temperature (Staunen)", + "ThoughtCtx.temperature (proxy scalar)", + ), + ( + 7, + "InnerCouncil negation projection", + "planner/mul/escalation.rs — unwired", + ), + ( + 8, + "HDR cascade INT1/4/8/32", + "ndarray HDR cascade — unwired", + ), + ( + 9, + "persona FieldModulation", + "thinking-engine persona — unwired", + ), + (10, "MUL DK + Brier", "planner/mul (DkPosition) — unwired"), + ( + 11, + "NARS opposing-truth", + "contract::nars NarsTruth — unwired (belief tuples proxy)", + ), + ( + 12, + "Markov ±5 / Granger", + "temporal.rs Markov stream — unwired (rotate proxy)", + ), + ( + 13, + "explore↔exploit temperature", + "ThoughtCtx.temperature (proxy scalar)", + ), + ( + 14, + "GrammarTriangle → 1 fingerprint", + "deepnsm GrammarTriangle — unwired (mean proxy)", + ), + ( + 15, + "CRP / Mexican-hat clusters", + "planner CRP — unwired (μ/σ proxy)", + ), + ( + 16, + "ThinkingTemplate slots", + "contract::recipes template mention — unwired", + ), + ( + 17, + "Festinger dissonance (NARS)", + "contract::nars — unwired (belief push proxy)", + ), + ( + 18, + "persistent BindSpace / episodic", + "BindSpace SoA — unwired (Vec push proxy)", + ), + ( + 19, + "ABBA unbind A⊗B⊗B=A", + "bgz17/holograph VSA bind — hardcoded u32 demo", + ), + ( + 20, + "N strategies + agreement", + "planner strategies — unwired (median proxy)", + ), + ( + 21, + "MUL uncertainty schedule", + "planner/mul — unwired (scalar proxy)", + ), + ( + 22, + "CLAM cluster geometry", + "graph/neighborhood CLAM — unwired", + ), + ( + 23, + "TD-learning Q-values (W32-39)", + "thinking-engine TD — unwired", + ), + ( + 24, + "VSA bind(A,B)", + "bgz17/holograph VSA bind — hardcoded u32 demo", + ), + ( + 25, + "fingerprint cosine/Hamming sweep", + "ndarray SIMD sweep — unwired (scalar proxy)", + ), + ( + 26, + "FreeEnergy / CRP percentiles", + "planner free-energy — unwired (scalar proxy)", + ), + ( + 27, + "bundle majority-per-bit", + "bgz17/holograph bundle — unwired (mean proxy)", + ), + ( + 28, + "NARS analogy + similarity", + "contract::nars — unwired (sd proxy)", + ), + ( + 29, + "GrammarTriangle CausalityFlow", + "deepnsm GrammarTriangle — unwired (max proxy)", + ), + ( + 30, + "independent paths + agreement", + "planner parallel — unwired (2-path proxy)", + ), + ( + 31, + "world⊗fact⊗cf XOR; CausalEdge64 −6", + "causal-edge CausalEdge64 — hardcoded u32 demo", + ), + ( + 32, + "Berry-Esseen noise floor", + "recipe_kernels NOISE_FLOOR (proxy scalar)", + ), + ( + 33, + "CollapseGate BLOCK template switch", + "recipe_kernels gate_state (proxy scalar)", + ), + ( + 34, + "cross-domain bind(A,rel,B)", + "bgz17/holograph VSA bind — hardcoded u32 demo", + ), +]; + +fn main() { + println!("recipe_claim_audit — do the 34 recipes measure what they claim?"); + println!( + "(Axis A: real kernel exercised on positive vs control ctx; Axis B: substrate grounding)\n" + ); + + println!(" id code AxisA detail | Axis B: named organ → ships?"); + println!("{}", "─".repeat(100)); + + let mut n_measures = 0; + let mut n_realizes = 0; + let mut n_inert = 0; + let mut n_constant = 0; + + for r in RECIPES.iter() { + let (tier, detail) = axis_a(r.id); + match tier { + Realize::Measures => n_measures += 1, + Realize::Realizes => n_realizes += 1, + Realize::Inert => n_inert += 1, + Realize::Constant => n_constant += 1, + } + let organ = ORGAN_SHIPS + .iter() + .find(|(id, _, _)| *id == r.id) + .map(|(_, o, s)| format!("{o} → {s}")) + .unwrap_or_default(); + println!("{:>3} {:<6} {:<9} {}", r.id, r.code, tier.tag(), detail); + println!(" {:<16} └ {organ}", " "); + } + + // ── aggregate ── + let recipe_weak = n_inert + n_constant; + let proxy_only = 34; // structural: every kernel's requires() is in the 8-scalar basis + println!("\n── aggregate ──"); + println!( + " Axis A: {} MEASURES + {} REALIZES = {} sound on the proxy; {} recipe-WEAK ({} INERT + {} CONSTANT)", + n_measures, + n_realizes, + n_measures + n_realizes, + recipe_weak, + n_inert, + n_constant + ); + println!( + " Axis B: {}/34 kernels consume ONLY the ThoughtField scalar proxy basis — 0 consume a real organ", + proxy_only + ); + + println!("\n── the operator's question: substrate or recipe? ──"); + println!(" • RECIPE-side weakness is NARROW and named:"); + println!(" INERT (compute-then-discard bug): CAS(8), ETD(22)"); + println!(" CONSTANT (input-independent algebra): ARE(19), ZCF(24), ICR(31), HKF(34)"); + println!(" • The DOMINANT weakness is SUBSTRATE-side: all 34 measure their claim against a"); + println!(" lightweight scalar proxy, never the real organ named (SPO 2³ / CAM-PQ 4096² /"); + println!( + " temporal.rs Markov / VSA 16k / deepnsm GrammarTriangle). The organs SHIP but are" + ); + println!(" NOT in the kernel input basis — the gap is WIRING, not composition."); + + // ═══ registered gates ═══ + println!("\n── gates ──"); + let mut green = true; + + // G1: the recipe layer is mostly compositionally sound on the proxy (≥28/34). + let sound = n_measures + n_realizes; + let g1 = sound >= 28; + println!( + "[{}] G1 ≥28/34 recipes sound on the proxy (measure or realize): {}", + pf(g1), + sound + ); + green &= g1; + + // G2: the recipe-side weaknesses are EXACTLY the 6 named ones (2 INERT + 4 CONSTANT). + let g2 = n_inert == 2 && n_constant == 4; + println!( + "[{}] G2 recipe-weak set is exactly {{CAS,ETD}}+{{ARE,ZCF,ICR,HKF}}: INERT={}, CONSTANT={}", + pf(g2), + n_inert, + n_constant + ); + green &= g2; + + // G3: the four detectors we can hand-verify actually FLIP (real discrimination, + // not theater) — SMAD, CR, MCP, DTMF. + let discriminators = [3u8, 11, 10, 33]; + let g3 = discriminators + .iter() + .all(|&id| axis_a(id).0 == Realize::Measures); + println!( + "[{}] G3 key detectors {{SMAD,CR,MCP,DTMF}} all discriminate positive↔control", + pf(g3) + ); + green &= g3; + + // G4: substrate axis is uniform — every recipe has an organ mapping (no gap in the map). + let g4 = ORGAN_SHIPS.len() == 34 + && (1..=34u8).all(|id| ORGAN_SHIPS.iter().any(|(i, _, _)| *i == id)); + println!( + "[{}] G4 every recipe mapped to its named organ (substrate axis complete)", + pf(g4) + ); + green &= g4; + + println!( + "\n{}", + if green { + "ALL GATES GREEN" + } else { + "GATE FAILURE" + } + ); + assert!(green, "recipe claim-audit gates failed"); +} + +fn pf(b: bool) -> &'static str { + if b { + "PASS" + } else { + "FAIL" + } +} diff --git a/crates/lance-graph-contract/examples/recipe_ladder_over_substrate.rs b/crates/lance-graph-contract/examples/recipe_ladder_over_substrate.rs new file mode 100644 index 000000000..150386340 --- /dev/null +++ b/crates/lance-graph-contract/examples/recipe_ladder_over_substrate.rs @@ -0,0 +1,198 @@ +//! `recipe_ladder_over_substrate` — the whole wiring, end to end: the three real +//! tenants → projected [`ThoughtCtx`] → the rung-ordered, NaN-gated **causal +//! ladder** of the 34 recipes. This is the answer to the audit's root cause +//! (recipes ran on a scalar proxy): now they run on **SPO + 24 witness edges + +//! qualia**. +//! +//! Demonstrates, on REAL tenant registers (not hand-set scalars): +//! 1. the escalation ladder (shallow deduction → deep counterfactual), keyed by +//! inference type, with each recipe's rung + NaN-disqualifier state; +//! 2. **qualia is additive & stakes-only** — swap the qualia register and ONLY +//! the affective temperature moves; the dispatch (the logic) is identical; +//! 3. **causality is a witness edge** — the KAUSAL locus places the cause; +//! 4. **NaN-disqualification** (the unmeasurable conjugate) — drop a tenant and +//! the recipes that required it are skipped, not read off noise. +//! +//! ```sh +//! cargo run -p lance-graph-contract --example recipe_ladder_over_substrate +//! ``` + +use lance_graph_contract::awareness_facet::SpoFacet; +use lance_graph_contract::causal_witness::{CausalWitnessFacet, Locus}; +use lance_graph_contract::qualia::{QualiaI4_16D, QUALIA_I4_LABELS}; +use lance_graph_contract::recipe_dispatch::{inference, ladder, RecipeStep}; +use lance_graph_contract::recipe_substrate::{affective_temperature, SubstrateView}; + +fn q(label: &str) -> usize { + QUALIA_I4_LABELS.iter().position(|&l| l == label).unwrap() +} + +/// A grounded row: a real SPO triple, a witness with the KAUSAL cause 3 events +/// back, an agreeing quorum peer and a preserved contradiction, and hot qualia. +fn grounded_row() -> SubstrateView { + let spo = SpoFacet::from_register([12, 40, 7, 88, 30, 5, 12, 41, 7, 90, 31, 6]); + let witness = CausalWitnessFacet::ZERO + .with(Locus::Kausal, -3) // the cause sits 3 events back + .with(Locus::Antecedent, -1) // coreference antecedent 1 back + .with(Locus::SMeaning, 2) + .with(Locus::Quorum, 4) // an agreeing peer 4 ahead + .with(Locus::Contradiction, -5) // a preserved dissenter 5 back + .with(Locus::MeaningLevel, 3); // rung-3 content (the 34-runbook tier) + let qualia = QualiaI4_16D::ZERO + .with(q("arousal"), 6) + .with(q("tension"), 4); + SubstrateView::new(spo, witness, qualia) +} + +fn print_ladder(title: &str, steps: &[RecipeStep]) { + println!("── {title} ──"); + let mut cur = 0u8; + for s in steps { + if s.rung != cur { + cur = s.rung; + println!(" rung {cur}:"); + } + let status = match s.disqualified_by { + None => "fire".to_string(), + Some(f) => format!("SKIP (NaN: {f:?})"), + }; + println!( + " #{:<2} {:<15} trig={} {}", + s.id, + inference(s.id).tag(), + s.trigger, + status + ); + } +} + +fn main() { + let row = grounded_row(); + let ctx = row.project(); + + println!( + "recipe_ladder_over_substrate — 34 recipes over real SPO + 24 witness edges + qualia\n" + ); + println!("projected ThoughtCtx from tenants:"); + println!( + " confidence {:.3} (SPO+quorum/contradiction edges) free_energy {:.3} (witness) dissonance {:.3}", + ctx.confidence, ctx.free_energy, ctx.dissonance + ); + println!( + " temperature {:.3} (qualia: additive stakes) rung {} (meaning-level edge) candidates {} (bound edges)", + ctx.temperature, + ctx.rung, + ctx.candidates.len() + ); + println!( + " cause (KAUSAL edge) = {} events; antecedent = {}; agreement w/ self = {}", + row.witness.cause(), + row.witness.antecedent(), + row.witness.agreement_count(row.witness) + ); + println!(); + + let steps = ladder(&ctx); + print_ladder("causal ladder (grounded — everything fires)", &steps); + + // ── 2. qualia is additive & stakes-only: swap qualia, logic unchanged ── + let cool = SubstrateView::new( + row.spo, + row.witness, + QualiaI4_16D::ZERO.with(q("groundedness"), 6), + ); + let cool_ctx = cool.project(); + println!("\n── qualia swap (hot → cool): ONLY temperature moves ──"); + println!( + " temperature {:.3} → {:.3} | confidence {:.3} → {:.3} free_energy {:.3} → {:.3}", + ctx.temperature, + cool_ctx.temperature, + ctx.confidence, + cool_ctx.confidence, + ctx.free_energy, + cool_ctx.free_energy + ); + + // ── 4. NaN-disqualification: drop the SPO + witness tenants ── + let bare = SubstrateView::new(SpoFacet::default(), CausalWitnessFacet::ZERO, row.qualia); + let bare_ctx = bare.project(); + let bare_steps = ladder(&bare_ctx); + let skipped = bare_steps + .iter() + .filter(|s| s.disqualified_by.is_some()) + .count(); + println!("\n── tenants dropped (SPO+witness absent): the unmeasurable conjugates ──"); + println!( + " confidence NaN={} free_energy NaN={} candidates empty={} → {} of 34 recipes disqualified", + bare_ctx.confidence.is_nan(), + bare_ctx.free_energy.is_nan(), + bare_ctx.candidates.is_empty(), + skipped + ); + + // ═══ gates ═══ + println!("\n── gates ──"); + let mut green = true; + + // G1: grounded row fires every recipe (all checklists covered). + let g1 = steps.iter().all(|s| s.disqualified_by.is_none()); + println!( + "[{}] G1 grounded row fires all 34 (full checklist coverage)", + pf(g1) + ); + green &= g1; + + // G2: qualia is additive & stakes-only — temperature moves, logic frozen. + let g2 = (ctx.temperature - cool_ctx.temperature).abs() > 1e-6 + && ctx.confidence == cool_ctx.confidence + && ctx.free_energy.to_bits() == cool_ctx.free_energy.to_bits() + && ctx.candidates == cool_ctx.candidates; + println!( + "[{}] G2 qualia swap moves ONLY temperature (additive, stakes-only)", + pf(g2) + ); + green &= g2; + + // G3: the ladder is rung-ordered (the causal chain escalates shallow→deep). + let g3 = steps.windows(2).all(|w| w[0].rung <= w[1].rung); + println!( + "[{}] G3 dispatch is rung-ordered (causal chain shallow→deep)", + pf(g3) + ); + green &= g3; + + // G4: dropping the tenants disqualifies real recipes (NaN conjugate skipped). + let g4 = skipped > 0 && bare_ctx.confidence.is_nan() && bare_ctx.free_energy.is_nan(); + println!( + "[{}] G4 absent tenants ⇒ NaN inputs ⇒ recipes disqualified ({} skipped)", + pf(g4), + skipped + ); + green &= g4; + + // G5: causality is on the witness edge (not qualia) and reads back. + let g5 = row.witness.cause() == -3 && affective_temperature(row.qualia) > 0.5; + println!( + "[{}] G5 causality = KAUSAL witness edge; qualia = additive stakes", + pf(g5) + ); + green &= g5; + + println!( + "\n{}", + if green { + "ALL GATES GREEN" + } else { + "GATE FAILURE" + } + ); + assert!(green, "ladder-over-substrate gates failed"); +} + +fn pf(b: bool) -> &'static str { + if b { + "PASS" + } else { + "FAIL" + } +} diff --git a/crates/lance-graph-contract/src/causal_witness.rs b/crates/lance-graph-contract/src/causal_witness.rs new file mode 100644 index 000000000..545739bba --- /dev/null +++ b/crates/lance-graph-contract/src/causal_witness.rs @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +//! `causal_witness` — the **CausalWitnessFacet** (A9): the L9 +//! `TekamoloWindowBinding` reading of a `12`-byte content-blind register as **24 +//! signed `i4` loci** (`.claude/plans/soa-32-tenant-awareness-redundancy-v1.md` +//! §2.9, le-contract §3 **L9** `G24N4`). +//! +//! This is a **reading, not a layout.** It re-labels the same 12 bytes a value +//! lane already holds — nothing here reserves, moves, or stores a byte, exactly +//! like [`awareness_facet::SpoFacet`](crate::awareness_facet::SpoFacet) re-labels +//! the register as `6×(8:8)`. The A9 reading carves those 12 bytes as `24×4-bit` +//! instead: **24 signed nibbles**, each a **locus** — a signed offset +//! `∈ [−8, +7]` naming WHERE in the `±8` `temporal.rs` Markov window that +//! awareness dimension's filler sits. +//! +//! # One register, THREE readings (PR #729 doctrine, extended) +//! +//! PR #729 shipped "one register, two readings" of the same content-blind +//! 12 bytes — the FROZEN `12×u8` palette-226 codebook and the Orchestration +//! `6×(8:8)` `style_rails_at` (V3-replayable, `E-H268-REPLAYABLE-TILE-1`). A9 is +//! the **third** ClassView-selected interpretation of the identical storage: +//! `24×4-bit` (`G24N4`). WHICH reading a class uses is an OGAR mint (the per-row/ +//! class "Place 2" decision, #729), never a property of the bytes and never a +//! slot (le-contract §2 slot purity). +//! +//! # Loci, not magnitudes (operator-locked) +//! +//! Every nibble is a **context pointer**, never a strength. `0` = **unbound** +//! (zero-fallback — the dimension is not currently placed in the window). The +//! sign is **orientation**: `−` = before / antecedent-cause, `+` = after / +//! consequent. The filler's meaning is **read at the offset** (the node at +//! `self_pos + offset` in the stream), never stored here — `I-VSA-IDENTITIES` +//! clean: identity pointers, not content. +//! +//! # The 16 named dimensions + 8 reserved +//! +//! Slots `0..16` are the operator-named [`Locus`] dimensions (§2.9), grounded in +//! shipped organs. Slots `16..24` stay **reserved-empty** (RESERVE-DON'T-RECLAIM +//! — held open, never padded with a construct to reach 24). The rung level +//! occupies ZERO slots: escalation is carried by the elected ClassView, which +//! re-interprets which loci are live, never by a stored magnitude. +//! +//! # Window agreement = loci comparison +//! +//! Two co-window rows agree about a higher-order dimension when their loci +//! **converge on the same context event** — [`Self::agrees_at`] compares two +//! nibbles (bound + equal offset after normalization), never re-derives meaning. +//! [`Self::quorum`] / [`Self::contradiction`] read the agreeing / preserved- +//! dissenting peer loci directly. + +/// The content-blind register width (le-contract §3): 12 bytes = 24 nibbles. +/// Same 12-byte lane [`awareness_facet::SpoFacet`](crate::awareness_facet::SpoFacet) +/// reads as `6×(8:8)`; A9 reads it as `24×4-bit` (`G24N4`). +pub const WITNESS_REGISTER_BYTES: usize = 12; + +/// Total loci in the A9 register (24 signed nibbles = 12 bytes). +pub const WITNESS_LOCI: usize = 24; +/// The operator-named loci (§2.9); the remaining `24 − 16 = 8` are reserved. +pub const NAMED_LOCI: usize = 16; + +/// The 16 named locus dimensions (plan §2.9), in canonical slot order. Reserved +/// slots `16..24` have no name and are always read `0` unless explicitly set. +pub const LOCUS_LABELS: [&str; WITNESS_LOCI] = [ + "temporal", + "kausal", + "modal", + "lokal", + "s_meaning", + "p_meaning", + "o_meaning", + "antecedent", + "basin_anchor", + "supported_by", + "supports", + "runbook_evidence", + "qualia_reference", + "meaning_level", + "quorum", + "contradiction", + "", + "", + "", + "", + "", + "", + "", + "", // 8 reserved-empty +]; + +/// A named A9 locus dimension — a coordinate into the 24-slot register. `as usize` +/// IS the slot index (`0..16`); the 8 reserved slots have no `Locus` variant. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Locus { + /// My time reference (`role_keys::TEMPORAL_KEY`). + Temporal = 0, + /// My cause — causality learning's stored answer (`role_keys::KAUSAL_KEY`). + Kausal = 1, + /// The manner/possibility context (`role_keys::MODAL_KEY`). + Modal = 2, + /// The where/context (`role_keys::LOKAL_KEY`). + Lokal = 3, + /// What S means here — the subject meaning-grounding event (SPO plane A1). + SMeaning = 4, + /// What P means here — the predicate meaning-grounding event (SPO plane A1). + PMeaning = 5, + /// What O means here — the object meaning-grounding event (SPO plane A1). + OMeaning = 6, + /// relativPronomen → its antecedent (MODIFIER/CONTEXT keys). + Antecedent = 7, + /// The event binding me to my AriGraph basin (`part_of:is_a`, L1). + BasinAnchor = 8, + /// The nested supporting basin's evidence ↓ (`hi_chain`). + SupportedBy = 9, + /// What I support ↑ (`lo_chain`). + Supports = 10, + /// Where my current runbook drew its finding (`RECIPES[34]` / A8). + RunbookEvidence = 11, + /// The event that set my current texture (QualiaColumn / i4-qualia). + QualiaReference = 12, + /// The context defining my current level of meaning (rung-content ladder 0–4). + MeaningLevel = 13, + /// The **agreeing peer** — the window event whose reading matches mine + /// (NARS freq·conf, A3). + Quorum = 14, + /// The **disagreeing peer** — the committed contradiction PRESERVED beside + /// the quorum (Staunen×Wisdom depth). + Contradiction = 15, +} + +impl Locus { + /// All 16 named loci in slot order. + pub const ALL: [Locus; NAMED_LOCI] = [ + Locus::Temporal, + Locus::Kausal, + Locus::Modal, + Locus::Lokal, + Locus::SMeaning, + Locus::PMeaning, + Locus::OMeaning, + Locus::Antecedent, + Locus::BasinAnchor, + Locus::SupportedBy, + Locus::Supports, + Locus::RunbookEvidence, + Locus::QualiaReference, + Locus::MeaningLevel, + Locus::Quorum, + Locus::Contradiction, + ]; + + /// The dimension's canonical name. + #[inline] + #[must_use] + pub const fn label(self) -> &'static str { + LOCUS_LABELS[self as usize] + } +} + +/// The A9 reading: a `12`-byte register carved as 24 signed `i4` loci. +/// +/// Stores the raw register (like [`QualiaI4_16D`](crate::qualia::QualiaI4_16D) +/// stores its packed `u64`); each nibble is sign-extended on read, so the +/// register round-trips loss-free by construction. +/// +/// ``` +/// use lance_graph_contract::causal_witness::{CausalWitnessFacet, Locus}; +/// // A row whose KAUSAL cause sits 3 events back and whose antecedent is 1 back. +/// let w = CausalWitnessFacet::ZERO +/// .with(Locus::Kausal, -3) +/// .with(Locus::Antecedent, -1); +/// assert_eq!(w.at(Locus::Kausal), -3); +/// assert_eq!(w.at(Locus::Antecedent), -1); +/// assert_eq!(w.at(Locus::Temporal), 0); // unbound (zero-fallback) +/// assert_eq!(w.to_register(), CausalWitnessFacet::from_register(w.to_register()).to_register()); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct CausalWitnessFacet { + reg: [u8; WITNESS_REGISTER_BYTES], +} + +impl CausalWitnessFacet { + /// The all-unbound register (every locus `0`). + pub const ZERO: Self = Self { + reg: [0u8; WITNESS_REGISTER_BYTES], + }; + + /// Read a raw 12-byte content-blind register as an A9 facet (identity — the + /// signed carve happens on [`Self::get`]). + #[inline] + #[must_use] + pub const fn from_register(reg: [u8; WITNESS_REGISTER_BYTES]) -> Self { + Self { reg } + } + + /// The 12-byte register — the inverse of [`from_register`](Self::from_register). + #[inline] + #[must_use] + pub const fn to_register(self) -> [u8; WITNESS_REGISTER_BYTES] { + self.reg + } + + /// The signed offset `∈ [−8, +7]` at slot `0..24` (sign-extended nibble). + /// Out-of-range slot returns `0` (defensive; the 16-named accessors are preferred). + #[inline] + #[must_use] + pub const fn get(self, slot: usize) -> i8 { + if slot >= WITNESS_LOCI { + return 0; + } + let byte = self.reg[slot / 2]; + let nibble = if slot & 1 == 0 { + byte & 0x0F + } else { + (byte >> 4) & 0x0F + }; + // sign-extend 4 → 8 bits (mirrors qualia::QualiaI4_16D::get) + ((nibble << 4) as i8) >> 4 + } + + /// Set the signed offset at slot `0..24`. Clamps to `[−8, +7]`; out-of-range + /// slot is a no-op. + #[inline] + pub fn set(&mut self, slot: usize, offset: i8) { + if slot >= WITNESS_LOCI { + return; + } + let v = (offset.clamp(-8, 7) as u8) & 0x0F; + let bi = slot / 2; + if slot & 1 == 0 { + self.reg[bi] = (self.reg[bi] & 0xF0) | v; + } else { + self.reg[bi] = (self.reg[bi] & 0x0F) | (v << 4); + } + } + + /// Builder-shape [`set`](Self::set) on a named [`Locus`]. + #[inline] + #[must_use] + pub fn with(mut self, locus: Locus, offset: i8) -> Self { + self.set(locus as usize, offset); + self + } + + /// The signed offset at a named [`Locus`]. + #[inline] + #[must_use] + pub const fn at(self, locus: Locus) -> i8 { + self.get(locus as usize) + } + + /// Is this locus **bound** (nonzero offset) rather than unbound (zero-fallback)? + #[inline] + #[must_use] + pub const fn is_bound(self, locus: Locus) -> bool { + self.at(locus) != 0 + } + + /// How many of the 16 named loci are bound. + #[inline] + #[must_use] + pub fn bound_count(self) -> usize { + Locus::ALL.iter().filter(|&&l| self.is_bound(l)).count() + } + + /// Resolve a locus to a **stream position** relative to `self_pos`: the node + /// at `self_pos + offset`, or `None` if unbound or the resolved index falls + /// outside `0..stream_len`. + #[inline] + #[must_use] + pub fn resolves_to(self, locus: Locus, self_pos: usize, stream_len: usize) -> Option { + let off = self.at(locus); + if off == 0 { + return None; + } + let idx = self_pos as isize + off as isize; + if idx < 0 || idx as usize >= stream_len { + None + } else { + Some(idx as usize) + } + } + + /// **Window agreement at a locus** — do `self` and `other` place this + /// dimension at the SAME context event? True iff both are bound AND their + /// offsets are equal (loci convergence; never re-derives meaning). + #[inline] + #[must_use] + pub const fn agrees_at(self, other: Self, locus: Locus) -> bool { + let a = self.at(locus); + let b = other.at(locus); + a != 0 && a == b + } + + /// Count of named loci on which `self` and `other` agree (the quorum measure). + #[inline] + #[must_use] + pub fn agreement_count(self, other: Self) -> usize { + Locus::ALL + .iter() + .filter(|&&l| self.agrees_at(other, l)) + .count() + } + + /// The agreeing-peer offset (the [`Locus::Quorum`] pointer). + #[inline] + #[must_use] + pub const fn quorum(self) -> i8 { + self.at(Locus::Quorum) + } + + /// The preserved-dissenting-peer offset (the [`Locus::Contradiction`] pointer). + #[inline] + #[must_use] + pub const fn contradiction(self) -> i8 { + self.at(Locus::Contradiction) + } + + /// The KAUSAL cause pointer — causality learning's stored answer. + #[inline] + #[must_use] + pub const fn cause(self) -> i8 { + self.at(Locus::Kausal) + } + + /// The coreference antecedent pointer (relativPronomen → antecedent). + #[inline] + #[must_use] + pub const fn antecedent(self) -> i8 { + self.at(Locus::Antecedent) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn register_roundtrip_is_loss_free() { + let reg = [ + 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x11, 0x22, 0x33, 0x44, + ]; + let w = CausalWitnessFacet::from_register(reg); + assert_eq!(w.to_register(), reg); + } + + #[test] + fn signed_nibble_range_is_minus8_to_plus7() { + let mut w = CausalWitnessFacet::ZERO; + for slot in 0..WITNESS_LOCI { + w.set(slot, 7); + assert_eq!(w.get(slot), 7, "max +7 at slot {slot}"); + w.set(slot, -8); + assert_eq!(w.get(slot), -8, "min −8 at slot {slot}"); + w.set(slot, 0); + assert_eq!(w.get(slot), 0, "unbound at slot {slot}"); + } + } + + #[test] + fn set_clamps_out_of_range() { + let mut w = CausalWitnessFacet::ZERO; + w.set(0, 100); + assert_eq!(w.get(0), 7); + w.set(1, -100); + assert_eq!(w.get(1), -8); + } + + #[test] + fn named_accessors_hit_the_right_slots() { + let w = CausalWitnessFacet::ZERO + .with(Locus::Kausal, -3) + .with(Locus::Antecedent, -1) + .with(Locus::Quorum, 2) + .with(Locus::Contradiction, -5); + assert_eq!(w.at(Locus::Kausal), -3); + assert_eq!(w.cause(), -3); + assert_eq!(w.antecedent(), -1); + assert_eq!(w.quorum(), 2); + assert_eq!(w.contradiction(), -5); + assert_eq!(w.at(Locus::Temporal), 0); // unbound + assert_eq!(w.bound_count(), 4); + } + + #[test] + fn slot_index_matches_locus_discriminant() { + for (i, l) in Locus::ALL.iter().enumerate() { + assert_eq!(*l as usize, i, "{} slot", l.label()); + assert!(!l.label().is_empty(), "named locus has a label"); + } + } + + #[test] + fn reserved_slots_16_to_23_have_empty_labels() { + for (slot, label) in LOCUS_LABELS.iter().enumerate().skip(NAMED_LOCI) { + assert_eq!(*label, "", "slot {slot} reserved-empty"); + } + } + + #[test] + fn resolves_to_respects_window_and_bounds() { + let w = CausalWitnessFacet::ZERO + .with(Locus::Kausal, -3) + .with(Locus::Temporal, 4); + // self at position 5 in a 10-node stream + assert_eq!(w.resolves_to(Locus::Kausal, 5, 10), Some(2)); // 5 + (−3) + assert_eq!(w.resolves_to(Locus::Temporal, 5, 10), Some(9)); // 5 + 4 + assert_eq!(w.resolves_to(Locus::Modal, 5, 10), None); // unbound + // out of bounds + assert_eq!(w.resolves_to(Locus::Kausal, 1, 10), None); // 1 + (−3) < 0 + assert_eq!(w.resolves_to(Locus::Temporal, 8, 10), None); // 8 + 4 ≥ 10 + } + + #[test] + fn window_agreement_is_loci_convergence() { + let a = CausalWitnessFacet::ZERO + .with(Locus::Kausal, -3) + .with(Locus::SMeaning, 2); + let b = CausalWitnessFacet::ZERO + .with(Locus::Kausal, -3) // agrees + .with(Locus::SMeaning, 5); // disagrees (different offset) + assert!(a.agrees_at(b, Locus::Kausal)); + assert!(!a.agrees_at(b, Locus::SMeaning)); + // unbound never agrees + assert!(!a.agrees_at(b, Locus::Modal)); + assert_eq!(a.agreement_count(b), 1); + } +} diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 26d385230..a47238b61 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -54,6 +54,7 @@ pub mod awareness_facet; pub mod callcenter; pub mod cam; pub mod canonical_node; +pub mod causal_witness; pub mod class_view; /// D-V3-W6a — classid adoption-scan counting logic (`ClassidForm`, /// `classify_form`, `AdoptionCounts`, `count_adoption`). See @@ -129,7 +130,9 @@ pub use qualia::{ }; pub mod materialize; pub mod reasoning; +pub mod recipe_dispatch; pub mod recipe_kernels; +pub mod recipe_substrate; pub mod recipes; pub mod repository; pub mod savants; diff --git a/crates/lance-graph-contract/src/recipe_dispatch.rs b/crates/lance-graph-contract/src/recipe_dispatch.rs new file mode 100644 index 000000000..734d7c833 --- /dev/null +++ b/crates/lance-graph-contract/src/recipe_dispatch.rs @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +//! `recipe_dispatch` — the 34 recipes as a **rung-ordered, NaN-gated causal +//! ladder**, keyed by NARS inference type. +//! +//! # Two dispatch modes, one catalogue (vs `docs/NARS_RECIPES_DISPATCH.md`) +//! +//! The canonical dispatch doc (`66f5a73`) measured the shipped surprise-band +//! selector ([`materialize::select_tactic`](crate::materialize::select_tactic)): +//! only **8/34 recipes are reachable** through it, all 14 Infrastructure recipes +//! never win, and ICR #31 is permanently shadowed by RCR #4 on the lowest-id tie +//! (`E-RECIPE-SELECTOR-REACHABILITY-1`). This module is the **complementary +//! mode**, not a replacement: the selector is the *foveated saccade* (ONE tactic +//! per surprise cycle); the [`ladder`] is the *systematic sweep* (ALL 34 in +//! escalation order, each gated only by its own NaN checklist). Under the +//! ladder, all 34 are reachable, CrossTier/Infrastructure recipes fire early +//! (rung base 1), and ICR #31 fires unshadowed at the deepest rung — the three +//! measured selector gaps are covered by mode-composition, not by retuning the +//! selector. +//! +//! # Thinking is an act of cause and effect (and a finder of it) +//! +//! Orchestrating the recipes is *itself* causal: firing a recipe is a **cause** +//! whose **effect** is the next [`ThoughtCtx`](crate::recipe_kernels::ThoughtCtx) +//! state, which triggers the next recipe. The rung order IS a causal chain. The +//! recipes also *find* cause and effect in the data (RCR/ICR read the witness +//! [`Kausal`](crate::causal_witness::Locus::Kausal) edge). And — the +//! **Versuchsleitereffekt** (experimenter effect) — the CHOICE of which recipe +//! fires at which rung biases what is found, so each dispatch records its own +//! **triggering cause** ([`RecipeStep::trigger`]): the grounded awareness that +//! licensed it. The orchestration's causal influence is logged, not hidden. +//! +//! # Deterministic collapse — the glass-box cat +//! +//! Before dispatch the awareness is in **superposition**: the witness register +//! holds many bundled loci; many recipes could fire. Dispatching a recipe (or +//! reading a locus by its role key) is a **measurement** that collapses it — but +//! *deterministically*: identity is recoverable by key, no measurement +//! randomness (Schrödinger's cat in a glass box). The [`ladder`] is the ordered +//! collapse. The **Versuchsleitereffekt** above IS the Heisenberg observer +//! effect: measuring disturbs, so the trigger is logged. And a [`nan_disqualifier`] +//! is a **conjugate variable not measurable in this basis** — an input the +//! current tenants cannot ground, so that recipe is skipped rather than read off +//! noise. The `N ≤ √d/4 ≈ 32` bundle bound (`I-VSA-IDENTITIES` Test 1) is the +//! substrate's uncertainty relation; the 24-locus witness sits safely under it. +//! +//! # What each recipe knows (operator ruling) +//! +//! 1. **inference type** — [`RecipeInference`] (deduction / induction=fanout / +//! abduction / revision / counterfactual), the NARS inference it embodies. +//! 2. **rung level + order** — [`rung`] + [`dispatch_order`]: shallow forward +//! deduction fires first, deep counterfactual/revision last, from the recipe +//! [`Tier`](crate::recipes::Tier) plus the shipped +//! [`InferenceType::rung_delta`](crate::nars::InferenceType::rung_delta). +//! 3. **NaN disqualifiers** — [`nan_disqualifier`]: a required input +//! ([`Tactic::requires`](crate::recipe_kernels::Tactic)) that is NaN / +//! undefined in the projected ctx DISQUALIFIES the recipe. This is +//! `E-RELIABILITY-IS-CHECKLIST-COVERAGE` made a runtime gate — a recipe never +//! fires on an input the tenants could not ground. + +use crate::nars::InferenceType; +use crate::recipe_kernels::{kernel, ThoughtCtx, ThoughtField, ThoughtMask}; +use crate::recipes::{recipe, Tier}; + +/// The inference a recipe embodies. Maps to [`InferenceType`] for the four NARS +/// primitives; `Counterfactual` is the causal-edge mantissa-`−6` extension +/// (`I-VSA-IDENTITIES` counterfactual world), which [`InferenceType`] represents +/// via mantissa, not a variant. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RecipeInference { + /// Forward, exact (NARS Deduction). + Deduction, + /// Wide generate / fan-out (NARS Induction). + Induction, + /// Backward, effect→cause (NARS Abduction). + Abduction, + /// Merge / consensus / belief update (NARS Revision). + Revision, + /// Counterfactual world construction (causal-edge mantissa −6). + Counterfactual, +} + +impl RecipeInference { + /// The shipped [`InferenceType`] this maps to (Counterfactual → Abduction's + /// backward direction, its nearest NARS primitive; the causal-edge `−6` + /// mantissa carries the counterfactual distinction downstream). The bridge + /// is by VALUE — [`InferenceType::to_mantissa`] is the shared little-endian + /// rule key every `CausalEdge64` consumer reads. + #[must_use] + pub fn nars(self) -> InferenceType { + match self { + RecipeInference::Deduction => InferenceType::Deduction, + RecipeInference::Induction => InferenceType::Induction, + RecipeInference::Abduction | RecipeInference::Counterfactual => { + InferenceType::Abduction + } + RecipeInference::Revision => InferenceType::Revision, + } + } + /// Escalation depth offset (the ORDER cost, not the mantissa *direction*): + /// forward-exact deduction is cheapest, backward counterfactual deepest. + /// Ded +1 → Ind +2 → Rev +3 → Abd +4 → Counterfactual +5. + #[must_use] + pub fn rung_delta(self) -> i16 { + match self { + RecipeInference::Deduction => 1, + RecipeInference::Induction => 2, + RecipeInference::Revision => 3, + RecipeInference::Abduction => 4, + RecipeInference::Counterfactual => 5, + } + } + /// Short tag. + #[must_use] + pub fn tag(self) -> &'static str { + match self { + RecipeInference::Deduction => "deduction", + RecipeInference::Induction => "induction/fanout", + RecipeInference::Abduction => "abduction", + RecipeInference::Revision => "revision", + RecipeInference::Counterfactual => "counterfactual", + } + } +} + +/// The inference each of the 34 recipes embodies (documented classification from +/// the recipe's characteristic operation — `recipes.rs` `substrate`/`mechanism`). +#[must_use] +pub fn inference(id: u8) -> RecipeInference { + use RecipeInference::*; + match id { + 1 => Induction, // RTE recursive expansion (fan-out depth) + 2 => Induction, // HTD hierarchical decompose (fan-out) + 3 => Revision, // SMAD debate → consensus revise + 4 => Abduction, // RCR reverse causality (backward S_O) — explicit + 5 => Deduction, // TCP prune (forward select) + 6 => Induction, // TR randomize (divergent generate) + 7 => Revision, // ASC adversarial self-critique (belief revision) + 8 => Deduction, // CAS abstraction scaling (level select) + 9 => Induction, // IRS roleplay synthesis (generate variants) + 10 => Revision, // MCP meta-cognition (calibration update) + 11 => Revision, // CR contradiction resolution (revise on conflict) + 12 => Deduction, // TCA temporal precedence (Granger forward) + 13 => Induction, // CDT convergent/divergent (divergent generate) + 14 => Deduction, // MCT multimodal fuse + 15 => Induction, // LSI latent introspection (distribution read) + 16 => Deduction, // PSO scaffold (order) + 17 => Revision, // CDI dissonance induction (revise) + 18 => Revision, // CWS context persistence (memory update) + 19 => Abduction, // ARE reverse-engineer (recover component) + 20 => Revision, // TCF cascade filter (agreement consensus) + 21 => Revision, // SSR self-skepticism (down-weight) + 22 => Induction, // ETD emergent decompose (fan-out) + 23 => Induction, // AMP adaptive meta (explore/adapt) + 24 => Deduction, // ZCF zero-shot fuse (compose) + 25 => Deduction, // HPM pattern match (nearest) + 26 => Deduction, // CUR uncertainty reduction (coarse→fine select) + 27 => Revision, // MPC multi-perspective compress (majority) + 28 => Abduction, // SSAM analogical mapping (analogy → hypothesis) + 29 => Deduction, // IDR intent reframe (select dominant) + 30 => Revision, // SPP shadow parallel (agreement verify) + 31 => Counterfactual, // ICR iterative counterfactual — explicit + 32 => Deduction, // SDD distortion detect (validate) + 33 => Revision, // DTMF meta-frame switch + 34 => Deduction, // HKF cross-domain fuse (compose) + _ => Deduction, + } +} + +/// The rung level (`1..=9`) a recipe fires at: a base from its difficulty +/// [`Tier`] plus its inference [`rung_delta`](RecipeInference::rung_delta). +/// Shallow forward deduction → low rung; deep counterfactual/revision → high. +#[must_use] +pub fn rung(id: u8) -> u8 { + let tier = recipe(id).map(|r| r.tier).unwrap_or(Tier::CrossTier); + let base: i16 = match tier { + Tier::CrossTier => 1, // infrastructure — fires everywhere, early + Tier::Hard => 3, // the ~65% plateau + Tier::ExtremelyHard => 5, // convergent lock-in + }; + (base + inference(id).rung_delta()).clamp(1, 9) as u8 +} + +/// The 34 recipe ids in **dispatch order** — ascending rung, then ascending id. +/// This IS the escalation ladder: the causal chain fires shallow → deep. +#[must_use] +pub fn dispatch_order() -> [u8; 34] { + let mut ids: [u8; 34] = core::array::from_fn(|i| i as u8 + 1); + ids.sort_by_key(|&id| (rung(id), id)); + ids +} + +/// Map a [`ThoughtField`] to whether its ctx marker is **NaN / undefined**. +/// f32 markers use `is_nan`; the two collection markers use `is_empty` (an empty +/// input is as undefined as a NaN). `Rung` (a `u8`) is never NaN. +fn field_is_undefined(field: ThoughtField, ctx: &ThoughtCtx) -> bool { + match field { + ThoughtField::Sd => ctx.sd.is_nan(), + ThoughtField::FreeEnergy => ctx.free_energy.is_nan(), + ThoughtField::Dissonance => ctx.dissonance.is_nan(), + ThoughtField::Temperature => ctx.temperature.is_nan(), + ThoughtField::Confidence => ctx.confidence.is_nan(), + ThoughtField::Rung => false, + ThoughtField::Candidates => ctx.candidates.is_empty(), + ThoughtField::Beliefs => ctx.beliefs.is_empty(), + } +} + +/// **The NaN disqualifier gate.** Returns the first required input that is NaN / +/// undefined in `ctx` (so the recipe is DISQUALIFIED from firing), or `None` if +/// the recipe's whole checklist is grounded. The required set is the kernel's +/// own [`requires`](crate::recipe_kernels::Tactic::requires) mask — reliability +/// as coverage, enforced at dispatch. +#[must_use] +pub fn nan_disqualifier(ctx: &ThoughtCtx, id: u8) -> Option { + let req: ThoughtMask = kernel(id)?.requires(); + const FIELDS: [ThoughtField; 8] = [ + ThoughtField::Sd, + ThoughtField::FreeEnergy, + ThoughtField::Dissonance, + ThoughtField::Temperature, + ThoughtField::Confidence, + ThoughtField::Rung, + ThoughtField::Candidates, + ThoughtField::Beliefs, + ]; + FIELDS + .into_iter() + .find(|&f| req.has(f) && field_is_undefined(f, ctx)) +} + +/// One rung-ordered dispatch decision, recording its **triggering cause** (the +/// grounded required checklist that licensed it) so the orchestration's own +/// causal influence is auditable (Versuchsleitereffekt). +#[derive(Debug, Clone, Copy)] +pub struct RecipeStep { + /// Recipe id (1..=34). + pub id: u8, + /// Rung this recipe fires at. + pub rung: u8, + /// Inference the recipe embodies. + pub inference: RecipeInference, + /// `None` = fired; `Some(field)` = disqualified because that input was NaN. + pub disqualified_by: Option, + /// The grounded required-field count that licensed firing (the cause the + /// orchestration records for itself). + pub trigger: u32, +} + +/// Walk all 34 recipes in [`dispatch_order`], gating each on +/// [`nan_disqualifier`]. The returned steps are the **causal chain of the +/// orchestration** — each records whether it fired, at what rung, on which +/// inference, and its triggering-cause coverage. +#[must_use] +pub fn ladder(ctx: &ThoughtCtx) -> Vec { + dispatch_order() + .into_iter() + .map(|id| { + let disq = nan_disqualifier(ctx, id); + let trigger = kernel(id).map(|k| k.requires().len()).unwrap_or(0); + RecipeStep { + id, + rung: rung(id), + inference: inference(id), + disqualified_by: disq, + trigger, + } + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::recipe_kernels::ThoughtCtx; + + #[test] + fn every_recipe_has_an_inference_and_a_rung_in_range() { + for id in 1..=34u8 { + let r = rung(id); + assert!((1..=9).contains(&r), "recipe {id} rung {r} out of 1..=9"); + let _ = inference(id); // total + } + } + + #[test] + fn dispatch_order_is_rung_ascending() { + let order = dispatch_order(); + assert_eq!(order.len(), 34); + // a permutation of 1..=34 + let mut seen = order; + seen.sort_unstable(); + assert_eq!(seen, core::array::from_fn::(|i| i as u8 + 1)); + // non-decreasing rung + let rungs: Vec = order.iter().map(|&id| rung(id)).collect(); + assert!( + rungs.windows(2).all(|w| w[0] <= w[1]), + "rungs ascend: {rungs:?}" + ); + } + + #[test] + fn counterfactual_fires_deepest() { + // ICR (31) is the only Counterfactual; it should sit at a high rung. + assert_eq!(inference(31), RecipeInference::Counterfactual); + assert!(rung(31) >= 9, "counterfactual is the deepest rung"); + // RCR (4) abduction is explicit backward causality. + assert_eq!(inference(4), RecipeInference::Abduction); + } + + #[test] + fn nan_input_disqualifies_the_requiring_recipe() { + // Cr(11) requires Beliefs. An empty belief set → disqualified. + let mut c = ThoughtCtx::new(vec![0.5]); + c.beliefs.clear(); + assert_eq!(nan_disqualifier(&c, 11), Some(ThoughtField::Beliefs)); + // give it a belief → no longer disqualified on that field + c.beliefs.push((7, 0.9, 0.8)); + assert_eq!(nan_disqualifier(&c, 11), None); + } + + #[test] + fn nan_free_energy_disqualifies_rte() { + // Rte(1) requires FreeEnergy + Rung. NaN free_energy → disqualified. + let mut c = ThoughtCtx::new(vec![0.5]); + c.free_energy = f32::NAN; + assert_eq!(nan_disqualifier(&c, 1), Some(ThoughtField::FreeEnergy)); + } + + #[test] + fn ladder_records_the_causal_chain() { + // A fully grounded ctx: everything fires. + let mut c = ThoughtCtx::new(vec![0.9, 0.5, 0.1]); + c.beliefs = vec![(7, 0.9, 0.8)]; + let steps = ladder(&c); + assert_eq!(steps.len(), 34); + assert!( + steps.iter().all(|s| s.disqualified_by.is_none()), + "all grounded → all fire" + ); + // rung-ordered + assert!(steps.windows(2).all(|w| w[0].rung <= w[1].rung)); + // an ungrounded ctx: candidate + belief recipes disqualified. + let empty = ThoughtCtx { + sd: f32::NAN, + free_energy: f32::NAN, + dissonance: f32::NAN, + temperature: 0.5, + confidence: f32::NAN, + rung: 1, + candidates: Vec::new(), + beliefs: Vec::new(), + }; + let steps2 = ladder(&empty); + assert!( + steps2.iter().any(|s| s.disqualified_by.is_some()), + "ungrounded inputs disqualify some recipes" + ); + } +} diff --git a/crates/lance-graph-contract/src/recipe_substrate.rs b/crates/lance-graph-contract/src/recipe_substrate.rs new file mode 100644 index 000000000..79511dc7b --- /dev/null +++ b/crates/lance-graph-contract/src/recipe_substrate.rs @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +//! `recipe_substrate` — wire the REAL tenants into the recipe layer's input. +//! +//! `recipe_claim_audit` proved the 34 [`recipe_kernels`](crate::recipe_kernels) +//! run on a lightweight [`ThoughtCtx`] of hand-set scalar markers — none read a +//! real organ. This module closes that gap: a [`SubstrateView`] carries the three +//! real awareness tenants and **projects** them into the marker basis, so the +//! kernels run on tenant-derived input instead of synthetic constants. +//! +//! # The three tenants +//! +//! * **SPO / AriGraph** — [`SpoFacet`](crate::awareness_facet::SpoFacet) (A1): the +//! semantic triple + episodic-witness triple grounding *what S/P/O mean here*. +//! * **causalwitness** — [`CausalWitnessFacet`](crate::causal_witness::CausalWitnessFacet) +//! (A9): the **24 edges** — signed loci placing each awareness dimension in the +//! `±8` Markov window. The quorum / contradiction / kausal edges are the logical +//! core. +//! * **qualia** — [`QualiaI4_16D`](crate::qualia::QualiaI4_16D): the affective +//! texture. +//! +//! # Two separations the operator locked +//! +//! 1. **Qualia is an ADDITIVE factor** — it is *added* to the affective marker +//! ([`affective_temperature`]), never multiplied in and never used to gate. +//! 2. **Qualia is stakes, not logic** — the *logical* markers (confidence, +//! surprise, dissonance) are derived ONLY from the SPO tenant + the 24 witness +//! edges. Qualia describes the **stakes** of a thought — how much it matters, +//! the felt arousal/tension — never whether a conclusion is true or +//! contradictory. You do not describe an epiphany's logical quality with a +//! qualia about how the epiphany feels. +//! 3. **Causality is NOT a qualia** — the cause is the witness [`Locus::Kausal`] +//! edge (logical, in A9), never a qualia dimension. +//! +//! So: **logic + causality ← SPO + witness edges; stakes ← qualia (additive, +//! temperature only).** + +use crate::awareness_facet::SpoFacet; +use crate::causal_witness::{CausalWitnessFacet, Locus}; +use crate::qualia::{QualiaI4_16D, QUALIA_I4_LABELS}; +use crate::recipe_kernels::ThoughtCtx; + +/// A row's real awareness tenants — the substrate a recipe reasons over. +#[derive(Debug, Clone, Copy, Default)] +pub struct SubstrateView { + /// SPO / AriGraph tenant (A1) — semantic + episodic-witness triple. + pub spo: SpoFacet, + /// causalwitness tenant (A9) — the 24 window-loci edges. + pub witness: CausalWitnessFacet, + /// qualia tenant — affective texture (additive; temperature only). + pub qualia: QualiaI4_16D, +} + +/// Palette256² grid similarity between two `(basin, identity)` centroids, in +/// `[0.0, 1.0]` (1 = identical). A structural stand-in for the ndarray 256×256 +/// CAM-PQ distance table (unavailable in the zero-dep contract) — deterministic +/// L1-in-grid; the exact table binding is an ndarray-side follow-up. +#[inline] +#[must_use] +pub fn pair_similarity(a: (u8, u8), b: (u8, u8)) -> f32 { + let d = (a.0 as i32 - b.0 as i32).unsigned_abs() + (a.1 as i32 - b.1 as i32).unsigned_abs(); + 1.0 - (d as f32 / 510.0) +} + +/// Index of a named qualia i4 dimension. +fn q_dim(label: &str) -> usize { + QUALIA_I4_LABELS + .iter() + .position(|&l| l == label) + .expect("qualia label present") +} + +/// The affective temperature marker — qualia as an **additive** factor. Base +/// explore/exploit `0.5` plus the felt arousal & tension, minus groundedness +/// (settled → cooler). Purely affective: it never touches the logical markers. +#[must_use] +pub fn affective_temperature(qualia: QualiaI4_16D) -> f32 { + // i4 values are −8..+7; normalize each additive contribution to ~[−0.1,0.1]. + let arousal = qualia.get(q_dim("arousal")) as f32 / 70.0; + let tension = qualia.get(q_dim("tension")) as f32 / 70.0; + let grounded = qualia.get(q_dim("groundedness")) as f32 / 70.0; + (0.5 + arousal + tension - grounded).clamp(0.0, 1.0) +} + +impl SubstrateView { + /// Build from the three tenant registers. + #[inline] + #[must_use] + pub const fn new(spo: SpoFacet, witness: CausalWitnessFacet, qualia: QualiaI4_16D) -> Self { + Self { + spo, + witness, + qualia, + } + } + + /// **Logical confidence** — from the SPO tenant + witness edges ONLY (no + /// qualia). Rises when a **quorum** (agreeing) peer is bound and when the + /// semantic triple agrees with its episodic witness; the contradiction edge + /// pulls it down. + #[must_use] + pub fn logical_confidence(&self) -> f32 { + // SPO semantic ↔ episodic-witness agreement (real: does the triple's + // meaning match what was witnessed). + let sw = (pair_similarity(self.spo.subject, self.spo.ew_subject) + + pair_similarity(self.spo.predicate, self.spo.ew_predicate) + + pair_similarity(self.spo.object, self.spo.ew_object)) + / 3.0; + let quorum = if self.witness.is_bound(Locus::Quorum) { + 0.2 + } else { + 0.0 + }; + let contra = if self.witness.is_bound(Locus::Contradiction) { + 0.2 + } else { + 0.0 + }; + (0.5 * sw + 0.3 + quorum - contra).clamp(0.0, 1.0) + } + + /// **Logical surprise (free energy)** — from the witness edges ONLY. A bound + /// contradiction edge raises surprise; a richly-bound window (many loci + /// placed) lowers it (the row is well-situated). No qualia. + #[must_use] + pub fn logical_surprise(&self) -> f32 { + let contra = if self.witness.is_bound(Locus::Contradiction) { + 0.4 + } else { + 0.0 + }; + let situated = self.witness.bound_count() as f32 / crate::causal_witness::NAMED_LOCI as f32; + (0.5 + contra - 0.4 * situated).clamp(0.0, 1.0) + } + + /// **Logical dissonance** — the contradiction edge is bound AND points to a + /// DIFFERENT event than the quorum edge (a preserved, real disagreement). No + /// qualia. Magnitude scales with the offset gap between the two peers. + #[must_use] + pub fn logical_dissonance(&self) -> f32 { + let q = self.witness.quorum(); + let c = self.witness.contradiction(); + if c != 0 && c != q { + ((q - c).unsigned_abs() as f32 / 15.0).clamp(0.0, 1.0) + } else { + 0.0 + } + } + + /// Candidate scores from the **bound witness edges** — the window's placed + /// loci as a real vector (their similarities to the quorum peer), the + /// substrate the parallel/prune/filter recipes actually search. Logical. + #[must_use] + pub fn logical_candidates(&self) -> Vec { + let q = self.witness.quorum(); + Locus::ALL + .iter() + .filter(|&&l| self.witness.is_bound(l)) + .map(|&l| { + // closeness of this edge's placement to the quorum peer (∈[0,1]) + let off = self.witness.at(l); + 1.0 - ((off - q).unsigned_abs() as f32 / 15.0) + }) + .collect() + } + + /// The meaning-depth rung, from the witness **meaning-level** edge magnitude + /// (1..=9). Logical. + #[must_use] + pub fn logical_rung(&self) -> u8 { + let m = self.witness.at(Locus::MeaningLevel).unsigned_abs(); + (m.min(8) + 1).min(9) + } + + /// Belief set grounded in the SPO tenant: the semantic triple as one belief + /// keyed by the subject centroid, with frequency = S↔O relatedness and + /// confidence = the logical confidence. Logical. + #[must_use] + pub fn logical_beliefs(&self) -> Vec<(u32, f32, f32)> { + let topic = u32::from(self.spo.subject.0) << 8 | u32::from(self.spo.subject.1); + let freq = pair_similarity(self.spo.subject, self.spo.object); + vec![(topic, freq, self.logical_confidence())] + } + + /// Is the SPO tenant present (not the all-zero default)? + #[inline] + #[must_use] + pub fn spo_present(&self) -> bool { + self.spo != SpoFacet::default() + } + + /// Is the causalwitness tenant present (at least one bound edge)? + #[inline] + #[must_use] + pub fn witness_present(&self) -> bool { + self.witness.bound_count() > 0 + } + + /// Project all three tenants into a [`ThoughtCtx`] the 34 kernels consume. + /// + /// LOGICAL markers ← SPO + witness edges; the AFFECTIVE `temperature` ← + /// qualia (additive). A marker a **missing** tenant cannot ground is emitted + /// as **NaN** (an f32) or **empty** (a Vec) — the honest "undefined input" + /// that a recipe's NaN-disqualifier gate ([`crate::recipe_dispatch::nan_disqualifier`]) + /// then reads to skip that recipe. `temperature` is always grounded (qualia + /// defaults are valid stakes). + #[must_use] + pub fn project(&self) -> ThoughtCtx { + let candidates = self.logical_candidates(); // empty if no bound edges + // free-energy (surprise about truth) needs the witness edges; ungrounded → NaN. + let free_energy = if self.witness_present() { + self.logical_surprise() + } else { + f32::NAN + }; + // confidence is grounded by SPO agreement OR an agreement/contradiction edge. + let conf_grounded = self.spo_present() + || self.witness.is_bound(Locus::Quorum) + || self.witness.is_bound(Locus::Contradiction); + let confidence = if conf_grounded { + self.logical_confidence() + } else { + f32::NAN + }; + let beliefs = if self.spo_present() { + self.logical_beliefs() + } else { + Vec::new() + }; + // dissonance is always computable from the witness (0.0 = no contradiction edge). + let dissonance = self.logical_dissonance(); + // sd = dispersion of the candidate edges (0 if <2 edges). + let sd = if candidates.len() >= 2 { + let m = candidates.iter().sum::() / candidates.len() as f32; + (candidates.iter().map(|&v| (v - m).powi(2)).sum::() / candidates.len() as f32) + .sqrt() + } else { + 0.25 + }; + ThoughtCtx { + sd, + free_energy, + dissonance, + temperature: affective_temperature(self.qualia), // qualia: additive, affective + confidence, + rung: self.logical_rung(), + candidates, + beliefs, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn qualia_hot() -> QualiaI4_16D { + QualiaI4_16D::ZERO + .with(q_dim("arousal"), 6) + .with(q_dim("tension"), 5) + } + fn qualia_cool() -> QualiaI4_16D { + QualiaI4_16D::ZERO.with(q_dim("groundedness"), 6) + } + + #[test] + fn qualia_is_additive_and_affective_only() { + // Two rows with IDENTICAL logic (same SPO + witness) but different qualia + // must differ ONLY in temperature — never in any logical marker. + let spo = SpoFacet::from_register([10, 20, 10, 20, 10, 20, 11, 21, 11, 21, 11, 21]); + let witness = CausalWitnessFacet::ZERO + .with(Locus::Quorum, 2) + .with(Locus::Kausal, -3); + let hot = SubstrateView::new(spo, witness, qualia_hot()).project(); + let cool = SubstrateView::new(spo, witness, qualia_cool()).project(); + + assert!( + hot.temperature > cool.temperature, + "qualia adds to temperature" + ); + // logic identical — qualia touched nothing logical + assert_eq!( + hot.confidence, cool.confidence, + "qualia must not touch confidence" + ); + assert_eq!( + hot.free_energy, cool.free_energy, + "qualia must not touch surprise" + ); + assert_eq!( + hot.dissonance, cool.dissonance, + "qualia must not touch dissonance" + ); + assert_eq!( + hot.candidates, cool.candidates, + "qualia must not touch candidates" + ); + assert_eq!(hot.rung, cool.rung, "qualia must not touch rung"); + } + + #[test] + fn contradiction_edge_drives_logical_dissonance_and_surprise() { + let spo = SpoFacet::default(); + let peaceful = SubstrateView::new( + spo, + CausalWitnessFacet::ZERO.with(Locus::Quorum, 2), + QualiaI4_16D::ZERO, + ); + let conflicted = SubstrateView::new( + spo, + CausalWitnessFacet::ZERO + .with(Locus::Quorum, 2) + .with(Locus::Contradiction, -4), // a preserved dissenting peer + QualiaI4_16D::ZERO, + ); + assert_eq!(peaceful.logical_dissonance(), 0.0); + assert!( + conflicted.logical_dissonance() > 0.0, + "contradiction edge → dissonance" + ); + assert!( + conflicted.logical_surprise() > peaceful.logical_surprise(), + "contradiction edge → higher surprise" + ); + } + + #[test] + fn quorum_edge_raises_logical_confidence() { + let spo = SpoFacet::default(); + let alone = SubstrateView::new(spo, CausalWitnessFacet::ZERO, QualiaI4_16D::ZERO); + let corroborated = SubstrateView::new( + spo, + CausalWitnessFacet::ZERO.with(Locus::Quorum, 3), + QualiaI4_16D::ZERO, + ); + assert!( + corroborated.logical_confidence() > alone.logical_confidence(), + "an agreeing peer edge raises confidence" + ); + } + + #[test] + fn candidates_come_from_bound_edges() { + let spo = SpoFacet::default(); + let w = CausalWitnessFacet::ZERO + .with(Locus::Quorum, 2) + .with(Locus::Kausal, -3) + .with(Locus::SMeaning, 1); + let v = SubstrateView::new(spo, w, QualiaI4_16D::ZERO); + assert_eq!(v.logical_candidates().len(), 3, "one score per bound edge"); + } +}