pmacs/docs/parse-budget-diagnosability...

151 lines
6.4 KiB
Markdown

# Parse-budget diagnosability — reporting the measurement that failed
**Status: revision 2 — APPROVED 2026-08-29, IMPLEMENTED, PR #244.**
Revision 2 answers review of 1. Three of its four changes are
corrections, and one is a scope reduction:
- **SPLIT.** Revision 1 bundled this with adding `workflow_dispatch` to
`ci.yml`. `docs/active-work.md` already records those as "two
follow-ups … both their own lanes", and a shared thesis is not an
atomic feature boundary. **`workflow_dispatch` is no longer in this
framing**; it gets its own.
- **The census was WRONG.** Revision 1 called
`dispatch_parse_round_trips_a_rust_source_file` the sole outlier.
`tests/m4_acceptance.rs:244` asserts the same measurement against the
same budget and also omits it. **Both are now in scope.**
- **The completeness claim is WITHDRAWN**, not repaired. See §3.
- The concurrency reasoning revision 1 got backwards belonged to the
other change and leaves with it.
## 1. What this fixes
Two assertions compare a parse duration against a 100ms budget and
report **nothing** about what they measured:
| where | assertion | message |
|---|---|---|
| `src/async_runtime.rs:3328` | `duration_ms < 100` | `"trivial parse should be fast"` |
| `tests/m4_acceptance.rs:244` | `duration_ms < 100` | `"200-line parse should be quick"` |
When either reds, the log says the parse was slow and stops there.
## 2. Why now, and the evidence
The first has redded **twice** on `Test (macos-latest / lua54)`, and
**both margins are unrecoverable**:
| occurrence | where | outcome |
|---|---|---|
| U11 | PR #242, run `32393462318`, job `96504773333` | red, green on rerun |
| U11's recurrence | PR #243, run `33156571314`, job `98800645872` | red, green on rerun |
`src/async_runtime.rs` was **byte-identical to `main`** for both — blob
`9310ce3fca8c5fd8ebd39a68c29ad6985e256049`. U11's own row predicted the
cost:
> *A recurrence is not another instance of this row. Because the margin
> was never captured, a second red cannot be compared with the first.*
That came true once. Nothing prevents a third.
**A 1ms overshoot and a 900ms overshoot are different failures** — one
says a threshold is marginal, the other says something stalled — and
today they produce identical logs.
## 3. Scope, and a claim this framing does NOT make
**In scope: the two assertions in §1.** They are the same measurement
against the same budget, in the same subsystem, and it would be strange
to fix one and leave the other to produce the next unreadable red.
**Out of scope, and deliberately: everything else.** Revision 1 claimed
these were the only measurement-omitting assertions in the codebase.
**That claim is withdrawn and is not replaced by a corrected one.** A
sweep wide enough to be complete also catches `Instant::now() <
deadline` loop guards and `eval::<bool>` turbofish, which are not
budget assertions at all; a sweep narrow enough to be accurate proves
nothing about completeness. **This lane is not an assertion-hygiene
audit and should not be read as one.**
Several nearby budget assertions do already report their measurements —
`composition_overhead_under_ten_percent`, `criterion_1_end_of_line_typing…`
(`optimistic.rs:989`), `dired_open_renders_10k_entries_under_200ms`,
`m6_2_pty_streaming_respects_byte_ceiling` (`observed {in_flight}`).
They are cited as **precedent for the shape**, not as evidence that the
set is exhausted.
## 4. What lands
```rust
assert!(
duration_ms < 100,
"trivial parse should be fast: took {duration_ms}ms against a \
100ms budget"
);
```
and, in `tests/m4_acceptance.rs`:
```rust
assert!(
duration_ms < 100,
"200-line parse should be quick: took {duration_ms}ms against a \
100ms budget"
);
```
**Both budgets stay at 100ms.** Widening is what R1 already rejected,
and it would discard the signal these reds carry.
## 5. Acceptance
| # | contract | witness | mutation |
|---|---|---|---|
| D1 | the `async_runtime` assertion reports the observed ms **and** the budget | a scratch build with the comparison bound forced to `0` panics with a message containing the observed value and `100ms` | restore the bare message → the row cannot separate a 1ms overshoot from a 900ms one |
| D2 | the `m4_acceptance` assertion does the same | same method, same row | same |
| D3 | both budgets are still `100` | the comparison literal is unchanged in both files | widen either → R1's rejected remedy returns |
**D1 and D2 are asserted against a real panic message, not by reading
the source.** A row that greps the format string would pass while the
assertion it describes had been deleted — which is the same
read-the-code-not-the-effect failure this project has repeatedly
caught.
**How the fault is injected, and what each half proves.** Temporarily
force **only the comparison bound** to `0` in a scratch build. The
resulting panic then proves two things and no more: that the observed
value is interpolated, and that the budget text reads `100ms`.
**It does NOT prove the exercised budget was 100ms — it was 0.** The
scratch build compares against `0` while the message still says `100ms`,
so that panic alone says nothing about the committed threshold. **D3
carries that half separately**, by pinning the comparison literal in
both files. The two rows are only a proof together, and neither
substitutes for the other.
This is the **smallest deterministic fault injection** available, not
the only conceivable one. A row that waited for a genuine 100ms
overshoot would be exactly as intermittent as the thing it documents.
## 6. Coherence impact (`COHERENCE.md` §20)
- **Journey steps touched: NONE.** No product behaviour changes. What
moves is **evidence quality** — whether a failing check can be
reasoned about — the same axis the `scripts/gate` SIGINT guard sat
on.
- **Interaction islands: none added.**
- **Config registry: no entry.** A test budget is not a user-tunable.
- **Background work: none started**, and no attribution moves.
## 7. What this does NOT do
- **It does not fix the intermittence**, and makes no claim about
cause. It makes the next occurrence *comparable*.
- **It does not widen, relax or `#[ignore]` any budget.** Two
intermittent reds are not evidence a threshold is wrong.
- **It does not add `workflow_dispatch`.** That is its own lane, per
the ledger's recorded decision.
- **It does not run U9's discriminating control**, which remains
unrun and is a third, separate piece of work.