From 5b944bf24b73db43e8c0858f3fcc93a5b4cbd3ef Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 13:29:57 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(mailbox=5Fsoa):=20W1c=20=E2=80=94=20de?= =?UTF-8?q?clared=20populated-row=20count=20(prefilter-bound=20fix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix the 3-brutal-critic pass flagged (PP-13 P1-3): a zeroed MetaWord passes MetaFilter::accepts (0>=0, thinking_mask==0 accepts all), so a row-bounded sweep clamped to the capacity N (e.g. 1024) returns N−len phantom rows vs a BindSpace window of len. The migration read-shim's meta_prefilter analogue MUST clamp to a logical row count, not n_rows(). - MailboxSoA gains `populated: usize` + `populated()` / `set_populated()` (clamped to N). Semantics mirror BindSpace::len: a DECLARED size set once (BindSpace::zeros(len) fixes len), NOT a per-write high-water-mark and NOT shrunk by reset_row. Defaults to 0 (empty mailbox). - Additive only; nothing deleted; BindSpace untouched. - test_mailbox_soa_populated_is_declared_len_not_capacity: default 0; n_rows()=N distinct from populated(); reset_row does not shrink; set_populated clamps to N. 17 mailbox_soa tests green, clippy clean. Unblocks W2 (the differential harness clamps its mailbox prefilter to populated()). Per the consolidated+critiqued wiring plan (bindspace-mailbox-soa-wiring-v1.md, v2). Co-Authored-By: Claude --- .../src/mailbox_soa.rs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index e68bab27..3755dad0 100644 --- a/crates/cognitive-shader-driver/src/mailbox_soa.rs +++ b/crates/cognitive-shader-driver/src/mailbox_soa.rs @@ -157,6 +157,25 @@ pub struct MailboxSoA { /// (see [`Self::pending_count`]). pub threshold: f32, + /// **Declared populated-row count (W1c).** The logical row count this mailbox is + /// using — the exact analogue of `BindSpace::len`, NOT the const capacity `N`. + /// + /// **Why it exists:** a zeroed `MetaWord` *passes* `MetaFilter::accepts` + /// (`0 >= 0`, `thinking_mask == 0` accepts all). A prefilter sweep clamped to the + /// capacity `N` (e.g. 1024) would therefore return `N − len` phantom rows for a + /// mailbox that only uses `len` rows — diverging from a `BindSpace` window of + /// `len`. Any row-bounded sweep (notably the migration read-shim's + /// `meta_prefilter` analogue) MUST clamp to [`Self::populated`], never to + /// `n_rows()` (= `N`). + /// + /// **Semantics mirror `BindSpace::len`:** a *declared* size, set once at + /// construction/population time via [`Self::set_populated`] (just as + /// `BindSpace::zeros(len)` fixes `len`), NOT a per-write high-water-mark and NOT + /// decremented by [`Self::reset_row`] (clearing a row's contents does not shrink + /// the logical size, exactly as it does not change `BindSpace::len`). Defaults to + /// `0` (an empty mailbox) until declared. + pub(crate) populated: usize, + /// 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`). @@ -210,6 +229,8 @@ impl MailboxSoA { content: vec![0u64; N * WORDS_PER_FP].into_boxed_slice(), topic: vec![0u64; N * WORDS_PER_FP].into_boxed_slice(), angle: vec![0u64; N * WORDS_PER_FP].into_boxed_slice(), + // ── W1c — empty mailbox uses zero rows until a write bumps the mark ── + populated: 0, // Pre-Rubicon: every mailbox starts in deliberation. phase: KanbanColumn::Planning, } @@ -281,6 +302,24 @@ impl MailboxSoA { self.current_cycle = self.current_cycle.wrapping_add(1); } + /// Declared populated-row count (W1c) — the `BindSpace::len` analogue, NOT `N`. + /// Row-bounded sweeps (the migration read-shim's `meta_prefilter`) clamp to this, + /// never to [`Self::n_rows`], so zeroed padding rows `populated..N` are not swept + /// (a zeroed `MetaWord` would otherwise pass `MetaFilter::accepts`). + #[inline] + pub fn populated(&self) -> usize { + self.populated + } + + /// Declare the populated-row count (clamped to the capacity `N`). Set this to the + /// logical size the mailbox represents — e.g. when mirroring a `BindSpace` window + /// of `len` rows, call `set_populated(len)`. Mirrors fixing `BindSpace::len` at + /// construction; it is a declaration, not a per-write counter. + #[inline] + pub fn set_populated(&mut self, n: usize) { + self.populated = n.min(N); + } + /// Reset one row to its zero-initialised state. /// /// Clears `energy`, `plasticity_counter`, and `last_active_cycle` @@ -1095,4 +1134,43 @@ mod tests { "row 3 content must survive row-2 reset" ); } + + // ── test 17: W1c populated() — the prefilter-bound declaration ─────────── + + /// `populated()` is the `BindSpace::len` analogue (declared logical size), NOT + /// the const capacity `N`. It defaults to 0, is set via `set_populated` (clamped + /// to `N`), and is NOT shrunk by `reset_row` — mirroring `BindSpace::len`, which + /// is fixed at construction regardless of row contents. Migration read-shims clamp + /// row-bounded sweeps to this so zeroed padding rows `populated..N` (which a zeroed + /// `MetaWord` would otherwise pass through `MetaFilter::accepts`) are not swept. + #[test] + fn test_mailbox_soa_populated_is_declared_len_not_capacity() { + let mut mb: MailboxSoA<1024> = MailboxSoA::new(1, 0, 1.0); + assert_eq!(mb.populated(), 0, "empty mailbox uses zero rows"); + assert_eq!( + mb.n_rows(), + 1024, + "n_rows() is the capacity N, distinct from populated()" + ); + + mb.set_populated(4); + assert_eq!(mb.populated(), 4, "declared logical size"); + assert!( + mb.populated() < mb.n_rows(), + "len < capacity (the phantom-row gap)" + ); + + // reset_row clears contents but does NOT shrink the declared size. + mb.set_content(2, &[0u64; WORDS_PER_FP]); + mb.reset_row(2); + assert_eq!( + mb.populated(), + 4, + "reset_row must not change populated (mirrors BindSpace::len)" + ); + + // set_populated clamps to the capacity N — never exceeds the backing arrays. + mb.set_populated(9999); + assert_eq!(mb.populated(), 1024, "set_populated clamps to N"); + } } From 018b0cd5bcc1a026509a081590284be7d680ed97 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 13:42:13 +0000 Subject: [PATCH 2/3] fix(#519 codex P1): MailboxSoaView::n_rows() returns populated, not capacity N MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex P1: the contract MailboxSoaView::n_rows() is documented "Number of populated rows in the SoA", but the MailboxSoA impl returned the const capacity N. Generic view consumers (e.g. SoaWavePrimer::project / markov_soa, which bound their row loop with soa.n_rows()) therefore scanned the zeroed padding rows populated..N through the shared trait surface — the exact phantom-row divergence W1c's inherent populated() prevents for the migration read-shim but NOT for the view surface. Fix: MailboxSoaView::n_rows() now returns self.populated (aligning the impl with its own documented contract). populated() (inherent) and n_rows() (trait) now agree; N stays the type-level capacity const. edges_raw()/meta_raw() still return the full backing slice (consumers bound by n_rows(), the documented pattern). W1c test updated to assert n_rows() tracks populated() (0 when empty, 4 after set_populated(4), 1024 after clamp). 17 mailbox_soa tests green incl. the scheduler driving loop; clippy clean. markov_soa is cross-crate generic (never receives a real MailboxSoA) so unaffected. Co-Authored-By: Claude --- .../src/mailbox_soa.rs | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index 3755dad0..da454412 100644 --- a/crates/cognitive-shader-driver/src/mailbox_soa.rs +++ b/crates/cognitive-shader-driver/src/mailbox_soa.rs @@ -557,7 +557,13 @@ impl MailboxSoaView for MailboxSoA { } #[inline] fn n_rows(&self) -> usize { - N + // Contract (`MailboxSoaView::n_rows`): "Number of POPULATED rows" — NOT the + // const capacity `N`. Generic view consumers (e.g. `SoaWavePrimer::project`) + // bound their row loop with `n_rows()`, so returning `N` would make them + // scan the zeroed padding rows `populated..N` (a zeroed `MetaWord` passes + // `MetaFilter::accepts`) — the exact phantom-row divergence W1c prevents. + // Returns the W1c declared logical size; `N` (capacity) is a type-level const. + self.populated } #[inline] fn w_slot(&self) -> u8 { @@ -1140,24 +1146,28 @@ mod tests { /// `populated()` is the `BindSpace::len` analogue (declared logical size), NOT /// the const capacity `N`. It defaults to 0, is set via `set_populated` (clamped /// to `N`), and is NOT shrunk by `reset_row` — mirroring `BindSpace::len`, which - /// is fixed at construction regardless of row contents. Migration read-shims clamp - /// row-bounded sweeps to this so zeroed padding rows `populated..N` (which a zeroed - /// `MetaWord` would otherwise pass through `MetaFilter::accepts`) are not swept. + /// is fixed at construction regardless of row contents. The contract trait method + /// **`MailboxSoaView::n_rows()` reflects `populated()`** (its doc: "Number of + /// populated rows") so generic view consumers (`SoaWavePrimer::project`) that + /// bound `0..n_rows()` do NOT scan the zeroed padding rows `populated..N` (a + /// zeroed `MetaWord` passes `MetaFilter::accepts`). #[test] fn test_mailbox_soa_populated_is_declared_len_not_capacity() { let mut mb: MailboxSoA<1024> = MailboxSoA::new(1, 0, 1.0); assert_eq!(mb.populated(), 0, "empty mailbox uses zero rows"); + // The trait surface mirrors the declared size, NOT the const capacity N=1024. assert_eq!( mb.n_rows(), - 1024, - "n_rows() is the capacity N, distinct from populated()" + 0, + "n_rows() (trait) reflects populated, not capacity N — the phantom-row guard" ); mb.set_populated(4); assert_eq!(mb.populated(), 4, "declared logical size"); - assert!( - mb.populated() < mb.n_rows(), - "len < capacity (the phantom-row gap)" + assert_eq!( + mb.n_rows(), + 4, + "n_rows() (trait) tracks populated() so view sweeps clamp correctly" ); // reset_row clears contents but does NOT shrink the declared size. @@ -1172,5 +1182,6 @@ mod tests { // set_populated clamps to the capacity N — never exceeds the backing arrays. mb.set_populated(9999); assert_eq!(mb.populated(), 1024, "set_populated clamps to N"); + assert_eq!(mb.n_rows(), 1024, "n_rows() tracks the clamped populated"); } } From 258fe02979d2873131cb41b5d81cfab4be96e7c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 13:50:28 +0000 Subject: [PATCH 3/3] mailbox_soa W1c: align stale n_rows/populated docs with impl (CodeRabbit) After the codex P1 fix bound MailboxSoaView::n_rows() to `populated` (no longer the const capacity N), three doc sites still described the OLD semantics. CodeRabbit (PR #519) flagged all three as a doc/code mismatch that would breed future misuse: - field doc (~L168): "MUST clamp to populated, never to n_rows() (= N)" was self-contradictory now that n_rows() == populated(). Reworded: clamp to the logical populated count, not the type-level capacity N; noted n_rows() is now bound to populated so either is safe, only N is wrong. - new() init comment (~L232): "until a write bumps the mark" was false (no write path bumps populated implicitly). Reworded to "until set_populated(...) declares the size". - populated() getter doc (~L305): "clamp to this, never to n_rows()" was stale. Reworded to the logical-size framing and made the manual-declaration (never per-write-counter) requirement explicit. Doc-only; semantics unchanged. 17 mailbox_soa tests green, clippy clean, no broken intra-doc links. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi --- .../src/mailbox_soa.rs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/cognitive-shader-driver/src/mailbox_soa.rs b/crates/cognitive-shader-driver/src/mailbox_soa.rs index da454412..df6fdf3b 100644 --- a/crates/cognitive-shader-driver/src/mailbox_soa.rs +++ b/crates/cognitive-shader-driver/src/mailbox_soa.rs @@ -165,8 +165,10 @@ pub struct MailboxSoA { /// capacity `N` (e.g. 1024) would therefore return `N − len` phantom rows for a /// mailbox that only uses `len` rows — diverging from a `BindSpace` window of /// `len`. Any row-bounded sweep (notably the migration read-shim's - /// `meta_prefilter` analogue) MUST clamp to [`Self::populated`], never to - /// `n_rows()` (= `N`). + /// `meta_prefilter` analogue) MUST clamp to [`Self::populated`] (the logical + /// row count), not the type-level capacity `N`. (Since W1c, `n_rows()` is + /// bound to `populated`, so it is now safe to clamp to either — `N` is the + /// only wrong choice.) /// /// **Semantics mirror `BindSpace::len`:** a *declared* size, set once at /// construction/population time via [`Self::set_populated`] (just as @@ -229,7 +231,8 @@ impl MailboxSoA { content: vec![0u64; N * WORDS_PER_FP].into_boxed_slice(), topic: vec![0u64; N * WORDS_PER_FP].into_boxed_slice(), angle: vec![0u64; N * WORDS_PER_FP].into_boxed_slice(), - // ── W1c — empty mailbox uses zero rows until a write bumps the mark ── + // ── W1c — empty mailbox: zero logical rows until `set_populated(...)` + // declares the size (no write path bumps this implicitly) ── populated: 0, // Pre-Rubicon: every mailbox starts in deliberation. phase: KanbanColumn::Planning, @@ -302,10 +305,14 @@ impl MailboxSoA { self.current_cycle = self.current_cycle.wrapping_add(1); } - /// Declared populated-row count (W1c) — the `BindSpace::len` analogue, NOT `N`. - /// Row-bounded sweeps (the migration read-shim's `meta_prefilter`) clamp to this, - /// never to [`Self::n_rows`], so zeroed padding rows `populated..N` are not swept - /// (a zeroed `MetaWord` would otherwise pass `MetaFilter::accepts`). + /// Declared populated-row count (W1c) — the `BindSpace::len` analogue, NOT the + /// type-level capacity `N`. Row-bounded sweeps (the migration read-shim's + /// `meta_prefilter`) clamp to this logical size, so zeroed padding rows + /// `populated..N` are not swept (a zeroed `MetaWord` would otherwise pass + /// `MetaFilter::accepts`). Since W1c, [`MailboxSoaView::n_rows`] is bound to + /// this field, so the two agree; only the const `N` is the wrong bound. + /// This is a *declaration*, never an implicit per-write counter — callers + /// manage it explicitly via [`Self::set_populated`]. #[inline] pub fn populated(&self) -> usize { self.populated