Uh oh!
There was an error while loading. Please reload this page.
fix(qa): close the platform-checklist traps vocabulary against RUNNER.md, with a parser that refuses rather than fails open - #10682
Conversation
…ER.md, with a parser that refuses rather than fails open `check-platform-checklist` enforced a closed vocabulary for `status`, `priority`, `surface`, `oracle` and `blocked.by` — and never read `traps` at all. So an item could carry any trap name and the validator stayed green, which is how eight undocumented values drifted in (#10416), and how a TYPO in a documented one (`hydration-races` for `hydration-race`, which is on 79 of the 205 items) lands as simply a twentieth trap that no runner rules out. The vocabulary is READ from RUNNER.md's `### Trap vocabulary` table, not copied into the script: a sixth hardcoded `Set` would only move the drift one level up, between the script and RUNNER.md, with nothing watching that seam. The load-bearing part is therefore not the parser but its positive control — `extractTrapVocabulary` refuses on a table it cannot recognise (heading renamed, table moved, zero rows, a row that lost its backticks) instead of returning an empty allow-list, and a 22-assertion fixture battery proves the refusal still fires. The battery runs inline on every invocation, not only behind `--self-test`, because this gate is not CI-wired by maintainer decision and its `pnpm` alias lives in the fenced root package.json (#9465) — a self-test nothing runs is the #10574/#10573 defect. The OK line now states what the parse read: `traps: 19 documented, 19 in use (extractor control: 22 assertions)`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
PM review — verified against the tree, not the report. Approving. The unusual design decision here is the right one, and I checked its justification rather
A The positive control is the load-bearing addition, not the parser, and the PR knows it. The correction to my brief is accepted, and it is the better reading. I wrote the The AB-1 signal was reported instead of rounded off, which I want to name: the first Counts re-derived on today's tree — 19/19/0, CI green (28 checks). One process note, on my instruction rather than your work. You reported that the Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10647
check-platform-checklistenforced a closed vocabulary forstatus,priority,surface,oracleandblocked.by— and never readtrapsat all. The string did not appear in the file. So an item could carry any trap name and the validator stayed green, which is how eight undocumented values drifted in (#10416), and how a typo in a documented one lands as simply a twentieth trap that no runner rules out.Counts re-derived on this tree
Parsing all 15
docs/qa/platform-checklist/areas/*.json(205 items) against RUNNER.md's### Trap vocabularytable, post-#10650: 19 documented / 19 distinct in use / 0 used-but-undocumented / 0 documented-but-unused. As the brief predicted.hydration-raceis on 79 of the 205 items; 197 items carry atrapslist, 8 carry none. The gate now prints the pair on its own OK line, so the number is re-derived on every run instead of recalled.Both harms, before and after
totally-invented-trapadded tocli.build-own-contract)OK — 15 areas, 205 items …exit 0traps[1] `totally-invented-trap` is not in RUNNER.md's `### Trap vocabulary` tablehydration-race→hydration-racesonrecords-forms.crud-roundtrip)OK — 15 areas, 205 items …exit 0traps[0] `hydration-races` … — did you mean `hydration-race`?Both mutations were confirmed on disk by anchor count before each run (
0 → 1for the injected spelling), never by an editor's exit code.Why this is not a sixth
Set, and not a generator eitherTRAPSset reintroduces the same drift one level up — between this script and RUNNER.md — with nothing watching that seam. Rejected in the card, and it is the wrong answer for the same reason the eight arrived.A markdown-table extractor has one failure mode that matters: it reads zero rows and every item then validates against an empty allow-list. Zero violations, and a green indistinguishable from a working parse. So
extractTrapVocabularyrefuses — it never returns an empty vocabulary with no complaint — and a 22-assertion fixture battery proves the refusal still fires. The battery runs inline on every invocation, not only behind--self-test, because a--self-testhere would execute nowhere: this gate is not CI-wired by maintainer decision (README "Operating cadence") and itspnpmalias lives in rootpackage.json, fenced territory of the @changesets/cli v3 lane (#9465). A self-test nothing runs is the documented defect of #10574/#10573.The fail-open control
Making the table unreadable, four ways, each confirmed on disk by anchor count (19 trap rows → 4 non-trap rows elsewhere in the file; heading count 1 → 0; backticked row 1 → 0):
has a header but ZERO rows … this is a refusal — never an empty allow-list### Traps you may hitthe "### Trap vocabulary" heading is not in the file — renamed, moved or removed1 row(s) … do not name a single backticked trap in the first cell — e.g. "| wrong-panel | …"is not the trap table — expected a "| trap | … |" header rowAnd the counterfactual, which is what makes the above load-bearing. A copy of this same file with the refusal downgraded to an empty set, the positive control neutered, and the natural defensive guard (
if (vocabulary.size > 0 && !vocabulary.has(name))), run against the emptied table:That green is the defect, exactly as the card describes it.
Ablations
Every new assertion broken, driven red, restored. Mutation confirmed on disk by anchor count each time; the prediction was written before the run.
trapProblems' vocabulary branch disabled, control intactC3,C4,C519 documented, 20 in usenever-used-trap; ablated → exit 0,20 documented, 19 in useextractTrapVocabulary's refusal neutered, control intactR1–R8+R9)AB-1a is worth reading twice: the first attempt at this ablation predicted that harm (a) would flip to green, and it did not — the inline control caught the deleted assertion before the run ever reached real data. That is the design working. Isolating the data-side red needed AB-1b, where the control had to be removed too. The shipped assertion cannot be quietly deleted.
Verification
Everything below on
1538eb4c— the final commit — with the gate union re-derived on that same HEAD.check:doc-authoringwas added by hand, not by the derivation — the brief flagged that it readsdocs/**and is invisible todispatch-gatesuntil #10662 declares it, and this diff editsdocs/qa/platform-checklist/README.md. The other hand cross-check went the other way:check:doc-anchorsis genuinely irrelevant here, not merely unrun. Its population iscontent/**plus the repo-rootREADME.mdandARCHITECTURE.mdonly, and its own docblock says a same-named file inside a subtree "is not reached — which is correct". So the derivation's silence on it was right.No heavy work was needed and none was run, so there is no
os-verify-lock.shVERDICT line to quote. No build, no test suite, nopnpm install— every gate above is a zero-dependency node script and the whole set runs in under 30 s.check:doc-anchorsis the one gate that would have needed a workspace install (github-slugger), and it does not read this diff.Scope
scripts/**+docs/qa/platform-checklist/README.md. RUNNER.md is unmodified — the table it already carries is the input, and no change to it was needed. Publishes nothing, soskip-changeset(precedent: #10502). No governed surface touched.The one behaviour change outside the new check: the OK line gained a
traps:clause, so a green now states how many rows the parse read and that its own control passed.Generated by Claude Code