Proposal (rationale)
The gates module documents fast_budget_ms as “compared against the ledger’s observed values, not
against typed per-gate numbers — that comparison is the reason the ledger exists”, and
ADR-0005 Tier A names it the ledger’s first
consumer: “rungs doctor compares observed durations against the registry’s declared budget and
reports drift.” F-055: nothing reads the parameter. The ledger also records less
than the ADR’s own schema — at is a date, not a timestamp, and tier is absent — so two runs on
one day cannot be told apart and a tier’s cost cannot be summed.
Decision
accepted — 2026-09-06 under WI-085. Implement the
comparison in doctor, where ADR-0005 puts it, as a measurement with no verdict. The parameter
promises reporting; nothing here introduces timeouts, cancellation or telemetry.
Plan
Requirements
- Each ledger row carries the ADR-0005 schema: gate id, timestamp, exit status, wall-clock ms and the gate’s tier, plus a run identifier so rows of one run can be grouped. Old rows without these fields are counted as unreadable history, not guessed at.
doctorreports, for the first declared tier: the declared budget, the number of recorded runs compared, the observed serial wall-clock of that tier per run (median and maximum over the most recent runs), and how many runs exceeded the budget. Ledger disabled or absent: says so. Malformed rows: skipped and counted. Fewer than three usable runs: says the history is too short and prints nothing else.- The comparison is stated as a measurement of this machine’s recorded runs. No score, no pass/fail, no change to the runner’s exit status or gate selection (ADR-0008 rejected budget-driven selection).
Impacts
src/check.ts(appendLedgerfields, a ledger reader),src/cli.ts(doctoroutput),docs/decisions/ADR-0005unchanged (the implementation is brought to the ADR, not the reverse).- The gitignored ledger’s line shape changes; nothing else consumes it.
ledgerQuestionsstill reads onlyidandstatus.
Approach
Tag every row of one runGates call with the same run timestamp (ISO-8601 with milliseconds)
and the gate’s tier. Read the ledger once in doctor, group by run, sum ms for rows whose
tier equals the first declared tier, and compare each run’s sum to fast_budget_ms. Print under
the existing ledger section so the pull-not-push rule holds.
Acceptance criteria / tests
- A ledger with ten runs of a fixture registry, some over budget, produces a report naming the budget, the runs compared, median and max, and the over-budget count; the same ledger with the budget raised reports zero over.
- Ledger disabled, ledger absent, malformed lines, and fewer than three runs each produce the specified sentence and nothing else, through the CLI.
rungs checkoutput and exit status are unchanged for the same registry before and after.- The packed consumer journey’s ledger assertions still hold; full
npm testand all gates pass.
Out of scope
- Command timeouts, cancellation, per-gate typed durations written back into the registry, and any transmission of ledger data. Each is a separate decision.
- A warning state in the runner.
Execution
Executed 2026-09-06 on feature/WI-088-observed-fast-budget-reporting, cut from main eaf5fe1
(WI-061 landed).
appendLedgerwrites ADR-0005’s schema in full:run(one ISO-8601 timestamp with milliseconds, shared by every row of one run) andtier(the gate’s own) joinat,id,status,msandexamined.checkCommandpasses onerunper invocation; the ejected runner inherits it through the same function.ledgerBudget(check.ts) groups rows byrun, sumsmsfor rows whosetieris the first declared tier, takes the ten most recent runs and reports median, maximum and the number overfast_budget_ms. States are explicit:disabled(runner.ledger = false),absent(no file),no-budget(no tiers or no numeric budget declared),too-short(fewer than three usable runs),report. Rows that do not parse or lackrun/tier/msare counted as unreadable, never guessed.doctorprints the comparison under the ledger questions (reportBudgetin cli.ts), only for a repo with an install record, and only as a measurement: “A measurement, not a verdict: the runner never selects or fails on it”.checkprints nothing about it and its selection and exit are untouched (ADR-0008 rejected budget-driven selection).
Found and fixed on the way. The fixture with a malformed ledger line took doctor down with a
SyntaxError before the budget code ran: ledgerQuestions parsed every line with no guard. It now
skips a line that does not parse or lacks id/status, which is the same tolerance the item
requires of the budget reader. The gitignored ledger is the only consumer of the new fields; old rows
stay readable to the questions and are counted as unreadable by the comparison.
Deviations. None from the plan.
Review
Against each acceptance criterion, 2026-09-06, Windows 11, Node v22.22.3, npm 10.9.8:
- Report.
test/core.test.js“doctor reports observed fast-tier wall-clock…” drives the CLI over a fixture ledger of ten runs whose fast-tier gates sum to 50–140 ms (a full-tier row of 5,000 ms per run is present and correctly ignored):Fast tier budget (declared fast_budget_ms = 100)·last 10 run(s) of the fast tier: median 95 ms · max 140 ms · 4 over budget. The same ledger against a 1,000 ms budget reports0 over budget. - Every state, through the CLI. No ledger → “No ledger yet”;
ledger = false→ “Ledger off (runner.ledger = false)”; two garbage/pre-schema lines → “2 ledger row(s) predate the run/tier fields or do not parse” beside an unchanged median; two usable runs → “only 2 recorded run(s) … 3 needed”, with no median printed. Each asserted by runningdoctor. checkuntouched. The same test runscheckagainst a 1 ms budget the fixture exceeds: exit 0, and none ofFast tier budget,over budget,fast_budget_msin its output. The eject regression asserts the ledger row keysat, run, id, status, ms, examined, tierand one sharedrunid per run, and the ejected runner’s rows match production’s.- Suite and gates. Serial
node --test --test-concurrency=1 test/*.test.js: 151 tests, 148 pass, 0 fail, 3 platform skips, 143 s; the packed consumer journey’s ledger-count assertions still hold.node src/cli.ts check: 31 pass. This repository’s owndoctornow prints: “only 2 recorded run(s) of the first tier carry run and tier fields; 3 needed” and “13352 ledger row(s) predate those fields or do not parse, and are not counted” — the honest state of a ledger that predates the schema.
Pending. The exact-SHA OS/Node matrix has not run: the branch is not pushed.