The record said "10 tests go red"; on the rebased base a full short-circuit reddens 11. Both numbers are real and measure different mutations: neutering only the scan loop leaves the empty-wing check live (10), while returning [] from the whole function disables that too (11). The reviewer flagged the phrasing as ambiguous before the count also drifted. Now states the mutation alongside the number, since a bare count is exactly the kind of frozen figure this session has watched rot four times. Refs #621 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.1 KiB
key, title, status, since, supersedes, superseded-by, rule, signals, mechanics
| key | title | status | since | supersedes | superseded-by | rule | signals | mechanics |
|---|---|---|---|---|---|---|---|---|
| docs.record-wing-parse-guard | 2026-07-26 — Every file in the record wings must parse to exactly one keyed record; the frontmatter reader stays flat-scalar-only (#621) | active | 2026-07-26 | none | none | `decisions_validate.py` asserts, per PATH, that every `*.md` under `docs/decisions/records/**` and `docs/decisions/archive/**` parses to exactly one record carrying a `key` — an ERROR, not a warning, since a file in the record wings that is not a record is a mistake by definition. A file sitting DIRECTLY in `archive/` is exempt only when it actually looks like a #610 stripped index — exactly one keyless record with a known generated heading — never merely by living there. The one other exemption, `archive/README.md`, is by exact RELATIVE PATH; nothing is ever exempt by BASENAME, since that would exempt the same filename in the active wing too. `_read_frontmatter` is deliberately NOT extended to accept YAML block scalars: every record value goes on ONE line, and the structural check is what makes that limitation loud instead of silent. | record silently invisible, parsed to 0 records, block scalar rule >- , unterminated frontmatter, file vanishes from corpus, validator OK but record absent, catalog up to date but missing, one keyed record per file, stripped legacy archive index exempt · paths: `scripts/decisions_validate.py`, `scripts/decisions_lib.py`, `scripts/tests/test_decisions_validate.py` · issues: #621, #610, #609, #603 | `scripts/decisions_validate.py` -> `record_wing_files` / `record_wing_faults`; `docs/ci-cd.md` -> "`decisions-guard` job" |
Found in adversarial review of #610 / PR #619. Nothing enforced that a file under the record wings
is actually a record. A file the dependency-free frontmatter reader cannot parse returns [] and
simply vanishes — decisions_validate.py prints OK, build_decisions_catalog.py --check says
up to date, the record is absent from the corpus, and there is no error anywhere.
This is the corpus's own failure mode turned on itself. The whole point of the decision system is
retrievability; the one thing worse than a missing record is a missing record that reports success.
Same family as the #609 marker that printed OK while doing nothing and the #603 stale-after field
that silently never fired.
What was already safe, and what wasn't. An existing record disappearing was always loud — the no-vanish diff check catches it. The hole is a newly added record, which the diff check structurally cannot see because there is no base state to compare against, so the author's own PR looks clean. That asymmetry is why the fix had to be path-driven rather than diff-driven.
Why per-path rather than per-construct. Enumerating the YAML the reader rejects would have to be re-done every time the reader meets something new. Asserting "this path must yield exactly one keyed record" converts a whole CLASS of reader limitations from silent to loud in one move — block scalars, indented structure, unterminated frontmatter, a stray note file, an empty file. It does NOT catch parse-to-WRONG (frontmatter yielding one keyed record with corrupted values); a separate junk-key check covers the realistic instance of that, so this is a strong guard, not a total one.
The block-scalar decision: NO. rule: >- is the natural thing to reach for on this corpus's very
long rule: values, and under PyYAML it parsed fine — so #610's dependency-free reader widened a
hole rather than creating it. Teaching _read_frontmatter to fold block scalars was still declined:
- Correct folding is a real subset of YAML (
>vs|, strip/clip/keep chomping, indentation-relative continuation). A subtly wrong folder is worse than a rejection, because it would silently alter rule text — the exact class of failure this record exists to close — whiletest_frontmatter_reader_matches_pyyaml(which compares against PyYAML on real records) would keep passing on the flat records that make up the entire corpus. - The reader must stay dependency-free: it runs in CI's
decisions lifecyclejob, in the Husky pre-commit hook, and on every contributor's machine, none of which install anything. - With the structural check in place the cost of not supporting block scalars is a loud error
naming the fix, and all 168 record files already keep each value on ONE line. (The invariant is
single-line, not single-quoted: 117
rule:values are unquoted plain scalars, 51 are quoted.) There is no value a flat scalar cannot hold.
Verified by mutation, not by a green tick: with record_wing_faults short-circuited to return [],
11 tests go red (10 if only the scan loop is neutered, leaving the empty-wing check live),
and separately, deleting the single line that WIRES it into main() reddens a dedicated test — that
one line's removal previously left the whole suite green while a real record vanished, which is the
guard's own failure mode applied to the guard. test_real_repo_record_wings_are_all_parseable additionally asserts the
wings are non-empty (>100 files) so a clean result can never be vacuous.