diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index bdc6d4292..05644ea31 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,582 @@ +## 2026-07-01 — E-OGAR-LANCEGRAPH-MOVE-IN-PARALLEL — OGAR + lance-graph are one coupled pair moved together every session; the `COUNT_FUSE` is the intentional dependency contract that enforces it (NOT a break to engineer away) + +**Status:** DOCTRINE (operator, 2026-07-01: "every session needs to move OGAR and +lance-graph in parallel … the fuse is okay for now, it's a dependency contract"). +Corrects my earlier recommendation to pin `lance-graph-ogar` to a rev to "retire +the recurring break class" — that was **wrong**: the recurrence IS the contract +working. + +**The doctrine.** +- **Move OGAR and lance-graph in parallel, every session.** They are one coupled + pair, not two independent repos. An OGAR codebook mint (`ogar_vocab::class_ids::ALL`) + and the paired `lance-graph-contract::ogar_codebook::CODEBOOK` mirror (+ the + `lance-graph-ogar::parity::domains_agree` arm) land in the **same coordinated + arc** — never split across sessions (cf. `E-CODEBOOK-MINT-IS-A-CROSS-REPO-ARC`, + `ISS-OGAR-AUTH-MIRROR-DRIFT`, `ISS-OGAR-GENETICS-MIRROR-PENDING`, + `ISS-OGAR-OSINT-MIRROR-PENDING`). +- **The `COUNT_FUSE` is a FEATURE.** `assert!(mirror::CODEBOOK.len() == + ogar_vocab::class_ids::ALL.len())` is the **dependency contract** that *forces* + the parallel movement — when it blows on merge, that is the contract correctly + demanding the mirror catch up, not a defect. `branch = "main"` tracking + the + fuse together are the enforcement mechanism. **Do NOT pin to a rev to dodge it; + do NOT delete/soften the fuse.** ("The fuse is okay for now.") +- **Merge sequencing = option 2 (coordinated / brief transient red is fine), + never option 1 (pin).** Merge the OGAR mint, then land the paired lance-graph + mirror rows immediately; the short red window is the fuse doing its job. + +**The plan the fuse is a stopgap FOR (target design).** When OGAR + lance-graph +are compiled into the **same binary**, lance-graph is **plug-and-play**, and +**when the contract "clicks" (links with OGAR present), OGAR knows it must emit +the vocab** — i.e. the mirror becomes *build-time-emitted from OGAR* (codegen, +not a hand-maintained copy), and the hand-mirror + `COUNT_FUSE` retire naturally. +Until that click ships, the hand-mirror + fuse is the correct stopgap. + +**Consequences.** +- `ISS-OGAR-OSINT-MIRROR-PENDING` resolution is now: keep the fuse, land the 2 + mirror rows + `domains_agree` arm in parallel with OGAR #145 (ready patch is in + the issue). Not a rev-pin. +- Any future OGAR concept mint carries its lance-graph mirror rows in the same + session's paired PRs — this session's #145 (OGAR) + #624 (lance-graph) is the + correct shape; the mirror rows just land in the coordinated merge, not before. + +**Cross-ref:** `ISS-OGAR-OSINT-MIRROR-PENDING`; `E-CODEBOOK-MINT-IS-A-CROSS-REPO-ARC`; +`I-LEGACY-API-FEATURE-GATED` (the same-name-different-semantics trap the fuse guards). + +--- + +## 2026-07-01 — E-CLASSID-SPLIT-ORDER-IS-A-FLIP — implement the classid field split as a single flippable "split order" (`(before)::Domain:appid::(after)`), so the deferred human-readable reorder is a one-place flag, not a rewrite + +**Status:** DIRECTIVE (operator, 2026-07-01: "when you start implementing the +substrate you make the `(before)::Domain:appid::(after)` a question of split +order that later you can flip"). Refines `E-CLASSID-HUMANREADABLE-REORDER-DEFERRED` +— names *how* to build the reorder so it stays a flip, not a migration. + +**The directive.** When the classid field extraction is implemented, route ALL +of it through **one split-order abstraction** — a single source of truth that +says which byte range is `before`, which is `Domain:appid`, which is `after`. +Layout template: **`(before) :: Domain:appid :: (after)`**. Every accessor +(`domain()`, `app_id()`, the V3-prefix check, the ClassView lookup, the codebook +row mapping) reads through that one definition. Then flipping the stored↔human- +readable order (`0x1000_0701` ⇄ `0x07:01::1000`) is a **one-place flag flip**, +not a rewrite scattered across the codebase. + +**Why it matters (the trap it avoids).** If field positions are hardcoded at +each call site, the deferred reorder becomes a risky N-site migration that can +silently desync (exactly the `I-LEGACY-API-FEATURE-GATED` failure mode — same +accessor, different bytes under a flag). One split-order definition = the flip is +atomic, testable with a single round-trip probe (`stored ⇄ human-readable` is +identity), and the little-endian contract is provably preserved end-to-end. + +**Consequences.** +- Do NOT hardcode classid byte offsets at call sites when the substrate impl + starts — thread them through the single split-order definition. +- The flip flag defaults to the CURRENT stored order (`(V3 0x1000)(domain 0x07)(app 01)`); + the human-readable `0x07:01::1000` is the flipped view, authored later. +- The reorder arc's acceptance test IS the round-trip: `flip(flip(x)) == x` and + `decode_stored(x).fields == decode_humanreadable(flip(x)).fields`. + +**Cross-ref:** `E-CLASSID-HUMANREADABLE-REORDER-DEFERRED` (the deferral this +implements); `I-LEGACY-API-FEATURE-GATED` (the same-name-different-bytes trap the +single-definition avoids); lance-graph `CLAUDE.md` CANON (LE 16-byte key). + +--- + +## 2026-07-01 — E-CLASSID-HUMANREADABLE-REORDER-DEFERRED — the classid field reorder to human-readable `0x07:01::1000` is a DELIBERATE post-V3 step; the little-endian stored form is the contract P1–P5 are built on — HANDS OFF until deliberately triggered + +**Status:** LOCK / DEFERRED-BY-DESIGN (operator, 2026-07-01). Both facts below +are **correct**; the presentation reorder is **postponed on purpose**, not an +omission to fix. Guards against a future session / CodeRabbit / codex +"helpfully" reversing the byte order mid-chase. + +**The semantic fields (operator-confirmed).** The classid decomposes as +**`(V3-substrate 0x1000)(domain 0x07)(app_prefix 0x01)`**. So in the current +`0x1000_0701`: hi-u16 `0x1000` = the **V3-substrate prefix** (the leading `1` is +the brute-force V3-format signal — NOT an app), and the low-u16 `0x0701` is +**`domain : app_prefix` = `0x07 : 01`** (OSINT domain, app-prefix 01). This +low-u16 is the **64k dynamic-class space** — 256 domains × 256 app-classes, each +resolving to a **ClassView**, modulated by the **bitmask** (focus of awareness). +> Correction of my earlier framing: I had called `0x1000` "q2's APP_PREFIX" and +> `0x0701` "the osint_person concept" — both wrong. `0x1000` is the V3 substrate +> marker; the concept split is `domain:app`, not a flat concept. + +**Why it is DEFERRED (the load-bearing part).** The human-readable form is +**`0x07:01::1000`** (`domain:app_prefix::v3-substrate`). Applying that reorder +now would **destroy the little-endian byte contracts** the V3 substrate is +chasing (cf. lance-graph `CLAUDE.md` CANON — the 16-byte key is explicitly +**little-endian**; `classid` is bytes 0..4 LE). P1–P5 and every codebook row are +built against the **current stored `0x1000_0701` LE form**. So the reorder was +**postponed until the V3 substrate was nailed** (the P-probes), on purpose, and +stays deferred until the operator deliberately triggers it as its own step. + +**Consequences (hands-off list).** +- **Do NOT** reorder classid bytes, "fix" `0x1000` → domain-first, or rewrite the + OGAR codebook rows (`osint_system 0x0700` / `osint_person 0x0701`) toward the + human-readable form. They are correct in the current LE contract. +- The **human-readable `0x07:01::1000`** is a *display/API* projection to be + authored later, as a deliberate reorder step — NOT a storage change. +- `ISS-OGAR-OSINT-MIRROR-PENDING` (the `COUNT_FUSE` row-count arc) is + **orthogonal** to this — it's about how many rows exist, not their byte order — + and still stands. +- When the reorder IS triggered post-V3, it gets its own arc + probe (round-trip + LE-stored ↔ human-readable) so the LE contract is preserved end-to-end. + +**Cross-ref:** lance-graph `CLAUDE.md` CANON (LE 16-byte key); +`ISS-OGAR-OSINT-MIRROR-PENDING`; OGAR canon §"THE CANONICAL GUID". Operator +directive 2026-07-01 ("1 is correct but we postponed until V3 substrate was +nailed … 2 is also correct it's just deferred … later human readable +`0x07:01::1000`"). + +--- + +## 2026-07-01 — E-SUBSTRATE-IS-DEEPNSM-PLUS-OGAR — the substrate IS a Semantic Transformer (deepnsm) + a transpiler (OGAR); the convergence probes are the de-blackboxing of the accumulated-wishlist entropy + +**Status:** STRATEGIC FRAME (operator, 2026-07-01: "in the end we're sitting on a +Semantic Transformer and transpiler as substrate and we didn't make it there +because our substrate was a bit black box accumulated wishlist"). Names the +*why* behind the P0–P5 probe arc. + +**The diagnosis (operator, verbatim intent).** The substrate accumulated as a +**black-box wishlist** — features piled up faster than they could be seen +through (the "massive codebase massive entropy" of the session opener), so the +fact that **deepnsm = the Semantic Transformer** and **OGAR = the transpiler** +were already the clean substrate got buried under the pile. "We didn't make it +there" = the two real substrate layers were obscured, not absent. + +**The cure = probe-first legibility (what P0–P5 actually did).** Each probe +converts one wishlist claim into an integer-exact green assert — that is the +mechanism that turns "accumulated wishlist" back into "Semantic Transformer + +transpiler": +- **P1 literally de-blackboxed the Semantic Transformer**: deepnsm's + `subspace_distance_table` *is* the one palette metric, byte-identical to the + two integer consumers. "deepnsm converges with everything" → a green test. +- **P2–P5** proved the edge / Pearl causality / ARM discovery / NAL reasoning all + ride that same palette. The wishlist became a verified five-vertex stack. +- The rung ladder + kanban Rubicon envelope (`graph-flow-kanban` already carries + the i4 32-D thinking style **and Rung depth** in `ThinkingContext`, advancing + on `GateDecision::{Flow,Block,Hold}`) are the transpiler-side orchestration — + shipped, not wishlist. + +**What is still black box (the honest edge of "made it there").** The **runtime +loop** — `rig → CausalEdge64 → AriGraph V3 → next cycle` feedback +(`E-RIG-ARIGRAPH-FEEDBACK-LAYERING`) — is asserted, not proven; gated on wiring +AriGraph write-back through the protoc boundary (P6/P9-class). Static substrate = +legible; dynamic substrate = not yet. + +**The standing rule this frame implies.** Entropy re-accumulates the moment a new +capability lands without a probe. To keep the substrate = deepnsm + OGAR (and not +re-become a wishlist): **no new substrate claim enters the canon without a green +integer-exact probe** — the same discipline as OGAR's probe-first doctrine and +the bf16-hhtl probe queue. The probes are not busywork; they are what keeps the +two real layers visible under everything built on them. + +**Cross-ref:** `E-SPO-2CUBE-GIVES-QUESTIONS-AND-CANDIDATES` (the shared atom); +`E-P1-DISTANCE-IDENTITY-GREEN` (the Semantic-Transformer de-blackboxing); +`E-RIG-ARIGRAPH-FEEDBACK-LAYERING` (the still-opaque runtime loop); OGAR +`docs/OGAR-AS-IR.md` (the transpiler-as-compiler framing). + +--- + +## 2026-07-01 — E-RIG-ARIGRAPH-FEEDBACK-LAYERING — the rig / rs-graph-llm / surrealdb / AriGraph-V3 stack is a hot-loop + cold-projection split, not an OR; the feedback round-trip is UNTESTED (blocked P9-class) + +**Status:** DECISION + honest gap (operator, 2026-07-01: "rs-graph-llm on top, +rig on top of surrealdb on kv-lance OR lancedb / or rig as LLM API in rs-graph-llm +feeding back akin to AriGraph using our AriGraph V3 — we didn't test it yet"). +Grounded in shipped-code reality (grep 2026-07-01), not doctrine alone. + +**On-disk maturity (what settles the "OR").** +- `rs-graph-llm/graph-flow-action-ogar` — **SHIPPED**: bridges graph-flow → OGAR + *Do* arm via `lance_graph_contract::{action, kanban::ExecTarget, rbac, + mul::GateDecision}`, contract-only ("no second lance-graph-contract in the + graph"). The orchestration↔OGAR seam exists. +- **AriGraph V3** — **COMMIT CONTRACT, round-trip untested**: named as terminus + everywhere (`kanban.rs` "calcify: commit to Lance SPO-G + AriGraph pointer"; + CausalEdge temporal = "AriGraph anchor"; `witness_table` crystallisation), but + agent-output → commit → readable-next-cycle is defined, not exercised. +- **rig** — **NOT wired** into rs-graph-llm anywhere yet. +- **surrealdb kv-lance** — **SCAFFOLD**: 8 open `TODO(lance-integration)` sites; + not load-bearing. + +**Decision — it is a layering, not an OR.** +- **Hot reasoning path (Option 2, doctrine-aligned + most-scaffolded):** + rs-graph-llm (replayable orchestration) drives graph-flow; **rig is the cold + LLM membrane** it calls for discovery/proposal; **AriGraph V3 is the thinking + tissue** the loop reads+writes (SoA palette tenant + CausalEdge commit + + witness crystallisation). "Memory wired INTO the struct, not called FROM it"; + "orchestration IS graph traversal." rig/spider/rs-graph-llm stay on the cold + external side of the Markov membrane (cf. `E-RIG-DISCOVERY-MEMBRANE`). +- **Cold persistence projection (Option 1, beside not between):** AriGraph V3 + *tombstones to* Lance SPO-G; a query surface reads that same store. Use + **lancedb NOW** (kv-lance has 8 TODOs — do not bet the loop on it yet); + surrealdb-on-kv-lance becomes the SurrealQL surface once its integration lands. + surrealdb sits *beside* the loop as the query surface, **never between** the + agent and its memory. + +**The untested thing (operator's flag, made precise).** The **feedback +round-trip**: (mock-)rig output → `CausalEdge64`/SPO → committed to AriGraph V3 → +readable next cycle → reshapes free-energy. `PROBE-RIG-ARIGRAPH-FEEDBACK`: +stub the LLM (deterministic, no network — same pattern as template-runtime stub +actions), assert the committed edge is read back and moves `F` on the next cycle. +**Blocker (P9-class):** `graph-flow-action-ogar` is contract-only, so it can test +the *decision* half (`ActionInvocation → GateDecision(FLOW)`) today, but the +*commit* half (real AriGraph V3 write-back) needs lance-graph core (protoc), +unwired into any testable OSINT/graph-flow crate. Testing only the decision half +and calling it "the loop" would be a manufactured green — do NOT. + +**Cross-ref:** `E-RIG-DISCOVERY-MEMBRANE` (rig = cold membrane); `E-RUNG-LADDER-IS-A-DEPENDENCY-STACK` ++ P6 (awareness rollover = the elevation this loop drives); the P9 hot/cold +blocker (arigraph feature off in `lance-graph-osint`). Same wiring milestone. + +--- + +## 2026-07-01 — E-RIG-DISCOVERY-MEMBRANE + E-6x2x8-EXACT-CENTROID — two addenda to the V3-stack capstone: rig is the RAG/LLM-API template-discovery membrane; and the 6×(8:8) tenant can double as an exact-centroid distance code + +**Status:** ADDENDUM to `E-SPO-2CUBE-GIVES-QUESTIONS-AND-CANDIDATES` +(operator, 2026-07-01). Part A is a stack correction `[G]` (rig was omitted from +the capstone's V3 list); Part B is a `[S]` CONJECTURE (operator "theoretically we +could even…"), a representation option, un-probed. + +**A — rig = the discovery membrane (cold path), never the hot path.** The V3 +stack capstone listed five components + OGAR-AST render; it omitted **rig** +(`AdaWorldAPI/rig`) as the **RAG + LLM-API** layer whose job is **template +discovery**. The shape: rig retrieves (RAG) + calls an external LLM to *propose* +templates (ElixirTemplate / thinking-style / OSINT source-ranking); those +proposals compile down to **deterministic golden-image reflexes** +(`template-runtime` / `template-equivalence`) that run on the palette substrate +with **no LLM on the hot path** (matches the crewai-rust "LLM is IN THE LOOP, NOT +source of truth" doctrine + the Markov blood-brain barrier). So rig sits with +`spider` + `rs-graph-llm` on the **cold, external, learning** side of the +membrane — exactly the "external OSINT in step AFTER" the operator gated ("only +if it works without"). Corrected V3 stack = {AriGraph, CausalEdge, thinking-style, +rung, OGAR-AST-transpile/ClassView+bitmask+askama} **on the palette**, fed by +{rig-RAG/LLM, spider, rs-graph-llm} **across the membrane**, templates flowing +cold→hot as compiled reflexes, edges/awareness flowing hot→cold as witnesses. + +**B — 6×2×8bit "exact" centroid (6× palette256²) vs CAM-PQ (6×256).** A distance +representation option: instead of CAM-PQ's 6 subspaces × 1 byte (nearest of 256 +centroids per subspace, 6-byte code, quantization error), use 6 subspaces × **2 +bytes** — each an `(x:y)` byte-pair indexing a 256×256 palette tile +(`palette256²`), i.e. a 2D product centroid = 65 536 effective positions per +subspace = "exact" relative to CAM-PQ's 256. Trade: **12 bytes, exact, +unified** vs **6 bytes, quantized, separate code**. +- **The coincidence worth the entry:** the OSINT V3 identity tenant is *already* + **6×(8:8)** (12 bytes: HEEL `currentStatus:type`, HIP `militaryUse:civicUse`, + …). So the semantic tenant's byte-pairs **are** the 6× palette256² tile + coordinates — the named semantic axes double as the exact-centroid distance + code. No separate CAM-PQ code needed; the tenant IS the distance carrier. +- **Preserves the one-distance-format (P1):** distance is still + `Σ_subspace palette256²[coord_a][coord_b]`, Pearl-maskable exactly like the 2³ + decomposition — the 8-questions-from-N-reads amortization + (`E-SPO-2CUBE-…`) generalizes from 3 planes to 6 subspaces unchanged. +- **CAM-PQ stays for *search*** (compressed NN, the smaller 6-byte code); the + exact 6×(8:8) form is for the *tenant that is already stored* (no extra bytes — + the identity is 12 bytes regardless). Per `I-VSA-IDENTITIES`: CAM-PQ = search, + this = the stored identity's own distance; separate tools. +- **CONJECTURE — needs a probe** (`PROBE-6x2x8-vs-CAMPQ`): fidelity (ρ of the + exact 6×palette256² distance vs ground-truth) **and** storage/cache (12 B × + N nodes, 6× 256×256 tables vs CAM-PQ's 6× 256×256 + 6-byte codes) **and** + whether the 2D product-centroid actually beats 256 flat centroids on OSINT + workloads. Do NOT adopt over CAM-PQ without the probe; recorded as an option, + not a decision. Certification-officer / palette-engineer own it. + +**Cross-ref:** `E-SPO-2CUBE-GIVES-QUESTIONS-AND-CANDIDATES` (the capstone this +extends); `E-P1-DISTANCE-IDENTITY-GREEN` (the one-format the exact form must +preserve); `I-VSA-IDENTITIES` (search vs identity-distance separation); OGAR +canon §"Tier interpretation — 256×256 CENTROID TILE" (the tile reading this +builds on). + +--- + +## 2026-07-01 — E-SPO-2CUBE-GIVES-QUESTIONS-AND-CANDIDATES — the 8 Pearl projections are 8 distinct questions (each with its own candidate answer), computed all-at-once from 3 cached reads; V3 stack capstone + +**Status:** FINDING `[G]` (coded; `cargo test --manifest-path +crates/lance-graph-osint/Cargo.toml --test p3b_spo_questions_amortized` → 3 +passed, 2026-07-01) + capstone (operator synthesis 2026-07-01). Sharpens +`E-P2-P3-EDGE-PEARL-GREEN` (P3) and `E-RUNG-LADDER-IS-A-DEPENDENCY-STACK`. + +**The mechanism (operator, verbatim intent).** The SPO 2³ rung decomposition +"gives the questions and candidates inherently" — which is why it is baked into +the Morton-tile 2bit×2bit 4×4 cascade as **2³ all-at-once amortization on the +same data in cache.** Grounded in `SpoDistances::all_projections` +(`ALL_MASKS = [NONE,S,P,O,SP,SO,PO,SPO]`): it reads the **three** per-plane +palette cells for one `(a,b)` pair and produces **all eight** Pearl projections +by masked summation — 3 cache reads → 8 causal questions, zero extra traffic. +Proven un-redundant three ways: +1. **Amortization** — the 8-vector is a pure function of the 3 scalar plane + distances (`p3b_eight_questions_are_a_pure_function_of_three_reads`). +2. **Monotone ladder** — a superset mask never undercounts; `SPO` + (counterfactual) upper-bounds every sub-question. +3. **Questions AND candidates inherently** — the SAME two candidates rank + **oppositely** under Association (`SO`) vs Intervention (`PO`), because `PO` + projects out the Subject plane. The 8 masks are 8 real retrievals, not one + distance in eight hats — each rung surfaces its own candidate. + +**V3 stack — assembled (capstone).** The convergence this probe series (P0–P5 + +P3b) validates is the click of five V3 components onto ONE palette substrate: +- **AriGraph V3** — SPO-G quads + the 6×(8:8) OSINT tenant + a cold-KV + `path:documentid` reference tenant (wiki blobs off the hot path). +- **CausalEdge V3** — the 64-bit SPO-palette + NARS⟨f,c⟩ + Pearl-mask + i4 + mantissa wire (P2/P4/P5 carrier). +- **Thinking-style V3** — the 36 styles weighting the 8 projections + (`style_weight[i] × all_projections[i]`). +- **Rung decomposition V3** — the 0–9 `RungLevel` dependency stack + (`E-RUNG-LADDER-…`), whose per-rung *operation* is a Pearl-masked projection. +- **OGAR AST-transpile** — ClassView + the ClassView **bitmask as the focus of + awareness**, askama-on-ClassView as an ERB-view pattern (akin to Redmine's + Active-Record ERB views) — the render surface over the same node. + +**Two rabbit holes, now closed (context so the WHY doesn't dilute).** The path +here went through two detours that are resolved and should not be re-entered: +(1) the monolithic **Singleton BindSpace → SoA** migration (settled: SoA first, +zero-copy in-place, no materialized singleton — cf. the P0 CLAUDE.md three-tier +supersession); (2) **hypothesis testing in kanban-view** (settled: `rs-graph-llm` +as *replayable* orchestration layered on top of OGAR's Ontology/Do/Think arms + +the thinking styles — orchestration is graph traversal, not a new bridge). + +**Consequence.** The 2³ projection is the atom the whole stack shares: it is the +rung's operation (P3b), the discovery oracle's distance (P1/P4), the edge's +Pearl level (P2), and the style's weighting input — one cached tile, eight +questions, eight candidates. Nothing above it needs its own distance function. + +**Cross-ref:** `E-P1-DISTANCE-IDENTITY-GREEN` … `E-P5-SYLLOGIZE-GREEN`; +`E-RUNG-LADDER-IS-A-DEPENDENCY-STACK`. Test: +`crates/lance-graph-osint/tests/p3b_spo_questions_amortized.rs`. + +--- + +## 2026-07-01 — E-RUNG-LADDER-IS-A-DEPENDENCY-STACK — the 0–9 `RungLevel` ladder gates higher reasoning on grounded lower rungs; the P1–P5 probes are exactly the per-rung capabilities + +**Status:** FINDING `[G]` (operator directive 2026-07-01: "the 1-9 rung ladder +exists to make higher reasoning depending on observations, hypothesis testing +with counterfactual on top"). Grounds in shipped code: +`lance_graph_contract::cognitive_shader::RungLevel` (canonical; mirrored in +`thinking-engine::cognitive_stack::RungLevel`). + +**The ladder (contract, 0..9).** `0 Surface` (literal/immediate = observation) · +`1 Shallow` (simple inference) · `2 Contextual` · `3 Analogical` · `4 Abstract` · +`5 Structural` (schema) · `6 Counterfactual` (what-if) · `7 Meta` (reasoning +about reasoning) · `8 Recursive` · `9 Transcendent`. + +**The invariant (why it's a stack, not a menu).** A rung's answer is only valid +resting on the rungs below it — you cannot answer a rung-N question with only +rung-(N-1) evidence (Pearl's ladder, generalized to 10 levels). The mechanism is +in the contract: `ShaderDispatch.rung` **"elevates on sustained BLOCK"** — the +cycle starts at `Surface` (cheapest, observation-only) and climbs **bottom-up** +only when the collapse gate cannot resolve at the current depth. So +`Counterfactual` (6) is reached *after* the observation (0–1) and +hypothesis-forming (2–5) rungs are grounded — "counterfactual on top," +literally. Meta/Recursive/Transcendent (7–9) then reason *about* that. + +**The P1–P5 probes ARE the per-rung capabilities — read as a ladder climb:** +- **Observation rungs (0–1)** ← **P1**: one integer-exact palette distance is the + literal/immediate comparison every higher rung reads. +- **Hypothesis rungs (2–5)** ← **P3** (Pearl `PO` = do-calculus intervention, + Subject confounder projected out) + **P4** (ARM discovery = deterministic + hypothesis *generation* off the same palette). +- **Counterfactual rung (6)** ← **P3** (full `SPO` mask = Pearl Level-3) + **P5** + (`syllogize` multi-hop deduction chains) — what-if reasoning composed on top. +- The **D-ARM-7 Jirak floor** (`I-NOISE-FLOOR-JIRAK`, jc Pillar 5) is precisely + the *stack guard*: a mined rule (rungs 2–4) must not be promoted to a + counterfactual (rung 6) claim without enough observation evidence to clear the + weak-dependence noise floor. The gate enforces the ladder's dependency. + +**Consequence for the runtime probes (P6–P9).** The "static convergence" (P1–P5) +proved the palette is one metric for each rung's *operation*; the runtime half is +the *elevation dynamics*. **P6 (awareness rollover) IS the rung-elevation +mechanism** — sustained BLOCK carried across cycles in the MailboxSoA `MetaWord` +awareness bits is what pushes `rung` up the ladder. So P6 is not just "does +awareness persist" — it is "does unresolved surprise climb the ladder toward +counterfactual, and rest back down when resolved." Design P6 to assert the +elevation order (Surface → … → Counterfactual on sustained BLOCK; descend on +FLOW), not merely bit persistence. + +**Tech-debt flagged.** `RungLevel` is duplicated verbatim in `thinking-engine` +(no `lance-graph-contract` dep) — same anti-pattern as `E-CE64-NAME-COLLISION-DEDUP`. +Canonical is the contract; dedup when thinking-engine takes the contract dep +(deferred — cross-crate dep addition, not a local rename). Logged in TECH_DEBT. + +**Cross-ref:** `E-P5-SYLLOGIZE-GREEN` … `E-P1-DISTANCE-IDENTITY-GREEN` (the rung +capabilities); `I-NOISE-FLOOR-JIRAK` (the stack guard); OGAR +`docs/OSINT-SUBSTRATE-REUSE-MAP.md` (probe ladder). + +--- + +## 2026-07-01 — E-P5-SYLLOGIZE-GREEN — NAL transitive deduction on the palette edge: `is_a(A,B)∧is_a(B,C) ⊢ is_a(A,C)`, integer-exact + +**Status:** FINDING `[G]` (coded; `cargo test --manifest-path +crates/lance-graph-osint/Cargo.toml --test p5_syllogize` → 4 passed, 2026-07-01, +branch `claude/v3-substrate-migration-review-o0yoxv`). Probe P5; the reasoning +vertex of the convergence. + +**What it proves.** Two `CausalEdge64` premises sharing a middle palette term +compose via `syllogize()` into a derived conclusion edge: +- `is_a(A,B).figure(is_a(B,C)) == Figure::Chain` (o1==s2==B); the conclusion is + `is_a(A,C)` — middle term B consumed, outer terms survive. +- Deduction truth composes exactly: f = f1·f2 = 1.0 → 255; c = f1·f2·c1·c2 = + 0.64 → 163 (integer-exact after the single f32 NARS composition). +- `InferenceType::Deduction` stamps mantissa +1 (forward-chain); Pearl mask = + AND of the premises (SPO & SPO = SPO). +- Negative cases hold: no shared term ⇒ `None`; identical (S,O) ⇒ `None` + (that's NARS *revision*, not a syllogism); `syllogize` is a pure function + (deterministic). + +**Why it matters.** This is the `part_of`/`is_a` ontology chaining OSINT needs +(actor `part_of` unit `part_of` command; system `is_a` drone `is_a` dual-use) — +and it runs entirely on the SPO palette with no external reasoner. The predicate +is a carried placeholder slot, so `is_a` and `part_of` chain by the identical +mechanism. With P1–P4, the palette is now one metric across **five** vertices: +distance sources, edge carrier, causal-mask semantics, ARM discovery, and +multi-hop NAL reasoning. The remaining probes (P6–P8 MailboxSoA +awareness/owner/reflex loop, P9 AriGraph hot/cold) exercise the *runtime* loop +and need subsystem wiring not yet present in `lance-graph-osint`. + +**Cross-ref:** `E-P4-DISCOVERY-EDGE-GREEN`, `E-P2-P3-EDGE-PEARL-GREEN`, +`E-P1-DISTANCE-IDENTITY-GREEN`. Test: +`crates/lance-graph-osint/tests/p5_syllogize.rs`. + +--- + +## 2026-07-01 — E-P4-DISCOVERY-EDGE-GREEN — the Aerial+ ARM probe joins the palette: deterministic mining → exact ppm → `arm_to_truth_u8` → `CausalEdge64` wire + +**Status:** FINDING `[G]` (coded; `cargo test --manifest-path +crates/lance-graph-osint/Cargo.toml --test p4_discovery_edge` → 3 passed, +2026-07-01, branch `claude/v3-substrate-migration-review-o0yoxv`). Probe P4; +extends the P1–P3 convergence onto the *discovery* arm. + +**What it proves.** An OSINT dual-use fixture (`militaryUse ⟹ impact`, perfect +correlation over 1000 rows) mined by `arm-discovery`'s `AerialProposer` over a +`MatrixDistance` oracle (the same integer palette shape P1 unified): +1. **Exact integer evidence** — the known rule `militaryUse=yes ⟹ impact=high` + is mined with `antecedent_count=500`, `cooccur=500`, `support_ppm=500_000`, + `confidence_ppm=1_000_000`. No float in the decision path. +2. **Determinism** — two mines over freshly-built identical data + θ produce + byte-identical `Vec` (no seed; the float-free guarantee). +3. **Edge join** — `arm_to_truth_u8(rule, k=1)` → `TruthU8{255, 254}` (freq=1.0, + conf=500·255/501) packs into `CausalEdge64::pack_v2`; the edge's + `frequency_u8()/confidence_u8()` equal the mined truth. Discovery output IS + the edge wire. + +**Why it matters.** P1–P3 closed the distance↔edge↔causality triangle; P4 makes +*discovery* a fourth vertex on the same palette — mined rules become +`CausalEdge64`s indistinguishable from hand-authored ones, and the mining is +reproducible integer-exact. Fence: live-`SpoStore` promotion of mined rules +stays gated on D-ARM-7 (jc Pillar 5, Jirak noise floor under weak dependence, +`I-NOISE-FLOOR-JIRAK`); this probe exercises only the in-memory +mine→truth→edge path. Next: P5 (`syllogize` multi-hop chains). + +**Cross-ref:** `E-P2-P3-EDGE-PEARL-GREEN`, `E-P1-DISTANCE-IDENTITY-GREEN`. +Test: `crates/lance-graph-osint/tests/p4_discovery_edge.rs`. + +--- + +## 2026-07-01 — E-P2-P3-EDGE-PEARL-GREEN — the `CausalEdge64` carrier and `SpoDistances` engine index one palette and share one 3-bit Pearl mask + +**Status:** FINDING `[G]` (coded; `cargo test --manifest-path +crates/lance-graph-osint/Cargo.toml --test p2_p3_edge_pearl` → 3 passed, +2026-07-01, branch `claude/v3-substrate-migration-review-o0yoxv`). Probes P2 + +P3 of the OSINT-substrate convergence roadmap; builds on `E-P1-DISTANCE-IDENTITY-GREEN`. + +**P2 — edge round-trip.** OSINT palette indices `(s,p,o)` → `CausalEdge64::pack_v2` +→ the packed edge's `s_idx()/p_idx()/o_idx()` return them unchanged, `causal_mask()` +returns the Pearl level it was stamped with, `frequency_u8()/confidence_u8()` +round-trip (2048 pairs). The edge carries the palette *coordinate* losslessly; +bridging two edges into `SpoHead`s, `causal_distance(mask=SPO)` equals the plain +per-plane palette sum — the edge and the distance engine index the SAME palette. + +**P3 — Pearl ladder.** The killer receipt: `causal_edge::CausalMask` and the +`mask` byte of `SpoDistances::causal_distance` are the SAME 3-bit convention +(S=0b100, P=0b010, O=0b001) — verified structurally, not by coincidence: each +mask keeps exactly its planes' terms (`S`→s_dist, `PO`→p+o, `SPO`→s+p+o, +`None`→0) over 4096 pairs. Level-2 Intervention (`PO`) projects out the Subject +confounder — strictly less distance than Level-3 Counterfactual (`SPO`) exactly +when the Subject term is non-zero (the strict-drop branch fired on >4000/4096 +pairs). Pearl's do-calculus ladder is literally a mask over the palette planes. + +**Why it matters.** P1 unified the three *distance sources*; P2/P3 unify the +*edge protocol* and the *causal semantics* onto that same palette. The Pearl +level a `CausalEdge64` carries IS the plane-selection mask the distance engine +applies — one enum, one algebra, both crates. This closes the +edge↔distance↔causality triangle on integer-exact shipped code, and is the base +for P4 (ARM discovery emitting `CausalEdge64` from mined rules) and P5 +(`syllogize` multi-hop chains). + +**Cross-ref:** `E-P1-DISTANCE-IDENTITY-GREEN` (P1 keystone); `E-CE64-NAME-COLLISION-DEDUP` +(P0). Test: `crates/lance-graph-osint/tests/p2_p3_edge_pearl.rs`. + +--- + +## 2026-07-01 — E-P1-DISTANCE-IDENTITY-GREEN — the V3 "one causal-distance format" claim holds against shipped code: deepnsm f32 → one palette → nars_engine u16 ≡ arm-discovery u32, byte-exact + +**Status:** FINDING `[G]` (coded; `cargo test --manifest-path +crates/lance-graph-osint/Cargo.toml --test p1_distance_identity` → 2 passed, +2026-07-01, branch `claude/v3-substrate-migration-review-o0yoxv`). The keystone +probe P1 of the OSINT-substrate convergence roadmap (OGAR +`docs/OSINT-SUBSTRATE-REUSE-MAP.md`). + +**What it proves.** Three real distance sources, driven off ONE table: +`deepnsm::codebook::Codebook::subspace_distance_table(s)` (256×256 f32 squared +L2, THE source) → a fixed quantizer → the u16 V3 palette → read by both integer +consumers: `SpoDistances::s_dist` (planner `nars_engine`) and +`MatrixDistance::distance` (arm-discovery oracle, single 256-cardinality +`FeatureSpec` so `code(c)==c`). For 4096 deterministic index pairs (SplitMix64, +no seed entropy): (i) `nars.s_dist(a,b) as u32 == arm.distance(a,b)` — the two +independently-authored lookup formulas agree byte-for-byte; (ii) both equal the +quantized deepnsm entry — the engine table IS the quantized deepnsm subspace +table, not an independent one; (iii) symmetry survives quantization. A second +test pins `causal_distance(mask=0b111) == s_dist+p_dist+o_dist` (the `SpoHead` +entry point → the 8 Pearl projections' composition). + +**Why it matters.** This is integer-exact convergence: the *only* float is the +deepnsm source, collapsed by the quantizer before either consumer sees it (the +"f32 NARS edge" the roadmap allows). The two integer engines are now provably +one metric — if a future edit flips a lookup to col-major, changes the feature +offset, or re-bakes the palette from a different codebook, P1 goes red. This is +the precondition for P2–P3 (edge round-trip, Pearl ladder) and for wiring the +per-plane palette as the single distance the ARM discovery / style-weighting / +kanban-gate paths share. deepnsm already ships the 6×256 (`[u8;6]`) CAM-PQ +shape, so no deepnsm re-bake was needed — the "fix deepnsm→6×(8:8)" the roadmap +hedged on was already done upstream. + +**Cross-ref:** `E-CE64-NAME-COLLISION-DEDUP` (P0, the edge-type dedup this rests +on); OGAR `docs/OSINT-SUBSTRATE-REUSE-MAP.md` (§ "one causal-distance format", +P1 keystone). Test: `crates/lance-graph-osint/tests/p1_distance_identity.rs`. + +--- + +## 2026-07-01 — E-CE64-NAME-COLLISION-DEDUP — thinking-engine's local 8-channel `CausalEdge64` shadowed the canonical `causal_edge::CausalEdge64` in the same file; renamed to `CascadeChannels8` + +**Status:** FINDING `[G]` (coded; `cargo check -p thinking-engine` green +2026-07-01 on branch `claude/v3-substrate-migration-review-o0yoxv`). First code +delta of the OSINT-substrate convergence roadmap (see OGAR +`docs/OSINT-SUBSTRATE-REUSE-MAP.md` P0). Pure rename — zero behavior change. + +**The trap.** `crates/thinking-engine/src/layered.rs` defined a *local* +`pub struct CausalEdge64(pub u64)` — an 8-channel Bach-counterpoint cascade +accumulator (7 constructive + 1 destructive channel) — while the SAME file +*also* imports the canonical SPO carrier `use causal_edge::CausalEdge64 as +SpoEdge`. Two unrelated types shared one name across the crate boundary: the +local one is a scratch cascade register; the canonical one is the frozen +64-bit SPO-palette + NARS⟨f,c⟩ + Pearl-mask wire truth. `domino.rs` re-exported +the local type via `use crate::layered::CausalEdge64`, propagating the shadow. +A reader (or a future baton-handoff) that saw `CausalEdge64` in this crate could +not tell which algebra was in scope without chasing the import — a name +collision that would silently mis-wire the convergence work that leans on the +canonical edge as the one true causal-distance carrier. + +**The fix.** Local type → `CascadeChannels8` (names what it is: 8 packed +u8 channels). Its `to_spo(s,p,o) -> SpoEdge` collapse and `from_spo` inverse are +unchanged — they still target the canonical `causal_edge::CausalEdge64` at the +L3 commit boundary. After the rename the identifier `CausalEdge64` appears in +thinking-engine ONLY as the canonical type (3 sites: the aliased import, the +collapse-boundary doc-comment, the round-trip test import). One name, one +algebra — the precondition for the P1 distance-identity probe +(`causal_distance ≡ arm-discovery oracle ≡ deepnsm`) to reference an +unambiguous edge type. + +**Cross-ref:** OGAR `docs/OSINT-SUBSTRATE-REUSE-MAP.md` (§ "one causal-distance +format", P0/P1); `E-CE64-MB-4` (sole-writer invariant on the canonical edge). + +--- + ## 2026-06-26 — E-V3-BASINS-ARE-MEREOLOGY-NOT-LABELS — the 6 V3 basins (+ relative location) are a structural ADDRESS (mereology / HHTL X;Y coordinates), never flat labels **Status:** FINDING `[H]` (operator directive 2026-06-26; impl in the FMA-V3 + diff --git a/.claude/board/ISSUES.md b/.claude/board/ISSUES.md index fb58536b2..5faa74602 100644 --- a/.claude/board/ISSUES.md +++ b/.claude/board/ISSUES.md @@ -1,5 +1,22 @@ # Issues Log — Open + Resolved (double-entry, append-only) +## 2026-07-01 — ISS-OGAR-OSINT-MIRROR-PENDING — OGAR #145's OSINT mint (+2 to `class_ids::ALL`) breaks the contract-mirror `COUNT_FUSE` on merge; the paired lance-graph mirror rows must land in the same arc + +**Status:** OPEN (tracked) · **Resolution path RULED by operator 2026-07-01: keep the fuse (it IS the dependency contract enforcing OGAR↔lance-graph parallel movement); do NOT pin to a rev — "option 1" is REJECTED. Land the 2 mirror rows + `domains_agree` arm in parallel with OGAR #145 (option 2 / coordinated merge; brief transient red is acceptable — "the fuse is okay for now"). See `E-OGAR-LANCEGRAPH-MOVE-IN-PARALLEL`.** · Owner: OGAR `ogar-vocab` (PR #145) + `lance-graph-contract::ogar_codebook` mirror + `lance-graph-ogar::parity::domains_agree`. Surfaced 2026-07-01 while self-reviewing PR #624 / #145. Same cross-repo-arc shape as `ISS-OGAR-AUTH-MIRROR-DRIFT` (which took medcare CI red) and `ISS-OGAR-GENETICS-MIRROR-PENDING`; cited by `E-CODEBOOK-MINT-IS-A-CROSS-REPO-ARC`. + +**READY PATCH (apply to lance-graph the moment OGAR #145 is on OGAR main; NOT to #624 while OGAR main is still 65 — that breaks #624's own fuse):** in `crates/lance-graph-contract/src/ogar_codebook.rs` add the two rows `("osint_system", 0x0700), ("osint_person", 0x0701)` to `mirror::CODEBOOK` (65 → 67); add the `(O::Osint, C::Osint)` arm to `lance-graph-ogar::parity::domains_agree` (the `ConceptDomain::Osint` enum + `0x07 => Osint` route already exist). Then `mirror::CODEBOOK.len() == ogar_vocab::class_ids::ALL.len()` (67 == 67) restored. + +**The break.** OGAR PR #145 mints `osint_system` (0x0700) + `osint_person` (0x0701) into `ogar_vocab::class_ids::ALL` (+2). `lance-graph-ogar` pins `ogar-vocab = { git = ".../OGAR", branch = "main" }` (tracks main, NOT a rev), and carries the compile-time `COUNT_FUSE`: `assert!(mirror::CODEBOOK.len() == ogar_vocab::class_ids::ALL.len())` (`lance-graph-ogar/src/lib.rs:119`). The contract mirror `lance-graph-contract::ogar_codebook::CODEBOOK` currently has **65 rows with NO osint entries** (it reserved `ConceptDomain::Osint` + the `0x07 => Osint` route + a domain-nibble test, but not the two concept rows). So **the instant #145 merges to OGAR main, `COUNT_FUSE` fires `error[E0080]` in every consumer vendoring `lance-graph-ogar`** — medcare, smb, woa, etc. + +**Why the mirror rows can't just be added to PR #624 now.** #624's `lance-graph-ogar` compiles against OGAR **main**, which still has 65 (osint mint is unmerged on #145). Adding +2 to the mirror now → mirror 67 vs OGAR-main 65 → breaks #624's OWN CI. The two sides are chicken-and-egg across the `branch = "main"` tracking. + +**Resolution (coordinated arc, per the auth precedent):** land in lock-step — +1. OGAR #145 merges to OGAR main (ALL → 67); **at this moment lance-graph main's `COUNT_FUSE` goes red** (known transient, as with the auth mint). +2. Immediately merge a lance-graph change adding the 2 osint rows to `ogar_codebook::CODEBOOK` (`("osint_system", 0x0700)`, `("osint_person", 0x0701)`) + the `(O::Osint, C::Osint)` arm to `lance-graph-ogar::parity::domains_agree` → 67 == 67 restored. + - The `ConceptDomain::Osint` enum + `0x07 => Osint` route already exist in the mirror, so only the 2 CODEBOOK rows + the `domains_agree` arm are missing. + +**Merge-ordering decision needed from operator:** whether to (a) merge #145 + the mirror follow-up back-to-back accepting the brief transient red, (b) hold #145 until the mirror PR is staged, or (c) pin `lance-graph-ogar` to a rev instead of `branch = "main"` to decouple the cadence. Flagged to the operator 2026-07-01. + ## 2026-06-26 — ISS-OGAR-GENETICS-MIRROR-PENDING — contract mirror gained `ConceptDomain::Genetics` (0x0E) ahead of OGAR; the `domains_agree` arm + OGAR side follow **Status:** OPEN (tracked) · Owner: OGAR `ogar-vocab` + `lance-graph-ogar` · Surfaced by: CodeRabbit on #618. The same cross-repo-arc shape as `ISS-OGAR-AUTH-MIRROR-DRIFT` / `E-CODEBOOK-MINT-IS-A-CROSS-REPO-ARC`, but **domain-only** so it does not break in isolation. diff --git a/.claude/board/TECH_DEBT.md b/.claude/board/TECH_DEBT.md index 2573c4895..2f1a38e70 100644 --- a/.claude/board/TECH_DEBT.md +++ b/.claude/board/TECH_DEBT.md @@ -15,6 +15,19 @@ ## Open Debt +### TD-RUNGLEVEL-DUP — `RungLevel` duplicated verbatim in `thinking-engine` vs the canonical `lance-graph-contract` (2026-07-01) + +**Open.** `thinking-engine::cognitive_stack::RungLevel` is a byte-for-byte +duplicate of `lance_graph_contract::cognitive_shader::RungLevel` (0..9: +Surface..Transcendent), and `thinking-engine` does **not** dep +`lance-graph-contract` — so the two can silently drift. Same anti-pattern as +`E-CE64-NAME-COLLISION-DEDUP` (the P0 dedup), but the fix is heavier: it needs +`thinking-engine` to take the zero-dep contract dep and `pub use` the canonical +enum, not a local rename. Canonical = the contract. Surfaced while grounding +`E-RUNG-LADDER-IS-A-DEPENDENCY-STACK` (operator's 1–9 rung-ladder reminder). +Deferred: cross-crate dep addition, out of scope for the convergence-probe +increment. Same class as the resolved `CausalEdge64` shadow. + ### TD-ONTOLOGY-LINT — `lance-graph-ontology` pre-existing clippy (12) on toolchain 1.95 (2026-06-18) `cargo clippy -p lance-graph-ontology -- -D warnings` exits 101 with 12 errors on the pinned 1.95 toolchain — all **pre-existing on `main`** (e.g. `odoo_blueprint/op_emitter.rs:182` is byte-identical on `origin/main`), in `hydrators/owl.rs` (2), `odoo_blueprint/op_emitter.rs` (1), `ttl_parse.rs` (3), + others. Mostly mechanical (`iter_cloned_collect` → `.to_vec()`, etc.). The crate is not in the CI clippy sweep ("CI tests 4 of ~30 crates"), so the debt accumulated un-gated. Surfaced while wiring `class_id_for_guid` (E-OGAR-ONTOLOGY-WIRED-1; `registry.rs` itself is clippy-clean + fmt-clean). Fix is a focused lint pass, out of scope for the wiring increment. Same class as `TD-CAUSAL-EDGE-LINT`. diff --git a/crates/lance-graph-osint/Cargo.lock b/crates/lance-graph-osint/Cargo.lock index db0b537aa..f4ed5d5b9 100644 --- a/crates/lance-graph-osint/Cargo.lock +++ b/crates/lance-graph-osint/Cargo.lock @@ -220,7 +220,7 @@ dependencies = [ [[package]] name = "causal-edge" -version = "0.1.0" +version = "0.2.0" [[package]] name = "cc" @@ -393,6 +393,10 @@ dependencies = [ [[package]] name = "deepnsm" version = "0.1.0" +dependencies = [ + "lance-graph-contract", + "ndarray", +] [[package]] name = "der" @@ -546,6 +550,13 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fractal" +version = "0.1.0" +dependencies = [ + "libm", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -653,6 +664,12 @@ dependencies = [ "wasip3", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "h2" version = "0.4.13" @@ -1015,6 +1032,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lance-graph-arm-discovery" +version = "0.1.0" + +[[package]] +name = "lance-graph-contract" +version = "0.1.0" +dependencies = [ + "glob", + "serde", + "serde_yaml", +] + [[package]] name = "lance-graph-osint" version = "0.1.0" @@ -1022,6 +1052,7 @@ dependencies = [ "bgz17", "causal-edge", "deepnsm", + "lance-graph-arm-discovery", "lance-graph-planner", "ndarray", "p64-bridge", @@ -1039,6 +1070,7 @@ version = "0.1.0" dependencies = [ "bgz17", "causal-edge", + "lance-graph-contract", "ndarray", "p64", "p64-bridge", @@ -1068,6 +1100,12 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libyml" version = "0.0.5" @@ -1192,12 +1230,13 @@ name = "ndarray" version = "0.17.2" dependencies = [ "blake3", + "fractal", "matrixmultiply", "num-complex", "num-integer", "num-traits", "p64", - "phyllotactic-manifold", + "paste", "portable-atomic", "portable-atomic-util", "rawpointer", @@ -1300,7 +1339,7 @@ dependencies = [ name = "p64" version = "0.1.0" dependencies = [ - "phyllotactic-manifold", + "fractal", ] [[package]] @@ -1311,6 +1350,12 @@ dependencies = [ "causal-edge", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pem-rfc7468" version = "1.0.0" @@ -1431,10 +1476,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "phyllotactic-manifold" -version = "0.1.0" - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -2007,6 +2048,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serde_yml" version = "0.0.12" @@ -2473,6 +2527,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/crates/lance-graph-osint/Cargo.toml b/crates/lance-graph-osint/Cargo.toml index 0136ebb50..8e47a5306 100644 --- a/crates/lance-graph-osint/Cargo.toml +++ b/crates/lance-graph-osint/Cargo.toml @@ -41,3 +41,6 @@ default = [] spider-crawl = ["dep:spider", "dep:tokio"] [dev-dependencies] +# P1 convergence probe: the third distance source (discovery oracle) — +# proves SpoDistances (planner) ≡ MatrixDistance (arm-discovery) on one palette. +lance-graph-arm-discovery = { path = "../lance-graph-arm-discovery" } diff --git a/crates/lance-graph-osint/tests/common/mod.rs b/crates/lance-graph-osint/tests/common/mod.rs new file mode 100644 index 000000000..9b42713a7 --- /dev/null +++ b/crates/lance-graph-osint/tests/common/mod.rs @@ -0,0 +1,15 @@ +//! Shared test helpers for the P-series convergence probes. +//! +//! Lives in `tests/common/mod.rs` (not `tests/common.rs`) so cargo does not +//! compile it as its own test binary — it is included by each probe via +//! `mod common;`. + +/// Deterministic PRNG — SplitMix64. No `rand`, no seed entropy; every probe's +/// index stream is byte-identical on every run and every target. +pub fn splitmix64(state: &mut u64) -> u64 { + *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = *state; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) +} diff --git a/crates/lance-graph-osint/tests/p1_distance_identity.rs b/crates/lance-graph-osint/tests/p1_distance_identity.rs new file mode 100644 index 000000000..1f7835ed5 --- /dev/null +++ b/crates/lance-graph-osint/tests/p1_distance_identity.rs @@ -0,0 +1,173 @@ +//! P1 · distance identity — THE convergence keystone. +//! +//! The V3 "one causal-distance format" claim: the 256×256 palette distance is a +//! single metric shared by every layer that reasons over centroid indices. This +//! probe pins that claim to *shipped code* by driving the three real distance +//! sources off ONE table and asserting they return identical values: +//! +//! ```text +//! deepnsm::codebook::Codebook::subspace_distance_table(s) f32 (source) +//! │ quantize (the single "f32 NARS edge" tolerance) +//! ▼ +//! palette[a*256 + b] : u16 the V3 palette +//! ├───────────────────────────────┐ +//! ▼ ▼ +//! SpoDistances.s_dist(a,b) MatrixDistance::distance(a,b) +//! (planner nars_engine, u16) (arm-discovery oracle, u32) +//! ``` +//! +//! Everything downstream (`causal_distance`, the 8 Pearl projections, ARM +//! discovery, style weighting) reads this same lookup. If the two integer +//! consumers disagree on identical bytes, or the engine table is not the +//! quantized deepnsm table, the "one format" claim breaks *here* — and this test +//! goes red before any convergence work built on it can be trusted. +//! +//! Integer-exact: the only float appears in the deepnsm source table, collapsed +//! by a fixed quantizer before either consumer sees it. No seed, no tolerance, +//! bit-identical on every target. + +use deepnsm::codebook::{Codebook, NUM_CENTROIDS}; +use lance_graph_arm_discovery::{rule::Item, CodebookDistance, FeatureSpec, MatrixDistance}; +use lance_graph_planner::cache::nars_engine::{SpoDistances, SpoHead}; + +mod common; +use common::splitmix64; + +/// Build a synthetic deepnsm `Codebook` via its `load_binary` path (its only +/// public constructor keeps `centroids` private). `[6][256][16]` f32 LE = +/// 24 576 floats = 98 304 bytes. Deterministic centroids in `[0, 1)`. +fn synth_codebook() -> Codebook { + const N_F32: usize = 6 * NUM_CENTROIDS * 16; // CODEBOOK_SIZE + let mut s = 0x0517_0700_0517_0701u64; // OSINT-flavoured constant, not entropy + let mut bytes = Vec::with_capacity(N_F32 * 4); + for _ in 0..N_F32 { + // top 24 bits → [0,1) float, deterministic. + let v = ((splitmix64(&mut s) >> 40) as f32) / (1u64 << 24) as f32; + bytes.extend_from_slice(&v.to_le_bytes()); + } + // Unique per call: both tests in this binary run concurrently under the + // parallel harness and each removes its file after loading; a pid+len-only + // path collides and one test can truncate the other's load mid-read. + static CODEBOOK_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let seq = CODEBOOK_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "p1_osint_codebook_{}_{}_{}.bin", + std::process::id(), + seq, + bytes.len() + )); + std::fs::write(&path, &bytes).expect("write synthetic codebook"); + let cb = Codebook::load_binary(&path).expect("load synthetic codebook"); + let _ = std::fs::remove_file(&path); + cb +} + +/// Quantize a 256×256 f32 squared-L2 table into the u16 palette. Fixed +/// max-normalize + round: deterministic function of the input bytes, the single +/// permitted "f32 NARS edge" collapse. +fn quantize(table_f32: &[f32]) -> Vec { + let max = table_f32 + .iter() + .copied() + .fold(0.0f32, f32::max) + .max(1e-6); + table_f32 + .iter() + .map(|&x| ((x / max) * 65_535.0).round().clamp(0.0, 65_535.0) as u16) + .collect() +} + +/// The three planes read from three distinct deepnsm subspaces — the OGAR +/// reading of the 6-byte CAM-PQ path as 3 SPO planes × 2 axes. We take the +/// first axis of each plane: S←subspace 0, P←subspace 2, O←subspace 4. +const S_SUBSPACE: usize = 0; +const P_SUBSPACE: usize = 2; +const O_SUBSPACE: usize = 4; + +#[test] +fn p1_deepnsm_source_equals_both_integer_consumers() { + let cb = synth_codebook(); + + // One f32 source per plane → one quantized palette per plane. + let s_pal = quantize(&cb.subspace_distance_table(S_SUBSPACE)); + let p_pal = quantize(&cb.subspace_distance_table(P_SUBSPACE)); + let o_pal = quantize(&cb.subspace_distance_table(O_SUBSPACE)); + assert_eq!(s_pal.len(), NUM_CENTROIDS * NUM_CENTROIDS); + + // Consumer 1 — planner nars_engine (u16 tables, direct lookup). + let nars = SpoDistances { + s_table: s_pal.clone(), + p_table: p_pal.clone(), + o_table: o_pal.clone(), + }; + + // Consumer 2 — arm-discovery oracle (u32 table, single 256-cardinality + // feature so `code(Item{feature:0,category:c}) == c`). Feed the SAME S-plane + // palette, widened to u32. + let spec = FeatureSpec::new(vec![NUM_CENTROIDS as u32]); + let s_pal_u32: Vec = s_pal.iter().map(|&v| v as u32).collect(); + let arm = MatrixDistance::new(&spec, s_pal_u32); + + // N deterministic index pairs. + let mut s = 0xDEAD_BEEF_0000_0700u64; + let n_pairs = 4096; + for _ in 0..n_pairs { + let a = (splitmix64(&mut s) % NUM_CENTROIDS as u64) as u8; + let b = (splitmix64(&mut s) % NUM_CENTROIDS as u64) as u8; + + let nars_s = nars.s_dist(a, b); + let arm_s = arm.distance(Item::new(0, a as u32), Item::new(0, b as u32)); + let src = s_pal[a as usize * NUM_CENTROIDS + b as usize]; + + // (i) the two integer consumers agree byte-for-byte on one palette. + assert_eq!( + nars_s as u32, arm_s, + "consumer divergence at ({a},{b}): nars_engine u16={nars_s} vs arm-discovery u32={arm_s}" + ); + // (ii) both equal the quantized deepnsm source entry — the engine table + // IS the quantized deepnsm subspace table, not an independent one. + assert_eq!( + nars_s, src, + "engine table is not the quantized deepnsm source at ({a},{b})" + ); + // (iii) squared-L2 is symmetric; the palette inherits it. + assert_eq!( + nars.s_dist(a, b), + nars.s_dist(b, a), + "palette lost the source's symmetry at ({a},{b})" + ); + } +} + +/// `causal_distance` over the full SPO mask is exactly the sum of the three +/// per-plane palette lookups — the composition the 8 Pearl projections and the +/// downstream style weighting are built on. (P3 tests the masked *subsets*; this +/// only pins the all-planes sum so P1 covers the `SpoHead` entry point too.) +#[test] +fn p1_causal_distance_is_the_plane_sum() { + let cb = synth_codebook(); + let nars = SpoDistances { + s_table: quantize(&cb.subspace_distance_table(S_SUBSPACE)), + p_table: quantize(&cb.subspace_distance_table(P_SUBSPACE)), + o_table: quantize(&cb.subspace_distance_table(O_SUBSPACE)), + }; + + let mut s = 0x0517_0700_DEAD_0000u64; + for _ in 0..1024 { + let head = |seed: &mut u64| { + let mut h = SpoHead::zero(); + h.s_idx = (splitmix64(seed) % 256) as u8; + h.p_idx = (splitmix64(seed) % 256) as u8; + h.o_idx = (splitmix64(seed) % 256) as u8; + h + }; + let a = head(&mut s); + let b = head(&mut s); + + let expected = nars.s_dist(a.s_idx, b.s_idx) as u32 + + nars.p_dist(a.p_idx, b.p_idx) as u32 + + nars.o_dist(a.o_idx, b.o_idx) as u32; + // mask 0b111 = all three planes active (S=0b100, P=0b010, O=0b001). + assert_eq!(nars.causal_distance(&a, &b, 0b111), expected); + } +} diff --git a/crates/lance-graph-osint/tests/p2_p3_edge_pearl.rs b/crates/lance-graph-osint/tests/p2_p3_edge_pearl.rs new file mode 100644 index 000000000..b1c81f1e8 --- /dev/null +++ b/crates/lance-graph-osint/tests/p2_p3_edge_pearl.rs @@ -0,0 +1,174 @@ +//! P2 · edge round-trip · and · P3 · Pearl ladder. +//! +//! P1 proved the 256×256 palette is one metric across three distance sources. +//! P2/P3 prove the *edge* carrier (`causal_edge::CausalEdge64`) and the +//! *distance* engine (`SpoDistances`) index that SAME palette, and that the +//! 3-bit Pearl mask means the same thing on both sides. +//! +//! **P2 — edge round-trip.** OSINT AIRO palette indices `(s,p,o)` → `pack_v2` → +//! the packed edge's `s_idx()/p_idx()/o_idx()` return them unchanged, and its +//! `causal_mask()` returns the Pearl level it was stamped with. The edge carries +//! the palette *coordinate* losslessly; `causal_distance` of the two edges' +//! heads equals the plain per-plane palette sum. +//! +//! **P3 — Pearl ladder.** `CausalMask` (causal-edge) and the `mask` byte +//! (`SpoDistances::causal_distance`) share ONE bit convention — S=0b100, +//! P=0b010, O=0b001 — verified by asserting the masked distance drops exactly +//! the excluded plane's term. `PO` (Level-2 Intervention) projects out the +//! Subject confounder: strictly less distance than `SPO` (Level-3 +//! Counterfactual) whenever the Subject term is non-zero. +//! +//! Integer-exact, deterministic, no external input. + +use causal_edge::{CausalEdge64, CausalMask, PlasticityState}; +use lance_graph_planner::cache::nars_engine::{SpoDistances, SpoHead}; + +mod common; +use common::splitmix64; + +/// A synthetic symmetric palette with a zero diagonal and strictly-positive +/// off-diagonal, so distinct indices always have a non-zero plane distance +/// (needed to exercise the `PO < SPO` confounder-projection assertion). +fn synth_palette(seed: u64) -> Vec { + let mut t = vec![0u16; 256 * 256]; + let mut s = seed; + for a in 0..256usize { + for b in (a + 1)..256usize { + let v = 1 + (splitmix64(&mut s) % 60_000) as u16; // 1..=60000 + t[a * 256 + b] = v; + t[b * 256 + a] = v; + } + } + t +} + +/// Bridge a packed edge into the distance engine's head: the SAME palette +/// indices, nothing else. This is the join point P2 certifies. +fn head_of(e: CausalEdge64) -> SpoHead { + let mut h = SpoHead::zero(); + h.s_idx = e.s_idx(); + h.p_idx = e.p_idx(); + h.o_idx = e.o_idx(); + h +} + +fn nars() -> SpoDistances { + SpoDistances { + s_table: synth_palette(0x0700_0001), + p_table: synth_palette(0x0700_0002), + o_table: synth_palette(0x0700_0003), + } +} + +#[test] +fn p2_edge_round_trips_palette_indices_and_mask() { + let mut s = 0x0517_0701_0000_0000u64; // osint_person flavour + for _ in 0..2048 { + let si = (splitmix64(&mut s) % 256) as u8; + let pi = (splitmix64(&mut s) % 256) as u8; + let oi = (splitmix64(&mut s) % 256) as u8; + let freq = (splitmix64(&mut s) % 256) as u8; + let conf = (splitmix64(&mut s) % 256) as u8; + + let edge = CausalEdge64::pack_v2( + si, + pi, + oi, + freq, + conf, + CausalMask::SPO, + 0, + PlasticityState::ALL_FROZEN, + ); + + // The palette coordinate survives pack/unpack unchanged. + assert_eq!(edge.s_idx(), si, "s_idx round-trip"); + assert_eq!(edge.p_idx(), pi, "p_idx round-trip"); + assert_eq!(edge.o_idx(), oi, "o_idx round-trip"); + // The Pearl level the edge was stamped with round-trips. + assert_eq!(edge.causal_mask(), CausalMask::SPO, "causal_mask round-trip"); + // Frequency/confidence quantized bytes survive too. + assert_eq!(edge.frequency_u8(), freq, "frequency_u8 round-trip"); + assert_eq!(edge.confidence_u8(), conf, "confidence_u8 round-trip"); + } +} + +#[test] +fn p2_causal_distance_of_two_edges_is_the_plane_sum() { + let d = nars(); + let mut s = 0x0517_0701_DEAD_0000u64; + for _ in 0..2048 { + let mk = |seed: &mut u64| { + CausalEdge64::pack_v2( + (splitmix64(seed) % 256) as u8, + (splitmix64(seed) % 256) as u8, + (splitmix64(seed) % 256) as u8, + 128, + 200, + CausalMask::SPO, + 0, + PlasticityState::ALL_FROZEN, + ) + }; + let a = head_of(mk(&mut s)); + let b = head_of(mk(&mut s)); + + let expected = d.s_dist(a.s_idx, b.s_idx) as u32 + + d.p_dist(a.p_idx, b.p_idx) as u32 + + d.o_dist(a.o_idx, b.o_idx) as u32; + assert_eq!(d.causal_distance(&a, &b, CausalMask::SPO as u8), expected); + } +} + +#[test] +fn p3_pearl_masks_drop_exactly_the_excluded_plane() { + let d = nars(); + let mut s = 0x0517_0701_BEEF_0000u64; + let mut saw_strict_drop = 0u32; + for _ in 0..4096 { + let mk = |seed: &mut u64| { + head_of(CausalEdge64::pack_v2( + (splitmix64(seed) % 256) as u8, + (splitmix64(seed) % 256) as u8, + (splitmix64(seed) % 256) as u8, + 128, + 200, + CausalMask::PO, // Level-2 Intervention on the osint_person pair + 0, + PlasticityState::ALL_FROZEN, + )) + }; + let a = mk(&mut s); + let b = mk(&mut s); + + let s_d = d.s_dist(a.s_idx, b.s_idx) as u32; + let p_d = d.p_dist(a.p_idx, b.p_idx) as u32; + let o_d = d.o_dist(a.o_idx, b.o_idx) as u32; + + // The bit convention is shared: each mask keeps exactly its planes. + assert_eq!(d.causal_distance(&a, &b, CausalMask::S as u8), s_d); + assert_eq!(d.causal_distance(&a, &b, CausalMask::PO as u8), p_d + o_d); + assert_eq!( + d.causal_distance(&a, &b, CausalMask::SPO as u8), + s_d + p_d + o_d + ); + assert_eq!(d.causal_distance(&a, &b, CausalMask::None as u8), 0); + + // Level-2 (PO) projects out the Subject confounder → strictly less than + // Level-3 (SPO) exactly when the Subject term is non-zero. + let po = d.causal_distance(&a, &b, CausalMask::PO as u8); + let spo = d.causal_distance(&a, &b, CausalMask::SPO as u8); + if s_d > 0 { + assert!(po < spo, "PO must drop the non-zero Subject term"); + saw_strict_drop += 1; + } else { + assert_eq!(po, spo); + } + } + // The synthetic palette has a zero diagonal only; distinct subjects give + // s_d>0, so the strict-drop branch must fire on the vast majority of pairs. + assert!( + saw_strict_drop > 4000, + "expected the confounder-projection branch to dominate, saw {saw_strict_drop}" + ); +} diff --git a/crates/lance-graph-osint/tests/p3b_spo_questions_amortized.rs b/crates/lance-graph-osint/tests/p3b_spo_questions_amortized.rs new file mode 100644 index 000000000..7b85911c2 --- /dev/null +++ b/crates/lance-graph-osint/tests/p3b_spo_questions_amortized.rs @@ -0,0 +1,146 @@ +//! P3b · the SPO 2³ decomposition gives questions AND candidates inherently, +//! amortized all-at-once on the same cached tile data. +//! +//! `SpoDistances::all_projections` reads the three per-plane palette cells for +//! one `(a,b)` pair — `s_dist`, `p_dist`, `o_dist` — and produces **all eight** +//! Pearl projections (the 2³ subsets of {S,P,O}) by masked summation. Three +//! cache reads → eight causal questions. This probe pins two properties the +//! in-crate `test_all_projections` (subset-sum arithmetic) does not: +//! +//! 1. **Amortization** — the 8-vector is a pure function of the 3 scalar plane +//! distances; no mask costs an extra table read. `all_projections` is the +//! Morton-tile "2³ all at once on the same data in cache." +//! 2. **Questions AND candidates inherently** — the 8 masks are 8 *distinct* +//! causal questions (prior / 3 marginals / confounder / association / +//! intervention / counterfactual), and because they weight the planes +//! differently, the SAME two candidates can rank **oppositely** under +//! Association (`SO`) vs Intervention (`PO`). The decomposition is not +//! redundant: each rung is its own retrieval. +//! +//! Integer-exact. `lance-graph-planner` only; no new deps. + +use lance_graph_planner::cache::nars_engine::{ + SpoDistances, SpoHead, ALL_MASKS, MASK_NONE, MASK_O, MASK_P, MASK_PO, MASK_S, MASK_SO, + MASK_SP, MASK_SPO, +}; + +fn head(s: u8, p: u8, o: u8) -> SpoHead { + let mut h = SpoHead::zero(); + h.s_idx = s; + h.p_idx = p; + h.o_idx = o; + h +} + +/// Reconstruct the 8 projections from ONLY the 3 scalar plane distances, by the +/// mask's popcount-subset sum — the amortized computation the engine performs +/// once the 3 cells are in cache. +fn expected_from_three(s_d: u32, p_d: u32, o_d: u32) -> [u32; 8] { + let mut out = [0u32; 8]; + for (i, &mask) in ALL_MASKS.iter().enumerate() { + let mut d = 0; + if mask & MASK_S != 0 { + d += s_d; + } + if mask & MASK_P != 0 { + d += p_d; + } + if mask & MASK_O != 0 { + d += o_d; + } + out[i] = d; + } + out +} + +#[test] +fn p3b_eight_questions_are_a_pure_function_of_three_reads() { + // One (a,b) pair; three plane cells set. + let mut d = SpoDistances::new_zero(); + let (s_d, p_d, o_d) = (17u32, 101u32, 255u32); + d.s_table[0 * 256 + 1] = s_d as u16; + d.p_table[0 * 256 + 1] = p_d as u16; + d.o_table[0 * 256 + 1] = o_d as u16; + + let a = head(0, 0, 0); + let b = head(1, 1, 1); + + let proj = d.all_projections(&a, &b); + // The whole 8-vector is determined by the 3 scalar reads — amortization. + assert_eq!(proj, expected_from_three(s_d, p_d, o_d)); + + // Named taxonomy sanity: prior=0, the three marginals, and the ladder tops. + assert_eq!(proj[0], 0, "MASK_NONE = prior"); + assert_eq!(proj[7], s_d + p_d + o_d, "MASK_SPO = counterfactual = full sum"); + let idx = |m: u8| ALL_MASKS.iter().position(|&x| x == m).unwrap(); + assert_eq!(proj[idx(MASK_S)], s_d); + assert_eq!(proj[idx(MASK_P)], p_d); + assert_eq!(proj[idx(MASK_O)], o_d); + assert_eq!(proj[idx(MASK_SP)], s_d + p_d); + assert_eq!(proj[idx(MASK_SO)], s_d + o_d, "Association"); + assert_eq!(proj[idx(MASK_PO)], p_d + o_d, "Intervention (S projected out)"); +} + +#[test] +fn p3b_ladder_is_monotone_a_superset_question_never_undercounts() { + // Adding a plane to a mask can never decrease its projection: the 2³ lattice + // is monotone, so the counterfactual (SPO) upper-bounds every sub-question. + let mut d = SpoDistances::new_zero(); + d.s_table[0 * 256 + 1] = 40; + d.p_table[0 * 256 + 1] = 70; + d.o_table[0 * 256 + 1] = 25; + let proj = d.all_projections(&head(0, 0, 0), &head(1, 1, 1)); + let idx = |m: u8| ALL_MASKS.iter().position(|&x| x == m).unwrap(); + + // A ⊆ B (as bitsets) ⇒ proj[A] ≤ proj[B]. Spot-check the ladder spine. + assert!(proj[idx(MASK_NONE)] <= proj[idx(MASK_S)]); + assert!(proj[idx(MASK_S)] <= proj[idx(MASK_SO)]); + assert!(proj[idx(MASK_SO)] <= proj[idx(MASK_SPO)]); + assert!(proj[idx(MASK_O)] <= proj[idx(MASK_PO)]); + assert!(proj[idx(MASK_PO)] <= proj[idx(MASK_SPO)]); + assert_eq!( + proj[idx(MASK_SPO)], + *proj.iter().max().unwrap(), + "counterfactual dominates all sub-questions" + ); +} + +#[test] +fn p3b_association_and_intervention_rank_candidates_oppositely() { + // The decomposition yields CANDIDATES inherently: the same two candidates + // seen from a fixed context rank oppositely under Association (SO) vs + // Intervention (PO), because PO projects out the Subject plane. + let mut d = SpoDistances::new_zero(); + let ctx = head(0, 0, 0); + let x = head(1, 1, 1); // near on S, far on P + let y = head(2, 2, 2); // far on S, near on P + + // Lookup is table[a_idx*256 + b_idx]; we call all_projections(candidate, ctx=0), + // so set the (candidate, 0) cells. + // Subject plane: X near, Y far. + d.s_table[1 * 256 + 0] = 10; + d.s_table[2 * 256 + 0] = 90; + // Predicate plane: X far, Y near. + d.p_table[1 * 256 + 0] = 90; + d.p_table[2 * 256 + 0] = 10; + // Object plane: equal — so it cannot decide the ordering. + d.o_table[1 * 256 + 0] = 50; + d.o_table[2 * 256 + 0] = 50; + + let px = d.all_projections(&x, &ctx); + let py = d.all_projections(&y, &ctx); + let idx = |m: u8| ALL_MASKS.iter().position(|&x| x == m).unwrap(); + + // Association (S+O): X wins (its Subject match dominates). + assert!( + px[idx(MASK_SO)] < py[idx(MASK_SO)], + "under Association, candidate X is nearer" + ); + // Intervention (P+O, Subject confounder projected out): Y wins. + assert!( + py[idx(MASK_PO)] < px[idx(MASK_PO)], + "under Intervention, candidate Y is nearer — the Subject match no longer counts" + ); + // Same data, same cache line, opposite winners: the 8 masks are 8 real + // questions, each with its own candidate answer — not one distance in 8 hats. +} diff --git a/crates/lance-graph-osint/tests/p4_discovery_edge.rs b/crates/lance-graph-osint/tests/p4_discovery_edge.rs new file mode 100644 index 000000000..3461f412b --- /dev/null +++ b/crates/lance-graph-osint/tests/p4_discovery_edge.rs @@ -0,0 +1,136 @@ +//! P4 · discovery determinism — ARM mining joins the palette. +//! +//! P1–P3 unified the distance sources, the edge carrier, and the Pearl-mask +//! semantics onto one 256×256 palette. P4 joins the *discovery* arm: the +//! Aerial+ codebook probe (`arm-discovery`) mines association rules off the same +//! integer oracle, and its output translates into the same `CausalEdge64` wire. +//! +//! The OSINT fixture encodes a dual-use signal — `militaryUse ⟹ impact` — as a +//! perfect correlation over 1000 rows. The probe must: +//! 1. mine the known rule with **exact** integer support/confidence ppm; +//! 2. be **deterministic** — same data + same θ ⇒ byte-identical rules, no +//! seed (the float-free discovery-path guarantee); +//! 3. translate `arm_to_truth_u8` → `CausalEdge64::pack_v2`, and the packed +//! edge's `frequency_u8()/confidence_u8()` equal the mined `TruthU8`. +//! +//! Live-`SpoStore` promotion stays gated on D-ARM-7 (jc Pillar 5, Jirak floor); +//! this probe exercises the pure in-memory mine → truth → edge path only. + +use causal_edge::{CausalEdge64, CausalMask, PlasticityState}; +use lance_graph_arm_discovery::{ + arm_to_truth_u8, AerialParams, AerialProposer, Dataset, FeatureSpec, Item, MatrixDistance, + NARS_PERSONALITY_K, +}; + +// Two OSINT features, cardinality 2 each: militaryUse ∈ {no,yes}, impact ∈ {low,high}. +// Global slots (block offsets): militaryUse=no→0, yes→1; impact=low→2, high→3. +const MILITARY_USE: u32 = 0; // feature index +const IMPACT: u32 = 1; // feature index + +/// Oracle: militaryUse=yes(slot 1) is near impact=high(slot 3); militaryUse=no +/// near impact=low. Off-diagonal defaults to "far" so an unset cell never reads +/// as nearest. dim = 4. +fn oracle(spec: &FeatureSpec) -> MatrixDistance { + let mut table = vec![50u32; 16]; + for d in 0..4 { + table[d * 4 + d] = 0; + } + let mut set = |a: usize, b: usize, v: u32| { + table[a * 4 + b] = v; + table[b * 4 + a] = v; + }; + set(0, 2, 1); // militaryUse=no ~ impact=low + set(1, 3, 1); // militaryUse=yes ~ impact=high + MatrixDistance::new(spec, table) +} + +/// 1000 rows, perfect dual-use correlation: militaryUse == impact. +/// → militaryUse=yes ⟹ impact=high with confidence 1.0, support 0.5. +fn osint_dataset(spec: &FeatureSpec) -> Dataset { + let rows: Vec> = (0..1000) + .map(|i| { + let mu = (i % 2) as u32; // 500 no, 500 yes + vec![mu, mu] // impact == militaryUse + }) + .collect(); + Dataset::new(spec.clone(), rows) +} + +fn params() -> AerialParams { + AerialParams { + theta: 2, + max_antecedent: 1, + min_support_ppm: 50_000, + min_confidence_ppm: 700_000, + } +} + +#[test] +fn p4_mines_the_known_dual_use_rule_with_exact_ppm() { + let spec = FeatureSpec::new(vec![2, 2]); + let rules = AerialProposer::new(osint_dataset(&spec), oracle(&spec), params()).mine(); + assert!(!rules.is_empty(), "probe must mine at least one rule"); + + // The known rule: militaryUse=yes ⟹ impact=high. + let known = rules + .iter() + .find(|r| { + r.antecedent == vec![Item::new(MILITARY_USE, 1)] + && r.consequent == vec![Item::new(IMPACT, 1)] + }) + .expect("militaryUse=yes ⟹ impact=high must be mined"); + + // Exact integer evidence: 500 rows have militaryUse=yes, all 500 also + // impact=high; window = 1000. + assert_eq!(known.antecedent_count, 500, "|militaryUse=yes|"); + assert_eq!(known.cooccur, 500, "|militaryUse=yes ∧ impact=high|"); + assert_eq!(known.window, 1000, "window"); + assert_eq!(known.support_ppm(), 500_000, "support = 500/1000"); + assert_eq!(known.confidence_ppm(), 1_000_000, "confidence = 500/500"); +} + +#[test] +fn p4_mining_is_deterministic_no_seed() { + let spec = FeatureSpec::new(vec![2, 2]); + let r1 = AerialProposer::new(osint_dataset(&spec), oracle(&spec), params()).mine(); + let r2 = AerialProposer::new(osint_dataset(&spec), oracle(&spec), params()).mine(); + assert_eq!(r1, r2, "same data + same θ ⇒ byte-identical rules"); +} + +#[test] +fn p4_rule_truth_packs_into_causal_edge() { + let spec = FeatureSpec::new(vec![2, 2]); + let rules = AerialProposer::new(osint_dataset(&spec), oracle(&spec), params()).mine(); + let known = rules + .iter() + .find(|r| { + r.antecedent == vec![Item::new(MILITARY_USE, 1)] + && r.consequent == vec![Item::new(IMPACT, 1)] + }) + .expect("known rule present"); + + // Stage B: integer ARM evidence → quantized NARS truth (the CausalEdge64 wire). + let truth = arm_to_truth_u8(known, NARS_PERSONALITY_K); + // frequency = cooccur*255/antecedent_count = 500*255/500 = 255 (P=1.0). + assert_eq!(truth.frequency, 255); + // confidence = m*255/(m+k) = 500*255/501 = 254. + assert_eq!(truth.confidence, ((500u64 * 255) / 501) as u8); + + // The mined rule becomes a CausalEdge64 on the same palette: subject = + // militaryUse slot, predicate = "implies" (fixed OSINT vocab slot), object = + // impact slot. Palette indices are illustrative here (P1 pinned their metric); + // P4's teeth are that the discovery truth round-trips into the edge wire. + let edge = CausalEdge64::pack_v2( + 1, // militaryUse=yes palette index + 0, // "implies" predicate slot + 3, // impact=high palette index + truth.frequency, + truth.confidence, + CausalMask::SPO, + 0, + PlasticityState::ALL_FROZEN, + ); + assert_eq!(edge.frequency_u8(), truth.frequency, "freq → edge wire"); + assert_eq!(edge.confidence_u8(), truth.confidence, "conf → edge wire"); + assert_eq!(edge.causal_mask(), CausalMask::SPO); +} diff --git a/crates/lance-graph-osint/tests/p5_syllogize.rs b/crates/lance-graph-osint/tests/p5_syllogize.rs new file mode 100644 index 000000000..a195bd19c --- /dev/null +++ b/crates/lance-graph-osint/tests/p5_syllogize.rs @@ -0,0 +1,84 @@ +//! P5 · syllogize chain — multi-hop reasoning on the palette edge. +//! +//! P4 made a single mined implication a `CausalEdge64`. P5 composes two edges +//! that share a term into a derived conclusion edge — NAL transitive deduction +//! `is_a(A,B) ∧ is_a(B,C) ⊢ is_a(A,C)`, carried entirely on the SPO palette. +//! +//! This is the `part_of`/`is_a` chaining the OSINT ontology needs (an actor +//! `part_of` a unit `part_of` a command ⊢ `part_of` the command; a system +//! `is_a` drone `is_a` dual-use-tech ⊢ dual-use-tech). The predicate is a +//! carried placeholder slot, so `is_a` and `part_of` chain by the identical +//! mechanism — the middle term `M` (shared palette index) is consumed, the two +//! outer terms survive, and the NARS deduction truth-function composes the +//! premise truths. `Figure::Chain`, `InferenceType::Deduction` (mantissa +1), +//! Pearl mask = AND of the premises. +//! +//! Integer-exact after the single f32 truth-composition (the NARS edge). + +use causal_edge::{CausalEdge64, CausalMask, Figure, PlasticityState}; + +/// Palette slots for the OSINT ontology chain. +const A: u8 = 0x10; // e.g. a specific drone model +const B: u8 = 0x20; // its class: "reconnaissance UAV" +const C: u8 = 0x30; // its category: "dual-use technology" +const IS_A: u8 = 0x07; // predicate placeholder slot (shared) + +/// Build an `is_a`-style premise edge on the palette with the given truth bytes. +fn premise(s: u8, o: u8, f: u8, c: u8) -> CausalEdge64 { + CausalEdge64::pack_v2(s, IS_A, o, f, c, CausalMask::SPO, 0, PlasticityState::ALL_FROZEN) +} + +#[test] +fn p5_is_a_chain_deduces_the_transitive_conclusion() { + // is_a(A,B) ∧ is_a(B,C). o1 == s2 == B ⇒ Figure::Chain. + let ab = premise(A, B, 255, 204); // f=1.0, c=0.8 + let bc = premise(B, C, 255, 204); // f=1.0, c=0.8 + + assert_eq!(ab.figure(bc), Some(Figure::Chain), "o1==s2==B ⇒ Chain"); + + let syl = ab.syllogize(bc).expect("shared middle term ⇒ a syllogism"); + assert_eq!(syl.figure, Figure::Chain); + + // Conclusion is is_a(A,C): the middle term B is consumed, outer terms survive. + let concl = syl.conclusion; + assert_eq!(concl.s_idx(), A, "conclusion subject = A"); + assert_eq!(concl.o_idx(), C, "conclusion object = C"); + + // Deduction truth: f = f1·f2 = 1.0 → 255; c = f1·f2·c1·c2 = 1·1·0.8·0.8 = 0.64 + // → round(0.64·255) = 163. + assert_eq!(concl.frequency_u8(), 255, "deduction f = f1·f2"); + assert_eq!(concl.confidence_u8(), 163, "deduction c = f1·f2·c1·c2"); + + // Deduction stamps a positive (forward-chain) mantissa. + assert_eq!(concl.inference_mantissa(), 1, "Deduction ⇒ mantissa +1"); + + // Pearl mask = AND of the premise masks: SPO & SPO = SPO. + assert_eq!(concl.causal_mask(), CausalMask::SPO); +} + +#[test] +fn p5_no_shared_term_is_not_a_syllogism() { + // is_a(A,B) and is_a(C,A-adjacent) with no shared S/O term ⇒ None. + let ab = premise(A, B, 255, 200); + let unrelated = premise(0x40, 0x50, 255, 200); + assert_eq!(ab.figure(unrelated), None, "no shared term ⇒ no figure"); + assert!(ab.syllogize(unrelated).is_none()); +} + +#[test] +fn p5_identical_statement_is_revision_not_syllogism() { + // Same S and O ⇒ NARS revision territory, not a syllogism. + let e1 = premise(A, B, 255, 100); + let e2 = premise(A, B, 255, 200); + assert_eq!(e1.figure(e2), None, "identical S,O ⇒ revision, not syllogism"); + assert!(e1.syllogize(e2).is_none()); +} + +#[test] +fn p5_chain_is_deterministic() { + let ab = premise(A, B, 255, 204); + let bc = premise(B, C, 255, 204); + let c1 = ab.syllogize(bc).unwrap().conclusion; + let c2 = ab.syllogize(bc).unwrap().conclusion; + assert_eq!(c1, c2, "syllogize is a pure function of its premises"); +} diff --git a/crates/thinking-engine/src/domino.rs b/crates/thinking-engine/src/domino.rs index f9e788196..f46ba044e 100644 --- a/crates/thinking-engine/src/domino.rs +++ b/crates/thinking-engine/src/domino.rs @@ -6,7 +6,7 @@ //! ```text //! Q1 = perturb(tokens) → 3σ top-K focus //! context = full row → NARS truth filter (freq, conf) -//! promoted + contradicts → CausalEdge64 +//! promoted + contradicts → CascadeChannels8 //! V1 = focus + promoted → feed as Q2 //! Q2 = V1 survivors → 3σ top-K focus //! ...cascade until NARS confidence saturates @@ -17,7 +17,7 @@ //! the few genuinely strong connections per row. use crate::engine::ThinkingEngine; -use crate::layered::CausalEdge64; +use crate::layered::CascadeChannels8; // Bach counterpoint channel indices pub const CH_BECOMES: u8 = 0; // voice crossing — identity shifts @@ -87,7 +87,7 @@ pub struct StageResult { pub struct Transition { pub from_atom: u16, pub to_atom: u16, - pub edge: CausalEdge64, + pub edge: CascadeChannels8, pub dissonance: f32, // 0.0 = consonant, 1.0 = fully dissonant } @@ -131,7 +131,7 @@ pub fn classify_transition( 1.0 }; - let mut edge = CausalEdge64::new(); + let mut edge = CascadeChannels8::new(); if above_floor > 0.8 { // Very similar: parallel motion (SUPPORTS) diff --git a/crates/thinking-engine/src/engine.rs b/crates/thinking-engine/src/engine.rs index dcdc3028c..adc13a26a 100644 --- a/crates/thinking-engine/src/engine.rs +++ b/crates/thinking-engine/src/engine.rs @@ -123,7 +123,7 @@ //! CAUSE: Contradicting attractors of equal strength //! FIX 1: This IS the 7+1 contradiction signal — use it, don't suppress it //! FIX 2: Increase max_cycles (10 → 20) -//! FIX 3: Check if CausalEdge64 CONTRADICTS channel resolves it at L2 +//! FIX 3: Check if CascadeChannels8 CONTRADICTS channel resolves it at L2 //! //! SYMPTOM: f32 precision insufficient (energy values collapse to 0) //! CAUSE: 4096 atoms × 10 cycles, normalization kills small values diff --git a/crates/thinking-engine/src/layered.rs b/crates/thinking-engine/src/layered.rs index 04d5ff93f..2b56c80d5 100644 --- a/crates/thinking-engine/src/layered.rs +++ b/crates/thinking-engine/src/layered.rs @@ -1,4 +1,4 @@ -//! Layered thinking cascade with CausalEdge64 upstream propagation. +//! Layered thinking cascade with CascadeChannels8 upstream propagation. //! //! Three tiers of ThinkingEngine, connected by causal edges: //! @@ -6,7 +6,7 @@ //! L1 (small, routing) ──edges──► L2 (mid, role resonance) ──edges──► L3 (full thought) //! ``` //! -//! CausalEdge64 packs 8 channels (7 constructive + 1 destructive) into a u64. +//! CascadeChannels8 packs 8 channels (7 constructive + 1 destructive) into a u64. //! Each channel is one byte (0-255). Constructive channels add energy; //! the CONTRADICTS channel subtracts energy. @@ -15,7 +15,7 @@ use crate::engine::ThinkingEngine; use causal_edge::{CausalEdge64 as SpoEdge, CausalMask}; // ═══════════════════════════════════════════════════════════════════════════ -// CausalEdge64: packed u64 with 7 constructive + 1 destructive channel +// CascadeChannels8: packed u64 with 7 constructive + 1 destructive channel // ═══════════════════════════════════════════════════════════════════════════ /// Channel indices. @@ -30,7 +30,7 @@ pub const CHANNEL_ABSTRACTS: u8 = 5; pub const CHANNEL_RELATES: u8 = 6; pub const CHANNEL_CONTRADICTS: u8 = 7; -/// CausalEdge64: packed u64 with 7 constructive + 1 destructive channel. +/// CascadeChannels8: packed u64 with 7 constructive + 1 destructive channel. /// /// Layout (little-endian byte order within the u64): /// bits 0..7 = channel 0 (BECOMES) @@ -43,12 +43,12 @@ pub const CHANNEL_CONTRADICTS: u8 = 7; /// bits 56..63 = channel 7 (CONTRADICTS) #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct CausalEdge64(pub u64); +pub struct CascadeChannels8(pub u64); -impl CausalEdge64 { +impl CascadeChannels8 { /// Create a zero edge (no causal strength on any channel). pub fn new() -> Self { - CausalEdge64(0) + CascadeChannels8(0) } /// Set a channel's value (u8, 0..=255). `channel` must be 0..=7. @@ -103,7 +103,7 @@ impl CausalEdge64 { /// Convenience: create an edge with CAUSES channel set to `strength`. /// /// Source and target are NOT stored inside the u64 (all 64 bits are channels). - /// They are carried alongside as the tuple key `(u16, CausalEdge64)`. + /// They are carried alongside as the tuple key `(u16, CascadeChannels8)`. pub fn with_source_target(_source: u16, _target: u16, strength: u8) -> Self { let mut e = Self::new(); e.set_channel_u8(CHANNEL_CAUSES, strength); @@ -111,7 +111,7 @@ impl CausalEdge64 { } } -impl Default for CausalEdge64 { +impl Default for CascadeChannels8 { fn default() -> Self { Self::new() } @@ -120,11 +120,11 @@ impl Default for CausalEdge64 { // ═══════════════════════════════════════════════════════════════════════════ // Transcoder impl block (D-CSV-9, Option R-3 per plan §5 L-12) // -// Collapses the 8-channel cascade form into one SPO-palette CausalEdge64 +// Collapses the 8-channel cascade form into one SPO-palette causal_edge::CausalEdge64 // at the L3 commit boundary, and provides the inverse for round-trip tests. // ═══════════════════════════════════════════════════════════════════════════ -impl CausalEdge64 { +impl CascadeChannels8 { /// 8 channel labels for diagnostics + tests. pub const CHANNEL_NAMES: [&'static str; 8] = [ "BECOMES", "CAUSES", "SUPPORTS", "REFINES", @@ -190,7 +190,7 @@ impl CausalEdge64 { } else { -(mantissa_magnitude as i8) }; - causal_edge::CausalEdge64::pack_v2(s_idx, p_idx, o_idx, freq_u8, conf_u8, causal_mask, 0, causal_edge::PlasticityState::ALL_FROZEN) + SpoEdge::pack_v2(s_idx, p_idx, o_idx, freq_u8, conf_u8, causal_mask, 0, causal_edge::PlasticityState::ALL_FROZEN) .with_inference_mantissa(mantissa_signed) } @@ -216,7 +216,7 @@ impl CausalEdge64 { } else { -((spo.frequency_u8() as i32 * 32 / 255).min(127) as i8) }; - let mut out = CausalEdge64::default(); + let mut out = CascadeChannels8::default(); out.set_channel(dom, net_signed); out } @@ -280,11 +280,11 @@ impl TierEngine { .collect() } - /// Emit CausalEdge64 events from top-k peaks. + /// Emit CascadeChannels8 events from top-k peaks. /// /// For each of the top-k peaks, emit edges to their 4 nearest neighbors /// in the distance table (highest similarity = strongest constructive edges). - pub fn emit_causal_edges(&self, k: usize) -> Vec<(u16, CausalEdge64)> { + pub fn emit_causal_edges(&self, k: usize) -> Vec<(u16, CascadeChannels8)> { let peaks = self.top_k(k); let mut edges = Vec::new(); @@ -312,7 +312,7 @@ impl TierEngine { if strength == 0 { continue; } - let mut edge = CausalEdge64::new(); + let mut edge = CascadeChannels8::new(); edge.set_channel_u8(CHANNEL_CAUSES, strength); edges.push((neighbor_idx as u16, edge)); } @@ -325,7 +325,7 @@ impl TierEngine { /// /// Constructive channels (0..=6) add positive energy. /// CONTRADICTS channel (7) subtracts energy. - pub fn apply_edges(&mut self, edges: &[(u16, CausalEdge64)]) { + pub fn apply_edges(&mut self, edges: &[(u16, CascadeChannels8)]) { for &(target, edge) in edges { let idx = target as usize; if idx >= self.size { @@ -379,7 +379,7 @@ impl TierEngine { // LayeredEngine: three-level cascade with causal edge propagation // ═══════════════════════════════════════════════════════════════════════════ -/// Three-level cascade: L1 → L2 → L3 with CausalEdge64 upstream propagation. +/// Three-level cascade: L1 → L2 → L3 with CascadeChannels8 upstream propagation. pub struct LayeredEngine { l1: TierEngine, l2: TierEngine, @@ -427,7 +427,7 @@ impl LayeredEngine { } else { 1 }; - let l2_edges: Vec<(u16, CausalEdge64)> = l1_edges + let l2_edges: Vec<(u16, CascadeChannels8)> = l1_edges .iter() .map(|&(idx, edge)| { let scaled = (idx as usize * l1_to_l2).min(l2_size.saturating_sub(1)); @@ -444,7 +444,7 @@ impl LayeredEngine { } else { 1 }; - let l3_edges: Vec<(u16, CausalEdge64)> = l2_edges_out + let l3_edges: Vec<(u16, CascadeChannels8)> = l2_edges_out .iter() .map(|&(idx, edge)| { let scaled = (idx as usize * l2_to_l3).min(l3_size.saturating_sub(1)); @@ -506,11 +506,11 @@ mod tests { table } - // ── CausalEdge64 tests ── + // ── CascadeChannels8 tests ── #[test] fn causal_edge_channels() { - let mut edge = CausalEdge64::new(); + let mut edge = CascadeChannels8::new(); assert_eq!(edge.0, 0); // Set each channel to a distinct value. @@ -530,7 +530,7 @@ mod tests { #[test] fn causal_edge_constructive_sum() { - let mut edge = CausalEdge64::new(); + let mut edge = CascadeChannels8::new(); // Channels 0-6 = 10 each. for ch in 0..7u8 { edge.set_channel_u8(ch, 10); @@ -544,7 +544,7 @@ mod tests { #[test] fn causal_edge_net_strength() { - let mut edge = CausalEdge64::new(); + let mut edge = CascadeChannels8::new(); // Constructive: channels 0-6 = 20 each = 140 total. for ch in 0..7u8 { edge.set_channel_u8(ch, 20); @@ -554,7 +554,7 @@ mod tests { assert_eq!(edge.net_strength(), 40); // 140 - 100 // Destructive dominates. - let mut edge2 = CausalEdge64::new(); + let mut edge2 = CascadeChannels8::new(); edge2.set_channel_u8(CHANNEL_CAUSES, 10); edge2.set_channel_u8(CHANNEL_CONTRADICTS, 200); assert_eq!(edge2.net_strength(), -190); // 10 - 200 @@ -603,7 +603,7 @@ mod tests { let mut tier = TierEngine::new(table, "test"); // Start with zero energy, apply constructive edges. - let mut edge = CausalEdge64::new(); + let mut edge = CascadeChannels8::new(); edge.set_channel_u8(CHANNEL_CAUSES, 100); edge.set_channel_u8(CHANNEL_SUPPORTS, 50); @@ -634,7 +634,7 @@ mod tests { assert!(initial > 0.0); // Apply a strongly contradicting edge to atom 3. - let mut edge = CausalEdge64::new(); + let mut edge = CascadeChannels8::new(); edge.set_channel_u8(CHANNEL_CONTRADICTS, 255); tier.apply_edges(&[(3, edge)]); @@ -689,8 +689,8 @@ mod transcoder_tests { use super::*; use causal_edge::CausalEdge64 as SpoEdge; - fn build_8ch_with(idx: usize, net: i8) -> CausalEdge64 { - let mut e = CausalEdge64::default(); + fn build_8ch_with(idx: usize, net: i8) -> CascadeChannels8 { + let mut e = CascadeChannels8::default(); e.set_channel(idx, net); e } @@ -707,14 +707,14 @@ mod transcoder_tests { #[test] fn test_dominant_channel_zero_default() { - let e = CausalEdge64::default(); + let e = CascadeChannels8::default(); assert_eq!(e.dominant_channel(), 0, "all-zero edge dominant idx is 0"); assert_eq!(e.active_channel_count(), 0); } #[test] fn test_dominant_channel_picks_max_abs() { - let mut e = CausalEdge64::default(); + let mut e = CascadeChannels8::default(); e.set_channel(2, 30); // SUPPORTS e.set_channel(5, -100); // ABSTRACTS, larger magnitude e.set_channel(7, 10); @@ -756,7 +756,7 @@ mod transcoder_tests { for &sign in &[1i8, -1i8] { let e = build_8ch_with(dom, sign * 64); let spo = e.to_spo(1, 1, 1); - let back = CausalEdge64::from_spo(spo); + let back = CascadeChannels8::from_spo(spo); let back_dom = back.dominant_channel(); // Channel mapping is many-to-one in the transcoder table; some // dominant channels collapse to the same SPO mantissa slot. @@ -788,7 +788,7 @@ mod transcoder_tests { #[test] fn test_set_channel_out_of_range_no_op() { - let mut e = CausalEdge64::default(); + let mut e = CascadeChannels8::default(); e.set_channel(8, 100); e.set_channel(255, 50); assert_eq!(e.0, 0, "out-of-range set_channel must be a no-op"); diff --git a/crates/thinking-engine/src/qualia.rs b/crates/thinking-engine/src/qualia.rs index 31ac701e1..ec731c346 100644 --- a/crates/thinking-engine/src/qualia.rs +++ b/crates/thinking-engine/src/qualia.rs @@ -17,7 +17,7 @@ //! - Tritone (√2:1) → tension (does not converge) //! //! Cross-validated against Jina v3 text embeddings (220 calibrated pairs in Upstash). -//! Bach's 7+1 counterpoint rules = CausalEdge64 7+1 channels = universal structure. +//! Bach's 7+1 counterpoint rules = CascadeChannels8 7+1 channels = universal structure. /// The 17 QPL dimensions, in canonical order. /// Each maps to a specific convergence observable.