Full spec scaffold (requirements/design/research/tasks all approved, ready_for_implementation=true) plus a roadmap backlog item recorded from the dirty-content-fingerprint spec's task 4.3 findings (Makefile/meson fingerprint-computation duplication risk). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
77 lines
13 KiB
Markdown
77 lines
13 KiB
Markdown
# Research & Design Decisions
|
|
|
|
## Summary
|
|
- **Feature**: `scheduler-sim-repl-verbosity`
|
|
- **Discovery Scope**: Extension (existing system) — light discovery
|
|
- **Key Findings**:
|
|
- Two independent, unconditional stdout print paths cause `continue`-mode volume to match `step`-mode volume: `xce_repl_on_event()` (repl_break.c:708-731, gated only by `sim->interactive`, not by `rs->mode`) and `sim_print_decision()` (sim_loop.c:388-411, called directly from `sched_sim_driver_run()`'s main loop with no `sim->interactive`/mode gate at all).
|
|
- `xce_repl_post_session_check()` (repl.c), called once per completed round from `sched_sim_driver_run()` right after each round's decisions are drained, is the correct integration point for round-boundary rollup/collapse logic — it already runs exactly once per round when interactive, and already exists purely for the `step`-budget pause check.
|
|
- `xce_repl_on_decision()` (repl_break.c:776-799) already tallies `last_n_dispatch`/`last_n_pend`/`last_n_preempt` per round, but that tally is decision-event-driven — real replay data shows it may never observe a fresh `DISPATCH` decision in replay mode even when jobs demonstrably transition to running (bug-1611). The pend-jobid set for the round rollup must therefore be derived by directly querying `core->jobs` for `JOB_IS_PEND` state (the same iteration idiom `repl_info_jobs()` already uses), not solely from the decision tally, to satisfy Requirement 2.4 regardless of bug-1611's outcome.
|
|
- Existing test file `test_repl_execution_control.c` already drives `sched_sim_driver_run()` end-to-end via a child-process harness (`child_continue_then_inspect`, `child_continue_twice_after_complete`) that captures real process output — this is the correct home for behavioral tests of Requirements 1-3 (mode gating, rollup, collapsing), since it observes actual printed text, not just internal state.
|
|
- No new external dependency is needed — all formatting is plain C string/stdio work consistent with the rest of `repl.c`/`repl_break.c`.
|
|
|
|
## Research Log
|
|
|
|
### Extension point analysis: where does continue-mode output actually originate?
|
|
- **Context**: Requirements 1-3 need continue mode to suppress/summarize output that today prints unconditionally.
|
|
- **Sources Consulted**: `repl_break.c` (`xce_repl_on_event`, `xce_repl_on_decision`), `sim_loop.c` (`sim_print_decision`), `repl.c` (`xce_repl_post_session_check`, `repl_should_pause`), `repl.h` (interactive-gated inline wrappers), `repl_internal.h` (`xce_sched_repl_state_s`), CodeGraph call-graph for `sched_sim_driver_run`.
|
|
- **Findings**:
|
|
- `xce_repl_check_event()` (repl.h, `static inline`) gates `xce_repl_on_event()` on `sim->interactive` only — inside `xce_repl_on_event()`, `repl_print_event_summary()` fires unconditionally before any breakpoint check, with no reference to `rs->mode`.
|
|
- `sim_print_decision()` has no interactive or mode gate at all; `sched_sim_driver_run()` calls it directly for every drained decision, the same call site that also drives `xce_repl_check_decision()` (which does the tally) and `sched_record_session_add_decision()` (recording).
|
|
- `xce_repl_post_session_check()` fires once per completed round, strictly after that round's decisions are drained and before the next round's events begin — confirmed via call-graph (`sched_sim_driver_run → xce_repl_post_session_pause`).
|
|
- **Implications**: Requirement 1's gate belongs inside `xce_repl_on_event()`/`repl_print_event_summary()`. Requirement 2/3's rollup and collapsing logic belongs at the `xce_repl_post_session_check()` boundary, reading state that `xce_repl_on_decision()`'s existing tally and a direct `core->jobs` query both feed. Requirement 2/3 also requires suppressing `sim_print_decision()`'s direct per-decision print when `sim->interactive && rs->mode == XCE_REPL_MODE_CONTINUE`, since that print path is the one bug-1610 identified as unconditional.
|
|
|
|
### Job identifier range compression: existing formatting conventions
|
|
- **Context**: Requirement 4 needs contiguous array-job indices displayed as ranges.
|
|
- **Sources Consulted**: `lib/utils.c` (`fmt_jobid`/`fmt_jobid_ex`), `repl_break.c`'s `repl_print_event_summary()` (current `JOB_PUSH` jobid list rendering).
|
|
- **Findings**: `fmt_jobid_ex()` already separates the packed 64-bit jobid into `(cluster, apiserver, timestamp, job[, idx])` components via `EXTRACT_*` macros; an array job's `idx` is already isolated during formatting. There is no existing range-compression helper anywhere in the REPL formatting code — every current jobid list (the `JOB_PUSH` line, and any future round-rollup pend list) is one `fmt_jobid()` call per id.
|
|
- **Implications**: A new helper is needed; the natural base for grouping is the already-decoded `(cluster, apiserver, timestamp, job)` tuple (identical for every element of one array submission), with only `idx` varying. No change to `fmt_jobid`/`fmt_jobid_ex` themselves — the new helper wraps a *list* of ids, not a single id.
|
|
|
|
## Architecture Pattern Evaluation
|
|
|
|
| Option | Description | Strengths | Risks / Limitations | Notes |
|
|
|--------|-------------|-----------|---------------------|-------|
|
|
| Gate at existing hook boundaries (chosen) | Add mode/collapse logic inside `xce_repl_on_event`, `sim_print_decision`'s call site, and `xce_repl_post_session_check` | Reuses every existing per-round/per-event integration point; no new hook call sites needed (sim_loop.c already at its documented line-budget ceiling per repl.h's own header comment) | Requires touching two files (`repl_break.c`, `sim_loop.c`) instead of one | Matches this codebase's own precedent (repl.h's inline-wrapper gating) |
|
|
| New dedicated "output policy" module | Introduce a separate `repl_output.c` owning all print decisions, called from a single new hook | Cleaner separation of concerns | Adds a 6th call site into `sim_loop.c`'s round loop, which repl.h's own header comment says there is no line budget for; larger diff for a formatting-only feature | Rejected — over-engineered for the boundary this spec owns |
|
|
|
|
## Design Decisions
|
|
|
|
### Decision: Round rollup's pend-jobid set is state-derived, not decision-tally-derived
|
|
- **Context**: Requirement 2.4 requires the round summary to reflect real job status changes even though bug-1611 shows the decision tally may never register a fresh `DISPATCH` in replay mode.
|
|
- **Alternatives Considered**:
|
|
1. Trust `xce_repl_on_decision()`'s existing `last_n_dispatch`/`last_n_pend`/`last_n_preempt` tally directly for the pend-jobid set — simplest, but inherits bug-1611's blind spot if it turns out to be a real gap rather than benign replay semantics.
|
|
2. Derive the pend-jobid set each round by directly iterating `core->jobs` for `JOB_IS_PEND` state (same idiom as `repl_info_jobs()`), independent of the decision tally.
|
|
- **Selected Approach**: Option 2 for the pend-jobid set; the existing tally is still used for the dispatch/preempt *counts* shown in the rollup header, since bug-1611 only questions dispatch visibility and preempt is unaffected.
|
|
- **Rationale**: Requirement 2.4 must hold regardless of how bug-1611 resolves. A state query is authoritative by construction; a decision-tally is only as complete as whatever decisions the replay happens to (re-)generate.
|
|
- **Trade-offs**: One extra `core->jobs` iteration per round in continue mode (bounded by job count, not a hot path — replay/step already tolerates equivalent iteration cost in `repl_info_jobs()`/`repl_count_jobs()`). If bug-1611 is later confirmed a real gap, the dispatch *count* shown in the rollup may still under-report until that separate bug is fixed — flagged as a known limitation, not silently masked.
|
|
- **Follow-up**: bug-1611 should be investigated (separately from this spec) before or during implementation; if it proves to be a real visibility gap rather than benign replay semantics, a follow-up task should switch the dispatch count to the same state-diff approach used for the pend set.
|
|
|
|
### Decision: Collapse fingerprint keys on (dispatch_count, pend_jobid_set, preempt_count), not raw decision objects
|
|
- **Context**: Requirement 3.2 needs to detect when consecutive rounds are "the same" to collapse them.
|
|
- **Alternatives Considered**:
|
|
1. Compare full decision-object lists round to round.
|
|
2. Compare a compact fingerprint: dispatch count + preempt count + the state-derived pend-jobid set (as an ordered/canonical list).
|
|
- **Selected Approach**: Option 2.
|
|
- **Rationale**: The real replay validated this exact fingerprint shape — 65 consecutive rounds shared an identical `(0, {8 jobids from base A + 6 jobids from base B}, 0)` fingerprint and collapsed correctly by hand-simulation. Comparing raw decision objects would be more expensive and no more precise, since the pend-jobid set already captures every observable difference the operator cares about.
|
|
- **Trade-offs**: A change in *why* a job is pending (a future pend-reason feature under `scheduler-sim-diagnostics`) would not by itself break the collapse, since the fingerprint doesn't include pend reason. Acceptable for this spec's boundary — pend-reason display is explicitly out of scope here.
|
|
- **Follow-up**: If `scheduler-sim-diagnostics` later adds pend-reason display, revisit whether the fingerprint needs to include it (a revalidation trigger, see design.md Boundary Commitments).
|
|
|
|
### Decision: Round Rollup exposes a shared flush entry point instead of owning every flush trigger itself
|
|
- **Context**: An independent task-graph sanity review (run before task generation) found that 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 calling `repl_command_loop()` directly on a match) — not in `repl.c` where the initial design placed all Round Rollup logic. Requirements 3.5 and 3.6 cannot be satisfied by round-boundary logic alone.
|
|
- **Alternatives Considered**:
|
|
1. Duplicate collapse-flush logic inside `repl_break.c` as well as `repl.c` — rejected, violates single-ownership of the collapse state and risks divergent flush behavior.
|
|
2. Move all breakpoint/structural-event detection into `repl.c` so Round Rollup owns every trigger directly — rejected, a much larger and riskier change to existing, already-tested breakpoint-matching code with no benefit to this spec's boundary.
|
|
3. Round Rollup exposes one flush entry point (`repl_round_collapse_flush`); `repl_break.c`'s existing hooks call it before their existing structural-event print / breakpoint-triggered command-loop entry.
|
|
- **Selected Approach**: Option 3.
|
|
- **Rationale**: Keeps collapse-state ownership entirely inside `repl.c` (single writer, no duplicated logic) while letting the two other legitimate trigger sites (`repl_break.c`'s breakpoint/structural-event detection, and `repl.c`'s own `xce_repl_on_replay_complete`) call a small, idempotent, safe-on-NULL interface — consistent with this codebase's existing "safe no-op on NULL/not-yet-allocated state" convention already documented on every other REPL hook.
|
|
- **Trade-offs**: Event Echo Gate (repl_break.c) now has a real outbound dependency on Round Rollup's interface, removing the parallel-safety the initial task draft assumed between those two components' implementation work — tests for both can still be written independently, but implementation must be sequenced.
|
|
- **Follow-up**: none — resolved before task generation; `design.md` and the task graph both reflect this.
|
|
|
|
## Risks & Mitigations
|
|
- **bug-1611 (dispatch visibility in replay mode) is unresolved** — Mitigation: pend-set correctness (Requirement 2.4) does not depend on its resolution (see Design Decision above); dispatch-count accuracy is called out as a known, separately-tracked limitation rather than silently assumed correct.
|
|
- **`sim_print_decision()`'s exact call site inside `sched_sim_driver_run()` was confirmed via call-graph but its surrounding loop body was not fully read** — Mitigation: implementation task must re-confirm the precise insertion point and existing control flow around it before editing; this is a normal implementation-time verification, not a design ambiguity (the gating *contract* — suppress under `sim->interactive && continue mode` — is unambiguous regardless of exact line placement).
|
|
- **No existing test coverage on `xce_repl_on_event`/`sim_print_decision` text output** (per CodeGraph) — Mitigation: Requirement-driven tests added in `test_repl_execution_control.c` (child-process output capture) before implementation, per this repo's TDD mandate.
|
|
|
|
## References
|
|
- `.wolf/buglog.json` bug-1609, bug-1610, bug-1611 — the three findings that motivated this spec, each with file:line root causes already recorded.
|
|
- `.kiro/specs/scheduler-sim-diagnostics/brief.md` — sibling brief; Requirement 2.4's "why did it pend" is explicitly out of this spec's boundary and belongs to that brief's seed capability 2 if formalized later.
|