myxce/.kiro/specs/scheduler-sim-repl-verbosity/design.md
Rongsong Shen bd97397373
feat(scheduler-sim-repl-verbosity): task 9.2 - discoverable help and break event types
Adds repl_bp_kind_names(char* buf, size_t bufsz) (repl_break.c),
comma-joining BP_KIND_TABLE's tokens in table order into a caller
buffer. repl_print_help() (repl.c) is rewritten into four labeled
sections (Execution Control, Breakpoints, State Inspection, Snapshot &
Parameters) with a single shared "  %-38s%s\n" column format, info
config moved into State Inspection next to its siblings, set units
added to Snapshot & Parameters, and the break line's description now
built from repl_bp_kind_names() so it enumerates the complete event
type set. repl_cmd_break()'s unrecognized-event-type error calls the
same accessor, so help and the error message can never disagree with
each other or with BP_KIND_TABLE (Requirements 7.1-7.6). No command's
existence, argument parsing, or other output changed.

repl_break.c grew from 905 to 949 lines (18.6% over the 800-line cap,
up from task 3.2's already-accepted 13%-over residual). Review
empirically tested extracting the BP_KIND_TABLE lookup-table domain
into a new repl_bp_kind.c: it built and passed, but only reached 838
lines (still 4.75% over cap) while requiring 22 white-box test files'
#include lines to be updated - the same disproportionate-churn,
doesn't-achieve-compliance outcome already accepted at tasks 6.2/8.2.
Reverted; 949 lines accepted in place.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd
2026-09-02 20:30:45 +00:00

71 KiB
Raw Blame History

Design Document

Overview

Purpose: This feature reduces and reorganizes the console output the xce_scheduler --interactive simulation REPL prints while executing continue, so an operator can watch a long replay's progress without wading through repetitive, low-information lines.

Users: XCE scheduler engineers and operators debugging scheduling behavior via xce_scheduler -i -s <replay>, a capability shipped by the scheduler-core spec.

Impact: Changes the text xce_repl_on_event() (repl_break.c) and sim_print_decision() (sim_loop.c) print while sim->repl_state->mode == XCE_REPL_MODE_CONTINUE; the REPL's command prompt string (repl_command_loop(), repl.c); info worker's/info resource's resource-value rendering and the set command (repl_info.c, repl_params.c); and help's listing plus break's unrecognized-event-type error text (repl.c, repl_break.c). No change to scheduling decisions, replay determinism, or step mode's per-event/per-decision detail; every other aspect of every REPL command's parsing and behavior is unchanged.

Goals

  • Suppress routine per-event echo during continue execution, while still surfacing structural changes (Requirement 1).
  • Replace unconditional per-decision echo with one summary line per round during continue (Requirement 2).
  • Collapse consecutive idle or unchanged rounds into a single range notification (Requirement 3).
  • Compress array-job identifier lists into index ranges everywhere the REPL displays multiple job identifiers together, including info worker's assigned-job listing (Requirement 4).
  • Leave step mode's per-event/per-decision detail, and every other aspect of the REPL's existing commands, unchanged except where this feature's own requirements explicitly say otherwise (Requirement 5).
  • Show the current session and timestamp in the REPL's command prompt, in both step and continue modes (Requirement 5.5).
  • Render byte/megabyte-denominated resource values in a human-readable unit under operator control (set units), and lay out info worker's/info resource's listings as a grouped table (Requirement 6).
  • Reorganize help's command listing into labeled, consistently-aligned sections, and make break's valid event types discoverable in both help and its error message (Requirement 7).

Non-Goals

  • New REPL commands or newly inspectable data (limit usage, pend-reason detail) — tracked separately under the scheduler-sim-diagnostics brief.
  • Any change to scheduling/allocation logic or to the deterministic outcome of a replay.
  • Non-interactive (headless, no -i) scheduler output.
  • Resolving bug-1611 (whether DISPATCH decisions are structurally invisible in replay mode) — this spec only guarantees Requirement 2.4 holds regardless of that investigation's outcome (see Design Decisions, research.md).
  • Unit-aware formatting for any resource outside the confirmed canonical-bytes list (mem, tmp, maxmem, maxtmp, maxswap) and the confirmed canonical-megabytes swp — xce_resource_t (src/include/xce/types.h) carries no units metadata at all, so an arbitrary operator-defined or plugin resource is displayed as a plain unitless number, exactly as today.

Boundary Commitments

This Spec Owns

  • Console output formatting/verbosity logic inside xce_repl_on_event() (repl_break.c), the continue-mode gate on sim_print_decision()'s call site (sim_loop.c), and the new round-boundary rollup/collapse logic added at xce_repl_post_session_check() (repl.c).
  • The new jobid-range-compression helper and every call site that adopts it, including info worker's assigned-job listing (repl_info.c).
  • New xce_sched_repl_state_s fields required to track collapse state across rounds.
  • The REPL prompt string construction in repl_command_loop() (repl.c).
  • Resource-value unit classification/scaling helpers, the units_mode display setting, and the table/grouping layout of info worker's and info resource's resource listings (repl_info.c, repl_params.c).
  • repl_print_help()'s listing structure (repl.c) and repl_cmd_break()'s unrecognized-event-type error text (repl_break.c).

Out of Boundary

  • Any new info/break/set top-level subcommand or newly inspectable scheduler data (set units is a new option of the existing set command, not a new command).
  • Scheduling/allocation/decision logic itself (scheduler.c, src/plugins/).
  • sim_print_decision()'s behavior when sim->interactive is false (headless mode) — unchanged.
  • Root-causing bug-1611 — a separate investigation; this spec only ensures its own requirements hold regardless of the answer.
  • Unit-aware formatting for resources outside the confirmed bytes/megabytes list (see Non-Goals) — no generic units-metadata system is added to xce_resource_t or the wire protocol.

Allowed Dependencies

  • Existing xce_sched_repl_state_s fields (mode, last_n_dispatch/last_n_pend/last_n_preempt, session_count) — repl_internal.h.
  • The existing core->jobs iteration idiom (iter_make/iter_next, JOB_IS_PEND) already used by repl_info_jobs().
  • Existing helpers fmt_jobid_ex() (lib/utils.c) and repl_format_ts_human() (repl_fmt.c post-review, task 7.2 Correction below — originally repl.c).
  • Must not add a new call site to sim_loop.c's main loop beyond gating the two calls that already exist there (sim_print_decision, the xce_repl_check_event wrapper) — that file is documented at its line-budget ceiling (repl.h's own header comment).
  • xce_sched_repl_state_s.session_count and xce_core_now()/repl_format_ts_human() (repl_fmt.c post-review, task 7.2 Correction below — originally repl.c) for the prompt's session/timestamp content — no new state needed for that part.
  • repl_break.c's existing BP_KIND_TABLE (currently static/file-local) — exposed via one new shared accessor declared in repl_internal.h, rather than duplicated as a second hardcoded list, so help and the unrecognized-event-type error can never drift out of sync with each other or with the table itself.
  • The existing repl_command_loop() call site's readline(XCE_REPL_PROMPT) (repl.c:439) — this feature makes the prompt argument dynamic in place; no new call site.
  • src/controller/testsuite/test.c's is_bytes_valued_resource()/format_bytes_human() as a design precedent only (same classification list, same adaptive B/KB/MB/GB/TB/PB algorithm) — not a shared symbol, since xce_test and xce_scheduler are separate binaries; the scheduler gets its own repl_*-named port of the same logic.

Revalidation Triggers

  • If scheduler-sim-diagnostics adds pend-reason data, the round-rollup collapse fingerprint (Design Decisions, research.md) may need to incorporate it — revisit Requirement 3.2's implementation then.
  • If bug-1611 is confirmed a real dispatch-visibility gap and separately fixed, the rollup's dispatch count source should switch from the decision tally to the same state-derived approach already used for the pend-jobid set.
  • Any change to xce_sched_repl_state_s's existing mode/tally fields by another spec should re-check this feature's rollup logic.
  • If a future spec adds real units metadata to xce_resource_t (see Non-Goals), the hardcoded bytes/megabytes classification lists added here should be replaced by reading that metadata instead of the name-match lists.
  • If repl_break.c's BP_KIND_TABLE gains or loses an entry (e.g. a new breakpoint-able event type), help and break's error message pick it up automatically via the shared accessor — no separate list to update, by construction.

Architecture

Existing Architecture Analysis

  • repl.c/repl_break.c/repl_info.c/repl_params.c form the REPL command family under scheduler-core. repl.h folds the sim->interactive gate into static inline wrapper functions so sim_loop.c's own diff stays to single-line, brace-free call expressions — this feature's sim_loop.c change must follow that same convention rather than adding a bare if block to the loop body.
  • xce_sched_repl_state_s (repl_internal.h) is the REPL's own session-scoped state struct, zero-initialized via calloc in repl_state_ensure() — new fields need no separate lifecycle.
  • xce_repl_post_session_check() (repl.c) already runs exactly once per completed round, immediately after that round's decisions are drained, purely to check the step-budget pause condition today.

Architecture Pattern & Boundary Map

Correction (post-review): breakpoint-hit detection and structural-event classification both live in repl_break.c (xce_repl_on_event, xce_repl_on_begin_round, xce_repl_on_decision — each calls repl_command_loop() directly on a match), not in repl.c. Requirement 3.5 ("a structural event ends the active range") and Requirement 3.6 ("a breakpoint hit ends the active range") therefore cannot be satisfied by round-boundary logic in repl.c alone — repl_break.c needs a way to force-flush the Round Rollup's active range from its own call sites. The Round Rollup component now exposes an explicit flush entry point for this, called from both files.

flowchart TD
    RoundEnd[Round decisions drained] --> Query[Query core jobs for PEND state]
    Query --> Fingerprint[Build fingerprint: dispatch count, pend jobid set, preempt count]
    Fingerprint --> Compare{Matches active collapsed range}
    Compare -->|Yes| Extend[Extend active range silently]
    Compare -->|No| FlushCall[Call round rollup flush]
    FlushCall --> Begin[Begin new collapsed range with this round]
    Extend --> NextRound[Advance to next round]
    Begin --> NextRound
    StructuralEvent[Structural event classified in repl_break.c] --> FlushCall
    BreakpointHit[Breakpoint hit detected in repl_break.c] --> FlushCall
    ReplayComplete[Replay completion in repl.c] --> FlushCall
    FlushCall --> PrintRange[Print the flushed range, if one was open]

Architecture Integration:

  • Selected pattern: a state accumulator attached to the existing round-boundary hook (xce_repl_post_session_check), exposing one shared flush entry point that repl_break.c's event/breakpoint/decision hooks and repl.c's replay-completion hook all call — no new hook call sites added to sim_loop.c.
  • Domain/feature boundaries: the Round Rollup's state and flush/print logic live in repl_rollup.c (split out of repl.c post-review — see Correction below); repl_break.c calls the flush entry point but does not own or mutate the collapse state directly; sim_loop.c only gains a mode gate on its two existing calls.
  • Existing patterns preserved: repl.h's sim->interactive-gated inline-wrapper convention; repl.c's shared-helper convention (repl_format_ts_human, repl_split_tokens) extended with new shared helpers (range compression and the resource-formatting helpers, now in repl_fmt.c per the task 7.2 Correction below; and the round-rollup flush entry point, now implemented in repl_rollup.c per the task 2.2 Correction below), declared in repl_internal.h for repl.c/repl_break.c/repl_info.c/repl_params.c to call.
  • New components rationale: deciding "print now vs. extend silently" requires comparing against the previous round's fingerprint, which must persist across separate xce_repl_post_session_check() invocations — hence new persistent fields on xce_sched_repl_state_s rather than function-local state. The flush entry point exists because three independent call sites (round boundary, breakpoint hit, replay completion) across two files all need to end an active range the same way.
  • Steering compliance: one domain per file preserved; snake_case/repl_* naming conventions followed throughout. Correction (post-review, task 2.2, supersedes "no new production .c file needed" above): task 2.2's first pass kept the Round Rollup entirely inside repl.c, per this bullet's original text. That pushed repl.c to 1160 lines — 45% over this project's 800-line-per-file cap (src/scheduler/CLAUDE.md) — while review confirmed the logic itself was correct. The Round Rollup's fingerprint-build/pend-jobid-query/emit/flush/round-process functions (repl_jobid_u64_cmp through repl_round_rollup_process) now live in a new src/scheduler/src/repl_rollup.c, following the exact domain-split precedent this same file family already established for repl_break.c/repl_info.c/repl_params.c (repl_internal.h's own "Why a split at all" section) — one more sibling sharing state via repl_internal.h, not a departure from that pattern. repl.c's repl_count_jobs() becomes non-static so repl_rollup.c's pend-jobid query can call it; both it and repl_round_rollup_process() gain extern declarations in repl_internal.h. repl.c's own wiring calls (xce_repl_post_session_check()/xce_repl_on_replay_complete()) are unchanged. Tasks 6 (REPL Prompt Context) and 9 (Help & Breakpoint Discoverability/BP_KIND_TABLE) remain correctly targeted at repl.c — different domains (prompt construction, help/breakpoint-table rendering) with no Round Rollup logic of their own, unaffected by this correction.

Technology Stack

Layer Choice / Version Role in Feature Notes
Scheduler daemon (C, gnu17) existing src/scheduler/ toolchain, no new dependency All logic lives in existing REPL/sim-driver translation units Matches tech.md; no new library, no CGO/Go involvement

File Structure Plan

Modified Files

Correction (post-review, task 2.2): keeping the Round Rollup's logic inside repl.c, as the original repl.c/repl_internal.h entries below described, pushed that file to 1160 lines — 45% over this project's 800-line cap (src/scheduler/CLAUDE.md) — even though review confirmed the logic itself was correct. The Round Rollup's fingerprint/query/emit/flush/process functions now live in a new src/scheduler/src/repl_rollup.c instead, matching this exact file family's established domain-split precedent (repl_break.c/repl_info.c/repl_params.c already split out of repl.c by domain, per repl_internal.h's own "Why a split at all" section) rather than growing repl.c further. The repl.c and repl_internal.h entries below, and the new repl_rollup.c entry, reflect this. Tasks 6 (REPL Prompt Context) and 9 (Help & Breakpoint Discoverability) are unaffected — both remain correctly targeted at repl.c, a different domain with no Round Rollup logic of its own.

Accepted residual (post-review, task 2.2): after the split, repl.c settled at 808 lines — 1% over the 800-line cap, down from the original 1160-line (45% over) violation. Verified empirically (not assumed): trimming the two doc comments this task added to their practical minimum recovers only 9 of the needed 17+ lines, and deleting both entirely would still leave 801. Getting strictly under 800 would require another file split for single-digit lines, which is disproportionate churn given this codebase already tolerates comparable or larger legacy exceptions (scheduler.c at 1382 lines, plugins/builtin_fcfs.c at 1026 lines, both noted in src/scheduler/CLAUDE.md as "do not use... as precedent for new files" but tolerated in place). 808 lines is accepted as this task's final state; if tasks 6/9's later repl.c additions push it meaningfully further over, revisit with a fresh split decision at that point rather than trimming comments further now.

Correction (post-review, task 7.2): task 7.2's Resource Value Formatter helpers (repl_is_bytes_valued_resource()/repl_format_bytes_human()) landed directly in repl.c, exactly as this section's original text directed, pushing it to 929 lines — 16.1% over the 800-line cap (src/scheduler/CLAUDE.md) — while review confirmed the logic itself was correct. Unlike task 6.2's repl_build_prompt()/repl_prompt.c extraction attempt (tried and reverted — see the REPL Prompt Context component's own "Accepted residual" below — because even after absorbing 19 test files' worth of new #include churn, repl.c still only dropped to 814 lines, still over cap, for a 15-line function), this remediation moves a materially larger, genuinely cohesive block: repl_format_ts_human() (pre-existing, bug-1587), the entire Jobid Range Compressor (xce_repl_jobid_group_t/repl_jobid_groups_free/repl_jobid_group_add_idx/repl_jobid_idx_cmp/repl_jr_appendf/repl_fmt_jobid_range, task 1.2), and this task's repl_is_bytes_valued_resource()/repl_format_bytes_human() — three groups of pure, read-only formatting helpers this file family's own shared-helper convention already treated as a natural unit (none of them depend on xce_sched_sim_driver_t/xce_sched_core_t or any REPL session state) — into a new src/scheduler/src/repl_fmt.c, following the same repl_rollup.c domain-split precedent (task 2.2's Correction above). This achieves genuine compliance: repl.c settles at 587 lines, well under the 800-line cap, and repl_fmt.c itself is a self-contained 402-line file. repl_internal.h's existing extern declarations for all of these functions are unchanged (only their doc comments now note the new defining file) — only which .c file DEFINES them changes, so every caller elsewhere in repl.c/repl_break.c/repl_info.c/repl_params.c keeps working unmodified. Added to both scheduler_srcs and sched_sim_isolation_lib's source list in src/scheduler/meson.build, immediately after repl.c (matching repl_rollup.c's own placement); every white-box test file that #includes repl.c directly (20 files, confirmed via grep -rl '#include "\.\./src/repl\.c"' src/scheduler/testsuite/) also gains a matching #include "../src/repl_fmt.c" line immediately after its existing #include "../src/repl.c"; test_sim_isolation.c gains a matching XCE_SIM_REPL_FMT_C_PATH-driven read/strip/assert/free block in both test_sim_never_includes_forbidden_headers and test_sim_never_calls_forbidden_symbols, mirroring the existing repl_rollup_c/repl_params_c pattern (repl_rollup.c was already covered by this scan since task 2.2's own commit; the only gap found was in the separate test_sim_source_paths_are_readable diagnostic sanity-check function, which does not affect actual isolation coverage and was left as-is). sched_repl_output_compression_test/sched_repl_resource_formatting_test (tasks 1.1/7.1) need no #include change: per bug-1762's fix, both already link the real scheduler_srcs object list rather than white-box #include-ing repl.c, so they pick up repl_fmt.c's new definitions automatically once it is added to scheduler_srcs.

  • src/scheduler/src/repl_internal.h — extend xce_sched_repl_state_s with round-collapse state fields (see Data Models), a new units_mode field (Requirement 6), and declare the new shared jobid-range-compression helper, resource-formatting helpers, and the BP_KIND_TABLE accessor (Requirement 7); post-review (Correction above), also declares repl_count_jobs() and repl_round_rollup_process() extern so repl_rollup.c can call the former and repl.c can call the latter. Post-review (task 7.2 Correction above): the extern declarations for repl_format_ts_human()/repl_fmt_jobid_range()/repl_is_bytes_valued_resource()/repl_format_bytes_human() are unchanged in shape or signature — only their doc comments were updated to point at repl_fmt.c as the new defining file instead of repl.c.
  • src/scheduler/src/repl_break.c — xce_repl_on_event() gains routine-vs-structural event classification gated on rs->mode (Requirement 1); repl_print_event_summary()'s JOB_PUSH-style cases adopt the jobid-range-compression helper (Requirement 4); xce_repl_on_event(), xce_repl_on_begin_round(), and xce_repl_on_decision() each call the Round Rollup's flush entry point before printing a structural-event line or before re-entering repl_command_loop() on a breakpoint hit (Requirement 3.5, 3.6); repl_cmd_break()'s unrecognized-event-type error gains the full valid-event-type listing, rendered via a new shared accessor over the existing BP_KIND_TABLE (Requirement 7.5); repl_cmd_delete() already fixed independently (bug-1761, strtol+endptr validation) — no further change needed here for this spec.
  • src/scheduler/src/repl.c — xce_repl_post_session_check() calls the Round Rollup's round-boundary fingerprint/collapse logic, and xce_repl_on_replay_complete() calls its flush entry point before printing the existing replay-complete banner (Requirements 2, 3; post-review Correction above: the Round Rollup's own logic — fingerprint build, collapse, flush, emit — now lives in the new repl_rollup.c below, not here); repl_command_loop()'s readline(XCE_REPL_PROMPT) call gains a per-iteration dynamically-built prompt (Requirement 5.5); repl_print_help() is rewritten into labeled, consistently-aligned sections, info config moves next to the other info lines, and the break line grows the same BP_KIND_TABLE-derived event-type listing repl_break.c's error message uses (Requirement 7). Correction (post-review, task 7.2, above): this bullet's original text also claimed repl.c "implements the new shared jobid-range-compression helper (Requirement 4), following the file's existing shared-helper convention" — that helper, repl_format_ts_human(), and this task's own repl_is_bytes_valued_resource()/repl_format_bytes_human() now live in the new repl_fmt.c below instead, not here.
  • src/scheduler/src/repl_rollup.c (new, post-review — Correction above) — the Round Rollup's own fingerprint-build/pend-jobid-query/emit/flush/round-boundary-process logic (Requirements 2, 3), split out of repl.c to stay under the 800-line cap; shares state with repl.c via repl_internal.h, matching repl_break.c/repl_info.c/repl_params.c's established split; added to scheduler_srcs in src/scheduler/meson.build immediately after repl.c.
  • src/scheduler/src/repl_fmt.c (new, post-review, task 7.2 — Correction above) — repl_format_ts_human() (pre-existing), the Jobid Range Compressor's repl_fmt_jobid_range() and its private grouping helpers (Requirement 4), and the Resource Value Formatter's repl_is_bytes_valued_resource()/repl_format_bytes_human() (Requirement 6.1, 6.2) — three groups of pure, read-only formatting helpers with zero dependency on sim/core/repl state, split out of repl.c to stay under the 800-line cap; shares declarations with repl.c/repl_break.c/repl_info.c/repl_params.c via repl_internal.h (whose existing extern entries for these functions are unchanged); added to both scheduler_srcs and sched_sim_isolation_lib's source list in src/scheduler/meson.build, immediately after repl.c.
  • src/scheduler/src/repl_info.c — new resource-value classification/scaling helpers (byte-denominated list, swp's megabyte special case, whole-number formatting) and a table/grouping rewrite of repl_info_worker()'s and repl_info_resource()'s resource-listing loops, both reading rs->units_mode (Requirement 6); the assigned-job listing in repl_info_worker() adopts the jobid-range-compression helper (Requirement 4.5).
  • src/scheduler/src/repl_params.c — repl_cmd_set() gains a units raw|human|both branch alongside the existing speed/output/verify branches, storing the selection on the new rs->units_mode field (Requirement 6.2, 6.3).
  • src/scheduler/src/sim_loop.c — the existing call site invoking sim_print_decision() gains a continue-mode gate, mirroring repl.h's inline-wrapper convention (Requirements 2, 5). Correction (post-review, task 4.2 remediation): this bullet's original "the existing call site" (singular) undercounted the file's actual production call sites — sim_loop.c has TWO direct calls to sim_print_decision(), sched_sim_driver_run()'s main decision-drain loop (this bullet's original target) AND sim_reconcile_unaccounted_jobs()'s reconstructed-decision report (added by the sibling scheduler-sim-decision-parity/scheduler-sim-eager-reconciliation specs, unrelated in authorship but real production code that reports DISPATCH decisions through this exact channel). Since that reconciliation path is a primary source of DISPATCH decisions surfacing in replay mode, leaving it unguarded reproduced bug-1610's own duplication there too. Both call sites now route through sim_print_decision_gate() — see the sim_print_decision Gate component's own Correction below for the updated Boundary Commitment.
  • src/scheduler/include/repl.h — no functional change expected; existing inline wrappers already cover the gating points this feature needs.

New/Modified Test Files

Correction (post-review, task 2.1): test_repl_execution_control.c was already over this project's 800-line cap (937 lines) before this feature's work began. Landing task 2.1's Round Rollup tests there per the original plan below pushed it to 1563 lines — nearly double the cap, and past the ~1000-line threshold this exact spec family already treats as a split trigger (test_repl_snapshot_params.c was split out of test_repl_breakpoints_inspection.c for precisely this reason; see that file's own meson.build comment). Round Rollup's tests (task 2.1, and any Round-Rollup-internal portion of later tasks, e.g. Requirement 3.5's flush-on-structural-event/breakpoint interaction) now live in a new src/scheduler/testsuite/test_repl_round_rollup.c instead, duplicating (not sharing) whatever pre-existing static fixture helpers it needs — the established convention this same file pair (test_repl_breakpoints_inspection.c/test_repl_snapshot_params.c) already follows, rather than factoring out a shared test-helper header. test_repl_execution_control.c remains at its restored 937-line pre-feature size (still over cap on its own, a pre-existing condition this feature doesn't need to fix) and is the target only for the Requirement 5 no-regression checks, the new prompt-content tests (Requirement 5.5), and help-listing structure tests (Requirement 7.1-7.4) — re-check its line count before landing each of those; if it approaches ~1000+ lines again, split further rather than growing past it.

Correction (post-review, task 3.1): applying the same reasoning that split Round Rollup's tests out in task 2.1 — Requirement 1's Event Echo Gate tests are comparable in scope (4 behavioral cases: routine-event suppression, step-mode-unaffected, structural-event-ends-range, breakpoint-ends-range) and would push the already-over-cap test_repl_execution_control.c (937 lines) well past 1300+ lines. They instead live in a new src/scheduler/testsuite/test_repl_event_echo.c, duplicating pre-existing fixture helpers per this file family's established convention, decided proactively before implementation rather than after a review round.

  • src/scheduler/testsuite/test_repl_execution_control.c — the Requirement 5 no-regression checks (this file already drives sched_sim_driver_run() end-to-end and captures real process output via child_continue_then_inspect-style helpers); also gains the new prompt-content tests (Requirement 5.5) and help-listing structure tests (Requirement 7.1-7.4) — the natural home given it already exercises readline()/prompt-adjacent behavior and no help test exists anywhere today.
  • src/scheduler/testsuite/test_repl_round_rollup.c (new, per the task 2.1 Correction above) — child-process output-capture tests for Requirements 2 and 3 (per-round decision summary, collapsing, the shared flush entry point, and its call from xce_repl_on_replay_complete).
  • src/scheduler/testsuite/test_repl_event_echo.c (new, per the task 3.1 Correction above) — child-process output-capture tests for Requirement 1 (mode-aware event echo classification, ending an active collapsed range on structural events/breakpoint hits).
  • src/scheduler/testsuite/test_repl_output_compression.c (new) — focused unit tests for the jobid-range-compression helper (Requirement 4): a pure function, tested without the process harness.
  • src/scheduler/testsuite/test_repl_resource_formatting.c (new) — focused unit tests for the byte/megabyte classification and adaptive human-scaling helpers (Requirement 6.1, 6.2, 6.5): pure functions, tested without the process harness, mirroring test_repl_output_compression.c's pattern.
  • src/scheduler/testsuite/test_repl_breakpoints_inspection.c — extended with break's enriched unrecognized-event-type error test (Requirement 7.5, alongside the existing test_break_unrecognized_event_type_rejected).

Correction (post-review, task 8.1): info worker/info resource table-layout and units-mode-display tests (Requirement 6.4-6.6) do NOT extend test_repl_breakpoints_inspection.c as this section originally directed — landing them there first pushed it to 3215 lines (3.2x this exact file family's own ~1000-line split trigger, the same threshold that already justified splitting test_repl_round_rollup.c/test_repl_event_echo.c out of related files). They instead live in a new src/scheduler/testsuite/test_repl_resource_table.c (1374 lines), duplicating the same pre-existing fixture helpers (make_job, capture_stdout_around, capture_dispatch, etc.) this file family's established convention already uses, decided proactively per this spec's now-standard practice rather than after a second review round.

  • src/scheduler/testsuite/test_repl_resource_table.c (new, per the Correction above) — info worker's/info resource's resource-listing table/grouping layout, units_mode-aware rendering, and info worker's assigned-job-listing jobid-range-compression adoption (Requirements 6.4, 6.5, 6.6, 4.5).
  • src/scheduler/testsuite/test_repl_snapshot_params.c — extended with set units raw|human|both parsing/validation tests (Requirement 6.2, 6.3), alongside its existing set speed/set output/set verify tests.

Correction (post-review, task 9.1): help-listing structure tests (Requirement 7.1-7.4) do NOT land in test_repl_execution_control.c as this section originally directed (line 123 above). That file was already at 949 lines going into task 9.1 — within reach of the ~1000-line split trigger this same Correction note (task 2.1) told this task to watch for — and the new section/column/event-type-listing tests would have pushed it to ~1500 lines. They instead live in a new src/scheduler/testsuite/test_repl_help.c (555 lines), duplicating pre-existing fixture helpers (capture_stdout_around) per this file family's established convention, decided proactively before landing rather than after a review round — the same practice tasks 2.1, 3.1, and 8.1 already established. test_repl_execution_control.c remains untouched by task 9.1 and stays the designated home only for the Requirement 5 no-regression checks and the prompt-content tests (Requirement 5.5) it already carries. break's own enriched unrecognized-event-type error test (Requirement 7.5) landed in test_repl_breakpoints_inspection.c exactly as line 128 above already specified — no correction needed there.

  • src/scheduler/testsuite/test_repl_help.c (new, per the Correction above) — help's four labeled sections in order, one consistent column width across all listed commands, info config's placement in State Inspection, set units's placement in Snapshot & Parameters, and the break line's complete BP_KIND_TABLE-derived event-type listing (Requirements 7.1, 7.2, 7.3, 7.4).

System Flows

See the Architecture Pattern & Boundary Map diagram above — it is the only non-trivial flow this feature introduces (a per-round branching decision with two exit points: silent extension, or flush-and-restart). Per-event classification (Requirement 1) is a simple, non-branching gate and does not warrant a separate diagram. The three later-added components (REPL Prompt Context, Resource Value Formatter, Help & Breakpoint Discoverability) are likewise simple, single-purpose lookups/formatters with no multi-step state machine of their own — a name-list check, an adaptive-unit loop, and a table lookup, respectively — so none of them warrant a separate diagram either.

Requirements Traceability

Requirement Summary Components Interfaces Flows
1.1, 1.2, 1.3, 1.4 Mode-aware event echo Event Echo Gate xce_repl_on_event n/a (per-event, upstream of round boundary)
2.1, 2.2, 2.3, 2.4 Per-round decision summary Round Rollup xce_repl_post_session_check, state-derived pend query Round-boundary flow
3.1, 3.2, 3.3, 3.4 Collapsing repetitive rounds (idle/unchanged detection, range content) Round Rollup (collapse state) fingerprint compare / flush Round-boundary flow
3.5, 3.6 Ending an active collapsed range on outcome change, structural event, breakpoint hit, or replay completion Round Rollup (flush entry point, and its own call from xce_repl_on_replay_complete), Event Echo Gate (calls it on structural events/breakpoint hits) repl_round_collapse_flush Round-boundary flow
4.1, 4.2, 4.3, 4.4, 4.5 Job identifier range compression Jobid Range Compressor repl_fmt_jobid_range n/a (pure formatting)
5.1, 5.2, 5.3, 5.4 No regression to step mode / other commands, outside this feature's explicit exceptions Event Echo Gate, sim_print_decision gate mode checks n/a
5.5 REPL prompt shows session/timestamp REPL Prompt Context repl_build_prompt n/a (per-iteration, upstream of dispatch)
6.1, 6.2, 6.3, 6.4, 6.5 Human-readable resource value formatting, set units option Resource Value Formatter repl_is_bytes_valued_resource, repl_format_bytes_human, repl_cmd_set's units branch n/a (pure formatting + state)
6.6 info worker/info resource table/grouping layout Resource Value Formatter repl_info_worker, repl_info_resource n/a (rendering only)
7.1, 7.2, 7.3 help listing reorganized into labeled, aligned sections Help & Breakpoint Discoverability repl_print_help n/a (rendering only)
7.4, 7.5, 7.6 break event types discoverable in help and its error message; no other command's existence, parsing, or output changes Help & Breakpoint Discoverability repl_bp_kind_names (shared accessor over BP_KIND_TABLE) n/a (pure formatting)

Components and Interfaces

Component Domain/Layer Intent Req Coverage Key Dependencies (P0/P1) Contracts
Event Echo Gate REPL / repl_break.c Classify each event as routine or structural and gate the per-event echo by mode 1.1-1.4 rs->mode (P0), Round Rollup flush entry point (P0) State
Round Rollup REPL / repl_rollup.c (post-review; see Correction, was repl.c) Build a per-round fingerprint, print it, or extend/flush a collapsed range; expose a flush entry point callable from breakpoint/structural-event/replay-complete sites 2.1-2.4, 3.1-3.6 core->jobs query (P0), existing decision tally (P0) State, Service
Jobid Range Compressor REPL / repl_fmt.c (shared helper; post-review, task 7.2 — was repl.c, see Correction) Compress array-job identifier lists into index ranges 4.1-4.5 fmt_jobid_ex decomposition (P0) Service
sim_print_decision gate Sim driver / sim_loop.c Suppress the direct per-decision print when the Round Rollup will report it instead 2.3, 5.1 REPL mode state (P0) State
REPL Prompt Context REPL / repl.c Build the readline() prompt string from the current session and timestamp, falling back to the plain prompt before any session begins 5.5 rs->session_count (P0), xce_core_now/repl_format_ts_human (P0) Service
Resource Value Formatter REPL / repl_info.c, repl_params.c (consumers; the two shared pure helpers themselves live in repl_fmt.c, post-review, task 7.2 — was repl.c, see Correction) Classify a resource's canonical wire unit (bytes/megabytes/unitless), render it per the operator's units_mode, and lay out info worker/info resource's listings as a grouped table 6.1-6.6 is_bytes_valued_resource/format_bytes_human (test.c) as design precedent (P0), rs->units_mode (P0) Service, State
Help & Breakpoint Discoverability REPL / repl.c, repl_break.c Group and align help's listing, and expose BP_KIND_TABLE's full valid-event-type set to both help and break's error message via one shared accessor 7.1-7.6 BP_KIND_TABLE (P0) Service

REPL Output (repl.c / repl_break.c / repl_info.c / repl_params.c / sim_loop.c)

Event Echo Gate

Field Detail
Intent Classify each decoded event and suppress its individual echo line under continue, while breakpoint matching still evaluates every event
Requirements 1.1, 1.2, 1.3, 1.4

Responsibilities & Constraints

  • Structural events (always echoed, any mode): CONFIG_PUSH, JOBS_PUSH, WORKERS_PUSH, RESOURCES_PUSH, RESVALS_FULL — population/config arrivals an operator needs to see even while continue is otherwise quiet. Before printing a structural event's line, this gate calls the Round Rollup's flush entry point so any active collapsed range is closed and reported first (Requirement 3.5) — a structural event and a stale collapsed range must never interleave out of order.
  • Routine events (echoed only in step mode): RESVALS_DELTA, JOB_STATE_CHANGE, JOB_UPDATE, QUEUE_UPDATE, JOB_RUSAGE. JOB_STATE_CHANGE is classified routine deliberately — the Round Rollup (Requirement 2.4) already surfaces job status transitions at round granularity, so an individual JOB_STATE_CHANGE line under continue would duplicate that information.
  • Breakpoint matching (repl_find_event_breakpoint, plus the equivalent checks in xce_repl_on_begin_round/xce_repl_on_decision) runs on every event regardless of echo suppression — Requirement 1.4 is unaffected by this gate. On a match, before re-entering repl_command_loop(), the matching hook calls the Round Rollup's flush entry point so the operator sees the collapsed range's final state before the prompt returns (Requirement 3.6).

Accepted residual (post-review, task 3.2): implementing this gate (classifier + 3 flush call sites + JOBS_PUSH's range-compression adoption) grew repl_break.c from 827 to 905 lines (13% over the 800-line cap; the file was already 827/3.4% over before this task, a pre-existing condition unrelated to this spec). Unlike Round Rollup's task 2.2, this logic is wiring inside repl_break.c's own pre-existing driver-facing hooks (xce_repl_on_event/xce_repl_on_begin_round/xce_repl_on_decision/repl_print_event_summary) rather than a relocatable standalone subsystem — there is no cohesive block to extract into a sibling file without fragmenting those functions. Accepted in place, same reasoning as repl.c's task-2.2 residual above.

Dependencies

  • Inbound: xce_repl_on_event — calls this gate before its existing breakpoint check (P0)
  • Outbound: repl_print_event_summary — invoked only when the gate allows it (P0); Round Rollup's flush entry point — called before a structural-event line or a breakpoint-triggered command-loop re-entry (P0)

Contracts: Service [ ] / API [ ] / Event [ ] / Batch [ ] / State [x]

State Management
  • State model: reads sim->repl_state->mode (existing field); no new state owned by this component.
  • Persistence & consistency: none — a per-call decision.
  • Concurrency strategy: none required — single-threaded sim driver, same as every other REPL hook.

Round Rollup

Field Detail
Intent Once per completed round in continue mode, compute a fingerprint and either extend an active collapsed range or flush it and begin reporting the new state
Requirements 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6
File src/scheduler/src/repl_rollup.c (post-review, task 2.2 — was repl.c; see the Architecture Integration / Modified Files Corrections above for why)

Responsibilities & Constraints

  • Active only while sim->interactive && rs->mode == XCE_REPL_MODE_CONTINUE; step mode keeps today's full per-decision path unchanged (Requirement 2.3, 5.1).
  • Pend-jobid set is sourced by directly querying core->jobs for JOB_IS_PEND state (same iteration idiom as repl_info_jobs()) — not solely from the decision tally, so Requirement 2.4 holds independent of bug-1611 (see research.md Design Decisions).
  • Dispatch/preempt counts are sourced from the existing last_n_dispatch/last_n_preempt tally.
  • Must flush (print) any active collapsed range before: a breakpoint-triggered re-entry into repl_command_loop(), replay completion, or a fingerprint change from the active range (Requirement 3.5, 3.6). Because the breakpoint and structural-event triggers originate in repl_break.c, not repl.c, this component exposes a flush entry point other files call rather than owning every trigger site itself.

Dependencies

  • Inbound: xce_repl_post_session_check (repl.c) — the round-boundary call site the fingerprint/collapse logic attaches to, calling repl_rollup.c's repl_round_rollup_process() (P0); the Event Echo Gate (xce_repl_on_event) and the breakpoint checks in xce_repl_on_begin_round/xce_repl_on_decision (all repl_break.c) — call the flush entry point (P0); xce_repl_on_replay_complete (repl.c) — calls the flush entry point before its existing banner (P0). All three inbound call sites live in a different file from this component's own implementation (post-review, task 2.2 — see Correction above) and reach it via repl_internal.h's extern declarations.
  • Outbound: core->jobs iterator (P0), repl.c's repl_count_jobs() (P0, extern-declared in repl_internal.h post-review for this cross-file call), xce_repl_on_decision's existing tally fields (P0), Jobid Range Compressor (repl_fmt.c, post-review, task 7.2 — was repl.c, see File Structure Plan Correction) for rendering the pend-jobid list (P0)

Contracts: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [x]

Service Interface
/*
 * Ends the active collapsed round range, if one is open, printing its
 * summary line immediately. Safe to call when no range is active (a
 * no-op). Called from the round boundary on a fingerprint change, and
 * from repl_break.c before a structural-event line or a breakpoint-
 * triggered command-loop re-entry, and from xce_repl_on_replay_complete
 * before its existing banner.
 */
void repl_round_collapse_flush(xce_sched_sim_driver_t* sim);
  • Preconditions: sim/sim->repl_state non-NULL (same "safe no-op on NULL" discipline every other REPL hook in this file family already follows).
  • Postconditions: no collapsed range is active after the call; if one was active, its summary line has been printed exactly once.
  • Invariants: idempotent — calling it again immediately afterward with no intervening round is a no-op, never a duplicate print.
State Management
  • State model: new fields on xce_sched_repl_state_s hold the active collapsed range (see Data Models) — whether one is open, its round/timestamp bounds, and its cached fingerprint.
  • Persistence & consistency: in-memory, scoped to one REPL session; zero-initialized for free by repl_state_ensure()'s existing calloc.
  • Concurrency strategy: none required — single-threaded sim driver.

Jobid Range Compressor

Field Detail
Intent Group a list of jobids by array base and render contiguous index runs as compressed ranges
Requirements 4.1, 4.2, 4.3, 4.4, 4.5
File src/scheduler/src/repl_fmt.c (post-review, task 7.2 — was repl.c; see the File Structure Plan Correction above for why)

Responsibilities & Constraints

  • Pure, read-only formatting function — never mutates its input list or any scheduler/job state.
  • Shared by repl_print_event_summary()'s job-arrival listings, the Round Rollup's pend-jobid display, and repl_info_worker()'s assigned-job listing (Requirement 4.5) — the same helper, no per-caller variant.

Dependencies

  • Inbound: repl_print_event_summary (P0), Round Rollup (P0), repl_info_worker (repl_info.c) (P0)
  • Outbound: fmt_jobid_ex (lib/utils.c) — reuses its existing (cluster, apiserver, timestamp, job[, idx]) decomposition (P0)

Contracts: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]

Service Interface
/*
 * Formats `njobids` raw jobids into `buf` (bufsz-bounded): groups by
 * array base (cluster, apiserver, timestamp, job) and compresses
 * contiguous [idx] runs into [lo-hi] form, comma-joining non-contiguous
 * runs within one base; non-array jobids are listed individually.
 * Returns the number of bytes written (excluding the NUL terminator),
 * or a negative value if buf was too small to hold the full listing.
 */
int repl_fmt_jobid_range(char* buf, size_t bufsz,
                          const uint64_t* jobids, int njobids);
  • Preconditions: jobids non-NULL whenever njobids > 0; buf/bufsz describe a valid, writable buffer.
  • Postconditions: buf holds a NUL-terminated, comma-separated listing; distinct array bases appear in input order, indices ascend within each base's compressed ranges.
  • Invariants: never fabricates or silently drops a jobid — the compressed listing always represents exactly the same id set the uncompressed listing would show.

sim_print_decision Gate

Field Detail
Intent Suppress sim_print_decision()'s direct per-decision stdout line when the Round Rollup will report the round instead
Requirements 2.3, 5.1

Responsibilities & Constraints

  • Skip the direct print only when sim->interactive && sim->repl_state->mode == XCE_REPL_MODE_CONTINUE; step mode and non-interactive (headless) runs are unaffected (Requirement 5.1, 5.4).
  • Implemented as an inline-wrapper-style gate, consistent with repl.h's established convention, rather than a bare if block added to each call site's body.

Correction (post-review, task 4.2 remediation): this component's original Boundary Commitment named "the existing call site in sched_sim_driver_run()" (singular). Review found sim_loop.c has a SECOND, direct, unguarded call to sim_print_decision() inside sim_reconcile_unaccounted_jobs() (added by the sibling scheduler-sim-decision-parity/scheduler-sim-eager-reconciliation specs after this design.md was originally written) — and that reconciliation path is actually a PRIMARY mechanism by which DISPATCH decisions surface in replay mode at all (most live-drain-loop activity is PEND). A round where reconciliation fires was therefore still showing both the bare per-decision line AND the Round Rollup's summary counting the same dispatch, reproducing bug-1610's own duplication via this second, previously-unaccounted-for call site. This component's gate (same function, same condition, no new symbol) now guards BOTH call sites: sched_sim_driver_run()'s main decision-drain loop (this component's original target) and sim_reconcile_unaccounted_jobs()'s reconstructed-decision report. Requirement 2.3/this feature's own Observable ("the round summary is the sole source of decision output") is only actually satisfied once both are covered — see test_sim_reconcile_unaccounted.c's two new tests (test_reconcile_continue_mode_with_repl_state_suppresses_bare_decision_line / test_reconcile_step_mode_with_repl_state_still_prints_bare_decision_line) for the second call site's own contract coverage, parallel to test_sim_print_decision_gate.c's existing coverage of the first.

Contracts: Service [ ] / API [ ] / Event [ ] / Batch [ ] / State [x]

State Management
  • State model: reads sim->interactive and sim->repl_state->mode (both existing fields); owns no new state.

REPL Prompt Context

Field Detail
Intent Build the string passed to readline() each loop iteration so it shows the current session and timestamp instead of the static "(sim) " prompt
Requirements 5.5

Responsibilities & Constraints

  • Applies in both step and continue modes — it changes the prompt, not any command's output, so it sits outside Requirement 5's "leave unchanged" boundary by design (Requirement 5.5 is one of that requirement's own named exceptions).
  • Before any session has begun (rs->session_count == 0, the same zero-initialized value repl_state_ensure()'s calloc already guarantees), falls back to the original plain "(sim) " prompt — there is no session/timestamp to show yet, and a fabricated placeholder value would be misleading.
  • Once at least one session has begun, renders "(sim session=<N> t=<human-ts>) " using rs->session_count and xce_core_now(&sim->core) formatted through the existing repl_format_ts_human() — no new timestamp source, no raw-epoch-nanosecond component (unlike the four existing t=%llu (...) print sites elsewhere in this file family, which are out of this feature's scope per Boundary Commitments). repl_format_ts_human()'s existing output is the full ISO8601 form (strftime's "%Y-%m-%dT%H:%M:%SZ", e.g. 2026-09-02T03:29:38Z), not a time-only string — the prompt reuses that format exactly rather than inventing a shorter one, since Allowed Dependencies commits to no new timestamp source.
  • Uses bare rs->session_count, matching info session's/info config's existing convention (repl_info.c:848,898) — not session_count + 1. That +1 exists only in repl_print_event_summary() (repl_break.c:611) to label events echoed mid-stream, before the session containing them has finished and incremented the count; the prompt, by contrast, is only ever shown after a step/continue has returned control (i.e. after xce_repl_on_begin_round() for the just-finished session has already run), so the bare count is the session that just completed — the same number info session would report if queried at that exact prompt. Copying the +1 convention here would make the prompt disagree with info session by one.
  • Built fresh on every repl_command_loop() iteration, immediately before the readline() call it feeds — never cached across iterations, since the session/timestamp can change between prompts.

Accepted residual (post-review, task 6.2): this task's repl_build_prompt() addition pushed repl.c to 852 lines (6.5% over the 800-line cap, up from 1% after task 2.2) — the exact trigger task 2.2's own residual note anticipated. Review empirically tried extracting repl_build_prompt() into a new repl_prompt.c (matching the repl_rollup.c precedent): it links and passes, but 19 sibling white-box test files each #include "../src/repl.c" directly and would each need a matching new #include line to keep linking, and even after absorbing that churn repl.c only drops to 814 lines — still over cap. Given the function is a small, single-purpose 15-line formatter and the split doesn't even achieve compliance, extraction was judged disproportionate and reverted; 852 lines is accepted as this task's residual. Tasks 7-9's repl_info.c/repl_params.c/repl.c (help/BP_KIND_TABLE accessor) additions are unaffected — revisit repl.c specifically only if a later task adds to it again.

Dependencies

  • Inbound: repl_command_loop() (repl.c) — calls this immediately before its existing readline(XCE_REPL_PROMPT) line
  • Outbound: repl_format_ts_human() (repl_fmt.c post-review, task 7.2 Correction above — originally repl.c), xce_core_now() (core_clock.c, existing)

Contracts: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]

Service Interface
/*
 * Builds this iteration's REPL prompt into `buf` (bufsz-bounded):
 * "(sim) " if no session has begun yet (rs->session_count == 0), else
 * "(sim session=<N> t=<ISO8601-ts>) " using the bare current session
 * count (NOT +1 -- see repl_print_event_summary()'s different mid-stream
 * convention) and xce_core_now()/repl_format_ts_human() (same
 * "%Y-%m-%dT%H:%M:%SZ" form used elsewhere, not a shortened one). Called
 * fresh from repl_command_loop() before every readline() -- never cached.
 */
void repl_build_prompt(xce_sched_sim_driver_t* sim, char* buf, size_t bufsz);
  • Preconditions: sim/sim->repl_state non-NULL (same discipline every other REPL hook in this file family follows); buf/bufsz describe a valid, writable buffer sized for the longest possible rendering (session count width + fixed-width human timestamp).
  • Postconditions: buf holds a NUL-terminated prompt string; identical across repeated calls within the same session/timestamp (a pure function of sim's current state).
  • Invariants: never blocks, never reads input, never mutates sim/sim->repl_state — a read-only formatter, same class as the Jobid Range Compressor.

Resource Value Formatter

Field Detail
Intent Classify a resource's canonical wire unit, render its value per the operator's selected display mode, and lay out info worker's/info resource's listings as a grouped table instead of a flat line-per-resource dump
Requirements 6.1, 6.2, 6.3, 6.4, 6.5, 6.6
File The classification/scaling helpers (repl_is_bytes_valued_resource()/repl_format_bytes_human()) live in src/scheduler/src/repl_fmt.c (post-review, task 7.2 — see the File Structure Plan Correction above); the table-rendering call sites (repl_info_worker()/repl_info_resource()) stay in repl_info.c, and set units' parsing stays in repl_params.c, both unaffected by this move

Responsibilities & Constraints

  • Classification is a small, explicit, hardcoded name list — repl_is_bytes_valued_resource(name) returns true for exactly mem, tmp, maxmem, maxtmp, maxswap (confirmed canonical-bytes on the wire: node_information.c's node_info_get_mem()/node_info_get_tmp() docstrings, xcecd.c's init_host_attrs() bug-1554 comment). This mirrors src/controller/testsuite/test.c's is_bytes_valued_resource() exactly (same 5 names, same rationale: "a small, explicit, client-side list rather than a wire field").
  • swp is a confirmed special case, deliberately not added to the bytes list: node_info_get_swap()'s own docstring states its canonical wire unit is megabytes, not bytes (unlike its maxswap counterpart, which the bug-1554 comment confirms IS bytes). Before formatting, swp's value is converted MB→bytes (× 1024.0 × 1024.0) and then passed through the same human-scaling formatter as the bytes-native resources, so it renders on the same adaptive B/KB/MB/GB/TB/PB scale rather than staying a raw MB float.
  • repl_format_bytes_human(bytes, buf, bufsz) adaptively scales to the smallest unit the value fits (B/KB/MB/GB/TB/PB), mirroring test.c's format_bytes_human() algorithm exactly (divide by 1024 while >= 1024.0, cap at PB).
  • Display mode (raw/human/both) is a new xce_sched_repl_state_s field, units_mode, defaulting to both (Requirement 6.3) and settable via set units raw|human|both (repl_params.c, alongside the existing speed/output/verify branches) — both renders "<raw> (<human>)", matching xce_test's own cmd_resval display convention exactly (test.c's "%f (%s)\n").
  • Numeric resources with no canonical unit (load averages, factors, counts — everything not in the bytes list and not swp) print without a unit suffix, and without a trailing .00 when the value has no fractional part — replaces the current unconditional "%.2f" in repl_info.c's Value_u_fvalue branch with a conditional format.
  • info worker's and info resource's resource-listing loops are rewritten from an unstructured per-resource fprintf into an aligned table, grouped into labeled sections where applicable to that listing (consumable/capacity resources with a used= split; load/utilization metrics; static hardware attributes such as type/model/cpuf/maxcpus/ndisks — info resource's cluster-wide GLOBAL/SHARED scope, per bug-1593, may not populate every section). Column widths are computed once (a shared field-width format), not hand-padded per line.
  • Never alters the underlying stored/wire value or the deterministic outcome of a replay (Requirement 6.4) — display-only, same discipline test.c's own format_bytes_human() doc comment states ("the stored/wire value stays raw bytes always, this never feeds back into anything").

Accepted residual (post-review, task 8.2): implementing the table/grouping rendering pushed repl_info.c from 985 to 1267 lines (58% over the 800-line cap). Unlike repl.c/repl_break.c, repl_info.c was already 23% over cap (985 lines) before this spec touched it at all — this is inherited debt, not something this feature introduced from a clean baseline. Review empirically tried the narrow extraction (moving just this task's new classification/formatting helpers — repl_classify_worker_resval_section/repl_classify_resource_def_section/repl_format_numeric_resource_value, 137 lines — into the existing repl_fmt.c): it only reaches 1130 lines, still 41% over cap, the same "doesn't achieve compliance" outcome that got task 6.2's single-function split rejected as disproportionate. Achieving genuine compliance would require extracting the entire pre-existing repl_info_worker_one()/repl_info_resource_one()/repl_info_resource() display bodies (~690 lines) into a new domain file — a materially larger structural change (new meson.build entries, ~20 test files' #include lists) restructuring code this feature doesn't otherwise touch, which is a scheduler-core-level cleanup decision, not something to fold into this feature's own scope. Accepted in place; revisit only if a future info-command spec (e.g. scheduler-sim-diagnostics) adds enough new content here to make a full domain split worth doing on its own merits.

Dependencies

  • Inbound: repl_info_worker(), repl_info_resource() (repl_info.c) (P0); repl_cmd_set() (repl_params.c) (P0)
  • Outbound: none beyond standard C formatting — a self-contained classification/formatting pair, same shape as the Jobid Range Compressor

Contracts: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [x]

Service Interface
/* Returns true iff `name`'s canonical wire unit is bytes (mem, tmp,
 * maxmem, maxtmp, maxswap only -- swp is handled separately, see above). */
bool repl_is_bytes_valued_resource(const char* name);

/* Adaptively scales `bytes` to the smallest unit it fits (B/KB/MB/GB/TB/
 * PB), writing e.g. "20.56 GB" into `buf` (bufsz-bounded). Display-only;
 * never touches the underlying stored value. */
void repl_format_bytes_human(double bytes, char* buf, size_t bufsz);
  • Preconditions: name non-NULL for the classifier; buf/bufsz describe a valid, writable buffer for the formatter.
  • Postconditions: the classifier returns a stable, name-only-dependent boolean; the formatter writes a NUL-terminated, human-readable string.
  • Invariants: both are pure functions — no side effects, no dependency on REPL session state.
State Management
  • State model: new units_mode field (enum raw/human/both) on xce_sched_repl_state_s, defaulting to both; read by the table-rendering code, written only by set units.
  • Persistence & consistency: in-memory, scoped to one REPL session. Unless the both enumerator is deliberately assigned value 0 in its enum declaration, calloc's zero-initialization will not select it — repl_state_ensure() must explicitly set units_mode = XCE_REPL_UNITS_BOTH after allocation regardless of member order, the one field in this struct that cannot rely on zero-initialization alone (called out explicitly since every other field in this struct today deliberately relies on calloc's zero value being the correct default).
  • Concurrency strategy: none required — single-threaded sim driver.

Help & Breakpoint Discoverability

Field Detail
Intent Reorganize help's listing into labeled, aligned sections, and expose break's complete valid-event-type set to both help and its own error message from one shared source
Requirements 7.1, 7.2, 7.3, 7.4, 7.5, 7.6

Responsibilities & Constraints

  • repl_print_help() (repl.c) is rewritten into four labeled sections — Execution Control (step/continue/quit/help), Breakpoints (break/info break/delete), State Inspection (info session/job/jobs/queue/worker/resource/summary/config — info config moved here, next to its siblings, per Requirement 7.3), Snapshot & Parameters (snapshot/set speed/set output/set verify/set units) — each section using one shared field-width fprintf format (e.g. " %-38s%s\n") instead of independently hand-counted spaces per line, so a future command addition can't silently break alignment the way info config's append did.
  • BP_KIND_TABLE (repl_break.c, static/file-local today) is exposed through one new shared accessor, repl_bp_kind_names(), that renders the complete comma-joined list of valid <EVENT_TYPE> tokens once. Both repl_print_help()'s break line (repl.c) and repl_cmd_break()'s unrecognized-event-type error (repl_break.c, same file as the table — a trivial local call) use this accessor rather than each hardcoding the list — the two can never drift out of sync with each other or with BP_KIND_TABLE itself (Revalidation Triggers).
  • No change to which commands exist, how any command parses its arguments, or any other command's output (Requirement 7.6) — this component only touches help's rendering and the two places that now surface BP_KIND_TABLE's contents.

Accepted residual (post-review, task 9.2): adding repl_bp_kind_names() and wiring it into repl_cmd_break()'s error message pushed repl_break.c from 905 to 949 lines — up from task 3.2's already-accepted 13%-over-cap residual to 18.6% over the 800-line cap. Review empirically tested extracting BP_KIND_TABLE/repl_bp_kind_from_token()/repl_bp_kind_names()/repl_bp_kind_for_core_event() (a self-contained lookup-table domain) into a new repl_bp_kind.c: it built and passed cleanly but only reduced repl_break.c to 838 lines — still 4.75% over cap — while requiring 22 white-box test files' #include "../src/repl_break.c" lines to gain a matching new include. Same "doesn't achieve compliance, disproportionate churn" outcome already accepted at tasks 6.2 and 8.2; the experiment was reverted and 949 lines is accepted in place. repl.c's task 9.2 growth (587 → 634 lines) remains comfortably under cap and needs no note.

Dependencies

  • Inbound: repl_print_help() (repl.c) (P0), repl_cmd_break()'s error path (repl_break.c) (P0)
  • Outbound: BP_KIND_TABLE (repl_break.c, existing) (P0)

Contracts: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]

Service Interface
/*
 * Renders BP_KIND_TABLE's complete set of valid break <EVENT_TYPE> tokens
 * as one comma-joined, NUL-terminated string into `buf` (bufsz-bounded) --
 * e.g. "CONFIG_PUSH, JOB_PUSH, ..., DECISION". Used by both
 * repl_print_help() and repl_cmd_break()'s unrecognized-event-type error
 * so the two can never disagree with each other or with BP_KIND_TABLE.
 */
void repl_bp_kind_names(char* buf, size_t bufsz);
  • Preconditions: buf/bufsz describe a valid, writable buffer sized for the full table (12 entries at the time of writing).
  • Postconditions: buf holds every BP_KIND_TABLE token, comma-joined, in table order.
  • Invariants: pure function of BP_KIND_TABLE's current contents — adding/removing a table entry changes this function's output with no other code change required.

Data Models

Domain Model

This feature adds two concepts to the REPL's existing session-scoped state (xce_sched_repl_state_s, repl_internal.h) — not a new top-level entity: a "round collapse state" (Requirements 2, 3) and an independent "resource display preference" (units_mode, Requirement 6). It follows the same pattern prior REPL task revisions used to add output_path/replay_complete to that struct.

Logical Data Model

New fields on xce_sched_repl_state_s (illustrative — exact types finalized at implementation time):

Field Type Purpose
collapse_active bool Whether a collapsed range is currently open
collapse_start_round / collapse_end_round uint64_t Round-number bounds of the active range
collapse_start_ts / collapse_end_ts uint64_t Sim-time bounds, for the collapsed range's time span (Requirement 3.4)
collapse_dispatch_count / collapse_preempt_count int Cached fingerprint counts
collapse_pend_jobids / collapse_n_pend_jobids pointer + count Cached fingerprint pend-jobid set, in whatever list form the Jobid Range Compressor consumes
units_mode enum (raw/human/both) Requirement 6's resource-value display mode, settable via set units; defaults to both

Consistency & Integrity: all fields reset together whenever a range flushes (print, then clear) or a new REPL session begins — repl_state_ensure()'s existing calloc zero-initializes new fields with no additional lifecycle code required, except units_mode: unless both is deliberately given enum value 0, calloc's zero-initialization will not select it, so repl_state_ensure() must explicitly set it to both after allocation regardless of the enum's member order — the one field in this struct where zero-initialization alone cannot be relied upon to be the correct default.

No Physical Data Model, API Data Transfer, or Event Schema subsections apply: this feature has no persistent storage, no network wire format, and adds no new wire-level contract — it stays inside scheduler-core's existing "simulation isolation" boundary (repl.h's own header comment: no scheduler.h, no client.h, no capnp types).

Error Handling

Error Strategy

Consistent with existing repl.c/repl_break.c conventions: allocation failures print an existing-style "[sim] ..." message and degrade to the safest available behavior rather than crashing or losing information, matching repl_state_ensure()'s own "never load-bearing, degrade to unattended" precedent.

Error Categories and Responses

  • Allocation failure while building a round's fingerprint or growing the collapsed-range pend-jobid buffer: fall back to printing that round's line directly (uncollapsed) rather than losing the round's information — preserves Requirement 2.1's obligation to report every round with decisions.
  • repl_fmt_jobid_range() buffer too small: returns a negative value; the caller falls back to the existing uncompressed per-id listing for that call — never truncates silently, never crashes, preserves the "never drop a jobid" invariant even in a buffer-constrained edge case.

Monitoring

No new logging framework is introduced — the existing [sim] ... stdout convention remains the only user-facing signal; this is an interactive CLI tool, not a daemon, so no zap/WARN-level logging applies here.

Testing Strategy

Unit Tests (new test_repl_output_compression.c)

  • Contiguous indices from one array base compress to a single [lo-hi] range (Requirement 4.1).
  • Non-contiguous indices compress to comma-joined sub-ranges within one base (Requirement 4.2).
  • Multiple array bases in one listing compress independently (Requirement 4.3).
  • A mixed array + non-array listing shows non-array ids individually and compresses only the array ids (Requirement 4.4).
  • A buffer too small to hold the full listing returns a negative value and writes nothing past bufsz (Error Handling).

Unit Tests (new test_repl_resource_formatting.c)

  • repl_is_bytes_valued_resource() returns true for exactly mem, tmp, maxmem, maxtmp, maxswap, and false for swp, unitless resources (e.g. r1m), and an unknown/custom name (Requirement 6.1).
  • repl_format_bytes_human() adaptively scales across the full B/KB/MB/GB/TB unit range, including exact boundary values (e.g. exactly 1024 bytes renders as 1.00 KB, not 1024.00 B) (Requirement 6.2).
  • swp's value is converted MB→bytes before formatting, so a raw swp value renders on the same adaptive scale as a byte-native resource of equivalent magnitude (Requirement 6.1).
  • A whole-number unitless value (e.g. 22.0) renders with no trailing .00; a fractional one (e.g. 0.57) still renders with its decimal places (Requirement 6.5).

Integration Tests (extend test_repl_breakpoints_inspection.c, test_repl_snapshot_params.c)

  • set units raw/set units human/set units both each select the corresponding rendering for a subsequent info worker/info resource call; an unrecognized set units value is rejected with the existing set: unrecognized parameter error shape (Requirement 6.2, 6.3).
  • info worker's resource listing renders as a grouped, aligned table with used= shown only for consumable/capacity resources, matching the hand-validated shape against a real captured worker's resource dump (Requirement 6.6).
  • info worker's assigned-job listing, given a job array's worth of contiguous indices, renders as a single compressed range via the same helper Requirement 4 already covers (Requirement 4.5).
  • break's unrecognized-event-type error message lists the complete, current BP_KIND_TABLE token set (Requirement 7.5).

Integration Tests (extend test_repl_execution_control.c, child-process output capture)

  • continue across multiple idle rounds (zero decisions) produces one collapsed range line, not one line per round (Requirement 3.1).
  • continue across rounds sharing an identical nonzero pend set produces one collapsed "unchanged" range line — the exact shape hand-validated against the real 108-round replay that motivated this spec (Requirement 3.2).
  • A structural event mid-continue ends any active collapsed range and prints its own line before continuing (Requirement 1.3, 3.5).
  • A breakpoint hit mid-collapse flushes the active range before entering the command loop (Requirement 3.6).
  • Replay completion (EOF) flushes any active collapsed range before the existing "replay complete" banner (Requirement 3.6).
  • step-mode output is unchanged versus this feature's pre-implementation baseline — regression check alongside the existing test_should_pause_fires_on_exactly_the_nth_session-style fixtures (Requirement 5.1, 5.3).
  • A job's tracked status changing between rounds is reflected in the next round summary even under a fixture that never emits a synthetic DISPATCH decision — exercises the state-derived-pend-set design decision independent of bug-1611's resolution (Requirement 2.4).
  • The REPL prompt is the plain "(sim) " before any session begins, then includes the current session count and a human-readable timestamp once at least one session has run, in both step and continue modes (Requirement 5.5).
  • help's output groups commands into the four labeled sections, uses one consistent column width throughout, lists info config alongside the other info subcommands, and includes the complete break event-type list (Requirement 7.1-7.4).

E2E

Not applicable beyond the integration tests above — this feature has no HTTP/API surface; the child-process harness already exercises the real xce_scheduler binary end-to-end at the process level.

Optional Sections

Security Considerations, Performance & Scalability, and Migration Strategy are omitted: this is a local, single-operator interactive CLI output change with no new attack surface, no persistent or high-load data path (round counts are bounded by replay length, already tolerated by existing step output), and no schema or data migration.

Supporting References

None beyond research.md — no external API, vendor library, or long schema requires separate documentation.