Skip to content

Commit bdefd49

Browse files
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

File tree

0 commit comments

Comments
 (0)