fix(603): close four defects found in adversarial review of the stale-after fields

1. `date.fromisoformat` is not a YYYY-MM-DD validator. On Python >= 3.11 it also
   accepts ISO basic format ("20270101") and week dates ("2027-W01-1"), so a
   malformed-looking value passed the blocking check — and which forms parse
   depends on the interpreter, meaning the same corpus could validate differently
   on a dev machine and on the runner (pr-checks.yml pins only python-version
   '3.x'). Knock-on: the catalog's Review-due section sorts on the raw STRING, so
   an accepted "20270101" sorted AFTER "2027-01-15" ('-' < '0'), contradicting the
   section's own "sorted soonest-first" text. Gate on ^\d{4}-\d{2}-\d{2}$ first,
   which fixes both — a fixed-width zero-padded form makes string sort == date sort.

2. A present-but-empty `stale-after:` was collapsed to None by `or None` in the
   parser and then skipped by a truthiness guard in the validator, so it passed as
   "absent" — a field that silently never fires, which is the exact failure mode
   the blocking check exists to prevent. Keep "" distinct from None and test with
   `is not None`.

3. `test_catalog_is_date_independent` was partly vacuous: with no date in either
   render, both sides were trivially equal after the .replace(). It did still catch
   an injected clock-derived marker, but it passed with the feature deleted. Assert
   the dates are present.

4. The malformed-date check ran only over the active set, exempting archive
   records. Staleness is moot there, but a typo is still a typo — check both wings.

Adds regression tests for each, plus a Review-due row for a topic-file record
(pinning the `../decisions.md` vs bare-filename link forms).
This commit is contained in:
2026-07-25 15:22:50 +02:00
parent 0ad02db651
commit 75139bbf31
5 changed files with 77 additions and 5 deletions
+5 -1
View File
@@ -84,7 +84,11 @@ def parse_text(text: str, source: Path) -> list[Record]:
rec.since = meta.get("since") or None
rec.supersedes = meta.get("supersedes") or None
rec.superseded_by = meta.get("superseded-by") or None
rec.stale_after = meta.get("stale-after") or None
# NOT `or None`: a present-but-empty `stale-after:` must stay "" so the validator can
# tell it from an absent field and reject it. Collapsing the two would let a blank or
# truncated value through as "absent" — a field that silently never fires, which is
# exactly what the blocking format check exists to prevent.
rec.stale_after = meta.get("stale-after")
# The metadata block is contiguous: scan only until the first blank line, so a
# bolded **Rule:** appearing later inside rationale prose can't overwrite the real one.
for bl2 in body_lines[k + 1 :]: