Live docs point into the corpus BY DATE -- "see `decisions.md` 2026-07-10" --
about 32 such references across 12 files, plus the same form in historical issue
comments. The split would dangle every one of them.
Each stripped file now ends with a generated "Records formerly in this file"
index: date, title, and a link to the record's new path. A reader following a
date pointer lands on the file it names and resolves from there. That is far
cheaper and less error-prone than rewriting 32 references by hand, and it also
covers the issue-comment breadcrumbs, which cannot be rewritten at all.
Caught while verifying it: the generated `## Records formerly in this file`
heading is itself an H2, so the record parser counted one legacy-unmigrated
record per stripped file -- the notice went 0 -> 6. Same treatment as the
existing `## Index` section: skip it by name. SKIP_HEADINGS moved to
decisions_lib as the single source of truth, since three modules now need it.
Found by reading the validator's notice output on a trial migration, not by
inspection -- the corpus still validated OK, so nothing else would have flagged it.
The migration is a MOVE, so correctness is provable rather than reviewable:
parse the corpus before, migrate, parse after, assert the Record sets are
identical field-for-field with byte-identical rationale.
scripts/migrate_decisions_split.py
166 records -> docs/decisions/records/<area>/<topic>.md (active) or
docs/decisions/archive/<area>/<topic>.md (superseded/retired), 26 directories.
Path is DERIVED from the key, so identity stays the key. Refuses to run if any
record lacks a key, and aborts on a destination collision.
Source files are KEPT, stripped to their narrative -- not deleted. 506 lines of
the corpus sit outside any record: decisions.md's lifecycle-schema header (the
status vocabulary, supersession rules, the edit-token contract) and each topic
file's preamble explaining why those records exist. For most of it that is the
only copy. It also cannot be filed per-area -- topic files hold several areas
and 4 of 23 areas span several files -- so the files themselves stay.
scripts/tests/test_migration_equivalence.py
Runs the real migration against a COPY of the real corpus in tmp_path, never
the working tree. Asserts: all 166 records survive with the same keys, every
metadata field round-trips, titles round-trip from headings, every rationale
body is byte-identical, path matches key, archived records land in the archive
wing, the legacy files keep their narrative, and no parseable record is left
behind in them.
Proven non-vacuous: corrupting one migrated record's prose is caught by the
byte-identical check, and deleting one is caught by the survival check.
One test-authoring note: an early assertion string-matched "## " to prove no
records were left in decisions.md. That is wrong -- the schema header quotes an
illustrative "## 2026-07-17 ..." example in prose. Whether records remain is a
PARSING question, so the parser-based leftover test is the real invariant.