diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index d750a78b..118a64bb 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,22 @@ +## 2026-06-17 — W3+W4a atomic read/write shim landed (BindSpace→MailboxSoA migration, first behaviour-touching step) + +**Main thread (Opus) — single implementer**, on branch `claude/bindspace-mailbox-soa-w3-w4a` (plan v2 already committed). Sole-owner working tree; ran cargo freely against the shared `target/`. + +**Shipped (D-id W3+W4a under `bindspace-singleton-to-mailbox-soa-v1`):** +- New `src/backing.rs` (`pub(crate)`): `BackingStore<'a>` (read shim) + `BackingStoreWrite<'a>` (write shim, C1). Enum-over-trait (OQ-C). `Singleton(&BindSpace)` = live default; `#[cfg(feature="mailbox-thoughtspace")] Mailbox(&MailboxSoA<1024>)` = migration target. Six read methods (prefilter/content_row/qualia_17d/edge/entity_type/len) + eight write methods. Mailbox prefilter iterates `win.start.min(populated)..win.end.min(populated)` — byte-identical to `BindSpace::meta_prefilter` window semantics (C2, P0). `set_edge` wraps `u64↔CausalEdge64` on the singleton arm. +- New Cargo feature `mailbox-thoughtspace` — **default-OFF, NOT in `lab`**. +- `driver.rs`: `const DEFAULT_MAILBOX: MailboxId = 0` (OQ-D Option A, no contract change), private `fn backing()` selector (`debug_assert!(mailboxes.len() <= 1)`, singleton fallback when none registered), ALL 6 reads in `run()` re-pointed through one `backing` value (one body, no `#[cfg]` inside `run`). `ontology()` stays on the singleton (W4b re-home); `entity_type` ctx_id read routes through the shim. +- `engine_bridge.rs::unbind_busdto`: C5 downgrade — cycle-plane index recovery `#[cfg(not(mailbox-thoughtspace))]` (cycle plane is never migrated, D-DIST-5); headline survives via `qualia[9]`; singleton build keeps bit-exact recovery. Doc migration pointer added (I-LEGACY-API-FEATURE-GATED). +- Tests: `tests/w2_differential.rs` (4, whole-ShaderCrystal `to_bits()` parity incl. non-zero-window + non-vacuity + meta-prefilter + alpha-merge cases); `tests/firewall.rs` (2, twin-bar lint via std::fs walk + planted-twin meta-sanity); `mailbox_soa.rs` field-isolation matrix + cycle-drop footprint; `backing.rs` in-module read+write round-trip (singleton + mailbox windowed prefilter); `busdto_bridge_test.rs` gated the 3 cycle-plane-dependent tests to `not(mailbox-thoughtspace)` + added a mailbox-arm test pinning the non-headline-idx→0 loss. + +**Test counts:** default `97 lib + 2 firewall + 2 e2e` all green (regression gate — singleton arm byte-identical, the existing 94 lib + 2 e2e untouched); feature-on `98 lib + 2 firewall + 2 e2e + 4 w2` all green. clippy `-p cognitive-shader-driver --all-targets` (both cfgs) + `cargo fmt` clean on touched files (no new warnings, the two `#[allow(dead_code)]` are on the forward-staged C1 write surface, justified + tested). + +**P0 surfaced (pre-existing, NOT mine, left untouched):** `--features with-engine` does NOT compile on clean HEAD (`engine_bridge.rs:259` references `QUALIA_DIMS` unimported); consequently the busdto tests (incl. my C5 mailbox-arm test) are dormant, and the D-CSV-5b i4-qualia cutover separately breaks the busdto `codebook_index` round-trip (u16 stored in ±7 i4 `qualia[9]`). Verified pre-existing via `git stash` on clean HEAD. Flagged for operator decision; out of W3+W4a scope. + +**Board hygiene (this commit):** STATUS_BOARD W3+W4a row (→ In PR), LATEST_STATE dated bullet, this AGENT_LOG entry — all in the same commit per the mandatory rule. + +--- + ## 2026-06-16 — odoo-rs SEEDED + PR #511 board hygiene (cross-repo) **Main thread (Opus 4.7) — two outputs, one session arc.** diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 03ec8398..df780a8e 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -10,6 +10,8 @@ --- +> **2026-06-17 — IN PR (branch `claude/bindspace-mailbox-soa-w3-w4a`)** — W3+W4a atomic read/write shim, the first behaviour-touching step of the BindSpace→MailboxSoA migration. New `cognitive-shader-driver::backing` module (`pub(crate)`): `BackingStore<'a>` (read) + `BackingStoreWrite<'a>` (write) — an enum with a `Singleton(&BindSpace)` arm (live default) and a `#[cfg(feature = "mailbox-thoughtspace")] Mailbox(&MailboxSoA<1024>)` arm. New Cargo feature **`mailbox-thoughtspace`** — **default-OFF, NOT in `lab`**; production stays singleton-read+write until W7. `driver.run()` keeps ONE body: all six dispatch reads (meta_prefilter / qualia17d / content_row / edge / entity_type / len) re-pointed through a `self.backing()` selector (`const DEFAULT_MAILBOX: MailboxId = 0`, `debug_assert!(mailboxes.len() <= 1)`, singleton fallback when no mailbox registered); `ontology()` stays on the singleton (re-home is W4b). Gates: **W2 differential** (`tests/w2_differential.rs`, 4 tests) asserts the WHOLE `ShaderCrystal` bit-identical (`f32::to_bits()`) across both arms incl. a non-zero-window case + non-vacuity; firewall CI lint (`tests/firewall.rs`) bars the two `CausalEdge64` twins (`ndarray::hpc::causal_diff` / `thinking_engine::layered`) from `src/`; field-isolation matrix + cycle-drop footprint (~6 KB/row vs ~71.6 KB) in `mailbox_soa.rs`. `unbind_busdto` C5 downgrade: cycle-plane index recovery feature-gated OUT under `mailbox-thoughtspace` (cycle plane never migrated — D-DIST-5), headline survives via `qualia[9]`; singleton build keeps bit-exact recovery. Tests: default **97 lib + 2 firewall + 2 e2e**; feature-on **98 lib + 2 firewall + 2 e2e + 4 w2**; clippy `--all-targets` (both cfgs) + fmt clean on touched files. **Pre-existing P0 surfaced (NOT introduced, NOT fixed here):** the `with-engine` build does not compile on `main`/HEAD (`engine_bridge.rs:259` uses `QUALIA_DIMS` without importing it); the busdto round-trip tests have never run, and (separately) the D-CSV-5b i4-qualia cutover breaks the `codebook_index` round-trip (stored in i4 `qualia[9]`, ±7 range, cannot hold a u16). Left untouched to keep scope to W3+W4a — flagged for operator. Plan: `.claude/plans/bindspace-mailbox-soa-w3-w4a-impl-v1.md`. +> > **2026-06-16 — MERGED #512** (perturbation-sim review fixes + **core-first transcode doctrine**): +591/-5 across 11 files. **Code fixes (review of #511):** `examples/calibrate.rs` divide-by-zero guard on degenerate grid; `src/hhtl.rs::basin_lambda2` `assert_eq!(keys.len(), grid.n, …)` precondition (silent corruption→loud panic); `TECH_DEBT.md` MD018 reflow. **Doctrine (the structural delivery):** new mandatory-read `core-first-transcode-doctrine.md` (218 LOC) + 3 new agent cards (`core-first-architect`, `core-gap-auditor`, `adapter-shaper`) + `BOOT.md`/`README.md` wires + EPIPHANIES entry + CLAUDE.md (+21 LOC, doctrine wire-up — NEW content unread by this session). Likely directly aligned with the ontology-first stance the operator locked on odoo-rs. Branch `claude/happy-hamilton-0azlw4`, merge `1e23c410`. 75 lib tests + clippy + fmt clean. > > **2026-06-16 — MERGED #513** (perturbation-sim: inertia §0 promotion gate + CAKES/CHAODA + witness standing-wave + H ingest): +1009/-2 across 10 files. Disjoint from #512 by design. **(1) §0 gate** — `GuardrailVerdict::RatifiedReuse`: `inertia_buffer` takes `ResidueEdge` `INERTIA_SLOT = 5`, reuses an existing tenant, invents no new axis → passes §0 by **reuse, not waiver**. Topology stays HHTL-OGAR GUID key; the buffer is one more value, orthogonal by key/value split. **(2) Probe 1 CAKES + CHAODA-lite** over HHTL basins: per-basin `[λ₂, size, inertia]` features; `CHAODA_FLAG=0.75` mirrors ndarray::clam's flag; example `chaoda` flags planted brittle block (basin 1.1.0, score 1.000). Full `ClamTree` ensemble path gated on local ndarray sibling. **(3) Probe 2 witness arc as standing wave** (METHODS §11): `particle == wave` via Parseval (`Hᵀ·H = N·I`), agreement to **0.00e0**; `witness_from_spectrum` is the O(N)-per-arc read-many amortization win. **(4) Probe 3** per-bus inertia (H) ingest path opened. Branch `claude/perturbation-sim-inertia-clam`, merge `8a3e335b`. Does NOT touch `canonical_node`. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 872893db..afd2dd3a 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -676,6 +676,7 @@ Plan path: `.claude/plans/bindspace-singleton-to-mailbox-soa-v1.md`. Epiphany `E | D-id | Title | Crate(s) | ~LOC | Risk | Status | PR / Evidence | |---|---|---|---|---|---|---| | D-MBX-1 | add migrated columns (`edges`/`qualia`/`meta`/`entity_type`) to `MailboxSoA` behind `mailbox-thoughtspace` feature | cognitive-shader-driver | 120 | MED | **Queued** | gated on D-CE64-MB-1-impl + PR-NDARRAY-MIRI-COMPLETE | +| W3+W4a | atomic read/write shim (`backing::{BackingStore,BackingStoreWrite}`) — `driver.run()` keeps ONE body, `mailbox-thoughtspace` (default-OFF) flips substrate singleton→`MailboxSoA`; 6 reads re-pointed; W2 differential proves bit-identity; firewall lint + field-isolation + footprint gates | cognitive-shader-driver | 600 | MED | **In PR** | branch `claude/bindspace-mailbox-soa-w3-w4a`; default 97+2+2 tests, feature 98+2+2+4; clippy/fmt clean; `unbind_busdto` C5 downgrade feature-gated (cycle plane never migrated). Plan `bindspace-mailbox-soa-w3-w4a-impl-v1.md` | | D-MBX-2 | move `engine_bridge` per-row read/write surface onto mailbox rows; `cycle` plane becomes a transient local | cognitive-shader-driver | 180 | MED | **Queued** | blocked on D-MBX-1 + OQ-1 (content-ref shape) | | D-MBX-3 | `ShaderDriver` holds a sea-star of mailboxes; kill the `BindSpace::zeros(4096)` singleton in `serve.rs` | cognitive-shader-driver | 160 | HIGH | **Queued** | blocked on D-MBX-2 + OQ-2 (temporal/expert fold) | | D-MBX-4 | death → SPO-G quad + Lance tombstone-witness (link-integrity back-pointer) | cognitive-shader-driver + Lance | 200 | HIGH | **Queued** | blocked on D-MBX-3 + Zone-2 persistence | diff --git a/.claude/plans/bindspace-mailbox-soa-w3-w4a-impl-v1.md b/.claude/plans/bindspace-mailbox-soa-w3-w4a-impl-v1.md new file mode 100644 index 00000000..e5823454 --- /dev/null +++ b/.claude/plans/bindspace-mailbox-soa-w3-w4a-impl-v1.md @@ -0,0 +1,212 @@ +# W3+W4a — atomic implementation plan (code-grounded, v1) + +> **Status:** v2 — 5-consolidation + 3-brutal-critic pass APPLIED (see § "v2 critique +> integration" below). v1 brutal verdict was **HOLD** (3 P0s); v2 integrates every +> P0/P1/P2. Architecture confirmed sound by all 8 angles (dto-soa ACCEPT, convergence +> single-body confirmed, firewall source-CLEAN, preflight zero-P0-drift); the fixes are +> the write-shim design, the prefilter window, OQ-D resolution, and parity-surface completion. +> **Date:** 2026-06-17. **Branch:** `claude/bindspace-mailbox-soa-w3-w4a`. +> **Parent plan:** `bindspace-singleton-to-mailbox-soa-v1.md` (the W2→W7 wiring plan on main; +> the v2 wiring snapshot lives on branch `claude/bindspace-mailbox-soa-wiring-plan`). +> **Unblocked:** W1 (#517), W1b (#518), W1c (#519) are ALL merged to `main`. +> The wiring plan's "BLOCKED:#518" gates are cleared. W2 (differential harness) +> and W3+W4a (atomic read/write re-point) are now buildable. +> **Operator constraints (binding):** two paths step by step; test the new before +> deleting the old; CausalEdge64 dedup precise; delete BindSpace LAST. + +## v2 critique integration — what the 5+3 pass changed (HOLD → build-ready) + +**Panel:** 5 consolidation (integration-lead, truth-architect, dto-soa-savant, +convergence-architect, firewall-warden) + 3 brutal (baton-handoff-auditor, +brutally-honest-tester, preflight-drift-auditor). Verdicts: architecture SOUND +(dto-soa ACCEPT enum-over-trait; convergence single-body confirmed + `edge()` +collapse real; preflight zero-P0-drift, line numbers exact; firewall source-CLEAN). +Blocking gaps fixed below. + +- **C1 — write surface (was P0; baton P0-2, brutal-tester P0-1).** The read shim is + `&self`; the WRITE path is separate — `driver.run()` writes NO bindspace columns + (verified: zero `self.bindspace.*.set/write_`). Writes are engine_bridge **free fns** + (`dispatch_busdto:234`, `persist_cycle:706`, **`ingest_codebook_indices:53`** — the + 4th writer v1 missed) called by **bins** (`serve.rs:156/695`, `grpc.rs:140`) and tests. + The bin call-site conversion (`Arc::get_mut` → owned `&mut MailboxSoA`) is **W4b**, not + this PR. **W3+W4a delivers the write SHIM** (a `BackingStoreWrite<'a>` `&mut` enum mirror + with `set_content/set_qualia/set_edge/set_meta/set_entity_type/set_temporal/set_expert/ + set_sigma`; `set_edge` wraps `u64→CausalEdge64` on the singleton arm — `bindspace` + `edges.set(u64)`:132 vs mailbox `set_edge(CausalEdge64)`:414) **exercised only by the + W2 differential harness**, feature **default-OFF**. Production stays singleton-read + + singleton-write until W4b/W7 → H-DW-1 satisfied (under the feature BOTH surfaces flip + in the harness; production never half-flips). +- **C2 — prefilter window (was P0; convergence, baton P1-2, brutal-tester P0-3).** Mailbox + arm MUST iterate `win.start.min(populated)..win.end.min(populated)`, matching + `BindSpace::meta_prefilter`'s `start..end` (bindspace.rs:358-362). `0..populated` is a + sentinel-lie: identical Vec shape, divergent rows on any `row_start>0` (wire.rs/grpc.rs + pass `req.row_start`). **W2 differential MUST include a non-full-window case** + (e.g. `ColumnWindow::new(1, len-1)`) or the gate passes green on the bug. +- **C3 — OQ-D MailboxId selection (was P0; integration-lead, baton P0-3, brutal-tester P1-2).** + `ShaderDispatch` carries no `MailboxId` (cognitive_shader.rs:178-209). **Resolution + (Option A, NO contract change):** under the feature the driver selects a single designated + mailbox via a driver-crate `const DEFAULT_MAILBOX: MailboxId = 0` (`MailboxId` is a `u32` + alias — collapse_gate.rs:121 — so it's a free const, not an associated const) + + `debug_assert!(self.mailboxes.len() == 1)`. Multi-mailbox routing is **W5**. Reject + Option B (adding `mailbox_id` to `ShaderDispatch` = zero-dep-contract change, W5 scope). +- **C4 — parity surface completion (was incomplete; truth-architect).** The W2 differential + asserts the **whole `ShaderCrystal` field-by-field** (every f32 via `to_bits()`), NOT a + hand-picked subset. v1 missed: `hit_count`, **`cycles_used`**, **`style_ord`** (the two + prefilter-order canaries), `emitted_edges`+`emitted_edge_count`, `gate`, `persisted_row`, + all of `MetaSummary` (3×f32+bool), `alpha_composite`, `ShaderHit._pad`. `to_bits()` is + REQUIRED (same `run()` body + identical read bytes = bit-identical arithmetic; ULP + tolerance would mask exactly the read-path bug this differential exists to catch). +- **C5 — unbind_busdto downgrade (was P1; baton P0-1, truth C1/C2, brutal-tester P1-1).** + Feature-gate the lossy narrowing with a doc migration pointer (I-LEGACY-API-FEATURE-GATED). + Keep the singleton arm's bit-exact dense-top_k assertion live via + `#[cfg(not(feature="mailbox-thoughtspace"))]`; add a SEPARATE explicitly-lossy mailbox-arm + assertion that non-headline `top_k[1..].0` recover as `0` (pin the loss, don't tolerate it). + Never relax the existing test in place. **OQ-B = NOW-OK to land** (loss is + `#[cfg(with-engine)]` lab-path, not read by live `run()`, extends an already-lossy + register recovery per I-VSA-IDENTITIES) — no new column needed. +- **C6 — firewall lint must be concrete (was BLOCK; firewall-warden).** Replace the prose + with a real `Grep`-CI rule failing on `(causal_diff|thinking_engine::layered)\s*::\s*CausalEdge64` + + the `... as` aliased forms, scoped to `crates/cognitive-shader-driver/`, AND explicitly + allowing the `repr(transparent)` cast sites at mailbox_soa.rs:606/620 (else false-positive). + Or a `compile_fail`/trybuild guard. Source is CLEAN today (no twin imports) — the gate must + have teeth. +- **C7 — resolved-favorably / notes.** `expert_at`:472 / `set_expert`:478 EXIST → `cycle_count` + is LOSSLESS on the mailbox arm (truth escalation closed). Feature-gate asymmetry (preflight P2): + `dispatch_busdto`/`unbind_busdto` are `#[cfg(with-engine)]`, `persist_cycle`/`ingest_codebook_indices` + are ungated (default build) → the writer re-point reconciles `with-engine ∧ mailbox-thoughtspace`. + `set_edge` is at :414 (v1 said :411 — doc-comment line). Board hygiene: the build PR updates + `STATUS_BOARD.md` + `INTEGRATION_PLANS.md` in the same commit. + +## Ground truth (verified against `main` HEAD d2f9b7d9, NOT guessed) + +### Dispatch read surface — `driver.rs::run()` (the 6 reads to re-point) +| line | read | shim method | +|---|---|---| +| 172 | `self.bindspace.meta_prefilter(req.rows, &req.meta_prefilter)` | `prefilter` | +| 178, 455 | `self.bindspace.qualia.row(row).to_f32_17d()` | `qualia_row` | +| 204, 206, 290 | `self.bindspace.fingerprints.content_row(row)` | `content_row` | +| 244 | `CausalEdge64(self.bindspace.edges.get(row))` | `edge` | +| 356 | `self.bindspace.entity_type[r]` | `entity_type` | +| 360 | `self.bindspace.ontology()` | **NOT shim** — `Arc` on driver (W4b) | +| 661 | `row_count` = `self.bindspace.len` | `len` | +| 665 | `byte_footprint` = `self.bindspace.byte_footprint()` | (trait, both impl) | +| 556 | `self.awareness.write()` | unchanged (leaf RwLock) | +| 288 | `cycle_fp = [0u64; WORDS_PER_FP]` transient fold | unchanged (stack, never a column) | + +### Mailbox accessor surface — `mailbox_soa.rs` (verified signatures) +- `content_row(row) -> &[u64]` (500) — byte-identical stride to `bs.fingerprints.content_row` +- `qualia_at(row) -> QualiaI4_16D` (420) +- `edge(row) -> CausalEdge64` (405) — **stores `causal_edge::CausalEdge64` NATIVELY** (`self.edges[row]`) +- `entity_type_at(row) -> u16` (444) +- `meta_at(row) -> MetaWord` (432) — no `meta_raw`; `MetaFilter::accepts` takes `MetaWord` +- `populated() -> usize` (317) — the prefilter bound (W1c) +- write side: `set_content` (506), `set_qualia` (426), `set_edge` (411), `apply_edges` (253) +- **no `meta_prefilter`** → shim mailbox arm iterates **`win.start.min(populated)..win.end.min(populated)`** ascending, `MetaFilter::accepts` (v2 P0-B: NOT `0..populated` — `BindSpace::meta_prefilter` honors the dispatch `ColumnWindow` start; wire/grpc pass non-zero `row_start`) +- `edges_raw()` (:592) and `meta_raw()` (:609) DO exist as `MailboxSoaView` trait methods (`unsafe repr(transparent)` casts) — the shim does NOT use them (typed `edge()`/`meta_at()` path is used instead; see CausalEdge64 resolution below). The firewall lint must allow the `repr(transparent)` cast sites. + +### Writer surface — `engine_bridge.rs` (3 fns take BindSpace directly) +- `dispatch_busdto(bs: &mut BindSpace, row, bus, style_ord) -> usize` (234) → `bs.write_cycle_fingerprint` (243) +- `unbind_busdto(bs: &BindSpace, row) -> BusDto` (315) → reads `bs.fingerprints.cycle_row(row)` (334) +- `persist_cycle(bs: &mut BindSpace, row, bus, style_ord)` (706) → `bs.write_cycle_fingerprint` (707) + +## RESOLVED: CausalEdge64 firewall (plan's `edges_raw()` worry is moot) + +The wiring plan (baton-handoff P1) said "use `mb.edges_raw()[row]` (raw), not +`mb.edge(row).0` (typed bounce)." **Ground truth overturns the premise:** +`MailboxSoA` stores `CausalEdge64` natively (`edges: [CausalEdge64; N]`, +`edge(row)` returns `self.edges[row]` — already typed). `mailbox_soa` and +`driver` both reference the SAME re-exported `causal_edge::CausalEdge64` +(`causal_edge::CausalEdge64` == `causal_edge::edge::CausalEdge64`). + +**Resolution — shim `edge` returns the typed `causal_edge::CausalEdge64`:** +- Singleton arm: `CausalEdge64(bs.edges.get(row))` (raw u64 wrapped, as today) +- Mailbox arm: `mb.edge(row)` (already typed — ZERO raw bounce) + +This is STRICTLY stronger than the plan: no raw u64 ever surfaces on the +mailbox arm, no `edges_raw()` accessor added, firewall is one typed surface. +The CI firewall lint (bar `ndarray::hpc::causal_diff::CausalEdge64` AND +`thinking_engine::layered::CausalEdge64`) still ships. + +## The shim — `backing.rs` (new, driver-crate-local, monomorphized) + +```rust +// crates/cognitive-shader-driver/src/backing.rs +pub(crate) enum BackingStore<'a> { + Singleton(&'a BindSpace), // path A — live default + #[cfg(feature = "mailbox-thoughtspace")] + Mailbox(&'a MailboxSoA<1024>), // path B — feature-gated +} + +impl<'a> BackingStore<'a> { + fn prefilter(&self, win: (u32,u32), f: &MetaFilter) -> Vec; + fn content_row(&self, row: usize) -> &[u64]; + fn qualia_row(&self, row: usize) -> QualiaI4_16D; + fn edge(&self, row: usize) -> CausalEdge64; // typed both arms + fn entity_type(&self, row: usize) -> u16; + fn len(&self) -> usize; +} +``` +- `run()` keeps ONE body, written against `BackingStore`. The feature flag + selects which variant is *constructed* (in `dispatch`/`dispatch_with_sink`), + NEVER `#[cfg]`-branches inside `run`. +- prefilter mailbox arm clamps to `populated()`, iterates `0..populated` ascending + so `passed_rows` order matches `BindSpace::meta_prefilter`'s `for row in start..end`. +- `ontology` is NOT a shim method (stays `Arc`; re-homed at W4b). + +## Feature gate (I-LEGACY-API-FEATURE-GATED-safe) +- ONE feature `mailbox-thoughtspace`, default **OFF** until W7. +- Gate the OWNER (which variant is built), not the fn body. +- No per-W sub-features. + +## Writer re-point (W4a half — engine_bridge + persist path) +The writers and readers flip **together** in this one PR (H-DW-1: never W3 +writers-only then W4 readers-later → silent same-row divergence). +- `dispatch_busdto` / `persist_cycle`: under feature, stop the + `write_cycle_fingerprint` column write; keep `edges`/`meta`/`qualia` writes + routed to the mailbox via `set_*`/`apply_edges`. Compute the Binary16K + accumulator transiently, discard (cycle plane never a mailbox column). +- `unbind_busdto`: REWRITE (not re-point) with a DOWNGRADED contract — only the + headline index (in `qualia[9]`) survives; non-headline `top_k` indices recovered + from cycle-plane set-bits are NOT recoverable from mailbox columns (D-DIST-5 + exception). Differential gate + `busdto_bridge_test.rs` tolerance update land + in THIS PR. + +## In-PR gates (all mandatory, per wiring plan §6 W3+W4a) +1. **W2 differential harness** (one driver, two backing arms; same awareness/planes/ + semiring): assert `ShaderResonance` bit-identical via `f32::to_bits()` (top_k + row/distance/predicates/cycle_index + resonance/entropy/std_dev bits) + + `cycle_fingerprint` + `MaterializeProvenance`; mailbox built with + `set_populated(len)` + prefilter clamped to `populated()`. Gate with + `entity_type==0` rows (neutralizes ontology); second assertion `entity_type!=0` + after W4b ontology re-home. +2. **BusDto differential**: `unbind_busdto(mailbox)` vs `unbind_busdto(bindspace)` — + headline + energies + cycle_count + converged parity; non-headline-index loss + documented in updated tolerance. +3. **temporal field-isolation matrix**: each migrated column write leaves all others + byte-unchanged; emit-path `driver.rs:402-413 pack(temporal=cycle_index)` either + lands in mailbox `temporal` column OR paired with `temporal_dropped_under_v2` test. +4. **CausalEdge64 firewall CI lint**: deny/grep that `cognitive-shader-driver` + imports neither `ndarray::hpc::causal_diff::CausalEdge64` nor + `thinking_engine::layered::CausalEdge64`; + `edge()` round-trip to typed. +5. **cycle-drop compile+bench**: `MailboxSoA` exposes no `cycle*` symbol; footprint + ≈ 6 KB/row (content/topic/angle), not ≈ 71.6 KB/row. +6. **`--features mailbox-thoughtspace` CI matrix row** (build + W2 differential), + live from this PR. + +## Explicitly OUT of this PR (deferred to later W-steps) +- W4b: `Arc::get_mut` → owned `&mut MailboxSoA` (4 bin sites) + ontology re-home. +- W5: bins build the mailbox set. +- W6: death→SPO-G+Lance tombstone (HARD-BLOCKED surrealdb #41). +- W7: delete BindSpace + cycle plane + drop feature. + +## Open questions for the 5+3 pass +- OQ-A: is W2 (the differential harness) a SEPARATE prior PR, or folded into the + W3+W4a atomic PR as the gate? (Plan v2 §6 says "run on the W3+W4a atomic PR".) +- OQ-B: `unbind_busdto` rewrite with downgraded contract — acceptable now, or does + the non-headline-index loss need a new mailbox column FIRST (escalate to operator)? +- OQ-C: should the shim be `enum BackingStore` or a generic `R: DriverRead` trait? + (Plan: equivalent, monomorphized either way; enum is simpler, no new pub trait.) +- OQ-D: does `dispatch`/`dispatch_with_sink` construct the variant cleanly without + a `#[cfg]` inside `run`, given `mailboxes: HashMap` is keyed by `MailboxId` — which + mailbox does a singleton-shaped `ShaderDispatch` select? (req carries no mailbox id today.) +``` diff --git a/crates/cognitive-shader-driver/Cargo.toml b/crates/cognitive-shader-driver/Cargo.toml index 761a3740..37148caa 100644 --- a/crates/cognitive-shader-driver/Cargo.toml +++ b/crates/cognitive-shader-driver/Cargo.toml @@ -74,6 +74,12 @@ tonic-build = { version = "0.12", optional = true } default = [] with-engine = ["dep:thinking-engine"] with-planner = ["dep:lance-graph-planner"] +# W3+W4a — BindSpace→MailboxSoA migration read/write shim. Default-OFF (NOT in +# `default`, NOT in `lab`): production stays singleton-read + singleton-write +# until W7. When ON, the driver constructs its `BackingStore` from the single +# designated `MailboxSoA` (DEFAULT_MAILBOX) instead of the singleton BindSpace, +# and the W2 differential harness exercises both arms for bit-identity. +mailbox-thoughtspace = [] # Shared LAB DTOs — `wire.rs` + `auto_detect.rs` + codec kernel scaffolds # + token_agreement use these regardless of whether the transport is REST # (serve) or gRPC (grpc). Both features pull this set. diff --git a/crates/cognitive-shader-driver/src/backing.rs b/crates/cognitive-shader-driver/src/backing.rs new file mode 100644 index 00000000..62787743 --- /dev/null +++ b/crates/cognitive-shader-driver/src/backing.rs @@ -0,0 +1,345 @@ +//! `BackingStore` — the read/write shim that lets `driver.run()` keep ONE body +//! while the W3+W4a migration flips its substrate from the singleton +//! [`BindSpace`] to a per-mailbox [`MailboxSoA`]. +//! +//! ## Why an enum, not a trait (OQ-C resolved) +//! +//! The dispatch hot path reads the same six column surfaces on every cycle. An +//! `enum BackingStore<'a>` monomorphizes to a single match per read with no +//! `dyn` indirection — equivalent to a generic `R: DriverRead` bound but with +//! no new public trait to maintain. The feature flag selects which *variant is +//! constructed* (in `ShaderDriver::backing`), NEVER a `#[cfg]` branch inside +//! `run()`. +//! +//! ## The two arms must agree byte-for-byte (C2 — the load-bearing invariant) +//! +//! The Singleton arm reproduces today's `driver.run()` reads verbatim. The +//! Mailbox arm reads the migrated columns of a `MailboxSoA<1024>` whose rows +//! mirror the BindSpace window. The W2 differential harness +//! (`tests/w2_differential.rs`) asserts the WHOLE `ShaderCrystal` is +//! bit-identical (`f32::to_bits()`) across the two arms — if the arms diverge, +//! that test fails, which is the entire point of building the shim before any +//! production flip. +//! +//! ## Prefilter window semantics (C2, P0) +//! +//! [`BackingStore::prefilter`] on the Mailbox arm iterates +//! `win.start.min(populated)..win.end.min(populated)` — byte-identical to +//! [`BindSpace::meta_prefilter`]'s `start..end`. It must NOT iterate +//! `0..populated`: that would silently ignore a non-zero `win.start` (the +//! wire/grpc paths pass `req.row_start`), producing the same Vec *shape* with +//! divergent rows — a sentinel-lie the differential's non-zero-window case +//! catches. +//! +//! ## Write shim (C1) +//! +//! [`BackingStoreWrite`] is the `&mut` mirror. `driver.run()` itself writes NO +//! bindspace columns (it is `&self`); the write surface is exercised only by +//! the W2 differential harness, mirroring a BindSpace window into a mailbox. +//! `set_edge` reconciles the column-type mismatch: BindSpace stores raw `u64` +//! (`EdgeColumn::set`), the mailbox stores typed `CausalEdge64` +//! (`MailboxSoA::set_edge`); the Singleton arm unwraps `e.0`. + +use causal_edge::edge::CausalEdge64; +use lance_graph_contract::cognitive_shader::{ColumnWindow, MetaFilter}; + +use crate::bindspace::BindSpace; +#[cfg(feature = "mailbox-thoughtspace")] +use crate::mailbox_soa::MailboxSoA; + +/// Read-only substrate the dispatch hot path sweeps. +/// +/// `Singleton` is the live default (the migrating-off-of singleton). `Mailbox` +/// is the migration target, compiled only under `mailbox-thoughtspace`. +pub(crate) enum BackingStore<'a> { + /// Path A — the live default. Reads the shared singleton `BindSpace`. + Singleton(&'a BindSpace), + /// Path B — feature-gated. Reads one designated `MailboxSoA<1024>`'s + /// migrated columns. + #[cfg(feature = "mailbox-thoughtspace")] + Mailbox(&'a MailboxSoA<1024>), +} + +impl<'a> BackingStore<'a> { + /// Apply `f` across the meta column within `win`, returning the dense Vec of + /// passing row indices (ascending) — the prefilter that drives the + /// fingerprint sweep. + /// + /// Both arms honour the dispatch [`ColumnWindow`] start AND end. The + /// Mailbox arm clamps to [`MailboxSoA::populated`] (the `BindSpace::len` + /// analogue) so zeroed padding rows `populated..N` are never swept — a + /// zeroed `MetaWord` would otherwise pass `MetaFilter::accepts`. + #[inline] + pub(crate) fn prefilter(&self, win: ColumnWindow, f: &MetaFilter) -> Vec { + match self { + BackingStore::Singleton(bs) => bs.meta_prefilter(win, f), + #[cfg(feature = "mailbox-thoughtspace")] + BackingStore::Mailbox(mb) => { + let populated = mb.populated(); + let start = (win.start as usize).min(populated); + let end = (win.end as usize).min(populated); + let mut out = Vec::with_capacity(end.saturating_sub(start)); + for row in start..end { + if f.accepts(mb.meta_at(row)) { + out.push(row as u32); + } + } + out + } + } + } + + /// Zero-copy view of `row`'s content identity fingerprint (256 u64). + /// The driver's resonance/Hamming search reads this on the hot path. + #[inline] + pub(crate) fn content_row(&self, row: usize) -> &[u64] { + match self { + BackingStore::Singleton(bs) => bs.fingerprints.content_row(row), + #[cfg(feature = "mailbox-thoughtspace")] + BackingStore::Mailbox(mb) => mb.content_row(row), + } + } + + /// `row`'s 17-dim affective vector as f32 (the `auto_style` / α-composite + /// read). The underlying store is `QualiaI4_16D`; conversion happens here + /// so both arms expose an identical `[f32; 17]`. + #[inline] + pub(crate) fn qualia_17d(&self, row: usize) -> [f32; 17] { + match self { + BackingStore::Singleton(bs) => bs.qualia.row(row).to_f32_17d(), + #[cfg(feature = "mailbox-thoughtspace")] + BackingStore::Mailbox(mb) => mb.qualia_at(row).to_f32_17d(), + } + } + + /// `row`'s `CausalEdge64` baton edge — typed on BOTH arms. + /// + /// The Singleton arm wraps the raw `u64` (`EdgeColumn` stores `u64`); the + /// Mailbox arm returns the natively-typed `CausalEdge64` (zero raw bounce). + #[inline] + pub(crate) fn edge(&self, row: usize) -> CausalEdge64 { + match self { + BackingStore::Singleton(bs) => CausalEdge64(bs.edges.get(row)), + #[cfg(feature = "mailbox-thoughtspace")] + BackingStore::Mailbox(mb) => mb.edge(row), + } + } + + /// `row`'s OGIT entity-type index (0 = untyped). + #[inline] + pub(crate) fn entity_type(&self, row: usize) -> u16 { + match self { + BackingStore::Singleton(bs) => bs.entity_type[row], + #[cfg(feature = "mailbox-thoughtspace")] + BackingStore::Mailbox(mb) => mb.entity_type_at(row), + } + } + + /// Declared logical row count (`BindSpace::len` / `MailboxSoA::populated`). + #[inline] + #[allow(dead_code)] // mirrors the read surface; row_count routes through bindspace until W4b + pub(crate) fn len(&self) -> usize { + match self { + BackingStore::Singleton(bs) => bs.len, + #[cfg(feature = "mailbox-thoughtspace")] + BackingStore::Mailbox(mb) => mb.populated(), + } + } +} + +/// Write mirror of [`BackingStore`] — the `&mut` write surface (C1). +/// +/// `driver.run()` writes nothing (it is `&self`), so this is exercised only by +/// the W2 differential harness, which mirrors a BindSpace window into a mailbox +/// row-for-row. Under the feature BOTH the read and write surfaces flip in the +/// harness; production never half-flips (H-DW-1). +/// +/// `dead_code` is allowed deliberately: C1 mandates this write surface exist +/// NOW (so W2 can flip BOTH read+write under the feature), but the production +/// write path is still `&self` and does not consume it until W4b/W7. This is a +/// forward-staged API, not a masked lint. It IS exercised by the in-module +/// tests below. +#[allow(dead_code)] +pub(crate) enum BackingStoreWrite<'a> { + /// Path A — writes the singleton `BindSpace` columns. + Singleton(&'a mut BindSpace), + /// Path B — feature-gated. Writes one designated `MailboxSoA<1024>`. + #[cfg(feature = "mailbox-thoughtspace")] + Mailbox(&'a mut MailboxSoA<1024>), +} + +#[allow(dead_code)] // forward-staged write surface (C1); consumed at W4b/W7. Tested below. +impl BackingStoreWrite<'_> { + /// Write `row`'s content identity fingerprint (256 u64). + #[inline] + pub(crate) fn set_content(&mut self, row: usize, words: &[u64]) { + match self { + BackingStoreWrite::Singleton(bs) => bs.fingerprints.set_content(row, words), + #[cfg(feature = "mailbox-thoughtspace")] + BackingStoreWrite::Mailbox(mb) => mb.set_content(row, words), + } + } + + /// Write `row`'s packed `QualiaI4_16D` affective vector. + #[inline] + pub(crate) fn set_qualia(&mut self, row: usize, q: lance_graph_contract::qualia::QualiaI4_16D) { + match self { + BackingStoreWrite::Singleton(bs) => bs.qualia.set(row, q), + #[cfg(feature = "mailbox-thoughtspace")] + BackingStoreWrite::Mailbox(mb) => mb.set_qualia(row, q), + } + } + + /// Write `row`'s `CausalEdge64` baton edge. + /// + /// The Singleton arm unwraps to the raw `u64` that `EdgeColumn::set` stores; + /// the Mailbox arm stores the typed edge directly. + #[inline] + pub(crate) fn set_edge(&mut self, row: usize, e: CausalEdge64) { + match self { + BackingStoreWrite::Singleton(bs) => bs.edges.set(row, e.0), + #[cfg(feature = "mailbox-thoughtspace")] + BackingStoreWrite::Mailbox(mb) => mb.set_edge(row, e), + } + } + + /// Write `row`'s packed `MetaWord`. + #[inline] + pub(crate) fn set_meta( + &mut self, + row: usize, + m: lance_graph_contract::cognitive_shader::MetaWord, + ) { + match self { + BackingStoreWrite::Singleton(bs) => bs.meta.set(row, m), + #[cfg(feature = "mailbox-thoughtspace")] + BackingStoreWrite::Mailbox(mb) => mb.set_meta(row, m), + } + } + + /// Write `row`'s OGIT entity-type index. + #[inline] + pub(crate) fn set_entity_type(&mut self, row: usize, t: u16) { + match self { + BackingStoreWrite::Singleton(bs) => bs.entity_type[row] = t, + #[cfg(feature = "mailbox-thoughtspace")] + BackingStoreWrite::Mailbox(mb) => mb.set_entity_type(row, t), + } + } + + /// Write `row`'s temporal stamp. + #[inline] + pub(crate) fn set_temporal(&mut self, row: usize, t: u64) { + match self { + BackingStoreWrite::Singleton(bs) => bs.temporal[row] = t, + #[cfg(feature = "mailbox-thoughtspace")] + BackingStoreWrite::Mailbox(mb) => mb.set_temporal(row, t), + } + } + + /// Write `row`'s expert/corpus id. + #[inline] + pub(crate) fn set_expert(&mut self, row: usize, e: u16) { + match self { + BackingStoreWrite::Singleton(bs) => bs.expert[row] = e, + #[cfg(feature = "mailbox-thoughtspace")] + BackingStoreWrite::Mailbox(mb) => mb.set_expert(row, e), + } + } + + /// Write `row`'s Σ-codebook index. + #[inline] + pub(crate) fn set_sigma(&mut self, row: usize, s: u8) { + match self { + BackingStoreWrite::Singleton(bs) => bs.fingerprints.write_sigma(row, s), + #[cfg(feature = "mailbox-thoughtspace")] + BackingStoreWrite::Mailbox(mb) => mb.set_sigma(row, s), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bindspace::WORDS_PER_FP; + use lance_graph_contract::cognitive_shader::MetaWord; + use lance_graph_contract::qualia::QualiaI4_16D; + + /// The Singleton read arm reproduces direct BindSpace reads byte-for-byte, + /// and the write shim's Singleton arm is the inverse of those reads. + #[test] + fn singleton_write_then_read_round_trips() { + let mut bs = BindSpace::zeros(4); + let content: [u64; WORDS_PER_FP] = { + let mut w = [0u64; WORDS_PER_FP]; + w[0] = 0xFEED_FACE; + w[WORDS_PER_FP - 1] = 0x1234; + w + }; + let q = QualiaI4_16D::ZERO.with(0, 5).with(9, -3); + let meta = MetaWord::new(3, 2, 111, 222, 4); + + // Write through the write shim (exercises BackingStoreWrite::Singleton). + { + let mut w = BackingStoreWrite::Singleton(&mut bs); + w.set_content(2, &content); + w.set_qualia(2, q); + w.set_meta(2, meta); + w.set_edge(2, CausalEdge64(0xABCD_0002)); + w.set_entity_type(2, 0); + w.set_temporal(2, 0xDEAD); + w.set_expert(2, 77); + w.set_sigma(2, 9); + } + + // Read back through the read shim (exercises BackingStore::Singleton). + let r = BackingStore::Singleton(&bs); + assert_eq!(r.content_row(2), &content[..], "content round-trip"); + assert_eq!(r.qualia_17d(2), q.to_f32_17d(), "qualia round-trip"); + assert_eq!(r.edge(2).0, 0xABCD_0002, "edge round-trip"); + assert_eq!(r.entity_type(2), 0, "entity_type round-trip"); + assert_eq!(r.len(), 4, "len reports BindSpace::len"); + + // Direct-read parity: the shim reads exactly what BindSpace exposes. + assert_eq!(r.content_row(2), bs.fingerprints.content_row(2)); + assert_eq!(r.edge(2).0, bs.edges.get(2)); + assert_eq!( + r.prefilter(crate::ColumnWindow::new(0, 4), &MetaFilter::ALL) + .len(), + 4 + ); + } + + /// Under the feature, the Mailbox write+read arms round-trip and the + /// prefilter honours a NON-ZERO window start (the C2 sentinel-lie guard at + /// the shim level, complementing the differential harness). + #[cfg(feature = "mailbox-thoughtspace")] + #[test] + fn mailbox_write_read_and_windowed_prefilter() { + let mut mb: MailboxSoA<1024> = MailboxSoA::new(0, 0, 1.0); + for row in 0..6usize { + let mut w = BackingStoreWrite::Mailbox(&mut mb); + let mut c = [0u64; WORDS_PER_FP]; + c[0] = row as u64 + 1; + w.set_content(row, &c); + w.set_qualia(row, QualiaI4_16D::ZERO.with(0, row as i8)); + w.set_meta(row, MetaWord::new(row as u8, 1, 200, 200, 0)); + w.set_edge(row, CausalEdge64(0xAB00 | row as u64)); + w.set_entity_type(row, 0); + } + mb.set_populated(6); + + let r = BackingStore::Mailbox(&mb); + assert_eq!(r.len(), 6, "len reports populated, not capacity N"); + assert_eq!(r.edge(3).0, 0xAB00 | 3, "mailbox edge round-trip"); + assert_eq!(r.content_row(3)[0], 4, "mailbox content round-trip"); + + // Non-zero window: rows [2, 5) only — proves the start is honoured. + let passed = r.prefilter(crate::ColumnWindow::new(2, 5), &MetaFilter::ALL); + assert_eq!( + passed, + vec![2, 3, 4], + "windowed prefilter honours start AND end" + ); + } +} diff --git a/crates/cognitive-shader-driver/src/driver.rs b/crates/cognitive-shader-driver/src/driver.rs index 7ca135eb..f5657091 100644 --- a/crates/cognitive-shader-driver/src/driver.rs +++ b/crates/cognitive-shader-driver/src/driver.rs @@ -48,10 +48,21 @@ use lance_graph_contract::thinking::ThinkingStyle; use p64_bridge::cognitive_shader::CognitiveShader; use crate::auto_style; +use crate::backing::BackingStore; use crate::bindspace::{BindSpace, WORDS_PER_FP}; use crate::mailbox_soa::MailboxSoA; use lance_graph_contract::collapse_gate::MailboxId; +/// The single designated mailbox the dispatch read-shim selects under the +/// `mailbox-thoughtspace` feature (OQ-D, Option A — no contract change). +/// +/// `ShaderDispatch` carries no `MailboxId` today, so a singleton-shaped +/// dispatch routes to this fixed id. Multi-mailbox routing is W5; until then +/// the driver `debug_assert!`s exactly one mailbox is registered. `MailboxId` +/// is a `u32` alias (`collapse_gate.rs`), so this is a free const. +#[cfg(feature = "mailbox-thoughtspace")] +const DEFAULT_MAILBOX: MailboxId = 0; + // ═══════════════════════════════════════════════════════════════════════════ // ShaderDriver — holds everything the shader needs to drive // ═══════════════════════════════════════════════════════════════════════════ @@ -166,16 +177,53 @@ impl ShaderDriver { **guard = new_planes; } + /// Select the dispatch read substrate (W3 read-shim). + /// + /// Default (`mailbox-thoughtspace` OFF): the live singleton `BindSpace` — + /// byte-identical to the pre-shim reads. Under the feature: the single + /// designated `MailboxSoA` ([`DEFAULT_MAILBOX`]). `run()` keeps ONE body + /// written against [`BackingStore`]; this method (NOT a `#[cfg]` inside + /// `run`) selects which variant is constructed. + #[inline] + fn backing(&self) -> BackingStore<'_> { + #[cfg(feature = "mailbox-thoughtspace")] + { + // OQ-D: multi-mailbox routing is W5. Until then AT MOST one mailbox + // is the BindSpace surrogate; a singleton-shaped dispatch selects it. + // An unmigrated driver (zero mailboxes) falls back to the singleton, + // so the feature build never panics on a driver that hasn't been + // populated with the designated mailbox yet. + debug_assert!( + self.mailboxes.len() <= 1, + "mailbox-thoughtspace expects at most one designated mailbox \ + (DEFAULT_MAILBOX) until W5 multi-mailbox routing; got {}", + self.mailboxes.len() + ); + if let Some(mb) = self.mailboxes.get(&DEFAULT_MAILBOX) { + return BackingStore::Mailbox(mb); + } + BackingStore::Singleton(&self.bindspace) + } + #[cfg(not(feature = "mailbox-thoughtspace"))] + { + BackingStore::Singleton(&self.bindspace) + } + } + /// Run one dispatch, feeding a sink. This is the single hot path. fn run(&self, req: &ShaderDispatch, sink: &mut S) -> ShaderCrystal { + // W3 read-shim: select the substrate (singleton BindSpace by default; + // the designated MailboxSoA under `mailbox-thoughtspace`). The body + // below is written ONCE against `backing` — no `#[cfg]` branches here. + let backing = self.backing(); + // [1] Cheap meta prefilter (u32 column sweep). - let passed_rows = self.bindspace.meta_prefilter(req.rows, &req.meta_prefilter); + let passed_rows = backing.prefilter(req.rows, &req.meta_prefilter); // [2] Resolve style — Auto reads the qualia of the FIRST surviving row. - // D-CSV-5b: bs.qualia is now QualiaI4Column (returns QualiaI4_16D by value). - // Convert to f32 at the call site for auto_style::resolve(&[f32]). + // D-CSV-5b: qualia is QualiaI4_16D; the shim converts to f32 at the read. let qualia_f32_arr: [f32; 17] = if let Some(&row) = passed_rows.first() { - self.bindspace.qualia.row(row as usize).to_f32_17d() + backing.qualia_17d(row as usize) } else { [0.0f32; 17] }; @@ -201,9 +249,9 @@ impl ShaderDriver { let min_resonance = style_cfg.resonance_threshold; for (i, &row_i) in passed_rows.iter().enumerate() { - let fp_i = self.bindspace.fingerprints.content_row(row_i as usize); + let fp_i = backing.content_row(row_i as usize); for (j_off, &row_j) in passed_rows.iter().enumerate().skip(i + 1) { - let fp_j = self.bindspace.fingerprints.content_row(row_j as usize); + let fp_j = backing.content_row(row_j as usize); let fp_i_bytes = unsafe { std::slice::from_raw_parts(fp_i.as_ptr() as *const u8, WORDS_PER_FP * 8) }; @@ -241,7 +289,7 @@ impl ShaderDriver { } // Use the SPO `s_idx` of the row's edge as the query palette index. // Rows with edge=0 default to palette 0 (identity probe). - let edge = CausalEdge64(self.bindspace.edges.get(row as usize)); + let edge = backing.edge(row as usize); let query = edge.s_idx(); let raw = shader.cascade(query, req.radius, req.layer_mask); for hit in raw.into_iter().take(4) { @@ -287,7 +335,7 @@ impl ShaderDriver { // even in binary space; full f32 VSA bundle is the next step. let mut cycle_fp = [0u64; WORDS_PER_FP]; for h in &hits { - let row_words = self.bindspace.fingerprints.content_row(h.row as usize); + let row_words = backing.content_row(h.row as usize); let pos = (h.cycle_index as usize) % WORDS_PER_FP; for (i, w) in row_words.iter().enumerate() { cycle_fp[(i + pos) % WORDS_PER_FP] ^= *w; @@ -353,7 +401,9 @@ impl ShaderDriver { .first() .copied() .and_then(|r| { - let etid = self.bindspace.entity_type[r as usize]; + // entity_type routes through the shim; ontology() stays on the + // singleton (the registry re-home is W4b — see plan §90). + let etid = backing.entity_type(r as usize); if etid == 0 { return None; } @@ -449,12 +499,7 @@ impl ShaderDriver { // Pre-materialize hit qualia as f32 so references are valid for the closure. let hit_qualia_f32: Vec<(u32, [f32; 17])> = hits .iter() - .map(|h| { - ( - h.row, - self.bindspace.qualia.row(h.row as usize).to_f32_17d(), - ) - }) + .map(|h| (h.row, backing.qualia_17d(h.row as usize))) .collect(); let alpha_composite = if effective_merge == MergeMode::AlphaFrontToBack { let threshold = req diff --git a/crates/cognitive-shader-driver/src/engine_bridge.rs b/crates/cognitive-shader-driver/src/engine_bridge.rs index 252e20ff..be73feae 100644 --- a/crates/cognitive-shader-driver/src/engine_bridge.rs +++ b/crates/cognitive-shader-driver/src/engine_bridge.rs @@ -311,6 +311,22 @@ pub fn dispatch_busdto(bs: &mut BindSpace, row: usize, bus: &BusDto, style_ord: /// Tolerance: bit-exact for codebook_index, top_k indices with positive /// energy at encode, energies (f32 in qualia), cycle_count, converged. /// LOSSY for top_k entries with non-positive energy at encode. +/// +/// ## Feature interaction: `mailbox-thoughtspace` (C5 / D-DIST-5) +/// +/// The non-headline `top_k[*].idx` recovery reads the `cycle` plane +/// (`Vsa16kF32` Binary16K set-bits). That plane is the deprecated one the +/// `MailboxSoA` migration **never carries** (it is computed transiently, never +/// a mailbox column — see `mailbox_soa.rs`). Under `mailbox-thoughtspace` the +/// cycle-plane index recovery is therefore feature-gated OUT: only the headline +/// `codebook_index` (stored losslessly in `qualia[9]`) survives, and the +/// non-headline `top_k[1..].idx` recover as `0`. This is an explicit downgrade +/// of an ALREADY-lossy register recovery (`I-VSA-IDENTITIES`: VSA bundles +/// identities, not content — the cycle-plane set-bits were never a faithful +/// register), on a `#[cfg(with-engine)]` lab path the live `run()` never reads. +/// Migration successor: read indices from the SoA edge/identity columns, not the +/// dropped cycle plane. The singleton (default) build keeps the bit-exact +/// cycle-plane recovery via `#[cfg(not(feature = "mailbox-thoughtspace"))]`. #[cfg(feature = "with-engine")] pub fn unbind_busdto(bs: &BindSpace, row: usize) -> BusDto { assert!( @@ -319,59 +335,64 @@ pub fn unbind_busdto(bs: &BindSpace, row: usize) -> BusDto { bs.len ); - // [1] qualia → energy + top_k energies. + // [1] qualia → energy + top_k energies + headline codebook_index. // D-CSV-5b: bs.qualia is now QualiaI4Column; convert to f32 at the read site. + // codex P2 fix (2026-05-07): the headline is stored explicitly in qualia[9] + // at encode, so it round-trips bit-exact regardless of cycle-plane state. let q_i4 = bs.qualia.row(row); let q = q_i4.to_f32_17d(); // [f32; 17] — sufficient for dims 0..9 let energy = q[0]; + let codebook_index = q[9] as u16; let mut top_k = [(0u16, 0.0f32); 8]; for i in 0..8 { top_k[i].1 = q[TOP_K_ENERGY_BASE_DIM + i]; } - // [2] cycle column → recover indices from set bits. - // Project Vsa16kF32 back to Binary16K (sign threshold → bit). - let cycle = bs.fingerprints.cycle_row(row); - let mut cycle_arr = [0.0f32; crate::bindspace::FLOATS_PER_VSA]; - cycle_arr.copy_from_slice(cycle); - let bits = lance_graph_contract::crystal::vsa16k_to_binary16k_threshold(&cycle_arr); - let set_bits: Vec = (0..(WORDS_PER_FP * 64)) - .filter(|&pos| bits[pos / 64] & (1u64 << (pos % 64)) != 0) - .map(|pos| pos as u16) - .collect(); - - // [3] Reconstruct top_k indices in the slots where the encoder set them. - // codex P2 fix (2026-05-07): the headline (codebook_index) is now - // stored explicitly in qualia[9] at encode, so we read it back - // directly rather than guessing from set_bits.iter().next() (which - // returned the LOWEST set bit, not the original headline, when - // codebook_index collided with or exceeded any positive-energy - // top_k index). The set_bits iterator now feeds only the - // non-headline top_k slots. - let codebook_index = q[9] as u16; - let mut bit_iter = set_bits.iter().copied().filter(|&b| b != codebook_index); - // For each positive-energy top_k slot at encode, attach the next set bit. - // We can't perfectly recover ordering for ties; we use the natural ascending - // bit order, which matches the encoder's deterministic walk for distinct indices. - // Note: the headline often equals top_k[0].idx — rebuild that match first. - if top_k[0].1 > 0.0 { - top_k[0].0 = codebook_index; - } - // Fill remaining positive-energy top_k slots from the remaining set bits. - // Skip the headline bit if top_k[0] used it. (bit_iter already filters - // out codebook_index above, so no second filter pass is needed.) - let remaining: Vec = bit_iter.collect(); - let mut r = remaining.into_iter(); - let skip_head = top_k[0].1 > 0.0; - for slot in top_k.iter_mut().skip(if skip_head { 1 } else { 0 }) { - if slot.1 > 0.0 { - if let Some(b) = r.next() { - slot.0 = b; + // [2/3] cycle column → recover NON-headline top_k indices from set bits. + // SINGLETON BUILD ONLY — the cycle plane is never migrated to the + // mailbox (C5 / D-DIST-5). Under `mailbox-thoughtspace` this block is + // gated out and the non-headline indices stay 0 (documented loss). + #[cfg(not(feature = "mailbox-thoughtspace"))] + { + // Project Vsa16kF32 back to Binary16K (sign threshold → bit). + let cycle = bs.fingerprints.cycle_row(row); + let mut cycle_arr = [0.0f32; crate::bindspace::FLOATS_PER_VSA]; + cycle_arr.copy_from_slice(cycle); + let bits = lance_graph_contract::crystal::vsa16k_to_binary16k_threshold(&cycle_arr); + let set_bits: Vec = (0..(WORDS_PER_FP * 64)) + .filter(|&pos| bits[pos / 64] & (1u64 << (pos % 64)) != 0) + .map(|pos| pos as u16) + .collect(); + + // The set_bits iterator feeds only the non-headline top_k slots. + let bit_iter = set_bits.iter().copied().filter(|&b| b != codebook_index); + // The headline often equals top_k[0].idx — rebuild that match first. + if top_k[0].1 > 0.0 { + top_k[0].0 = codebook_index; + } + // Fill remaining positive-energy top_k slots from the remaining set bits. + let remaining: Vec = bit_iter.collect(); + let mut r = remaining.into_iter(); + let skip_head = top_k[0].1 > 0.0; + for slot in top_k.iter_mut().skip(if skip_head { 1 } else { 0 }) { + if slot.1 > 0.0 { + if let Some(b) = r.next() { + slot.0 = b; + } } } + // If top_k[0].1 was non-positive but the encoder always sets the headline, + // we still recovered codebook_index above — it's authoritative. + } + // Under `mailbox-thoughtspace`: top_k[0].idx still gets the headline if it + // had positive energy (the headline is in qualia[9], not the dropped plane); + // all other non-headline indices remain 0 (the documented C5 loss). + #[cfg(feature = "mailbox-thoughtspace")] + { + if top_k[0].1 > 0.0 { + top_k[0].0 = codebook_index; + } } - // If top_k[0].1 was non-positive but the encoder always sets the headline, - // we still recovered codebook_index above — it's authoritative. // [4] meta column → converged. let m = bs.meta.get(row); diff --git a/crates/cognitive-shader-driver/src/lib.rs b/crates/cognitive-shader-driver/src/lib.rs index 741cbbd6..f22b6b68 100644 --- a/crates/cognitive-shader-driver/src/lib.rs +++ b/crates/cognitive-shader-driver/src/lib.rs @@ -93,6 +93,7 @@ pub mod attention_mask; pub mod attention_mask_actor; pub mod auto_style; +pub(crate) mod backing; pub mod bindspace; pub mod driver; pub mod engine_bridge; diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index df6fdf3b..34bc5412 100644 --- a/crates/cognitive-shader-driver/src/mailbox_soa.rs +++ b/crates/cognitive-shader-driver/src/mailbox_soa.rs @@ -1191,4 +1191,171 @@ mod tests { assert_eq!(mb.populated(), 1024, "set_populated clamps to N"); assert_eq!(mb.n_rows(), 1024, "n_rows() tracks the clamped populated"); } + + // ── test 18: W4a field-isolation matrix — each column write is independent ─ + + /// **The layout-bit-boundary regression guard (W4a, I-LEGACY-API-FEATURE-GATED).** + /// Writing one migrated column on a row must leave EVERY other migrated column + /// on that row byte-unchanged. This is the field-isolation matrix the iron rule + /// mandates whenever a layout reclaims or co-locates per-row state. We seed a + /// row with a known baseline across all columns, then mutate exactly one column + /// at a time and assert the others are untouched. + #[test] + fn test_mailbox_soa_field_isolation_matrix() { + const N: usize = 4; + const R: usize = 2; + + // Baseline values (distinct, non-zero where the column allows). + let base_edge = CausalEdge64(0x1111_2222_3333_4444); + let base_qualia = QualiaI4_16D::ZERO.with(0, 3).with(5, -4); + let base_meta = MetaWord::new(5, 2, 100, 120, 7); + let base_etype = 42u16; + let base_temporal = 0x9999_0000_0000_0001u64; + let base_expert = 77u16; + let base_sigma = 9u8; + let base_content = { + let mut w = [0u64; WORDS_PER_FP]; + w[0] = 0xCAFE; + w[WORDS_PER_FP - 1] = 0xBEEF; + w + }; + + let seed = |mb: &mut MailboxSoA| { + mb.set_edge(R, base_edge); + mb.set_qualia(R, base_qualia); + mb.set_meta(R, base_meta); + mb.set_entity_type(R, base_etype); + mb.set_temporal(R, base_temporal); + mb.set_expert(R, base_expert); + mb.set_sigma(R, base_sigma); + mb.set_content(R, &base_content); + }; + + // Assert all columns EXCEPT `changed` match baseline. `changed` is a tag. + let assert_others_unchanged = |mb: &MailboxSoA, changed: &str| { + if changed != "edge" { + assert_eq!(mb.edge(R).0, base_edge.0, "edge changed by {changed}"); + } + if changed != "qualia" { + assert_eq!(mb.qualia_at(R), base_qualia, "qualia changed by {changed}"); + } + if changed != "meta" { + assert_eq!(mb.meta_at(R).0, base_meta.0, "meta changed by {changed}"); + } + if changed != "entity_type" { + assert_eq!( + mb.entity_type_at(R), + base_etype, + "entity_type changed by {changed}" + ); + } + if changed != "temporal" { + assert_eq!( + mb.temporal_at(R), + base_temporal, + "temporal changed by {changed}" + ); + } + if changed != "expert" { + assert_eq!(mb.expert_at(R), base_expert, "expert changed by {changed}"); + } + if changed != "sigma" { + assert_eq!(mb.sigma_at(R), base_sigma, "sigma changed by {changed}"); + } + if changed != "content" { + assert_eq!( + mb.content_row(R), + &base_content[..], + "content changed by {changed}" + ); + } + }; + + // Mutate each column to a DISTINCT new value, one at a time, from a fresh + // baseline each iteration, and assert isolation. + { + let mut mb: MailboxSoA = MailboxSoA::new(1, 0, 1.0); + seed(&mut mb); + mb.set_edge(R, CausalEdge64(0xDEAD_BEEF_DEAD_BEEF)); + assert_others_unchanged(&mb, "edge"); + } + { + let mut mb: MailboxSoA = MailboxSoA::new(1, 0, 1.0); + seed(&mut mb); + mb.set_qualia(R, QualiaI4_16D::ZERO.with(15, 7)); + assert_others_unchanged(&mb, "qualia"); + } + { + let mut mb: MailboxSoA = MailboxSoA::new(1, 0, 1.0); + seed(&mut mb); + mb.set_meta(R, MetaWord::new(11, 3, 5, 6, 1)); + assert_others_unchanged(&mb, "meta"); + } + { + let mut mb: MailboxSoA = MailboxSoA::new(1, 0, 1.0); + seed(&mut mb); + mb.set_entity_type(R, 999); + assert_others_unchanged(&mb, "entity_type"); + } + { + let mut mb: MailboxSoA = MailboxSoA::new(1, 0, 1.0); + seed(&mut mb); + mb.set_temporal(R, 0x1234_5678_9ABC_DEF0); + assert_others_unchanged(&mb, "temporal"); + } + { + let mut mb: MailboxSoA = MailboxSoA::new(1, 0, 1.0); + seed(&mut mb); + mb.set_expert(R, 12345); + assert_others_unchanged(&mb, "expert"); + } + { + let mut mb: MailboxSoA = MailboxSoA::new(1, 0, 1.0); + seed(&mut mb); + mb.set_sigma(R, 200); + assert_others_unchanged(&mb, "sigma"); + } + { + let mut mb: MailboxSoA = MailboxSoA::new(1, 0, 1.0); + seed(&mut mb); + let mut other = [0u64; WORDS_PER_FP]; + other[3] = 0xABCD; + mb.set_content(R, &other); + assert_others_unchanged(&mb, "content"); + } + } + + // ── test 19: cycle-plane footprint — no cycle* symbol, ~6 KB/row hot ────── + + /// **The cycle-drop proof (cycle-plane is NEVER migrated).** The mailbox's + /// hot per-row footprint is the three dense identity planes (content / topic + /// / angle), each `WORDS_PER_FP` u64 = 2 KB, totalling ≈ 6 KB/row — NOT the + /// ≈ 71.6 KB/row a BindSpace row costs (dominated by the 64 KB Vsa16kF32 + /// `cycle` plane). The absence of any `cycle*` storage on `MailboxSoA` is the + /// structural guarantee; this test pins the dense-plane byte count. + #[test] + fn test_mailbox_soa_hot_footprint_excludes_cycle_plane() { + const N: usize = 1024; + // Per-row dense identity planes: 3 × WORDS_PER_FP × 8 bytes. + let per_row_dense = 3 * WORDS_PER_FP * 8; + assert_eq!(per_row_dense, 6144, "content+topic+angle = 6 KB/row"); + + // The whole dense backing store is exactly N × per_row_dense bytes. + let mb: MailboxSoA = MailboxSoA::new(1, 0, 1.0); + let dense_bytes = (mb.content.len() + mb.topic.len() + mb.angle.len()) * 8; + assert_eq!( + dense_bytes, + N * per_row_dense, + "dense planes total = N × 6 KB" + ); + + // The BindSpace per-row cost (incl. the 64 KB Vsa16kF32 cycle plane) is + // ~71.6 KB — the mailbox is ~12× lighter per row because cycle is dropped. + const BINDSPACE_PER_ROW: usize = 71_713; // see bindspace::tests footprint(1) + assert!( + per_row_dense * 11 < BINDSPACE_PER_ROW, + "mailbox hot row ({per_row_dense} B) must be >10× lighter than a \ + BindSpace row ({BINDSPACE_PER_ROW} B) — the dropped 64 KB cycle plane" + ); + } } diff --git a/crates/cognitive-shader-driver/tests/busdto_bridge_test.rs b/crates/cognitive-shader-driver/tests/busdto_bridge_test.rs index 52089063..8a230a40 100644 --- a/crates/cognitive-shader-driver/tests/busdto_bridge_test.rs +++ b/crates/cognitive-shader-driver/tests/busdto_bridge_test.rs @@ -39,6 +39,12 @@ fn make_dense_bus(seed: u16) -> BusDto { } } +// The full index-recovery round-trips (dense + sparse + zero-headline) depend +// on the `cycle` plane, which `mailbox-thoughtspace` drops (C5 / D-DIST-5). +// They stay LIVE on the singleton (default) build; a separate mailbox-arm test +// below pins the documented loss. Energy/cycle_count/converged/headline parity +// holds on BOTH builds and is asserted unconditionally where convenient. +#[cfg(not(feature = "mailbox-thoughtspace"))] #[test] fn busdto_round_trip_dense_top_k_is_bit_exact() { let mut bs = BindSpace::zeros(8); @@ -91,6 +97,7 @@ fn busdto_round_trip_dense_top_k_is_bit_exact() { } } +#[cfg(not(feature = "mailbox-thoughtspace"))] #[test] fn busdto_round_trip_sparse_top_k_preserves_positive_idx_set() { // Mix of positive + zero + negative energy supporters. The encoder only @@ -217,3 +224,66 @@ fn busdto_round_trip_zero_codebook_index_is_handled() { assert_eq!(recovered.converged, true); assert_eq!(recovered.energy.to_bits(), 0.1f32.to_bits()); } + +// ── C5 / D-DIST-5: mailbox-arm downgraded contract (pin the documented loss) ── +// +// Under `mailbox-thoughtspace` the `cycle` plane is dropped, so `unbind_busdto` +// recovers ONLY the headline `codebook_index` (from qualia[9]); the non-headline +// `top_k[1..].idx` recover as `0`. This test asserts that loss EXPLICITLY rather +// than tolerating it (never relax the bit-exact singleton tests above — they +// stay live on the default build). Requires BOTH `with-engine` (BusDto) and +// `mailbox-thoughtspace` (the gated path). +#[cfg(feature = "mailbox-thoughtspace")] +#[test] +fn busdto_mailbox_arm_recovers_headline_only_nonheadline_idx_zero() { + let mut bs = BindSpace::zeros(4); + // Distinct non-zero indices so a non-zero recovery would be visible. + let bus = BusDto { + codebook_index: 4321, + energy: 0.77, + top_k: [ + (4321, 0.9), // headline-matching positive slot + (1111, 0.8), // positive non-headline — idx WILL be lost (→ 0) + (2222, 0.7), // positive non-headline — idx WILL be lost (→ 0) + (3333, 0.6), + (0, 0.0), + (0, 0.0), + (0, 0.0), + (0, 0.0), + ], + cycle_count: 5, + converged: true, + }; + dispatch_busdto(&mut bs, 0, &bus, 1); + let recovered = unbind_busdto(&bs, 0); + + // Headline survives — qualia[9] is lossless and not on the dropped plane. + assert_eq!( + recovered.codebook_index, 4321, + "headline codebook_index must survive the cycle-plane drop (qualia[9])" + ); + // top_k[0].idx echoes the headline because top_k[0] had positive energy. + assert_eq!( + recovered.top_k[0].0, 4321, + "top_k[0].idx echoes the headline (positive energy slot)" + ); + // Every NON-headline top_k idx is lost → recovers as 0 (the C5 loss). + for i in 1..8 { + assert_eq!( + recovered.top_k[i].0, 0, + "mailbox arm: non-headline top_k[{i}].idx must recover as 0 \ + (cycle plane dropped — C5 / D-DIST-5)" + ); + } + // Energies, cycle_count, converged still round-trip bit-exact (qualia/expert/meta). + for i in 0..8 { + assert_eq!( + recovered.top_k[i].1.to_bits(), + bus.top_k[i].1.to_bits(), + "top_k[{i}].energy must round-trip bit-exact even on the mailbox arm", + ); + } + assert_eq!(recovered.energy.to_bits(), bus.energy.to_bits()); + assert_eq!(recovered.cycle_count, 5); + assert_eq!(recovered.converged, true); +} diff --git a/crates/cognitive-shader-driver/tests/firewall.rs b/crates/cognitive-shader-driver/tests/firewall.rs new file mode 100644 index 00000000..52a5477e --- /dev/null +++ b/crates/cognitive-shader-driver/tests/firewall.rs @@ -0,0 +1,121 @@ +//! CausalEdge64 firewall CI lint (C6) — the substrate-twin bar with teeth. +//! +//! `cognitive-shader-driver` must use exactly ONE `CausalEdge64`: the +//! `causal_edge::{edge::,}CausalEdge64` re-export. Two layout-incompatible +//! twins exist in the workspace and must NEVER be imported into this crate's +//! `src/`: +//! * `ndarray::hpc::causal_diff::CausalEdge64` +//! * `thinking_engine::layered::CausalEdge64` +//! +//! This test walks `src/` (std::fs, no external deps) and FAILS if either twin +//! path — or an aliased `... as` form of it — appears. The legitimate +//! `#[repr(transparent)]` reinterpret cast sites in `mailbox_soa.rs` +//! (`edges_raw` / `meta_raw`) reference only `causal_edge::CausalEdge64`, so +//! they are not matched; the test is scoped to the twin module paths, not the +//! word `CausalEdge64` itself. +//! +//! Source is CLEAN today (no twin imports). This gate keeps it that way as the +//! migration adds mailbox edge plumbing. + +use std::fs; +use std::path::{Path, PathBuf}; + +/// The two forbidden twin module paths, tolerant of whitespace around `::`. +/// Matching is substring-based after whitespace normalization, so it also +/// catches `use ndarray::hpc::causal_diff::CausalEdge64 as Foo;` aliases. +const FORBIDDEN_TWINS: &[&str] = &[ + "ndarray::hpc::causal_diff::CausalEdge64", + "thinking_engine::layered::CausalEdge64", +]; + +fn collect_rs_files(dir: &Path, out: &mut Vec) { + let entries = match fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return, + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_rs_files(&path, out); + } else if path.extension().and_then(|e| e.to_str()) == Some("rs") { + out.push(path); + } + } +} + +/// Normalize whitespace around `::` so `causal_diff :: CausalEdge64` is caught. +fn normalize(src: &str) -> String { + let mut s = src.to_string(); + // Collapse any spacing around `::` to the bare `::` token. + while s.contains(" ::") { + s = s.replace(" ::", "::"); + } + while s.contains(":: ") { + s = s.replace(":: ", "::"); + } + s +} + +#[test] +fn no_substrate_twin_causaledge64_in_src() { + // The crate's own src/ directory (resolved relative to CARGO_MANIFEST_DIR + // so the test is location-independent). + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let src_dir = Path::new(manifest_dir).join("src"); + assert!( + src_dir.is_dir(), + "firewall: src dir not found at {}", + src_dir.display() + ); + + let mut files = Vec::new(); + collect_rs_files(&src_dir, &mut files); + assert!( + !files.is_empty(), + "firewall: walked src/ but found no .rs files — the walk is broken" + ); + + let mut violations: Vec = Vec::new(); + for file in &files { + let raw = match fs::read_to_string(file) { + Ok(c) => c, + Err(_) => continue, + }; + let norm = normalize(&raw); + for twin in FORBIDDEN_TWINS { + if norm.contains(twin) { + violations.push(format!( + "{} imports forbidden twin `{}`", + file.display(), + twin + )); + } + } + } + + assert!( + violations.is_empty(), + "CausalEdge64 firewall BLOCK — cognitive-shader-driver must use only \ + `causal_edge::CausalEdge64`, never a substrate twin:\n {}", + violations.join("\n ") + ); +} + +/// Meta-sanity: the firewall's own matcher must actually fire on a planted +/// twin string (guards against a normalize() bug silently disabling the gate). +#[test] +fn firewall_matcher_detects_planted_twin() { + let planted = "use ndarray :: hpc :: causal_diff :: CausalEdge64;"; + let norm = normalize(planted); + assert!( + FORBIDDEN_TWINS.iter().any(|t| norm.contains(t)), + "firewall matcher failed to detect a planted (whitespaced) twin import \ + — the lint would not catch a real violation" + ); + // And the legitimate path must NOT trip the matcher. + let ok = normalize("use causal_edge::edge::CausalEdge64;"); + assert!( + !FORBIDDEN_TWINS.iter().any(|t| ok.contains(t)), + "firewall matcher false-positives on the canonical causal_edge path" + ); +} diff --git a/crates/cognitive-shader-driver/tests/w2_differential.rs b/crates/cognitive-shader-driver/tests/w2_differential.rs new file mode 100644 index 00000000..4c18a663 --- /dev/null +++ b/crates/cognitive-shader-driver/tests/w2_differential.rs @@ -0,0 +1,384 @@ +//! W2 differential harness — one `run()` body, two backing arms, bit-identical. +//! +//! The load-bearing proof of the W3+W4a read-shim: a `ShaderDriver` dispatched +//! against the singleton `BindSpace` and against a `MailboxSoA` that MIRRORS the +//! same window must produce a **byte-identical** `ShaderCrystal`. Every f32 is +//! compared via `to_bits()` (NOT a ULP tolerance): the two arms run the SAME +//! `run()` body and read identical bytes, so the arithmetic is bit-identical — +//! any ULP gap would mean the read path diverged, which is precisely the bug +//! this differential exists to catch (C4). +//! +//! Gated on `mailbox-thoughtspace` because the Mailbox arm of `BackingStore` +//! only exists under that feature. +//! +//! Construction detail: a `ShaderDriver` with NO registered mailbox routes its +//! `backing()` to the singleton fallback; a driver with the designated +//! `DEFAULT_MAILBOX` (id 0) registered routes to the mailbox arm. Both drivers +//! share the SAME `BindSpace`, planes, and semiring, so the only difference is +//! which substrate the six dispatch reads sweep. + +#![cfg(feature = "mailbox-thoughtspace")] + +use std::sync::Arc; + +use bgz17::base17::Base17; +use bgz17::palette::Palette; +use bgz17::palette_semiring::PaletteSemiring; +use cognitive_shader_driver::bindspace::{BindSpace, BindSpaceBuilder, WORDS_PER_FP}; +use cognitive_shader_driver::mailbox_soa::MailboxSoA; +use cognitive_shader_driver::{ + auto_style, CognitiveShaderBuilder, CognitiveShaderDriver, ColumnWindow, MetaFilter, MetaWord, + ShaderCrystal, ShaderDispatch, ShaderHit, StyleSelector, +}; +use lance_graph_contract::qualia::QualiaI4_16D; + +const DEFAULT_MAILBOX: u32 = 0; + +/// Deterministic per-row content fingerprint with a row-dependent bit pattern, +/// so distinct rows produce distinct Hamming distances (non-vacuous resonance). +fn content_for(row: usize) -> [u64; WORDS_PER_FP] { + let mut w = [0u64; WORDS_PER_FP]; + // A dense, overlapping-but-row-shifted pattern: rows near each other are + // similar (high resonance), distant rows differ (low) — exercises the + // content Hamming pre-pass meaningfully. + for bit in 0..2000usize { + if (bit + row * 13) % 7 < 5 { + w[bit / 64] |= 1u64 << (bit % 64); + } + } + w[0] ^= row as u64; // guarantee per-row distinctness + w +} + +fn qualia_for(row: usize) -> QualiaI4_16D { + QualiaI4_16D::ZERO + .with(0, (row % 7) as i8) + .with(3, -((row % 5) as i8)) + .with(9, (row % 4) as i8) +} + +/// entity_type stays 0 (untyped) for every row so the ontology ctx_id path is +/// neutralized — the differential isolates the column reads, not ontology +/// resolution (re-homed at W4b). +fn meta_for(row: usize) -> MetaWord { + MetaWord::new( + (row % 12) as u8, + 2, + 200u8.saturating_sub((row * 3) as u8), + 200u8.saturating_sub((row * 5) as u8), + (row % 6) as u8, + ) +} + +fn edge_for(row: usize) -> u64 { + // Vary s_idx (low byte) so the palette cascade query differs per row. + 0xAB00u64 | (row as u64 & 0xFF) +} + +fn demo_semiring() -> PaletteSemiring { + let entries: Vec = (0..16) + .map(|i| { + let mut dims = [0i16; 17]; + dims[0] = (i as i16) * 100; + dims[1] = ((i as i16) * 37) % 200; + Base17 { dims } + }) + .collect(); + let palette = Palette { entries }; + PaletteSemiring::build(&palette) +} + +fn demo_planes() -> [[u64; 64]; 8] { + let mut planes = [[0u64; 64]; 8]; + for (i, causes) in planes[0].iter_mut().take(16).enumerate() { + if i + 1 < 64 { + *causes |= 1u64 << (i + 1); + } + } + for (i, supports) in planes[2].iter_mut().take(16).enumerate() { + *supports |= 1u64 << i; + } + planes +} + +/// Build a `BindSpace` of `len` rows with distinct per-row columns. +fn build_bindspace(len: usize) -> BindSpace { + let mut b = BindSpaceBuilder::new(len); + for row in 0..len { + // entity_type = 0 (untyped) — neutralizes ontology (push, not push_typed). + b = b.push( + &content_for(row), + meta_for(row), + edge_for(row), + qualia_for(row), + 0, + 0, + ); + } + b.build() +} + +/// Build a `MailboxSoA<1024>` that mirrors a `len`-row `BindSpace` window, +/// row-for-row, across every migrated read column. `set_populated(len)` makes +/// it the `len`-row logical surrogate; `w_slot = 0` matches the edges' default. +fn mirror_mailbox(len: usize) -> MailboxSoA<1024> { + let mut mb: MailboxSoA<1024> = MailboxSoA::new(DEFAULT_MAILBOX, 0, 1.0); + for row in 0..len { + mb.set_content(row, &content_for(row)); + mb.set_qualia(row, qualia_for(row)); + mb.set_meta(row, meta_for(row)); + mb.set_edge(row, causal_edge::CausalEdge64(edge_for(row))); + mb.set_entity_type(row, 0); + } + mb.set_populated(len); + mb +} + +/// Assert two `ShaderCrystal`s are byte-identical across EVERY field (C4). +fn assert_crystals_bit_identical(a: &ShaderCrystal, b: &ShaderCrystal, ctx: &str) { + // ── bus.resonance ────────────────────────────────────────────────────── + let ra = &a.bus.resonance; + let rb = &b.bus.resonance; + assert_eq!(ra.hit_count, rb.hit_count, "{ctx}: hit_count"); + assert_eq!(ra.cycles_used, rb.cycles_used, "{ctx}: cycles_used"); + assert_eq!(ra.style_ord, rb.style_ord, "{ctx}: style_ord"); + assert_eq!( + ra.entropy.to_bits(), + rb.entropy.to_bits(), + "{ctx}: entropy bits" + ); + assert_eq!( + ra.std_dev.to_bits(), + rb.std_dev.to_bits(), + "{ctx}: std_dev bits" + ); + for k in 0..8 { + let (ha, hb) = (&ra.top_k[k], &rb.top_k[k]); + assert_hit_bit_identical(ha, hb, &format!("{ctx}: top_k[{k}]")); + } + + // ── bus.cycle_fingerprint / emitted_edges / gate ─────────────────────── + assert_eq!( + a.bus.cycle_fingerprint, b.bus.cycle_fingerprint, + "{ctx}: cycle_fingerprint" + ); + assert_eq!( + a.bus.emitted_edge_count, b.bus.emitted_edge_count, + "{ctx}: emitted_edge_count" + ); + assert_eq!( + a.bus.emitted_edges, b.bus.emitted_edges, + "{ctx}: emitted_edges" + ); + assert_eq!(a.bus.gate, b.bus.gate, "{ctx}: gate"); + + // ── persisted_row ────────────────────────────────────────────────────── + assert_eq!(a.persisted_row, b.persisted_row, "{ctx}: persisted_row"); + + // ── meta (MetaSummary: 3×f32 + bool) ─────────────────────────────────── + assert_eq!( + a.meta.confidence.to_bits(), + b.meta.confidence.to_bits(), + "{ctx}: meta.confidence bits" + ); + assert_eq!( + a.meta.meta_confidence.to_bits(), + b.meta.meta_confidence.to_bits(), + "{ctx}: meta.meta_confidence bits" + ); + assert_eq!( + a.meta.brier.to_bits(), + b.meta.brier.to_bits(), + "{ctx}: meta.brier bits" + ); + assert_eq!( + a.meta.should_admit_ignorance, b.meta.should_admit_ignorance, + "{ctx}: meta.should_admit_ignorance" + ); + + // ── materialize (MaterializeProvenance) ──────────────────────────────── + assert_eq!( + a.materialize.first_tactic, b.materialize.first_tactic, + "{ctx}: materialize.first_tactic" + ); + assert_eq!( + a.materialize.steps, b.materialize.steps, + "{ctx}: materialize.steps" + ); + assert_eq!( + a.materialize.rested, b.materialize.rested, + "{ctx}: materialize.rested" + ); + assert_eq!( + a.materialize.final_free_energy.to_bits(), + b.materialize.final_free_energy.to_bits(), + "{ctx}: materialize.final_free_energy bits" + ); + assert_eq!( + a.materialize.fork, b.materialize.fork, + "{ctx}: materialize.fork" + ); + + // ── alpha_composite (Option) ─────────────────────────── + match (&a.alpha_composite, &b.alpha_composite) { + (None, None) => {} + (Some(ca), Some(cb)) => { + assert_eq!( + ca.alpha_acc.to_bits(), + cb.alpha_acc.to_bits(), + "{ctx}: alpha_acc bits" + ); + assert_eq!( + ca.hits_consumed, cb.hits_consumed, + "{ctx}: alpha hits_consumed" + ); + assert_eq!(ca.saturated, cb.saturated, "{ctx}: alpha saturated"); + for (i, (x, y)) in ca.color_acc.iter().zip(cb.color_acc.iter()).enumerate() { + assert_eq!(x.to_bits(), y.to_bits(), "{ctx}: alpha color_acc[{i}] bits"); + } + } + _ => panic!("{ctx}: alpha_composite presence differs across arms"), + } +} + +fn assert_hit_bit_identical(a: &ShaderHit, b: &ShaderHit, ctx: &str) { + assert_eq!(a.row, b.row, "{ctx}: row"); + assert_eq!(a.distance, b.distance, "{ctx}: distance"); + assert_eq!(a.predicates, b.predicates, "{ctx}: predicates"); + assert_eq!(a._pad, b._pad, "{ctx}: _pad"); + assert_eq!( + a.resonance.to_bits(), + b.resonance.to_bits(), + "{ctx}: resonance bits" + ); + assert_eq!(a.cycle_index, b.cycle_index, "{ctx}: cycle_index"); +} + +/// Run the SAME dispatch on a singleton-backed driver and a mailbox-backed +/// driver (both over the identical BindSpace/planes/semiring) and assert +/// bit-identity. Returns the singleton crystal so the caller can probe +/// non-vacuity. +fn diff_dispatch(len: usize, req: &ShaderDispatch) -> ShaderCrystal { + let semiring = Arc::new(demo_semiring()); + let planes = demo_planes(); + + // Arm A — singleton (no mailbox registered → backing() = Singleton fallback). + let driver_singleton = CognitiveShaderBuilder::new() + .bindspace(Arc::new(build_bindspace(len))) + .semiring(semiring.clone()) + .planes(planes) + .build(); + + // Arm B — mailbox (DEFAULT_MAILBOX registered → backing() = Mailbox). + let driver_mailbox = CognitiveShaderBuilder::new() + .bindspace(Arc::new(build_bindspace(len))) + .semiring(semiring) + .planes(planes) + .with_mailbox(DEFAULT_MAILBOX, mirror_mailbox(len)) + .build(); + + let crystal_a = driver_singleton.dispatch(req); + let crystal_b = driver_mailbox.dispatch(req); + assert_crystals_bit_identical(&crystal_a, &crystal_b, "full-window vs mirror"); + crystal_a +} + +#[test] +fn w2_differential_full_window_bit_identical() { + let len = 12; + let req = ShaderDispatch { + rows: ColumnWindow::new(0, len as u32), + meta_prefilter: MetaFilter::ALL, + layer_mask: 0xFF, + radius: u16::MAX, + style: StyleSelector::Ordinal(auto_style::CREATIVE), + ..Default::default() + }; + let crystal = diff_dispatch(len, &req); + // Non-vacuity: a creative style over similar rows must produce ≥1 hit. + assert!( + crystal.bus.resonance.hit_count > 0, + "full-window dispatch must be non-vacuous (got 0 hits) — the \ + differential would pass trivially on an empty top_k" + ); + assert!( + crystal + .bus + .resonance + .top_k + .iter() + .any(|h| h.resonance > 0.0), + "expected at least one non-zero-resonance hit in top_k" + ); +} + +#[test] +fn w2_differential_non_zero_window_bit_identical() { + // C2 (P0): a window with start > 0. If the Mailbox prefilter iterated + // `0..populated` instead of honouring `win.start`, this is the case that + // diverges — same Vec shape, different rows. Bit-identity here is the + // sentinel against that exact sentinel-lie. + let len = 12; + let req = ShaderDispatch { + rows: ColumnWindow::new(1, (len - 1) as u32), // 1..11 + meta_prefilter: MetaFilter::ALL, + layer_mask: 0xFF, + radius: u16::MAX, + style: StyleSelector::Ordinal(auto_style::CREATIVE), + ..Default::default() + }; + let crystal = diff_dispatch(len, &req); + // Non-vacuity on the windowed case too: ≥1 hit must come from the [1,11) + // window, and the headline hit's row must be inside the window. + assert!( + crystal.bus.resonance.hit_count > 0, + "non-zero-window dispatch must be non-vacuous (got 0 hits)" + ); + let headline_row = crystal.bus.resonance.top_k[0].row; + assert!( + (1..(len as u32 - 1)).contains(&headline_row), + "headline hit row {headline_row} must lie inside the dispatched window [1, {})", + len - 1 + ); +} + +#[test] +fn w2_differential_with_meta_prefilter_bit_identical() { + // A restrictive prefilter (high nars_c_min) drops rows on BOTH arms; the + // surviving-row order must match so the auto-style probe + cascade agree. + let len = 12; + let req = ShaderDispatch { + rows: ColumnWindow::new(0, len as u32), + meta_prefilter: MetaFilter { + nars_c_min: 150, + ..MetaFilter::ALL + }, + layer_mask: 0xFF, + radius: u16::MAX, + style: StyleSelector::Ordinal(auto_style::ANALYTICAL), + ..Default::default() + }; + diff_dispatch(len, &req); +} + +#[test] +fn w2_differential_alpha_merge_bit_identical() { + // Exercise the α-front-to-back sink branch across both arms — its qualia + // closure reads through the shim per hit, so this proves the qualia read + // surface agrees under the Kerbl compositing path too. + let len = 12; + let req = ShaderDispatch { + rows: ColumnWindow::new(0, len as u32), + meta_prefilter: MetaFilter::ALL, + layer_mask: 0xFF, + radius: u16::MAX, + style: StyleSelector::Ordinal(auto_style::CREATIVE), + merge_override: Some(lance_graph_contract::collapse_gate::MergeMode::AlphaFrontToBack), + ..Default::default() + }; + let crystal = diff_dispatch(len, &req); + assert!( + crystal.alpha_composite.is_some(), + "AlphaFrontToBack override must populate alpha_composite on both arms" + ); +}