Skip to content

Commit 6b2114e

Browse files
committed
symbiont: D2 kanban loop (pure-SoA slice) — version-tick -> scheduler -> advance
The kanban loop shape over real SoA NodeRows, reusing the COMPLETE contract surface (zero new types): SymbiontBoard impls MailboxSoaView + MailboxSoaOwner over the Vec<NodeRow>; a u32 version_tick stands in for the Lance subscription; the IN-direction loop runs verbatim from the contract scheduler: tick -> NextPhaseScheduler::on_version(view) -> KanbanMove -> try_advance_phase with the BF16 Domino sweep as the CognitiveWork phase. Drove Planning->CognitiveWork[sweep]->Evaluation->Commit, Libet anchor on the Sigma-crossing, halted absorbing in 3 cycles, NaN-clean. - domino.rs: expose seed_boards / domino_sweep / energy_of for the loop. - cargo test -p symbiont 7/7 (2 new kanban_loop tests); cargo run prints the D2 line. Incremental native build (contract untouched). Deferred (named, shipped types to swap in): live LanceVersionScheduler / LanceVersionWatcher subscription, the ractor Actor wrapper (off StubConsumerActor), SurrealQL read_via_kv_lance. Board: STATUS_BOARD D2 = Shipped (slice); AGENT_LOG. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi
1 parent dcb409c commit 6b2114e

5 files changed

Lines changed: 255 additions & 2 deletions

File tree

.claude/board/AGENT_LOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 2026-06-20 (cont.⁴) — D2 kanban loop (pure-SoA slice) green
2+
3+
**Main thread (Opus), autoattended.** Scoped via a read-only explorer (the contract `kanban`/`soa_view`/`scheduler` surface is COMPLETE), then **read the actual files** (kanban.rs/soa_view.rs/scheduler.rs — after a self-caught scent-skim the operator flagged: I'd `grep`/`sed`'d instead of reading, the exact E-SCENT-IS-NOT-READING anti-pattern; corrected by reading in full).
4+
5+
`crates/symbiont/src/kanban_loop.rs` (+ `domino::{seed_boards, domino_sweep, energy_of}` exposed; `main.rs` wires it). `SymbiontBoard` impls `MailboxSoaView` + `MailboxSoaOwner` over the existing `Vec<NodeRow>`; a `u32` `version_tick` stands in for the Lance subscription. The IN-direction loop runs verbatim from the contract: `tick → NextPhaseScheduler::on_version(view) → KanbanMove → try_advance_phase`, with `domino_sweep` (BF16 AMX-or-fallback) as the `CognitiveWork` phase. `cargo test -p symbiont` **7/7** (2 new: forward-arc-to-Commit incl. the −550 000 µs Libet anchor + monotonic cycle stamps; illegal-skip rejected no-mutation). `cargo run`: `mailbox 7 (64 boards) drove [CognitiveWork, Evaluation, Commit] … halted absorbing in 3 cycles; max Energy = 40.7930`. Incremental native build (contract untouched, 17s). **Zero new types** — reuses the complete contract kanban surface. STATUS_BOARD D2 = Shipped (slice). **Deferred (named):** live `LanceVersionScheduler`/`LanceVersionWatcher`, the ractor `Actor` wrapper, SurrealQL `read_via_kv_lance`.
6+
7+
---
8+
19
## 2026-06-20 (cont.³) — NaN-detection projection surface (BindSpace demoted) + BF16/AMX/Domino pivot
210

311
**Main thread (Opus), autoattended.** Operator: "kill the singleton BindSpace and use it only as a projection surface to have an NaN-detection projection surface"; then "use BF16 and add_mul where possible and use amx ... the 2bit×2bit 4×4 Morton tile AMX is our magic bullet to ... burn through some simple Domino thinking style ... very basic POC just to prove the SoA Orchestration."

.claude/board/STATUS_BOARD.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The golden image (`crates/symbiont`, workspace-`exclude`d): the full Ada stack i
88
| D1 | Grid→NodeRow bridge — each bus = 1 SoA board, f64 → `Energy` tenant | symbiont/bridge.rs | **Shipped** | 2 probes green; 64 buses→64 NodeRows, perturbation in the Energy(f32) tenant, all finite |
99
| E2 | Parallel SoA sweep at scale (16k boards = 8 MiB, zero-copy) | symbiont/bridge.rs | **Shipped** | `run_scale_demo(16384)` → 8 MiB, all 16384 Energy tenants finite |
1010
| D3-AMX | Domino POC — 16-board AMX 16×16 BF16 Morton-tile cascade + NaN-projection | symbiont/domino.rs | **Shipped** | 3/3 tests green; 256 boards × 16 AMX-16×16 batches × 3-stage BF16 Morton-tile Domino cascade, NaN-clean via the projection surface. Polyfill-only (`ndarray::simd::bf16_tile_gemm_16x16` re-export `05bfea7a` jirak; `f32_to_bf16_batch_rne`; only `morton4` consumer-side). **Ran AVX-512 fallback** — AMX genuinely OFF on this guest (functional probe `/tmp/amxcheck`: XCR0 tile bits 17/18 = 0, `arch_prctl(158)` XTILEDATA = **-95 -EOPNOTSUPP** kernel refuses; CPUID also masked). NOT merely CPUID-masked → cannot be enabled here; a forced byte-encoded TDPBF16PS would fault. AMX dispatch correct + arch_prctl-158 gotcha-safe; fires `[AMX TDPBF16PS]` on an AMX-granted guest. |
11-
| D2 | Kanban loop (`LanceVersionScheduler``KanbanMove`→SoA write→Lance commit) | symbiont + lance-graph-supervisor | Queued | |
11+
| D2 | Kanban loop — pure-SoA slice (version-tick → `NextPhaseScheduler``try_advance_phase`) | symbiont/kanban_loop.rs | **Shipped (slice)** | 2/2 tests green; `SymbiontBoard` impls `MailboxSoaView`+`MailboxSoaOwner` over the `Vec<NodeRow>`, a `u32` tick stands in for the Lance subscription; drove `Planning→CognitiveWork[BF16 Domino sweep]→Evaluation→Commit`, Libet anchor on the Σ-crossing, halted absorbing in 3 cycles, NaN-clean. Reuses the COMPLETE contract kanban surface (`KanbanColumn`/`KanbanMove`/`NextPhaseScheduler`/`MailboxSoa{View,Owner}`) — zero new types. **Deferred (named):** live `LanceVersionScheduler`/`LanceVersionWatcher` subscription, the ractor `Actor` wrapper (pattern off `StubConsumerActor`), SurrealQL `read_via_kv_lance`. |
1212
| E1 | Spain-grid acceptance gate (real fixture, NaN-free, clippy+machete clean) | symbiont | Queued | the north star — first N *real* nodes on the SoA in parallel |
1313
| BT | Battle-test plan (probes A1–E3, gated behind singleton-BindSpace→SoA) | workspace | **Shipped (doc)** | `crates/symbiont/BATTLE_TEST_PLAN.md`; A1 partial-green + D1 green; A2–E3 specced |
1414

crates/symbiont/src/domino.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ fn set_energy(row: &mut NodeRow, e: f32) {
7878
let off = en_off();
7979
row.value[off..off + 4].copy_from_slice(&e.to_le_bytes());
8080
}
81-
fn energy_of(row: &NodeRow) -> f32 {
81+
pub fn energy_of(row: &NodeRow) -> f32 {
8282
let off = en_off();
8383
f32::from_le_bytes(row.value[off..off + 4].try_into().expect("4 bytes"))
8484
}
@@ -181,6 +181,24 @@ pub fn run_poc(n_boards: usize, stages: usize) {
181181
println!(" amx gate: {}", amx_report());
182182
}
183183

184+
/// Seed `n` boards (each a Morton-addressed 4×4 BF16 tile). Public so the kanban
185+
/// loop (`kanban_loop::SymbiontBoard`) can spawn a mailbox over them.
186+
pub fn seed_boards(n: usize) -> Vec<NodeRow> {
187+
(0..n).map(seed_board).collect()
188+
}
189+
190+
/// Run a `stages`-deep Domino sweep over an existing board-set, in full 16-board
191+
/// AMX batches — the `CognitiveWork` phase of the kanban loop. A trailing partial
192+
/// batch (`n` not a multiple of 16) is left untouched.
193+
pub fn domino_sweep(rows: &mut [NodeRow], stages: usize) {
194+
let w = weight();
195+
for batch in rows.chunks_mut(BATCH) {
196+
if batch.len() == BATCH {
197+
domino_batch(batch, &w, stages);
198+
}
199+
}
200+
}
201+
184202
#[cfg(test)]
185203
mod tests {
186204
use super::*;

crates/symbiont/src/kanban_loop.rs

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
//! D2 — the kanban loop, the pure-SoA slice (no Lance, no ractor, no async).
2+
//!
3+
//! Proves the loop SHAPE the operator named — surrealdb (version tick) + ractor
4+
//! (owner/driver) + lance-graph-planner (move policy) = one planner SoA — using
5+
//! ONLY shipped contract types: `KanbanColumn`/`KanbanMove`/`ExecTarget`
6+
//! (`kanban`), `MailboxSoaView`+`MailboxSoaOwner` (`soa_view`),
7+
//! `NextPhaseScheduler`+`VersionScheduler::on_version`+`DatasetVersion`
8+
//! (`scheduler`). This module is the ~glue: a `SymbiontBoard` owner over the
9+
//! existing `Vec<NodeRow>` board-set that impls the two traits, driven by a `u32`
10+
//! version tick standing in for the Lance subscription.
11+
//!
12+
//! IN-direction loop, verbatim from the contract (`scheduler.rs` §IN):
13+
//! version tick → `NextPhaseScheduler::on_version(view)` → `Option<KanbanMove>`
14+
//! → `owner.try_advance_phase(move.to)` [CognitiveWork runs the Domino sweep]
15+
//! Forward arc (`next_phases().first()`): `Planning → CognitiveWork`[sweep]` →
16+
//! Evaluation → Commit` (absorbing → halt). The scheduler PROPOSES (`&view`); the
17+
//! owner DISPOSES (`&mut`) — R1 read/write split, the same as in the contract.
18+
//!
19+
//! DEFERRED (named, shipped types to swap in): the real Lance subscription
20+
//! (`lance-graph` `LanceVersionScheduler::drive_at_latest` / callcenter
21+
//! `LanceVersionWatcher::wait_changed`), the ractor `Actor` wrapper (pattern off
22+
//! `lance-graph-supervisor` `StubConsumerActor`), and the SurrealQL re-read
23+
//! (`surreal_container::view::read_via_kv_lance`).
24+
25+
use lance_graph_contract::canonical_node::NodeRow;
26+
use lance_graph_contract::kanban::{ExecTarget, KanbanColumn, KanbanMove};
27+
use lance_graph_contract::scheduler::{DatasetVersion, NextPhaseScheduler, VersionScheduler};
28+
use lance_graph_contract::soa_view::{MailboxSoaOwner, MailboxSoaView};
29+
30+
use crate::domino;
31+
32+
/// A mailbox-as-owner over symbiont's flat `Vec<NodeRow>` board-set. The SoA
33+
/// columns are kept parallel to the rows so the trait's zero-copy `&[T]` borrows
34+
/// are real slices: `energy` is synced from the boards' `Energy` tenant after a
35+
/// sweep; `edges`/`meta`/`entity` are zeroed for the POC (not read by
36+
/// `NextPhaseScheduler`, whose policy is `phase`/`cycle`/`mailbox_id` only).
37+
pub struct SymbiontBoard {
38+
rows: Vec<NodeRow>,
39+
energy: Vec<f32>,
40+
edges: Vec<u64>,
41+
meta: Vec<u32>,
42+
entity: Vec<u16>,
43+
phase: KanbanColumn,
44+
cycle: u32,
45+
mailbox: u32, // MailboxId = u32 (collapse_gate::MailboxId)
46+
}
47+
48+
impl SymbiontBoard {
49+
/// Spawn a mailbox in `Planning` (the canonical spawn column) over `n_boards`
50+
/// seeded BF16-tile boards.
51+
pub fn spawn(n_boards: usize, mailbox: u32) -> Self {
52+
let rows = domino::seed_boards(n_boards);
53+
let n = rows.len();
54+
Self {
55+
rows,
56+
energy: vec![0.0; n],
57+
edges: vec![0; n],
58+
meta: vec![0; n],
59+
entity: vec![0; n],
60+
phase: KanbanColumn::Planning,
61+
cycle: 0,
62+
mailbox,
63+
}
64+
}
65+
66+
/// Project each board's `Energy` tenant into the SoA energy column.
67+
fn sync_energy(&mut self) {
68+
for (e, row) in self.energy.iter_mut().zip(self.rows.iter()) {
69+
*e = domino::energy_of(row);
70+
}
71+
}
72+
73+
/// The `u32` version tick — the stand-in for one Lance dataset `versions()`
74+
/// event (the IN-direction trigger).
75+
fn version_tick(&mut self) -> DatasetVersion {
76+
self.cycle += 1;
77+
DatasetVersion(self.cycle as u64)
78+
}
79+
80+
/// The `CognitiveWork` phase: the BF16 Domino sweep over the boards, then the
81+
/// result projected into the energy column.
82+
fn cognitive_work(&mut self) {
83+
domino::domino_sweep(&mut self.rows, 3);
84+
self.sync_energy();
85+
}
86+
87+
/// One IN-direction step: tick → scheduler PROPOSES → owner DISPOSES. Runs the
88+
/// sweep on the `CognitiveWork` crossing. Returns the applied move, or `None`
89+
/// once the mailbox has reached an absorbing column.
90+
pub fn step(&mut self, sched: &NextPhaseScheduler) -> Option<KanbanMove> {
91+
let at = self.version_tick();
92+
let proposed = sched.on_version(&*self, at, ExecTarget::Native)?;
93+
if proposed.to == KanbanColumn::CognitiveWork {
94+
self.cognitive_work();
95+
}
96+
self.try_advance_phase(proposed.to).ok()
97+
}
98+
99+
/// Drive the forward arc to an absorbing column, returning the move trail.
100+
pub fn run_to_absorbing(&mut self, sched: &NextPhaseScheduler) -> Vec<KanbanMove> {
101+
let mut trail = Vec::new();
102+
while let Some(mv) = self.step(sched) {
103+
let absorbing = self.phase.is_absorbing();
104+
trail.push(mv);
105+
if absorbing {
106+
break;
107+
}
108+
}
109+
trail
110+
}
111+
}
112+
113+
impl MailboxSoaView for SymbiontBoard {
114+
fn mailbox_id(&self) -> u32 {
115+
self.mailbox
116+
}
117+
fn n_rows(&self) -> usize {
118+
self.rows.len()
119+
}
120+
fn w_slot(&self) -> u8 {
121+
(self.mailbox & 0x3F) as u8
122+
}
123+
fn current_cycle(&self) -> u32 {
124+
self.cycle
125+
}
126+
fn phase(&self) -> KanbanColumn {
127+
self.phase
128+
}
129+
fn energy(&self) -> &[f32] {
130+
&self.energy
131+
}
132+
fn edges_raw(&self) -> &[u64] {
133+
&self.edges
134+
}
135+
fn meta_raw(&self) -> &[u32] {
136+
&self.meta
137+
}
138+
fn entity_type(&self) -> &[u16] {
139+
&self.entity
140+
}
141+
}
142+
143+
impl MailboxSoaOwner for SymbiontBoard {
144+
fn advance_phase(&mut self, to: KanbanColumn) -> KanbanMove {
145+
let from = self.phase;
146+
self.phase = to;
147+
let libet_offset_us =
148+
if from == KanbanColumn::Planning && to == KanbanColumn::CognitiveWork {
149+
-550_000
150+
} else {
151+
0
152+
};
153+
KanbanMove {
154+
mailbox: self.mailbox,
155+
from,
156+
to,
157+
witness_chain_position: self.cycle,
158+
libet_offset_us,
159+
exec: ExecTarget::Native,
160+
}
161+
}
162+
}
163+
164+
/// The D2 demo: one mailbox drives the Rubicon forward arc; the `CognitiveWork`
165+
/// crossing burns the BF16 Domino sweep through the SoA; the NaN-projection
166+
/// surface keeps it finite; the mailbox halts at the absorbing `Commit`.
167+
pub fn run_demo() {
168+
let mut board = SymbiontBoard::spawn(64, 7);
169+
let trail = board.run_to_absorbing(&NextPhaseScheduler);
170+
let arc: Vec<KanbanColumn> = trail.iter().map(|m| m.to).collect();
171+
let max_e = board.energy().iter().copied().fold(0.0_f32, f32::max);
172+
println!(
173+
"D2 kanban loop: mailbox {} ({} boards) — version-tick → NextPhaseScheduler → \
174+
try_advance_phase drove {arc:?}; CognitiveWork ran the BF16 Domino sweep; halted \
175+
absorbing at {:?} in {} cycles; max Energy = {max_e:.4}",
176+
board.mailbox_id(),
177+
board.n_rows(),
178+
board.phase(),
179+
board.current_cycle(),
180+
);
181+
}
182+
183+
#[cfg(test)]
184+
mod tests {
185+
use super::*;
186+
187+
#[test]
188+
fn loop_drives_forward_arc_to_commit() {
189+
let mut board = SymbiontBoard::spawn(32, 1);
190+
assert_eq!(board.phase(), KanbanColumn::Planning);
191+
let trail = board.run_to_absorbing(&NextPhaseScheduler);
192+
let arc: Vec<KanbanColumn> = trail.iter().map(|m| m.to).collect();
193+
assert_eq!(
194+
arc,
195+
vec![
196+
KanbanColumn::CognitiveWork,
197+
KanbanColumn::Evaluation,
198+
KanbanColumn::Commit,
199+
]
200+
);
201+
assert!(board.phase().is_absorbing());
202+
// the Planning→CognitiveWork crossing carries the Libet anchor; others 0.
203+
assert_eq!(trail[0].libet_offset_us, -550_000);
204+
assert_eq!(trail[1].libet_offset_us, 0);
205+
// monotonic cycle stamps (the SoA cycle-ownership stamp, R4).
206+
assert_eq!(
207+
trail.iter().map(|m| m.cycle()).collect::<Vec<_>>(),
208+
vec![1, 2, 3]
209+
);
210+
// CognitiveWork actually ran the sweep, and it stayed finite (else the NaN
211+
// projection surface inside the sweep would have caught it).
212+
assert!(board.energy().iter().all(|e| e.is_finite()));
213+
assert!(board.energy().iter().any(|&e| e != 0.0));
214+
}
215+
216+
#[test]
217+
fn illegal_skip_is_rejected_no_mutation() {
218+
let mut board = SymbiontBoard::spawn(16, 2);
219+
// Planning → Evaluation is not a legal Rubicon edge.
220+
assert!(board.try_advance_phase(KanbanColumn::Evaluation).is_err());
221+
assert_eq!(board.phase(), KanbanColumn::Planning);
222+
}
223+
}

crates/symbiont/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
1111
mod bridge;
1212
mod domino;
13+
mod kanban_loop;
1314

1415
fn main() {
1516
println!(
@@ -22,4 +23,7 @@ fn main() {
2223
bridge::run_scale_demo(bridge::MAX_BOARDS);
2324
// The SoA-orchestration POC: 16-board AMX BF16 Morton-tile Domino batches.
2425
domino::run_poc(256, 3);
26+
// D2 — the kanban loop: version-tick → NextPhaseScheduler → try_advance_phase,
27+
// with the Domino sweep as the CognitiveWork phase over the SoA.
28+
kanban_loop::run_demo();
2529
}

0 commit comments

Comments
 (0)