feat(scheduler-core): implement REPL snapshot capture and simulation parameter commands (task 12.3)

Adds `snapshot [FILE]` and `set speed|output|verify ...`/`info config`
(Req 5 AC 22 "Snapshots"/"Simulation parameters") to the interactive
simulation REPL.

`snapshot` reuses sim_record.c's existing sim_record_write_header_and_
snapshot() (task 11.5) rather than reimplementing capnp encoding, so
the produced file is genuinely loadable via a fresh --sim invocation
-- the task's own Observable. `set speed`/`set output`/`set verify`
mutate fields sim_loop.c and core_clock.c already read fresh on every
event/session, so no new hook points were needed and sim_loop.c stays
untouched at exactly 800/800 lines (this project's hard per-file cap).

New repl_params.c is a fourth sibling in the repl.c/repl_break.c/
repl_info.c split (task 12.2's precedent) -- neither existing file had
headroom left under the 800-line cap for this task's own logic.

Scheduler suite: 36/36 clean (was 35/35). Review independently
reproduced the RED-phase probe and additionally verified the
snapshot round-trip Observable is non-vacuous.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPPXeKSUXUqcqvzNXY1rzJ
This commit is contained in:
Rongsong Shen 2026-08-07 10:25:32 +00:00
parent 2fecea3592
commit 2329dbf2a8
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
18 changed files with 2755 additions and 110 deletions

View file

@ -369,11 +369,12 @@
- _Depends: 12.1_
- _Verified: 2026-08-07 — repl.c (task 12.1, 370 lines) split three ways under this project's 800-line cap (task 12.2's own feature surface did not fit in one file): repl.c (416 lines, execution control + dispatch, unchanged in scope), repl_break.c (breakpoint kind/filter matching, breakpoint CRUD, and the three new driver-facing hooks xce_repl_on_event()/xce_repl_on_begin_round()/xce_repl_on_decision()), repl_info.c (`info job`/`queue`/`worker`/`resource`/`summary`/`session`/`break` dumps) -- all sharing private state via a new repl_internal.h (the full xce_sched_repl_state_s definition, moved out of repl.c). sim_loop.c gained exactly 3 new bare call expressions (xce_repl_check_event()/xce_repl_check_begin_round()/xce_repl_check_decision(), repl.h inline wrappers gated on sim->interactive, same pattern as task 12.1's two existing hooks) at the three points design.md's pseudocode requires: per decoded ReplayEvent before it joins the accumulated batch, per ReplaySession boundary before the batch is pushed, and per drained decision after sim_print_decision() -- landed sim_loop.c at exactly 800/800 lines (trimmed its own header comment to make room). repl_should_pause()'s stale doc comment (which wrongly predicted this task would extend it) was corrected: breakpoints are a separate mechanism, never folded into the `step N` pause path. test_sim_isolation.c (task 11.3, already extended by 12.1) extended again to scan repl_break.c/repl_info.c/repl_internal.h (8-file precedent). New test_repl_breakpoints_inspection.c (17 tests, 4 groups: command parsing/listing, pure matcher predicates, state-inspection dumps against a real xce_sched_core_t, full-integration scripted-stdin pause proof for both an event breakpoint and a decision breakpoint). RED-phase: two REAL bugs surfaced and fixed during development (bug-1506: test fixtures calling repl_dispatch()'s `break` command without first calling xce_sched_core_init() left makeString()'s backing xstr_table uninitialized, silently returning NULL breakpoint fields instead of failing loud; bug-1507: repl_breakpoint_free() called a bare free() on a makeString()-produced xstring -- RTTI reference-counted, not a plain malloc() chunk -- causing a heap-corruption abort, fixed by never freeing those fields at all, matching kill_sched_job()/kill_sched_queue()'s existing "makeString()'d fields live for the process's lifetime" precedent). PLUS two targeted synthetic RED-phase probes reproduced independently after both real bugs were fixed: (1) repl_find_event_breakpoint() hardcoded to always return NULL -- rebuilt, ran the suite, exactly 3 of 17 tests failed (test_event_breakpoint_matches_jobid_filter, test_event_breakpoint_matches_state_filter, test_event_breakpoint_pauses_repl_at_matching_event), all others (including decision/begin_round matching and every info dump) stayed green; (2) repl_job_state_name() hardcoded to return a fixed bogus string -- exactly 1 of 17 failed (test_info_job_reports_state_queue_priority). Both probes reverted (grep-confirmed zero residue), rebuilt, reconfirmed green. Final: 35/35 scheduler suite (was 34/34), whole-project build clean. Remediation (2026-08-07): review found `repl_info_job()`'s resreq= field reported only a `set`/`(none)` presence flag instead of real content, despite `format_resreq()` (resreq.h) already existing and already used one file away by `build_wire_job()` (snapshot_wire.c) for this exact purpose; fixed by calling `format_resreq()` in `repl_info_job()` (mirroring `build_wire_job()`'s WARN-and-fallback error handling, freed after printing since this dump is transient), corrected the function's stale doc-comment claim that no print-form existed, and extended `test_info_job_reports_state_queue_priority` to assert the real formatted text (`"Primary:"`/`"Tasks: min = 0, max = 0"`/`"Rusages:"`) appears instead of the old boolean flag. Still 35/35 scheduler suite.
- [ ] 12.3 Implement REPL snapshot capture and simulation parameter commands
- [x] 12.3 Implement REPL snapshot capture and simulation parameter commands
- Support writing a full snapshot to a file at the current simulation position, and adjusting speed/output-file/verify-mode from the prompt without restarting
- Observable: a snapshot command produces a file that the simulation driver can later load via a fresh `--sim` invocation starting from that exact position
- _Requirements: 5.22_
- _Depends: 12.1_
- _Verified: 2026-08-07 — new repl_params.c (fourth sibling in the repl.c/repl_break.c/repl_info.c family, repl_internal.h's own "Why a split at all" section extended again): `snapshot [FILE]` and `set speed|output|verify ...` (repl_cmd_snapshot()/repl_cmd_set()), reusing sim_record.c's sim_record_write_header_and_snapshot() (task 11.5) unchanged for both the one-shot standalone snapshot and `set output`'s live-stream preamble -- neither hand-rolls capnp encoding. `set speed <N>` mutates BOTH sim->sim_speed_num/sim_speed_den (the driver-level shadow copy) AND sim->core.speed_num/speed_den via the SAME atomic_store_explicit() pair sim_set_virtual_clock() (sim.c) uses at startup -- confirmed by reading core_clock.c's xce_core_now() that it reads the atomic pair fresh, uncached, on every call, so sim_loop.c needed zero new call sites (verified: `sim->sim_speed_num`/`sim->sim_speed_den` alone are never read by sim_loop.c or core_clock.c -- only the atomic `core.speed_num`/`core.speed_den` pair is; the shadow copy exists purely for the REPL/`info config` to read without reaching into `core`, per sim.h's own doc comment -- both fields are kept in sync by repl_set_speed() so this distinction never surfaces as a bug). `info config` (repl_info.c, alongside every other `info` dump) added as the read-only counterpart. `snapshot`'s auto-generated filename (`<basename>-snap-<event_index>.replay`) uses a documented fixed basename fallback ("sim") since xce_sched_sim_driver_t retains no string form of the replay path (sim.h has only a bare FILE*) -- a deliberate, documented deviation matching repl_count_jobs()'s own AC22-pseudocode-deviation precedent, judged not worth adding a retained-path field to sim.h/sim.c for. sim_loop.c untouched, still exactly 800/800 lines. New test_repl_snapshot_params.c (7 tests): the central Observable proved end to end via a forked child (`step 1`/`snapshot FILE`/`quit` against a real fixture) followed by a SECOND, real sched_sim_driver_start() call in the parent against the file the child wrote, asserting success, an exact reloaded snapshot_ts (2000, frozen clock), the pre-session job present and the post-session job absent (proves the snapshot captured the position at command time, not the whole file); plus auto-filename generation, `set speed`/`set verify` state+malformed-input unit tests, `set output` open/preamble-readback/off round trip, `info config` field-format assertions, and a `help` listing regression check. Every other sim-family white-box test file (`test_sim_equivalence.c`/`test_sim_event_loop.c`/`test_repl_breakpoints_inspection.c`/`test_sim_record_output.c`/`test_repl_execution_control.c`/`test_sim_alt_events.c`/`test_sim_verify.c`) needed a one-line `#include "../src/repl_params.c"` addition alongside their existing `repl.c` include -- repl.c's repl_dispatch() now unconditionally references repl_cmd_snapshot()/repl_cmd_set(), the same "unconditional extern forces every white-box TU to add the new source file" pattern this file family already established. RED phase: initial build failed exactly as expected (`meson setup` error "File src/repl_params.c does not exist") after wiring the test file, repl_internal.h extern declarations, and both meson.build files but before writing the implementation; after implementation, a real bug surfaced and was self-fixed before commit (an accidental `*/` inside a prose doc comment -- `xce_scheduler_t*/record_dir` -- prematurely closed a block comment, a variant of this codebase's known recurring `-Wcomment` pitfall). Synthetic RED-phase probe on the finished implementation: removed both `atomic_store_explicit()` calls from `repl_set_speed()` (leaving only the shadow-copy assignment), rebuilt, ran the suite: exactly 1 of 7 tests failed (`test_set_speed_updates_shadow_and_core_atomics`, "Expected 7 Was 0"), all 6 others stayed green; reverted, rebuilt, reconfirmed clean. test_sim_isolation.c extended to scan repl_params.c too (9-file precedent, seventh sched_sim_isolation_lib archive member). Final: 36/36 scheduler suite (was 35/35), whole-project build clean._
- [ ] 12.4 REPL equivalence end-to-end test
- Run a fixture file through the REPL using step/continue to completion and through batch simulation mode, and assert both produce the same decision sequence