Skip to content

Commit 2ca26f0

Browse files
authored
Merge pull request #580 from AdaWorldAPI/claude/jirak-math-theorems-harvest-rfii13
run_to_absorbing — actor-side run-NaN proven green (full Rubicon cycle, lance-free)
2 parents 98d5d2f + d5652d4 commit 2ca26f0

3 files changed

Lines changed: 109 additions & 1 deletion

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.³⁵) — run-NaN actor-side half PROVEN green — run_to_absorbing drives a full Rubicon cycle, lance-free
2+
3+
**Main thread (Opus), self-directed ("what do you choose next").** Chose the highest-value LIGHT move over forcing a disk-heavy lance build: answered the buildable half of the capstone's **run-NaN HYPOTHESIS**. New `lance-graph-supervisor::kanban_actor::run_to_absorbing(actor, max_ticks)` — repeatedly `drive_version_tick` until the owner reports an absorbing column (`Commit`/`Prune`), returning the forward-arc `KanbanMove` trace; `max_ticks` is a defensive non-termination guard (pure forward arc always reaches `Commit`). This is the actor-side, lance-free, symbiont-free analog of `symbiont::kanban_loop::run_to_absorbing`. **+1 test (14 total green):** `run_to_absorbing_drives_a_full_rubicon_cycle_no_nan_no_panic` — a mailbox runs `Planning → CognitiveWork → Evaluation → Commit`, terminates, every move is a legal Rubicon edge, no panic, no spurious `Illegal`, idempotent at rest (second run empty, phase unchanged). The phase/i4 path is integer-only ⇒ **NaN is structurally impossible on this half**, so green IS the actor-side run-NaN answer. clippy + fmt clean; light build, no lance/disk/symbiont gate. **Remaining run-NaN (symbiont/disk-gated):** the cognitive half — instrument `symbiont::kanban_loop::run_to_absorbing` over the energy column for a live-cycle NaN% (other session owns symbiont; coordinate). Plan run-NaN status annotated "actor-side half PROVEN". Rides a PR on jirak. Capstone actor-side substrate now complete: S4 (#576/#577) + S2 (#578) + S3 (#579) + run-to-absorbing (this).
14
## 2026-06-21 (cont.³⁴) — S3 IN-leg driver SHIPPED (actor-side) — version tick → owner forward-arc advance, no-op suppressed
25

36
**Main thread (Opus), self-directed ("PR, easy").** Closed the actor-side half of S3 on the same light crate, mirroring the S2 atomic pattern. New in `lance-graph-supervisor::kanban_actor` (feature `supervisor`): (1) `KanbanMsg::Tick { at, reply }` — the **atomic** in-actor realization of the contract's `NextPhaseScheduler`: a substrate version tick advances the owner along the Rubicon forward arc (`phase().next_phases().first()`) in ONE serialized message, reading the phase at the instant of mutation (the codex-#578 atomicity lesson applied to the IN-leg); absorbing column → `None`, **the no-op tick is suppressed** (not an error; forward arc is legal by construction so the infallible `advance_phase` is used). (2) `drive_version_tick(actor, at)` — thin async wrapper. (3) `drive_scheduled_tick(scheduler, view, at, exec, actor)` — generic consumer that drives the EXISTING `VersionScheduler` trait ("propose, don't dispose": scheduler proposes from a view, owner disposes via `Advance`, `None` suppresses), for custom policies (version-delta gating, `Plan`/`Prune`, batching) reading a richer view; documented as advisory (proposal computed outside the owner message → may relay a typed `Illegal` rather than corrupt). **+3 tests (now green):** `version_tick_advances_forward_arc_then_suppresses_at_absorbing` (Planning→CognitiveWork→Evaluation→Commit then suppressed), `concurrent_version_ticks_serialize_along_the_arc` (two ticks chain, no stale-phase collision), `custom_scheduler_proposes_and_owner_disposes` (drives `NextPhaseScheduler` propose→dispose + suppresses an absorbing proposal). `cargo test -p lance-graph-supervisor --features supervisor --lib` = 12 passed/0 failed; clippy clean (no supervisor-crate warnings; pre-existing ontology/callcenter warnings only) + fmt clean; light build, no lance/disk/symbiont gate. **Remaining S3 (lance/disk-gated):** wire the LIVE `LanceVersionScheduler::drive_at_latest` over a real `VersionedGraph::versions()` to feed `at` — the apply + no-op-suppress loop is now done, only the live `versions()` poll remains. OUT-leg actor side now: S4 owner-advance (#576) + delivery edge (#577) + S2 driver (#578) + **S3 driver (this)**. Plan S3 status annotated. Rides a PR on jirak.

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,25 @@ ractor `cast` — **same ownership, same `try_advance_phase`**. No no-owner test
199199

200200
## run-NaN — the measurement (Wave-0's third metric)
201201

202-
**Census state:** HYPOTHESIS. `symbiont::kanban_loop::run_to_absorbing
202+
**Status (2026-06-21): actor-side half PROVEN green** (lance-free, symbiont-free).
203+
`lance-graph-supervisor::kanban_actor::run_to_absorbing(actor, max_ticks)` drives a
204+
mailbox to its absorbing column through the REAL actor messages (the `Tick` arc)
205+
and returns the forward-arc `KanbanMove` trace. Test
206+
`run_to_absorbing_drives_a_full_rubicon_cycle_no_nan_no_panic`: a mailbox runs
207+
`Planning → CognitiveWork → Evaluation → Commit`, terminates within the bound,
208+
every move is a legal Rubicon edge, no panic, no spurious `Illegal`, and the run
209+
is idempotent at rest (a second run is an empty trace, phase unchanged). The
210+
phase/i4 path is integer-only → **NaN is structurally impossible on this half**,
211+
so the green run IS the actor-side run-NaN answer. 14 tests green; clippy + fmt
212+
clean; light build.
213+
214+
**Remaining (symbiont/disk-gated):** the *cognitive* half — instrument
215+
`symbiont::kanban_loop::run_to_absorbing(&NextPhaseScheduler)` over the energy
216+
column + observable outputs (not just the phase trail) for a live-cycle NaN%.
217+
That harness drives the full domino sweep over a real SoA and is owned by the
218+
cognitive-compilation session — coordinate first.
219+
220+
**Census state (orig):** HYPOTHESIS. `symbiont::kanban_loop::run_to_absorbing
203221
(&NextPhaseScheduler)` is the runnable harness.
204222

205223
**Work:** instrument one `run_to_absorbing` cycle; count valid vs

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,40 @@ where
347347
}
348348
}
349349

350+
// ─── Capstone run-to-absorbing: drive a mailbox to its terminal column ─────────
351+
352+
/// Drive a mailbox to its **absorbing column** by repeatedly ticking
353+
/// ([`drive_version_tick`]) until the owner reports no further move
354+
/// (`Commit`/`Prune`). Returns the full forward-arc [`KanbanMove`] trace.
355+
///
356+
/// This is the actor-side, lance-free analog of the cognitive loop's
357+
/// run-to-absorbing: it proves the OUT/IN-leg substrate carries a mailbox through
358+
/// a complete Rubicon cycle to a terminal state with no panic and no spurious
359+
/// rejection (the integer-only phase/i4 path cannot produce NaN). The live S3
360+
/// source feeds real `versions()` ticks through the same `drive_version_tick`;
361+
/// here the loop counter stands in for the version stream.
362+
///
363+
/// `max_ticks` bounds the loop defensively. The pure forward arc always reaches
364+
/// `Commit` (`Planning → CognitiveWork → Evaluation → Commit`), so the bound is a
365+
/// guard against a future non-terminating policy, not a normal exit: exceeding it
366+
/// returns [`KanbanRouteError::Rpc`] with a non-termination note rather than
367+
/// looping forever.
368+
pub async fn run_to_absorbing(
369+
actor: &ActorRef<KanbanMsg>,
370+
max_ticks: usize,
371+
) -> Result<Vec<KanbanMove>, KanbanRouteError> {
372+
let mut trace = Vec::new();
373+
for tick in 0..max_ticks {
374+
match drive_version_tick(actor, DatasetVersion(tick as u64 + 1)).await? {
375+
Some(mv) => trace.push(mv),
376+
None => return Ok(trace), // absorbing column reached — the cycle ended
377+
}
378+
}
379+
Err(KanbanRouteError::Rpc(format!(
380+
"run_to_absorbing did not reach an absorbing column within {max_ticks} ticks"
381+
)))
382+
}
383+
350384
#[cfg(test)]
351385
mod tests {
352386
use super::*;
@@ -737,4 +771,57 @@ mod tests {
737771
handle.await.expect("actor join");
738772
}
739773
}
774+
775+
#[tokio::test]
776+
async fn run_to_absorbing_drives_a_full_rubicon_cycle_no_nan_no_panic() {
777+
// Capstone run-NaN (actor-side, lance-free): a mailbox driven from
778+
// Planning runs to the absorbing Commit column through the REAL actor
779+
// messages — it terminates, never panics, never emits a spurious Illegal,
780+
// and the trace is the deterministic forward arc. The integer phase/i4
781+
// path cannot produce NaN, so a green run here IS the actor-side half of
782+
// the loop's run-NaN answer.
783+
let (actor, handle) = Actor::spawn(
784+
None,
785+
KanbanActor::<TestBoard>::default(),
786+
board(KanbanColumn::Planning),
787+
)
788+
.await
789+
.expect("spawn");
790+
791+
let trace = run_to_absorbing(&actor, 16)
792+
.await
793+
.expect("reaches an absorbing column within the bound");
794+
795+
// Forward arc: Planning → CognitiveWork → Evaluation → Commit (3 moves).
796+
let arc: Vec<_> = trace.iter().map(|m| m.to).collect();
797+
assert_eq!(
798+
arc,
799+
vec![
800+
KanbanColumn::CognitiveWork,
801+
KanbanColumn::Evaluation,
802+
KanbanColumn::Commit,
803+
]
804+
);
805+
// Every move en route is a legal Rubicon edge (no corruption).
806+
for m in &trace {
807+
assert!(
808+
m.from.can_transition_to(m.to),
809+
"{:?} -> {:?} must be legal",
810+
m.from,
811+
m.to
812+
);
813+
}
814+
815+
// The owner rests in the absorbing column: a further run is empty, and
816+
// the phase is unchanged (idempotent at rest — no spurious advance/error).
817+
let again = run_to_absorbing(&actor, 4)
818+
.await
819+
.expect("idempotent at the absorbing column");
820+
assert!(again.is_empty(), "absorbing column yields no further moves");
821+
let phase = ractor::call!(actor, |reply| KanbanMsg::Phase { reply }).expect("rpc");
822+
assert_eq!(phase, KanbanColumn::Commit);
823+
824+
actor.stop(None);
825+
handle.await.expect("actor join");
826+
}
740827
}

0 commit comments

Comments
 (0)