Repository navigation
Commit bdefd49
fix(adr-index): read the whole ADR header block and fail when a regeneration drops a field (#31252)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XMyHy9Dq4o9YTEEjb9GqkHq)_
domain:devx#3 · os-project-manager · session_01FEkQ5of5Rharw5eu6Hvhes
Fixes #30916
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:`scripts/adr-index.mjs` 不再只读 ADR 前 40 行找 `**Supersedes**:`
等字段,改为读到标题后第一个小节标题为止的整个头部;另外,若重新生成的目录会把已有的 Date / Supersedes /
Superseded by 格子清空,`--report`、`--check` 和写模式都以退出码 1
失败。其它发现(目录过期、状态行读不出)仍只警告、退出 0。
- 为什么改:ADR-0146 修订时状态说明一长,`Supersedes` 被挤到第 42 行,目录那一栏悄悄变成 `—`,检查却照样通过。
- 风险与回滚:只改一个仓库工具脚本,不发布;`main` 上重新生成的目录逐字节不变。lint 的 ADR 步骤本来就由 `Lane
verdict` 读结果,所以「清空一个格子」现在会让 CI 变红;回滚即还原本 PR。
- 先看:`scripts/adr-index.mjs` 的 `headerBlock` /
`isSupersessionNotice`(解析)和 `droppedFields` / `reportExitCode`(失败条件)。
- 两分钟验证:`node scripts/adr-index.mjs --self-test && node
scripts/adr-index.mjs --check`,预期 60 条断言全绿、目录为最新。
- 需要你做的:无,席位落地。
## What changes
`scripts/adr-index.mjs` only (+180 / −22). No other file;
`docs/adr/README.md` regenerates byte-identical.
- **The header is a block, not a 40-line window.** `headerBlock(lines)`
is every line before the first heading after the title (the whole file
when there is none). `**Key**:` fields (Date, Supersedes, Superseded by)
and supersession blockquotes are read from it. A `**Key**:` line under a
section heading is body prose.
- **A supersession blockquote must state `superseded` in a bold span**
(`isSupersessionNotice`). Without that, the whole-block read misreads
ADR-0126 (see Acceptance notes). The five cloud-owned notices and the
ADR-0080/0081 amendment notes all state it in bold, so their cells stay
as they are.
- **A dropped field fails.** `droppedFields(committed, expected)`
compares the committed index's record rows with the regeneration, keyed
by link target. A Date, Supersedes or Superseded-by cell that held a
value and becomes `—` is a drop. A changed value, a filled cell and a
removed row are not. `verdict()` now returns `dropped`, and:
- `--report` prints each drop as an `::error` annotation and exits 1
(`reportExitCode`). A stale index, an unparsed record, unreadable
registries and a red self-test still warn and exit 0 (#26076, ruling A).
- `--check` prints each drop; it already exits 1 on the stale index a
drop implies.
- the write mode (`pnpm adr:index`) writes the index, prints each drop
and exits 1, so the author who regenerates sees it at once. A deliberate
drop is one rerun away: the written index no longer carries the cell.
- Self-test: two new batteries, **header block** (6) and **dropped
fields** (7). The drift battery's old pin, "modeReport must end in
`return 0`", is replaced by two pins: report exits 0 on every finding
but a drop, and `modeReport` exits only through `reportExitCode`. The
floor goes from 7 to 9 batteries; 46 → 60 assertions.
## Why
#30916: a status note grown past line 40 emptied ADR-0146's Supersedes
cell, and every mode still exited 0. The card's Expected, first branch
of each: read the whole header block, and fail when a regeneration would
drop a field the previous index carried.
## Tests
`node scripts/adr-index.mjs --self-test` → `✓ adr-index --self-test: 60
assertions` (head `d8acb85f`).
<details><summary>Ablations: each new pin goes red when the old
behaviour is put back (4 legs, all restored)</summary>
Run through `node scripts/ablation-replace.mjs` (each leg: anchor x1 →
x0, blob moved, then `git diff HEAD -- scripts/adr-index.mjs` empty
after restore; trap on EXIT restores from `HEAD`):
- **window**: `const header = headerBlock(lines);` → `lines.slice(0,
40)` → exit 1, 6 failures: `supersedes-past-line-40-is-read`,
`date-past-line-40-is-read`, `the-index-keeps-the-cell`,
`a-supersession-notice-past-line-40-is-read`,
`an-emptied-supersedes-cell-is-a-drop`,
`verdict-carries-the-drop-and-report-exits-1`.
- **notice**: `header.filter(isSupersessionNotice)` → the old `/^>[
\t]*\*\*[^\n]*\bsuperseded\b/i` → exit 1,
`a-plain-prose-mention-is-not-a-notice`.
- **report exit**: `reportExitCode` → `return 0` → exit 1,
`verdict-carries-the-drop-and-report-exits-1`.
- **no drop**: the drop condition → `if (false)` → exit 1, 3 failures:
`an-emptied-supersedes-cell-is-a-drop`,
`ui-date-and-superseded-by-drops-name-the-ui-file`,
`verdict-carries-the-drop-and-report-exits-1`.
</details>
<details><summary>The card's incident replayed on the real registry
(base vs this branch)</summary>
Copies of `docs/adr/` and `ui/docs/adr/` with seven lines added to
ADR-0146's status note, which moves `**Supersedes in part**` from line
39 to line 46:
- base script (`5b22ee75`): write mode exit 0, ADR-0146's Supersedes
cell `—` (the card's failure).
- this branch: write mode exit 0, the cell is `ADR-0126, ADR-0131,
ADR-0005, ADR-0048, ADR-0141, ADR-0070`, index unchanged.
And the drop path on the real tree: hand-setting ADR-0123's Supersedes
cell to `ADR-0099` in `docs/adr/README.md` gives `--report` exit 1 (one
`::error file=docs/adr/0123-…md` line, plus the stale warning),
`--check` exit 1, write mode exit 1 naming the cell, and a rerun exit 0
with the index restored.
</details>
## Acceptance notes
- **Which ADRs were checked.** All 160 records (`docs/adr/` and
`ui/docs/adr/`) were measured for where each `**Key**:` line and each
supersession blockquote sits relative to line 40, the first heading and
the first `---`:
- Every record has a heading after its title.
- ADR-0104 has `**Date**` at line 43, before its first heading (53). It
is now read. Its cell is unchanged because the status-line fallback
already gave the same date, 2026-07-22.
- ADR-0076 has `Supersedes the rev.9 …` at line 216, under a heading. It
is body prose and stays unread.
- **ADR-0126 is the one record the plain "up to the first heading" rule
misreads.** Line 44, a blockquote after the header's `---`, says "A
third draft of ADR-0131 marked this record Superseded". The old window
hid it by accident. Read as a notice, it adds `ADR-0131` to ADR-0126's
Superseded-by cell, and the record says it is not superseded.
- Ending the block at `---` instead fixes ADR-0126 but drops `ADR-0065`
from ADR-0080 and ADR-0081: their bold amendment notices ("**superseded
on styling**") sit after `---`, at line 14.
- The bold-span rule keeps ADR-0080, ADR-0081 and the five cloud-owned
notices, and leaves out ADR-0126's prose. With it, the regenerated index
on `5b22ee75` is byte-identical (`node scripts/adr-index.mjs --check` →
OK).
- **"Previous index" is the committed index in the tree being checked.**
In CI that is the PR's own `docs/adr/README.md`. A PR that regenerates
and commits a drop passes the check, but the write mode already exited 1
for its author, and the emptied cell is in the README diff. Comparing
against the base branch's index would need git history in the lint job;
not done.
- **The CI half holds without a workflow edit.** The `ADR index is
current` step in `lint.yml` (id `adr-index-current-report-only`) is
`continue-on-error: true`, and `Lane verdict`
(`scripts/ci/lane-verdict.mjs`) reads each gate step's `outcome`. A
drop's exit 1 therefore reds the lane and `Lint & Repo Gates`. Root
`ci.yml` is untouched.
- **One comment in `lint.yml` goes stale** (outside this claim's file
surface, so not edited). Lines 688–696 say "So the only red this step
can show is a broken generator, never a record". After this PR, a
dropped field also reds it. Named for the seat in the report's
`open_questions`.
- The banner line in the generated README ("reports a stale index as a
warning and never fails on it") stays true: a stale index alone still
never fails.
## Verification
Gate union derived by `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` on head `d8acb85f` (1 path, 31
commands), each exit captured before any pipe, reconciled with `--ran`:
- **31 run, 31 exit 0.** `--ran`: "31 derived famil(ies) accounted for —
31 run, 0 NOT-MEASURED". The slow ones: `pnpm check:pm-dispatch-gates`
449 s, `check-comment-mask-corpus.mjs` 205 s,
`check-self-test-workflow-commands.mjs` 144 s.
- `pnpm check:adr-index` (the step this PR hardens): `✓ adr-index
--self-test: 60 assertions` then `adr-index --report: OK —
docs/adr/README.md is current, every record parsed`.
- Not in the derived list, and not run locally: the `Plan Test Core
shards` CI step and the type-check lanes `dispatch-gates` names as
CI-only. This diff has no TypeScript. CI families are read from this
PR's check-runs.
`pnpm lint` scoped: no package touched; `pnpm exec eslint
--no-inline-config scripts/adr-index.mjs` exit 0.
`skip-changeset`: `scripts/**` ships in no package.
## Human Minimum
None; the seat lands it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5of5Rharw5eu6Hvhes)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 1aa4dd1 commit bdefd49
1 file changed
Lines changed: 180 additions & 22 deletions
0 commit comments