fix(621): make an unparseable decision record loud instead of silently invisible #641
+8
-1
@@ -579,7 +579,14 @@ Enforces decision-record lifecycle invariants (ersatztv#521, supersedes the ersa
|
||||
append-only mechanic): well-formed 5-field metadata, exactly one `active` record per `key`,
|
||||
reciprocal `supersedes`/`superseded-by` links, no record vanishing from the active set without an
|
||||
archive copy, no rationale-prose rewrite without a `Decisions-Edit: yes` trailer on a non-merge commit
|
||||
in the range (ersatztv#609),
|
||||
in the range (ersatztv#609), a structural per-path check that every `*.md` under
|
||||
`docs/decisions/records/**` and `docs/decisions/archive/**` parses to **exactly one keyed
|
||||
record** (ersatztv#621 — without it, a file the dependency-free frontmatter reader cannot parse,
|
||||
such as one using a YAML block scalar, yields `[]` and vanishes from the corpus with every check
|
||||
still reporting green; a file directly in `archive/` is exempt only when it really is a stripped index — one keyless
|
||||
record with a known generated heading — never merely by its location; the single further exemption,
|
||||
`archive/README.md`, is by exact relative path, never by basename, which would otherwise exempt the
|
||||
same filename in the active wing),
|
||||
and the generated active catalog (`docs/decisions/README.md`) in sync with source. Two steps:
|
||||
`scripts/decisions_validate.py --base origin/<base> --head HEAD` (the merge-base diff checks, which
|
||||
need a base/head range — CI-only) and `scripts/build_decisions_catalog.py --check` (catalog drift).
|
||||
|
||||
@@ -66,6 +66,7 @@ the link for rationale. Superseded/retired history lives in `archive/`. Regenera
|
||||
| `docs.decision-lifecycle` | every decision `##` record (active or archived) carries a 5-field metadata block (`key`, `status`, `since`, `supersedes`, `superseded-by`) checked by `scripts/decisions_validate.py`; a record is never deleted or line-edited to reverse a call — it is moved to `docs/decisions/archive/` with `status: superseded`/`retired` and a reciprocal `superseded-by`/`supersedes` key pair to its replacement. | 2026-07-21 | [link](records/docs/decision-lifecycle.md) |
|
||||
| `docs.decision-one-file-per-record` | Each decision record is its own file at `docs/decisions/records/<area>/<topic>.md` (archived ones at `docs/decisions/archive/<area>/<topic>.md`) with YAML frontmatter; the filename IS the key, so one-active-record-per-key is a filesystem property rather than a validator check, and supersession is a `git mv`. | 2026-07-25 | [link](records/docs/decision-one-file-per-record.md) |
|
||||
| `docs.decision-optional-provenance` | Decision records gain two OPTIONAL fields — `stale-after: YYYY-MM-DD` on the metadata line and a `**Sources:**` line in the metadata block; the Open Knowledge Format (OKF) itself is NOT adopted as the record format. | 2026-07-25 | [link](records/docs/decision-optional-provenance.md) |
|
||||
| `docs.record-wing-parse-guard` | `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. | 2026-07-26 | [link](records/docs/record-wing-parse-guard.md) |
|
||||
| `docs.tracker-comment-retrofit` | When the knowledge exporter flags an over-cap tracker issue and excludes it from ingestion, triage its comments instead of assuming a retrofit is owed — and for each decision-shaped item check the **worked issue first**, because a tracker session comment is by construction a précis of the fuller closing record posted on the issue it narrates. Applied to #237 (111 comments) this yielded **zero** records, so server-management#642's "a fact found only in a #237 comment" retrieval row has no valid subject and its interim target (an already-migrated record) is permanent. | 2026-07-21 | [link](records/docs/tracker-comment-retrofit.md) |
|
||||
| `ffmpeg.external-logo-graphics-engine` | External-URL channel logos pass through to the graphics engine like any other watermark source; `WatermarkSelector` must never gate them on `File.Exists` (always false for a URL) and never route them through the ffmpeg-native overlay shortcut. | 2026-07-20 | [link](records/ffmpeg/external-logo-graphics-engine.md) |
|
||||
| `ffmpeg.hls-cold-start-burst` | HLS cold-start latency is fixed with a bounded `-readrate_initial_burst` (gated on FFmpeg ≥6.1 capability detection), not by raising `work_ahead_limit`, which would remove the concurrency guarantee it exists for. | 2026-07-20 | [link](records/ffmpeg/hls-cold-start-burst.md) |
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
key: docs.record-wing-parse-guard
|
||||
title: '2026-07-26 — Every file in the record wings must parse to exactly one keyed record; the frontmatter reader stays flat-scalar-only (#621)'
|
||||
status: active
|
||||
since: '2026-07-26'
|
||||
supersedes: none
|
||||
superseded-by: none
|
||||
rule: '`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.'
|
||||
signals: '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'
|
||||
mechanics: '`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 — while
|
||||
`test_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 lifecycle` job, 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.
|
||||
@@ -96,9 +96,15 @@ def validate(
|
||||
rewritten,
|
||||
archive_records=None,
|
||||
demoted=(),
|
||||
wing_faults=(),
|
||||
) -> list[str]:
|
||||
errs: list[str] = []
|
||||
archive_records = archive_records or []
|
||||
# #621: structural "one keyed record per file". ERRORS, not warnings — a file under the record
|
||||
# wings that is not a record is a mistake by definition. Reported first because a file that
|
||||
# failed to parse is absent from `records`, so every downstream check below is silently
|
||||
# evaluating an incomplete corpus.
|
||||
errs += list(wing_faults)
|
||||
decision_recs = [r for r in records if r.heading not in SKIP_HEADINGS and r.status != "legacy-unmigrated"]
|
||||
|
||||
active_by_key: dict[str, int] = {}
|
||||
@@ -406,6 +412,154 @@ def _diff_findings(base: str, head: str) -> tuple[list[str], list[str], list[str
|
||||
return sorted(set(removed)), sorted(set(rewritten)), sorted(set(demoted))
|
||||
|
||||
|
||||
def record_wing_files(records_dir: Path | None = None, archive_dir: Path | None = None) -> list[Path]:
|
||||
"""Every file that MUST be exactly one keyed record.
|
||||
|
||||
Scope is EVERY `*.md` under `records/**` and `archive/**` — including the files directly in
|
||||
`archive/`, which are scanned but may qualify for the stripped-index exemption applied in
|
||||
`record_wing_faults` (see there). Nothing is excluded by BASENAME.
|
||||
|
||||
Excluding by BASENAME was a real hole: `_NON_DECISION_FILES` is `{README.md, migration-map.md,
|
||||
retrieval-eval.md}` — three TOPIC-dir names — and applying it to the wings meant a genuine
|
||||
record at `records/docs/retrieval-eval.md` was silently skipped. That path is not hypothetical:
|
||||
the path<->key rule forces key `docs.retrieval-eval` to live at exactly that filename, and
|
||||
`docs/decisions/retrieval-eval.md` is a real unmigrated file, i.e. a plausible migration target.
|
||||
Worse, `dl.active_files()` applies that filter only to the TOPIC_DIR glob, not to
|
||||
`RECORDS_DIR.rglob` — so such a file IS a corpus source while being exempt from the guard.
|
||||
|
||||
So the exemption is by exact RELATIVE PATH, never by basename. The only entry is
|
||||
`archive/README.md`, a hand-written directory README that really does exist — an earlier
|
||||
version of this function excluded any wing-root `README.md` "since no such file exists today",
|
||||
which was simply false and would additionally have exempted a future `records/README.md`, i.e.
|
||||
reintroduced the very hole one directory over.
|
||||
"""
|
||||
records_dir = dl.RECORDS_DIR if records_dir is None else records_dir
|
||||
archive_dir = dl.ARCHIVE_DIR if archive_dir is None else archive_dir
|
||||
exempt_paths = {archive_dir / "README.md"}
|
||||
files: list[Path] = []
|
||||
for wing in (records_dir, archive_dir):
|
||||
if wing.exists():
|
||||
files += sorted(p for p in wing.rglob("*.md") if p not in exempt_paths)
|
||||
return files
|
||||
|
||||
|
||||
def _is_stripped_index(path: Path, archive_dir: Path, recs: list) -> bool:
|
||||
"""True for a #610 stripped legacy topic file sitting DIRECTLY in `archive/`.
|
||||
|
||||
Those five files (`api.md`, `scan.md`, `spa.md`, `startup.md`, `release-ci-governance.md`) are
|
||||
generated "Records formerly in this file" indexes — keyless by construction, and what keeps
|
||||
older date-based pointers resolvable. They are indexes, not records.
|
||||
|
||||
The exemption is by IDENTITY, not by location. Exempting everything directly in `archive/`
|
||||
would leave that one directory unguarded: a new unparseable `archive/foo.md` would vanish
|
||||
silently — the same defect this guard exists to close, in the last place it isn't watched. So a
|
||||
file there is exempt only if it actually LOOKS like a stripped index: exactly one keyless
|
||||
record whose heading is one of the known generated ones (`dl.SKIP_HEADINGS`).
|
||||
"""
|
||||
return (
|
||||
path.parent == archive_dir
|
||||
and len(recs) == 1
|
||||
and not recs[0].key
|
||||
and recs[0].heading in dl.SKIP_HEADINGS
|
||||
)
|
||||
|
||||
|
||||
def record_wing_faults(records_dir: Path | None = None, archive_dir: Path | None = None) -> list[str]:
|
||||
"""Structural check (#621): every file in the record wings parses to exactly ONE keyed record.
|
||||
|
||||
Without this, a file the frontmatter reader cannot parse yields `[]` and simply VANISHES — no
|
||||
error, no warning, validator OK, catalog "up to date", record absent from the corpus. That is
|
||||
the corpus's own failure mode turned on itself: the one thing worse than a missing record is a
|
||||
missing record that reports success (cf. the #609 marker that printed OK while doing nothing and
|
||||
the #603 `stale-after` that silently never fired).
|
||||
|
||||
An EXISTING record disappearing was already loud — the no-vanish diff check catches it. This
|
||||
closes the case the diff check structurally cannot see: a NEWLY ADDED record, where the author's
|
||||
own PR looks clean because there is no prior state to diff against.
|
||||
|
||||
Making it path-driven rather than record-driven is the point: it converts a whole CLASS of
|
||||
reader limitations — parse-to-zero, parse-to-many, keyless — from silent to loud in one move,
|
||||
instead of enumerating the constructs we happen to know about today.
|
||||
|
||||
It does NOT catch parse-to-WRONG: frontmatter that yields one keyed record with corrupted
|
||||
values. `rule: >-` followed by an UNINDENTED continuation containing a colon parses to a valid
|
||||
record whose `rule` is literally `>-`, plus a junk key from the continuation line. The junk-key
|
||||
check below is what catches that case; a mis-parse producing only known fields would still slip
|
||||
through, so this is a strong guard, not a total one.
|
||||
"""
|
||||
records_dir = dl.RECORDS_DIR if records_dir is None else records_dir
|
||||
archive_dir = dl.ARCHIVE_DIR if archive_dir is None else archive_dir
|
||||
faults: list[str] = []
|
||||
files = record_wing_files(records_dir, archive_dir)
|
||||
|
||||
# A wing that is absent or empty must be LOUD, not silently clean. Otherwise a partial checkout,
|
||||
# a renamed directory, or a bad monkeypatch turns the whole guard into a no-op that reports
|
||||
# success — which is the defect this function exists to close, applied to itself.
|
||||
# DELIBERATELY asymmetric: only the ACTIVE wing must be non-empty. A corpus with zero active
|
||||
# records is definitionally broken (and means the scan is measuring nothing, so a clean result
|
||||
# would be vacuous). An empty ARCHIVE is a perfectly normal state — it just means nothing has
|
||||
# been superseded or retired yet, which is true of any young repo and of a fresh clone before
|
||||
# the first supersession. Faulting on it would fail a correct corpus. Review asked for symmetry
|
||||
# here; the semantics genuinely differ, so this stays asymmetric with the reason stated.
|
||||
if not records_dir.exists() or not any(records_dir.rglob("*.md")):
|
||||
faults.append(
|
||||
f"{records_dir}: the active record wing is missing or contains no *.md files — refusing "
|
||||
f"to report a clean corpus from an empty scan."
|
||||
)
|
||||
|
||||
for p in files:
|
||||
try:
|
||||
recs = dl.parse_file(p)
|
||||
except Exception as exc: # unreadable/undecodable file is a fault, not a crash
|
||||
faults.append(f"{p}: could not be read as a decision record ({exc})")
|
||||
continue
|
||||
if _is_stripped_index(p, archive_dir, recs):
|
||||
continue
|
||||
if len(recs) != 1:
|
||||
faults.append(
|
||||
f"{p}: parsed to {len(recs)} records, expected exactly 1 — a file under the record "
|
||||
f"wings must be one record. Common cause: YAML the dependency-free frontmatter "
|
||||
f"reader does not accept (a block scalar such as `rule: >-` or `rule: |`, any "
|
||||
f"indented/nested structure, or unterminated `---` frontmatter). Put the value on "
|
||||
f"ONE line."
|
||||
)
|
||||
continue
|
||||
if not recs[0].key:
|
||||
faults.append(f"{p}: parsed to a record with no `key` — every record in the wings must be keyed.")
|
||||
continue
|
||||
# Junk keys: the reader accepted a line it should not have. The realistic source is a block
|
||||
# scalar whose continuation was read as its own `k: v` pair, which silently truncates the
|
||||
# real value (`rule` becomes `>-`). PyYAML REJECTS that input, so the hand reader is more
|
||||
# permissive than the writer and nothing else in the pipeline notices.
|
||||
unknown = _unknown_frontmatter_keys(p)
|
||||
if unknown:
|
||||
faults.append(
|
||||
f"{p}: frontmatter has unrecognized key(s) {sorted(unknown)} — a value was probably "
|
||||
f"split across lines (a block scalar continuation read as its own key), which "
|
||||
f"silently truncates the real value. Put each value on ONE line."
|
||||
)
|
||||
return faults
|
||||
|
||||
|
||||
def _unknown_frontmatter_keys(path: Path) -> set[str]:
|
||||
"""Frontmatter keys outside the known schema. Empty on any read/parse failure (reported elsewhere)."""
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
return set()
|
||||
if not dl.has_frontmatter(text):
|
||||
return set()
|
||||
lines = text.splitlines()
|
||||
end = next((i for i, ln in enumerate(lines[1:], start=1) if ln.rstrip() == "---"), None)
|
||||
if end is None:
|
||||
return set()
|
||||
meta = dl._read_frontmatter("\n".join(lines[1:end]))
|
||||
if not meta:
|
||||
return set()
|
||||
known = set(dl._FM_TO_FIELD) | {"title"}
|
||||
return {k for k in meta if k not in known}
|
||||
|
||||
|
||||
def _archive_keys() -> set[str]:
|
||||
keys: set[str] = set()
|
||||
if dl.ARCHIVE_DIR.exists():
|
||||
@@ -491,6 +645,7 @@ def main(argv=None) -> int:
|
||||
rewritten=rewritten,
|
||||
archive_records=archive_records,
|
||||
demoted=demoted,
|
||||
wing_faults=record_wing_faults(),
|
||||
)
|
||||
|
||||
if not budget_ok:
|
||||
|
||||
@@ -700,3 +700,241 @@ def test_retitling_a_record_is_not_reported_as_a_removal(tmp_path):
|
||||
assert removed == [], f"a retitle was reported as a removal: {removed}"
|
||||
assert rewritten == [], f"a retitle was reported as a prose rewrite: {rewritten}"
|
||||
assert demoted == [], f"a retitle was reported as a demotion: {demoted}"
|
||||
|
||||
|
||||
# --- #621: structural "one keyed record per file" over the record wings -----------------------
|
||||
#
|
||||
# The defect these cover: a file the dependency-free frontmatter reader cannot parse yields `[]`
|
||||
# and vanishes from the corpus with NO error anywhere — validator OK, catalog "up to date", record
|
||||
# absent. Only a NEWLY ADDED record is affected; an existing one disappearing is already caught by
|
||||
# the no-vanish diff check, which structurally cannot see a record that never existed in the base.
|
||||
|
||||
|
||||
def _wing(tmp_path) -> tuple[Path, Path]:
|
||||
"""A tmp record-wing pair: (records_dir, archive_dir)."""
|
||||
records = tmp_path / "docs" / "decisions" / "records"
|
||||
archive = tmp_path / "docs" / "decisions" / "archive"
|
||||
(records / "ci").mkdir(parents=True)
|
||||
(archive / "ci").mkdir(parents=True)
|
||||
return records, archive
|
||||
|
||||
|
||||
_GOOD = (
|
||||
"---\n"
|
||||
"key: ci.good\n"
|
||||
"title: '2026-01-01 — Good (#1)'\n"
|
||||
"status: active\n"
|
||||
"since: '2026-01-01'\n"
|
||||
"supersedes: none\n"
|
||||
"superseded-by: none\n"
|
||||
"rule: 'a rule on one quoted line'\n"
|
||||
"signals: 'concept · paths: a/b.py · issues: #1'\n"
|
||||
"mechanics: 'x'\n"
|
||||
"---\n\nRationale prose.\n"
|
||||
)
|
||||
|
||||
|
||||
def test_wing_faults_clean_corpus_is_silent(tmp_path):
|
||||
records, archive = _wing(tmp_path)
|
||||
(records / "ci" / "good.md").write_text(_GOOD)
|
||||
assert dv.record_wing_faults(records, archive) == []
|
||||
|
||||
|
||||
def test_wing_faults_block_scalar_record_fails_loudly(tmp_path):
|
||||
"""The exact reproduction from #621: a YAML block scalar — the natural thing to reach for on
|
||||
this corpus's very long `rule:` values — makes the whole record silently invisible."""
|
||||
records, archive = _wing(tmp_path)
|
||||
bad = records / "ci" / "blockscalar.md"
|
||||
bad.write_text(_GOOD.replace("rule: 'a rule on one quoted line'\n", "rule: >-\n a long rule wrapped\n over two lines\n"))
|
||||
|
||||
# Precondition: this really is the silent-vanish case, not some other parse error.
|
||||
assert dl.parse_file(bad) == [], "expected the reader to drop the record entirely"
|
||||
|
||||
faults = dv.record_wing_faults(records, archive)
|
||||
assert len(faults) == 1, faults
|
||||
assert "blockscalar.md" in faults[0]
|
||||
assert "expected exactly 1" in faults[0]
|
||||
|
||||
|
||||
def test_wing_faults_unterminated_frontmatter_fails_loudly(tmp_path):
|
||||
records, archive = _wing(tmp_path)
|
||||
bad = records / "ci" / "unterminated.md"
|
||||
bad.write_text("---\nkey: ci.unterminated\nstatus: active\n\nNo closing delimiter.\n")
|
||||
assert dl.parse_file(bad) == []
|
||||
|
||||
faults = dv.record_wing_faults(records, archive)
|
||||
assert len(faults) == 1, faults
|
||||
assert "unterminated.md" in faults[0]
|
||||
|
||||
|
||||
def test_wing_faults_stray_note_file_fails_loudly(tmp_path):
|
||||
"""A file under records/ that is not a record at all — no frontmatter whatsoever."""
|
||||
records, archive = _wing(tmp_path)
|
||||
(records / "ci" / "notes.md").write_text("# Scratch notes\n\nNot a record.\n")
|
||||
faults = dv.record_wing_faults(records, archive)
|
||||
assert len(faults) == 1, faults
|
||||
assert "notes.md" in faults[0]
|
||||
|
||||
|
||||
def test_wing_faults_keyless_record_fails_loudly(tmp_path):
|
||||
"""Parses to exactly one record, but carries no `key` — still a fault."""
|
||||
records, archive = _wing(tmp_path)
|
||||
(records / "ci" / "keyless.md").write_text(_GOOD.replace("key: ci.good\n", ""))
|
||||
faults = dv.record_wing_faults(records, archive)
|
||||
assert len(faults) == 1, faults
|
||||
assert "no `key`" in faults[0]
|
||||
|
||||
|
||||
def test_wing_faults_exempts_stripped_legacy_archive_files(tmp_path):
|
||||
"""The #610 split left five KEYLESS stripped topic files at the archive TOP level (api.md,
|
||||
scan.md, spa.md, startup.md, release-ci-governance.md) whose only content is a generated
|
||||
"Records formerly in this file" index — that index is what keeps older date-based pointers
|
||||
resolvable. They are indexes, not records, and must stay exempt. Their area-NESTED siblings
|
||||
are real records and are checked."""
|
||||
records, archive = _wing(tmp_path)
|
||||
(records / "ci" / "good.md").write_text(_GOOD)
|
||||
# a stripped legacy file at the archive top level: parses to a single keyless pseudo-record
|
||||
(archive / "api.md").write_text("# api\n\n## Records formerly in this file\n\n- `api.thing`\n")
|
||||
(archive / "README.md").write_text("# archive\n")
|
||||
assert dv.record_wing_faults(records, archive) == []
|
||||
|
||||
# ...but a real archived record nested under an area IS checked
|
||||
(archive / "ci" / "broken.md").write_text("# not a record\n")
|
||||
faults = dv.record_wing_faults(records, archive)
|
||||
assert len(faults) == 1 and "broken.md" in faults[0], faults
|
||||
|
||||
|
||||
def test_validate_surfaces_wing_faults_as_errors(tmp_path):
|
||||
"""Faults must arrive as validator ERRORS (exit 1), not warnings."""
|
||||
errs = _v([_rec(key="ci.a", source=Path("docs/decisions/records/ci/a.md"), heading="A")],
|
||||
wing_faults=["docs/decisions/records/ci/x.md: parsed to 0 records, expected exactly 1"])
|
||||
assert any("x.md" in e for e in errs), errs
|
||||
|
||||
|
||||
def test_real_repo_record_wings_are_all_parseable():
|
||||
"""Positive control against the LIVE corpus: every one of the real record files parses to
|
||||
exactly one keyed record. This is what makes the check's clean result meaningful rather than
|
||||
vacuous — if the wings were empty or unreadable, the checks above would pass trivially."""
|
||||
files = dv.record_wing_files()
|
||||
assert len(files) > 100, f"record wings look empty ({len(files)} files) — check is vacuous"
|
||||
assert dv.record_wing_faults() == []
|
||||
|
||||
|
||||
def test_wing_faults_sees_a_DEEPER_nested_archive_record(tmp_path):
|
||||
"""A one-level `archive/*/*.md` glob would exempt the top-level stripped files correctly but
|
||||
silently skip anything nested deeper — a path escaping the check, which is the very failure
|
||||
mode this guard exists to close. The exemption must be 'directly in archive/', not 'exactly
|
||||
one level down'."""
|
||||
records, archive = _wing(tmp_path)
|
||||
(records / "ci" / "good.md").write_text(_GOOD)
|
||||
(archive / "api.md").write_text("# api\n\n## Records formerly in this file\n") # still exempt
|
||||
deep = archive / "ci" / "sub"
|
||||
deep.mkdir(parents=True)
|
||||
(deep / "broken.md").write_text("# not a record\n")
|
||||
|
||||
faults = dv.record_wing_faults(records, archive)
|
||||
assert len(faults) == 1 and "broken.md" in faults[0], faults
|
||||
|
||||
|
||||
# --- #621 cold-review findings ------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_main_actually_CALLS_the_wing_scan(monkeypatch, capsys):
|
||||
"""The guard's only wiring was untested: deleting `wing_faults=record_wing_faults()` from
|
||||
main() left the entire suite green, and a real block-scalar record vanished again with
|
||||
`decisions-validate: OK`. Every other test either calls the collector directly or hands
|
||||
validate() a hand-built list, so nothing pinned that main() invokes it at all — the #609
|
||||
'marker that printed OK while doing nothing' defect, one level up."""
|
||||
monkeypatch.setattr(dv, "record_wing_faults", lambda *a, **k: ["SENTINEL-WING-FAULT"])
|
||||
rc = dv.main([])
|
||||
assert rc == 1, "a wing fault must fail the validator"
|
||||
assert "SENTINEL-WING-FAULT" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_a_record_is_not_exempted_by_its_BASENAME(tmp_path):
|
||||
"""`_NON_DECISION_FILES` is a set of TOPIC-dir names ({README, migration-map, retrieval-eval}).
|
||||
Applying it to the wings meant a genuine record at `records/docs/retrieval-eval.md` was
|
||||
silently skipped — and that path is forced, not hypothetical: the path<->key rule puts key
|
||||
`docs.retrieval-eval` at exactly that filename. Worse, dl.active_files() applies the filter
|
||||
only to the TOPIC_DIR glob, so such a file IS a corpus source while being exempt from the
|
||||
guard."""
|
||||
records, archive = _wing(tmp_path)
|
||||
(records / "docs").mkdir(parents=True, exist_ok=True)
|
||||
(records / "ci" / "good.md").write_text(_GOOD)
|
||||
(records / "docs" / "retrieval-eval.md").write_text("# just a note, not a record\n")
|
||||
faults = dv.record_wing_faults(records, archive)
|
||||
assert len(faults) == 1 and "retrieval-eval.md" in faults[0], faults
|
||||
|
||||
|
||||
def test_archive_toplevel_is_exempt_by_IDENTITY_not_by_location(tmp_path):
|
||||
"""Exempting everything directly in `archive/` left that one directory unguarded: a new
|
||||
unparseable `archive/foo.md` would vanish silently. The exemption is for the five #610 stripped
|
||||
INDEX files, so it must test for that shape — one keyless record with a known generated
|
||||
heading — not merely for sitting in that directory."""
|
||||
records, archive = _wing(tmp_path)
|
||||
(records / "ci" / "good.md").write_text(_GOOD)
|
||||
(archive / "api.md").write_text("# api\n\n## Records formerly in this file\n\n- `api.thing`\n")
|
||||
assert dv.record_wing_faults(records, archive) == [], "a real stripped index must stay exempt"
|
||||
|
||||
(archive / "foo.md").write_text("rule: >-\n wrapped\n value\n")
|
||||
faults = dv.record_wing_faults(records, archive)
|
||||
assert len(faults) == 1 and "foo.md" in faults[0], faults
|
||||
|
||||
|
||||
def test_junk_frontmatter_key_from_a_split_value_is_faulted(tmp_path):
|
||||
"""parse-to-WRONG, the case the structural check alone cannot see. A block scalar with an
|
||||
UNINDENTED continuation containing a colon parses to ONE valid keyed record whose `rule` is
|
||||
literally `>-`, plus a junk key from the continuation — silently truncating the real value.
|
||||
PyYAML rejects this input, so the hand reader is more permissive than the writer."""
|
||||
records, archive = _wing(tmp_path)
|
||||
bad = records / "ci" / "corrupt.md"
|
||||
bad.write_text(_GOOD.replace("rule: 'a rule on one quoted line'\n",
|
||||
"rule: >-\nthe real rule: with a colon\n"))
|
||||
recs = dl.parse_file(bad)
|
||||
assert len(recs) == 1 and recs[0].key, "precondition: this parses to one KEYED record"
|
||||
assert recs[0].rule == ">-", f"precondition: the real value was truncated, got {recs[0].rule!r}"
|
||||
|
||||
faults = dv.record_wing_faults(records, archive)
|
||||
assert len(faults) == 1 and "unrecognized key" in faults[0], faults
|
||||
|
||||
|
||||
def test_an_empty_or_missing_record_wing_is_LOUD(tmp_path):
|
||||
"""A guard that reports a clean corpus from a scan of nothing is the defect it exists to close,
|
||||
applied to itself. Reachable via a partial checkout, a renamed directory, or a bad monkeypatch."""
|
||||
missing = dv.record_wing_faults(tmp_path / "nope" / "records", tmp_path / "nope" / "archive")
|
||||
assert missing and "missing or contains no" in missing[0], missing
|
||||
|
||||
records, archive = _wing(tmp_path) # exists but holds no *.md
|
||||
empty = dv.record_wing_faults(records, archive)
|
||||
assert empty and "missing or contains no" in empty[0], empty
|
||||
|
||||
|
||||
def test_a_wing_root_README_is_exempt_by_PATH_not_by_basename(tmp_path):
|
||||
"""`docs/decisions/archive/README.md` really exists (a hand-written directory README), so it
|
||||
must be exempt — but by exact relative path, not by basename.
|
||||
|
||||
An earlier version excluded ANY wing-root `README.md` on the stated grounds that no such file
|
||||
existed. That was false, and it would additionally have exempted a future
|
||||
`records/README.md` — reintroducing the basename hole one directory over, in the wing that
|
||||
matters most."""
|
||||
records, archive = _wing(tmp_path)
|
||||
(records / "ci" / "good.md").write_text(_GOOD)
|
||||
(archive / "README.md").write_text("# archive\n\nHand-written directory README.\n")
|
||||
assert dv.record_wing_faults(records, archive) == [], "archive/README.md must stay exempt"
|
||||
|
||||
(records / "README.md").write_text("# not a record\n")
|
||||
faults = dv.record_wing_faults(records, archive)
|
||||
assert len(faults) == 1 and "README.md" in faults[0], (
|
||||
f"a README in the ACTIVE wing must NOT inherit the archive exemption: {faults}"
|
||||
)
|
||||
|
||||
|
||||
def test_an_empty_ARCHIVE_wing_is_legitimate(tmp_path):
|
||||
"""Deliberate asymmetry with the active-wing check, pinned so nobody 'fixes' it into symmetry.
|
||||
|
||||
Zero active records means the scan measured nothing and any clean result is vacuous. Zero
|
||||
ARCHIVED records just means nothing has been superseded yet — normal for a young repo and for
|
||||
every fresh clone before the first supersession."""
|
||||
records, archive = _wing(tmp_path)
|
||||
(records / "ci" / "good.md").write_text(_GOOD)
|
||||
assert dv.record_wing_faults(records, archive) == [], "an empty archive must not fault"
|
||||
|
||||
Reference in New Issue
Block a user