Skip to content

Commit c29b580

Browse files
authored
Merge pull request #578 from AdaWorldAPI/claude/jirak-math-theorems-harvest-rfii13
feat(supervisor): S2 driver — MUL gate → owner advance (drive_mul_advance)
2 parents 3e5c689 + f543bfc commit c29b580

3 files changed

Lines changed: 153 additions & 0 deletions

File tree

.claude/board/AGENT_LOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 2026-06-21 (cont.³³) — S2 MUL→phase driver SHIPPED (actor-side) — gate → owner advance
2+
3+
**Main thread (Opus), self-directed (da-capo).** S2→S4 composition on the same light crate: `drive_mul_advance(actor, qualia, mantissa)` in `lance-graph-supervisor::kanban_actor` reads the owner's phase (`KanbanMsg::Phase`), runs the contract's `mul::i4_eval::gate_decision_i4` → `KanbanColumn::advance_on_gate` (Flow→forward, Block→Prune-where-legal, Hold→None), and on a non-Hold gate `cast`s `KanbanMsg::Advance` to the owning actor (the owner advances ITSELF — the operator model). `mul_target` is the pure lowering. Integer i4 path — no f64/NaN. **+1 test (5 total green):** `s2_driver_gate_advances_then_holds` (Flow qualia+mantissa>0 → Planning→CognitiveWork; neutral+0 → Hold → no advance, phase stays). clippy + fmt clean; light build, no disk/symbiont gate. This is the actor-side S2 consumer (`mul_phase_step` node wrapper stays the single-node convenience). **Remaining S2:** the per-row `cognitive-shader-driver` owner loop over the `qualia` column (needs `MailboxSoaView::qualia()` + the shader-driver build = disk) — heavier, deferred. **OUT-leg now real+tested on the light crate: S4 actor (#576) + delivery edge (#577) + S2 actor-side driver (this).** Only S3 (lance `LanceVersionScheduler` consumer) + run-NaN need the heavier builds. Plan S2 status annotated. Rides a PR on jirak.
14
## 2026-06-21 (cont.³²) — S4 delivery edge SHIPPED — S4 mechanism now COMPLETE end-to-end
25

36
**Main thread (Opus), self-directed (da-capo).** Completed S4 on the same light crate: `deliver_kanban_step("kanban.<mailbox>.<phase>")` in `lance-graph-supervisor::kanban_actor` — `parse_kanban_step` (snake_case phase vocab) → `ractor::registry::where_is(mailbox)` → `cast(KanbanMsg::Advance{to})` → relays the owner's `try_advance_phase` result. Address source = the step's existing string + the actor system's OWN registry (NOT a bespoke bridge registry, NOT a `UnifiedStep` field — exactly the codex-#574-corrected design). `KanbanRouteError`: `BadStepType` / `NoMailbox` (routing miss, NOT a no-owner case — a live mailbox is always owned) / `Illegal` (relayed RubiconTransitionError) / `Rpc`. **+2 tests (4 total green):** `parse_kanban_step_shapes`, `delivery_edge_resolves_via_registry_then_advances` (legal advance via where_is; unknown mailbox → graceful NoMailbox; illegal edge → Illegal; malformed → BadStepType). clippy clean (fixed an `unnecessary_to_owned` on the where_is arg) + fmt; light build, no disk/symbiont gate. **S4 mechanism is now COMPLETE end-to-end** (owner-advance #576 + delivery edge); only the S2/S3 *drivers* that SEND Advance remain, composing on top. Plan S4 status → "COMPLETE". Rides a PR on jirak.

.claude/plans/capstone-out-leg-wiring-v1.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ moment a surface frees.
3535

3636
## S2 — MUL→phase seam gets a real owner-side consumer
3737

38+
**Status (2026-06-21): MUL→phase DRIVER shipped** (actor-side path).
39+
`lance-graph-supervisor::kanban_actor::drive_mul_advance(actor, qualia, mantissa)`
40+
reads the owner's phase, runs `gate_decision_i4``advance_on_gate`, and on a
41+
non-Hold gate `cast`s `KanbanMsg::Advance` to the owning actor (the S2→S4
42+
composition; the owner advances itself). `mul_target` is the pure lowering.
43+
Integer i4 gate — no f64/NaN. Test `s2_driver_gate_advances_then_holds` green
44+
(Flow → Planning→CognitiveWork; Hold → no advance). This is the actor-side S2
45+
consumer the census wanted (the `mul_phase_step` node wrapper stays the
46+
single-node convenience). **Remaining (heavier, deferred):** the per-row owner
47+
loop in `cognitive-shader-driver` that reads the `qualia` column and drives many
48+
rows — needs `MailboxSoaView::qualia()` (the `soa_view.rs:157` deferral) + the
49+
shader-driver build (disk). The actor-side trigger is real, tested code now.
50+
3851
**Census state:** GAP. `NodeRow::mul_phase_step` (gate→phase) is test-only;
3952
`sigma-tier-router` consumes `gate_decision_i4` for tier dispatch, not phase.
4053

crates/lance-graph-supervisor/src/kanban_actor.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
//! Rubicon edge is a typed [`RubiconTransitionError`], never silent corruption.
2626
2727
use lance_graph_contract::kanban::{KanbanColumn, KanbanMove, RubiconTransitionError};
28+
use lance_graph_contract::mul::i4_eval::gate_decision_i4;
2829
use lance_graph_contract::soa_view::MailboxSoaOwner;
30+
use lance_graph_contract::QualiaI4_16D;
2931
use ractor::{Actor, ActorProcessingErr, ActorRef, RpcReplyPort};
3032

3133
/// Messages the kanban actor accepts.
@@ -39,6 +41,17 @@ pub enum KanbanMsg {
3941
},
4042
/// Read the owned mailbox's current Rubicon phase (no mutation).
4143
Phase { reply: RpcReplyPort<KanbanColumn> },
44+
/// **Atomic** S2 step: run the MUL gate (`gate_decision_i4` over `qualia` +
45+
/// `mantissa`) against the owner's CURRENT phase and advance in ONE message.
46+
/// Replies `Ok(Some(move))` on advance, `Ok(None)` on Hold, or the typed
47+
/// error on an illegal edge. Gate-read and transition are serialized with the
48+
/// owner state (one mailbox message), so a concurrent sender cannot make the
49+
/// phase read stale between decision and mutation (codex #578).
50+
MulAdvance {
51+
qualia: QualiaI4_16D,
52+
mantissa: i8,
53+
reply: RpcReplyPort<Result<Option<KanbanMove>, RubiconTransitionError>>,
54+
},
4255
}
4356

4457
/// A ractor actor whose `State` IS a [`MailboxSoaOwner`] — the SoA mailbox and
@@ -91,6 +104,21 @@ where
91104
KanbanMsg::Phase { reply } => {
92105
let _ = reply.send(state.phase());
93106
}
107+
KanbanMsg::MulAdvance {
108+
qualia,
109+
mantissa,
110+
reply,
111+
} => {
112+
// Gate-decision + transition in ONE serialized message: the gate
113+
// reads `state.phase()` at the instant of mutation, so a
114+
// concurrent sender can't make it stale (mailbox-as-owner
115+
// atomicity — codex #578).
116+
let result = match mul_target(state.phase(), &qualia, mantissa) {
117+
None => Ok(None), // Hold
118+
Some(to) => state.try_advance_phase(to).map(Some), // advance
119+
};
120+
let _ = reply.send(result);
121+
}
94122
}
95123
Ok(())
96124
}
@@ -168,6 +196,49 @@ pub async fn deliver_kanban_step(step_type: &str) -> Result<KanbanMove, KanbanRo
168196
})
169197
}
170198

199+
// ─── S2 driver: MUL gate (`gate_decision_i4`) → owner advance ─────────────────
200+
201+
/// The MUL-gated target phase for `phase` given a node's `qualia` + inference
202+
/// `mantissa`: run the i4 gate ([`gate_decision_i4`]) and lower it to the
203+
/// DAG-legal next phase via [`KanbanColumn::advance_on_gate`] (Flow → forward,
204+
/// Block → Prune-where-legal, Hold → `None`). Pure + integer-only (no f64/NaN).
205+
pub fn mul_target(
206+
phase: KanbanColumn,
207+
qualia: &QualiaI4_16D,
208+
mantissa: i8,
209+
) -> Option<KanbanColumn> {
210+
let gate = gate_decision_i4(qualia, mantissa);
211+
phase.advance_on_gate(&gate)
212+
}
213+
214+
/// S2 driver: the MUL gate decides, the owner advances ITSELF — in ONE atomic
215+
/// actor message ([`KanbanMsg::MulAdvance`]). Returns the emitted [`KanbanMove`]
216+
/// on advance, `None` on Hold, or [`KanbanRouteError::Illegal`] on an illegal
217+
/// edge.
218+
///
219+
/// **Atomicity (codex #578):** the gate-read and the transition run inside the
220+
/// SAME serialized mailbox message, so the gate sees the owner's phase at the
221+
/// instant of mutation — two concurrent drivers can't both read a stale
222+
/// `Planning` and collide. (The earlier two-RPC `Phase`-then-`Advance` shape had
223+
/// that race.) `advance_on_gate` only yields a DAG-legal successor, so `Illegal`
224+
/// here would signal a gate/DAG drift bug — surfaced, not panicked.
225+
pub async fn drive_mul_advance(
226+
actor: &ActorRef<KanbanMsg>,
227+
qualia: QualiaI4_16D,
228+
mantissa: i8,
229+
) -> Result<Option<KanbanMove>, KanbanRouteError> {
230+
let inner = ractor::call!(actor, |reply| KanbanMsg::MulAdvance {
231+
qualia,
232+
mantissa,
233+
reply
234+
})
235+
.map_err(|e| KanbanRouteError::Rpc(e.to_string()))?;
236+
inner.map_err(|e| KanbanRouteError::Illegal {
237+
from: e.from,
238+
to: e.to,
239+
})
240+
}
241+
171242
#[cfg(test)]
172243
mod tests {
173244
use super::*;
@@ -345,4 +416,70 @@ mod tests {
345416
actor.stop(None);
346417
handle.await.expect("actor join");
347418
}
419+
420+
#[tokio::test]
421+
async fn s2_driver_gate_advances_then_holds() {
422+
let (actor, handle) = Actor::spawn(
423+
None,
424+
KanbanActor::<TestBoard>::default(),
425+
board(KanbanColumn::Planning),
426+
)
427+
.await
428+
.expect("spawn");
429+
430+
// Flow qualia (warmth/groundedness high, low tension, calibrated) +
431+
// mantissa>0 → gate Flow → forward advance Planning → CognitiveWork.
432+
let flow_q = QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2);
433+
let mv = drive_mul_advance(&actor, flow_q, 4)
434+
.await
435+
.expect("driver ok")
436+
.expect("advanced on Flow");
437+
assert_eq!(mv.from, KanbanColumn::Planning);
438+
assert_eq!(mv.to, KanbanColumn::CognitiveWork);
439+
440+
// Neutral qualia + mantissa 0 → gate Hold → None (owner stays put).
441+
let held = drive_mul_advance(&actor, QualiaI4_16D(0), 0)
442+
.await
443+
.expect("driver ok");
444+
assert!(held.is_none(), "Hold must not advance");
445+
let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc");
446+
assert_eq!(phase, KanbanColumn::CognitiveWork);
447+
448+
actor.stop(None);
449+
handle.await.expect("actor join");
450+
}
451+
452+
#[tokio::test]
453+
async fn concurrent_mul_drivers_serialize_no_spurious_rejection() {
454+
// codex #578: two concurrent Flow drivers must NOT both read a stale
455+
// `Planning` and collide. The atomic `MulAdvance` serializes gate+advance
456+
// in the owner's mailbox, so they chain Planning→CognitiveWork→Evaluation
457+
// — both succeed, neither is a spurious `Illegal`.
458+
let (actor, handle) = Actor::spawn(
459+
None,
460+
KanbanActor::<TestBoard>::default(),
461+
board(KanbanColumn::Planning),
462+
)
463+
.await
464+
.expect("spawn");
465+
466+
let flow = || QualiaI4_16D(0).with(3, 4).with(14, 3).with(9, 4).with(1, 2);
467+
let a1 = actor.clone();
468+
let a2 = actor.clone();
469+
let (r1, r2) = tokio::join!(
470+
drive_mul_advance(&a1, flow(), 4),
471+
drive_mul_advance(&a2, flow(), 4),
472+
);
473+
474+
// Neither call is a spurious rejection; both advanced along the arc.
475+
assert!(r1.expect("driver1 ok").is_some(), "first advanced");
476+
assert!(r2.expect("driver2 ok").is_some(), "second advanced");
477+
478+
// Serialized chain: Planning → CognitiveWork → Evaluation.
479+
let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc");
480+
assert_eq!(phase, KanbanColumn::Evaluation);
481+
482+
actor.stop(None);
483+
handle.await.expect("actor join");
484+
}
348485
}

0 commit comments

Comments
 (0)