diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index b6e7fc8e..787a99ac 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,50 @@ +## 2026-06-16 — PR #507 review pass: 5+3 agent fleet (5 specialists + PP-13/15/16 hardeners) → 1 P0 + 2 P1 + P2 docs fixed + +**Main thread (Opus 4.7) spawned an 8-agent review fleet against PR #507** (the 4-task unblock-cascade below): 5 specialists (sentinel-qa, dto-soa-savant, iron-rule-savant, scenario-world, container-architect) + the 3 brutal hardeners (PP-13 brutally-honest-tester, PP-15 baton-handoff-auditor, PP-16 preflight-drift-auditor). Verdicts: dto-soa **LAND**, iron-rule **YIELDS-ALL**, container-architect **EXACT**, sentinel-qa **SOUND+P1**, scenario-world **CORRECT+P1**, PP-15 **CATCH-LATENT**, PP-16 **HOLD(P0)**, PP-13 **HOLD(P1×2)**. Consensus: mergeable after mechanical fixes; **zero REJECT, zero architectural rework** — "high-quality work" (PP-13), tests verified not-theater. + +**The fleet earned its keep — two real defects all four green test suites missed:** + +- **P0 (PP-16, root cause) — incomplete cherry-pick.** Task 2 cherry-picked `463d71b`'s `mailbox_soa.rs` half (+149) but **dropped its `causal-edge/src/edge.rs` half** (+6: the `#[repr(transparent)]` on `CausalEdge64` that is the layout enabler the `&[CausalEdge64]→&[u64]` reinterpret depends on). The SAFETY comment cited a `repr(transparent)` the type didn't carry — sound on today's rustc by newtype-layout coincidence, unsound by the letter, invisible to CI (borrow-checker ignores `repr`). Confirmed via `git show 463d71b --stat`. **Process lesson → EPIPHANIES E-CHERRYPICK-SPANS-CRATES-1.** +- **P1 (PP-13) — `fmt --check` overclaim.** The prior AGENT_LOG entry below said "clippy/fmt clean" but only clippy had been run; `cargo fmt --check` actually FAILED on PR-added lines (`hhtl.rs`, `scheduler.rs`, `view.rs`). Honest correction. + +**Fixes applied (new commit on the same branch — preserves review history):** +- **FIX-A (P0):** restored the dropped enabler — `#[repr(transparent)]` + doc on `CausalEdge64` (`causal-edge/src/edge.rs:148-156`); added `const _` size/align guards at the `edges_raw`/`meta_raw` cast sites (compile-error on any layout regression); corrected both SAFETY comments. +- **FIX-B (P1):** ran `cargo fmt` on all 5 touched crates; `fmt --check` now exits 0. +- **FIX-C (P1, PP-15):** `SurrealMailboxView::from_columns` `debug_assert_eq!` → `assert_eq!` (the column-length invariant now fails loudly in ALL profiles — closes a release-build OOB where a ragged kv-lance projection → `n_rows() > entity_type().len()` → `SoaWavePrimer::project` indexes out of bounds). + `from_columns_rejects_ragged_projection` panic test. +- **FIX-D (P1, 4 agents):** `pub fn base_path()` on `VersionedGraph`; deleted the `format!("{:?}")` Debug-scrape in `scheduler.rs` (embedded-quote truncation hazard). +- **FIX-E (PP-13):** `test_edges_raw_meta_raw_reinterpret_round_trips` — the unsafe cast had ZERO coverage; now bit-exact round-trip + pointer-identity asserted. +- **FIX-F/H (P2 docs):** `hhtl` bijection doc `0..=16` → `1..=16` (prefix(0)=EMPTY is ancestor of nothing); `drive_at_latest` scope note (version-agnostic policies only) + `versions().last()` upstream-pagination caveat tying the ascending-sort assumption to the lance =7.0.0 pin. + +**Disk verification this turn:** `git diff` confirms FIX-A landed (`#[repr(transparent)]` on `CausalEdge64` at `edge.rs:156`, `const _:` size/align guards at both cast sites in `mailbox_soa.rs`, `test_edges_raw_meta_raw_reinterpret_round_trips` at `mailbox_soa.rs:716`, `from_columns_rejects_ragged_projection` at `surreal_container/src/view.rs:257`). 34 files modified, +2841/-1100 — all uncommitted, awaits operator decision. + +**Discipline note:** this entry prepended BEFORE commit, per board-hygiene rule (board update must land in same commit, not as retroactive cleanup). + +--- + +## 2026-06-16 — 4-task unblock-cascade landing: NiblePath::from_guid_prefix + MailboxSoaOwner cherry-pick + LanceVersionScheduler + SurrealMailboxView (D-PG-6 contract slice) + +**Main thread (Opus 4.7) — single agent**, four ordered tasks responding to the user's "1 2 3 and 4" go-ahead on the shortest-unblocking-path list surfaced after #497-#501 + the surrealdb fork bump (`AdaWorldAPI/surrealdb` PR #34/#35/#36/#37 → main at `3aa6ab9` with `lance=7.0.0`/`lancedb=0.30.0`). All four committed together on `claude/odoo-savant-reasoners`, branch fast-forwarded through `cb14704`. + +**Tasks shipped (in dep order, smaller → larger):** + +1. **(3) `lance_graph_contract::hhtl::NiblePath::{from_guid_prefix, prefix}`** — the ontology-side keystone follow-up of #498 (`classid → ReadMode`). Deterministic 20→16 nibble fold of `classid_lo(4) | HEEL(4) | HIP(4) | TWIG(4)` (root-first), returns `None` when the canon-reserved high `u16` of classid is in use (refuses the lossy fold). `prefix(d)` is the O(1) single-shot ancestor view that satisfies `prefix(d).is_ancestor_of(self)` for every `d ≤ self.depth`. Zero-dep. +7 tests in `hhtl::tests` (612 → 619 → 632 contract lib green; clippy/fmt clean). + +2. **(2) `impl MailboxSoaView + MailboxSoaOwner for MailboxSoA`** — cherry-pick of commit `463d71b` (jolly-cori-clnf9, the +149 LOC the integrated-cognitive-planner-v1 §2 named as Seam #3). Adds `pub phase: KanbanColumn` field + zero-copy `repr(transparent)` slice impls (`edges_raw` / `meta_raw`) + the in-RAM Rubicon driving loop. The contract spine (#437/#439) now drives an actual loop — no surreal, no ractor bus needed for the in-process case. +1 driving-loop test (`test_in_ram_driving_loop_walks_rubicon_to_commit`, walks Planning → CognitiveWork → Evaluation → Commit with the −550 µs Libet anchor). 86 driver lib tests green. + +3. **(1) `lance_graph::graph::scheduler::LanceVersionScheduler`** — D-MBX-9-IN core impl (the CI-gated twin of the contract slice D-MBX-9-IN shipped 2026-05-31). Wraps a `VersionedGraph` + inner `VersionScheduler` and lowers a Lance `versions()` tick into the next legal `KanbanMove` via `drive_once` / `drive_at_latest` / `current_dataset_version`. Closes `E-SUBSTRATE-IS-THE-SCHEDULER`'s OUT-direction end-to-end. +5 tests, real on-disk tempdir Lance (no mocks). New module wired in `crates/lance-graph/src/graph/mod.rs`. + +4. **(4) `surreal_container::view::SurrealMailboxView`** — D-PG-6 contract slice (polyglot-container-query-membrane-v1 §D-PG-6). Read-only `MailboxSoaView` adapter that a SurrealQL projection over kv-lance populates via `from_columns(...)` — pure zero-copy borrow, no SurrealQL types cross the cognitive-side seam. Module imports `MailboxSoaView` but NOT `MailboxSoaOwner` (compile-time enforcement of the "surreal=project-read-only" ruling, `kanban.rs:1-21`). `read_via_kv_lance()` returns the new typed `SurrealContainerError::BlockedColdBuild` until the surrealdb fork dep in `Cargo.toml` is uncommented (kept off by default to avoid the cold-build cost on contributors who don't need it). +4 tests. New `lance-graph-contract` dep added to `surreal_container/Cargo.toml`; `BLOCKED(C)` note updated to RESOLVED. + +**Test summary (this session):** lance-graph-contract **632** (+7) · cognitive-shader-driver **86** (+1) · lance-graph::graph::scheduler **5** (new) · surreal_container::view **4** (new). All clippy `-D warnings` clean on my files (pre-existing lints in `lance-graph-ontology`/`lance-graph-planner`/`ndarray_bridge.rs` ignored — out of session scope). + +**Cross-PR unblocks closed by this commit:** +- D-MBX-9-IN-impl → SHIPPED (the contract trait now has a real Lance-backed implementor). +- D-MBX-A6-P3 (planner emit KanbanMove) → still queued, BUT Seams #3 (the loop) is now in-tree, so a downstream session can wire the emit-side without depending on the unmerged jolly branch. +- D-PG-6 (Rubicon kanban VIEW over surrealdb) → contract slice SHIPPED (typed `MailboxSoaView` impl); impl-side gated on `BlockedColdBuild` flip-on. +- Identity-architecture v1 §3 P-SCOPE-CLASSIFY blocker → solved (`from_guid_prefix` is deterministic + bijective + ancestor-preserving). + +**Discipline:** PR_ARC entry deferred until merge commit; board hygiene (LATEST_STATE Contract Inventory + EPIPHANIES E-UNBLOCK-CASCADE-1) landed in the SAME commit per the mandatory rule. + ## 2026-06-16 — 5-specialist framing of #497 OCR-transcode plans → plans rebaselined to #498 + probes spec'd **Main thread (Opus 4.8 1M) + 5 Opus specialists in parallel** (cascade-architect / family-codec-smith / palette-engineer / dto-soa-savant / truth-architect), each read the 7 merged #497 plans + post-#498 source in full (Rule 7 — read, don't grep-judge). Operator: *"review the plans against your awareness of the new architecture incl. the last 15 PR arc (Morton Cascade + Helix 48 + turbovec residue) — send 5 specialist framing it."* See `EPIPHANIES.md` E-OCR-PLAN-DRIFT-1 for the consolidated framing. diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 9fbffac0..fd2b4c79 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,31 @@ +## 2026-06-16 — E-UNBLOCK-CASCADE-1 — three independent fork/contract landings collapsed onto the same `MailboxSoaView` seam, closing four queued deliverables in one commit + +**Status:** FINDING. +**Confidence:** High — every claim is grounded in shipped code (`hhtl::from_guid_prefix`, `MailboxSoaOwner for MailboxSoA`, `LanceVersionScheduler`, `SurrealMailboxView`) and a single-pass `cargo test` sweep (632 + 86 + 5 + 4 green). + +**Context.** Three landings hit the workspace within ~24 h: +- PR #498 (`feat(contract): GUID decode→read-mode keystone + helix Signed360 right-size + OCR→NodeRow transcode`) — surfaced `NodeGuid::decode() → GuidParts` + `classid_read_mode`. +- `AdaWorldAPI/surrealdb` PR #34/#35/#36/#37 → main at `3aa6ab9` (lance/lancedb/object_store pins reconciled) — closed the `BLOCKED(C)` on `surreal_container`. +- The cherry-pick of `jolly-cori-clnf9` commit `463d71b` (the +149 LOC `MailboxSoaOwner` impl for `MailboxSoA`) had been the integrated-cognitive-planner-v1 Seam #3. + +**The find.** All three meet at exactly **one trait surface**: `lance_graph_contract::soa_view::MailboxSoaView`. Each landing made a DIFFERENT impl viable on the same boundary: +- **`MailboxSoA` (cognitive)** — the in-process owner+view, so the Rubicon loop runs in-RAM (was only on `jolly`). +- **`SurrealMailboxView<'a>` (surreal-side view)** — D-PG-6's read glove, now buildable end-to-end via the fork's `kv-lance` backend. +- **`NiblePath::from_guid_prefix`** — the ontology-side keystone follow-up of #498's `classid → ReadMode` LazyLock: a deterministic 20→16 nibble fold that satisfies the routing-prefix `is_ancestor_of` invariant the LE contract names. + +`LanceVersionScheduler` (D-MBX-9-IN core impl) sits one layer up and consumes ANY `V: MailboxSoaView` — so a single OUT-direction wrapper drives all three impls without case-splitting. The trait's read-only-by-design (`MailboxSoaView` has no mutator method) is the structural enforcement of `kanban.rs:1-21`'s "surreal=project-read-only, callcenter=commit" ruling; the SurrealQL adapter NOT importing `MailboxSoaOwner` is the compile-time tripwire if a future drift tries to mutate through the projection. + +**Why this matters.** Four "still BLOCKED" rows from the most recent unblock-list synthesis (last sync turn) all collapse onto a SINGLE commit, because the substrate already had the shape — only three independent dep/code landings had to converge. The pattern: +- Substrate trait designed once + multiple implementors (no `Box` in hot paths — generic `V: MailboxSoaView` everywhere). +- Read-only-by-trait-design = compile-time enforcement of the architectural ruling (no need for a runtime "you can't write through this" guard). +- A typed `BlockedColdBuild` error variant lets a heavy dep wire-up (surrealdb cold build) be deferred without breaking the contract-side adapter — the surface ships, the integrator flips it on in their branch. + +**Lesson.** When three plans cite the same trait surface as their unblock dependency, the first session that lands ANY one of the implementors should ALSO ship the trait-impl shape for the others (even as a stub returning a typed error). This collapses N independent post-unblock follow-ups into 1 commit's worth of trait engineering. The cost is ~50 LOC of stub + a typed error variant; the benefit is N − 1 fewer post-merge commits per queued plan. + +**Cross-ref.** Identity-architecture v1 §3 (the bijection-width problem); polyglot-container-query-membrane-v1 §D-PG-6 (the Rubicon kanban VIEW); integrated-cognitive-planner-v1 §2 (Seams #1–#6, the additive-only convergence); `E-SUBSTRATE-IS-THE-SCHEDULER` (the bidirectional kanban subscription); `kanban.rs:1-21` (the read-only ruling the trait shape enforces). + +--- + ## 2026-06-16 — E-OCR-PLAN-DRIFT-1 — the #497 OCR-transcode plans drifted from the substrate in 6 ways; 2 were showstoppers **Status:** FINDING (5-specialist framing — cascade-architect / family-codec-smith / palette-engineer / dto-soa-savant / truth-architect, each read the merged plans + source in full). diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 00b9c104..5ad91b2a 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -68,6 +68,8 @@ ## Current Contract Inventory (lance-graph-contract) +> **2026-06-16 — ADDED (4-task unblock-cascade)**: `lance_graph_contract::hhtl::NiblePath::{from_guid_prefix(&NodeGuid) -> Option, prefix(depth: u8) -> Option}` — the ontology-side keystone follow-up of #498's `classid → ReadMode` LE contract. The 20-nibble `classid · HEEL · HIP · TWIG` prefix is deterministically folded to 16 (the canon-reserved high `u16` of classid drops); returns `None` when the fold would be lossy (callers don't get silent collisions). `prefix(d)` is the O(1) single-shot ancestor view that satisfies `prefix(d).is_ancestor_of(self)` for every `d ≤ self.depth` — the routing-cache view of a deeper class path. **One layer up** in `cognitive-shader-driver::MailboxSoA`: `impl MailboxSoaView + MailboxSoaOwner` (cherry-pick of `jolly-cori-clnf9::463d71b`) + the `pub phase: KanbanColumn` field — the in-RAM Rubicon owner the contract's `MailboxSoaOwner` had no real implementor for (integrated-cognitive-planner-v1 §2 Seam #3 closed). In `lance_graph::graph::scheduler`: `LanceVersionScheduler` — D-MBX-9-IN core impl over `VersionedGraph::versions()`, generic over the inner `VersionScheduler` policy (closes `E-SUBSTRATE-IS-THE-SCHEDULER`'s OUT-direction). In `surreal_container::view`: `SurrealMailboxView<'a>` + `read_via_kv_lance()` (D-PG-6 contract slice) — the SurrealQL read-glove the integrator wires once the cold-build of the surrealdb fork is taken; the contract surface is available today. Plus `SurrealContainerError::BlockedColdBuild` — typed signal for callers to pattern-match the cold-build gate (distinct from the pre-existing `Blocked` variant which signals coordinate/API gaps). Zero-dep contract additions (+7 hhtl tests, 632 lib green); cognitive-shader-driver +1 driving-loop test (86 lib green); lance-graph::scheduler new module (+5 tests, real tempdir Lance); surreal_container::view new module (+4 tests). All four green; clippy `-D warnings` clean on the new files. EPIPHANIES `E-UNBLOCK-CASCADE-1` records the convergence of three independent landings onto the single `MailboxSoaView` trait surface. + > **2026-06-09 — ADDED (D-IDENTITY-1, Phase A of identity-architecture)**: `lance_graph_contract::identity::{NodeGuid([u8;16]), IDENTITY_LAYOUT_VERSION}` — the workspace's first **stable binary instance identity**: a structured 128-bit UUIDv8 (RFC 9562) = the HHTL nibble-address **formalized + namespaced**. **Composed from existing committed scalars, never re-invented** (Agent A sweep confirmed the 128-bit id space was empty): octets carry `namespace:u8 | entity_type:u16 | kind:u8` (the `SchemaPtr.packed` convention) ⊕ a truncated `NiblePath` routing prefix (`PREFIX_NIBBLES=4`) ⊕ a 22-bit `shape_hash` (truncated `StructuralSignature`) ⊕ a 24-bit `local`, with UUIDv8 version(=8)/variant(=0b10) at their RFC-fixed positions + an `IDENTITY_LAYOUT_VERSION` stamp. **Eineindeutigkeit**: `entity_type` is the canonical exact class identity; the `NiblePath` prefix is the bijective DERIVED view (a *truncated* prefix can't be the identity — deep classes collide past it; the prefix `is_ancestor_of` the full path). Five readings: resolve (`entity_type`) / route (`niblepath`) / witness (frozen bytes + merkle) / ground-truth (`shape_hash` drift) / dispatch-to-store (`as_bytes` → `EntityKey`). Also added `hhtl::NiblePath::from_packed` (inverse of `packed`). Zero-dep; 599 contract lib tests (+15: field-isolation matrix, UUIDv8 gates, ancestor-prefix invariant, Display=canonical-UUID); clippy `-D warnings` clean; fmt clean. Plans: `identity-architecture-exists-vs-needs-v1.md` (exists-vs-needs map + phases A→H), `cognitive-write-roundtrip-substrate-v1.md`. Epiphany: E-IDENTITY-WHITEBOX-1. > **2026-05-31 — ADDED (D-EW64-1 + D-VIEW-1, episodic-RISC-spine)**: `episodic_edges::{EpisodicEdges64(u64), EdgeRef{family:u8,local:u16}}` — AriGraph episodic edges, 4x[4-bit family | 12-bit local]: family 0 = intra-basin (inherited, ~98.6% per #444), 1..=15 = cross-family index into the OGIT-class-inherited palette (~1.4%; identities inherited, never on the edge — I-VSA-IDENTITIES). Plus `view_angle::ViewAngle` (4-bit view-schema selector; presence bitmask doubles as attention mask, inherited). Zero-dep; 527 contract lib tests; clippy pedantic+nursery clean. Plan: episodic-risc-spine-v1.md. diff --git a/Cargo.lock b/Cargo.lock index 71e275c4..4d272dbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8477,6 +8477,7 @@ version = "0.1.0" dependencies = [ "futures", "lance", + "lance-graph-contract", "lancedb", "snafu 0.8.9", "tempfile", diff --git a/crates/causal-edge/src/edge.rs b/crates/causal-edge/src/edge.rs index fa03f654..e5b1abc3 100644 --- a/crates/causal-edge/src/edge.rs +++ b/crates/causal-edge/src/edge.rs @@ -61,19 +61,19 @@ impl InferenceType { pub fn to_mantissa(self) -> i8 { match self { // Forward-chain (positive mantissa) - Self::Deduction => 1, - Self::Induction => 2, + Self::Deduction => 1, + Self::Induction => 2, // NOTE: Abduction is backward in the signed encoding; v1 enum maps it to +3 slot // for forward semantics, but in v2 signed scheme Abduction is negative direction. // For v1 back-compat, Abduction here returns the forward slot (+3 = Exemplification // slot). Callers must use from_mantissa() to distinguish signed direction. - Self::Abduction => -1, // backward: |1| = Abduction - Self::Revision => 4, // forward: Revision-positive slot - Self::Synthesis => 5, // forward: Synthesis slot - Self::Intervention => 6, // forward: PR-LL-1 Intervention (+6 per L-9) + Self::Abduction => -1, // backward: |1| = Abduction + Self::Revision => 4, // forward: Revision-positive slot + Self::Synthesis => 5, // forward: Synthesis slot + Self::Intervention => 6, // forward: PR-LL-1 Intervention (+6 per L-9) // Backward-chain (negative mantissa) Self::Counterfactual => -6, // backward: PR-LL-1 Counterfactual (−6 per L-9) - Self::Reserved7 => 7, // extension slot (positive, forward) + Self::Reserved7 => 7, // extension slot (positive, forward) } } @@ -87,14 +87,44 @@ impl InferenceType { let mag = m.unsigned_abs() & 0x7; // magnitude 0..7 let forward = m >= 0; match mag { - 0 => Self::Deduction, // 0 = Identity/neutral → treat as Deduction - 1 => if forward { Self::Deduction } else { Self::Abduction }, - 2 => if forward { Self::Induction } else { Self::Abduction }, // Contraposition → Abduction - 3 => if forward { Self::Synthesis } else { Self::Abduction }, // Exemplification / Analogy-neg - 4 => Self::Revision, // Revision +/- (same enum, sign distinguishes) - 5 => if forward { Self::Synthesis } else { Self::Synthesis }, // Synthesis / Decomposition - 6 => if forward { Self::Intervention } else { Self::Counterfactual }, // PR-LL-1 (L-9) - _ => Self::Reserved7, // 7 = Extension / Intension-negative (future) + 0 => Self::Deduction, // 0 = Identity/neutral → treat as Deduction + 1 => { + if forward { + Self::Deduction + } else { + Self::Abduction + } + } + 2 => { + if forward { + Self::Induction + } else { + Self::Abduction + } + } // Contraposition → Abduction + 3 => { + if forward { + Self::Synthesis + } else { + Self::Abduction + } + } // Exemplification / Analogy-neg + 4 => Self::Revision, // Revision +/- (same enum, sign distinguishes) + 5 => { + if forward { + Self::Synthesis + } else { + Self::Synthesis + } + } // Synthesis / Decomposition + 6 => { + if forward { + Self::Intervention + } else { + Self::Counterfactual + } + } // PR-LL-1 (L-9) + _ => Self::Reserved7, // 7 = Extension / Intension-negative (future) } } } @@ -114,7 +144,16 @@ impl InferenceType { /// [49:51] Plasticity flags (3 bits, hot/cold per S,P,O) /// [52:63] Temporal index (12 bits, 4096 time slots) /// ``` +/// +/// `#[repr(transparent)]`: a single-`u64` newtype whose layout is GUARANTEED +/// identical to `u64`, so `&[CausalEdge64]` can be soundly reinterpreted as +/// `&[u64]` (the zero-copy `MailboxSoaView::edges_raw` borrow in +/// `cognitive-shader-driver`). Layout-neutral — the in-memory representation +/// was already a bare `u64`; this only documents and compiler-enforces it for +/// the reinterpret (a future second field becomes a compile error). Mirrors +/// the ndarray twin `ndarray::hpc::causal_diff::CausalEdge64`. #[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] pub struct CausalEdge64(pub u64); // Bit field positions and masks @@ -470,9 +509,13 @@ impl CausalEdge64 { #[inline(always)] pub fn plasticity(self) -> PlasticityState { #[cfg(feature = "causal-edge-v2-layout")] - { PlasticityState::from_bits(((self.0 >> crate::layout::PLAST_SHIFT) & BITS3_MASK) as u8) } + { + PlasticityState::from_bits(((self.0 >> crate::layout::PLAST_SHIFT) & BITS3_MASK) as u8) + } #[cfg(not(feature = "causal-edge-v2-layout"))] - { PlasticityState::from_bits(((self.0 >> PLAST_SHIFT) & BITS3_MASK) as u8) } + { + PlasticityState::from_bits(((self.0 >> PLAST_SHIFT) & BITS3_MASK) as u8) + } } /// Set plasticity state. @@ -484,8 +527,8 @@ impl CausalEdge64 { #[cfg(feature = "causal-edge-v2-layout")] { let shift = crate::layout::PLAST_SHIFT; - self.0 = (self.0 & !(BITS3_MASK << shift)) - | (((p.bits() as u64) & BITS3_MASK) << shift); + self.0 = + (self.0 & !(BITS3_MASK << shift)) | (((p.bits() as u64) & BITS3_MASK) << shift); } #[cfg(not(feature = "causal-edge-v2-layout"))] { @@ -592,7 +635,8 @@ impl CausalEdge64 { // (e.g. -1 = 0b1111 reads as Reserved7), bypassing Abduction/ // Counterfactual semantics entirely. #[cfg(feature = "causal-edge-v2-layout")] - #[allow(deprecated)] // weight.inference_type() is the v1 fallback below; v2 uses mantissa + #[allow(deprecated)] + // weight.inference_type() is the v1 fallback below; v2 uses mantissa let resolved_infer = InferenceType::from_mantissa(weight.inference_mantissa()); #[cfg(not(feature = "causal-edge-v2-layout"))] let resolved_infer = weight.inference_type(); @@ -783,10 +827,10 @@ impl CausalEdge64 { direction: u8, plasticity: PlasticityState, ) -> Self { - use crate::layout::{S_SHIFT as LS, P_SHIFT as LP, O_SHIFT as LO, - FREQ_SHIFT as LF, CONF_SHIFT as LC, - CAUSAL_SHIFT as LCA, DIR_SHIFT as LD, - PLAST_SHIFT as LPL, BITS3_MASK as B3}; + use crate::layout::{ + BITS3_MASK as B3, CAUSAL_SHIFT as LCA, CONF_SHIFT as LC, DIR_SHIFT as LD, + FREQ_SHIFT as LF, O_SHIFT as LO, PLAST_SHIFT as LPL, P_SHIFT as LP, S_SHIFT as LS, + }; let mut v: u64 = 0; v |= (s_idx as u64) << LS; v |= (p_idx as u64) << LP; @@ -800,7 +844,6 @@ impl CausalEdge64 { // W-slot, truth-band, spare default to 0 Self(v) } - } // ─── V2 Accessors and Builders ───────────────────────────────────────────── @@ -819,7 +862,7 @@ impl CausalEdge64 { /// of unknown provenance. `CausalEdge64::ZERO` returns 0 (correct default). #[inline(always)] pub fn w_slot(self) -> u8 { - use crate::layout::{W_SHIFT, BITS6_MASK}; + use crate::layout::{BITS6_MASK, W_SHIFT}; ((self.0 >> W_SHIFT) & BITS6_MASK) as u8 } @@ -833,16 +876,26 @@ impl CausalEdge64 { /// sign-extended to i8 via arithmetic shift: if bit 3 set, OR with 0xF0. #[inline(always)] pub fn inference_mantissa(self) -> i8 { - use crate::layout::{INFER_SHIFT, BITS4_MASK}; + use crate::layout::{BITS4_MASK, INFER_SHIFT}; let raw = ((self.0 >> INFER_SHIFT) & BITS4_MASK) as u8; - if raw & 0x8 != 0 { (raw | 0xF0) as i8 } else { raw as i8 } + if raw & 0x8 != 0 { + (raw | 0xF0) as i8 + } else { + raw as i8 + } } /// Chain direction extracted from mantissa sign: 1=forward, −1=backward, 0=neutral. #[inline(always)] pub fn inference_direction(self) -> i8 { let m = self.inference_mantissa(); - if m > 0 { 1 } else if m < 0 { -1 } else { 0 } + if m > 0 { + 1 + } else if m < 0 { + -1 + } else { + 0 + } } /// Base NARS rule index (0..7) extracted from mantissa magnitude. @@ -857,7 +910,7 @@ impl CausalEdge64 { /// may read as Solid/Fuzzy/Murky. Apply a version gate on edges of unknown provenance. #[inline(always)] pub fn truth(self) -> crate::layout::TrustTexture { - use crate::layout::{TRUTH_SHIFT, BITS2_MASK, TrustTexture}; + use crate::layout::{TrustTexture, BITS2_MASK, TRUTH_SHIFT}; TrustTexture::from_bits_2(((self.0 >> TRUTH_SHIFT) & BITS2_MASK) as u8) } @@ -865,7 +918,7 @@ impl CausalEdge64 { /// Useful for round-trip tests and direct comparisons. #[inline(always)] pub fn truth_raw(self) -> u8 { - use crate::layout::{TRUTH_SHIFT, BITS2_MASK}; + use crate::layout::{BITS2_MASK, TRUTH_SHIFT}; ((self.0 >> TRUTH_SHIFT) & BITS2_MASK) as u8 } @@ -873,7 +926,7 @@ impl CausalEdge64 { /// Returns 0 for ZERO edges and all v1-written edges (temporal MSBs were ≤ 0xFFF). #[inline(always)] pub fn spare(self) -> u8 { - use crate::layout::{SPARE_SHIFT, BITS3_MASK}; + use crate::layout::{BITS3_MASK, SPARE_SHIFT}; ((self.0 >> SPARE_SHIFT) & BITS3_MASK) as u8 } @@ -882,7 +935,7 @@ impl CausalEdge64 { /// Return new edge with W slot set to `w` (0..=63). #[inline] pub fn with_w_slot(self, w: u8) -> Self { - use crate::layout::{W_SHIFT, BITS6_MASK, W_MASK}; + use crate::layout::{BITS6_MASK, W_MASK, W_SHIFT}; debug_assert!(w <= 63, "w_slot must fit 6 bits (0..=63), got {w}"); Self((self.0 & !W_MASK) | (((w as u64) & BITS6_MASK) << W_SHIFT)) } @@ -890,7 +943,7 @@ impl CausalEdge64 { /// Return new edge with truth-band lens set. #[inline] pub fn with_truth(self, t: crate::layout::TrustTexture) -> Self { - use crate::layout::{TRUTH_SHIFT, BITS2_MASK, TRUTH_MASK}; + use crate::layout::{BITS2_MASK, TRUTH_MASK, TRUTH_SHIFT}; Self((self.0 & !TRUTH_MASK) | ((t.to_bits_2() as u64 & BITS2_MASK) << TRUTH_SHIFT)) } @@ -900,7 +953,7 @@ impl CausalEdge64 { /// are naturally wrapped by the 4-bit mask (low nibble of `m as u8`). #[inline] pub fn with_inference_mantissa(self, m: i8) -> Self { - use crate::layout::{INFER_SHIFT, BITS4_MASK, INFER_MASK}; + use crate::layout::{BITS4_MASK, INFER_MASK, INFER_SHIFT}; debug_assert!((-8..=7).contains(&m), "mantissa must be −8..+7, got {m}"); let raw = (m as u8) & 0xF; Self((self.0 & !INFER_MASK) | ((raw as u64 & BITS4_MASK) << INFER_SHIFT)) @@ -909,7 +962,7 @@ impl CausalEdge64 { /// Return new edge with spare bits set (0..=7, 3-bit field). #[inline] pub fn with_spare(self, s: u8) -> Self { - use crate::layout::{SPARE_SHIFT, BITS3_MASK, SPARE_MASK}; + use crate::layout::{BITS3_MASK, SPARE_MASK, SPARE_SHIFT}; debug_assert!(s <= 7, "spare must fit 3 bits (0..=7), got {s}"); Self((self.0 & !SPARE_MASK) | ((s as u64 & BITS3_MASK) << SPARE_SHIFT)) } @@ -922,7 +975,7 @@ impl CausalEdge64 { /// Composable: `edge.with_routing(12, TrustTexture::Solid).with_inference_mantissa(-1)`. #[inline] pub fn with_routing(self, w: u8, t: crate::layout::TrustTexture) -> Self { - use crate::layout::{W_SHIFT, TRUTH_SHIFT, BITS6_MASK, BITS2_MASK, W_MASK, TRUTH_MASK}; + use crate::layout::{BITS2_MASK, BITS6_MASK, TRUTH_MASK, TRUTH_SHIFT, W_MASK, W_SHIFT}; debug_assert!(w <= 63, "w_slot ({w}) out of 6-bit range"); let routing = ((w as u64 & BITS6_MASK) << W_SHIFT) | ((t.to_bits_2() as u64 & BITS2_MASK) << TRUTH_SHIFT); @@ -934,7 +987,7 @@ impl CausalEdge64 { /// Set W slot in-place. #[inline] pub fn set_w_slot(&mut self, w: u8) { - use crate::layout::{W_SHIFT, BITS6_MASK, W_MASK}; + use crate::layout::{BITS6_MASK, W_MASK, W_SHIFT}; debug_assert!(w <= 63); self.0 = (self.0 & !W_MASK) | (((w as u64) & BITS6_MASK) << W_SHIFT); } @@ -942,14 +995,14 @@ impl CausalEdge64 { /// Set truth-band lens in-place. #[inline] pub fn set_truth(&mut self, t: crate::layout::TrustTexture) { - use crate::layout::{TRUTH_SHIFT, BITS2_MASK, TRUTH_MASK}; + use crate::layout::{BITS2_MASK, TRUTH_MASK, TRUTH_SHIFT}; self.0 = (self.0 & !TRUTH_MASK) | ((t.to_bits_2() as u64 & BITS2_MASK) << TRUTH_SHIFT); } /// Set signed inference mantissa in-place (range −8..+7). #[inline] pub fn set_inference_mantissa(&mut self, m: i8) { - use crate::layout::{INFER_SHIFT, BITS4_MASK, INFER_MASK}; + use crate::layout::{BITS4_MASK, INFER_MASK, INFER_SHIFT}; debug_assert!((-8..=7).contains(&m)); let raw = (m as u8) & 0xF; self.0 = (self.0 & !INFER_MASK) | ((raw as u64 & BITS4_MASK) << INFER_SHIFT); @@ -958,7 +1011,7 @@ impl CausalEdge64 { /// Set spare bits in-place (0..=7, 3-bit field). #[inline] pub fn set_spare(&mut self, s: u8) { - use crate::layout::{SPARE_SHIFT, BITS3_MASK, SPARE_MASK}; + use crate::layout::{BITS3_MASK, SPARE_MASK, SPARE_SHIFT}; debug_assert!(s <= 7); self.0 = (self.0 & !SPARE_MASK) | ((s as u64 & BITS3_MASK) << SPARE_SHIFT); } @@ -971,29 +1024,53 @@ impl CausalEdge64 { #[cfg(not(feature = "causal-edge-v2-layout"))] impl CausalEdge64 { #[inline(always)] - pub fn w_slot(self) -> u8 { 0 } + pub fn w_slot(self) -> u8 { + 0 + } #[inline(always)] - pub fn inference_mantissa(self) -> i8 { 0 } + pub fn inference_mantissa(self) -> i8 { + 0 + } #[inline(always)] - pub fn inference_direction(self) -> i8 { 0 } + pub fn inference_direction(self) -> i8 { + 0 + } #[inline(always)] - pub fn inference_rule_index(self) -> u8 { 0 } + pub fn inference_rule_index(self) -> u8 { + 0 + } #[inline(always)] - pub fn truth(self) -> crate::layout::TrustTexture { crate::layout::TrustTexture::Crystalline } + pub fn truth(self) -> crate::layout::TrustTexture { + crate::layout::TrustTexture::Crystalline + } #[inline(always)] - pub fn truth_raw(self) -> u8 { 0 } + pub fn truth_raw(self) -> u8 { + 0 + } #[inline(always)] - pub fn spare(self) -> u8 { 0 } + pub fn spare(self) -> u8 { + 0 + } #[inline] - pub fn with_w_slot(self, _w: u8) -> Self { self } + pub fn with_w_slot(self, _w: u8) -> Self { + self + } #[inline] - pub fn with_truth(self, _t: crate::layout::TrustTexture) -> Self { self } + pub fn with_truth(self, _t: crate::layout::TrustTexture) -> Self { + self + } #[inline] - pub fn with_inference_mantissa(self, _m: i8) -> Self { self } + pub fn with_inference_mantissa(self, _m: i8) -> Self { + self + } #[inline] - pub fn with_spare(self, _s: u8) -> Self { self } + pub fn with_spare(self, _s: u8) -> Self { + self + } #[inline] - pub fn with_routing(self, _w: u8, _t: crate::layout::TrustTexture) -> Self { self } + pub fn with_routing(self, _w: u8, _t: crate::layout::TrustTexture) -> Self { + self + } #[inline] pub fn set_w_slot(&mut self, _w: u8) {} #[inline] @@ -1004,7 +1081,6 @@ impl CausalEdge64 { pub fn set_spare(&mut self, _s: u8) {} } - impl std::fmt::Debug for CausalEdge64 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CausalEdge64") diff --git a/crates/causal-edge/src/layout.rs b/crates/causal-edge/src/layout.rs index 4b11276a..8a158abd 100644 --- a/crates/causal-edge/src/layout.rs +++ b/crates/causal-edge/src/layout.rs @@ -5,13 +5,13 @@ //! OQ-LAYOUT-1: RESOLVED. G-slot dropped (L-3). Mantissa = 4b signed i4 (L-4). // ── v1 fields preserved (shifts unchanged from v1) ───────────────────────── -pub const S_SHIFT: u32 = 0; -pub const P_SHIFT: u32 = 8; -pub const O_SHIFT: u32 = 16; -pub const FREQ_SHIFT: u32 = 24; -pub const CONF_SHIFT: u32 = 32; +pub const S_SHIFT: u32 = 0; +pub const P_SHIFT: u32 = 8; +pub const O_SHIFT: u32 = 16; +pub const FREQ_SHIFT: u32 = 24; +pub const CONF_SHIFT: u32 = 32; pub const CAUSAL_SHIFT: u32 = 40; -pub const DIR_SHIFT: u32 = 43; +pub const DIR_SHIFT: u32 = 43; // ── v1→v2 EXPANDED field ──────────────────────────────────────────────────── /// Inference mantissa: 4-bit signed (−8..+7). @@ -23,18 +23,18 @@ pub const DIR_SHIFT: u32 = 43; /// 7=Extension/Intension-negative (future). /// Encodes direction × NARS rule in one field. /// See pr-ce64-mb-2-causaledge64-v2.md §"Signed Mantissa Rationale". -pub const INFER_SHIFT: u32 = 46; +pub const INFER_SHIFT: u32 = 46; /// 4-bit unsigned mask for pack/unpack of the signed i4 mantissa field. -pub const BITS4_MASK: u64 = 0xF; +pub const BITS4_MASK: u64 = 0xF; /// Mask covering the mantissa field (bits 46-49) in the u64 word. -pub const INFER_MASK: u64 = BITS4_MASK << INFER_SHIFT; +pub const INFER_MASK: u64 = BITS4_MASK << INFER_SHIFT; // ── v1 field SHIFTED ──────────────────────────────────────────────────────── /// Plasticity flags: bits 50-52 (shifted by +1 from v1's bits 49-51 due to /// mantissa expansion from 3b unsigned to 4b signed i4 per L-4). -pub const PLAST_SHIFT: u32 = 50; +pub const PLAST_SHIFT: u32 = 50; // ── v1 field DEPRECATED ───────────────────────────────────────────────────── /// Deprecated: temporal field shift from v1. Bits 52-63 reclaimed in v2. @@ -51,26 +51,26 @@ pub const V1_TEMPORAL_SHIFT: u32 = 52; // ── v2 NEW fields (reclaimed from dropped temporal 12 bits) ───────────────── /// W slot: 6-bit witness corpus root handle (bits 53-58), 0..=63. /// 0 = no corpus anchor. Per cognitive-substrate-convergence-v1.md L-6. -pub const W_SHIFT: u32 = 53; +pub const W_SHIFT: u32 = 53; /// Truth-band lens: 2-bit TrustTexture ordinal (bits 59-60). /// 0 = Crystalline. Per cognitive-substrate-convergence-v1.md L-7. -pub const TRUTH_SHIFT: u32 = 59; +pub const TRUTH_SHIFT: u32 = 59; /// Spare: 3-bit reserved for sprint-12+ (bits 61-63). /// Candidates: Rubicon-commit marker, Markov-decay quantum, I-NOISE-FLOOR-JIRAK threshold. -pub const SPARE_SHIFT: u32 = 61; +pub const SPARE_SHIFT: u32 = 61; // ── Common masks ───────────────────────────────────────────────────────────── -pub const BYTE_MASK: u64 = 0xFF; -pub const BITS3_MASK: u64 = 0x7; -pub const BITS6_MASK: u64 = 0x3F; -pub const BITS2_MASK: u64 = 0x3; +pub const BYTE_MASK: u64 = 0xFF; +pub const BITS3_MASK: u64 = 0x7; +pub const BITS6_MASK: u64 = 0x3F; +pub const BITS2_MASK: u64 = 0x3; -pub const PLAST_MASK: u64 = BITS3_MASK << PLAST_SHIFT; -pub const W_MASK: u64 = BITS6_MASK << W_SHIFT; -pub const TRUTH_MASK: u64 = BITS2_MASK << TRUTH_SHIFT; -pub const SPARE_MASK: u64 = BITS3_MASK << SPARE_SHIFT; +pub const PLAST_MASK: u64 = BITS3_MASK << PLAST_SHIFT; +pub const W_MASK: u64 = BITS6_MASK << W_SHIFT; +pub const TRUTH_MASK: u64 = BITS2_MASK << TRUTH_SHIFT; +pub const SPARE_MASK: u64 = BITS3_MASK << SPARE_SHIFT; // ── Compile-time layout coverage assertion ──────────────────────────────────── /// Const-assert: all 64 bits covered exactly once. @@ -88,7 +88,7 @@ const _LAYOUT_COVERAGE: () = { | (BITS3_MASK << PLAST_SHIFT) // bits 50-52 (shifted from v1) | (BITS6_MASK << W_SHIFT) // bits 53-58 (NEW) | (BITS2_MASK << TRUTH_SHIFT) // bits 59-60 (NEW) - | (BITS3_MASK << SPARE_SHIFT); // bits 61-63 (NEW) + | (BITS3_MASK << SPARE_SHIFT); // bits 61-63 (NEW) assert!( all == u64::MAX, "CausalEdge64 v2 bit layout must cover all 64 bits exactly once" diff --git a/crates/causal-edge/src/v2_layout_tests.rs b/crates/causal-edge/src/v2_layout_tests.rs index 5e221baf..224403c7 100644 --- a/crates/causal-edge/src/v2_layout_tests.rs +++ b/crates/causal-edge/src/v2_layout_tests.rs @@ -29,10 +29,7 @@ mod v2_layout_tests { fn test_w_slot_roundtrip() { for w in [0u8, 1, 31, 63] { let edge = CausalEdge64::ZERO.with_w_slot(w); - assert_eq!( - edge.w_slot(), w, - "w_slot round-trip failed for w={w}" - ); + assert_eq!(edge.w_slot(), w, "w_slot round-trip failed for w={w}"); } } @@ -47,10 +44,7 @@ mod v2_layout_tests { TrustTexture::Murky, ] { let edge = CausalEdge64::ZERO.with_truth(t); - assert_eq!( - edge.truth(), t, - "truth round-trip failed for {t:?}" - ); + assert_eq!(edge.truth(), t, "truth round-trip failed for {t:?}"); } } @@ -62,7 +56,8 @@ mod v2_layout_tests { for m in [-8i8, -7, -1, 0, 1, 7] { let edge = CausalEdge64::ZERO.with_inference_mantissa(m); assert_eq!( - edge.inference_mantissa(), m, + edge.inference_mantissa(), + m, "inference_mantissa signed round-trip failed for m={m}" ); } @@ -75,7 +70,11 @@ mod v2_layout_tests { // v2 signature: with_routing(w: u8, t: TrustTexture) — no g parameter (L-3) let edge = CausalEdge64::ZERO.with_routing(42, TrustTexture::Fuzzy); assert_eq!(edge.w_slot(), 42, "with_routing: w_slot mismatch"); - assert_eq!(edge.truth(), TrustTexture::Fuzzy, "with_routing: truth mismatch"); + assert_eq!( + edge.truth(), + TrustTexture::Fuzzy, + "with_routing: truth mismatch" + ); } // ── test_v2_fields_do_not_disturb_v1_fields ──────────────────────────── @@ -85,13 +84,16 @@ mod v2_layout_tests { // Build a v1-style edge using the existing pack() (back-compat path). #[allow(deprecated)] let base = CausalEdge64::pack( - 143, 7, 201, // S, P, O palette indices - 209, 181, // NARS f=0.82, c=0.71 - CausalMask::PO, // interventional level - 0b101, // direction triad + 143, + 7, + 201, // S, P, O palette indices + 209, + 181, // NARS f=0.82, c=0.71 + CausalMask::PO, // interventional level + 0b101, // direction triad InferenceType::Deduction, PlasticityState::S_HOT, - 0, // temporal = 0 (v1 compat; bits 52-63 must be 0 for v2 clean read) + 0, // temporal = 0 (v1 compat; bits 52-63 must be 0 for v2 clean read) ); // Apply v2 routing and mantissa @@ -120,7 +122,11 @@ mod v2_layout_tests { fn test_zero_edge_v2_defaults() { let e = CausalEdge64::ZERO; assert_eq!(e.w_slot(), 0, "ZERO: w_slot must be 0"); - assert_eq!(e.truth(), TrustTexture::Crystalline, "ZERO: truth must be Crystalline"); + assert_eq!( + e.truth(), + TrustTexture::Crystalline, + "ZERO: truth must be Crystalline" + ); assert_eq!(e.inference_mantissa(), 0, "ZERO: mantissa must be 0"); assert_eq!(e.spare(), 0, "ZERO: spare must be 0"); } @@ -134,7 +140,8 @@ mod v2_layout_tests { let e = CausalEdge64::ZERO.with_w_slot(63); assert_eq!(e.w_slot(), 63, "w_slot max round-trip failed"); assert_eq!( - e.truth(), TrustTexture::Crystalline, + e.truth(), + TrustTexture::Crystalline, "w_slot=63 must not contaminate truth-band (bits 59-60)" ); } @@ -148,7 +155,8 @@ mod v2_layout_tests { let e = CausalEdge64::ZERO.with_truth(TrustTexture::Murky); assert_eq!(e.truth_raw(), 3, "truth_raw Murky must be 3"); assert_eq!( - e.w_slot(), 0, + e.w_slot(), + 0, "truth=Murky must not contaminate W-slot (bits 53-58)" ); } @@ -163,7 +171,8 @@ mod v2_layout_tests { assert_eq!(e.spare(), 0b111, "spare round-trip failed"); assert_eq!(e.w_slot(), 0, "spare must not disturb W-slot"); assert_eq!( - e.truth(), TrustTexture::Crystalline, + e.truth(), + TrustTexture::Crystalline, "spare must not disturb truth-band" ); } @@ -176,12 +185,10 @@ mod v2_layout_tests { // Plasticity is bits 50-52 (shifted by +1 from v1 per L-4). // Bits 50-52 must be untouched (i.e., plasticity = ALL_FROZEN = 0). let e = CausalEdge64::ZERO.with_inference_mantissa(-1); + assert_eq!(e.inference_mantissa(), -1, "mantissa -1 round-trip failed"); assert_eq!( - e.inference_mantissa(), -1, - "mantissa -1 round-trip failed" - ); - assert_eq!( - e.plasticity(), PlasticityState::ALL_FROZEN, + e.plasticity(), + PlasticityState::ALL_FROZEN, "mantissa=-1 (bits 46-49 all set) must not contaminate plasticity (bits 50-52)" ); } @@ -191,11 +198,13 @@ mod v2_layout_tests { #[test] fn test_size_unchanged() { assert_eq!( - std::mem::size_of::(), 8, + std::mem::size_of::(), + 8, "CausalEdge64 must be exactly 8 bytes (one register)" ); assert_eq!( - 8 * std::mem::size_of::(), 64, + 8 * std::mem::size_of::(), + 64, "8 × CausalEdge64 must equal one cache line (64 bytes)" ); } @@ -218,7 +227,8 @@ mod v2_layout_tests { for m in -8i8..=7 { let e = CausalEdge64::ZERO.with_inference_mantissa(m); assert_eq!( - e.inference_mantissa(), m, + e.inference_mantissa(), + m, "inference_mantissa round-trip failed for m={m}" ); } @@ -232,7 +242,11 @@ mod v2_layout_tests { .with_routing(10, TrustTexture::Fuzzy) .with_routing(20, TrustTexture::Murky); assert_eq!(e.w_slot(), 20, "second with_routing should override W"); - assert_eq!(e.truth(), TrustTexture::Murky, "second with_routing should override truth"); + assert_eq!( + e.truth(), + TrustTexture::Murky, + "second with_routing should override truth" + ); } // ── Bonus: InferenceType to_mantissa / from_mantissa round-trip ───────── @@ -241,20 +255,24 @@ mod v2_layout_tests { fn test_intervention_counterfactual_mantissa_slots() { // PR-LL-1 absorbed at slots 6 and -6 per L-9 assert_eq!( - InferenceType::Intervention.to_mantissa(), 6, + InferenceType::Intervention.to_mantissa(), + 6, "Intervention must map to mantissa +6" ); assert_eq!( - InferenceType::Counterfactual.to_mantissa(), -6, + InferenceType::Counterfactual.to_mantissa(), + -6, "Counterfactual must map to mantissa -6" ); // from_mantissa round-trip for PR-LL-1 slots assert_eq!( - InferenceType::from_mantissa(6), InferenceType::Intervention, + InferenceType::from_mantissa(6), + InferenceType::Intervention, "from_mantissa(+6) must return Intervention" ); assert_eq!( - InferenceType::from_mantissa(-6), InferenceType::Counterfactual, + InferenceType::from_mantissa(-6), + InferenceType::Counterfactual, "from_mantissa(-6) must return Counterfactual" ); } @@ -264,14 +282,21 @@ mod v2_layout_tests { #[test] fn test_pack_v2_v2_field_defaults() { let e = CausalEdge64::pack_v2( - 1, 2, 3, - 200, 200, + 1, + 2, + 3, + 200, + 200, CausalMask::None, 0, PlasticityState::ALL_FROZEN, ); assert_eq!(e.w_slot(), 0, "pack_v2: w_slot defaults to 0"); - assert_eq!(e.truth(), TrustTexture::Crystalline, "pack_v2: truth defaults to Crystalline"); + assert_eq!( + e.truth(), + TrustTexture::Crystalline, + "pack_v2: truth defaults to Crystalline" + ); assert_eq!(e.inference_mantissa(), 0, "pack_v2: mantissa defaults to 0"); assert_eq!(e.spare(), 0, "pack_v2: spare defaults to 0"); // v1 fields must be set correctly @@ -292,16 +317,24 @@ mod v2_layout_tests { // Build a weight edge with mantissa = -1 (Abduction direction). // Use pack_v2 so the v1 enum discriminant path is bypassed. let mut weight = CausalEdge64::pack_v2( - 10, 20, 30, - 200, 200, + 10, + 20, + 30, + 200, + 200, CausalMask::SPO, 0, PlasticityState::ALL_FROZEN, ); weight = weight.with_inference_mantissa(-1); - assert_eq!(weight.inference_mantissa(), -1, "weight must carry mantissa=-1"); assert_eq!( - InferenceType::from_mantissa(-1), InferenceType::Abduction, + weight.inference_mantissa(), + -1, + "weight must carry mantissa=-1" + ); + assert_eq!( + InferenceType::from_mantissa(-1), + InferenceType::Abduction, "from_mantissa(-1) is Abduction per the v2 mapping table" ); // The actual forward() execution is tested by feeding it through; @@ -310,7 +343,8 @@ mod v2_layout_tests { // bits 46-48 = 0b111 and dispatched as Reserved7/Synthesis instead. let resolved = InferenceType::from_mantissa(weight.inference_mantissa()); assert_eq!( - resolved, InferenceType::Abduction, + resolved, + InferenceType::Abduction, "v2 forward() must dispatch negative mantissa through Abduction" ); } @@ -323,23 +357,43 @@ mod v2_layout_tests { #[test] fn test_set_temporal_no_op_under_v2() { let mut edge = CausalEdge64::pack_v2( - 1, 2, 3, 200, 200, - CausalMask::SPO, 0, PlasticityState::ALL_FROZEN, + 1, + 2, + 3, + 200, + 200, + CausalMask::SPO, + 0, + PlasticityState::ALL_FROZEN, ); - edge = edge.with_w_slot(42).with_truth(TrustTexture::Fuzzy).with_spare(0b101); + edge = edge + .with_w_slot(42) + .with_truth(TrustTexture::Fuzzy) + .with_spare(0b101); let pre = edge; // Call set_temporal with a value that, under v1, would set bits 52-61. edge.set_temporal(1023); // Under v2, the routing state must survive. - assert_eq!(edge.w_slot(), 42, - "set_temporal must not clobber w_slot under v2"); - assert_eq!(edge.truth(), TrustTexture::Fuzzy, - "set_temporal must not clobber truth under v2"); - assert_eq!(edge.spare(), 0b101, - "set_temporal must not clobber spare under v2"); + assert_eq!( + edge.w_slot(), + 42, + "set_temporal must not clobber w_slot under v2" + ); + assert_eq!( + edge.truth(), + TrustTexture::Fuzzy, + "set_temporal must not clobber truth under v2" + ); + assert_eq!( + edge.spare(), + 0b101, + "set_temporal must not clobber spare under v2" + ); // Raw bits identical to pre-call. - assert_eq!(edge.0, pre.0, - "set_temporal under v2 must be a complete no-op on the raw u64"); + assert_eq!( + edge.0, pre.0, + "set_temporal under v2 must be a complete no-op on the raw u64" + ); } /// Codex P2: pack() under v2 must write the signed mantissa via @@ -353,42 +407,71 @@ mod v2_layout_tests { fn test_pack_uses_mantissa_mapping_under_v2() { // Abduction: to_mantissa() = -1, decodes from -1 back to Abduction. let abd_edge = CausalEdge64::pack( - 1, 2, 3, 200, 200, - CausalMask::SPO, 0, + 1, + 2, + 3, + 200, + 200, + CausalMask::SPO, + 0, InferenceType::Abduction, PlasticityState::ALL_FROZEN, 0, ); let m = abd_edge.inference_mantissa(); - assert_eq!(m, InferenceType::Abduction.to_mantissa(), - "pack(Abduction) under v2 must round-trip through to_mantissa()"); - assert_eq!(InferenceType::from_mantissa(m), InferenceType::Abduction, - "pack(Abduction) under v2 must decode back to Abduction, not Induction"); + assert_eq!( + m, + InferenceType::Abduction.to_mantissa(), + "pack(Abduction) under v2 must round-trip through to_mantissa()" + ); + assert_eq!( + InferenceType::from_mantissa(m), + InferenceType::Abduction, + "pack(Abduction) under v2 must decode back to Abduction, not Induction" + ); // Counterfactual: to_mantissa() = -6, decodes back to Counterfactual. let cf_edge = CausalEdge64::pack( - 1, 2, 3, 200, 200, - CausalMask::SPO, 0, + 1, + 2, + 3, + 200, + 200, + CausalMask::SPO, + 0, InferenceType::Counterfactual, PlasticityState::ALL_FROZEN, 0, ); let m = cf_edge.inference_mantissa(); - assert_eq!(m, InferenceType::Counterfactual.to_mantissa(), - "pack(Counterfactual) under v2 must round-trip through to_mantissa()"); - assert_eq!(InferenceType::from_mantissa(m), InferenceType::Counterfactual, - "pack(Counterfactual) under v2 must decode back to Counterfactual"); + assert_eq!( + m, + InferenceType::Counterfactual.to_mantissa(), + "pack(Counterfactual) under v2 must round-trip through to_mantissa()" + ); + assert_eq!( + InferenceType::from_mantissa(m), + InferenceType::Counterfactual, + "pack(Counterfactual) under v2 must decode back to Counterfactual" + ); // Intervention: to_mantissa() = +6, decodes back to Intervention. let iv_edge = CausalEdge64::pack( - 1, 2, 3, 200, 200, - CausalMask::SPO, 0, + 1, + 2, + 3, + 200, + 200, + CausalMask::SPO, + 0, InferenceType::Intervention, PlasticityState::ALL_FROZEN, 0, ); - assert_eq!(InferenceType::from_mantissa(iv_edge.inference_mantissa()), + assert_eq!( + InferenceType::from_mantissa(iv_edge.inference_mantissa()), InferenceType::Intervention, - "pack(Intervention) under v2 must decode back to Intervention"); + "pack(Intervention) under v2 must decode back to Intervention" + ); } } diff --git a/crates/cognitive-shader-driver/build.rs b/crates/cognitive-shader-driver/build.rs index 85f33662..9a8e2ab9 100644 --- a/crates/cognitive-shader-driver/build.rs +++ b/crates/cognitive-shader-driver/build.rs @@ -1,7 +1,6 @@ fn main() { #[cfg(feature = "grpc")] { - tonic_build::compile_protos("proto/shader.proto") - .expect("Failed to compile shader.proto"); + tonic_build::compile_protos("proto/shader.proto").expect("Failed to compile shader.proto"); } } diff --git a/crates/cognitive-shader-driver/examples/villager_ai.rs b/crates/cognitive-shader-driver/examples/villager_ai.rs index 62bf477a..730476cb 100644 --- a/crates/cognitive-shader-driver/examples/villager_ai.rs +++ b/crates/cognitive-shader-driver/examples/villager_ai.rs @@ -45,27 +45,27 @@ impl WorldMapRenderer for VillagerRenderer { fn axis_label(&self, idx: usize) -> &str { // Villager-flavoured axis names mapped to the canonical 11 dimensions. const LABELS: [&str; 11] = [ - "comfort", // warmth - "awareness", // clarity - "patience", // depth - "safety", // safety - "energy", // vitality - "intuition", // insight - "sociability", // contact - "unease", // tension - "curiosity", // novelty - "wonder", // wonder - "rapport", // attunement + "comfort", // warmth + "awareness", // clarity + "patience", // depth + "safety", // safety + "energy", // vitality + "intuition", // insight + "sociability", // contact + "unease", // tension + "curiosity", // novelty + "wonder", // wonder + "rapport", // attunement ]; LABELS.get(idx).copied().unwrap_or("") } fn anchor_label(&self, anchor: StateAnchor) -> &str { match anchor { - StateAnchor::Intake => "listening", - StateAnchor::Focused => "trading", - StateAnchor::Rest => "sleeping", - StateAnchor::Flow => "working", + StateAnchor::Intake => "listening", + StateAnchor::Focused => "trading", + StateAnchor::Rest => "sleeping", + StateAnchor::Flow => "working", StateAnchor::Observer => "watching", StateAnchor::Balanced => "socialising", StateAnchor::Baseline => "idle", @@ -97,7 +97,12 @@ struct Pet { impl Pet { fn new(name: &'static str, species: &'static str) -> Self { - Self { name, species, bond: 0.0, anchor: StateAnchor::Baseline } + Self { + name, + species, + bond: 0.0, + anchor: StateAnchor::Baseline, + } } /// Positive interaction: feed, pet, play. Bond climbs toward 1. @@ -148,9 +153,17 @@ impl Villager { name, profession, axes: ProprioceptionAxes { - warmth: 0.5, clarity: 0.5, depth: 0.5, safety: 0.6, - vitality: 0.5, insight: 0.5, contact: 0.4, - tension: 0.3, novelty: 0.5, wonder: 0.4, attunement: 0.5, + warmth: 0.5, + clarity: 0.5, + depth: 0.5, + safety: 0.6, + vitality: 0.5, + insight: 0.5, + contact: 0.4, + tension: 0.3, + novelty: 0.5, + wonder: 0.4, + attunement: 0.5, }, } } diff --git a/crates/cognitive-shader-driver/src/attention_mask.rs b/crates/cognitive-shader-driver/src/attention_mask.rs index 03373875..4cc796ef 100644 --- a/crates/cognitive-shader-driver/src/attention_mask.rs +++ b/crates/cognitive-shader-driver/src/attention_mask.rs @@ -202,13 +202,17 @@ mod tests { let mut soa = AttentionMaskSoA::new(2); soa.touch(1, 0); // cycle 0 - soa.tick(); // cycle 1 + soa.tick(); // cycle 1 soa.touch(2, 0); // cycle 1 - soa.tick(); // cycle 2 + soa.tick(); // cycle 2 soa.touch(3, 0); // cycle 2 — now active_count = 3 > max_active = 2 let evicted = soa.evict_lru(); - assert_eq!(evicted, Some(1), "mailbox_id 1 was touched at cycle 0 (oldest)"); + assert_eq!( + evicted, + Some(1), + "mailbox_id 1 was touched at cycle 0 (oldest)" + ); assert_eq!(soa.active_count(), 2); assert!(!soa.is_active(1)); assert!(soa.is_active(2)); @@ -256,11 +260,11 @@ mod tests { // Touch three entries at different cycles. soa.touch(10, 0); // cycle 0 - soa.tick(); // cycle 1 + soa.tick(); // cycle 1 soa.touch(20, 0); // cycle 1 - soa.tick(); // cycle 2 + soa.tick(); // cycle 2 soa.touch(30, 0); // cycle 2 - soa.tick(); // cycle 3 + soa.tick(); // cycle 3 // Re-touch id=10 to make it fresh; id=20 is now oldest. soa.touch(10, 0); // cycle 3 @@ -269,7 +273,11 @@ mod tests { soa.touch(40, 0); // cycle 3, active_count = 4 > max_active = 3 let evicted = soa.evict_lru(); - assert_eq!(evicted, Some(20), "id=20 was last touched at cycle 1 (oldest remaining)"); + assert_eq!( + evicted, + Some(20), + "id=20 was last touched at cycle 1 (oldest remaining)" + ); assert!(!soa.is_active(20)); assert!(soa.is_active(10)); assert!(soa.is_active(30)); diff --git a/crates/cognitive-shader-driver/src/attention_mask_actor.rs b/crates/cognitive-shader-driver/src/attention_mask_actor.rs index 8e2a7a8e..00f2a42a 100644 --- a/crates/cognitive-shader-driver/src/attention_mask_actor.rs +++ b/crates/cognitive-shader-driver/src/attention_mask_actor.rs @@ -9,17 +9,32 @@ use lance_graph_contract::collapse_gate::MailboxId; /// Messages the AttentionMaskActor accepts. #[derive(Clone, Debug)] pub enum AttentionMaskMsg { - BindRequest { mailbox_id: MailboxId, w_slot: u8, reply_to: u32 }, - BindReply { mailbox_id: MailboxId, was_new: bool, reply_to: u32 }, - EvictionMsg { evicted: MailboxId }, + BindRequest { + mailbox_id: MailboxId, + w_slot: u8, + reply_to: u32, + }, + BindReply { + mailbox_id: MailboxId, + was_new: bool, + reply_to: u32, + }, + EvictionMsg { + evicted: MailboxId, + }, Tick, } /// Outcome of one actor message handle. #[derive(Clone, Debug, PartialEq, Eq)] pub enum AttentionMaskOutcome { - Bound { mailbox_id: MailboxId, was_new: bool }, - Evicted { mailbox_id: MailboxId }, + Bound { + mailbox_id: MailboxId, + was_new: bool, + }, + Evicted { + mailbox_id: MailboxId, + }, Ticked, NoOp, } @@ -59,22 +74,32 @@ pub struct AttentionMaskActor { impl AttentionMaskActor { pub fn new(inner: B) -> Self { - Self { inner, pending_evictions: Vec::new() } + Self { + inner, + pending_evictions: Vec::new(), + } } pub fn handle(&mut self, msg: AttentionMaskMsg) -> AttentionMaskOutcome { match msg { - AttentionMaskMsg::BindRequest { mailbox_id, w_slot, .. } => { + AttentionMaskMsg::BindRequest { + mailbox_id, w_slot, .. + } => { let was_new = self.inner.touch(mailbox_id, w_slot); if let Some(evicted) = self.inner.evict_lru() { self.pending_evictions.push(evicted); } - AttentionMaskOutcome::Bound { mailbox_id, was_new } + AttentionMaskOutcome::Bound { + mailbox_id, + was_new, + } } AttentionMaskMsg::BindReply { .. } => AttentionMaskOutcome::NoOp, AttentionMaskMsg::EvictionMsg { evicted } => { self.pending_evictions.push(evicted); - AttentionMaskOutcome::Evicted { mailbox_id: evicted } + AttentionMaskOutcome::Evicted { + mailbox_id: evicted, + } } AttentionMaskMsg::Tick => { self.inner.tick(); @@ -163,7 +188,13 @@ mod tests { w_slot: 3, reply_to: 0, }); - assert_eq!(outcome, AttentionMaskOutcome::Bound { mailbox_id: 1, was_new: true }); + assert_eq!( + outcome, + AttentionMaskOutcome::Bound { + mailbox_id: 1, + was_new: true + } + ); // Second bind for the same id: already present → was_new == false let outcome2 = actor.handle(AttentionMaskMsg::BindRequest { @@ -171,7 +202,13 @@ mod tests { w_slot: 3, reply_to: 0, }); - assert_eq!(outcome2, AttentionMaskOutcome::Bound { mailbox_id: 1, was_new: false }); + assert_eq!( + outcome2, + AttentionMaskOutcome::Bound { + mailbox_id: 1, + was_new: false + } + ); } #[test] @@ -220,8 +257,16 @@ mod tests { let mut actor = AttentionMaskActor::new(backend); // Two BindRequests: each causes one LRU eviction from the queue - actor.handle(AttentionMaskMsg::BindRequest { mailbox_id: 1, w_slot: 0, reply_to: 0 }); - actor.handle(AttentionMaskMsg::BindRequest { mailbox_id: 2, w_slot: 0, reply_to: 0 }); + actor.handle(AttentionMaskMsg::BindRequest { + mailbox_id: 1, + w_slot: 0, + reply_to: 0, + }); + actor.handle(AttentionMaskMsg::BindRequest { + mailbox_id: 2, + w_slot: 0, + reply_to: 0, + }); // First drain returns both and clears let first_drain = actor.drain_pending_evictions(); diff --git a/crates/cognitive-shader-driver/src/auto_detect.rs b/crates/cognitive-shader-driver/src/auto_detect.rs index 255d2d78..d22740a6 100644 --- a/crates/cognitive-shader-driver/src/auto_detect.rs +++ b/crates/cognitive-shader-driver/src/auto_detect.rs @@ -58,9 +58,15 @@ pub enum DetectError { /// `config.json` not found next to the safetensors file. ConfigMissing { path: String }, /// IO failure reading `config.json`. - Io { path: String, source: std::io::Error }, + Io { + path: String, + source: std::io::Error, + }, /// `config.json` failed JSON parse. - Parse { path: String, source: serde_json::Error }, + Parse { + path: String, + source: serde_json::Error, + }, /// `config.json` missing a required field (listed in `field`). MissingField { path: String, field: &'static str }, } @@ -117,10 +123,14 @@ pub fn detect(model_path: &Path) -> Result { return Err(DetectError::ConfigMissing { path: path_str }); } - let raw = fs::read_to_string(&config_path) - .map_err(|e| DetectError::Io { path: path_str.clone(), source: e })?; - let cfg: HfConfig = serde_json::from_str(&raw) - .map_err(|e| DetectError::Parse { path: path_str.clone(), source: e })?; + let raw = fs::read_to_string(&config_path).map_err(|e| DetectError::Io { + path: path_str.clone(), + source: e, + })?; + let cfg: HfConfig = serde_json::from_str(&raw).map_err(|e| DetectError::Parse { + path: path_str.clone(), + source: e, + })?; let architecture = cfg .model_type @@ -132,15 +142,20 @@ pub fn detect(model_path: &Path) -> Result { let hidden_size = cfg .hidden_size .or(cfg.d_model) - .ok_or(DetectError::MissingField { path: path_str.clone(), field: "hidden_size" })?; + .ok_or(DetectError::MissingField { + path: path_str.clone(), + field: "hidden_size", + })?; - let n_layers = cfg - .num_hidden_layers - .ok_or(DetectError::MissingField { path: path_str.clone(), field: "num_hidden_layers" })?; + let n_layers = cfg.num_hidden_layers.ok_or(DetectError::MissingField { + path: path_str.clone(), + field: "num_hidden_layers", + })?; - let vocab_size = cfg - .vocab_size - .ok_or(DetectError::MissingField { path: path_str.clone(), field: "vocab_size" })?; + let vocab_size = cfg.vocab_size.ok_or(DetectError::MissingField { + path: path_str.clone(), + field: "vocab_size", + })?; let default_lane_width = suggest_lane_width(&architecture, cfg.torch_dtype.as_deref()); let default_distance = suggest_distance(&architecture); @@ -348,6 +363,12 @@ mod tests { None, ); let err = detect(&dir).unwrap_err(); - assert!(matches!(err, DetectError::MissingField { field: "hidden_size", .. })); + assert!(matches!( + err, + DetectError::MissingField { + field: "hidden_size", + .. + } + )); } } diff --git a/crates/cognitive-shader-driver/src/auto_style.rs b/crates/cognitive-shader-driver/src/auto_style.rs index e05ae4c7..aefd2769 100644 --- a/crates/cognitive-shader-driver/src/auto_style.rs +++ b/crates/cognitive-shader-driver/src/auto_style.rs @@ -18,7 +18,6 @@ use lance_graph_contract::cognitive_shader::StyleSelector; - /// Mapping from qualia shape to a style ordinal (0..11 matches /// `thinking_engine::cognitive_stack::ThinkingStyle::all()`). pub const DELIBERATE: u8 = 0; @@ -57,7 +56,10 @@ pub fn style_from_qualia(q: &[f32]) -> u8 { ("valence", score(valence)), ] .into_iter() - .fold(("deliberate", 0.0), |acc, (name, v)| if v > acc.1 { (name, v) } else { acc }); + .fold( + ("deliberate", 0.0), + |acc, (name, v)| if v > acc.1 { (name, v) } else { acc }, + ); if dom_value < 0.25 { return DELIBERATE; @@ -65,15 +67,15 @@ pub fn style_from_qualia(q: &[f32]) -> u8 { match dom_axis { "certainty" if urgency.abs() < 0.3 => ANALYTICAL, - "certainty" => CONVERGENT, - "arousal" if activation > 0.5 => CREATIVE, - "arousal" => EXPLORATORY, - "urgency" if activation > 0.5 => FOCUSED, - "urgency" => INTUITIVE, - "depth" if certainty < 0.3 => METACOGNITIVE, - "depth" => SYSTEMATIC, - "valence" if activation > 0.5 => DIVERGENT, - "valence" => DIFFUSE, + "certainty" => CONVERGENT, + "arousal" if activation > 0.5 => CREATIVE, + "arousal" => EXPLORATORY, + "urgency" if activation > 0.5 => FOCUSED, + "urgency" => INTUITIVE, + "depth" if certainty < 0.3 => METACOGNITIVE, + "depth" => SYSTEMATIC, + "valence" if activation > 0.5 => DIVERGENT, + "valence" => DIFFUSE, _ => DELIBERATE, } } @@ -108,7 +110,9 @@ mod tests { fn q(vals: &[(usize, f32)]) -> [f32; QUALIA_DIMS] { let mut out = [0.0f32; QUALIA_DIMS]; - for &(i, v) in vals { out[i] = v; } + for &(i, v) in vals { + out[i] = v; + } out } diff --git a/crates/cognitive-shader-driver/src/bin/grpc.rs b/crates/cognitive-shader-driver/src/bin/grpc.rs index 140771c4..30bfe546 100644 --- a/crates/cognitive-shader-driver/src/bin/grpc.rs +++ b/crates/cognitive-shader-driver/src/bin/grpc.rs @@ -4,23 +4,25 @@ //! cargo run -p cognitive-shader-driver --features grpc --bin shader-grpc //! ``` -use std::sync::Arc; use cognitive_shader_driver::bindspace::BindSpace; use cognitive_shader_driver::driver::CognitiveShaderBuilder; use cognitive_shader_driver::grpc::ShaderGrpcService; +use std::sync::Arc; use bgz17::base17::Base17; use bgz17::palette::Palette; use bgz17::palette_semiring::PaletteSemiring; fn demo_palette() -> PaletteSemiring { - let entries: Vec = (0..256).map(|i| { - let mut dims = [0i16; 17]; - dims[0] = (i * 100 % 3400) as i16; - dims[1] = ((i * 37) % 200) as i16; - dims[2] = ((i * 53) % 300) as i16; - Base17 { dims } - }).collect(); + let entries: Vec = (0..256) + .map(|i| { + let mut dims = [0i16; 17]; + dims[0] = (i * 100 % 3400) as i16; + dims[1] = ((i * 37) % 200) as i16; + dims[2] = ((i * 53) % 300) as i16; + Base17 { dims } + }) + .collect(); PaletteSemiring::build(&Palette { entries }) } diff --git a/crates/cognitive-shader-driver/src/bin/serve.rs b/crates/cognitive-shader-driver/src/bin/serve.rs index 30485216..b704ff2c 100644 --- a/crates/cognitive-shader-driver/src/bin/serve.rs +++ b/crates/cognitive-shader-driver/src/bin/serve.rs @@ -4,23 +4,25 @@ //! cargo run -p cognitive-shader-driver --features serve --bin shader-serve //! ``` -use std::sync::Arc; use cognitive_shader_driver::bindspace::BindSpace; use cognitive_shader_driver::driver::CognitiveShaderBuilder; use cognitive_shader_driver::serve; +use std::sync::Arc; use bgz17::base17::Base17; use bgz17::palette::Palette; use bgz17::palette_semiring::PaletteSemiring; fn demo_palette() -> PaletteSemiring { - let entries: Vec = (0..256).map(|i| { - let mut dims = [0i16; 17]; - dims[0] = (i * 100 % 3400) as i16; - dims[1] = ((i * 37) % 200) as i16; - dims[2] = ((i * 53) % 300) as i16; - Base17 { dims } - }).collect(); + let entries: Vec = (0..256) + .map(|i| { + let mut dims = [0i16; 17]; + dims[0] = (i * 100 % 3400) as i16; + dims[1] = ((i * 37) % 200) as i16; + dims[2] = ((i * 53) % 300) as i16; + Base17 { dims } + }) + .collect(); PaletteSemiring::build(&Palette { entries }) } diff --git a/crates/cognitive-shader-driver/src/bindspace.rs b/crates/cognitive-shader-driver/src/bindspace.rs index 6e0ff4f4..ff6468fb 100644 --- a/crates/cognitive-shader-driver/src/bindspace.rs +++ b/crates/cognitive-shader-driver/src/bindspace.rs @@ -99,14 +99,12 @@ impl FingerprintColumns { /// Write a row's content fingerprint. pub fn set_content(&mut self, row: usize, words: &[u64]) { assert_eq!(words.len(), WORDS_PER_FP); - self.content[row * WORDS_PER_FP..(row + 1) * WORDS_PER_FP] - .copy_from_slice(words); + self.content[row * WORDS_PER_FP..(row + 1) * WORDS_PER_FP].copy_from_slice(words); } pub fn set_cycle(&mut self, row: usize, vsa: &[f32]) { assert_eq!(vsa.len(), FLOATS_PER_VSA); - self.cycle[row * FLOATS_PER_VSA..(row + 1) * FLOATS_PER_VSA] - .copy_from_slice(vsa); + self.cycle[row * FLOATS_PER_VSA..(row + 1) * FLOATS_PER_VSA].copy_from_slice(vsa); } /// Write a cycle fingerprint from Binary16K (u64×256) by projecting to Vsa16kF32 bipolar. @@ -114,8 +112,7 @@ impl FingerprintColumns { pub fn set_cycle_from_bits(&mut self, row: usize, bits: &[u64; WORDS_PER_FP]) { use lance_graph_contract::crystal::binary16k_to_vsa16k_bipolar; let vsa = binary16k_to_vsa16k_bipolar(bits); - self.cycle[row * FLOATS_PER_VSA..(row + 1) * FLOATS_PER_VSA] - .copy_from_slice(&*vsa); + self.cycle[row * FLOATS_PER_VSA..(row + 1) * FLOATS_PER_VSA].copy_from_slice(&*vsa); } } @@ -124,9 +121,17 @@ impl FingerprintColumns { pub struct EdgeColumn(pub Box<[u64]>); impl EdgeColumn { - pub fn zeros(len: usize) -> Self { Self(vec![0u64; len].into_boxed_slice()) } - #[inline] pub fn get(&self, row: usize) -> u64 { self.0[row] } - #[inline] pub fn set(&mut self, row: usize, edge: u64) { self.0[row] = edge; } + pub fn zeros(len: usize) -> Self { + Self(vec![0u64; len].into_boxed_slice()) + } + #[inline] + pub fn get(&self, row: usize) -> u64 { + self.0[row] + } + #[inline] + pub fn set(&mut self, row: usize, edge: u64) { + self.0[row] = edge; + } } /// **DEPRECATED** since 0.2.0 — use `QualiaI4Column` directly. @@ -136,13 +141,18 @@ impl EdgeColumn { /// /// 18 × f32 per row (legacy layout). Replaced by 8 B/row packed i4×16 /// per cognitive-substrate-convergence-v1.md §7.2 and plan decision L-10. -#[deprecated(since = "0.2.0", note = "use QualiaI4Column directly; this f32 column was retired in D-CSV-5b cutover")] +#[deprecated( + since = "0.2.0", + note = "use QualiaI4Column directly; this f32 column was retired in D-CSV-5b cutover" +)] #[derive(Debug)] pub struct QualiaColumn(pub Box<[f32]>); #[allow(deprecated)] impl QualiaColumn { - pub fn zeros(len: usize) -> Self { Self(vec![0.0f32; len * QUALIA_DIMS].into_boxed_slice()) } + pub fn zeros(len: usize) -> Self { + Self(vec![0.0f32; len * QUALIA_DIMS].into_boxed_slice()) + } #[inline] pub fn row(&self, row: usize) -> &[f32] { @@ -217,9 +227,17 @@ impl QualiaI4Column { pub struct MetaColumn(pub Box<[u32]>); impl MetaColumn { - pub fn zeros(len: usize) -> Self { Self(vec![0u32; len].into_boxed_slice()) } - #[inline] pub fn get(&self, row: usize) -> MetaWord { MetaWord(self.0[row]) } - #[inline] pub fn set(&mut self, row: usize, w: MetaWord) { self.0[row] = w.0; } + pub fn zeros(len: usize) -> Self { + Self(vec![0u32; len].into_boxed_slice()) + } + #[inline] + pub fn get(&self, row: usize) -> MetaWord { + MetaWord(self.0[row]) + } + #[inline] + pub fn set(&mut self, row: usize, w: MetaWord) { + self.0[row] = w.0; + } } /// The BindSpace — read-only universal address space. @@ -272,7 +290,10 @@ impl std::fmt::Debug for BindSpace { .field("temporal", &self.temporal) .field("expert", &self.expert) .field("entity_type", &self.entity_type) - .field("ontology", &self.ontology.as_ref().map(|_| "")) + .field( + "ontology", + &self.ontology.as_ref().map(|_| ""), + ) .finish() } } @@ -320,7 +341,15 @@ impl BindSpace { let temporal_bytes = self.len * 8; let expert_bytes = self.len * 2; let entity_type_bytes = self.len * 2; - content_topic_angle + cycle_bytes + sigma_bytes + edge_bytes + qualia_bytes + meta_bytes + temporal_bytes + expert_bytes + entity_type_bytes + content_topic_angle + + cycle_bytes + + sigma_bytes + + edge_bytes + + qualia_bytes + + meta_bytes + + temporal_bytes + + expert_bytes + + entity_type_bytes } /// Apply MetaFilter across a row window. Returns a dense Vec of row @@ -360,7 +389,10 @@ pub struct BindSpaceBuilder { impl BindSpaceBuilder { pub fn new(capacity: usize) -> Self { - Self { bs: BindSpace::zeros(capacity), cursor: 0 } + Self { + bs: BindSpace::zeros(capacity), + cursor: 0, + } } /// Push a row with default entity_type (0 = untyped). @@ -412,7 +444,8 @@ impl BindSpaceBuilder { assert!( self.cursor < self.bs.len, "BindSpaceBuilder overflow: tried to push row {} into capacity {}", - self.cursor, self.bs.len, + self.cursor, + self.bs.len, ); let row = self.cursor; self.bs.fingerprints.set_content(row, content); @@ -464,7 +497,11 @@ mod tests { let fp = FingerprintColumns::zeros(8); assert_eq!(fp.sigma.len(), 8); for row in 0..8 { - assert_eq!(fp.sigma_at(row), 0, "row {row} must default to codebook index 0"); + assert_eq!( + fp.sigma_at(row), + 0, + "row {row} must default to codebook index 0" + ); } } @@ -513,7 +550,10 @@ mod tests { bs.meta.set(2, MetaWord::new(0, 0, 255, 255, 0)); bs.meta.set(3, MetaWord::new(0, 0, 10, 10, 0)); - let filter = MetaFilter { nars_c_min: 150, ..MetaFilter::ALL }; + let filter = MetaFilter { + nars_c_min: 150, + ..MetaFilter::ALL + }; let hits = bs.meta_prefilter(ColumnWindow::new(0, 4), &filter); assert_eq!(hits, vec![0, 2]); } @@ -539,9 +579,9 @@ mod tests { bs.write_cycle_fingerprint(1, &fp); // After bipolar projection: bit 0 set → dim 0 = +1.0, bit 1 unset → dim 1 = -1.0 let row = bs.fingerprints.cycle_row(1); - assert_eq!(row[0], 1.0); // bit 0 was set - assert_eq!(row[1], -1.0); // bit 1 was not set - // Row 0 should still be all zeros (not projected) + assert_eq!(row[0], 1.0); // bit 0 was set + assert_eq!(row[1], -1.0); // bit 1 was not set + // Row 0 should still be all zeros (not projected) assert!(bs.fingerprints.cycle_row(0).iter().all(|&v| v == 0.0)); } @@ -570,7 +610,15 @@ mod tests { let qualia = QualiaI4_16D::ZERO; let content = [0u64; WORDS_PER_FP]; let bs = BindSpaceBuilder::new(2) - .push_typed(&content, MetaWord::new(1, 0, 100, 100, 0), 0, qualia, 0, 0, 5) + .push_typed( + &content, + MetaWord::new(1, 0, 100, 100, 0), + 0, + qualia, + 0, + 0, + 5, + ) .push(&content, MetaWord::new(2, 0, 200, 200, 0), 0, qualia, 0, 0) .build(); assert_eq!(bs.entity_type[0], 5, "push_typed should set entity_type"); @@ -613,7 +661,12 @@ mod tests { // qualia is now QualiaI4Column; len() is row count (not flat f32 len). assert_eq!(bs.qualia.len(), N); for i in 0..N { - assert_eq!(bs.qualia.row(i), QualiaI4_16D::ZERO, "row {} should be ZERO", i); + assert_eq!( + bs.qualia.row(i), + QualiaI4_16D::ZERO, + "row {} should be ZERO", + i + ); } assert!(!bs.qualia.is_empty()); assert!(BindSpace::zeros(0).qualia.is_empty()); @@ -627,20 +680,31 @@ mod tests { let footprint = bs.byte_footprint(); // Explicit formula (D-CSV-5b): no f32 qualia column (72 B/row gone). let content_topic_angle = 3 * N * WORDS_PER_FP * 8; // 3 × 2048 × N - let cycle_bytes = N * FLOATS_PER_VSA * 4; // 65536 × N - let sigma_bytes = N; // 1 × N - let edge_bytes = N * 8; // 8 × N - let qualia_bytes = N * 8; // i4: 8 × N (NOT 72 × N) + let cycle_bytes = N * FLOATS_PER_VSA * 4; // 65536 × N + let sigma_bytes = N; // 1 × N + let edge_bytes = N * 8; // 8 × N + let qualia_bytes = N * 8; // i4: 8 × N (NOT 72 × N) let meta_bytes = N * 4; let temporal_bytes = N * 8; let expert_bytes = N * 2; let entity_type_bytes = N * 2; - let expected = content_topic_angle + cycle_bytes + sigma_bytes + edge_bytes - + qualia_bytes + meta_bytes + temporal_bytes - + expert_bytes + entity_type_bytes; - assert_eq!(footprint, expected, + let expected = content_topic_angle + + cycle_bytes + + sigma_bytes + + edge_bytes + + qualia_bytes + + meta_bytes + + temporal_bytes + + expert_bytes + + entity_type_bytes; + assert_eq!( + footprint, + expected, "D-CSV-5b: byte_footprint should be {} (i4 8 B/row × {} rows), not {} (f32 72 B/row)", - expected, N, expected + N * (QUALIA_DIMS * 4 - 8)); + expected, + N, + expected + N * (QUALIA_DIMS * 4 - 8) + ); } /// 3. push_typed writes i4; read back via bs.qualia.row(0) equals the input. @@ -650,10 +714,21 @@ mod tests { let known = QualiaI4_16D::ZERO.with(0, 3).with(7, -5).with(15, 7); let content = [0u64; WORDS_PER_FP]; let bs = BindSpaceBuilder::new(1) - .push_typed(&content, MetaWord::new(1, 0, 100, 100, 0), 0, known, 0, 0, 0) + .push_typed( + &content, + MetaWord::new(1, 0, 100, 100, 0), + 0, + known, + 0, + 0, + 0, + ) .build(); - assert_eq!(bs.qualia.row(0), known, - "push_typed must write QualiaI4_16D verbatim to bs.qualia.row(0)"); + assert_eq!( + bs.qualia.row(0), + known, + "push_typed must write QualiaI4_16D verbatim to bs.qualia.row(0)" + ); } /// 4. engine_bridge conversion: from_f32_17d at the bridge produces the @@ -664,7 +739,7 @@ mod tests { // Simulate what dispatch_busdto does post-cutover: // the engine produces f32; from_f32_17d converts at the bridge boundary. let mut q = [0.0f32; QUALIA_DIMS]; // QUALIA_DIMS=18 (bindspace local const) - q[0] = 0.8; // energy + q[0] = 0.8; // energy q[1] = 0.3; q[3] = -0.6; q[9] = 512.0; // codebook_index as f32 @@ -677,8 +752,11 @@ mod tests { // Write to BindSpace the same way the bridge will post-cutover let mut bs = BindSpace::zeros(1); bs.qualia.set(0, oracle); // bridge writes the converted i4 directly - assert_eq!(bs.qualia.row(0), oracle, - "bridge-converted i4 must match QualiaI4_16D::from_f32_17d oracle"); + assert_eq!( + bs.qualia.row(0), + oracle, + "bridge-converted i4 must match QualiaI4_16D::from_f32_17d oracle" + ); } /// 5. QualiaColumn deprecation attribute is present (meta-test). @@ -696,8 +774,11 @@ mod tests { { let col = QualiaColumn::zeros(2); // The type exists and is functional during the deprecation cycle. - assert_eq!(col.0.len(), 2 * QUALIA_DIMS, - "deprecated QualiaColumn must still allocate during deprecation cycle"); + assert_eq!( + col.0.len(), + 2 * QUALIA_DIMS, + "deprecated QualiaColumn must still allocate during deprecation cycle" + ); } // The canonical field on BindSpace is QualiaI4Column, not QualiaColumn. let bs = BindSpace::zeros(1); @@ -705,5 +786,4 @@ mod tests { use lance_graph_contract::qualia::QualiaI4_16D; let _: QualiaI4_16D = bs.qualia.row(0); } - } diff --git a/crates/cognitive-shader-driver/src/codec_bridge.rs b/crates/cognitive-shader-driver/src/codec_bridge.rs index 8dba5759..34b7d096 100644 --- a/crates/cognitive-shader-driver/src/codec_bridge.rs +++ b/crates/cognitive-shader-driver/src/codec_bridge.rs @@ -29,16 +29,21 @@ pub struct CodecResearchBridge; impl OrchestrationBridge for CodecResearchBridge { fn route(&self, step: &mut UnifiedStep) -> Result<(), OrchestrationError> { - let domain = StepDomain::from_step_type(&step.step_type) - .ok_or_else(|| OrchestrationError::RoutingFailed(format!( - "unknown step_type prefix: {}", step.step_type - )))?; + let domain = StepDomain::from_step_type(&step.step_type).ok_or_else(|| { + OrchestrationError::RoutingFailed(format!( + "unknown step_type prefix: {}", + step.step_type + )) + })?; if domain != StepDomain::Ndarray { return Err(OrchestrationError::DomainUnavailable(domain)); } step.status = StepStatus::Running; - let op = step.step_type.strip_prefix("nd.").unwrap_or(&step.step_type); + let op = step + .step_type + .strip_prefix("nd.") + .unwrap_or(&step.step_type); let args = step.reasoning.as_deref().unwrap_or("{}"); match op { @@ -75,7 +80,9 @@ impl OrchestrationBridge for CodecResearchBridge { step.status = StepStatus::Completed; step.reasoning = Some(format!( "probe tensor={} n_rows={} entries={}", - r.tensor_name, r.n_rows, r.entries.len() + r.tensor_name, + r.n_rows, + r.entries.len() )); Ok(()) } diff --git a/crates/cognitive-shader-driver/src/codec_kernel_cache.rs b/crates/cognitive-shader-driver/src/codec_kernel_cache.rs index 5e98a01f..2c550ae5 100644 --- a/crates/cognitive-shader-driver/src/codec_kernel_cache.rs +++ b/crates/cognitive-shader-driver/src/codec_kernel_cache.rs @@ -136,7 +136,11 @@ impl CodecKernelCache { let hits = self.hit_count() as f64; let compiles = self.compile_count() as f64; let total = hits + compiles; - if total < 0.5 { 0.0 } else { hits / total } + if total < 0.5 { + 0.0 + } else { + hits / total + } } /// Check whether a specific signature is cached without calling compile. @@ -184,7 +188,11 @@ impl StubKernel { Self { signature: params.kernel_signature(), is_matmul_heavy: params.is_matmul_heavy(), - backend: if params.is_matmul_heavy() { "amx" } else { "avx512" }, + backend: if params.is_matmul_heavy() { + "amx" + } else { + "avx512" + }, } } } @@ -254,7 +262,10 @@ mod tests { fn matmul_heavy_params_select_amx_backend_in_stub() { let opq = CodecParamsBuilder::new() .lane_width(LaneWidth::BF16x32) - .rotation(Rotation::Opq { matrix_blob_id: 42, dim: 4096 }) + .rotation(Rotation::Opq { + matrix_blob_id: 42, + dim: 4096, + }) .build() .unwrap(); let identity = CodecParamsBuilder::new().build().unwrap(); @@ -319,11 +330,19 @@ mod tests { CodecParamsBuilder::new().centroids(256).build().unwrap(), CodecParamsBuilder::new().centroids(512).build().unwrap(), CodecParamsBuilder::new().centroids(1024).build().unwrap(), - CodecParamsBuilder::new().centroids(256).seed(999).build().unwrap(), // same sig as first + CodecParamsBuilder::new() + .centroids(256) + .seed(999) + .build() + .unwrap(), // same sig as first CodecParamsBuilder::new() .lane_width(LaneWidth::BF16x32) - .rotation(Rotation::Opq { matrix_blob_id: 1, dim: 4096 }) - .build().unwrap(), + .rotation(Rotation::Opq { + matrix_blob_id: 1, + dim: 4096, + }) + .build() + .unwrap(), ]; for p in &candidates { diff --git a/crates/cognitive-shader-driver/src/codec_research.rs b/crates/cognitive-shader-driver/src/codec_research.rs index 5d58797a..71db1950 100644 --- a/crates/cognitive-shader-driver/src/codec_research.rs +++ b/crates/cognitive-shader-driver/src/codec_research.rs @@ -71,18 +71,23 @@ pub fn calibrate_tensor(req: &WireCalibrateRequest) -> Result> = match req.max_rows { - Some(n) if n < n_rows => rows[..n].iter().map(|r| r[..adjusted_dim].to_vec()).collect(), + Some(n) if n < n_rows => rows[..n] + .iter() + .map(|r| r[..adjusted_dim].to_vec()) + .collect(), _ => rows.iter().map(|r| r[..adjusted_dim].to_vec()).collect(), }; let cal_n = calibration_rows.len(); - let codebook = - cam_pq::train_geometric(&calibration_rows, adjusted_dim, req.kmeans_iterations); + let codebook = cam_pq::train_geometric(&calibration_rows, adjusted_dim, req.kmeans_iterations); let sliced: Vec> = rows.iter().map(|r| r[..adjusted_dim].to_vec()).collect(); let icc = measure_icc(&sliced, &codebook, req.icc_samples); diff --git a/crates/cognitive-shader-driver/src/cypher_bridge.rs b/crates/cognitive-shader-driver/src/cypher_bridge.rs index 13782e17..7cd30ae3 100644 --- a/crates/cognitive-shader-driver/src/cypher_bridge.rs +++ b/crates/cognitive-shader-driver/src/cypher_bridge.rs @@ -46,11 +46,7 @@ pub fn disambiguate_parse_candidates( position: usize, candidates: Vec, ) -> Result { - let result = chain.disambiguate_with( - position, - candidates, - DisambiguateOpts::default(), - ); + let result = chain.disambiguate_with(position, candidates, DisambiguateOpts::default()); if result.escalate_to_llm { Err(result) } else { @@ -69,8 +65,8 @@ impl OrchestrationBridge for CypherBridge { // through to the planner bridge. if !step.step_type.starts_with("lg.cypher") { // Signal domain mismatch so the route_handler falls through. - let domain = StepDomain::from_step_type(&step.step_type) - .unwrap_or(StepDomain::LanceGraph); + let domain = + StepDomain::from_step_type(&step.step_type).unwrap_or(StepDomain::LanceGraph); return Err(OrchestrationError::DomainUnavailable(domain)); } @@ -99,16 +95,14 @@ impl OrchestrationBridge for CypherBridge { let upper = query.to_uppercase(); if upper.starts_with("CREATE") { step.status = StepStatus::Completed; - step.reasoning = Some( - "cypher CREATE parsed (stub — actual SPO commit pending)".to_string(), - ); + step.reasoning = + Some("cypher CREATE parsed (stub — actual SPO commit pending)".to_string()); step.confidence = Some(0.5); Ok(()) } else if upper.starts_with("MATCH") { step.status = StepStatus::Completed; - step.reasoning = Some( - "cypher MATCH parsed (stub — actual BindSpace search pending)".to_string(), - ); + step.reasoning = + Some("cypher MATCH parsed (stub — actual BindSpace search pending)".to_string()); step.confidence = Some(0.5); Ok(()) } else { @@ -169,7 +163,11 @@ mod tests { let bridge = CypherBridge; let mut step = make_step("lg.cypher", Some("CREATE (c:Customer {id:1})")); let result = bridge.route(&mut step); - assert!(result.is_ok(), "CREATE should be accepted, got {:?}", result); + assert!( + result.is_ok(), + "CREATE should be accepted, got {:?}", + result + ); assert_eq!(step.status, StepStatus::Completed); assert_eq!(step.confidence, Some(0.5)); assert!(step diff --git a/crates/cognitive-shader-driver/src/decode_kernel.rs b/crates/cognitive-shader-driver/src/decode_kernel.rs index 46f74e89..70b465a8 100644 --- a/crates/cognitive-shader-driver/src/decode_kernel.rs +++ b/crates/cognitive-shader-driver/src/decode_kernel.rs @@ -82,14 +82,19 @@ pub struct StubDecodeKernel { } impl StubDecodeKernel { - pub const fn new(dim: u32, tag: u64) -> Self { Self { dim, tag } } + pub const fn new(dim: u32, tag: u64) -> Self { + Self { dim, tag } + } } impl DecodeKernel for StubDecodeKernel { fn decode(&self, bytes: &[u8]) -> Result, DecodeError> { let expected = self.bytes_per_row() as usize; if bytes.len() != expected { - return Err(DecodeError::SizeMismatch { expected, actual: bytes.len() }); + return Err(DecodeError::SizeMismatch { + expected, + actual: bytes.len(), + }); } let mut out = Vec::with_capacity(self.dim as usize); for chunk in bytes.chunks_exact(4) { @@ -101,7 +106,10 @@ impl DecodeKernel for StubDecodeKernel { fn encode(&self, vec: &[f32]) -> Result, DecodeError> { let expected = self.dim as usize; if vec.len() != expected { - return Err(DecodeError::SizeMismatch { expected, actual: vec.len() }); + return Err(DecodeError::SizeMismatch { + expected, + actual: vec.len(), + }); } let mut out = Vec::with_capacity(expected * 4); for &v in vec { @@ -110,8 +118,12 @@ impl DecodeKernel for StubDecodeKernel { Ok(out) } - fn bytes_per_row(&self) -> u32 { self.dim * 4 } - fn dim(&self) -> u32 { self.dim } + fn bytes_per_row(&self) -> u32 { + self.dim * 4 + } + fn dim(&self) -> u32 { + self.dim + } fn signature(&self) -> u64 { let mut h = DefaultHasher::new(); @@ -121,7 +133,9 @@ impl DecodeKernel for StubDecodeKernel { h.finish() } - fn backend(&self) -> &'static str { "stub" } + fn backend(&self) -> &'static str { + "stub" + } } // ─── Residual composer ─────────────────────────────────────────────────── @@ -165,16 +179,25 @@ impl DecodeKernel for ResidualComposer { let base_b = self.base.bytes_per_row() as usize; let expected = self.bytes_per_row() as usize; if bytes.len() != expected { - return Err(DecodeError::SizeMismatch { expected, actual: bytes.len() }); + return Err(DecodeError::SizeMismatch { + expected, + actual: bytes.len(), + }); } let base_v = self .base .decode(&bytes[..base_b]) - .map_err(|e| DecodeError::Stage { stage: "base::decode", detail: e.to_string() })?; - let residual_v = self - .residual - .decode(&bytes[base_b..]) - .map_err(|e| DecodeError::Stage { stage: "residual::decode", detail: e.to_string() })?; + .map_err(|e| DecodeError::Stage { + stage: "base::decode", + detail: e.to_string(), + })?; + let residual_v = + self.residual + .decode(&bytes[base_b..]) + .map_err(|e| DecodeError::Stage { + stage: "residual::decode", + detail: e.to_string(), + })?; let mut out = base_v; for (dst, &r) in out.iter_mut().zip(&residual_v) { *dst += r; @@ -185,24 +208,36 @@ impl DecodeKernel for ResidualComposer { fn encode(&self, vec: &[f32]) -> Result, DecodeError> { let expected = self.dim() as usize; if vec.len() != expected { - return Err(DecodeError::SizeMismatch { expected, actual: vec.len() }); + return Err(DecodeError::SizeMismatch { + expected, + actual: vec.len(), + }); } // First-pass encode + its self-reconstruction. - let base_bytes = self - .base - .encode(vec) - .map_err(|e| DecodeError::Stage { stage: "base::encode", detail: e.to_string() })?; + let base_bytes = self.base.encode(vec).map_err(|e| DecodeError::Stage { + stage: "base::encode", + detail: e.to_string(), + })?; let base_reconstructed = self .base .decode(&base_bytes) - .map_err(|e| DecodeError::Stage { stage: "base::decode", detail: e.to_string() })?; + .map_err(|e| DecodeError::Stage { + stage: "base::decode", + detail: e.to_string(), + })?; // Residual = original − base.decode(base.encode(original)). - let residual_vec: Vec = - vec.iter().zip(&base_reconstructed).map(|(a, b)| a - b).collect(); - let residual_bytes = self - .residual - .encode(&residual_vec) - .map_err(|e| DecodeError::Stage { stage: "residual::encode", detail: e.to_string() })?; + let residual_vec: Vec = vec + .iter() + .zip(&base_reconstructed) + .map(|(a, b)| a - b) + .collect(); + let residual_bytes = + self.residual + .encode(&residual_vec) + .map_err(|e| DecodeError::Stage { + stage: "residual::encode", + detail: e.to_string(), + })?; // Concat. let mut out = Vec::with_capacity(base_bytes.len() + residual_bytes.len()); out.extend_from_slice(&base_bytes); @@ -214,7 +249,9 @@ impl DecodeKernel for ResidualComposer { self.base.bytes_per_row() + self.residual.bytes_per_row() } - fn dim(&self) -> u32 { self.base.dim() } + fn dim(&self) -> u32 { + self.base.dim() + } fn signature(&self) -> u64 { let mut h = DefaultHasher::new(); @@ -254,9 +291,21 @@ mod tests { fn stub_rejects_wrong_input_size() { let k = StubDecodeKernel::new(4, 0); let err = k.encode(&[1.0, 2.0, 3.0]).unwrap_err(); - assert!(matches!(err, DecodeError::SizeMismatch { expected: 4, actual: 3 })); + assert!(matches!( + err, + DecodeError::SizeMismatch { + expected: 4, + actual: 3 + } + )); let err = k.decode(&[0u8; 10]).unwrap_err(); - assert!(matches!(err, DecodeError::SizeMismatch { expected: 16, actual: 10 })); + assert!(matches!( + err, + DecodeError::SizeMismatch { + expected: 16, + actual: 10 + } + )); } #[test] @@ -278,13 +327,19 @@ mod tests { let base = Box::new(StubDecodeKernel::new(4, 0)); let residual = Box::new(StubDecodeKernel::new(8, 0)); let err = ResidualComposer::new(base, residual).unwrap_err(); - assert!(matches!(err, DecodeError::SizeMismatch { expected: 4, actual: 8 })); + assert!(matches!( + err, + DecodeError::SizeMismatch { + expected: 4, + actual: 8 + } + )); } #[test] fn residual_compose_bytes_per_row_sums_stages() { - let base = Box::new(StubDecodeKernel::new(6, 1)); // 24 bytes - let residual = Box::new(StubDecodeKernel::new(6, 2)); // 24 bytes + let base = Box::new(StubDecodeKernel::new(6, 1)); // 24 bytes + let residual = Box::new(StubDecodeKernel::new(6, 2)); // 24 bytes let comp = ResidualComposer::new(base, residual).unwrap(); assert_eq!(comp.bytes_per_row(), 48); assert_eq!(comp.dim(), 6); @@ -326,7 +381,11 @@ mod tests { let a2 = Box::new(StubDecodeKernel::new(4, 1)); let b2 = Box::new(StubDecodeKernel::new(4, 2)); let ba = ResidualComposer::new(b2, a2).unwrap(); - assert_ne!(ab.signature(), ba.signature(), "base/residual order is part of identity"); + assert_ne!( + ab.signature(), + ba.signature(), + "base/residual order is part of identity" + ); } #[test] diff --git a/crates/cognitive-shader-driver/src/driver.rs b/crates/cognitive-shader-driver/src/driver.rs index 8a53b4a7..d9a89ae5 100644 --- a/crates/cognitive-shader-driver/src/driver.rs +++ b/crates/cognitive-shader-driver/src/driver.rs @@ -29,16 +29,17 @@ use bgz17::palette_semiring::PaletteSemiring; use causal_edge::edge::{CausalEdge64, InferenceType}; use causal_edge::pearl::CausalMask; use causal_edge::plasticity::PlasticityState; -use causal_edge::tables::{NarsTables, unpack_c, unpack_f}; +use causal_edge::tables::{unpack_c, unpack_f, NarsTables}; use lance_graph_contract::cognitive_shader::{ AlphaComposite, CognitiveShaderDriver, EmitMode, MetaSummary, NullSink, ShaderBus, - ShaderCrystal, ShaderDispatch, ShaderHit, ShaderResonance, ShaderSink, - ALPHA_COMPOSITE_DIMS, + ShaderCrystal, ShaderDispatch, ShaderHit, ShaderResonance, ShaderSink, ALPHA_COMPOSITE_DIMS, }; use lance_graph_contract::collapse_gate::{GateDecision, MergeMode, ALPHA_SATURATION_THRESHOLD}; use lance_graph_contract::grammar::free_energy::{FreeEnergy, EPIPHANY_MARGIN}; use lance_graph_contract::grammar::inference::NarsInference; -use lance_graph_contract::grammar::thinking_styles::{GrammarStyleAwareness, ParamKey, ParseOutcome}; +use lance_graph_contract::grammar::thinking_styles::{ + GrammarStyleAwareness, ParamKey, ParseOutcome, +}; use lance_graph_contract::mul::{MulAssessment, MulThresholdProfile, SituationInput}; use lance_graph_contract::thinking::ThinkingStyle; use p64_bridge::cognitive_shader::CognitiveShader; @@ -135,7 +136,9 @@ impl ShaderDriver { /// Borrow the underlying BindSpace (read-only). #[inline] - pub fn bindspace(&self) -> &BindSpace { &self.bindspace } + pub fn bindspace(&self) -> &BindSpace { + &self.bindspace + } /// Snapshot the topology planes (8 × 64 u64). /// @@ -178,8 +181,7 @@ impl ShaderDriver { // [3] Shader cascade — bgz17 O(1) per probed block. // Snapshot the planes under the read lock so the cascade sees a // consistent topology even if `update_planes` fires mid-dispatch. - let planes_snapshot: [[u64; 64]; 8] = - **self.planes.read().expect("planes RwLock poisoned"); + let planes_snapshot: [[u64; 64]; 8] = **self.planes.read().expect("planes RwLock poisoned"); let shader = CognitiveShader::new(planes_snapshot, &self.semiring); let max_dist = (self.semiring.k as f32) * (self.semiring.k as f32); let mut hits = Vec::::with_capacity(passed_rows.len().min(64)); @@ -199,9 +201,14 @@ impl ShaderDriver { let fp_i = self.bindspace.fingerprints.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_i_bytes = unsafe { std::slice::from_raw_parts(fp_i.as_ptr() as *const u8, WORDS_PER_FP * 8) }; - let fp_j_bytes = unsafe { std::slice::from_raw_parts(fp_j.as_ptr() as *const u8, WORDS_PER_FP * 8) }; - let hamming = ndarray::hpc::bitwise::hamming_distance_raw(fp_i_bytes, fp_j_bytes) as u32; + let fp_i_bytes = unsafe { + std::slice::from_raw_parts(fp_i.as_ptr() as *const u8, WORDS_PER_FP * 8) + }; + let fp_j_bytes = unsafe { + std::slice::from_raw_parts(fp_j.as_ptr() as *const u8, WORDS_PER_FP * 8) + }; + let hamming = + ndarray::hpc::bitwise::hamming_distance_raw(fp_i_bytes, fp_j_bytes) as u32; let resonance = 1.0 - (hamming as f32 / FP_BITS); if resonance >= min_resonance { hits.push(ShaderHit { @@ -226,7 +233,9 @@ impl ShaderDriver { } for (cycle_idx, &row) in passed_rows.iter().enumerate() { - if cycle_idx as u16 >= req.max_cycles.saturating_mul(4) { break; } + if cycle_idx as u16 >= req.max_cycles.saturating_mul(4) { + break; + } // 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)); @@ -261,7 +270,11 @@ impl ShaderDriver { } // Sort by resonance descending, keep top-8. - hits.sort_by(|a, b| b.resonance.partial_cmp(&a.resonance).unwrap_or(std::cmp::Ordering::Equal)); + hits.sort_by(|a, b| { + b.resonance + .partial_cmp(&a.resonance) + .unwrap_or(std::cmp::Ordering::Equal) + }); hits.truncate(8); // [4] Build the cycle_fingerprint with positional Markov braiding. @@ -310,9 +323,14 @@ impl ShaderDriver { // NARS-revised awareness. Maps directly to MUL's skill_level // axis — competence as the system has demonstrated it, not as // it feels right now. - let awareness_skill = self.awareness.read() + let awareness_skill = self + .awareness + .read() .ok() - .and_then(|aw| aw.get(style_ord as usize).map(|s| s.recent_success.frequency as f64)) + .and_then(|aw| { + aw.get(style_ord as usize) + .map(|s| s.recent_success.frequency as f64) + }) .unwrap_or(0.5); let std_dev_clamped = std_dev.clamp(0.0, 1.0) as f64; let situation = SituationInput { @@ -328,14 +346,20 @@ impl ShaderDriver { // D-CASCADE-V1-7: ctx_id resolves via BindSpace.entity_type + // optional OntologyRegistry handle; per-row context column is // Wave-3.5 follow-up (gate is one-per-dispatch today). - let ctx_id: u32 = passed_rows.first().copied().and_then(|r| { - let etid = self.bindspace.entity_type[r as usize]; - if etid == 0 { return None; } - self.bindspace.ontology().and_then(|reg| { - reg.enumerate_first_with_entity_type_id(etid) - .map(|row| row.ontology_context_id()) + let ctx_id: u32 = passed_rows + .first() + .copied() + .and_then(|r| { + let etid = self.bindspace.entity_type[r as usize]; + if etid == 0 { + return None; + } + self.bindspace.ontology().and_then(|reg| { + reg.enumerate_first_with_entity_type_id(etid) + .map(|row| row.ontology_context_id()) + }) }) - }).unwrap_or(0); + .unwrap_or(0); let profile = MulThresholdProfile::for_context(ctx_id); let trust_below_floor = (mul.trust.value as f32) < profile.trust_min; @@ -353,7 +377,10 @@ impl ShaderDriver { } else if is_epiphany { GateDecision::HOLD } else if free_energy.is_homeostatic() { - GateDecision { gate: 0, merge: MergeMode::Bundle } + GateDecision { + gate: 0, + merge: MergeMode::Bundle, + } } else { GateDecision::HOLD }; @@ -362,7 +389,9 @@ impl ShaderDriver { let mut emitted = [0u64; 8]; let mut emitted_n = 0u8; for h in hits.iter().take(8) { - if h.resonance < 0.2 { continue; } + if h.resonance < 0.2 { + continue; + } let f = (h.resonance.clamp(0.0, 1.0) * 255.0) as u8; let c = (h.resonance.clamp(0.0, 1.0) * 255.0) as u8; let s_palette = (h.row % 256) as u8; @@ -415,8 +444,14 @@ impl ShaderDriver { // D-CSV-5b: bs.qualia is now QualiaI4Column (returns QualiaI4_16D by value). // alpha_front_to_back_composite expects F: Fn(u32) -> &'a [f32]. // 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())) + 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(), + ) + }) .collect(); let alpha_composite = if effective_merge == MergeMode::AlphaFrontToBack { let threshold = req @@ -425,7 +460,8 @@ impl ShaderDriver { Some(alpha_front_to_back_composite( &hits, |row| { - hit_qualia_f32.iter() + hit_qualia_f32 + .iter() .find(|(r, _)| *r == row) .map(|(_, q)| &q[..]) .unwrap_or(&[][..]) @@ -505,7 +541,12 @@ impl ShaderDriver { } } - let crystal = ShaderCrystal { bus, persisted_row, meta, alpha_composite }; + let crystal = ShaderCrystal { + bus, + persisted_row, + meta, + alpha_composite, + }; sink.on_crystal(&crystal); crystal } @@ -575,7 +616,12 @@ where } } - AlphaComposite { color_acc, alpha_acc, hits_consumed, saturated } + AlphaComposite { + color_acc, + alpha_acc, + hits_consumed, + saturated, + } } impl CognitiveShaderDriver for ShaderDriver { @@ -584,11 +630,17 @@ impl CognitiveShaderDriver for ShaderDriver { self.run(req, &mut null) } - fn dispatch_with_sink(&self, req: &ShaderDispatch, sink: &mut S) -> ShaderCrystal { + fn dispatch_with_sink( + &self, + req: &ShaderDispatch, + sink: &mut S, + ) -> ShaderCrystal { self.run(req, sink) } - fn row_count(&self) -> u32 { self.bindspace.len as u32 } + fn row_count(&self) -> u32 { + self.bindspace.len as u32 + } fn byte_footprint(&self) -> usize { self.bindspace.byte_footprint() @@ -687,7 +739,9 @@ impl CognitiveShaderBuilder { } impl Default for CognitiveShaderBuilder { - fn default() -> Self { Self::new() } + fn default() -> Self { + Self::new() + } } // ═══════════════════════════════════════════════════════════════════════════ @@ -695,18 +749,26 @@ impl Default for CognitiveShaderBuilder { // ═══════════════════════════════════════════════════════════════════════════ fn entropy_std(hits: &[ShaderHit]) -> (f32, f32) { - if hits.is_empty() { return (0.0, 0.0); } + if hits.is_empty() { + return (0.0, 0.0); + } let sum: f32 = hits.iter().map(|h| h.resonance).sum(); - if sum <= 0.0 { return (0.0, 0.0); } + if sum <= 0.0 { + return (0.0, 0.0); + } let mut ent = 0.0f32; for h in hits { let p = h.resonance / sum; - if p > 1e-9 { ent -= p * p.ln(); } + if p > 1e-9 { + ent -= p * p.ln(); + } } let mean = sum / hits.len() as f32; - let var: f32 = hits.iter() + let var: f32 = hits + .iter() .map(|h| (h.resonance - mean).powi(2)) - .sum::() / hits.len() as f32; + .sum::() + / hits.len() as f32; (ent, var.sqrt()) } @@ -715,9 +777,16 @@ fn collapse_gate(sd: f32) -> GateDecision { // Matches thinking_engine::cognitive_stack::{SD_FLOW_THRESHOLD, SD_BLOCK_THRESHOLD}. const FLOW: f32 = 0.15; const BLOCK: f32 = 0.35; - if sd < FLOW { GateDecision { gate: 0, merge: MergeMode::Xor } } - else if sd > BLOCK { GateDecision::BLOCK } - else { GateDecision::HOLD } + if sd < FLOW { + GateDecision { + gate: 0, + merge: MergeMode::Xor, + } + } else if sd > BLOCK { + GateDecision::BLOCK + } else { + GateDecision::HOLD + } } fn style_ord_to_inference(ord: u8) -> InferenceType { @@ -730,8 +799,8 @@ fn style_ord_to_inference(ord: u8) -> InferenceType { 1..=3 => InferenceType::Deduction, 4..=6 => InferenceType::Induction, 7..=9 => InferenceType::Abduction, - 0 | 10 => InferenceType::Revision, - _ => InferenceType::Synthesis, + 0 | 10 => InferenceType::Revision, + _ => InferenceType::Synthesis, } } @@ -740,18 +809,18 @@ fn style_ord_to_inference(ord: u8) -> InferenceType { /// the closest semantic match per cluster. fn ord_to_thinking_style(ord: u8) -> ThinkingStyle { match ord { - 0 => ThinkingStyle::Methodical, // deliberate - 1 => ThinkingStyle::Analytical, // analytical - 2 => ThinkingStyle::Logical, // convergent - 3 => ThinkingStyle::Systematic, // systematic - 4 => ThinkingStyle::Creative, // creative - 5 => ThinkingStyle::Imaginative, // divergent - 6 => ThinkingStyle::Exploratory, // exploratory - 7 => ThinkingStyle::Precise, // focused - 8 => ThinkingStyle::Speculative, // diffuse - 9 => ThinkingStyle::Curious, // peripheral - 10 => ThinkingStyle::Reflective, // intuitive - _ => ThinkingStyle::Metacognitive, // metacognitive + 0 => ThinkingStyle::Methodical, // deliberate + 1 => ThinkingStyle::Analytical, // analytical + 2 => ThinkingStyle::Logical, // convergent + 3 => ThinkingStyle::Systematic, // systematic + 4 => ThinkingStyle::Creative, // creative + 5 => ThinkingStyle::Imaginative, // divergent + 6 => ThinkingStyle::Exploratory, // exploratory + 7 => ThinkingStyle::Precise, // focused + 8 => ThinkingStyle::Speculative, // diffuse + 9 => ThinkingStyle::Curious, // peripheral + 10 => ThinkingStyle::Reflective, // intuitive + _ => ThinkingStyle::Metacognitive, // metacognitive } } @@ -778,10 +847,10 @@ mod tests { use crate::bindspace::{BindSpaceBuilder, QUALIA_DIMS, WORDS_PER_FP}; use bgz17::base17::Base17; use bgz17::palette::Palette; + use lance_graph_contract::cognitive_shader::MetaWord; use lance_graph_contract::cognitive_shader::{ ColumnWindow, MetaFilter, ShaderDispatch, StyleSelector, }; - use lance_graph_contract::cognitive_shader::MetaWord; fn demo_bindspace() -> BindSpace { use lance_graph_contract::qualia::QualiaI4_16D; @@ -790,18 +859,20 @@ mod tests { BindSpaceBuilder::new(4) .push(&content, MetaWord::new(1, 1, 200, 200, 5), 0, q, 0, 0) .push(&content, MetaWord::new(2, 2, 100, 100, 5), 0, q, 0, 0) - .push(&content, MetaWord::new(3, 3, 50, 50, 5), 0, q, 0, 0) - .push(&content, MetaWord::new(4, 4, 0, 0, 5), 0, q, 0, 0) + .push(&content, MetaWord::new(3, 3, 50, 50, 5), 0, q, 0, 0) + .push(&content, MetaWord::new(4, 4, 0, 0, 5), 0, q, 0, 0) .build() } 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 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) } @@ -809,8 +880,10 @@ mod tests { fn demo_planes() -> [[u64; 64]; 8] { let mut planes = [[0u64; 64]; 8]; for i in 0..4 { - if i + 1 < 4 { planes[0][i] |= 1u64 << (i + 1); } // CAUSES - planes[2][i] |= 1u64 << i; // SUPPORTS self + if i + 1 < 4 { + planes[0][i] |= 1u64 << (i + 1); + } // CAUSES + planes[2][i] |= 1u64 << i; // SUPPORTS self } planes } @@ -861,7 +934,10 @@ mod tests { .planes(demo_planes()) .build(); - let tight = MetaFilter { nars_c_min: 150, ..MetaFilter::ALL }; + let tight = MetaFilter { + nars_c_min: 150, + ..MetaFilter::ALL + }; let req = ShaderDispatch { rows: ColumnWindow::new(0, 4), meta_prefilter: tight, @@ -879,7 +955,13 @@ mod tests { let q = QualiaI4_16D::ZERO; let mut builder = BindSpaceBuilder::new(rows.len()); for (idx, content) in rows.iter().enumerate() { - let meta = MetaWord::new((idx as u8).wrapping_add(1), (idx as u8).wrapping_add(1), 200, 200, 5); + let meta = MetaWord::new( + (idx as u8).wrapping_add(1), + (idx as u8).wrapping_add(1), + 200, + 200, + 5, + ); builder = builder.push(content, meta, 0, q, 0, 0); } builder.build() @@ -890,17 +972,24 @@ mod tests { // Two rows with near-identical content (differ in only 4 bits) // → resonance ≈ 0.9998, well above any style threshold. let mut a = [0u64; WORDS_PER_FP]; - for i in 0..250 { a[i / 64] |= 1u64 << (i % 64); } + for i in 0..250 { + a[i / 64] |= 1u64 << (i % 64); + } let mut b = a; b[0] ^= 0xF; // 4-bit difference → Hamming = 4 - // A third row with substantially different content. + // A third row with substantially different content. let mut c = [0u64; WORDS_PER_FP]; - for i in 8000..8250 { c[i / 64] |= 1u64 << (i % 64); } + for i in 8000..8250 { + c[i / 64] |= 1u64 << (i % 64); + } let bs = Arc::new(bindspace_with_content(&[a, b, c])); let sr = Arc::new(demo_semiring()); let driver = CognitiveShaderBuilder::new() - .bindspace(bs).semiring(sr).planes(demo_planes()).build(); + .bindspace(bs) + .semiring(sr) + .planes(demo_planes()) + .build(); let req = ShaderDispatch { rows: ColumnWindow::new(0, 3), @@ -912,15 +1001,23 @@ mod tests { }; let crystal = driver.dispatch(&req); // Top-k must contain at least one content-match hit (predicates=0x01). - let content_hits: Vec<_> = crystal.bus.resonance.top_k.iter() + let content_hits: Vec<_> = crystal + .bus + .resonance + .top_k + .iter() .filter(|h| h.predicates & 0x01 != 0 && h.resonance > 0.0) .collect(); - assert!(!content_hits.is_empty(), + assert!( + !content_hits.is_empty(), "expected at least one content-match hit, got top_k={:?}", - crystal.bus.resonance.top_k); + crystal.bus.resonance.top_k + ); // Similarity should be very high (differ in only 4/16384 bits). - assert!(content_hits.iter().any(|h| h.resonance > 0.5), - "content-match resonance should be > 0.5 for near-identical rows"); + assert!( + content_hits.iter().any(|h| h.resonance > 0.5), + "content-match resonance should be > 0.5 for near-identical rows" + ); } #[test] @@ -929,15 +1026,22 @@ mod tests { // is BELOW analytical threshold (0.85). Analytical must not emit // a content-match hit. let mut a = [0u64; WORDS_PER_FP]; - for i in 0..5000 { a[i / 64] |= 1u64 << (i % 64); } + for i in 0..5000 { + a[i / 64] |= 1u64 << (i % 64); + } let mut b = [0u64; WORDS_PER_FP]; - for i in 8000..13000 { b[i / 64] |= 1u64 << (i % 64); } + for i in 8000..13000 { + b[i / 64] |= 1u64 << (i % 64); + } // Disjoint ranges → Hamming ≈ 10000. let bs = Arc::new(bindspace_with_content(&[a, b])); let sr = Arc::new(demo_semiring()); let driver = CognitiveShaderBuilder::new() - .bindspace(bs).semiring(sr).planes(demo_planes()).build(); + .bindspace(bs) + .semiring(sr) + .planes(demo_planes()) + .build(); let req = ShaderDispatch { rows: ColumnWindow::new(0, 2), @@ -948,12 +1052,18 @@ mod tests { ..Default::default() }; let crystal = driver.dispatch(&req); - let content_hits: Vec<_> = crystal.bus.resonance.top_k.iter() + let content_hits: Vec<_> = crystal + .bus + .resonance + .top_k + .iter() .filter(|h| h.predicates & 0x01 != 0 && h.resonance > 0.0) .collect(); - assert!(content_hits.is_empty(), + assert!( + content_hits.is_empty(), "analytical style should not emit content hits when resonance < 0.85; got {:?}", - content_hits); + content_hits + ); } #[test] @@ -964,9 +1074,13 @@ mod tests { // a = bits [0..5000), b = bits [2500..7500) → overlap 2500 bits, // disjoint 2500+2500 = 5000, Hamming ≈ 5000. let mut a = [0u64; WORDS_PER_FP]; - for i in 0..5000 { a[i / 64] |= 1u64 << (i % 64); } + for i in 0..5000 { + a[i / 64] |= 1u64 << (i % 64); + } let mut b = [0u64; WORDS_PER_FP]; - for i in 2500..7500 { b[i / 64] |= 1u64 << (i % 64); } + for i in 2500..7500 { + b[i / 64] |= 1u64 << (i % 64); + } // Use empty planes so the palette cascade produces no hits — // isolates the content pre-pass so it cannot be drowned out by @@ -976,7 +1090,10 @@ mod tests { let bs = Arc::new(bindspace_with_content(&[a, b])); let sr = Arc::new(demo_semiring()); CognitiveShaderBuilder::new() - .bindspace(bs).semiring(sr).planes(empty_planes).build() + .bindspace(bs) + .semiring(sr) + .planes(empty_planes) + .build() }; let mk_req = |style_ord: u8| ShaderDispatch { rows: ColumnWindow::new(0, 2), @@ -988,16 +1105,29 @@ mod tests { }; let strict = mk_driver().dispatch(&mk_req(auto_style::ANALYTICAL)); - let loose = mk_driver().dispatch(&mk_req(auto_style::CREATIVE)); - let strict_hits = strict.bus.resonance.top_k.iter() - .filter(|h| h.predicates & 0x01 != 0 && h.resonance > 0.0).count(); - let loose_hits = loose.bus.resonance.top_k.iter() - .filter(|h| h.predicates & 0x01 != 0 && h.resonance > 0.0).count(); + let loose = mk_driver().dispatch(&mk_req(auto_style::CREATIVE)); + let strict_hits = strict + .bus + .resonance + .top_k + .iter() + .filter(|h| h.predicates & 0x01 != 0 && h.resonance > 0.0) + .count(); + let loose_hits = loose + .bus + .resonance + .top_k + .iter() + .filter(|h| h.predicates & 0x01 != 0 && h.resonance > 0.0) + .count(); // Monotonicity: loosening the style cannot reduce the set of // content-match hits. This is the load-bearing invariant. - assert!(strict_hits <= loose_hits, + assert!( + strict_hits <= loose_hits, "creative (loose) should emit >= analytical (strict) content hits: strict={} loose={}", - strict_hits, loose_hits); + strict_hits, + loose_hits + ); assert!(loose_hits > 0, "creative (threshold 0.35) should emit content hits for resonance ≈ 0.695\nloose top_k: {:?}", loose.bus.resonance.top_k); @@ -1007,7 +1137,9 @@ mod tests { fn sink_short_circuits_on_false() { struct Stop; impl ShaderSink for Stop { - fn on_resonance(&mut self, _r: &ShaderResonance) -> bool { false } + fn on_resonance(&mut self, _r: &ShaderResonance) -> bool { + false + } } let bs = Arc::new(demo_bindspace()); @@ -1019,7 +1151,10 @@ mod tests { .build(); let mut stop = Stop; - let req = ShaderDispatch { rows: ColumnWindow::new(0, 4), ..Default::default() }; + let req = ShaderDispatch { + rows: ColumnWindow::new(0, 4), + ..Default::default() + }; let crystal = driver.dispatch_with_sink(&req, &mut stop); // Short-circuited → persisted_row is None, meta is default. assert!(crystal.persisted_row.is_none()); @@ -1036,24 +1171,32 @@ mod tests { /// Build hits inline with the given resonances. Each hit gets a unique /// `row` so the qualia closure can map row → distinct color. fn mk_hits(resonances: &[f32]) -> Vec { - resonances.iter().enumerate().map(|(i, &r)| ShaderHit { - row: i as u32, - distance: 0, - predicates: 0, - _pad: 0, - resonance: r, - cycle_index: i as u32, - }).collect() + resonances + .iter() + .enumerate() + .map(|(i, &r)| ShaderHit { + row: i as u32, + distance: 0, + predicates: 0, + _pad: 0, + resonance: r, + cycle_index: i as u32, + }) + .collect() } /// Per-row qualia: row k → all-ones vector × (k+1) so we can tell which /// hits actually contributed to the composited color. fn qualia_with(rows: usize) -> Vec<[f32; QUALIA_DIMS]> { - (0..rows).map(|k| { - let mut q = [0.0f32; QUALIA_DIMS]; - for slot in q.iter_mut() { *slot = (k + 1) as f32; } - q - }).collect() + (0..rows) + .map(|k| { + let mut q = [0.0f32; QUALIA_DIMS]; + for slot in q.iter_mut() { + *slot = (k + 1) as f32; + } + q + }) + .collect() } #[test] @@ -1069,13 +1212,20 @@ mod tests { |row| &qualia[row as usize][..], ALPHA_SATURATION_THRESHOLD, ); - assert_eq!(composite.hits_consumed, 2, + assert_eq!( + composite.hits_consumed, 2, "early-ray-termination should fire after 2 hits at α=0.99 each, got {}", - composite.hits_consumed); - assert!(composite.saturated, - "saturated flag should be set when α exceeds threshold"); - assert!(composite.alpha_acc > ALPHA_SATURATION_THRESHOLD, - "α_acc must exceed threshold at termination, got {}", composite.alpha_acc); + composite.hits_consumed + ); + assert!( + composite.saturated, + "saturated flag should be set when α exceeds threshold" + ); + assert!( + composite.alpha_acc > ALPHA_SATURATION_THRESHOLD, + "α_acc must exceed threshold at termination, got {}", + composite.alpha_acc + ); } #[test] @@ -1095,9 +1245,11 @@ mod tests { // Second hit weight = 0.5 · 0.5 = 0.25 → contributes 0.25 · 2.0 = 0.5 // color_acc[0] = 0.5 + 0.5 = 1.0 // α_acc = 0.75 - assert!((asc_composite.color_acc[0] - 1.0).abs() < 1e-5, + assert!( + (asc_composite.color_acc[0] - 1.0).abs() < 1e-5, "expected color_acc[0] = 1.0 (front=row0 dominates first), got {}", - asc_composite.color_acc[0]); + asc_composite.color_acc[0] + ); // Reverse order: row 1 first (qualia × 2), row 0 second (qualia × 1) let mut hits_rev = hits_desc.clone(); @@ -1111,11 +1263,15 @@ mod tests { // First hit weight 0.5 · 1.0 = 0.5 → 0.5 · 2.0 = 1.0 // Second hit weight 0.5 · 0.5 = 0.25 → 0.25 · 1.0 = 0.25 // color_acc[0] = 1.25, distinct from the 1.0 we got front-first. - assert!((rev_composite.color_acc[0] - 1.25).abs() < 1e-5, + assert!( + (rev_composite.color_acc[0] - 1.25).abs() < 1e-5, "reversed order should give color_acc[0] = 1.25, got {}", - rev_composite.color_acc[0]); - assert!((asc_composite.color_acc[0] - rev_composite.color_acc[0]).abs() > 0.1, - "front-to-back composite must be order-dependent; got identical results"); + rev_composite.color_acc[0] + ); + assert!( + (asc_composite.color_acc[0] - rev_composite.color_acc[0]).abs() > 0.1, + "front-to-back composite must be order-dependent; got identical results" + ); } #[test] @@ -1130,8 +1286,11 @@ mod tests { assert_eq!(composite.hits_consumed, 0); assert!(!composite.saturated); for &slot in composite.color_acc.iter() { - assert_eq!(slot, 0.0, - "zero-hits composite must be all-zero color, found {}", slot); + assert_eq!( + slot, 0.0, + "zero-hits composite must be all-zero color, found {}", + slot + ); } // Sanity: AlphaComposite::default() matches the zero-hits result. let default = AlphaComposite::default(); @@ -1156,21 +1315,33 @@ mod tests { ALPHA_SATURATION_THRESHOLD, ); assert_eq!(composite.hits_consumed, 1); - assert!((composite.alpha_acc - 1.0).abs() < 1e-6, - "α_acc must be 1.0 after one opaque hit, got {}", composite.alpha_acc); - assert!(composite.saturated, - "α=1.0 > 0.99 threshold → saturated must be true"); + assert!( + (composite.alpha_acc - 1.0).abs() < 1e-6, + "α_acc must be 1.0 after one opaque hit, got {}", + composite.alpha_acc + ); + assert!( + composite.saturated, + "α=1.0 > 0.99 threshold → saturated must be true" + ); // qualia[0] = [1.0; QUALIA_DIMS]. weight = 1.0 · 1.0 = 1.0. // color_acc[0..QUALIA_DIMS] = qualia[0]. for i in 0..QUALIA_DIMS { - assert!((composite.color_acc[i] - 1.0).abs() < 1e-6, + assert!( + (composite.color_acc[i] - 1.0).abs() < 1e-6, "color_acc[{}] must equal qualia[0][{}] = 1.0, got {}", - i, i, composite.color_acc[i]); + i, + i, + composite.color_acc[i] + ); } // Trailing slots [QUALIA_DIMS..ALPHA_COMPOSITE_DIMS) stay zero. for i in QUALIA_DIMS..ALPHA_COMPOSITE_DIMS { - assert_eq!(composite.color_acc[i], 0.0, - "color_acc[{}] beyond QUALIA_DIMS must be zero", i); + assert_eq!( + composite.color_acc[i], 0.0, + "color_acc[{}] beyond QUALIA_DIMS must be zero", + i + ); } } @@ -1185,7 +1356,10 @@ mod tests { let bs = Arc::new(demo_bindspace()); let sr = Arc::new(demo_semiring()); let driver = CognitiveShaderBuilder::new() - .bindspace(bs).semiring(sr).planes(demo_planes()).build(); + .bindspace(bs) + .semiring(sr) + .planes(demo_planes()) + .build(); let baseline = ShaderDispatch { rows: ColumnWindow::new(0, 4), @@ -1198,8 +1372,10 @@ mod tests { let baseline_crystal = driver.dispatch(&baseline); // No override → alpha_composite must be None (gate decides Bundle // by default for homeostatic paths, never AlphaFrontToBack). - assert!(baseline_crystal.alpha_composite.is_none(), - "default dispatch must not populate alpha_composite"); + assert!( + baseline_crystal.alpha_composite.is_none(), + "default dispatch must not populate alpha_composite" + ); // Bundle override — the gate's Bundle path stays. let bundle_req = ShaderDispatch { @@ -1207,13 +1383,19 @@ mod tests { ..baseline }; let bundle_crystal = driver.dispatch(&bundle_req); - assert!(bundle_crystal.alpha_composite.is_none(), - "MergeMode::Bundle override must not populate alpha_composite"); + assert!( + bundle_crystal.alpha_composite.is_none(), + "MergeMode::Bundle override must not populate alpha_composite" + ); // Top-K outputs remain identical to baseline. - assert_eq!(bundle_crystal.bus.resonance.hit_count, - baseline_crystal.bus.resonance.hit_count); - assert_eq!(bundle_crystal.bus.resonance.entropy.to_bits(), - baseline_crystal.bus.resonance.entropy.to_bits()); + assert_eq!( + bundle_crystal.bus.resonance.hit_count, + baseline_crystal.bus.resonance.hit_count + ); + assert_eq!( + bundle_crystal.bus.resonance.entropy.to_bits(), + baseline_crystal.bus.resonance.entropy.to_bits() + ); // Xor override — same negative-space property. let xor_req = ShaderDispatch { @@ -1221,10 +1403,14 @@ mod tests { ..baseline }; let xor_crystal = driver.dispatch(&xor_req); - assert!(xor_crystal.alpha_composite.is_none(), - "MergeMode::Xor override must not populate alpha_composite"); - assert_eq!(xor_crystal.bus.resonance.hit_count, - baseline_crystal.bus.resonance.hit_count); + assert!( + xor_crystal.alpha_composite.is_none(), + "MergeMode::Xor override must not populate alpha_composite" + ); + assert_eq!( + xor_crystal.bus.resonance.hit_count, + baseline_crystal.bus.resonance.hit_count + ); // Superposition override — same. let super_req = ShaderDispatch { @@ -1232,8 +1418,10 @@ mod tests { ..baseline }; let super_crystal = driver.dispatch(&super_req); - assert!(super_crystal.alpha_composite.is_none(), - "MergeMode::Superposition override must not populate alpha_composite"); + assert!( + super_crystal.alpha_composite.is_none(), + "MergeMode::Superposition override must not populate alpha_composite" + ); // Positive control: AlphaFrontToBack override DOES populate it. let alpha_req = ShaderDispatch { @@ -1241,18 +1429,32 @@ mod tests { ..baseline }; let alpha_crystal = driver.dispatch(&alpha_req); - assert!(alpha_crystal.alpha_composite.is_some(), - "MergeMode::AlphaFrontToBack override MUST populate alpha_composite"); + assert!( + alpha_crystal.alpha_composite.is_some(), + "MergeMode::AlphaFrontToBack override MUST populate alpha_composite" + ); } /// Sanity probe: `confidence_to_alpha` clamps NaN / out-of-range /// resonances to zero so a poisoned hit can't break the composite. #[test] fn confidence_to_alpha_clamps_pathological() { - let hit_nan = ShaderHit { resonance: f32::NAN, ..Default::default() }; - let hit_inf = ShaderHit { resonance: f32::INFINITY, ..Default::default() }; - let hit_neg = ShaderHit { resonance: -0.5, ..Default::default() }; - let hit_big = ShaderHit { resonance: 5.0, ..Default::default() }; + let hit_nan = ShaderHit { + resonance: f32::NAN, + ..Default::default() + }; + let hit_inf = ShaderHit { + resonance: f32::INFINITY, + ..Default::default() + }; + let hit_neg = ShaderHit { + resonance: -0.5, + ..Default::default() + }; + let hit_big = ShaderHit { + resonance: 5.0, + ..Default::default() + }; assert_eq!(hit_nan.confidence_to_alpha(), 0.0); assert_eq!(hit_inf.confidence_to_alpha(), 0.0); assert_eq!(hit_neg.confidence_to_alpha(), 0.0); diff --git a/crates/cognitive-shader-driver/src/engine_bridge.rs b/crates/cognitive-shader-driver/src/engine_bridge.rs index c7563fde..252e20ff 100644 --- a/crates/cognitive-shader-driver/src/engine_bridge.rs +++ b/crates/cognitive-shader-driver/src/engine_bridge.rs @@ -27,12 +27,11 @@ //! ``` use lance_graph_contract::cognitive_shader::{ - ColumnWindow, MetaFilter, MetaWord, ShaderBus, ShaderDispatch, - StyleSelector, EmitMode, + ColumnWindow, EmitMode, MetaFilter, MetaWord, ShaderBus, ShaderDispatch, StyleSelector, }; -use lance_graph_contract::qualia::QualiaI4_16D; use crate::bindspace::{BindSpace, WORDS_PER_FP}; +use lance_graph_contract::qualia::QualiaI4_16D; #[cfg(feature = "with-engine")] use thinking_engine::dto::BusDto; @@ -62,7 +61,9 @@ pub fn ingest_codebook_indices( let mut cursor = start; for &idx in indices { - if cursor >= bs.meta.0.len() { break; } + if cursor >= bs.meta.0.len() { + break; + } // Build content fingerprint: set bit at `idx` position. let mut content = [0u64; WORDS_PER_FP]; @@ -71,7 +72,8 @@ pub fn ingest_codebook_indices( bs.fingerprints.set_content(cursor, &content); // Meta: source_ordinal as thinking style, no NARS yet. - bs.meta.set(cursor, MetaWord::new(source_ordinal, 0, 0, 0, 0)); + bs.meta + .set(cursor, MetaWord::new(source_ordinal, 0, 0, 0, 0)); bs.temporal[cursor] = timestamp; cursor += 1; @@ -95,7 +97,8 @@ pub fn dispatch_from_top_k( total_rows: u32, style: StyleSelector, ) -> ShaderDispatch { - let active: Vec = top_k.iter() + let active: Vec = top_k + .iter() .filter(|&&(_, e)| e > 0.01) .map(|&(idx, _)| idx) .collect(); @@ -228,13 +231,12 @@ fn busdto_to_binary16k(bus: &BusDto) -> [u64; WORDS_PER_FP] { /// /// Returns the row written. #[cfg(feature = "with-engine")] -pub fn dispatch_busdto( - bs: &mut BindSpace, - row: usize, - bus: &BusDto, - style_ord: u8, -) -> usize { - assert!(row < bs.len, "dispatch_busdto: row {row} out of bounds {}", bs.len); +pub fn dispatch_busdto(bs: &mut BindSpace, row: usize, bus: &BusDto, style_ord: u8) -> usize { + assert!( + row < bs.len, + "dispatch_busdto: row {row} out of bounds {}", + bs.len + ); // [1] cycle column — codebook_index + top_k indices as Binary16K → Vsa16kF32. let bits = busdto_to_binary16k(bus); @@ -280,7 +282,10 @@ pub fn dispatch_busdto( }; let nars_c = (bus.energy * 255.0).clamp(0.0, 255.0) as u8; let free_e = bus.cycle_count.min(63) as u8; - bs.meta.set(row, MetaWord::new(style_ord, awareness, nars_f, nars_c, free_e)); + bs.meta.set( + row, + MetaWord::new(style_ord, awareness, nars_f, nars_c, free_e), + ); // [4] expert column — cycle_count (full u16 fidelity, lossless). bs.expert[row] = bus.cycle_count; @@ -308,7 +313,11 @@ pub fn dispatch_busdto( /// LOSSY for top_k entries with non-positive energy at encode. #[cfg(feature = "with-engine")] pub fn unbind_busdto(bs: &BindSpace, row: usize) -> BusDto { - assert!(row < bs.len, "unbind_busdto: row {row} out of bounds {}", bs.len); + assert!( + row < bs.len, + "unbind_busdto: row {row} out of bounds {}", + bs.len + ); // [1] qualia → energy + top_k energies. // D-CSV-5b: bs.qualia is now QualiaI4Column; convert to f32 at the read site. @@ -439,12 +448,12 @@ pub fn classification_distance(experienced: &[f32; 17]) -> f32 { // Basic emotion archetypes in QPL space (arousal, valence, tension, warmth, clarity, ...) const ARCHETYPES: [[f32; 6]; 6] = [ // arousal, valence, tension, warmth, clarity, boundary - [0.9, -0.8, 0.9, 0.1, 0.3, 0.8], // fear - [0.8, -0.6, 0.7, 0.2, 0.4, 0.6], // anger - [0.2, -0.7, 0.3, 0.6, 0.2, 0.3], // sadness - [0.7, 0.8, 0.1, 0.9, 0.8, 0.2], // joy - [0.3, 0.1, 0.5, 0.3, 0.1, 0.9], // surprise - [0.1, -0.2, 0.1, 0.2, 0.6, 0.4], // disgust + [0.9, -0.8, 0.9, 0.1, 0.3, 0.8], // fear + [0.8, -0.6, 0.7, 0.2, 0.4, 0.6], // anger + [0.2, -0.7, 0.3, 0.6, 0.2, 0.3], // sadness + [0.7, 0.8, 0.1, 0.9, 0.8, 0.2], // joy + [0.3, 0.1, 0.5, 0.3, 0.1, 0.9], // surprise + [0.1, -0.2, 0.1, 0.2, 0.6, 0.4], // disgust ]; let mut min_dist = f32::MAX; @@ -499,53 +508,185 @@ pub struct UnifiedStyle { /// The 12 unified styles. Index = ordinal. pub const UNIFIED_STYLES: [UnifiedStyle; 12] = [ // 0: Deliberate - UnifiedStyle { ordinal: 0, name: "deliberate", - layer_mask: 0b0111_1111, combine: 3, contra: 0, density_target: 0.08, - resonance_threshold: 0.70, fan_out: 7, exploration: 0.20, speed: 0.1, collapse_bias: -0.05, butterfly_sensitivity: 0.50 }, + UnifiedStyle { + ordinal: 0, + name: "deliberate", + layer_mask: 0b0111_1111, + combine: 3, + contra: 0, + density_target: 0.08, + resonance_threshold: 0.70, + fan_out: 7, + exploration: 0.20, + speed: 0.1, + collapse_bias: -0.05, + butterfly_sensitivity: 0.50, + }, // 1: Analytical - UnifiedStyle { ordinal: 1, name: "analytical", - layer_mask: 0b0111_0111, combine: 1, contra: 0, density_target: 0.05, - resonance_threshold: 0.85, fan_out: 3, exploration: 0.05, speed: 0.1, collapse_bias: -0.10, butterfly_sensitivity: 0.80 }, + UnifiedStyle { + ordinal: 1, + name: "analytical", + layer_mask: 0b0111_0111, + combine: 1, + contra: 0, + density_target: 0.05, + resonance_threshold: 0.85, + fan_out: 3, + exploration: 0.05, + speed: 0.1, + collapse_bias: -0.10, + butterfly_sensitivity: 0.80, + }, // 2: Convergent - UnifiedStyle { ordinal: 2, name: "convergent", - layer_mask: 0b0011_0111, combine: 1, contra: 0, density_target: 0.04, - resonance_threshold: 0.75, fan_out: 4, exploration: 0.10, speed: 0.3, collapse_bias: -0.05, butterfly_sensitivity: 0.70 }, + UnifiedStyle { + ordinal: 2, + name: "convergent", + layer_mask: 0b0011_0111, + combine: 1, + contra: 0, + density_target: 0.04, + resonance_threshold: 0.75, + fan_out: 4, + exploration: 0.10, + speed: 0.3, + collapse_bias: -0.05, + butterfly_sensitivity: 0.70, + }, // 3: Systematic - UnifiedStyle { ordinal: 3, name: "systematic", - layer_mask: 0b0111_1111, combine: 1, contra: 0, density_target: 0.03, - resonance_threshold: 0.70, fan_out: 5, exploration: 0.10, speed: 0.2, collapse_bias: 0.00, butterfly_sensitivity: 0.60 }, + UnifiedStyle { + ordinal: 3, + name: "systematic", + layer_mask: 0b0111_1111, + combine: 1, + contra: 0, + density_target: 0.03, + resonance_threshold: 0.70, + fan_out: 5, + exploration: 0.10, + speed: 0.2, + collapse_bias: 0.00, + butterfly_sensitivity: 0.60, + }, // 4: Creative - UnifiedStyle { ordinal: 4, name: "creative", - layer_mask: 0b1111_1111, combine: 0, contra: 1, density_target: 0.40, - resonance_threshold: 0.35, fan_out: 12, exploration: 0.80, speed: 0.5, collapse_bias: 0.15, butterfly_sensitivity: 0.20 }, + UnifiedStyle { + ordinal: 4, + name: "creative", + layer_mask: 0b1111_1111, + combine: 0, + contra: 1, + density_target: 0.40, + resonance_threshold: 0.35, + fan_out: 12, + exploration: 0.80, + speed: 0.5, + collapse_bias: 0.15, + butterfly_sensitivity: 0.20, + }, // 5: Divergent - UnifiedStyle { ordinal: 5, name: "divergent", - layer_mask: 0b1000_1001, combine: 0, contra: 2, density_target: 0.30, - resonance_threshold: 0.40, fan_out: 10, exploration: 0.70, speed: 0.4, collapse_bias: 0.10, butterfly_sensitivity: 0.35 }, + UnifiedStyle { + ordinal: 5, + name: "divergent", + layer_mask: 0b1000_1001, + combine: 0, + contra: 2, + density_target: 0.30, + resonance_threshold: 0.40, + fan_out: 10, + exploration: 0.70, + speed: 0.4, + collapse_bias: 0.10, + butterfly_sensitivity: 0.35, + }, // 6: Exploratory - UnifiedStyle { ordinal: 6, name: "exploratory", - layer_mask: 0b1111_1111, combine: 0, contra: 1, density_target: 0.50, - resonance_threshold: 0.30, fan_out: 15, exploration: 0.90, speed: 0.6, collapse_bias: 0.20, butterfly_sensitivity: 0.15 }, + UnifiedStyle { + ordinal: 6, + name: "exploratory", + layer_mask: 0b1111_1111, + combine: 0, + contra: 1, + density_target: 0.50, + resonance_threshold: 0.30, + fan_out: 15, + exploration: 0.90, + speed: 0.6, + collapse_bias: 0.20, + butterfly_sensitivity: 0.15, + }, // 7: Focused - UnifiedStyle { ordinal: 7, name: "focused", - layer_mask: 0b0000_0011, combine: 1, contra: 0, density_target: 0.02, - resonance_threshold: 0.90, fan_out: 1, exploration: 0.00, speed: 0.2, collapse_bias: -0.15, butterfly_sensitivity: 0.90 }, + UnifiedStyle { + ordinal: 7, + name: "focused", + layer_mask: 0b0000_0011, + combine: 1, + contra: 0, + density_target: 0.02, + resonance_threshold: 0.90, + fan_out: 1, + exploration: 0.00, + speed: 0.2, + collapse_bias: -0.15, + butterfly_sensitivity: 0.90, + }, // 8: Diffuse - UnifiedStyle { ordinal: 8, name: "diffuse", - layer_mask: 0b0111_0111, combine: 2, contra: 3, density_target: 0.20, - resonance_threshold: 0.45, fan_out: 8, exploration: 0.40, speed: 0.5, collapse_bias: 0.05, butterfly_sensitivity: 0.25 }, + UnifiedStyle { + ordinal: 8, + name: "diffuse", + layer_mask: 0b0111_0111, + combine: 2, + contra: 3, + density_target: 0.20, + resonance_threshold: 0.45, + fan_out: 8, + exploration: 0.40, + speed: 0.5, + collapse_bias: 0.05, + butterfly_sensitivity: 0.25, + }, // 9: Peripheral - UnifiedStyle { ordinal: 9, name: "peripheral", - layer_mask: 0b1110_0000, combine: 0, contra: 1, density_target: 0.35, - resonance_threshold: 0.20, fan_out: 20, exploration: 0.60, speed: 0.7, collapse_bias: 0.25, butterfly_sensitivity: 0.10 }, + UnifiedStyle { + ordinal: 9, + name: "peripheral", + layer_mask: 0b1110_0000, + combine: 0, + contra: 1, + density_target: 0.35, + resonance_threshold: 0.20, + fan_out: 20, + exploration: 0.60, + speed: 0.7, + collapse_bias: 0.25, + butterfly_sensitivity: 0.10, + }, // 10: Intuitive - UnifiedStyle { ordinal: 10, name: "intuitive", - layer_mask: 0b0000_0001, combine: 0, contra: 1, density_target: 0.50, - resonance_threshold: 0.50, fan_out: 3, exploration: 0.30, speed: 0.9, collapse_bias: 0.00, butterfly_sensitivity: 0.30 }, + UnifiedStyle { + ordinal: 10, + name: "intuitive", + layer_mask: 0b0000_0001, + combine: 0, + contra: 1, + density_target: 0.50, + resonance_threshold: 0.50, + fan_out: 3, + exploration: 0.30, + speed: 0.9, + collapse_bias: 0.00, + butterfly_sensitivity: 0.30, + }, // 11: Metacognitive - UnifiedStyle { ordinal: 11, name: "metacognitive", - layer_mask: 0b1110_0000, combine: 2, contra: 3, density_target: 0.10, - resonance_threshold: 0.50, fan_out: 5, exploration: 0.30, speed: 0.3, collapse_bias: 0.00, butterfly_sensitivity: 0.40 }, + UnifiedStyle { + ordinal: 11, + name: "metacognitive", + layer_mask: 0b1110_0000, + combine: 2, + contra: 3, + density_target: 0.10, + resonance_threshold: 0.50, + fan_out: 5, + exploration: 0.30, + speed: 0.3, + collapse_bias: 0.00, + butterfly_sensitivity: 0.40, + }, ]; pub fn unified_style(ord: u8) -> &'static UnifiedStyle { @@ -562,12 +703,7 @@ pub fn unified_style(ord: u8) -> &'static UnifiedStyle { /// - meta update (gate state → awareness, NARS → f/c) /// /// Returns the row written. -pub fn persist_cycle( - bs: &mut BindSpace, - row: usize, - bus: &ShaderBus, - style_ord: u8, -) { +pub fn persist_cycle(bs: &mut BindSpace, row: usize, bus: &ShaderBus, style_ord: u8) { bs.write_cycle_fingerprint(row, &bus.cycle_fingerprint); if bus.emitted_edge_count > 0 { @@ -583,7 +719,10 @@ pub fn persist_cycle( let nars_c = ((1.0 - bus.resonance.std_dev) * 255.0).clamp(0.0, 255.0) as u8; let free_e = ((bus.resonance.entropy / 3.0) * 63.0).clamp(0.0, 63.0) as u8; - bs.meta.set(row, MetaWord::new(style_ord, awareness, nars_f, nars_c, free_e)); + bs.meta.set( + row, + MetaWord::new(style_ord, awareness, nars_f, nars_c, free_e), + ); } // ═══════════════════════════════════════════════════════════════════════════ @@ -628,8 +767,12 @@ mod tests { fn engine_bus_bridge_extracts_top1() { let mut bus = ShaderBus::empty(); bus.resonance.top_k[0] = ShaderHit { - row: 42, distance: 100, predicates: 0x07, _pad: 0, - resonance: 0.85, cycle_index: 0, + row: 42, + distance: 100, + predicates: 0x07, + _pad: 0, + resonance: 0.85, + cycle_index: 0, }; bus.gate = GateDecision::FLOW_XOR; let bridge = EngineBusBridge::from_shader_bus(&bus); @@ -645,46 +788,64 @@ mod tests { // Round-trip tolerance must be >= 1 i4 step (0.15 covers it). let mut bs = BindSpace::zeros(2); let mut q17 = [0.0f32; 17]; - q17[0] = 0.8; // arousal: round(0.8*7)=6 → 6/7 ≈ 0.857 (within 0.15 of 0.8) + q17[0] = 0.8; // arousal: round(0.8*7)=6 → 6/7 ≈ 0.857 (within 0.15 of 0.8) q17[4] = 0.571; // clarity: round(0.571*7)=4 → 4/7 ≈ 0.571 (near-exact) q17[14] = -0.25; // groundedness: round(-0.25*8)=-2 → -2/8 = -0.25 (exact) write_qualia_17d(&mut bs, 0, &q17); let back = read_qualia_17d(&bs, 0); // Tolerance = 0.15 (1 i4 step). Values chosen to be representable in i4. - assert!((back[0] - q17[0]).abs() < 0.15, - "dim 0: expected ~{}, got {} (i4 quantization ±0.15)", q17[0], back[0]); - assert!((back[4] - q17[4]).abs() < 0.15, - "dim 4: expected ~{}, got {} (i4 quantization ±0.15)", q17[4], back[4]); - assert!((back[14] - q17[14]).abs() < 0.15, - "dim 14: expected ~{}, got {} (i4 quantization ±0.15)", q17[14], back[14]); + assert!( + (back[0] - q17[0]).abs() < 0.15, + "dim 0: expected ~{}, got {} (i4 quantization ±0.15)", + q17[0], + back[0] + ); + assert!( + (back[4] - q17[4]).abs() < 0.15, + "dim 4: expected ~{}, got {} (i4 quantization ±0.15)", + q17[4], + back[4] + ); + assert!( + (back[14] - q17[14]).abs() < 0.15, + "dim 14: expected ~{}, got {} (i4 quantization ±0.15)", + q17[14], + back[14] + ); } #[test] fn cmyk_rgb_fear_is_near_zero_distance() { // "Fear" archetype: high arousal, negative valence, high tension let mut fear = [0.0f32; 17]; - fear[0] = 0.9; // arousal + fear[0] = 0.9; // arousal fear[1] = -0.8; // valence - fear[2] = 0.9; // tension - fear[3] = 0.1; // warmth - fear[4] = 0.3; // clarity - fear[5] = 0.8; // boundary + fear[2] = 0.9; // tension + fear[3] = 0.1; // warmth + fear[4] = 0.3; // clarity + fear[5] = 0.8; // boundary let cd = classification_distance(&fear); - assert!(cd < 0.15, "Fear should be near-zero distance (named), got {cd}"); + assert!( + cd < 0.15, + "Fear should be near-zero distance (named), got {cd}" + ); } #[test] fn cmyk_rgb_steelwind_is_far() { // "Steelwind" — a novel qualia with no archetype match let mut steelwind = [0.0f32; 17]; - steelwind[0] = 0.5; // moderate arousal - steelwind[1] = 0.5; // positive valence - steelwind[2] = 0.8; // high tension (unusual with positive valence) - steelwind[3] = 0.0; // no warmth - steelwind[4] = 0.9; // very clear - steelwind[5] = 0.0; // no boundary + steelwind[0] = 0.5; // moderate arousal + steelwind[1] = 0.5; // positive valence + steelwind[2] = 0.8; // high tension (unusual with positive valence) + steelwind[3] = 0.0; // no warmth + steelwind[4] = 0.9; // very clear + steelwind[5] = 0.0; // no boundary let cd = classification_distance(&steelwind); - assert!(cd > 0.3, "Steelwind should be far from named emotions, got {cd}"); + assert!( + cd > 0.3, + "Steelwind should be far from named emotions, got {cd}" + ); } #[test] @@ -698,11 +859,18 @@ mod tests { experienced[0] = 4.0 / 7.0; // exact i4 representation: round(4/7*7)=4 → 4/7 write_qualia_observed(&mut bs, 0, &experienced, 0.75); let (back_exp, back_cd) = read_qualia_decomposed(&bs, 0); - assert!((back_exp[0] - experienced[0]).abs() < 0.15, - "experienced dim 0: expected ~{}, got {} (i4 quantization)", experienced[0], back_exp[0]); + assert!( + (back_exp[0] - experienced[0]).abs() < 0.15, + "experienced dim 0: expected ~{}, got {} (i4 quantization)", + experienced[0], + back_exp[0] + ); // classification_distance is not stored post-cutover; returns 1.0 (default) - assert!((back_cd - 1.0).abs() < 1e-6, - "D-CSV-5b: classification_distance not stored in i4 column; expected 1.0, got {}", back_cd); + assert!( + (back_cd - 1.0).abs() < 1e-6, + "D-CSV-5b: classification_distance not stored in i4 column; expected 1.0, got {}", + back_cd + ); } #[test] @@ -732,8 +900,8 @@ mod tests { let meta = bs.meta.get(0); assert_eq!(meta.thinking(), auto_style::ANALYTICAL); assert_eq!(meta.awareness(), 3); // Flow = 3 - assert!(meta.nars_f() > 200); // 0.9 * 255 ≈ 230 - assert!(meta.nars_c() > 200); // (1-0.1) * 255 ≈ 230 + assert!(meta.nars_f() > 200); // 0.9 * 255 ≈ 230 + assert!(meta.nars_c() > 200); // (1-0.1) * 255 ≈ 230 assert_eq!(bs.edges.get(0), 0xDEAD); } } diff --git a/crates/cognitive-shader-driver/src/grpc.rs b/crates/cognitive-shader-driver/src/grpc.rs index a3912e18..1adf7cd1 100644 --- a/crates/cognitive-shader-driver/src/grpc.rs +++ b/crates/cognitive-shader-driver/src/grpc.rs @@ -23,8 +23,8 @@ use tonic::{Request, Response, Status}; use crate::driver::ShaderDriver; use crate::engine_bridge::{self, unified_style, UNIFIED_STYLES}; use lance_graph_contract::cognitive_shader::{ - ColumnWindow, CognitiveShaderDriver as DriverTrait, EmitMode, MetaFilter, - RungLevel, ShaderDispatch, StyleSelector, + CognitiveShaderDriver as DriverTrait, ColumnWindow, EmitMode, MetaFilter, RungLevel, + ShaderDispatch, StyleSelector, }; pub mod pb { @@ -59,19 +59,33 @@ impl CognitiveShaderService for ShaderGrpcService { ) -> Result, Status> { let req = request.into_inner(); let internal = proto_to_dispatch(&req); - let drv = self.driver.lock().map_err(|_| Status::internal("lock poisoned"))?; + let drv = self + .driver + .lock() + .map_err(|_| Status::internal("lock poisoned"))?; let crystal = drv.dispatch(&internal); // Convert cycle_fingerprint [u64; 256] → bytes (2048) - let fp_bytes: Vec = crystal.bus.cycle_fingerprint.iter() + let fp_bytes: Vec = crystal + .bus + .cycle_fingerprint + .iter() .flat_map(|w| w.to_le_bytes()) .collect(); - let gate = if crystal.bus.gate.is_flow() { 0 } - else if crystal.bus.gate.is_block() { 1 } - else { 2 }; + let gate = if crystal.bus.gate.is_flow() { + 0 + } else if crystal.bus.gate.is_block() { + 1 + } else { + 2 + }; - let hits: Vec = crystal.bus.resonance.top_k.iter() + let hits: Vec = crystal + .bus + .resonance + .top_k + .iter() .filter(|h| h.resonance > 0.0) .map(|h| pb::HitMessage { row: h.row, @@ -85,7 +99,8 @@ impl CognitiveShaderService for ShaderGrpcService { Ok(Response::new(pb::CrystalResponse { bus: Some(pb::BusMessage { cycle_fingerprint: fp_bytes, - emitted_edges: crystal.bus.emitted_edges[..crystal.bus.emitted_edge_count as usize].to_vec(), + emitted_edges: crystal.bus.emitted_edges[..crystal.bus.emitted_edge_count as usize] + .to_vec(), gate, resonance: Some(pb::ResonanceMessage { top_k: hits, @@ -113,14 +128,21 @@ impl CognitiveShaderService for ShaderGrpcService { let req = request.into_inner(); let indices: Vec = req.codebook_indices.iter().map(|&v| v as u16).collect(); - let mut cursor = self.write_cursor.lock().map_err(|_| Status::internal("lock"))?; + let mut cursor = self + .write_cursor + .lock() + .map_err(|_| Status::internal("lock"))?; let mut drv = self.driver.lock().map_err(|_| Status::internal("lock"))?; let bs = Arc::get_mut(&mut drv.bindspace) .ok_or_else(|| Status::failed_precondition("bindspace has multiple refs"))?; let c = *cursor; let (start, end) = engine_bridge::ingest_codebook_indices( - bs, &indices, req.source_ordinal as u8, req.timestamp, c, + bs, + &indices, + req.source_ordinal as u8, + req.timestamp, + c, ); *cursor = end as usize; @@ -182,23 +204,30 @@ impl CognitiveShaderService for ShaderGrpcService { let req = request.into_inner(); let wire_req = crate::wire::WireTensorsRequest { model_path: req.model_path, - route_filter: if req.route_filter.is_empty() { None } else { Some(req.route_filter) }, + route_filter: if req.route_filter.is_empty() { + None + } else { + Some(req.route_filter) + }, }; - let r = crate::codec_research::list_tensors(&wire_req) - .map_err(Status::invalid_argument)?; + let r = crate::codec_research::list_tensors(&wire_req).map_err(Status::invalid_argument)?; Ok(Response::new(pb::TensorsResponse { total: r.total as u32, shown: r.shown as u32, cam_pq: r.cam_pq as u32, passthrough: r.passthrough as u32, skip: r.skip as u32, - tensors: r.tensors.iter().map(|t| pb::TensorEntry { - name: t.name.clone(), - dims: t.dims.clone(), - dtype: t.dtype.clone(), - route: t.route.clone(), - n_elements: t.n_elements, - }).collect(), + tensors: r + .tensors + .iter() + .map(|t| pb::TensorEntry { + name: t.name.clone(), + dims: t.dims.clone(), + dtype: t.dtype.clone(), + route: t.route.clone(), + n_elements: t.n_elements, + }) + .collect(), })) } @@ -215,14 +244,34 @@ impl CognitiveShaderService for ShaderGrpcService { // REST-only until the proto schema catches up (D0.3b). params: None, tensor_view: None, - num_subspaces: if req.num_subspaces == 0 { 6 } else { req.num_subspaces as usize }, - num_centroids: if req.num_centroids == 0 { 256 } else { req.num_centroids as usize }, - kmeans_iterations: if req.kmeans_iterations == 0 { 20 } else { req.kmeans_iterations as usize }, - max_rows: if req.max_rows == 0 { None } else { Some(req.max_rows as usize) }, - icc_samples: if req.icc_samples == 0 { 512 } else { req.icc_samples as usize }, + num_subspaces: if req.num_subspaces == 0 { + 6 + } else { + req.num_subspaces as usize + }, + num_centroids: if req.num_centroids == 0 { + 256 + } else { + req.num_centroids as usize + }, + kmeans_iterations: if req.kmeans_iterations == 0 { + 20 + } else { + req.kmeans_iterations as usize + }, + max_rows: if req.max_rows == 0 { + None + } else { + Some(req.max_rows as usize) + }, + icc_samples: if req.icc_samples == 0 { + 512 + } else { + req.icc_samples as usize + }, }; - let r = crate::codec_research::calibrate_tensor(&wire_req) - .map_err(Status::invalid_argument)?; + let r = + crate::codec_research::calibrate_tensor(&wire_req).map_err(Status::invalid_argument)?; Ok(Response::new(pb::CalibrateResponse { tensor_name: r.tensor_name, dims: r.dims, @@ -250,10 +299,14 @@ impl CognitiveShaderService for ShaderGrpcService { model_path: req.model_path, tensor_name: req.tensor_name, row_counts: req.row_counts.iter().map(|&n| n as usize).collect(), - icc_samples: if req.icc_samples == 0 { 512 } else { req.icc_samples as usize }, + icc_samples: if req.icc_samples == 0 { + 512 + } else { + req.icc_samples as usize + }, }; - let r = crate::codec_research::row_count_probe(&wire_req) - .map_err(Status::invalid_argument)?; + let r = + crate::codec_research::row_count_probe(&wire_req).map_err(Status::invalid_argument)?; Ok(Response::new(pb::ProbeResponse { tensor_name: r.tensor_name, n_rows: r.n_rows as u32, @@ -261,20 +314,26 @@ impl CognitiveShaderService for ShaderGrpcService { adjusted_dim: r.adjusted_dim as u32, num_subspaces: r.num_subspaces as u32, num_centroids: r.num_centroids as u32, - entries: r.entries.iter().map(|e| pb::ProbeEntry { - n_train: e.n_train as u32, - icc_train: e.icc_train, - icc_all_rows: e.icc_all_rows, - relative_l2_error: e.relative_l2_error, - elapsed_ms: e.elapsed_ms, - }).collect(), + entries: r + .entries + .iter() + .map(|e| pb::ProbeEntry { + n_train: e.n_train as u32, + icc_train: e.icc_train, + icc_all_rows: e.icc_all_rows, + relative_l2_error: e.relative_l2_error, + elapsed_ms: e.elapsed_ms, + }) + .collect(), })) } } fn proto_to_dispatch(req: &pb::DispatchRequest) -> ShaderDispatch { - let style = req.style.as_ref().map(|s| { - match &s.selector { + let style = req + .style + .as_ref() + .map(|s| match &s.selector { Some(pb::style_selector::Selector::Auto(_)) => StyleSelector::Auto, Some(pb::style_selector::Selector::Ordinal(n)) => StyleSelector::Ordinal(*n as u8), Some(pb::style_selector::Selector::Named(name)) => { @@ -284,8 +343,8 @@ fn proto_to_dispatch(req: &pb::DispatchRequest) -> ShaderDispatch { )) } None => StyleSelector::Auto, - } - }).unwrap_or(StyleSelector::Auto); + }) + .unwrap_or(StyleSelector::Auto); let rung = match req.rung { 0 => RungLevel::Surface, @@ -306,13 +365,17 @@ fn proto_to_dispatch(req: &pb::DispatchRequest) -> ShaderDispatch { _ => EmitMode::Cycle, }; - let meta = req.meta_filter.as_ref().map(|f| MetaFilter { - thinking_mask: f.thinking_mask, - awareness_min: f.awareness_min as u8, - nars_f_min: f.nars_f_min as u8, - nars_c_min: f.nars_c_min as u8, - free_e_max: f.free_e_max as u8, - }).unwrap_or(MetaFilter::ALL); + let meta = req + .meta_filter + .as_ref() + .map(|f| MetaFilter { + thinking_mask: f.thinking_mask, + awareness_min: f.awareness_min as u8, + nars_f_min: f.nars_f_min as u8, + nars_c_min: f.nars_c_min as u8, + free_e_max: f.free_e_max as u8, + }) + .unwrap_or(MetaFilter::ALL); ShaderDispatch { meta_prefilter: meta, @@ -328,18 +391,21 @@ fn proto_to_dispatch(req: &pb::DispatchRequest) -> ShaderDispatch { } fn unified_styles_proto() -> Vec { - UNIFIED_STYLES.iter().map(|s| pb::StyleInfo { - ordinal: s.ordinal as u32, - name: s.name.to_string(), - layer_mask: s.layer_mask as u32, - density_target: s.density_target, - resonance_threshold: s.resonance_threshold, - fan_out: s.fan_out as u32, - combine: s.combine as u32, - contra: s.contra as u32, - exploration: s.exploration, - speed: s.speed, - collapse_bias: s.collapse_bias, - butterfly_sensitivity: s.butterfly_sensitivity, - }).collect() + UNIFIED_STYLES + .iter() + .map(|s| pb::StyleInfo { + ordinal: s.ordinal as u32, + name: s.name.to_string(), + layer_mask: s.layer_mask as u32, + density_target: s.density_target, + resonance_threshold: s.resonance_threshold, + fan_out: s.fan_out as u32, + combine: s.combine as u32, + contra: s.contra as u32, + exploration: s.exploration, + speed: s.speed, + collapse_bias: s.collapse_bias, + butterfly_sensitivity: s.butterfly_sensitivity, + }) + .collect() } diff --git a/crates/cognitive-shader-driver/src/lib.rs b/crates/cognitive-shader-driver/src/lib.rs index 2d0444cd..741cbbd6 100644 --- a/crates/cognitive-shader-driver/src/lib.rs +++ b/crates/cognitive-shader-driver/src/lib.rs @@ -92,12 +92,12 @@ // engine are just consumers that plug in via the bridge. pub mod attention_mask; pub mod attention_mask_actor; +pub mod auto_style; pub mod bindspace; pub mod driver; -pub mod auto_style; pub mod engine_bridge; -pub mod sigma_rosetta; pub mod mailbox_soa; +pub mod sigma_rosetta; // ────────────────────────────────────────────────────────────────────── // LAB-ONLY modules — compiled only into the shader-lab binary. Never @@ -183,9 +183,9 @@ pub mod cypher_bridge; pub mod planner_bridge; pub use lance_graph_contract::cognitive_shader::{ - CognitiveShaderDriver, ColumnWindow, EmitMode, MetaFilter, MetaSummary, MetaWord, - NullSink, RungLevel, ShaderBus, ShaderCrystal, ShaderDispatch, ShaderHit, - ShaderResonance, ShaderSink, StyleSelector, + CognitiveShaderDriver, ColumnWindow, EmitMode, MetaFilter, MetaSummary, MetaWord, NullSink, + RungLevel, ShaderBus, ShaderCrystal, ShaderDispatch, ShaderHit, ShaderResonance, ShaderSink, + StyleSelector, }; pub use lance_graph_contract::collapse_gate::{GateDecision, MergeMode}; @@ -194,13 +194,13 @@ pub use lance_graph_contract::collapse_gate::{GateDecision, MergeMode}; // QualiaColumn (f32, 72 B/row) is deprecated and will be removed in the next // major version. Use bs.qualia (now QualiaI4Column) for all new code. #[allow(deprecated)] -pub use bindspace::{BindSpace, BindSpaceBuilder, EdgeColumn, FingerprintColumns, - MetaColumn, QualiaI4Column, - QualiaColumn}; // deprecated — use QualiaI4Column +pub use bindspace::{ + BindSpace, BindSpaceBuilder, EdgeColumn, FingerprintColumns, MetaColumn, QualiaColumn, + QualiaI4Column, +}; // deprecated — use QualiaI4Column pub use driver::{CognitiveShaderBuilder, ShaderDriver}; pub use engine_bridge::{ - EngineBusBridge, UnifiedStyle, UNIFIED_STYLES, unified_style, - ingest_codebook_indices, dispatch_from_top_k, - write_qualia_17d, read_qualia_17d, persist_cycle, + dispatch_from_top_k, ingest_codebook_indices, persist_cycle, read_qualia_17d, unified_style, + write_qualia_17d, EngineBusBridge, UnifiedStyle, UNIFIED_STYLES, }; -pub use mailbox_soa::{MailboxSoA, DefaultMailboxSoA}; +pub use mailbox_soa::{DefaultMailboxSoA, MailboxSoA}; diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index f32e3f50..e88df3d2 100644 --- a/crates/cognitive-shader-driver/src/mailbox_soa.rs +++ b/crates/cognitive-shader-driver/src/mailbox_soa.rs @@ -29,7 +29,9 @@ use causal_edge::CausalEdge64; use lance_graph_contract::cognitive_shader::MetaWord; use lance_graph_contract::collapse_gate::MailboxId; +use lance_graph_contract::kanban::{ExecTarget, KanbanColumn, KanbanMove}; use lance_graph_contract::qualia::QualiaI4_16D; +use lance_graph_contract::soa_view::{MailboxSoaOwner, MailboxSoaView}; /// Spatial-temporal accumulator for per-row edge receipts. /// @@ -71,7 +73,6 @@ pub struct MailboxSoA { pub last_active_cycle: [u32; N], // ── NEW: migrated thoughtspace columns (per-mailbox owned, D-MBX-A1) ── - /// Per-row LE baton edge (`CausalEdge64`, 8 B/row). /// Migrated from `BindSpace.edges` (EdgeColumn). /// This IS the LE contract / baton edge for this mailbox row. @@ -105,6 +106,18 @@ pub struct MailboxSoA { /// A row is firing when `energy[row].abs() >= threshold` /// (see [`Self::pending_count`]). pub threshold: f32, + + /// The Rubicon lifecycle column this mailbox currently occupies — the + /// **cognitive** FSM state (distinct from ractor's process-lifecycle + /// `ActorStatus`; see `.claude/knowledge/orchestration-boundary-v1.md`). + /// Mutated only via [`MailboxSoaOwner::advance_phase`] / + /// [`MailboxSoaOwner::try_advance_phase`]; starts at + /// [`KanbanColumn::Planning`]. Read it through the + /// [`MailboxSoaView::phase`] getter. `pub(crate)` (not `pub`) so the + /// "mutated only via the owner trait" invariant is compiler-enforced — a + /// downstream crate cannot assign an arbitrary column directly and bypass + /// the lifecycle DAG check + `KanbanMove` emission (PR #507 review). + pub(crate) phase: KanbanColumn, } /// Default capacity: 1024 rows (4× current BindSpace row count). @@ -139,6 +152,8 @@ impl MailboxSoA { qualia: [QualiaI4_16D::ZERO; N], meta: [MetaWord(0); N], entity_type: [0u16; N], + // Pre-Rubicon: every mailbox starts in deliberation. + phase: KanbanColumn::Planning, } } @@ -168,8 +183,7 @@ impl MailboxSoA { let m = edge.inference_mantissa() as f32 / 8.0; let c = edge.confidence(); self.energy[row] += m * c; - self.plasticity_counter[row] = - self.plasticity_counter[row].saturating_add(1); + self.plasticity_counter[row] = self.plasticity_counter[row].saturating_add(1); accepted += 1; } accepted @@ -324,6 +338,100 @@ impl MailboxSoA { } } +// ── Contract trait impls: MailboxSoA IS the in-RAM Rubicon owner ────────────── +// +// `MailboxSoaView` (read) + `MailboxSoaOwner` (write) make `MailboxSoA` the +// in-RAM owner the contract names (`soa_view.rs` doc: "implements +// MailboxSoaOwner"). With these, a real `MailboxSoA` is BOTH the `view` a +// `VersionScheduler::on_version` reads AND the `owner` whose `try_advance_phase` +// applies the proposed `KanbanMove` — the in-process driving loop, no surreal / +// ractor message bus needed. The surreal-side external trigger (`surreal_container` +// view + `Notification → on_version`) is the fork-blocked follow-on (OQ-11.6). + +impl MailboxSoaView for MailboxSoA { + #[inline] + fn mailbox_id(&self) -> MailboxId { + self.mailbox_id + } + #[inline] + fn n_rows(&self) -> usize { + N + } + #[inline] + fn w_slot(&self) -> u8 { + self.w_slot + } + #[inline] + fn current_cycle(&self) -> u32 { + self.current_cycle + } + #[inline] + fn phase(&self) -> KanbanColumn { + self.phase + } + #[inline] + fn energy(&self) -> &[f32] { + &self.energy + } + #[inline] + fn edges_raw(&self) -> &[u64] { + // The `#[repr(transparent)]` on `CausalEdge64` (causal-edge `edge.rs`) is + // the load-bearing layout guarantee for this cast; the const guards below + // make a layout regression a COMPILE error (not silent UB) even if the + // repr is ever removed upstream. + const _: () = assert!(core::mem::size_of::() == core::mem::size_of::()); + const _: () = + assert!(core::mem::align_of::() == core::mem::align_of::()); + // SAFETY: `CausalEdge64` is `#[repr(transparent)]` over `u64` (causal-edge + // `edge.rs`), so `[CausalEdge64; N]` has identical size/alignment/layout to + // `[u64; N]` (the two `const _` asserts above enforce it at compile time). + // The pointer is non-null, aligned, and valid for `N` `u64` reads; the + // returned slice borrows `&self`, so it cannot outlive the backing array. + // Zero-copy — never clones the store (R1). + unsafe { core::slice::from_raw_parts(self.edges.as_ptr() as *const u64, N) } + } + #[inline] + fn meta_raw(&self) -> &[u32] { + const _: () = assert!(core::mem::size_of::() == core::mem::size_of::()); + const _: () = assert!(core::mem::align_of::() == core::mem::align_of::()); + // SAFETY: `MetaWord` is `#[repr(transparent)]` over `u32` + // (lance-graph-contract `cognitive_shader.rs`), so `[MetaWord; N]` has + // identical layout to `[u32; N]` (const-asserted above). Same + // validity/lifetime reasoning as `edges_raw`. Zero-copy. + unsafe { core::slice::from_raw_parts(self.meta.as_ptr() as *const u32, N) } + } + #[inline] + fn entity_type(&self) -> &[u16] { + &self.entity_type + } +} + +impl MailboxSoaOwner for MailboxSoA { + /// Advance the Rubicon column and emit the move. Prefer the trait's checked + /// [`MailboxSoaOwner::try_advance_phase`] (it enforces the lifecycle DAG); + /// this unchecked primitive is what it calls after the legality check passes. + fn advance_phase(&mut self, to: KanbanColumn) -> KanbanMove { + let from = self.phase; + self.phase = to; + KanbanMove { + mailbox: self.mailbox_id, + from, + to, + // Structural witness position (R4): the monotonic cycle stamp stands in + // for the chain index until the witness_arc column lands — matching + // `NextPhaseScheduler`'s convention. + witness_chain_position: self.current_cycle, + libet_offset_us: if from == KanbanColumn::Planning && to == KanbanColumn::CognitiveWork + { + -550_000 + } else { + 0 + }, + exec: ExecTarget::Native, + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -422,7 +530,11 @@ mod tests { assert_eq!(accepted, 0, "wrong-w_slot baton must be rejected"); assert_eq!(mb.energy_at(2), 0.0, "energy[2] must be unchanged"); - assert_eq!(mb.plasticity_at(2), 0, "plasticity_counter[2] must be unchanged"); + assert_eq!( + mb.plasticity_at(2), + 0, + "plasticity_counter[2] must be unchanged" + ); } // ── test 5: out-of-range target rejected ───────────────────────────────── @@ -461,8 +573,7 @@ mod tests { "energy[3] must reset to 0 after consumption" ); assert_eq!( - mb.last_active_cycle[3], - mb.current_cycle, + mb.last_active_cycle[3], mb.current_cycle, "last_active_cycle[3] must equal current_cycle" ); } @@ -475,7 +586,10 @@ mod tests { let mut mb: MailboxSoA<8> = MailboxSoA::new(5, 2, 1.0); mb.energy[3] = 0.5; // below 1.0 threshold - assert!(!mb.consume_firing(3), "below-threshold row must not consume"); + assert!( + !mb.consume_firing(3), + "below-threshold row must not consume" + ); assert_eq!(mb.energy_at(3), 0.5, "energy[3] must be unchanged"); } @@ -538,4 +652,101 @@ mod tests { assert!(!mb.consume_firing(8), "row == N must be rejected"); assert!(!mb.consume_firing(100), "row > N must be rejected"); } + + // ── test 11: the in-RAM driving loop (OUT+IN over a REAL MailboxSoA) ────── + + /// `VersionScheduler::on_version` proposes → `MailboxSoaOwner::try_advance_phase` + /// applies, on a real `MailboxSoA` (not the contract's `FakeView`/`FakeSoa`). + /// Drives the Rubicon forward arc Planning → CognitiveWork → Evaluation → + /// Commit and halts at the absorbing column. This is the unblocked in-RAM + /// loop — surreal's external version trigger is the fork-blocked follow-on. + #[test] + fn test_in_ram_driving_loop_walks_rubicon_to_commit() { + use lance_graph_contract::kanban::ExecTarget; + use lance_graph_contract::scheduler::{ + DatasetVersion, NextPhaseScheduler, VersionScheduler, + }; + + let mut mb: MailboxSoA<8> = MailboxSoA::new(42, 5, 1.0); + assert_eq!(mb.phase(), KanbanColumn::Planning, "starts pre-Rubicon"); + + let sched = NextPhaseScheduler; + let mut steps = 0u32; + let mut first_libet = 0i32; + for v in 1..=10u64 { + // IN-direction: the scheduler lowers a version tick to the next move… + let Some(mv) = sched.on_version(&mb, DatasetVersion(v), ExecTarget::Native) else { + break; // absorbing column reached — the cycle ended + }; + // …proposed from the mailbox's CURRENT phase… + assert_eq!(mv.from, mb.phase()); + // …and OUT-direction: the owner applies it through the checked airgap. + let applied = mb + .try_advance_phase(mv.to) + .expect("the scheduler proposes only legal Rubicon edges"); + assert_eq!(applied.to, mv.to); + if steps == 0 { + first_libet = applied.libet_offset_us; + } + steps += 1; + } + + assert_eq!( + mb.phase(), + KanbanColumn::Commit, + "the forward arc calcifies at Commit" + ); + assert!(mb.phase().is_absorbing()); + assert_eq!( + steps, 3, + "Planning→CognitiveWork→Evaluation→Commit = 3 advances" + ); + assert_eq!( + first_libet, -550_000, + "the Planning→CognitiveWork crossing carries the Libet −550 ms anchor" + ); + } + + // ── test 12: the unsafe zero-copy reinterpret casts round-trip ─────────── + + /// Exercises the `repr(transparent)` reinterpret casts in `edges_raw()` / + /// `meta_raw()` — the only genuinely `unsafe` code path in this impl, which + /// the driving-loop test never touches. Writes known bit patterns into the + /// `[CausalEdge64; N]` / `[MetaWord; N]` columns and asserts the `&[u64]` / + /// `&[u32]` views read the SAME bytes back. A layout regression on either + /// newtype would fail HERE (in addition to the `const _` size/align guards + /// at the cast site failing to compile). + #[test] + fn test_edges_raw_meta_raw_reinterpret_round_trips() { + let mut mb: MailboxSoA<4> = MailboxSoA::new(1, 0, 1.0); + // Distinct, non-trivial bit patterns per row. + for (i, raw) in [0xDEAD_BEEF_CAFE_0001u64, 2, 0xFFFF_FFFF_FFFF_FFFF, 0] + .into_iter() + .enumerate() + { + mb.edges[i] = CausalEdge64(raw); + mb.meta[i] = MetaWord((raw & 0xFFFF_FFFF) as u32); + } + + let edges_view: &[u64] = mb.edges_raw(); + let meta_view: &[u32] = mb.meta_raw(); + assert_eq!(edges_view.len(), 4); + assert_eq!(meta_view.len(), 4); + for i in 0..4 { + // The reinterpret reads the EXACT u64/u32 backing the newtype. + assert_eq!(edges_view[i], mb.edges[i].0, "edges_raw[{i}] bit-exact"); + assert_eq!(meta_view[i], mb.meta[i].0, "meta_raw[{i}] bit-exact"); + } + // Pointer identity: the view borrows the column, never copies it (R1). + assert_eq!( + edges_view.as_ptr() as usize, + mb.edges.as_ptr() as usize, + "edges_raw is zero-copy (same backing pointer)" + ); + assert_eq!( + meta_view.as_ptr() as usize, + mb.meta.as_ptr() as usize, + "meta_raw is zero-copy (same backing pointer)" + ); + } } diff --git a/crates/cognitive-shader-driver/src/planner_bridge.rs b/crates/cognitive-shader-driver/src/planner_bridge.rs index fdf0051a..d9eeaa0a 100644 --- a/crates/cognitive-shader-driver/src/planner_bridge.rs +++ b/crates/cognitive-shader-driver/src/planner_bridge.rs @@ -54,9 +54,14 @@ pub fn plan(planner: &PlannerAwareness, req: &WirePlanRequest) -> Result { let s = req.situation.clone().unwrap_or_default(); let situation = situation_from_wire(&s); - p.plan_full(&req.query, &situation).map_err(|e| e.to_string())? + p.plan_full(&req.query, &situation) + .map_err(|e| e.to_string())? + } + other => { + return Err(format!( + "unknown plan mode '{other}' (use 'auto' or 'full')" + )) } - other => return Err(format!("unknown plan mode '{other}' (use 'auto' or 'full')")), }; Ok(WirePlanResponse { @@ -66,7 +71,10 @@ pub fn plan(planner: &PlannerAwareness, req: &WirePlanRequest) -> Result) -> std::fmt::Result { match self { Self::DimMismatch { expected, actual } => { - write!(f, "rotation input dim mismatch: expected {expected}, got {actual}") + write!( + f, + "rotation input dim mismatch: expected {expected}, got {actual}" + ) } Self::HadamardNotPow2 { dim } => { write!(f, "Hadamard dim must be power of two, got {dim}") @@ -97,7 +100,10 @@ pub fn build(rotation: &Rotation, dim: u32) -> Result, R } Ok(Box::new(HadamardRotation { dim: *h_dim })) } - Rotation::Opq { matrix_blob_id, dim: o_dim } => { + Rotation::Opq { + matrix_blob_id, + dim: o_dim, + } => { if *o_dim != dim { return Err(RotationError::DimMismatch { expected: *o_dim as usize, @@ -134,7 +140,9 @@ impl RotationKernel for IdentityRotation { Ok(()) } - fn dim(&self) -> u32 { self.dim } + fn dim(&self) -> u32 { + self.dim + } fn signature(&self) -> u64 { let mut h = DefaultHasher::new(); @@ -143,7 +151,9 @@ impl RotationKernel for IdentityRotation { h.finish() } - fn backend(&self) -> &'static str { "avx512" } + fn backend(&self) -> &'static str { + "avx512" + } } // ─── Hadamard (Sylvester butterfly) ────────────────────────────────────── @@ -167,7 +177,10 @@ impl RotationKernel for HadamardRotation { fn apply(&self, vec: &mut [f32]) -> Result<(), RotationError> { let n = self.dim as usize; if vec.len() != n { - return Err(RotationError::DimMismatch { expected: n, actual: vec.len() }); + return Err(RotationError::DimMismatch { + expected: n, + actual: vec.len(), + }); } if n == 0 || !n.is_power_of_two() { return Err(RotationError::HadamardNotPow2 { dim: self.dim }); @@ -192,7 +205,9 @@ impl RotationKernel for HadamardRotation { Ok(()) } - fn dim(&self) -> u32 { self.dim } + fn dim(&self) -> u32 { + self.dim + } fn signature(&self) -> u64 { let mut h = DefaultHasher::new(); @@ -201,7 +216,9 @@ impl RotationKernel for HadamardRotation { h.finish() } - fn backend(&self) -> &'static str { "avx512" } + fn backend(&self) -> &'static str { + "avx512" + } } // ─── OPQ (stub — real impl plugs JIT engine in D1.1b) ──────────────────── @@ -229,10 +246,14 @@ impl RotationKernel for OpqRotationStub { }); } // Stub — no matrix loaded yet. - Err(RotationError::OpqMatrixNotLoaded { matrix_blob_id: self.matrix_blob_id }) + Err(RotationError::OpqMatrixNotLoaded { + matrix_blob_id: self.matrix_blob_id, + }) } - fn dim(&self) -> u32 { self.dim } + fn dim(&self) -> u32 { + self.dim + } fn signature(&self) -> u64 { let mut h = DefaultHasher::new(); @@ -242,7 +263,9 @@ impl RotationKernel for OpqRotationStub { h.finish() } - fn backend(&self) -> &'static str { "stub" } + fn backend(&self) -> &'static str { + "stub" + } } #[cfg(test)] @@ -264,7 +287,13 @@ mod tests { let r = IdentityRotation { dim: 8 }; let mut v = vec![0.0; 16]; let err = r.apply(&mut v).unwrap_err(); - assert!(matches!(err, RotationError::DimMismatch { expected: 8, actual: 16 })); + assert!(matches!( + err, + RotationError::DimMismatch { + expected: 8, + actual: 16 + } + )); } #[test] @@ -309,15 +338,26 @@ mod tests { let norm_sq_out: f32 = v.iter().map(|x| x * x).sum(); let expected = 16.0 * norm_sq_in; let rel_err = (norm_sq_out - expected).abs() / expected; - assert!(rel_err < 1e-5, "norm² out {norm_sq_out} vs expected {expected}"); + assert!( + rel_err < 1e-5, + "norm² out {norm_sq_out} vs expected {expected}" + ); } #[test] fn opq_stub_returns_matrix_not_loaded() { - let r = OpqRotationStub { matrix_blob_id: 0xDEAD_BEEF, dim: 4096 }; + let r = OpqRotationStub { + matrix_blob_id: 0xDEAD_BEEF, + dim: 4096, + }; let mut v = vec![0.0; 4096]; let err = r.apply(&mut v).unwrap_err(); - assert!(matches!(err, RotationError::OpqMatrixNotLoaded { matrix_blob_id: 0xDEAD_BEEF })); + assert!(matches!( + err, + RotationError::OpqMatrixNotLoaded { + matrix_blob_id: 0xDEAD_BEEF + } + )); assert_eq!(r.backend(), "stub"); } @@ -338,7 +378,13 @@ mod tests { #[test] fn build_hadamard_rejects_mismatched_dim() { let err = build(&Rotation::Hadamard { dim: 4096 }, 2048).unwrap_err(); - assert!(matches!(err, RotationError::DimMismatch { expected: 4096, actual: 2048 })); + assert!(matches!( + err, + RotationError::DimMismatch { + expected: 4096, + actual: 2048 + } + )); } #[test] @@ -349,7 +395,14 @@ mod tests { #[test] fn build_opq_returns_stub() { - let k = build(&Rotation::Opq { matrix_blob_id: 42, dim: 4096 }, 4096).unwrap(); + let k = build( + &Rotation::Opq { + matrix_blob_id: 42, + dim: 4096, + }, + 4096, + ) + .unwrap(); assert_eq!(k.dim(), 4096); assert_eq!(k.backend(), "stub"); } @@ -358,7 +411,10 @@ mod tests { fn kernel_signatures_are_distinct_across_variants() { let id = IdentityRotation { dim: 256 }; let had = HadamardRotation { dim: 256 }; - let opq = OpqRotationStub { matrix_blob_id: 1, dim: 256 }; + let opq = OpqRotationStub { + matrix_blob_id: 1, + dim: 256, + }; assert_ne!(id.signature(), had.signature()); assert_ne!(id.signature(), opq.signature()); assert_ne!(had.signature(), opq.signature()); @@ -373,8 +429,14 @@ mod tests { #[test] fn opq_signature_depends_on_matrix_blob_id() { - let a = OpqRotationStub { matrix_blob_id: 1, dim: 4096 }; - let b = OpqRotationStub { matrix_blob_id: 2, dim: 4096 }; + let a = OpqRotationStub { + matrix_blob_id: 1, + dim: 4096, + }; + let b = OpqRotationStub { + matrix_blob_id: 2, + dim: 4096, + }; assert_ne!(a.signature(), b.signature()); } } diff --git a/crates/cognitive-shader-driver/src/serve.rs b/crates/cognitive-shader-driver/src/serve.rs index 297378c0..06676ab0 100644 --- a/crates/cognitive-shader-driver/src/serve.rs +++ b/crates/cognitive-shader-driver/src/serve.rs @@ -55,8 +55,8 @@ use crate::wire::{ WireTokenAgreement, WireTokenAgreementResult, WireUnifiedStep, }; use lance_graph_contract::cam::CodecParams; -use std::path::Path as StdPath; use lance_graph_contract::cognitive_shader::CognitiveShaderDriver; +use std::path::Path as StdPath; struct ServerState { driver: ShaderDriver, @@ -127,7 +127,10 @@ async fn dispatch_handler( ) -> Result, (StatusCode, Json)> { let internal = wire.to_internal(); let st = state.lock().map_err(|_| { - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "lock poisoned"}))) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "lock poisoned"})), + ) })?; let crystal = st.driver.dispatch(&internal); Ok(Json(WireCrystal::from(&crystal))) @@ -138,11 +141,17 @@ async fn ingest_handler( Json(wire): Json, ) -> Result, (StatusCode, Json)> { let mut st = state.lock().map_err(|_| { - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "lock poisoned"}))) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "lock poisoned"})), + ) })?; let cursor = st.write_cursor; let bs = Arc::get_mut(&mut st.driver.bindspace).ok_or_else(|| { - (StatusCode::CONFLICT, Json(json!({"error": "bindspace has multiple references"}))) + ( + StatusCode::CONFLICT, + Json(json!({"error": "bindspace has multiple references"})), + ) })?; let (start, end) = engine_bridge::ingest_codebook_indices( bs, @@ -160,21 +169,22 @@ async fn ingest_handler( }))) } -async fn health_handler( - State(state): State, -) -> Json { +async fn health_handler(State(state): State) -> Json { let st = state.lock().unwrap(); Json(WireHealth { row_count: st.driver.row_count(), byte_footprint: st.driver.byte_footprint(), - styles: UNIFIED_STYLES.iter().map(|s| WireStyleInfo { - ordinal: s.ordinal, - name: s.name.to_string(), - layer_mask: s.layer_mask, - density_target: s.density_target, - resonance_threshold: s.resonance_threshold, - fan_out: s.fan_out, - }).collect(), + styles: UNIFIED_STYLES + .iter() + .map(|s| WireStyleInfo { + ordinal: s.ordinal, + name: s.name.to_string(), + layer_mask: s.layer_mask, + density_target: s.density_target, + resonance_threshold: s.resonance_threshold, + fan_out: s.fan_out, + }) + .collect(), neural_debug: None, // populated when neural-debug dep is wired }) } @@ -184,11 +194,17 @@ async fn qualia_handler( Path(row): Path, ) -> Result, (StatusCode, Json)> { let st = state.lock().map_err(|_| { - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "lock poisoned"}))) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "lock poisoned"})), + ) })?; let bs = st.driver.bindspace(); if row as usize >= bs.len { - return Err((StatusCode::NOT_FOUND, Json(json!({"error": "row out of range"})))); + return Err(( + StatusCode::NOT_FOUND, + Json(json!({"error": "row out of range"})), + )); } let (experienced, cd) = engine_bridge::read_qualia_decomposed(bs, row as usize); let style_ord = crate::auto_style::style_from_qualia(&experienced); @@ -202,14 +218,19 @@ async fn qualia_handler( } async fn styles_handler() -> Json> { - Json(UNIFIED_STYLES.iter().map(|s| WireStyleInfo { - ordinal: s.ordinal, - name: s.name.to_string(), - layer_mask: s.layer_mask, - density_target: s.density_target, - resonance_threshold: s.resonance_threshold, - fan_out: s.fan_out, - }).collect()) + Json( + UNIFIED_STYLES + .iter() + .map(|s| WireStyleInfo { + ordinal: s.ordinal, + name: s.name.to_string(), + layer_mask: s.layer_mask, + density_target: s.density_target, + resonance_threshold: s.resonance_threshold, + fan_out: s.fan_out, + }) + .collect(), + ) } // ─── Codec research handlers ──────────────────────────────────────────────── @@ -258,20 +279,24 @@ async fn token_agreement_handler( ) -> Result, (StatusCode, Json)> { // Validate CodecParams at ingress (precision-ladder / overfit guard // fire here, not inside the harness). - let _params: CodecParams = req - .candidate - .clone() - .try_into() - .map_err(|e: lance_graph_contract::cam::CodecParamsError| { - (StatusCode::BAD_REQUEST, Json(json!({"error": format!("invalid CodecParams: {e}")}))) - })?; + let _params: CodecParams = req.candidate.clone().try_into().map_err( + |e: lance_graph_contract::cam::CodecParamsError| { + ( + StatusCode::BAD_REQUEST, + Json(json!({"error": format!("invalid CodecParams: {e}")})), + ) + }, + )?; // Reference model — real path if it exists, stub otherwise. D2.2 // replaces with a strict path check once the safetensors loader lands. let model_path = StdPath::new(&req.model_path); let reference = if model_path.exists() { ReferenceModel::load(model_path).map_err(|e| { - (StatusCode::BAD_REQUEST, Json(json!({"error": format!("model load: {e}")}))) + ( + StatusCode::BAD_REQUEST, + Json(json!({"error": format!("model load: {e}")})), + ) })? } else { // Deterministic stub keyed on the path string so repeated calls @@ -282,16 +307,13 @@ async fn token_agreement_handler( ReferenceModel::stub(std::hash::Hasher::finish(&h), 0) }; - let harness = TokenAgreementHarness::new( - reference, - req.reference, - req.candidate, - req.n_tokens, - ); - harness - .measure_stub() - .map(Json) - .map_err(|e| (StatusCode::BAD_REQUEST, Json(json!({"error": format!("{e}")})))) + let harness = TokenAgreementHarness::new(reference, req.reference, req.candidate, req.n_tokens); + harness.measure_stub().map(Json).map_err(|e| { + ( + StatusCode::BAD_REQUEST, + Json(json!({"error": format!("{e}")})), + ) + }) } /// D3.1 — `POST /v1/shader/sweep` handler (batch mode). @@ -331,14 +353,16 @@ async fn sweep_handler( let mut results = Vec::with_capacity(candidates.len()); for (idx, wire_params) in candidates.into_iter().enumerate() { // Validate each grid point at ingress — surface typed errors early. - let params: CodecParams = wire_params - .clone() - .try_into() - .map_err(|e: lance_graph_contract::cam::CodecParamsError| { - (StatusCode::BAD_REQUEST, Json(json!({ - "error": format!("grid point {idx}: invalid CodecParams: {e}") - }))) - })?; + let params: CodecParams = wire_params.clone().try_into().map_err( + |e: lance_graph_contract::cam::CodecParamsError| { + ( + StatusCode::BAD_REQUEST, + Json(json!({ + "error": format!("grid point {idx}: invalid CodecParams: {e}") + })), + ) + }, + )?; results.push(WireSweepResult { grid_index: idx as u32, @@ -368,9 +392,7 @@ async fn route_handler( State(_state): State, Json(wire): Json, ) -> Result, (StatusCode, Json)> { - use lance_graph_contract::orchestration::{ - OrchestrationBridge, StepStatus, UnifiedStep, - }; + use lance_graph_contract::orchestration::{OrchestrationBridge, StepStatus, UnifiedStep}; let mut step = UnifiedStep { step_id: wire.step_id.clone(), @@ -389,7 +411,10 @@ async fn route_handler( // If codec bridge rejected with DomainUnavailable, try CypherBridge // (lg.cypher). This keeps the nd.* hot path unchanged while adding // `lg.cypher` routing ahead of the planner fallthrough. - if matches!(result, Err(lance_graph_contract::orchestration::OrchestrationError::DomainUnavailable(_))) { + if matches!( + result, + Err(lance_graph_contract::orchestration::OrchestrationError::DomainUnavailable(_)) + ) { let cypher_bridge = crate::cypher_bridge::CypherBridge; let cypher_result = cypher_bridge.route(&mut step); @@ -402,7 +427,10 @@ async fn route_handler( #[cfg(feature = "with-planner")] { let st = _state.lock().map_err(|_| { - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "lock poisoned"}))) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "lock poisoned"})), + ) })?; let _ = OrchestrationBridge::route(&st.planner, &mut step); } @@ -444,11 +472,13 @@ fn run_plan( state: &AppState, req: &WirePlanRequest, ) -> Result { - let st = state - .lock() - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "lock poisoned".to_string()))?; - crate::planner_bridge::plan(&st.planner, req) - .map_err(|e| (StatusCode::BAD_REQUEST, e)) + let st = state.lock().map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "lock poisoned".to_string(), + ) + })?; + crate::planner_bridge::plan(&st.planner, req).map_err(|e| (StatusCode::BAD_REQUEST, e)) } #[cfg(not(feature = "with-planner"))] @@ -539,10 +569,7 @@ async fn encode_handler( // ── 4b. Build fingerprint hex and popcount ──────────────────────────── let vsa_words = sentence_vec.as_words(); // &[u64; VSA_WORDS] (VSA_WORDS = 8) - let fingerprint_hex: String = vsa_words - .iter() - .map(|w| format!("{:016x}", w)) - .collect(); + let fingerprint_hex: String = vsa_words.iter().map(|w| format!("{:016x}", w)).collect(); let bits_set = sentence_vec.popcount() as usize; // ── 5. Expand 8 × u64 → 256 × u64 (16 Kbit) ───────────────────────── @@ -562,14 +589,20 @@ async fn encode_handler( // ── 6. Write to BindSpace, advance write_cursor ─────────────────────── let row_written = { let mut st = state.lock().map_err(|_| { - (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "lock poisoned"}))) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "lock poisoned"})), + ) })?; let cursor = st.write_cursor; if cursor >= st.driver.bindspace.len { None } else { let bs = Arc::get_mut(&mut st.driver.bindspace).ok_or_else(|| { - (StatusCode::CONFLICT, Json(json!({"error": "bindspace has multiple references"}))) + ( + StatusCode::CONFLICT, + Json(json!({"error": "bindspace has multiple references"})), + ) })?; bs.fingerprints.set_content(cursor, &content_fp); st.write_cursor = cursor + 1; @@ -624,12 +657,24 @@ async fn runbook_handler( WireRunbookStep::Plan(_) => "plan", }; let outcome: Result = match s.step { - WireRunbookStep::Tensors(r) => codec_research::list_tensors(&r) - .map(|response| WireRunbookStepResult::Tensors { label: label.clone(), response }), - WireRunbookStep::Calibrate(r) => codec_research::calibrate_tensor(&r) - .map(|response| WireRunbookStepResult::Calibrate { label: label.clone(), response }), - WireRunbookStep::Probe(r) => codec_research::row_count_probe(&r) - .map(|response| WireRunbookStepResult::Probe { label: label.clone(), response }), + WireRunbookStep::Tensors(r) => { + codec_research::list_tensors(&r).map(|response| WireRunbookStepResult::Tensors { + label: label.clone(), + response, + }) + } + WireRunbookStep::Calibrate(r) => codec_research::calibrate_tensor(&r).map(|response| { + WireRunbookStepResult::Calibrate { + label: label.clone(), + response, + } + }), + WireRunbookStep::Probe(r) => { + codec_research::row_count_probe(&r).map(|response| WireRunbookStepResult::Probe { + label: label.clone(), + response, + }) + } WireRunbookStep::Dispatch(wd) => match state.lock() { Err(_) => Err("lock poisoned".to_string()), Ok(st) => { @@ -648,7 +693,11 @@ async fn runbook_handler( None => Err("bindspace has multiple references".to_string()), Some(bs) => { let (start, end) = engine_bridge::ingest_codebook_indices( - bs, &wi.codebook_indices, wi.source_ordinal, wi.timestamp, cursor, + bs, + &wi.codebook_indices, + wi.source_ordinal, + wi.timestamp, + cursor, ); st.write_cursor = end as usize; Ok(WireRunbookStepResult::Ingest { diff --git a/crates/cognitive-shader-driver/src/sigma_rosetta.rs b/crates/cognitive-shader-driver/src/sigma_rosetta.rs index 9a24b383..5f41cceb 100644 --- a/crates/cognitive-shader-driver/src/sigma_rosetta.rs +++ b/crates/cognitive-shader-driver/src/sigma_rosetta.rs @@ -53,8 +53,18 @@ pub const VERB_ROOTS: [&str; 12] = [ ]; pub const VERB_TENSES: [&str; 12] = [ - "present", "past", "future", "continuous", "perfect", "pluperfect", - "future_perfect", "habitual", "potential", "imperative", "subjunctive", "gerund", + "present", + "past", + "future", + "continuous", + "perfect", + "pluperfect", + "future_perfect", + "habitual", + "potential", + "imperative", + "subjunctive", + "gerund", ]; pub const N_VERBS: usize = 144; @@ -94,73 +104,393 @@ pub enum GlyphFamily { pub const GLYPHS: [QualiaGlyph; 64] = [ // Core (0-15) - QualiaGlyph { idx: 0, name: "void", emoji: "🌑", family: GlyphFamily::Core }, - QualiaGlyph { idx: 1, name: "presence", emoji: "✨", family: GlyphFamily::Core }, - QualiaGlyph { idx: 2, name: "warmth", emoji: "🔥", family: GlyphFamily::Core }, - QualiaGlyph { idx: 3, name: "flow", emoji: "🌊", family: GlyphFamily::Core }, - QualiaGlyph { idx: 4, name: "clarity", emoji: "💎", family: GlyphFamily::Core }, - QualiaGlyph { idx: 5, name: "grounding", emoji: "🌳", family: GlyphFamily::Core }, - QualiaGlyph { idx: 6, name: "expansion", emoji: "🌌", family: GlyphFamily::Core }, - QualiaGlyph { idx: 7, name: "contraction", emoji: "🫧", family: GlyphFamily::Core }, - QualiaGlyph { idx: 8, name: "resonance", emoji: "🎵", family: GlyphFamily::Core }, - QualiaGlyph { idx: 9, name: "dissonance", emoji: "⚡", family: GlyphFamily::Core }, - QualiaGlyph { idx: 10, name: "wonder", emoji: "🌸", family: GlyphFamily::Core }, - QualiaGlyph { idx: 11, name: "grief", emoji: "🌧", family: GlyphFamily::Core }, - QualiaGlyph { idx: 12, name: "play", emoji: "🎭", family: GlyphFamily::Core }, - QualiaGlyph { idx: 13, name: "stillness", emoji: "🧘", family: GlyphFamily::Core }, - QualiaGlyph { idx: 14, name: "boundary", emoji: "🚪", family: GlyphFamily::Core }, - QualiaGlyph { idx: 15, name: "emergence", emoji: "🌱", family: GlyphFamily::Core }, + QualiaGlyph { + idx: 0, + name: "void", + emoji: "🌑", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 1, + name: "presence", + emoji: "✨", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 2, + name: "warmth", + emoji: "🔥", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 3, + name: "flow", + emoji: "🌊", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 4, + name: "clarity", + emoji: "💎", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 5, + name: "grounding", + emoji: "🌳", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 6, + name: "expansion", + emoji: "🌌", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 7, + name: "contraction", + emoji: "🫧", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 8, + name: "resonance", + emoji: "🎵", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 9, + name: "dissonance", + emoji: "⚡", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 10, + name: "wonder", + emoji: "🌸", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 11, + name: "grief", + emoji: "🌧", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 12, + name: "play", + emoji: "🎭", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 13, + name: "stillness", + emoji: "🧘", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 14, + name: "boundary", + emoji: "🚪", + family: GlyphFamily::Core, + }, + QualiaGlyph { + idx: 15, + name: "emergence", + emoji: "🌱", + family: GlyphFamily::Core, + }, // Relational (16-31) - QualiaGlyph { idx: 16, name: "connection", emoji: "🤝", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 17, name: "distance", emoji: "🌉", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 18, name: "trust", emoji: "💜", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 19, name: "caution", emoji: "🦔", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 20, name: "bond", emoji: "🤝", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 21, name: "privacy", emoji: "🔐", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 22, name: "giving", emoji: "🎁", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 23, name: "receiving", emoji: "🙏", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 24, name: "mirroring", emoji: "🪞", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 25, name: "autonomy", emoji: "🦋", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 26, name: "belonging", emoji: "🏠", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 27, name: "solitude", emoji: "🌙", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 28, name: "witnessing", emoji: "👁", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 29, name: "being_seen", emoji: "🌅", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 30, name: "holding", emoji: "🫂", family: GlyphFamily::Relational }, - QualiaGlyph { idx: 31, name: "release", emoji: "🕊", family: GlyphFamily::Relational }, + QualiaGlyph { + idx: 16, + name: "connection", + emoji: "🤝", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 17, + name: "distance", + emoji: "🌉", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 18, + name: "trust", + emoji: "💜", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 19, + name: "caution", + emoji: "🦔", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 20, + name: "bond", + emoji: "🤝", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 21, + name: "privacy", + emoji: "🔐", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 22, + name: "giving", + emoji: "🎁", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 23, + name: "receiving", + emoji: "🙏", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 24, + name: "mirroring", + emoji: "🪞", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 25, + name: "autonomy", + emoji: "🦋", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 26, + name: "belonging", + emoji: "🏠", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 27, + name: "solitude", + emoji: "🌙", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 28, + name: "witnessing", + emoji: "👁", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 29, + name: "being_seen", + emoji: "🌅", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 30, + name: "holding", + emoji: "🫂", + family: GlyphFamily::Relational, + }, + QualiaGlyph { + idx: 31, + name: "release", + emoji: "🕊", + family: GlyphFamily::Relational, + }, // Cognitive (32-47) - QualiaGlyph { idx: 32, name: "focus", emoji: "🎯", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 33, name: "diffuse", emoji: "☁", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 34, name: "analysis", emoji: "🔬", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 35, name: "synthesis", emoji: "🧬", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 36, name: "certainty", emoji: "✓", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 37, name: "uncertainty", emoji: "❓", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 38, name: "memory", emoji: "📜", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 39, name: "anticipation", emoji: "🔮", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 40, name: "learning", emoji: "📚", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 41, name: "forgetting", emoji: "🍂", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 42, name: "love", emoji: "💜", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 43, name: "creation", emoji: "🎨", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 44, name: "destruction", emoji: "💥", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 45, name: "transformation",emoji: "🦋", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 46, name: "persistence", emoji: "⚓", family: GlyphFamily::Cognitive }, - QualiaGlyph { idx: 47, name: "adaptation", emoji: "🌿", family: GlyphFamily::Cognitive }, + QualiaGlyph { + idx: 32, + name: "focus", + emoji: "🎯", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 33, + name: "diffuse", + emoji: "☁", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 34, + name: "analysis", + emoji: "🔬", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 35, + name: "synthesis", + emoji: "🧬", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 36, + name: "certainty", + emoji: "✓", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 37, + name: "uncertainty", + emoji: "❓", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 38, + name: "memory", + emoji: "📜", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 39, + name: "anticipation", + emoji: "🔮", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 40, + name: "learning", + emoji: "📚", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 41, + name: "forgetting", + emoji: "🍂", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 42, + name: "love", + emoji: "💜", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 43, + name: "creation", + emoji: "🎨", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 44, + name: "destruction", + emoji: "💥", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 45, + name: "transformation", + emoji: "🦋", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 46, + name: "persistence", + emoji: "⚓", + family: GlyphFamily::Cognitive, + }, + QualiaGlyph { + idx: 47, + name: "adaptation", + emoji: "🌿", + family: GlyphFamily::Cognitive, + }, // Somatic (48-63) - QualiaGlyph { idx: 48, name: "tension", emoji: "💪", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 49, name: "relaxation", emoji: "🌀", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 50, name: "energy", emoji: "⚡", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 51, name: "fatigue", emoji: "😴", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 52, name: "pleasant", emoji: "🌺", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 53, name: "pain", emoji: "🩹", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 54, name: "hunger", emoji: "🍽", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 55, name: "satiation", emoji: "😌", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 56, name: "excited", emoji: "🔥", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 57, name: "numbness", emoji: "❄", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 58, name: "breath", emoji: "💨", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 59, name: "pulse", emoji: "💓", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 60, name: "warmth_soma", emoji: "☀", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 61, name: "cold", emoji: "🧊", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 62, name: "softness", emoji: "🪶", family: GlyphFamily::Somatic }, - QualiaGlyph { idx: 63, name: "hardness", emoji: "🗿", family: GlyphFamily::Somatic }, + QualiaGlyph { + idx: 48, + name: "tension", + emoji: "💪", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 49, + name: "relaxation", + emoji: "🌀", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 50, + name: "energy", + emoji: "⚡", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 51, + name: "fatigue", + emoji: "😴", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 52, + name: "pleasant", + emoji: "🌺", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 53, + name: "pain", + emoji: "🩹", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 54, + name: "hunger", + emoji: "🍽", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 55, + name: "satiation", + emoji: "😌", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 56, + name: "excited", + emoji: "🔥", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 57, + name: "numbness", + emoji: "❄", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 58, + name: "breath", + emoji: "💨", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 59, + name: "pulse", + emoji: "💓", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 60, + name: "warmth_soma", + emoji: "☀", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 61, + name: "cold", + emoji: "🧊", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 62, + name: "softness", + emoji: "🪶", + family: GlyphFamily::Somatic, + }, + QualiaGlyph { + idx: 63, + name: "hardness", + emoji: "🗿", + family: GlyphFamily::Somatic, + }, ]; pub fn glyph(idx: u8) -> &'static QualiaGlyph { @@ -181,15 +511,20 @@ pub fn glyph_by_name(name: &str) -> Option<&'static QualiaGlyph> { /// cognitive shader substrate. Three u8s + one f32 = 7 bytes. #[derive(Debug, Clone, Copy, PartialEq)] pub struct SigmaState { - pub qualia_idx: u8, // 0..63 (or 0..255 when extended) - pub verb_idx: u8, // 0..143 (root * 12 + tense) - pub tau: f32, // 0.0 = qualia only, 1.0 = fully verb-bound - pub confidence: f32, // 0.0..1.0 + pub qualia_idx: u8, // 0..63 (or 0..255 when extended) + pub verb_idx: u8, // 0..143 (root * 12 + tense) + pub tau: f32, // 0.0 = qualia only, 1.0 = fully verb-bound + pub confidence: f32, // 0.0..1.0 } impl Default for SigmaState { fn default() -> Self { - Self { qualia_idx: 0, verb_idx: 0, tau: 0.5, confidence: 1.0 } + Self { + qualia_idx: 0, + verb_idx: 0, + tau: 0.5, + confidence: 1.0, + } } } @@ -204,19 +539,30 @@ impl SigmaState { } /// Is the state verb-dominated (action-infused)? - pub fn is_verb_bound(&self) -> bool { self.tau >= 0.5 } + pub fn is_verb_bound(&self) -> bool { + self.tau >= 0.5 + } pub fn qualia_name(&self) -> &'static str { glyph(self.qualia_idx).name } pub fn verb_name(&self) -> String { - format!("{}_{}", verb_root_name(self.verb_idx / 12), verb_tense_name(self.verb_idx % 12)) + format!( + "{}_{}", + verb_root_name(self.verb_idx / 12), + verb_tense_name(self.verb_idx % 12) + ) } /// Human-readable Σ(qualia, verb, τ=...) notation. pub fn describe(&self) -> String { - format!("Σ({}, {}, τ={:.2})", self.qualia_name(), self.verb_name(), self.tau) + format!( + "Σ({}, {}, τ={:.2})", + self.qualia_name(), + self.verb_name(), + self.tau + ) } } @@ -228,9 +574,9 @@ impl SigmaState { /// The SD of the three vertices drives the CollapseGate decision. #[derive(Debug, Clone, Copy, PartialEq)] pub struct TriangleGestalt { - pub clarity: f32, // top — cognition - pub warmth: f32, // left — emotion - pub presence: f32, // right — somatic + pub clarity: f32, // top — cognition + pub warmth: f32, // left — emotion + pub presence: f32, // right — somatic } impl TriangleGestalt { @@ -259,16 +605,21 @@ impl TriangleGestalt { let mean = (self.clarity + self.warmth + self.presence) / 3.0; let var = ((self.clarity - mean).powi(2) + (self.warmth - mean).powi(2) - + (self.presence - mean).powi(2)) / 3.0; + + (self.presence - mean).powi(2)) + / 3.0; var.sqrt() } /// Compute gate (Flow/Hold/Block) from SD — matches shader driver thresholds. pub fn gate(&self) -> GestaltGate { let sd = self.std_dev(); - if sd < 0.15 { GestaltGate::Flow } - else if sd < 0.35 { GestaltGate::Hold } - else { GestaltGate::Block } + if sd < 0.15 { + GestaltGate::Flow + } else if sd < 0.35 { + GestaltGate::Hold + } else { + GestaltGate::Block + } } /// Area of the triangle (energy). @@ -292,10 +643,22 @@ pub enum GestaltGate { /// Sigma Rosetta 17D band order — canonical, experientially loaded. pub const SIGMA_BAND_NAMES: [&str; 16] = [ - "warmth", "presence", "openness", "sovereignty", - "tenderness", "groundedness", "activation", "clarity", - "coherence", "closeness", "surrender", "seeking", - "awakening", "bond", "synthesis", "resonance", + "warmth", + "presence", + "openness", + "sovereignty", + "tenderness", + "groundedness", + "activation", + "clarity", + "coherence", + "closeness", + "surrender", + "seeking", + "awakening", + "bond", + "synthesis", + "resonance", ]; /// Convert QPL-17D (thinking-engine qualia) → Sigma-Rosetta 16 bands. @@ -304,44 +667,44 @@ pub const SIGMA_BAND_NAMES: [&str; 16] = [ /// QPL indices 0..16 — see thinking-engine `qualia::DIMS_17D` for labels. pub fn qpl_to_sigma(qpl: &[f32; 17]) -> [f32; 16] { [ - qpl[3], // 0: warmth ← warmth - qpl[11], // 1: presence ← presence - qpl[15], // 2: openness ← expansion - (1.0 - qpl[5]).clamp(0.0, 1.0),// 3: sovereignty ← 1 - boundary - qpl[10], // 4: tenderness ← qpl[10] - qpl[14], // 5: groundedness ← groundedness - qpl[0], // 6: activation ← arousal - qpl[4], // 7: clarity ← clarity - qpl[9], // 8: coherence ← coherence - qpl[10], // 9: closeness ← qpl[10] - qpl[13], // 10: surrender ← receptivity - qpl[1], // 11: seeking ← valence (positive seeks) - qpl[6], // 12: awakening ← depth - qpl[12], // 13: bond ← assertion (relational) - qpl[16], // 14: synthesis ← integration - (1.0 - qpl[2]).clamp(0.0, 1.0),// 15: resonance ← 1 - tension + qpl[3], // 0: warmth ← warmth + qpl[11], // 1: presence ← presence + qpl[15], // 2: openness ← expansion + (1.0 - qpl[5]).clamp(0.0, 1.0), // 3: sovereignty ← 1 - boundary + qpl[10], // 4: tenderness ← qpl[10] + qpl[14], // 5: groundedness ← groundedness + qpl[0], // 6: activation ← arousal + qpl[4], // 7: clarity ← clarity + qpl[9], // 8: coherence ← coherence + qpl[10], // 9: closeness ← qpl[10] + qpl[13], // 10: surrender ← receptivity + qpl[1], // 11: seeking ← valence (positive seeks) + qpl[6], // 12: awakening ← depth + qpl[12], // 13: bond ← assertion (relational) + qpl[16], // 14: synthesis ← integration + (1.0 - qpl[2]).clamp(0.0, 1.0), // 15: resonance ← 1 - tension ] } /// Inverse: Sigma 16 bands → QPL 17D (some dims approximate). pub fn sigma_to_qpl(sigma: &[f32; 16]) -> [f32; 17] { let mut qpl = [0.0f32; 17]; - qpl[0] = sigma[6]; // qpl[0] ← activation - qpl[1] = sigma[11]; // qpl[1] ← seeking - qpl[2] = 1.0 - sigma[15]; // qpl[2] ← 1 - resonance - qpl[3] = sigma[0]; // qpl[3] ← warmth - qpl[4] = sigma[7]; // qpl[4] ← clarity - qpl[5] = 1.0 - sigma[3]; // qpl[5] ← 1 - sovereignty - qpl[6] = sigma[12]; // qpl[6] ← awakening - qpl[9] = sigma[8]; // qpl[9] ← coherence - qpl[10] = sigma[9]; // qpl[10] ← closeness - qpl[11] = sigma[1]; // qpl[11] ← presence - qpl[12] = sigma[13]; // qpl[12] ← bond - qpl[13] = sigma[10]; // qpl[13] ← surrender - qpl[14] = sigma[5]; // qpl[14] ← groundedness - qpl[15] = sigma[2]; // qpl[15] ← openness - qpl[16] = sigma[14]; // qpl[16] ← synthesis - // qpl[7] + qpl[8] are convergence-only — no sigma mapping + qpl[0] = sigma[6]; // qpl[0] ← activation + qpl[1] = sigma[11]; // qpl[1] ← seeking + qpl[2] = 1.0 - sigma[15]; // qpl[2] ← 1 - resonance + qpl[3] = sigma[0]; // qpl[3] ← warmth + qpl[4] = sigma[7]; // qpl[4] ← clarity + qpl[5] = 1.0 - sigma[3]; // qpl[5] ← 1 - sovereignty + qpl[6] = sigma[12]; // qpl[6] ← awakening + qpl[9] = sigma[8]; // qpl[9] ← coherence + qpl[10] = sigma[9]; // qpl[10] ← closeness + qpl[11] = sigma[1]; // qpl[11] ← presence + qpl[12] = sigma[13]; // qpl[12] ← bond + qpl[13] = sigma[10]; // qpl[13] ← surrender + qpl[14] = sigma[5]; // qpl[14] ← groundedness + qpl[15] = sigma[2]; // qpl[15] ← openness + qpl[16] = sigma[14]; // qpl[16] ← synthesis + // qpl[7] + qpl[8] are convergence-only — no sigma mapping qpl } @@ -373,13 +736,19 @@ pub struct QuadTriangleGestalt { impl QuadTriangleGestalt { pub fn neutral() -> Self { let n = TriangleGestalt::new(0.5, 0.5, 0.5); - Self { processing: n, content: n, gestalt: n, crystallization: n } + Self { + processing: n, + content: n, + gestalt: n, + crystallization: n, + } } /// Maximum SD across all four triangles. /// Determines the "worst" axis — drives the overall gate decision. pub fn max_std_dev(&self) -> f32 { - self.processing.std_dev() + self.processing + .std_dev() .max(self.content.std_dev()) .max(self.gestalt.std_dev()) .max(self.crystallization.std_dev()) @@ -388,9 +757,13 @@ impl QuadTriangleGestalt { /// Conservative gate: any triangle in Block → Block, any in Hold → Hold, else Flow. pub fn gate(&self) -> GestaltGate { let worst = self.max_std_dev(); - if worst > 0.35 { GestaltGate::Block } - else if worst > 0.15 { GestaltGate::Hold } - else { GestaltGate::Flow } + if worst > 0.35 { + GestaltGate::Block + } else if worst > 0.15 { + GestaltGate::Hold + } else { + GestaltGate::Flow + } } /// Mean balance across all four triangles. @@ -398,14 +771,21 @@ impl QuadTriangleGestalt { (self.processing.balance() + self.content.balance() + self.gestalt.balance() - + self.crystallization.balance()) / 4.0 + + self.crystallization.balance()) + / 4.0 } } /// Triangle-A presets from agi-chat's COGNITIVE_PROFILES (the 3 anchor styles). -pub fn processing_analytical() -> TriangleGestalt { TriangleGestalt::new(0.9, 0.1, 0.5) } -pub fn processing_intuitive() -> TriangleGestalt { TriangleGestalt::new(0.1, 0.9, 0.3) } -pub fn processing_procedural() -> TriangleGestalt { TriangleGestalt::new(0.3, 0.1, 0.9) } +pub fn processing_analytical() -> TriangleGestalt { + TriangleGestalt::new(0.9, 0.1, 0.5) +} +pub fn processing_intuitive() -> TriangleGestalt { + TriangleGestalt::new(0.1, 0.9, 0.3) +} +pub fn processing_procedural() -> TriangleGestalt { + TriangleGestalt::new(0.3, 0.1, 0.9) +} #[cfg(test)] mod tests { @@ -460,7 +840,7 @@ mod tests { fn quad_triangle_worst_triangle_dominates() { let mut q = QuadTriangleGestalt::neutral(); q.processing = TriangleGestalt::new(1.0, 0.0, 0.5); // Block - // Other triangles neutral, but processing blocks → overall Block + // Other triangles neutral, but processing blocks → overall Block assert_eq!(q.gate(), GestaltGate::Block); } @@ -472,7 +852,7 @@ mod tests { assert!(a.warmth < 0.2); let i = processing_intuitive(); - assert!(i.warmth > 0.8); // intuitive = high feeling + assert!(i.warmth > 0.8); // intuitive = high feeling assert!(i.clarity < 0.2); let p = processing_procedural(); @@ -549,14 +929,23 @@ impl InteractionKinematic { 0 // CAUSES }; - Self { a, b, tension, predicate_plane } + Self { + a, + b, + tension, + predicate_plane, + } } /// Resonance = 1 - tension. - pub fn resonance(&self) -> f32 { 1.0 - self.tension } + pub fn resonance(&self) -> f32 { + 1.0 - self.tension + } /// Is this pair aligned (low tension)? - pub fn aligned(&self) -> bool { self.tension < 0.3 } + pub fn aligned(&self) -> bool { + self.tension < 0.3 + } } #[cfg(test)] @@ -577,7 +966,10 @@ mod sigma13_tests { let b = SigmaState::new(4, verb_index(8, 0), 0.5); // transform_present let k = InteractionKinematic::new(a, b); assert!(k.tension > 0.0); - assert_ne!(k.predicate_plane, 2, "not same-verb-root, should not be SUPPORTS"); + assert_ne!( + k.predicate_plane, 2, + "not same-verb-root, should not be SUPPORTS" + ); } #[test] diff --git a/crates/cognitive-shader-driver/src/token_agreement.rs b/crates/cognitive-shader-driver/src/token_agreement.rs index 1afe841a..020146f2 100644 --- a/crates/cognitive-shader-driver/src/token_agreement.rs +++ b/crates/cognitive-shader-driver/src/token_agreement.rs @@ -89,7 +89,10 @@ impl std::fmt::Display for TokenAgreementError { match self { Self::ModelPathMissing { path } => write!(f, "model path missing: {path}"), Self::EmptyPromptSet => write!(f, "prompt set empty"), - Self::TokenCountMismatch { reference, candidate } => { + Self::TokenCountMismatch { + reference, + candidate, + } => { write!(f, "token count mismatch: ref={reference} cand={candidate}") } Self::NotImplementedYet { what } => { @@ -157,12 +160,20 @@ impl TopKAgreement { /// top1 match rate ∈ [0, 1]. Passes the cert gate at ≥ 0.99 per D0.2. pub fn top1_rate(&self) -> f32 { - if self.total_positions == 0 { 0.0 } else { self.top1_matches as f32 / self.total_positions as f32 } + if self.total_positions == 0 { + 0.0 + } else { + self.top1_matches as f32 / self.total_positions as f32 + } } /// top5 match rate ∈ [0, 1]. Passes the cert gate at ≥ 0.999 per D0.2. pub fn top5_rate(&self) -> f32 { - if self.total_positions == 0 { 0.0 } else { self.top5_matches as f32 / self.total_positions as f32 } + if self.total_positions == 0 { + 0.0 + } else { + self.top5_matches as f32 / self.total_positions as f32 + } } /// Meets D0.2 acceptance thresholds (top1 ≥ 0.99 AND top5 ≥ 0.999). @@ -210,7 +221,12 @@ impl TokenAgreementHarness { candidate: WireCodecParams, n_tokens: u32, ) -> Self { - Self { reference, baseline, candidate, n_tokens } + Self { + reference, + baseline, + candidate, + n_tokens, + } } /// D2.1 STUB: returns a deterministic zero-rate result with `stub: true` @@ -255,7 +271,10 @@ mod tests { WireCodecParams { subspaces: 6, centroids: 256, - residual: WireResidualSpec { depth: 0, centroids: 256 }, + residual: WireResidualSpec { + depth: 0, + centroids: 256, + }, lane_width: WireLaneWidth::F32x16, pre_rotation: WireRotation::Identity, distance: WireDistance::AdcU8, @@ -275,7 +294,8 @@ mod tests { #[test] fn reference_model_load_missing_path_yields_typed_error() { - let err = ReferenceModel::load(Path::new("/nonexistent/xyz/model.safetensors")).unwrap_err(); + let err = + ReferenceModel::load(Path::new("/nonexistent/xyz/model.safetensors")).unwrap_err(); assert!(matches!(err, TokenAgreementError::ModelPathMissing { .. })); } @@ -320,7 +340,13 @@ mod tests { let r = vec![vec![1], vec![2]]; let c = vec![vec![1]]; let err = TopKAgreement::compare(&r, &c).unwrap_err(); - assert!(matches!(err, TokenAgreementError::TokenCountMismatch { reference: 2, candidate: 1 })); + assert!(matches!( + err, + TokenAgreementError::TokenCountMismatch { + reference: 2, + candidate: 1 + } + )); } #[test] @@ -370,7 +396,10 @@ mod tests { total_positions: 1000, divergence_positions: vec![], }; - assert!(!a.meets_cert_gate(), "top1 just below threshold should fail even if top5 passes"); + assert!( + !a.meets_cert_gate(), + "top1 just below threshold should fail even if top5 passes" + ); } #[test] @@ -383,20 +412,22 @@ mod tests { total_positions: 1000, divergence_positions: vec![], }; - assert!(!a.meets_cert_gate(), "top5 just below threshold should fail even if top1 passes"); + assert!( + !a.meets_cert_gate(), + "top5 just below threshold should fail even if top1 passes" + ); } #[test] fn harness_measure_stub_returns_machine_checkable_stub_flag() { let ref_model = ReferenceModel::stub(1, 16); - let harness = TokenAgreementHarness::new( - ref_model, - WireBaseline::Passthrough, - stub_candidate(), - 128, - ); + let harness = + TokenAgreementHarness::new(ref_model, WireBaseline::Passthrough, stub_candidate(), 128); let result = harness.measure_stub().unwrap(); - assert!(result.stub, "stub flag MUST be true so clients cannot confuse stub for real measurement"); + assert!( + result.stub, + "stub flag MUST be true so clients cannot confuse stub for real measurement" + ); assert_eq!(result.backend, "stub"); assert_eq!(result.top1_rate, 0.0); assert_eq!(result.top5_rate, 0.0); @@ -406,12 +437,8 @@ mod tests { #[test] fn harness_measure_full_returns_not_implemented_pointing_at_d22() { let ref_model = ReferenceModel::stub(1, 16); - let harness = TokenAgreementHarness::new( - ref_model, - WireBaseline::Passthrough, - stub_candidate(), - 128, - ); + let harness = + TokenAgreementHarness::new(ref_model, WireBaseline::Passthrough, stub_candidate(), 128); let err = harness.measure_full().unwrap_err(); assert!(matches!(err, TokenAgreementError::NotImplementedYet { .. })); } @@ -419,12 +446,8 @@ mod tests { #[test] fn harness_measure_stub_rejects_zero_n_tokens() { let ref_model = ReferenceModel::stub(1, 16); - let harness = TokenAgreementHarness::new( - ref_model, - WireBaseline::Passthrough, - stub_candidate(), - 0, - ); + let harness = + TokenAgreementHarness::new(ref_model, WireBaseline::Passthrough, stub_candidate(), 0); let err = harness.measure_stub().unwrap_err(); assert!(matches!(err, TokenAgreementError::EmptyPromptSet)); } diff --git a/crates/cognitive-shader-driver/src/wire.rs b/crates/cognitive-shader-driver/src/wire.rs index 35eed867..7070fd5c 100644 --- a/crates/cognitive-shader-driver/src/wire.rs +++ b/crates/cognitive-shader-driver/src/wire.rs @@ -60,10 +60,18 @@ pub struct WireDispatch { pub meta_filter: Option, } -fn default_layer_mask() -> u8 { 0xFF } -fn default_radius() -> u16 { u16::MAX } -fn default_max_cycles() -> u16 { 10 } -fn default_entropy_floor() -> f32 { 0.05 } +fn default_layer_mask() -> u8 { + 0xFF +} +fn default_radius() -> u16 { + u16::MAX +} +fn default_max_cycles() -> u16 { + 10 +} +fn default_entropy_floor() -> f32 { + 0.05 +} #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(tag = "type", content = "value")] @@ -88,7 +96,9 @@ pub struct WireMetaFilter { pub free_e_max: u8, } -fn default_free_e_max() -> u8 { 63 } +fn default_free_e_max() -> u8 { + 63 +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WireIngest { @@ -201,10 +211,18 @@ pub struct WireCalibrateRequest { pub icc_samples: usize, } -fn default_cal_subspaces() -> usize { 6 } -fn default_cal_centroids() -> usize { 256 } -fn default_cal_iters() -> usize { 20 } -fn default_icc_samples() -> usize { 512 } +fn default_cal_subspaces() -> usize { + 6 +} +fn default_cal_centroids() -> usize { + 256 +} +fn default_cal_iters() -> usize { + 20 +} +fn default_icc_samples() -> usize { + 512 +} #[derive(Debug, Clone, Serialize, Deserialize)] #[non_exhaustive] @@ -239,7 +257,9 @@ pub struct WireCalibrateResponse { pub backend: String, } -fn default_backend() -> String { "legacy".to_string() } +fn default_backend() -> String { + "legacy".to_string() +} // ═══════════════════════════════════════════════════════════════════════════ // D0.1 — CodecParams serde mirrors (Rule F: serialise at edges only) @@ -251,10 +271,18 @@ fn default_backend() -> String { "legacy".to_string() } // ═══════════════════════════════════════════════════════════════════════════ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum WireLaneWidth { F32x16, U8x64, F64x8, BF16x32 } +pub enum WireLaneWidth { + F32x16, + U8x64, + F64x8, + BF16x32, +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum WireDistance { AdcU8, AdcI8 } +pub enum WireDistance { + AdcU8, + AdcI8, +} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "lowercase")] @@ -291,12 +319,27 @@ pub struct WireCodecParams { pub seed: u64, } -fn default_wire_residual() -> WireResidualSpec { WireResidualSpec { depth: 0, centroids: 256 } } -fn default_wire_lane() -> WireLaneWidth { WireLaneWidth::F32x16 } -fn default_wire_rotation() -> WireRotation { WireRotation::Identity } -fn default_wire_distance() -> WireDistance { WireDistance::AdcU8 } -fn default_calibration_rows() -> u32 { 2048 } -fn default_seed() -> u64 { 42 } +fn default_wire_residual() -> WireResidualSpec { + WireResidualSpec { + depth: 0, + centroids: 256, + } +} +fn default_wire_lane() -> WireLaneWidth { + WireLaneWidth::F32x16 +} +fn default_wire_rotation() -> WireRotation { + WireRotation::Identity +} +fn default_wire_distance() -> WireDistance { + WireDistance::AdcU8 +} +fn default_calibration_rows() -> u32 { + 2048 +} +fn default_seed() -> u64 { + 42 +} // ─────── TryFrom conversions — one decode at ingress (Rule F) ─────── @@ -330,14 +373,23 @@ impl From for CamRotation { match r { WireRotation::Identity => CamRotation::Identity, WireRotation::Hadamard { dim } => CamRotation::Hadamard { dim }, - WireRotation::Opq { matrix_blob_id, dim } => CamRotation::Opq { matrix_blob_id, dim }, + WireRotation::Opq { + matrix_blob_id, + dim, + } => CamRotation::Opq { + matrix_blob_id, + dim, + }, } } } impl From for CamResidualSpec { fn from(r: WireResidualSpec) -> Self { - CamResidualSpec { depth: r.depth, centroids: r.centroids } + CamResidualSpec { + depth: r.depth, + centroids: r.centroids, + } } } @@ -435,8 +487,12 @@ impl AlignedBytes { (self.ptr as usize).is_multiple_of(64) } - pub fn len(&self) -> usize { self.len } - pub fn is_empty(&self) -> bool { self.len == 0 } + pub fn len(&self) -> usize { + self.len + } + pub fn is_empty(&self) -> bool { + self.len == 0 + } } impl Drop for AlignedBytes { @@ -462,7 +518,11 @@ impl std::fmt::Display for WireTensorViewError { match self { Self::Base64(e) => write!(f, "base64 decode: {}", e), Self::SizeMismatch { expected, actual } => { - write!(f, "byte size mismatch: expected {} got {}", expected, actual) + write!( + f, + "byte size mismatch: expected {} got {}", + expected, actual + ) } Self::ZeroShape => write!(f, "tensor view shape contains zero dimension"), } @@ -488,10 +548,14 @@ impl WireTensorView { } /// Row count. - pub fn row_count(&self) -> u32 { self.shape[0] } + pub fn row_count(&self) -> u32 { + self.shape[0] + } /// Column count (elements per row). - pub fn col_count(&self) -> u32 { self.shape[1] } + pub fn col_count(&self) -> u32 { + self.shape[1] + } /// Row stride in bytes. pub fn row_bytes(&self) -> usize { @@ -512,7 +576,10 @@ impl WireTensorView { .map_err(WireTensorViewError::Base64)?; let expected = self.expected_bytes(); if raw.len() != expected { - return Err(WireTensorViewError::SizeMismatch { expected, actual: raw.len() }); + return Err(WireTensorViewError::SizeMismatch { + expected, + actual: raw.len(), + }); } let mut aligned = AlignedBytes::alloc_zeroed(expected); aligned.as_mut_slice().copy_from_slice(&raw); @@ -557,7 +624,9 @@ pub struct WireProbeRequest { pub icc_samples: usize, } -fn default_probe_counts() -> Vec { vec![128, 256, 512, 1024] } +fn default_probe_counts() -> Vec { + vec![128, 256, 512, 1024] +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WireProbeEntry { @@ -603,7 +672,9 @@ pub struct WirePlanRequest { pub situation: Option, } -fn default_plan_mode() -> String { "auto".to_string() } +fn default_plan_mode() -> String { + "auto".to_string() +} // ═══════════════════════════════════════════════════════════════════════════ // Generic OrchestrationBridge routing — UnifiedStep as JSON @@ -629,15 +700,23 @@ pub struct WireStepResult { /// Mirror of lance_graph_contract::mul::SituationInput for JSON transport. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct WireSituation { - #[serde(default = "half")] pub felt_competence: f64, - #[serde(default = "half")] pub demonstrated_competence: f64, - #[serde(default = "half")] pub source_reliability: f64, - #[serde(default = "half")] pub environment_stability: f64, - #[serde(default = "half")] pub calibration_accuracy: f64, - #[serde(default = "half")] pub complexity_ratio: f64, + #[serde(default = "half")] + pub felt_competence: f64, + #[serde(default = "half")] + pub demonstrated_competence: f64, + #[serde(default = "half")] + pub source_reliability: f64, + #[serde(default = "half")] + pub environment_stability: f64, + #[serde(default = "half")] + pub calibration_accuracy: f64, + #[serde(default = "half")] + pub complexity_ratio: f64, } -fn half() -> f64 { 0.5 } +fn half() -> f64 { + 0.5 +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WirePlanResponse { @@ -704,13 +783,38 @@ pub struct WireRunbookStepLabeled { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind")] pub enum WireRunbookStepResult { - Tensors { label: String, response: WireTensorsResponse }, - Calibrate { label: String, response: WireCalibrateResponse }, - Probe { label: String, response: WireProbeResponse }, - Dispatch { label: String, response: WireCrystal }, - Ingest { label: String, ingested: u32, row_start: u32, row_end: u32, write_cursor: u32 }, - Plan { label: String, response: WirePlanResponse }, - Error { label: String, step: String, error: String }, + Tensors { + label: String, + response: WireTensorsResponse, + }, + Calibrate { + label: String, + response: WireCalibrateResponse, + }, + Probe { + label: String, + response: WireProbeResponse, + }, + Dispatch { + label: String, + response: WireCrystal, + }, + Ingest { + label: String, + ingested: u32, + row_start: u32, + row_end: u32, + write_cursor: u32, + }, + Plan { + label: String, + response: WirePlanResponse, + }, + Error { + label: String, + step: String, + error: String, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -812,9 +916,7 @@ impl WireDispatch { let style = match &self.style { WireStyleSelector::Auto => StyleSelector::Auto, WireStyleSelector::Ordinal(n) => StyleSelector::Ordinal(*n), - WireStyleSelector::Named(s) => { - StyleSelector::Ordinal(named_to_ordinal(s)) - } + WireStyleSelector::Named(s) => StyleSelector::Ordinal(named_to_ordinal(s)), }; let rung = match self.rung { 0 => RungLevel::Surface, @@ -833,13 +935,17 @@ impl WireDispatch { "persist" => EmitMode::Persist, _ => EmitMode::Cycle, }; - let meta_prefilter = self.meta_filter.as_ref().map(|f| MetaFilter { - thinking_mask: f.thinking_mask, - awareness_min: f.awareness_min, - nars_f_min: f.nars_f_min, - nars_c_min: f.nars_c_min, - free_e_max: f.free_e_max, - }).unwrap_or(MetaFilter::ALL); + let meta_prefilter = self + .meta_filter + .as_ref() + .map(|f| MetaFilter { + thinking_mask: f.thinking_mask, + awareness_min: f.awareness_min, + nars_f_min: f.nars_f_min, + nars_c_min: f.nars_c_min, + free_e_max: f.free_e_max, + }) + .unwrap_or(MetaFilter::ALL); ShaderDispatch { meta_prefilter, @@ -874,7 +980,12 @@ impl From<&ShaderHit> for WireHit { impl From<&ShaderResonance> for WireResonance { fn from(r: &ShaderResonance) -> Self { Self { - top_k: r.top_k.iter().filter(|h| h.resonance > 0.0).map(WireHit::from).collect(), + top_k: r + .top_k + .iter() + .filter(|h| h.resonance > 0.0) + .map(WireHit::from) + .collect(), hit_count: r.hit_count, cycles_used: r.cycles_used, entropy: r.entropy, @@ -886,13 +997,19 @@ impl From<&ShaderResonance> for WireResonance { impl From<&ShaderBus> for WireBus { fn from(b: &ShaderBus) -> Self { - let hex: String = b.cycle_fingerprint.iter() + let hex: String = b + .cycle_fingerprint + .iter() .map(|w| format!("{:016x}", w)) .collect::>() .join(""); - let gate_str = if b.gate.is_flow() { "flow" } - else if b.gate.is_hold() { "hold" } - else { "block" }; + let gate_str = if b.gate.is_flow() { + "flow" + } else if b.gate.is_hold() { + "hold" + } else { + "block" + }; Self { cycle_fingerprint_hex: hex, emitted_edges: b.emitted_edges[..b.emitted_edge_count as usize].to_vec(), @@ -969,7 +1086,6 @@ pub enum WireBaseline { Passthrough, } - /// `POST /v1/shader/token-agreement` request. /// /// Client provides the model + a `CodecParams` candidate + a prompt-set @@ -1031,7 +1147,9 @@ pub struct WireTokenAgreementResult { pub backend: String, } -fn default_ta_backend() -> String { "stub".to_string() } +fn default_ta_backend() -> String { + "stub".to_string() +} // ═══════════════════════════════════════════════════════════════════════════ // D0.3 — WireSweep: the streaming cross-product sweep surface (Phase 0 stub) @@ -1102,13 +1220,27 @@ pub struct WireSweepGrid { pub seed: u64, } -fn default_subspaces_axis() -> Vec { vec![6] } -fn default_centroids_axis() -> Vec { vec![256] } -fn default_residual_depths_axis() -> Vec { vec![0] } -fn default_rotations_axis() -> Vec { vec![WireRotation::Identity] } -fn default_distances_axis() -> Vec { vec![WireDistance::AdcU8] } -fn default_lane_widths_axis() -> Vec { vec![WireLaneWidth::F32x16] } -fn default_residual_centroids() -> u32 { 256 } +fn default_subspaces_axis() -> Vec { + vec![6] +} +fn default_centroids_axis() -> Vec { + vec![256] +} +fn default_residual_depths_axis() -> Vec { + vec![0] +} +fn default_rotations_axis() -> Vec { + vec![WireRotation::Identity] +} +fn default_distances_axis() -> Vec { + vec![WireDistance::AdcU8] +} +fn default_lane_widths_axis() -> Vec { + vec![WireLaneWidth::F32x16] +} +fn default_residual_centroids() -> u32 { + 256 +} impl WireSweepGrid { /// Product of all axis lengths. @@ -1239,7 +1371,8 @@ mod tests { #[test] fn wire_dispatch_with_style() { - let json = r#"{"row_start": 0, "row_end": 50, "style": {"type": "Named", "value": "analytical"}}"#; + let json = + r#"{"row_start": 0, "row_end": 50, "style": {"type": "Named", "value": "analytical"}}"#; let wd: WireDispatch = serde_json::from_str(json).unwrap(); let internal = wd.to_internal(); matches!(internal.style, StyleSelector::Ordinal(1)); @@ -1331,7 +1464,12 @@ mod tests { let crystal = ShaderCrystal { bus: ShaderBus::empty(), persisted_row: Some(42), - meta: MetaSummary { confidence: 0.9, meta_confidence: 0.8, brier: 0.1, should_admit_ignorance: false }, + meta: MetaSummary { + confidence: 0.9, + meta_confidence: 0.8, + brier: 0.1, + should_admit_ignorance: false, + }, alpha_composite: None, }; let wire = WireCrystal::from(&crystal); @@ -1349,18 +1487,29 @@ mod tests { let wire = WireCodecParams { subspaces: 6, centroids: 1024, - residual: WireResidualSpec { depth: 1, centroids: 256 }, + residual: WireResidualSpec { + depth: 1, + centroids: 256, + }, lane_width: WireLaneWidth::BF16x32, - pre_rotation: WireRotation::Opq { matrix_blob_id: 0xDEADBEEF, dim: 4096 }, + pre_rotation: WireRotation::Opq { + matrix_blob_id: 0xDEADBEEF, + dim: 4096, + }, distance: WireDistance::AdcU8, calibration_rows: 2048, measurement_rows: 512, seed: 42, }; - let params: CodecParams = wire.try_into().expect("OPQ + BF16x32 is precision-ladder valid"); + let params: CodecParams = wire + .try_into() + .expect("OPQ + BF16x32 is precision-ladder valid"); assert_eq!(params.subspaces, 6); assert_eq!(params.centroids, 1024); - assert!(params.is_matmul_heavy(), "OPQ + wide codebook must be matmul-heavy"); + assert!( + params.is_matmul_heavy(), + "OPQ + wide codebook must be matmul-heavy" + ); } #[test] @@ -1369,9 +1518,15 @@ mod tests { let wire = WireCodecParams { subspaces: 6, centroids: 256, - residual: WireResidualSpec { depth: 0, centroids: 256 }, + residual: WireResidualSpec { + depth: 0, + centroids: 256, + }, lane_width: WireLaneWidth::F32x16, - pre_rotation: WireRotation::Opq { matrix_blob_id: 1, dim: 4096 }, + pre_rotation: WireRotation::Opq { + matrix_blob_id: 1, + dim: 4096, + }, distance: WireDistance::AdcU8, calibration_rows: 2048, measurement_rows: 0, @@ -1387,7 +1542,10 @@ mod tests { let wire = WireCodecParams { subspaces: 6, centroids: 256, - residual: WireResidualSpec { depth: 0, centroids: 256 }, + residual: WireResidualSpec { + depth: 0, + centroids: 256, + }, lane_width: WireLaneWidth::F32x16, pre_rotation: WireRotation::Identity, distance: WireDistance::AdcU8, @@ -1396,18 +1554,21 @@ mod tests { seed: 42, }; let err = CodecParams::try_from(wire).unwrap_err(); - assert!(matches!(err, CodecParamsError::CalibrationEqualsMeasurement { rows: 128 })); + assert!(matches!( + err, + CodecParamsError::CalibrationEqualsMeasurement { rows: 128 } + )); } #[test] fn wire_codec_params_deserializes_from_minimal_json() { let json = r#"{"subspaces":6,"centroids":256}"#; let wire: WireCodecParams = serde_json::from_str(json).unwrap(); - assert_eq!(wire.lane_width, WireLaneWidth::F32x16); // default - assert_eq!(wire.distance, WireDistance::AdcU8); // default - assert_eq!(wire.pre_rotation, WireRotation::Identity); // default - assert_eq!(wire.calibration_rows, 2048); // default - assert_eq!(wire.seed, 42); // default + assert_eq!(wire.lane_width, WireLaneWidth::F32x16); // default + assert_eq!(wire.distance, WireDistance::AdcU8); // default + assert_eq!(wire.pre_rotation, WireRotation::Identity); // default + assert_eq!(wire.calibration_rows, 2048); // default + assert_eq!(wire.seed, 42); // default } #[cfg(feature = "serve")] @@ -1425,7 +1586,10 @@ mod tests { assert_eq!(view.expected_bytes(), 256); assert_eq!(view.row_bytes(), 64); let decoded = view.decode().expect("valid base64 + matching size"); - assert!(decoded.is_aligned_64(), "Rule A: decoded buffer MUST be 64-byte aligned"); + assert!( + decoded.is_aligned_64(), + "Rule A: decoded buffer MUST be 64-byte aligned" + ); assert_eq!(decoded.len(), 256); // Rule A: slice::array_windows::<64>() must consume the row directly. @@ -1435,7 +1599,10 @@ mod tests { for _w in row0.array_windows::<64>() { windows += 1; } - assert_eq!(windows, 1, "exactly one 64-byte window per row at this size"); + assert_eq!( + windows, 1, + "exactly one 64-byte window per row at this size" + ); } #[cfg(feature = "serve")] @@ -1449,7 +1616,13 @@ mod tests { bytes_base64: STANDARD.encode(&wrong_bytes), }; let err = view.decode().unwrap_err(); - assert!(matches!(err, WireTensorViewError::SizeMismatch { expected: 256, actual: 100 })); + assert!(matches!( + err, + WireTensorViewError::SizeMismatch { + expected: 256, + actual: 100 + } + )); } #[cfg(feature = "serve")] @@ -1464,7 +1637,9 @@ mod tests { bytes_base64: STANDARD.encode(&bytes), }; let decoded = view.decode().unwrap(); - let sub = view.subspace(&decoded, 0, 2, 16).expect("subspace 2 of row 0"); + let sub = view + .subspace(&decoded, 0, 2, 16) + .expect("subspace 2 of row 0"); assert_eq!(sub.len(), 16); // subspace 2 of row 0 starts at byte 32 (0 × 96 + 2 × 16) — value = 32. assert_eq!(sub[0], 32); @@ -1497,9 +1672,15 @@ mod tests { candidate: WireCodecParams { subspaces: 6, centroids: 1024, - residual: WireResidualSpec { depth: 1, centroids: 256 }, + residual: WireResidualSpec { + depth: 1, + centroids: 256, + }, lane_width: WireLaneWidth::BF16x32, - pre_rotation: WireRotation::Opq { matrix_blob_id: 0x42, dim: 4096 }, + pre_rotation: WireRotation::Opq { + matrix_blob_id: 0x42, + dim: 4096, + }, distance: WireDistance::AdcU8, calibration_rows: 2048, measurement_rows: 512, @@ -1653,7 +1834,10 @@ mod tests { #[test] fn sweep_measure_serializes_snake_case() { let m = WireMeasure::ReconstructionIccHeldOut; - assert_eq!(serde_json::to_string(&m).unwrap(), "\"reconstruction_icc_held_out\""); + assert_eq!( + serde_json::to_string(&m).unwrap(), + "\"reconstruction_icc_held_out\"" + ); let m: WireMeasure = serde_json::from_str("\"token_agreement_top1\"").unwrap(); assert_eq!(m, WireMeasure::TokenAgreementTop1); } diff --git a/crates/cognitive-shader-driver/tests/busdto_bridge_test.rs b/crates/cognitive-shader-driver/tests/busdto_bridge_test.rs index 49d28e13..52089063 100644 --- a/crates/cognitive-shader-driver/tests/busdto_bridge_test.rs +++ b/crates/cognitive-shader-driver/tests/busdto_bridge_test.rs @@ -77,7 +77,10 @@ fn busdto_round_trip_dense_top_k_is_bit_exact() { got_idx.sort_unstable(); sent_idx.dedup(); got_idx.dedup(); - assert_eq!(sent_idx, got_idx, "top_k index SET must be bit-exact (positions in 0..16384)"); + assert_eq!( + sent_idx, got_idx, + "top_k index SET must be bit-exact (positions in 0..16384)" + ); for i in 0..8 { assert_eq!( @@ -176,7 +179,11 @@ fn busdto_dispatch_writes_meta_thinking_style() { let bus = make_dense_bus(99); dispatch_busdto(&mut bs, 2, &bus, 7 /* focused */); let m = bs.meta.get(2); - assert_eq!(m.thinking(), 7, "style ordinal must land in MetaWord.thinking"); + assert_eq!( + m.thinking(), + 7, + "style ordinal must land in MetaWord.thinking" + ); assert_eq!(m.awareness(), 3, "converged=true → awareness=FLOW(3)"); // free_e clamped to <=63, but cycle_count was 7 so no clamp. assert_eq!(m.free_e(), 7); @@ -190,8 +197,16 @@ fn busdto_round_trip_zero_codebook_index_is_handled() { let bus = BusDto { codebook_index: 0, energy: 0.1, - top_k: [(0, 0.1), (0, 0.0), (0, 0.0), (0, 0.0), - (0, 0.0), (0, 0.0), (0, 0.0), (0, 0.0)], + top_k: [ + (0, 0.1), + (0, 0.0), + (0, 0.0), + (0, 0.0), + (0, 0.0), + (0, 0.0), + (0, 0.0), + (0, 0.0), + ], cycle_count: 0, converged: true, }; diff --git a/crates/cognitive-shader-driver/tests/end_to_end.rs b/crates/cognitive-shader-driver/tests/end_to_end.rs index 1d3e78a4..f77fdb8b 100644 --- a/crates/cognitive-shader-driver/tests/end_to_end.rs +++ b/crates/cognitive-shader-driver/tests/end_to_end.rs @@ -6,24 +6,26 @@ use bgz17::base17::Base17; use bgz17::palette::Palette; use bgz17::palette_semiring::PaletteSemiring; -use cognitive_shader_driver::bindspace::{BindSpace, WORDS_PER_FP, FLOATS_PER_VSA}; +use cognitive_shader_driver::auto_style; +use cognitive_shader_driver::bindspace::{BindSpace, FLOATS_PER_VSA, WORDS_PER_FP}; use cognitive_shader_driver::driver::CognitiveShaderBuilder; use cognitive_shader_driver::engine_bridge::{ - ingest_codebook_indices, persist_cycle, read_qualia_17d, write_qualia_17d, - classification_distance, read_qualia_decomposed, + classification_distance, ingest_codebook_indices, persist_cycle, read_qualia_17d, + read_qualia_decomposed, write_qualia_17d, }; use cognitive_shader_driver::{ CognitiveShaderDriver, ColumnWindow, MetaFilter, ShaderDispatch, StyleSelector, }; -use cognitive_shader_driver::auto_style; fn palette_256() -> PaletteSemiring { - let entries: Vec = (0..256).map(|i| { - let mut dims = [0i16; 17]; - dims[0] = (i * 100 % 3400) as i16; - dims[1] = ((i * 37) % 200) as i16; - Base17 { dims } - }).collect(); + let entries: Vec = (0..256) + .map(|i| { + let mut dims = [0i16; 17]; + dims[0] = (i * 100 % 3400) as i16; + dims[1] = ((i * 37) % 200) as i16; + Base17 { dims } + }) + .collect(); PaletteSemiring::build(&Palette { entries }) } @@ -53,17 +55,26 @@ fn full_pipeline_ingest_dispatch_persist_read() { for row in 0..32u32 { let words = bs.fingerprints.content_row(row as usize); let popcount: u32 = words.iter().map(|w| w.count_ones()).sum(); - assert_eq!(popcount, 1, "each ingested row should have exactly 1 bit set"); + assert_eq!( + popcount, 1, + "each ingested row should have exactly 1 bit set" + ); } // [3] Write qualia for row 0 (fear-like) and row 1 (steelwind-like). let mut fear_q = [0.0f32; 17]; - fear_q[0] = 0.9; fear_q[1] = -0.8; fear_q[2] = 0.9; - fear_q[3] = 0.1; fear_q[4] = 0.3; fear_q[5] = 0.8; + fear_q[0] = 0.9; + fear_q[1] = -0.8; + fear_q[2] = 0.9; + fear_q[3] = 0.1; + fear_q[4] = 0.3; + fear_q[5] = 0.8; write_qualia_17d(&mut bs, 0, &fear_q); let mut novel_q = [0.0f32; 17]; - novel_q[0] = 0.5; novel_q[1] = 0.5; novel_q[2] = 0.8; + novel_q[0] = 0.5; + novel_q[1] = 0.5; + novel_q[2] = 0.8; novel_q[4] = 0.9; write_qualia_17d(&mut bs, 1, &novel_q); @@ -98,13 +109,25 @@ fn full_pipeline_ingest_dispatch_persist_read() { let crystal = driver.dispatch(&req); // [5] Verify cycle_fingerprint is not all-zero (XOR fold of content rows). - let fp_popcount: u32 = crystal.bus.cycle_fingerprint.iter() - .map(|w| w.count_ones()).sum(); - assert!(fp_popcount > 0, "cycle_fingerprint should have bits from XOR fold"); + let fp_popcount: u32 = crystal + .bus + .cycle_fingerprint + .iter() + .map(|w| w.count_ones()) + .sum(); + assert!( + fp_popcount > 0, + "cycle_fingerprint should have bits from XOR fold" + ); // [6] Verify resonance top_k has hits. - let active_hits = crystal.bus.resonance.top_k.iter() - .filter(|h| h.resonance > 0.0).count(); + let active_hits = crystal + .bus + .resonance + .top_k + .iter() + .filter(|h| h.resonance > 0.0) + .count(); assert!(active_hits > 0, "should have at least one resonance hit"); // [7] Verify style was auto-detected. @@ -119,14 +142,22 @@ fn full_pipeline_ingest_dispatch_persist_read() { let persisted_vsa = persist_bs.fingerprints.cycle_row(0); // Verify it's not all zeros (the bus had bits set via XOR fold). let nonzero_count = persisted_vsa.iter().filter(|&&v| v != 0.0).count(); - assert_eq!(nonzero_count, FLOATS_PER_VSA, "bipolar projection should have no zeros — every dim is ±1"); + assert_eq!( + nonzero_count, FLOATS_PER_VSA, + "bipolar projection should have no zeros — every dim is ±1" + ); // Verify round-trip: threshold back to bits, compare. use lance_graph_contract::crystal::vsa16k_to_binary16k_threshold; let round_tripped = vsa16k_to_binary16k_threshold( - persisted_vsa.try_into().expect("cycle_row len must be 16384") + persisted_vsa + .try_into() + .expect("cycle_row len must be 16384"), + ); + assert_eq!( + &round_tripped[..], + &crystal.bus.cycle_fingerprint[..], + "bipolar projection must be lossless round-trip" ); - assert_eq!(&round_tripped[..], &crystal.bus.cycle_fingerprint[..], - "bipolar projection must be lossless round-trip"); // Verify meta was packed. let meta = persist_bs.meta.get(0); @@ -137,10 +168,21 @@ fn full_pipeline_ingest_dispatch_persist_read() { eprintln!(" Ingested: {} rows", end - start); eprintln!(" Cycle FP popcount: {}", fp_popcount); eprintln!(" Active hits: {}", active_hits); - eprintln!(" Style: {} ({})", style_ord, - cognitive_shader_driver::engine_bridge::unified_style(style_ord).name); - eprintln!(" Gate: {}", if crystal.bus.gate.is_flow() { "Flow" } - else if crystal.bus.gate.is_hold() { "Hold" } else { "Block" }); + eprintln!( + " Style: {} ({})", + style_ord, + cognitive_shader_driver::engine_bridge::unified_style(style_ord).name + ); + eprintln!( + " Gate: {}", + if crystal.bus.gate.is_flow() { + "Flow" + } else if crystal.bus.gate.is_hold() { + "Hold" + } else { + "Block" + } + ); eprintln!(" Fear CD: {:.3}, Novel CD: {:.3}", cd_fear, cd_novel); } @@ -156,6 +198,9 @@ fn style_auto_detect_matches_qualia() { let back = read_qualia_17d(&bs, 0); let detected = auto_style::style_from_qualia(&back); - assert_eq!(detected, auto_style::ANALYTICAL, - "high certainty + low urgency should auto-detect as analytical"); + assert_eq!( + detected, + auto_style::ANALYTICAL, + "high certainty + low urgency should auto-detect as analytical" + ); } diff --git a/crates/lance-graph-contract/src/hhtl.rs b/crates/lance-graph-contract/src/hhtl.rs index 3ffe8349..db353df6 100644 --- a/crates/lance-graph-contract/src/hhtl.rs +++ b/crates/lance-graph-contract/src/hhtl.rs @@ -210,6 +210,79 @@ impl NiblePath { Some(Self { path, depth }) } + /// The first `depth` nibbles of this path as a shorter `NiblePath` — an + /// ancestor-or-equal of `self`. Returns `None` if `depth > self.depth`; + /// `prefix(0)` is [`EMPTY`](NiblePath::EMPTY). + /// + /// Single-shot O(1) alternative to repeated [`parent`](NiblePath::parent) + /// calls. By construction `self.prefix(d).is_ancestor_of(self)` whenever + /// `Some(_)` is returned: this is the coarse-routing-cache view of a + /// deeper class path (the 4-nibble routing prefix vs the full 16-nibble + /// class path, identity-architecture v1 §3). + #[must_use] + pub const fn prefix(self, depth: u8) -> Option { + if depth > self.depth { + return None; + } + if depth == 0 { + return Some(Self::EMPTY); + } + // Right-shift the path so the desired prefix occupies the low 4·depth + // bits. When self.depth == MAX_DEPTH and depth == MAX_DEPTH the shift + // is zero; when depth < self.depth, drop the trailing (self.depth - + // depth) nibbles. `shift < 64` always (depth >= 1 ⇒ shift ≤ 4·15 = 60). + let shift = 4 * (self.depth as u32 - depth as u32); + Some(Self { + path: self.path >> shift, + depth, + }) + } + + /// Lower a [`NodeGuid`](crate::canonical_node::NodeGuid) prefix to a 16-nibble + /// `NiblePath`, the routing-path counterpart of the GUID's + /// `classid · HEEL · HIP · TWIG` cascade (identity-architecture v1 §3). + /// + /// The 20-nibble prefix `classid(8) | HEEL(4) | HIP(4) | TWIG(4)` overflows + /// `MAX_DEPTH = 16`. The deterministic fold drops the **HIGH 4 classid + /// nibbles** (the canon-reserved high `u16` of `classid`) and packs the + /// remaining 16 nibbles root-first as + /// `classid_lo(4) | HEEL(4) | HIP(4) | TWIG(4)`. Returns `None` when the + /// HIGH 4 classid nibbles are nonzero — the fold would be lossy, and the + /// caller must mint within the low `u16` space (CANON: RESERVE, DON'T + /// RECLAIM — high classid bits are documented as reserved-zero). + /// + /// **Bijection invariant.** For any GUID whose `classid >> 16 == 0`, + /// `from_guid_prefix(guid).prefix(d).is_ancestor_of(from_guid_prefix(guid))` + /// holds for every `d in 1..=16` (`prefix(0)` is [`EMPTY`](NiblePath::EMPTY), + /// which by definition is an ancestor of nothing — the "no basin routed" + /// sentinel). The routing-cache view (typically `prefix(4)` over + /// `classid_lo`) is therefore a valid HHTL ancestor of the full class path — + /// the LE contract the `classid → ReadMode` keystone meets at the classid. + #[must_use] + pub const fn from_guid_prefix(guid: &crate::canonical_node::NodeGuid) -> Option { + let parts = guid.decode(); + // High 4 classid nibbles (the canon-reserved high u16) must be zero — + // otherwise the 20→16 nibble fold drops information. The caller mints + // into the low u16 of classid (see CANON / identity-architecture v1 + // §3): a nonzero high u16 is not silently re-routed, it's reported. + if (parts.classid >> 16) != 0 { + return None; + } + // Pack root-first into 16 nibbles = 64 bits = the full u64 path: + // nibbles 0..4 (high) = classid_lo (basin = top nibble of classid_lo) + // nibbles 4..8 = HEEL + // nibbles 8..12 = HIP + // nibbles 12..16 (low) = TWIG (leaf = low nibble of TWIG) + let classid_lo = (parts.classid & 0xFFFF) as u64; + let path = (classid_lo << 48) + | ((parts.heel as u64) << 32) + | ((parts.hip as u64) << 16) + | (parts.twig as u64); + // from_packed handles the high-bit guard; at MAX_DEPTH every u64 is + // valid by construction (4·16 = 64 used bits). + Self::from_packed(path, MAX_DEPTH) + } + /// Is this path a descendant-or-equal of `other`? — the symmetric form of /// [`is_ancestor_of`]. `self.is_descendant_of(other)` is equivalent to /// `other.is_ancestor_of(self)` BUT the form is sometimes more natural at @@ -537,4 +610,169 @@ mod tests { assert_eq!(a.common_ancestor(NiblePath::EMPTY), None); assert_eq!(NiblePath::EMPTY.common_ancestor(a), None); } + + // ── NiblePath::prefix — single-shot ancestor view ───────────────────────── + + #[test] + fn prefix_returns_ancestor_or_equal_at_requested_depth() { + // depth 0 ⇒ EMPTY (the "no basin routed" sentinel — symmetrical with + // parent() of a basin returning None). + let p = NiblePath::root(0x2).child(0x5).child(0xA).child(0x3); + assert_eq!(p.prefix(0), Some(NiblePath::EMPTY)); + assert_eq!(p.prefix(1), Some(NiblePath::root(0x2))); + assert_eq!(p.prefix(2), Some(NiblePath::root(0x2).child(0x5))); + assert_eq!( + p.prefix(3), + Some(NiblePath::root(0x2).child(0x5).child(0xA)) + ); + assert_eq!(p.prefix(4), Some(p), "prefix(self.depth) is reflexive"); + assert_eq!(p.prefix(5), None, "prefix beyond own depth is rejected"); + } + + #[test] + fn prefix_is_always_an_ancestor_of_self() { + // The structural invariant the routing-cache view relies on: every + // returned prefix passes is_ancestor_of(self). Walk the whole depth. + let p = NiblePath::root(0x1) + .child(0x2) + .child(0x3) + .child(0x4) + .child(0x5); + for d in 1..=p.depth() { + let pre = p.prefix(d).unwrap(); + assert!( + pre.is_ancestor_of(p), + "prefix(d={d})={pre:?} must be an ancestor of self={p:?}" + ); + assert_eq!(pre.depth(), d); + } + } + + #[test] + fn prefix_matches_repeated_parent_chain() { + // O(1) prefix(d) must agree with O(depth-d) parent()-loop. + let p = NiblePath::root(0x7) + .child(0x3) + .child(0xA) + .child(0x1) + .child(0xC); + let mut walked = p; + let mut d = p.depth(); + while d > 0 { + assert_eq!(p.prefix(d), Some(walked), "depth {d}"); + d -= 1; + walked = walked.parent().unwrap_or(NiblePath::EMPTY); + } + assert_eq!(p.prefix(0), Some(NiblePath::EMPTY)); + } + + // ── NiblePath::from_guid_prefix — 20→16 nibble fold (identity-arch v1 §3) ── + + #[test] + fn from_guid_prefix_returns_full_max_depth_path() { + use crate::canonical_node::NodeGuid; + // A canonical GUID with classid in the low u16 round-trips to a + // 16-nibble path with the documented root-first layout. + let g = NodeGuid::new(0x0000_ABCD, 0x1234, 0x5678, 0x9ABC, 0x00_0001, 0x00_0002); + let path = NiblePath::from_guid_prefix(&g).expect("classid_lo only ⇒ Some"); + assert_eq!(path.depth(), MAX_DEPTH, "fold occupies the full u64"); + + // Root-first: top nibble of classid_lo is the basin (0xA from 0xABCD). + assert_eq!(path.basin(), Some(0xA)); + // Leaf: low nibble of TWIG (0xC from 0x9ABC). + assert_eq!(path.leaf(), Some(0xC)); + + // Packed value mirrors classid_lo|HEEL|HIP|TWIG, root-first. + let expected: u64 = (0xABCDu64 << 48) | (0x1234u64 << 32) | (0x5678u64 << 16) | 0x9ABCu64; + assert_eq!(path.packed(), (expected, MAX_DEPTH)); + } + + #[test] + fn from_guid_prefix_returns_none_when_high_classid_nibbles_in_use() { + use crate::canonical_node::NodeGuid; + // The 20→16 fold drops the HIGH 4 classid nibbles. When the high u16 + // is nonzero, the fold is lossy — None signals it, callers don't get + // a silent collision. + let g = NodeGuid::new(0xDEAD_BEEF, 0, 0, 0, 0, 0); + assert_eq!( + NiblePath::from_guid_prefix(&g), + None, + "high classid u16 != 0 ⇒ refuse the lossy fold" + ); + let g = NodeGuid::new(0x0001_0000, 0, 0, 0, 0, 0); + assert_eq!( + NiblePath::from_guid_prefix(&g), + None, + "boundary: bit 16 set" + ); + // At exactly the boundary (high u16 == 0) the fold is lossless. + let g = NodeGuid::new(0x0000_FFFF, 0, 0, 0, 0, 0); + assert!(NiblePath::from_guid_prefix(&g).is_some()); + } + + #[test] + fn from_guid_prefix_bootstrap_classid_is_all_zero_path() { + use crate::canonical_node::NodeGuid; + // CANON bootstrap: classid 0 + HHT zero ⇒ a 16-nibble path of all-zero + // nibbles. Basin = 0 (Endurant by ontology convention; agnostic here). + let g = NodeGuid::local(0x00_00CD); + let path = NiblePath::from_guid_prefix(&g).unwrap(); + assert_eq!(path.depth(), MAX_DEPTH); + assert_eq!(path.packed(), (0u64, MAX_DEPTH)); + assert_eq!(path.basin(), Some(0)); + assert_eq!(path.leaf(), Some(0)); + } + + #[test] + fn from_guid_prefix_bijection_classid_lo_heel_hip_twig_only() { + use crate::canonical_node::NodeGuid; + // Two GUIDs that differ ONLY in family/identity (the trailing 6 bytes) + // share the same routing path — the routing prefix is class-scoped, not + // instance-scoped. This is the bijection the operator pinned: identity + // doesn't perturb the class path. + let g1 = NodeGuid::new(0x0000_1234, 0xAAAA, 0xBBBB, 0xCCCC, 0x11_2233, 0x44_5566); + let g2 = NodeGuid::new(0x0000_1234, 0xAAAA, 0xBBBB, 0xCCCC, 0x00_0001, 0x00_0002); + assert_eq!( + NiblePath::from_guid_prefix(&g1), + NiblePath::from_guid_prefix(&g2), + "same (classid_lo, HEEL, HIP, TWIG) ⇒ same routing path" + ); + // Two GUIDs that differ in any of the four tier groups produce + // distinct paths. + let g3 = NodeGuid::new(0x0000_1234, 0xAAAA, 0xBBBB, 0xCCCD, 0, 0); + let g4 = NodeGuid::new(0x0000_1234, 0xAAAA, 0xBBBC, 0xCCCC, 0, 0); + assert_ne!( + NiblePath::from_guid_prefix(&g1), + NiblePath::from_guid_prefix(&g3) + ); + assert_ne!( + NiblePath::from_guid_prefix(&g1), + NiblePath::from_guid_prefix(&g4) + ); + } + + #[test] + fn from_guid_prefix_routing_cache_is_ancestor_of_full_path() { + use crate::canonical_node::NodeGuid; + // The keystone invariant the `classid → ReadMode` LE contract meets: + // a coarser routing prefix (e.g. PREFIX_NIBBLES = 4, the classid_lo + // tier only) MUST be an HHTL ancestor of the full 16-nibble class + // path. Without this, the consumer's read-mode resolution can't + // dispatch by prefix. + let g = NodeGuid::new(0x0000_ABCD, 0x1111, 0x2222, 0x3333, 0, 0); + let full = NiblePath::from_guid_prefix(&g).unwrap(); + // Routing prefix at every depth from 1..=MAX_DEPTH is_ancestor_of full. + for d in 1..=MAX_DEPTH { + let routing = full.prefix(d).unwrap(); + assert!( + routing.is_ancestor_of(full), + "routing prefix d={d} ({routing:?}) must HHTL-reach full ({full:?})" + ); + } + // The 4-nibble routing prefix (identity-architecture v1 PREFIX_NIBBLES) + // is the classid_lo: 0xABCD, basin 0xA. + let routing4 = full.prefix(4).unwrap(); + assert_eq!(routing4.packed(), (0xABCDu64, 4)); + assert_eq!(routing4.basin(), Some(0xA)); + } } diff --git a/crates/lance-graph/src/graph/mod.rs b/crates/lance-graph/src/graph/mod.rs index 762820ad..923258dd 100644 --- a/crates/lance-graph/src/graph/mod.rs +++ b/crates/lance-graph/src/graph/mod.rs @@ -16,6 +16,7 @@ pub mod hydrate; pub mod metadata; pub mod neighborhood; pub mod neuron; +pub mod scheduler; pub mod semiring_map; pub mod sparse; pub mod spo; diff --git a/crates/lance-graph/src/graph/scheduler.rs b/crates/lance-graph/src/graph/scheduler.rs new file mode 100644 index 00000000..d92e9f5c --- /dev/null +++ b/crates/lance-graph/src/graph/scheduler.rs @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +//! # `scheduler` — `LanceVersionScheduler` over `VersionedGraph::versions()`. +//! +//! The **OUT-direction core impl** of the contract's `VersionScheduler` +//! (`lance_graph_contract::scheduler`, D-MBX-9-IN-impl, the CI-gated twin of +//! D-MBX-9-IN): subscribes to a [`VersionedGraph`]'s Lance dataset versions +//! and lowers each tick into the next legal Rubicon +//! [`KanbanMove`](lance_graph_contract::kanban::KanbanMove) for a given +//! `MailboxSoaView`. +//! +//! ## Why it lives here (not in the contract) +//! +//! The contract trait is sync, zero-dep, and substrate-free (it composes only +//! `MailboxSoaView` + `KanbanColumn` + `KanbanMove` + `ExecTarget`). Real Lance +//! I/O is async and brings the `lance` dep; that combination would violate the +//! contract's BBB zero-dep rule. This crate is the right home: it already +//! owns `VersionedGraph` and depends on `lance`. +//! +//! ## What it does +//! +//! `LanceVersionScheduler` wraps an inner [`VersionScheduler`] (typically +//! [`NextPhaseScheduler`], the canonical forward-arc reference) and a +//! [`VersionedGraph`]. The async surface either: +//! +//! - reads the current Lance dataset version once (`drive_once`) and feeds it +//! to the inner scheduler, or +//! - polls `versions()` and folds the latest tick into a single proposed +//! move (`drive_at_latest`). +//! +//! The OUT direction stays **propose, not dispose** (R1 "one SoA never +//! transformed"): the returned [`KanbanMove`] is for the caller to apply via +//! `MailboxSoaOwner::try_advance_phase` — the scheduler never mutates the +//! mailbox. +//! +//! ## Pairing with the IN-direction +//! +//! The contract `scheduler` module ships [`NextPhaseScheduler`] (reference +//! impl) and [`VersionScheduler`] (trait); this crate is the IN-direction +//! impl that closes the bidirectional kanban subscription +//! (`E-SUBSTRATE-IS-THE-SCHEDULER`). Together with `MailboxSoaOwner` (OUT +//! direction, shipped on `MailboxSoA` in `cognitive-shader-driver`), the +//! loop now runs end-to-end in a Lance-backed deployment. + +use lance::dataset::Dataset; +use lance_graph_contract::kanban::{ExecTarget, KanbanMove}; +use lance_graph_contract::scheduler::{DatasetVersion, NextPhaseScheduler, VersionScheduler}; +use lance_graph_contract::soa_view::MailboxSoaView; + +use crate::error::{GraphError, Result}; +use crate::graph::versioned::VersionedGraph; + +/// `LanceVersionScheduler` — wraps a [`VersionedGraph`] and an inner +/// [`VersionScheduler`], lowering each Lance dataset tick into the next +/// proposed Rubicon [`KanbanMove`]. +/// +/// Generic over the inner policy `S` so a deployment can swap +/// [`NextPhaseScheduler`] (the canonical forward-arc reference) for a custom +/// policy (e.g. one that batches ticks, gates by delta, or routes to +/// non-`Native` exec targets). +/// +/// **Lifetimes / costs.** `LanceVersionScheduler` is cheap to clone (it +/// borrows nothing) and reads a Lance dataset once per call. Each `drive_*` +/// call opens the nodes dataset; downstream optimisation would cache a +/// `Dataset` handle, but that's a real-deployment concern, not a contract +/// invariant. +#[derive(Debug, Clone)] +pub struct LanceVersionScheduler { + graph: VersionedGraph, + inner: S, +} + +impl LanceVersionScheduler { + /// Construct a `LanceVersionScheduler` with the canonical forward-arc + /// reference [`NextPhaseScheduler`]: every tick proposes + /// `Planning → CognitiveWork → Evaluation → Commit`, halting at the + /// absorbing column. The `-550 µs` Libet anchor stamps the + /// `Planning → CognitiveWork` crossing — the same convention + /// `MailboxSoa::advance_phase` uses. + pub fn new(graph: VersionedGraph) -> Self { + Self { + graph, + inner: NextPhaseScheduler, + } + } +} + +impl LanceVersionScheduler { + /// Construct with a custom inner [`VersionScheduler`] policy. + pub fn with_policy(graph: VersionedGraph, inner: S) -> Self { + Self { graph, inner } + } + + /// The underlying [`VersionedGraph`] this scheduler reads from. + pub fn graph(&self) -> &VersionedGraph { + &self.graph + } + + /// The inner policy that lowers `(view, version, exec)` to a move. + pub fn policy(&self) -> &S { + &self.inner + } + + /// Read the current Lance dataset version (nodes), wrap it as a + /// [`DatasetVersion`], and feed it to the inner policy on `view`. + /// + /// The "drive one tick" surface — the caller polls this per cycle (or per + /// outside trigger) and applies the returned move via + /// `MailboxSoaOwner::try_advance_phase`. + /// + /// Returns `Ok(None)` when the policy decides no advance is due (e.g. + /// `view.phase().is_absorbing()`); `Ok(Some(move))` otherwise. + /// Propagates any Lance error from opening the dataset (cold path — + /// callers in tests use `tempfile::TempDir` + `commit_encounter_round`). + pub async fn drive_once( + &self, + view: &V, + exec: ExecTarget, + ) -> Result> { + let v = self.current_dataset_version().await?; + Ok(self.inner.on_version(view, v, exec)) + } + + /// Read the **latest** dataset version among `versions()` and lower it + /// via the inner policy. + /// + /// For the default version-agnostic [`NextPhaseScheduler`] this is + /// equivalent to [`drive_once`](Self::drive_once) (that policy ignores the + /// `at` argument — the move is a pure function of `view.phase()`). For a + /// custom version-sensitive `S` the two entry points can legally diverge + /// under concurrent commits between the two reads; such a policy should + /// pick one entry point and stick to it. Separated from `drive_once` to + /// mirror the reactive shape a real `LIVE` subscription takes (the + /// substrate fires `versions()` on every commit; the scheduler folds them + /// into one move). + pub async fn drive_at_latest( + &self, + view: &V, + exec: ExecTarget, + ) -> Result> { + let versions = self.graph.versions().await?; + // `versions()` is ascending-sorted by version number in the pinned + // lance =7.0.0 (`Dataset::versions()` ends with `sort_by_key(|v| + // v.version)`), so `.last()` is the head = latest commit. NB the + // upstream surface carries a `// TODO: support pagination` — if a + // future lance bump paginates `versions()`, prefer the head via + // `current_dataset_version()` (which reads `version().version` + // directly) over `.last()`. An empty list is treated as v=0 (the + // pre-commit sentinel, matching `NextPhaseScheduler`'s expectation + // that any version triggers the forward arc). + let latest = versions.last().map(|v| v.version).unwrap_or(0); + Ok(self.inner.on_version(view, DatasetVersion(latest), exec)) + } + + /// Current Lance dataset version (nodes), wrapped as [`DatasetVersion`]. + /// + /// Equivalent to `VersionedGraph::current_version()` but returns the + /// contract carrier directly — saves a `DatasetVersion(...)` at every + /// call site. + pub async fn current_dataset_version(&self) -> Result { + // Open the nodes dataset once; we don't keep the handle across calls + // because Lance datasets are cheap to reopen (the I/O is metadata + // read). Real deployments cache the handle. + let ds = Dataset::open(self.nodes_path().as_str()) + .await + .map_err(GraphError::from)?; + Ok(DatasetVersion(ds.version().version)) + } + + /// Path of the nodes dataset under `VersionedGraph` — `base_path/nodes.lance`, + /// the same convention `VersionedGraph` uses internally. Reads the base path + /// through the crate-public `VersionedGraph::base_path()` accessor (no + /// `Debug`-scraping — that brittle path was removed per PR #507 review). + fn nodes_path(&self) -> String { + format!("{}/nodes.lance", self.graph.base_path()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::graph::blasgraph::columnar::{EdgeSchema, FingerprintSchema, NodeSchema}; + use arrow_array::builder::FixedSizeBinaryBuilder; + use arrow_array::{FixedSizeBinaryArray, RecordBatch, UInt32Array}; + use lance_graph_contract::collapse_gate::MailboxId; + use lance_graph_contract::kanban::{ExecTarget, KanbanColumn}; + use std::sync::Arc; + use tempfile::TempDir; + + /// Minimal `MailboxSoaView` with a settable phase — same pattern as the + /// contract's `scheduler::tests::FakeView`, scoped to this module so the + /// driving-loop test doesn't depend on a real `MailboxSoA` (which + /// lives in `cognitive-shader-driver`, not a dep of this crate). + struct FakeView { + id: MailboxId, + phase: KanbanColumn, + cycle: u32, + } + impl MailboxSoaView for FakeView { + fn mailbox_id(&self) -> MailboxId { + self.id + } + fn n_rows(&self) -> usize { + 0 + } + fn w_slot(&self) -> u8 { + (self.id & 0x3F) as u8 + } + fn current_cycle(&self) -> u32 { + self.cycle + } + fn phase(&self) -> KanbanColumn { + self.phase + } + fn energy(&self) -> &[f32] { + &[] + } + fn edges_raw(&self) -> &[u64] { + &[] + } + fn meta_raw(&self) -> &[u32] { + &[] + } + fn entity_type(&self) -> &[u16] { + &[] + } + } + + const PLANE_BYTES: i32 = 2048; + const SEAL_BYTES: i32 = 6; + + fn fixed_bin_single(width: i32) -> Arc { + let mut b = FixedSizeBinaryBuilder::with_capacity(1, width); + b.append_value(vec![0u8; width as usize]).unwrap(); + Arc::new(b.finish()) + } + + /// Build a single-row NodeSchema RecordBatch — cheapest valid input for + /// `commit_encounter_round` to advance the Lance nodes-dataset version. + /// Schema is `[node_id u32, plane_s/p/o FixedSizeBinary(2048), + /// seal_s/p/o FixedSizeBinary(6), encounters u32]` (8 cols, exact match + /// for `blasgraph::columnar::NodeSchema`). + fn empty_node_batch() -> RecordBatch { + RecordBatch::try_new( + NodeSchema::arrow_schema_ref(), + vec![ + Arc::new(UInt32Array::from(vec![0u32])), + fixed_bin_single(PLANE_BYTES), + fixed_bin_single(PLANE_BYTES), + fixed_bin_single(PLANE_BYTES), + fixed_bin_single(SEAL_BYTES), + fixed_bin_single(SEAL_BYTES), + fixed_bin_single(SEAL_BYTES), + Arc::new(UInt32Array::from(vec![0u32])), + ], + ) + .unwrap() + } + /// Empty EdgeSchema batch — `[src_id u32, dst_id u32, weight Float16, + /// label FixedSizeBinary(2048)]`. Zero rows is valid for Lance writes; + /// the Float16 column is built empty via the builder API to avoid + /// pulling in the `half` crate transitively just for tests. + fn empty_edge_batch() -> RecordBatch { + let label = FixedSizeBinaryBuilder::with_capacity(0, PLANE_BYTES).finish(); + let weight = arrow_array::builder::Float16Builder::with_capacity(0).finish(); + RecordBatch::try_new( + EdgeSchema::arrow_schema_ref(), + vec![ + Arc::new(UInt32Array::from(Vec::::new())), + Arc::new(UInt32Array::from(Vec::::new())), + Arc::new(weight), + Arc::new(label), + ], + ) + .unwrap() + } + /// Single-row FingerprintSchema batch — `[id u32, fingerprint + /// FixedSizeBinary(2048)]`. + fn empty_fp_batch() -> RecordBatch { + RecordBatch::try_new( + FingerprintSchema::arrow_schema_ref(), + vec![ + Arc::new(UInt32Array::from(vec![0u32])), + fixed_bin_single(PLANE_BYTES), + ], + ) + .unwrap() + } + + #[tokio::test(flavor = "current_thread")] + async fn current_dataset_version_reads_nodes_head() { + let tmp = TempDir::new().unwrap(); + let g = VersionedGraph::local(tmp.path().to_str().unwrap()); + // First commit — creates the dataset; nodes head is version 1 under + // Lance's 1-based versioning (every write ticks). + let v1 = g + .commit_encounter_round(empty_node_batch(), empty_edge_batch(), empty_fp_batch()) + .await + .unwrap(); + let sched = LanceVersionScheduler::new(g.clone()); + let observed = sched.current_dataset_version().await.unwrap(); + assert_eq!(observed.0, v1, "current_dataset_version == latest commit"); + // Second commit advances the version; the scheduler sees the new head. + let v2 = g + .commit_encounter_round(empty_node_batch(), empty_edge_batch(), empty_fp_batch()) + .await + .unwrap(); + assert!(v2 > v1); + assert_eq!(sched.current_dataset_version().await.unwrap().0, v2); + } + + #[tokio::test(flavor = "current_thread")] + async fn drive_once_proposes_forward_arc_on_planning() { + let tmp = TempDir::new().unwrap(); + let g = VersionedGraph::local(tmp.path().to_str().unwrap()); + g.commit_encounter_round(empty_node_batch(), empty_edge_batch(), empty_fp_batch()) + .await + .unwrap(); + let sched = LanceVersionScheduler::new(g); + let view = FakeView { + id: 7, + phase: KanbanColumn::Planning, + cycle: 11, + }; + let mv = sched + .drive_once(&view, ExecTarget::Native) + .await + .unwrap() + .expect("Planning is not absorbing"); + // Forward arc: Planning -> CognitiveWork carries the Libet anchor. + assert_eq!(mv.from, KanbanColumn::Planning); + assert_eq!(mv.to, KanbanColumn::CognitiveWork); + assert_eq!(mv.libet_offset_us, -550_000); + assert_eq!(mv.mailbox, 7); + assert_eq!(mv.witness_chain_position, 11); + assert_eq!(mv.exec, ExecTarget::Native); + } + + #[tokio::test(flavor = "current_thread")] + async fn drive_at_latest_matches_drive_once() { + let tmp = TempDir::new().unwrap(); + let g = VersionedGraph::local(tmp.path().to_str().unwrap()); + // Two commits — exercise the multi-version code path. + g.commit_encounter_round(empty_node_batch(), empty_edge_batch(), empty_fp_batch()) + .await + .unwrap(); + g.commit_encounter_round(empty_node_batch(), empty_edge_batch(), empty_fp_batch()) + .await + .unwrap(); + let sched = LanceVersionScheduler::new(g); + let view = FakeView { + id: 13, + phase: KanbanColumn::CognitiveWork, + cycle: 1, + }; + let a = sched.drive_once(&view, ExecTarget::Native).await.unwrap(); + let b = sched + .drive_at_latest(&view, ExecTarget::Native) + .await + .unwrap(); + // Both lower the latest version to the same move (CognitiveWork -> + // Evaluation, no Libet anchor); proves equivalence under current + // single-head semantics. + assert_eq!(a, b); + let mv = a.unwrap(); + assert_eq!(mv.from, KanbanColumn::CognitiveWork); + assert_eq!(mv.to, KanbanColumn::Evaluation); + assert_eq!(mv.libet_offset_us, 0); + } + + #[tokio::test(flavor = "current_thread")] + async fn absorbing_columns_return_none_even_with_lance_head() { + let tmp = TempDir::new().unwrap(); + let g = VersionedGraph::local(tmp.path().to_str().unwrap()); + g.commit_encounter_round(empty_node_batch(), empty_edge_batch(), empty_fp_batch()) + .await + .unwrap(); + let sched = LanceVersionScheduler::new(g); + // Commit and Prune are absorbing — the scheduler proposes nothing, + // regardless of how many Lance versions are committed downstream. + for absorbing in [KanbanColumn::Commit, KanbanColumn::Prune] { + let view = FakeView { + id: 1, + phase: absorbing, + cycle: 0, + }; + assert!(sched + .drive_once(&view, ExecTarget::Native) + .await + .unwrap() + .is_none()); + assert!(sched + .drive_at_latest(&view, ExecTarget::Native) + .await + .unwrap() + .is_none()); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn exec_target_threads_through_lance_drive() { + let tmp = TempDir::new().unwrap(); + let g = VersionedGraph::local(tmp.path().to_str().unwrap()); + g.commit_encounter_round(empty_node_batch(), empty_edge_batch(), empty_fp_batch()) + .await + .unwrap(); + let sched = LanceVersionScheduler::new(g); + let view = FakeView { + id: 1, + phase: KanbanColumn::Planning, + cycle: 0, + }; + for exec in [ + ExecTarget::Native, + ExecTarget::Jit, + ExecTarget::SurrealQl, + ExecTarget::Elixir, + ] { + let mv = sched.drive_once(&view, exec).await.unwrap().unwrap(); + assert_eq!(mv.exec, exec, "exec target threads through"); + } + } +} diff --git a/crates/lance-graph/src/graph/versioned.rs b/crates/lance-graph/src/graph/versioned.rs index 6b8dc44f..df9150c6 100644 --- a/crates/lance-graph/src/graph/versioned.rs +++ b/crates/lance-graph/src/graph/versioned.rs @@ -139,6 +139,15 @@ impl VersionedGraph { // -- dataset paths ------------------------------------------------------ + /// The base directory or URI this graph is rooted at (local path, `s3://`, + /// `az://`, `gs://`). The per-dataset paths (`nodes.lance`, `edges.lance`, + /// …) are this suffixed by their dataset name. Exposed so downstream + /// crates (e.g. `graph::scheduler::LanceVersionScheduler`) can derive a + /// dataset path without scraping the `Debug` representation. + pub fn base_path(&self) -> &str { + &self.base_path + } + fn nodes_path(&self) -> String { format!("{}/nodes.lance", self.base_path) } diff --git a/crates/surreal_container/Cargo.toml b/crates/surreal_container/Cargo.toml index 21645f51..4c9f78fc 100644 --- a/crates/surreal_container/Cargo.toml +++ b/crates/surreal_container/Cargo.toml @@ -16,32 +16,39 @@ build on. Depends on task 01 (deps_substrate) being resolved before compiling. """ -# ── BLOCKED(C): FULLY RESOLVED 2026-06-14 (coordinates verified against the fork) ── +# ── BLOCKED(C): RESOLVED 2026-06-16 (fork lance/lancedb pins aligned to workspace) ── # -# All coordinates confirmed by reading github.com/AdaWorldAPI/surrealdb via GH_TOKEN+REST -# (the scoped git PROXY denies it, but github.com w/ token reaches any fork — the earlier -# "inaccessible" / "session-auth" framing was wrong): +# Coordinates confirmed against the fork (github.com/AdaWorldAPI/surrealdb): # • git URL = https://github.com/AdaWorldAPI/surrealdb ✅ -# • branch = feat/sdk-forward-kv-lance ✅ (Lance KV backend is -# INTEGRATED here at surrealdb/core/src/kvs/lance/{mod,schema,timeline,tx_buffer, -# background_optimizer,cnf,tests}.rs — not WIP, fully present on this branch) +# • branch = main ✅ (was feat/sdk-forward-kv-lance; +# PRs #34 (ndarray pin) / #35 (SDK kv-lance forwarding) / #36 (Lance backend struct + +# endpoint helper) / #37 (C16c bridge From for catalog::*) ALL +# merged to main; lance/lance-index =7.0.0 + lancedb =0.30.0 since 2026-06-16) # • feature = kv-lance ✅ (surrealdb/core/Cargo.toml:27 # `kv-lance = ["dep:lance","dep:lance-index","dep:lancedb","dep:arrow-array","dep:arrow-schema"]`) # -# The fork ALREADY pulls our stack (its Cargo.lock pins): lance-graph-contract 0.1.0, -# lance 6.0.0, lance-index 6.0.0, lancedb 0.29.0, ndarray 0.16.1 + 0.17.2. So this is NOT -# a from-scratch integration — the deps are wired in the fork. +# Version reconcile DONE: surrealdb fork now pins lance =7.0.0 / lance-index =7.0.0 / +# lancedb =0.30.0 (matches this workspace). PR #34 also pinned ndarray fork exact-rev. +# Unblocks D-PG-6 (Rubicon kanban VIEW), D-MBX-9 OUT (LanceVersionScheduler over +# versions() — shipped in lance-graph::graph::scheduler), identity-architecture Phase H +# (SurrealQL read glove). See TD-SURREALDB-KVLANCE-LANCE7 closure note. # -# REAL REMAINING ITEM = version reconcile (NOT access, branch, or feature): -# fork pins lance 6.0.0 / lancedb 0.29.0 ; this crate pins lance =7.0.0 / lancedb =0.30.0. -# Align the workspace on ONE lance/lancedb line before building (the lance family moves -# in lockstep). Then build-verify (cargo git-fetches the fork; mind the SurrealDB -# cold-build disk cost). Until aligned, the dep stays commented so the manifest resolves. +# OPERATIONAL NOTE: uncommenting the dep below triggers a cold surrealdb build (~hundreds +# of crates, ~10+ min cold). This crate keeps the surrealdb dep commented so default +# `cargo check` stays fast for non-surreal contributors; the kv-lance integrator +# uncomments it in their branch and takes the cold-build cost. The contract-side +# adapter (`view.rs`) ships independently and depends only on lance-graph-contract. [dependencies] -# ── BLOCKED(C) RESOLVED: uncomment once lance/lancedb are aligned across the workspace -# (fork is 6.0.0/0.29.0; this crate is 7.0.0/0.30.0) — see note above ────────── -# surrealdb = { git = "https://github.com/AdaWorldAPI/surrealdb", branch = "feat/sdk-forward-kv-lance", default-features = false, features = ["kv-lance"] } +# ── BLOCKED(C) RESOLVED 2026-06-16: surrealdb fork now aligned at lance=7.0.0 / +# lancedb=0.30.0 (PR #34/#35/#36/#37 merged to main). Uncomment to wire kv-lance +# real I/O — note the cold-build cost (~10+ min for the surrealdb crate graph). +# Until uncommented, the contract-side `view.rs` adapter compiles and ships +# the MailboxSoaView surface with a documented `BlockedColdBuild` stub. +# surrealdb = { git = "https://github.com/AdaWorldAPI/surrealdb", branch = "main", default-features = false, features = ["kv-lance"] } + +# Contract types the view glove (`view.rs`) implements. +lance-graph-contract = { path = "../lance-graph-contract" } lance = "=7.0.0" lancedb = { version = "=0.30.0", optional = true, default-features = false } diff --git a/crates/surreal_container/src/lib.rs b/crates/surreal_container/src/lib.rs index bf72cf2b..3b76c9a8 100644 --- a/crates/surreal_container/src/lib.rs +++ b/crates/surreal_container/src/lib.rs @@ -91,6 +91,20 @@ pub struct SurrealStore { _placeholder: std::marker::PhantomData<()>, } +impl SurrealStore { + /// Test-only constructor for an uninitialised `SurrealStore`. Lets + /// downstream tests exercise stub code paths (like + /// [`view::read_via_kv_lance`]) without going through the + /// `BLOCKED(C)`-gated `open()`. Not part of the public surface; gated + /// by `cfg(test)` to keep the prod constructor singular. + #[cfg(test)] + pub(crate) fn test_placeholder() -> Self { + Self { + _placeholder: std::marker::PhantomData, + } + } +} + impl SurrealStore { /// Open (or create) an embedded `kv-lance` Datastore at `lance_path`. /// @@ -158,6 +172,18 @@ pub enum SurrealContainerError { reason: &'static str, }, + /// The surrealdb fork dep is commented in `Cargo.toml` to keep default + /// builds fast (cold surrealdb build is heavy). The caller pattern-matches + /// this to fall back to a direct lance-graph read, or instructs the user + /// to uncomment the dep and take the cold-build cost. + /// + /// Distinct from [`SurrealContainerError::Blocked`] (which signals an + /// unresolved coordinate / API gap): `BlockedColdBuild` means everything + /// is *known* and *aligned*; only the cold-build flip-on is pending. + BlockedColdBuild { + /// Human-readable pointer to the Cargo.toml note and integration site. + reason: &'static str, + }, // BLOCKED(C): add `Init { source: surrealdb::Error }` once fork dep lands. // BLOCKED(A)/(B): add `Lance { source: lance::Error }` once Lance 6 is pinned. } @@ -166,6 +192,9 @@ impl std::fmt::Display for SurrealContainerError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Blocked { reason } => write!(f, "surreal_container blocked: {reason}"), + Self::BlockedColdBuild { reason } => { + write!(f, "surreal_container blocked (cold-build gate): {reason}") + } } } } @@ -213,3 +242,12 @@ pub mod compaction; /// Clean-writer invariants: append-only, single-writer, no LWW collision (task 12). // TODO task 12 pub mod writer_invariants; + +/// SurrealQL read-glove: `MailboxSoaView` adapter over a kv-lance-backed +/// mailbox row (D-PG-6 contract slice). Pairs with +/// `lance-graph::graph::scheduler::LanceVersionScheduler` to close the +/// IN-direction of the bidirectional kanban subscription +/// (`E-SUBSTRATE-IS-THE-SCHEDULER`). The actual SurrealQL projection + +/// kv-lance scan is the cold-build follow-on; the contract surface is +/// available today. +pub mod view; diff --git a/crates/surreal_container/src/view.rs b/crates/surreal_container/src/view.rs new file mode 100644 index 00000000..33634c0d --- /dev/null +++ b/crates/surreal_container/src/view.rs @@ -0,0 +1,366 @@ +//! # `view` — the SurrealQL read-glove (D-PG-6, `MailboxSoaView` adapter) +//! +//! The contract-compliant **read-only** view that `surreal_container` +//! materialises over a Lance-backed mailbox row. Pairs with the +//! `LanceVersionScheduler` in `lance-graph::graph::scheduler` to close +//! `E-SUBSTRATE-IS-THE-SCHEDULER`'s IN-direction (substrate version tick → +//! next legal `KanbanMove`) for a SurrealQL projection. +//! +//! ## What this is (and isn't) +//! +//! `MailboxSoaView` is **read-only by trait design** (per +//! `soa_view.rs`: "view is read-only" is structural — surreal implements only +//! the read half; the OUT-direction `MailboxSoaOwner` is on the cognitive-side +//! `MailboxSoA`). That's exactly the polyglot-container ruling: +//! +//! > LanceDB leads; SurrealDB is a view/dialect (handover 2026-05-28 §2, +//! > E-RUBICON-RACTOR; kanban.rs:1-21 "surreal=project-read-only, +//! > callcenter=commit"). +//! +//! So this module: +//! +//! - DOES expose `SurrealMailboxView<'a>` — a typed view a SurrealQL +//! projection populates from kv-lance scan data (zero-copy borrows over the +//! underlying byte buffers). +//! - DOES expose `SurrealMailboxView::from_columns(...)` — the constructor +//! the kv-lance read-path calls once it has the row bytes. +//! - DOES expose `read_via_kv_lance()` — the integration-point stub the +//! surrealdb-fork integrator implements once the cold build is taken. +//! - Does NOT implement `MailboxSoaOwner`. That trait lives on the cognitive +//! side (`MailboxSoA` in `cognitive-shader-driver`). Trying to mutate +//! through a `SurrealMailboxView` is a category error the trait surface +//! already forbids. +//! +//! ## How it plugs in +//! +//! ```text +//! Lance dataset SurrealDB (kv-lance backend) consumer +//! ───────────── ──────────────────────────── ──────── +//! versions().tick ─► SurrealQL projection / LIVE ────► SurrealMailboxView<'a> +//! │ +//! ▼ +//! VersionScheduler::on_version +//! │ +//! ▼ +//! Option +//! │ +//! ▼ +//! cognitive owner.try_advance_phase +//! ``` +//! +//! `LanceVersionScheduler` (shipped this PR in `lance-graph`) and this view +//! are the IN-direction pair: they meet at the `MailboxSoaView` trait +//! boundary, with no SurrealQL types crossing the cognitive-side seam. +//! +//! ## Status +//! +//! - `SurrealMailboxView<'a>` and `MailboxSoaView` impl: **shipped here** +//! (contract-only, zero surrealdb dep). +//! - `read_via_kv_lance()`: **stub** — returns +//! [`SurrealContainerError::BlockedColdBuild`] until the surrealdb fork dep +//! in `Cargo.toml` is uncommented and the kv-lance scan code is filled in. +//! This is the integrator's task; the trait surface above stays unchanged. + +use lance_graph_contract::collapse_gate::MailboxId; +use lance_graph_contract::kanban::KanbanColumn; +use lance_graph_contract::soa_view::MailboxSoaView; + +use crate::SurrealContainerError; + +/// A read-only mailbox view materialised from a SurrealQL projection over +/// a Lance-backed kv-lance row. +/// +/// **Zero-copy borrow design.** Per-row column slices are borrowed from the +/// scan's byte buffers (which outlive the view); the view is constructed once +/// per `versions()` tick, fed to `VersionScheduler::on_version`, and +/// dropped. No allocation, no clone — same shape as the existing +/// `MailboxSoaView` impl on `MailboxSoA`. +/// +/// **Borrow lifetime `'a`** is the lifetime of the projection's byte buffers. +/// The surrealdb-fork integrator picks the right backing — likely an arrow +/// `RecordBatch`-borrowed slice — when wiring `read_via_kv_lance`. +#[derive(Debug, Clone, Copy)] +pub struct SurrealMailboxView<'a> { + mailbox_id: MailboxId, + w_slot: u8, + current_cycle: u32, + phase: KanbanColumn, + energy: &'a [f32], + edges_raw: &'a [u64], + meta_raw: &'a [u32], + entity_type: &'a [u16], +} + +impl<'a> SurrealMailboxView<'a> { + /// Construct a view from already-projected column slices. The SurrealQL + /// kv-lance scan calls this once it has the row bytes; the slices borrow + /// from the scan's underlying buffers and the view is dropped before the + /// next tick. + /// + /// **Invariant (enforced):** all column slices MUST have the same length + /// `n_rows() == energy.len() == edges_raw.len() == meta_raw.len() == + /// entity_type.len()`. A `MailboxSoaView` consumer (e.g. + /// `lance-graph`'s `SoaWavePrimer::project`) iterates `0..n_rows()` and + /// indexes every column at `row`, so a ragged projection would index + /// out of bounds. This constructor **asserts** the invariant in ALL build + /// profiles (not just `debug`) — a mismatched-length kv-lance projection is + /// a programming error that must fail loudly, not produce a view that + /// out-of-bounds-indexes downstream in release (PR #507 review, PP-15). + /// The 4 length comparisons are negligible against a kv-lance scan; "zero- + /// copy" means the column DATA is borrowed, not that length checks are + /// skipped. + // + // `#[allow(clippy::too_many_arguments)]`: the arg list IS the + // `MailboxSoaView` column shape (4 scalars + 4 column slices). Packing + // them into a struct would add a copy/layout layer for zero readers — + // every caller is a kv-lance scan that already has the 8 values fanned + // out from its arrow `RecordBatch`. A builder would land an allocation + // in the zero-copy path. Trait shape wins. + #[allow(clippy::too_many_arguments)] + #[inline] + pub fn from_columns( + mailbox_id: MailboxId, + w_slot: u8, + current_cycle: u32, + phase: KanbanColumn, + energy: &'a [f32], + edges_raw: &'a [u64], + meta_raw: &'a [u32], + entity_type: &'a [u16], + ) -> Self { + assert_eq!( + energy.len(), + edges_raw.len(), + "MailboxSoaView column-length invariant: energy vs edges_raw" + ); + assert_eq!( + energy.len(), + meta_raw.len(), + "MailboxSoaView column-length invariant: energy vs meta_raw" + ); + assert_eq!( + energy.len(), + entity_type.len(), + "MailboxSoaView column-length invariant: energy vs entity_type" + ); + Self { + mailbox_id, + w_slot, + current_cycle, + phase, + energy, + edges_raw, + meta_raw, + entity_type, + } + } +} + +impl<'a> MailboxSoaView for SurrealMailboxView<'a> { + #[inline] + fn mailbox_id(&self) -> MailboxId { + self.mailbox_id + } + #[inline] + fn n_rows(&self) -> usize { + self.energy.len() + } + #[inline] + fn w_slot(&self) -> u8 { + self.w_slot + } + #[inline] + fn current_cycle(&self) -> u32 { + self.current_cycle + } + #[inline] + fn phase(&self) -> KanbanColumn { + self.phase + } + #[inline] + fn energy(&self) -> &[f32] { + self.energy + } + #[inline] + fn edges_raw(&self) -> &[u64] { + self.edges_raw + } + #[inline] + fn meta_raw(&self) -> &[u32] { + self.meta_raw + } + #[inline] + fn entity_type(&self) -> &[u16] { + self.entity_type + } +} + +/// Read a mailbox row from the SurrealDB `kv-lance` backend and present it +/// as a [`SurrealMailboxView`]. +/// +/// **Stub.** The surrealdb fork dep in `Cargo.toml` is commented out (cold +/// build cost — see Cargo.toml `[dependencies]` note for the rationale). +/// Once an integrator uncomments it, this body is filled in with: +/// +/// 1. A SurrealQL projection: `SELECT energy, edges_raw, meta_raw, +/// entity_type, current_cycle, phase, w_slot FROM mailbox WHERE id = +/// $mailbox_id` — the polyglot-container plan's D-PG-3 +/// record-range scan. +/// 2. Decode the projection's arrow `RecordBatch` columns into the typed +/// slices. +/// 3. Call [`SurrealMailboxView::from_columns`] with the borrowed slices and +/// return. +/// +/// Until then this returns [`SurrealContainerError::BlockedColdBuild`] — +/// the typed signal a caller can pattern-match on to know whether to fall +/// back to a direct lance-graph read. +#[allow(unused_variables)] +pub async fn read_via_kv_lance( + _store: &crate::SurrealStore, + _mailbox_id: MailboxId, +) -> Result, SurrealContainerError> { + // Reference implementation (paste body below once the surrealdb dep is + // uncommented; the contract surface above stays unchanged): + // + // let ds = store.ds().ok_or(SurrealContainerError::Blocked { .. })?; + // let q = "SELECT * FROM mailbox WHERE mailbox_id = $id;"; + // let result = ds.execute(q, &session, vars).await?; + // let row = result.into_first_row()?; + // Ok(SurrealMailboxView::from_columns( + // row.mailbox_id, row.w_slot, row.current_cycle, row.phase, + // row.energy_slice(), row.edges_slice(), row.meta_slice(), + // row.entity_type_slice(), + // )) + Err(SurrealContainerError::BlockedColdBuild { + reason: "surrealdb fork dep commented (cold-build gate); see Cargo.toml \ + [dependencies] BLOCKED(C) note and view.rs::read_via_kv_lance docs", + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use lance_graph_contract::scheduler::{DatasetVersion, NextPhaseScheduler, VersionScheduler}; + + fn make_view<'a>( + phase: KanbanColumn, + energy: &'a [f32], + edges: &'a [u64], + meta: &'a [u32], + et: &'a [u16], + ) -> SurrealMailboxView<'a> { + SurrealMailboxView::from_columns(7, 5, 13, phase, energy, edges, meta, et) + } + + #[test] + #[should_panic(expected = "column-length invariant")] + fn from_columns_rejects_ragged_projection() { + // A kv-lance projection that hands mismatched-length columns must fail + // loudly at construction (in ALL build profiles) rather than produce a + // view whose n_rows() exceeds a shorter column → downstream OOB index + // (PR #507 review, PP-15). entity_type is one short here. + let energy = [1.0f32, 2.0, 3.0]; + let edges = [10u64, 20, 30]; + let meta = [100u32, 200, 300]; + let et = [1u16, 2]; // ragged: len 2 vs 3 + let _ = SurrealMailboxView::from_columns( + 1, + 0, + 0, + KanbanColumn::Planning, + &energy, + &edges, + &meta, + &et, + ); + } + + #[test] + fn view_columns_borrow_from_caller_buffers() { + // Zero-copy: the slices the view returns are the slices the caller + // passed in — no clone, no allocation. This mirrors the existing + // MailboxSoaView impl on MailboxSoA. + let energy = [1.0f32, 2.0, 3.0]; + let edges = [10u64, 20, 30]; + let meta = [100u32, 200, 300]; + let et = [1u16, 2, 3]; + let v = make_view(KanbanColumn::Planning, &energy, &edges, &meta, &et); + assert_eq!(v.mailbox_id(), 7); + assert_eq!(v.w_slot(), 5); + assert_eq!(v.current_cycle(), 13); + assert_eq!(v.phase(), KanbanColumn::Planning); + assert_eq!(v.n_rows(), 3); + assert_eq!(v.energy(), &energy[..]); + assert_eq!(v.edges_raw(), &edges[..]); + assert_eq!(v.meta_raw(), &meta[..]); + assert_eq!(v.entity_type(), &et[..]); + // Pointer equality — proves the view borrows the original buffers. + assert_eq!(v.energy().as_ptr(), energy.as_ptr()); + assert_eq!(v.edges_raw().as_ptr(), edges.as_ptr()); + } + + #[test] + fn view_is_a_legal_input_to_next_phase_scheduler() { + // The whole point of the read glove: a SurrealMailboxView plugged + // into the contract's `NextPhaseScheduler` lowers a version tick to + // the next legal Rubicon move — same as MailboxSoA, no SurrealQL + // types cross the boundary. + let energy = []; + let edges = []; + let meta = []; + let et = []; + let view = make_view(KanbanColumn::Planning, &energy, &edges, &meta, &et); + let mv = NextPhaseScheduler + .on_version( + &view, + DatasetVersion(42), + lance_graph_contract::kanban::ExecTarget::SurrealQl, + ) + .expect("Planning is not absorbing"); + assert_eq!(mv.from, KanbanColumn::Planning); + assert_eq!(mv.to, KanbanColumn::CognitiveWork); + assert_eq!(mv.libet_offset_us, -550_000); // Libet anchor + assert_eq!( + mv.exec, + lance_graph_contract::kanban::ExecTarget::SurrealQl, + "exec target routed through unchanged" + ); + } + + #[test] + fn view_implements_only_read_half_compile_time() { + // Trait-system proof that the read glove cannot mutate: this + // module imports MailboxSoaView but NOT MailboxSoaOwner. If a + // future drift adds `impl MailboxSoaOwner for SurrealMailboxView`, + // it would have to import the Owner trait — a code-review tripwire + // and the structural enforcement the kanban.rs:1-21 doc states. + fn assert_view(_: &V) {} + let energy = []; + let edges = []; + let meta = []; + let et = []; + let view = make_view(KanbanColumn::Evaluation, &energy, &edges, &meta, &et); + assert_view(&view); // compiles ⇒ View only. + } + + #[tokio::test(flavor = "current_thread")] + async fn read_via_kv_lance_returns_typed_blocked_cold_build() { + // Until the surrealdb fork dep is uncommented, the kv-lance read + // path returns BlockedColdBuild — the typed signal a caller can + // pattern-match to fall back. NOT an unwrap-or-panic stub. + // SurrealStore::open itself is Blocked (pre-existing); compose on + // a placeholder PhantomData store to test read_via_kv_lance directly. + let placeholder = crate::SurrealStore::test_placeholder(); + let result = read_via_kv_lance(&placeholder, 42).await; + assert!( + matches!(result, Err(SurrealContainerError::BlockedColdBuild { .. })), + "stub must return BlockedColdBuild until dep is wired" + ); + // The display surface carries the cold-build hint pointing at Cargo.toml. + let msg = result.unwrap_err().to_string(); + assert!( + msg.contains("cold-build"), + "BlockedColdBuild display must name the gate" + ); + } +}