diff --git a/.claude/board/AGENT_LOG.md b/.claude/board/AGENT_LOG.md index 542d7a27..09e65460 100644 --- a/.claude/board/AGENT_LOG.md +++ b/.claude/board/AGENT_LOG.md @@ -1,3 +1,39 @@ +## [Agent-A4 / Sonnet] D-MBX-A4 — append §10 architectural refinements to bindspace→mailbox plan + +**D-id:** D-MBX-A4 | **Commit:** 0f448730 (cherry-picked from worktree `worktree-agent-a1961cf1d2ca1db93` f5cdcbe8) | **Branch:** claude/lance-surrealdb-analysis-LXmug +**Files touched:** `.claude/plans/bindspace-singleton-to-mailbox-soa-v1.md` (+36 lines, new §10 at end). 36 insertions, 0 deletions — no existing text modified. +**Markers:** 9 `` comments placed (7 refinement bullets + 2 OQ entries); orchestrator removed all 9 in review pass. +**Outcome:** DONE. §10 captures: (1) SoA Lance container ≠ cascade; (2) cascade is NOT index space; (3) 64K-256K envelope; (4) W-slot mailbox-witness table semantics; (5) cascade granularities = CPU/cache boundaries; (6) `simd_soa.rs` introspection framework; (7) SoA invariant spawn→commit. Surviving open questions: OQ-MBX-8 (`persisted_row` vs Lance versioning) + OQ-MBX-15′ (container scoping). + +--- + +## [Agent-A3 / Sonnet] D-MBX-A3 — WitnessTable column-type primitive (W-slot resolver) + +**D-id:** D-MBX-A3 | **Commit:** ef848a34 | **Branch:** claude/lance-surrealdb-analysis-LXmug +**Files touched:** `crates/lance-graph-contract/src/witness_table.rs` (new, +185 lines); `crates/lance-graph-contract/src/lib.rs` (+2 lines, `pub mod witness_table`) +**Tests:** `cargo test -p lance-graph-contract --lib witness_table` → 3/3 passed; `cargo check -p lance-graph-contract` → `Finished dev` 0 errors 0 warnings +**Outcome:** DONE. `WitnessEntry` + `WitnessTable` declared; zero new dependencies; `/// work` markers on all pub items. + +--- + +## [Agent-A1 / Sonnet] D-MBX-A1 — add thoughtspace columns to MailboxSoA + +**D-id:** D-MBX-A1 | **Commit:** 1df12eca | **Branch:** claude/lance-surrealdb-analysis-LXmug +**Files touched:** `crates/cognitive-shader-driver/src/mailbox_soa.rs` (+103 lines) +**cargo check:** `Finished dev` — 0 errors; pre-existing warnings only (causal-edge/p64-bridge/ontology — none in mailbox_soa.rs). `--features hpc-extras` absent from this crate; ran with default features. +**Outcome:** SUCCESS — added 4 SoA fields (edges/qualia/meta/entity_type), 8 getter/setter methods, updated new() + reset_row(). All new items marked `/// work`. + +--- + +## [Agent-A2 / Sonnet] D-MBX-A2 — transitional per-mailbox routing field+builder on ShaderDriver + +**D-id:** D-MBX-A2 | **Commit:** 61b641d5 | **Branch:** claude/lance-surrealdb-analysis-LXmug +**Files touched:** `crates/cognitive-shader-driver/src/driver.rs` (+42 lines) +**cargo check:** `Finished dev` — 0 errors; pre-existing warnings only (causal-edge/p64-bridge/ontology deprecations — none in cognitive-shader-driver). Note: `--features hpc-extras` absent from this crate; check ran with default features. +**Outcome:** SUCCESS — added `HashMap>` field on `ShaderDriver`, `with_mailbox` builder setter on `CognitiveShaderBuilder`, `mailbox()` read accessor. Singleton `Arc` untouched. All new items marked `/// work`. + +--- + ## [Main-thread] D-ODOO-SAV-4 — odoo-savant Reasoner layer (4 impls, one per ReasoningKind) Implemented `crates/lance-graph-callcenter/src/savant_reasoners.rs`: `SavantConclusion { savant_id, query_strategy, confidence: NarsTruth, rationale }` (suggestion-only, **no serde** — the one-binary contract; JSON only at the MedCareV2 FFI boundary) + the 4 `Reasoner` impls per the dispatch decision pinned in PR #419: `CustomerCategoryReasoner` / `PostingAnomalyReasoner` / `NextBestActionReasoner` / `OtherReasoner`, covering all 25 savants in `contract::savants::SAVANTS`. Each resolves the concrete savant from `(kind, namespace)`, selects `QueryStrategy` via `InferenceType::default_strategy()`, and fuses evidence-ref coverage into a NARS `(frequency, confidence)`. diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 87ad4a7d..27ae30bb 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -42,6 +42,8 @@ ## Current Contract Inventory (lance-graph-contract) +> **2026-05-28 — PR-in-flight addition** (bindspace→mailbox migration wave A1-A4): `lance_graph_contract::witness_table::{WitnessEntry, WitnessTable}` — column-type primitive resolving the 6-bit W-slot in `CausalEdge64 v2` into a per-cohort `(mailbox_ref: u32, spo_fact_ref: Option)` table (`mailbox_ref` carries the full canonical `MailboxId`, NOT a truncated cohort-local index — see PR #427 Codex P2 fix). Zero-dep, 3 unit tests, `WitnessTable::{new, get, set, default}`. Cross-ref: `.claude/plans/bindspace-singleton-to-mailbox-soa-v1.md` §10 (architectural refinements landed in same wave). Also in same wave: `cognitive-shader-driver::MailboxSoA` gains four thoughtspace columns (`edges: [CausalEdge64; N]`, `qualia: [QualiaI4_16D; N]`, `meta: [MetaWord; N]`, `entity_type: [u16; N]`) + 8 row accessors; `ShaderDriver` gains transitional `mailboxes: HashMap>` + `with_mailbox()` builder + `mailbox()` read accessor (sibling-shape, additive — singleton untouched). 457 contract+driver tests pass. + Types that EXIST — do NOT re-propose them: **`grammar/`**: `FailureTicket`, `PartialParse`, `CausalAmbiguity`, `TekamoloSlots`, `TekamoloSlot`, `WechselAmbiguity`, `WechselRole`, `FinnishCase`, `finnish_case_for_suffix`, `NarsInference`, `inference_to_style_cluster`, `ContextChain` (with coherence_at / total_coherence / replay_with_alternative / disambiguate / DisambiguationResult / WeightingKernel), `RoleKey` + 47 `LazyLock` instances + `Tense` enum + `finnish_case_key / tense_key / nars_inference_key` lookups, **`RoleKey::bind/unbind/recovery_margin`** (slice-masked XOR), **`Vsa10k`** + `VSA_ZERO` + `vsa_xor` + `vsa_similarity`, **`GrammarStyleConfig`** + **`GrammarStyleAwareness`** + `revise_truth` + `ParseOutcome` + `divergence_from`, **`FreeEnergy`** + **`Hypothesis`** + **`Resolution`** (Commit / Epiphany / FailureTicket) + `from_ranked` + thresholds. diff --git a/.claude/plans/bindspace-singleton-to-mailbox-soa-v1.md b/.claude/plans/bindspace-singleton-to-mailbox-soa-v1.md index fe260c1e..4a1d42ce 100644 --- a/.claude/plans/bindspace-singleton-to-mailbox-soa-v1.md +++ b/.claude/plans/bindspace-singleton-to-mailbox-soa-v1.md @@ -396,3 +396,30 @@ accumulator), `EPIPHANIES.md` (`E-BATON-1`, `E-CE64-MB-4`, `E-LADDER-SERVES-MAIL `I-VSA-IDENTITIES`, `I-LEGACY-API-FEATURE-GATED`), `.claude/surreal/RECONCILIATION_with_canonical_plan.md` (Vsa16kF32-deprecation contradiction flag), code: `crates/cognitive-shader-driver/src/{bindspace.rs, mailbox_soa.rs, driver.rs, engine_bridge.rs, bin/serve.rs}`. + +--- + +## §10 — 2026-05-28 architectural refinements (post-PR-#423 sync) + +The following refinements were ratified after the initial plan was written. They are +append-only findings; no prior section has been modified. + +1. **SoA Lance container ≠ cascade.** The cascade is resolution-laddered superposition over per-axis granularities; the SoA Lance container is the materialized data substrate (same SoA the cognitive-shader-driver handles, same SoA the singleton BindSpace updated). One cascade resolves to one or more SoA Lance containers via top-k emission (Gaussian splat / CAM-PQ top-k). The container is the StreamDTO operand variable; the cascade is the resolver function. + +2. **Cascade is NOT an index space.** L1-L4 (`64²/256²/4096²/16384²`) are per-axis granularities on the same semantic axis (causal / palette / COCA codebook / outcome), superposed and streamed like x265 cascaded prediction levels. No level is fully materialized; only the emission (rendered SoA container) reaches downstream. + +3. **64K-256K mailbox envelope** (~360 MB - 1.4 GB total working set at 6 KB per mailbox). The whole population is RAM-resident on any reasonable server. No hot/warm/cold tier split needed. Tombstone retention is free at this scale; eviction is moot. + +4. **W-slot resolves into a per-cohort witness table** of `(mailbox_ref, spo_fact_ref)` entries — NOT a witness-corpus pointer to a static repository. Active mailboxes carry a mailbox-ref alone (spo_fact_ref = None); once the belief crystallizes via Rubicon commit, the spo_fact_ref binds to the SPO triple. Tombstones stay reachable through their mailbox-ref. The chain of W-references across edges forms a Markov belief-update arc via AriGraph episodic-reference vectors (AriGraph today is a transcode; the chaining engine is the target shape). + +5. **Cascade granularities are CPU/cache boundaries, not abstract resolutions.** 64 = AVX-512 i8 register / cache line; 256 = AMX tile row; 4096 = page (4 KiB); 16384 = L1d cache (16 KiB). The ladder is hardware-natural so SIMD sweeps stay register/cache/page-aligned by construction. + +6. **`simd_soa.rs` (ndarray) is the SoA dispatch framework.** It adapts to any SoA shape by introspecting members; consumers declare their own column tuple via derive/const-generic and get SIMD sweeps for free. The MailboxSoA migration is positional, not structural — the framework already swallows the column shape. + +7. **SoA invariant from spawn → commit.** The same SoA byte layout runs end-to-end: cognitive-shader-driver creates a mailbox + SoA via cascade hot path → traverse cold path with gridlake SIMD ops → commit via one of two egress modes: **external** (REST / sea-orm SQL via tokio, backpressure expected) or **internal** (SurrealDB → LanceDB or RocksDB, no backpressure). No marshalling at any boundary; Lance columnar IS the repr. + +### Open Questions surviving these refinements + +- **OQ-MBX-8** — `persisted_row` stub vs Lance native versioning (load-bearing; evidence at `REFACTOR_NOTES.md:129` + `driver.rs:458`). + +- **OQ-MBX-15′** — container scoping: per-cognitive-cycle, per-shader-dispatch, or per-mailbox-cohort? diff --git a/crates/cognitive-shader-driver/src/driver.rs b/crates/cognitive-shader-driver/src/driver.rs index 6496092a..8a53b4a7 100644 --- a/crates/cognitive-shader-driver/src/driver.rs +++ b/crates/cognitive-shader-driver/src/driver.rs @@ -45,6 +45,8 @@ use p64_bridge::cognitive_shader::CognitiveShader; use crate::auto_style; use crate::bindspace::{BindSpace, WORDS_PER_FP}; +use crate::mailbox_soa::MailboxSoA; +use lance_graph_contract::collapse_gate::MailboxId; // ═══════════════════════════════════════════════════════════════════════════ // ShaderDriver — holds everything the shader needs to drive @@ -72,6 +74,14 @@ pub struct ShaderDriver { /// Lives in `causal-edge` (zero-dep), so attaching it does NOT pull /// the planner into shader-driver. pub(crate) nars_tables: Option>, + /// Transitional per-mailbox routing surface (slice A2). + /// + /// Consumers can opt into per-mailbox routing by inserting + /// `MailboxSoA<1024>` instances here via the builder's + /// `with_mailbox` method. The singleton `Arc` (above) + /// is unchanged — this field is purely additive and does not alter + /// any existing dispatch semantics. Removed at cutover (plan S3). + pub(crate) mailboxes: std::collections::HashMap>, } impl ShaderDriver { @@ -92,6 +102,7 @@ impl ShaderDriver { default_style, awareness: RwLock::new(awareness), nars_tables: None, + mailboxes: std::collections::HashMap::new(), } } @@ -111,6 +122,17 @@ impl ShaderDriver { self.nars_tables.as_ref() } + /// Return a read reference to the `MailboxSoA<1024>` registered under + /// `id`, or `None` if no mailbox with that id has been inserted via + /// the builder's `with_mailbox` method. + /// + /// The singleton `Arc` is unchanged by this accessor. + /// This is the transitional per-mailbox routing read surface (slice A2). + #[inline] + pub fn mailbox(&self, id: MailboxId) -> Option<&MailboxSoA<1024>> { + self.mailboxes.get(&id) + } + /// Borrow the underlying BindSpace (read-only). #[inline] pub fn bindspace(&self) -> &BindSpace { &self.bindspace } @@ -596,6 +618,9 @@ pub struct CognitiveShaderBuilder { planes: Option<[[u64; 64]; 8]>, default_style: u8, nars_tables: Option>, + /// Transitional per-mailbox routing map populated by `with_mailbox`. + /// Forwarded into `ShaderDriver::mailboxes` at `build()` time. + mailboxes: std::collections::HashMap>, } impl CognitiveShaderBuilder { @@ -606,6 +631,7 @@ impl CognitiveShaderBuilder { planes: None, default_style: auto_style::DELIBERATE, nars_tables: None, + mailboxes: std::collections::HashMap::new(), } } @@ -635,6 +661,15 @@ impl CognitiveShaderBuilder { self } + /// Register a `MailboxSoA<1024>` for transitional per-mailbox routing + /// (slice A2). The mailbox is keyed by `id`; a second call with the + /// same `id` replaces the previous entry. Multiple mailboxes are + /// supported. The singleton `Arc` is not affected. + pub fn with_mailbox(mut self, id: MailboxId, soa: MailboxSoA<1024>) -> Self { + self.mailboxes.insert(id, soa); + self + } + pub fn build(self) -> ShaderDriver { let awareness = (0..12) .map(|ord| GrammarStyleAwareness::bootstrap(ord_to_thinking_style(ord))) @@ -646,6 +681,7 @@ impl CognitiveShaderBuilder { default_style: self.default_style, awareness: RwLock::new(awareness), nars_tables: self.nars_tables, + mailboxes: self.mailboxes, } } } diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index a14a182f..4028c1c1 100644 --- a/crates/cognitive-shader-driver/src/mailbox_soa.rs +++ b/crates/cognitive-shader-driver/src/mailbox_soa.rs @@ -20,7 +20,9 @@ //! gated steps: `.claude/plans/bindspace-singleton-to-mailbox-soa-v1.md`. use causal_edge::CausalEdge64; +use lance_graph_contract::cognitive_shader::MetaWord; use lance_graph_contract::collapse_gate::{CollapseGateEmission, MailboxId, MergeMode}; +use lance_graph_contract::qualia::QualiaI4_16D; /// Spatial-temporal accumulator for per-row baton receipts. /// @@ -57,6 +59,29 @@ pub struct MailboxSoA { /// if `last_emission_cycle[row] == current_cycle`, emission is suppressed. pub last_emission_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. + pub edges: [CausalEdge64; N], + + /// Per-row affective role vector (`QualiaI4_16D`, 8 B/row). + /// Migrated from `BindSpace.qualia` (QualiaI4Column). + /// 16 signed i4 dimensions (arousal/valence/tension/…); 9× compression vs f32. + pub qualia: [QualiaI4_16D; N], + + /// Per-row packed meta word (`MetaWord`, 4 B/row). + /// Migrated from `BindSpace.meta` (MetaColumn). + /// Layout: `thinking(6) + awareness(4) + nars_f(8) + nars_c(8) + free_e(6)`. + pub meta: [MetaWord; N], + + /// Per-row OGIT entity-type index (`u16`, 2 B/row). + /// Migrated from `BindSpace.entity_type`. + /// 1-based index into the shared (immutable) ontology registry. + /// The registry itself stays `Arc` (cold Zone-2, not owned here). + pub entity_type: [u16; N], + /// Monotonic cycle stamp; advanced by `tick()`. pub current_cycle: u32, @@ -97,6 +122,11 @@ impl MailboxSoA { current_cycle: 0, w_slot, threshold, + // ── NEW thoughtspace columns — zero-initialised (D-MBX-A1) ── + edges: [CausalEdge64::ZERO; N], + qualia: [QualiaI4_16D::ZERO; N], + meta: [MetaWord(0); N], + entity_type: [0u16; N], } } @@ -189,6 +219,11 @@ impl MailboxSoA { // Restore the "never emitted" sentinel so the row can emit immediately // on the next cycle without triggering the same-cycle guard. self.last_emission_cycle[row] = u32::MAX; + // ── NEW thoughtspace columns reset (D-MBX-A1) ── + self.edges[row] = CausalEdge64::ZERO; + self.qualia[row] = QualiaI4_16D::ZERO; + self.meta[row] = MetaWord(0); + self.entity_type[row] = 0; } // ── Read-only inspectors ────────────────────────────────────────────────── @@ -227,6 +262,62 @@ impl MailboxSoA { .filter(|&&e| e.abs() >= self.threshold) .count() } + + // ── Thoughtspace column accessors (D-MBX-A1) ───────────────────────────── + + /// Return the `CausalEdge64` baton edge for `row`. + /// + /// Panics (debug) / wraps (release) on out-of-bounds; callers + /// should stay within `[0, N)`. + #[inline] + pub fn edge(&self, row: usize) -> CausalEdge64 { + self.edges[row] + } + + /// Set the `CausalEdge64` baton edge for `row`. + /// + /// Panics (debug) / wraps (release) on out-of-bounds; callers + /// should stay within `[0, N)`. + #[inline] + pub fn set_edge(&mut self, row: usize, e: CausalEdge64) { + self.edges[row] = e; + } + + /// Return the packed `QualiaI4_16D` affective vector for `row`. + #[inline] + pub fn qualia_at(&self, row: usize) -> QualiaI4_16D { + self.qualia[row] + } + + /// Set the packed `QualiaI4_16D` affective vector for `row`. + #[inline] + pub fn set_qualia(&mut self, row: usize, q: QualiaI4_16D) { + self.qualia[row] = q; + } + + /// Return the packed `MetaWord` for `row`. + #[inline] + pub fn meta_at(&self, row: usize) -> MetaWord { + self.meta[row] + } + + /// Set the packed `MetaWord` for `row`. + #[inline] + pub fn set_meta(&mut self, row: usize, m: MetaWord) { + self.meta[row] = m; + } + + /// Return the OGIT entity-type index for `row` (1-based, shared ontology). + #[inline] + pub fn entity_type_at(&self, row: usize) -> u16 { + self.entity_type[row] + } + + /// Set the OGIT entity-type index for `row`. + #[inline] + pub fn set_entity_type(&mut self, row: usize, t: u16) { + self.entity_type[row] = t; + } } #[cfg(test)] diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 6c88b7fe..9ea3381a 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -84,6 +84,7 @@ pub mod splat; pub mod tax; pub mod thinking; pub mod vsa; +pub mod witness_table; pub mod world_map; pub mod world_model; diff --git a/crates/lance-graph-contract/src/witness_table.rs b/crates/lance-graph-contract/src/witness_table.rs new file mode 100644 index 00000000..d3594ae9 --- /dev/null +++ b/crates/lance-graph-contract/src/witness_table.rs @@ -0,0 +1,209 @@ +//! # `WitnessTable` — column-type primitive resolving the 6-bit W slot in `CausalEdge64 v2`. +//! +//! ## Architectural context +//! +//! `CausalEdge64 v2` (plan §6 / bits 53–58) reserves a **6-bit W-slot index** (0..=63) +//! that points into a *per-cohort* `WitnessTable<64>`. Each table entry is a +//! `(mailbox_ref, spo_fact_ref)` tuple: +//! +//! - `mailbox_ref`: full canonical [`contract::collapse_gate::MailboxId`] (`u32`) of +//! the mailbox that witnessed the belief — either currently active or carrying a +//! tombstone flag. The W-slot is the per-cohort *index*; `mailbox_ref` is the +//! *identity* at that slot, preserved at full canonical width across the entire +//! workspace mailbox envelope (64K-256K, plan §10 refinement (3)). +//! - `spo_fact_ref`: optional handle into the AriGraph SPO-G quad store. `None` while +//! the belief is still accumulating in the mailbox's energy column; `Some(u64)` once +//! the belief crystallises and the triple is committed to graph. +//! +//! The chain of W-references across edges forms a **Markov-style belief-update arc** +//! through episodic-reference vectors: each edge's W-slot resolves to the entry that +//! witnessed the prior state, so the arc can be walked backwards (most-recent → oldest +//! witness) without dereferencing the full SPO store on every hop. +//! +//! ## Cohort scoping +//! +//! The table is *per-cohort*, not global. A cohort is a bounded set of collaborating +//! mailboxes (e.g. one rotating sea-star topology partition). `WitnessTable` takes +//! the cohort capacity as a const-generic; the canonical width is `N = 64` (matching +//! the 6-bit W-slot address space). Smaller `N` is legal for test harnesses; larger `N` +//! is UB in the W-slot protocol (indices ≥ 64 would exceed the field width). +//! +//! ## Plan cross-reference +//! +//! `.claude/plans/bindspace-singleton-to-mailbox-soa-v1.md` — §6 "W slot" defines the +//! bit layout, cohort scope, and the Markov arc traversal contract that this type +//! satisfies. Read that plan before wiring `WitnessTable` into emission paths (a later +//! slice). +//! +//! ## Slice scope (A3) +//! +//! This file declares the *column-type primitive only*. It does **not** wire the table +//! into `CausalEdge64`, `MailboxSoA`, or any emission path — those are later slices. + +// ── Type declarations ──────────────────────────────────────────────────────── + +/// A single entry in a per-cohort [`WitnessTable`]. +/// +/// Carries the pair `(mailbox_ref, spo_fact_ref)` that resolves one W-slot index: +/// +/// - `mailbox_ref` is the **full canonical** [`contract::collapse_gate::MailboxId`] +/// (`u32`). The W-slot is the per-cohort *index* (0..=63); `mailbox_ref` is the +/// globally-unique identity of the mailbox at that slot, so the belief arc can +/// resolve to the correct originating mailbox even when cohort membership rotates +/// across the workspace's 64K-256K mailbox envelope (see plan §10 refinement (3)). +/// - `spo_fact_ref` is `None` while the belief is ephemeral (energy accumulating) and +/// `Some(u64)` once the triple is committed to AriGraph (the "crystallisation" event). +/// +/// # Size +/// +/// `u32` (4 B) + `Option` (16 B: 1-byte tag + 7 bytes alignment padding + +/// 8-byte payload — `u64` has no niche, so the discriminant cannot be folded into +/// the payload). With `#[repr(Rust)]` field reordering and 8-byte struct alignment +/// the total is **24 B** per entry; an `N=64` table is therefore 1.5 KiB. `Copy` is +/// intentional: the struct is small enough to pass by value on any target ABI. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)] +pub struct WitnessEntry { + /// Handle to the mailbox that witnessed this belief event. + /// + /// Stores the full [`contract::collapse_gate::MailboxId`] (`u32`). Active + /// mailboxes have a live `w_slot` association; tombstoned mailboxes retain their + /// ref so the arc walk can detect decommissioned cohort members. + pub mailbox_ref: u32, + + /// Optional reference into the AriGraph SPO-G quad store. + /// + /// `None`: the belief has not yet crystallised to a committed triple. + /// `Some(fact_id)`: the triple is committed; `fact_id` is the opaque u64 handle + /// used by the quad store's lookup surface. + pub spo_fact_ref: Option, +} + +/// Per-cohort witness table: a fixed-size array of [`WitnessEntry`] values indexed +/// by the 6-bit W-slot field from `CausalEdge64 v2`. +/// +/// The const parameter `N` is the cohort capacity. The canonical value is `N = 64` +/// (matching the 6-bit address space of the W-slot field). The default type alias +/// `WitnessTable` (no explicit `N`) uses `N = 64`. +/// +/// # Invariant +/// +/// The caller is responsible for ensuring that W-slot indices passed to [`get`] and +/// [`set`] are in `0..N`. Indices ≥ N return `None`/`Err` respectively — no panic. +/// +/// [`get`]: WitnessTable::get +/// [`set`]: WitnessTable::set +#[derive(Debug, Clone)] +pub struct WitnessTable { + /// Flat array of witness entries, one per addressable W-slot index. + pub entries: [WitnessEntry; N], +} + +// ── impl WitnessTable ──────────────────────────────────────────────────────── + +impl WitnessTable { + /// Construct a `WitnessTable` with every entry set to its zero-initialised default. + /// + /// `mailbox_ref` is 0 (the null mailbox handle) and `spo_fact_ref` is `None` + /// (belief not yet crystallised) for every slot. This is the correct starting + /// state for a freshly allocated cohort. + /// + /// `const fn` so tables can be embedded in `static` initialisers. + pub const fn new() -> Self { + Self { + entries: [WitnessEntry { + mailbox_ref: 0, + spo_fact_ref: None, + }; N], + } + } + + /// Look up the entry at `w_slot`. + /// + /// Returns `None` if `w_slot as usize >= N` (out-of-bounds for this cohort). + /// Returns `Some(&WitnessEntry)` otherwise. + pub fn get(&self, w_slot: u8) -> Option<&WitnessEntry> { + self.entries.get(w_slot as usize) + } + + /// Write `e` into slot `w_slot`. + /// + /// Returns `Ok(())` on success. + /// Returns `Err("w_slot out of range for this WitnessTable")` if + /// `w_slot as usize >= N` — no panic, caller decides how to handle overflow. + pub fn set(&mut self, w_slot: u8, e: WitnessEntry) -> Result<(), &'static str> { + match self.entries.get_mut(w_slot as usize) { + Some(slot) => { + *slot = e; + Ok(()) + } + None => Err("w_slot out of range for this WitnessTable"), + } + } +} + +// ── Default ────────────────────────────────────────────────────────────────── + +impl Default for WitnessTable { + fn default() -> Self { + Self::new() + } +} + +// ── Tests ──────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + /// Round-trip: set a slot, then get it back and confirm the value matches. + #[test] + fn witness_table_round_trip_set_get() { + let mut table: WitnessTable<64> = WitnessTable::new(); + let entry = WitnessEntry { + mailbox_ref: 42, + spo_fact_ref: Some(0xDEAD_BEEF_0000_0001), + }; + table.set(7, entry).expect("slot 7 is in range"); + let got = table.get(7).expect("slot 7 must be present"); + assert_eq!(*got, entry, "get must return the exact entry written by set"); + } + + /// Out-of-bounds set returns Err; out-of-bounds get returns None. + #[test] + fn witness_table_out_of_bounds_returns_err() { + let mut table: WitnessTable<4> = WitnessTable::new(); + // slot 4 is out of bounds for N=4 (valid range: 0..=3) + let result = table.set(4, WitnessEntry::default()); + assert!( + result.is_err(), + "set with w_slot >= N must return Err, got Ok" + ); + assert_eq!(table.get(4), None, "get with w_slot >= N must return None"); + // Confirm the in-range slots are untouched + for i in 0u8..4 { + assert_eq!( + table.get(i), + Some(&WitnessEntry::default()), + "slot {i} must still be default after out-of-bounds write" + ); + } + } + + /// A freshly constructed table has all entries at their zero default: + /// `mailbox_ref = 0`, `spo_fact_ref = None`. + #[test] + fn witness_table_default_is_all_zero() { + let table: WitnessTable<64> = WitnessTable::default(); + for i in 0u8..64 { + let entry = table.get(i).expect("all 64 slots must be present"); + assert_eq!( + entry.mailbox_ref, 0, + "slot {i}: mailbox_ref must be 0 on default" + ); + assert_eq!( + entry.spo_fact_ref, None, + "slot {i}: spo_fact_ref must be None on default" + ); + } + } +}