Skip to content

docs(agents): a Documentation Guardrails row for packages/*/CHANGELOG.md - #17078

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-16849-agents-changelog-guardrail-row
Sep 9, 2026
Merged

yinlianghui merged 2 commits into
mainfrom
claude/issue-16849-agents-changelog-guardrail-row

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #16849

The Documentation Guardrails table had four rows and none of them covered the one
generated, consumer-shipped documentation file that is neither a .changeset/ input nor a
content/docs/releases/ page. This adds the fifth row, directly under the
content/docs/releases/ row whose disposition it takes.

The row

| `packages/*/CHANGELOG.md` | **RELEASE-OWNED** | ❌ Never edit in a code PR — `changeset version` compiles it from `.changeset/*.md` and it ships inside the npm tarball as the text an upgrading agent greps (§ *Post-Task Checklist* step 3). Your PR's input is its **changeset**, on a hard, unwatched deadline: the release that consumes it deletes that input and publishes the sentence. Factual error in a released entry → **amend that entry in a dedicated docs-only PR**, ⛔ never an erratum in a later entry and never a rider on code changes — the reader greps the tombstoned symbol and lands on the old entry, so a correction anywhere else is one it never reaches; what shipped stays recorded in the published tarballs and in git history. |

751 bytes. The widest-row pin is 768 and is unmoved: the widest row in the file is still
the translations row at 768 bytes (line 681 after this change).

It answers the card's three questions:

  1. May a code PR edit it? No — the strict reading, the same disposition as the row above
    it. It is compiled by changeset version and it ships inside the npm tarball.
  2. Correction route for a factual error in a released entry? Amend that entry, in a
    dedicated docs-only PR. Not an erratum in a later entry. Reasoning below; this is the one
    thing on this PR that is a judgement call rather than a transcription.
  3. What is a PR's input, and until when? The changeset — on a hard, unwatched deadline.
    The release that consumes it deletes the input and publishes the sentence, and nothing
    warns anybody that the window has closed. That deadline is the whole reason finding(changeset): the pending field-rows/option-description changeset still says the canonical field-level spelling is depends_on — false for this package, and it is release-notes input #15058 and
    finding(changeset): the pending changeset still tells authors the lint accepts the canonical ListView spelling — the step-1 tightening makes every clause of that sentence false #15026 now prescribe a route that no longer exists.

The decision the card asked me to make and not leave open

A — amend the historical entry in a dedicated docs-only PR (chosen) vs
B — add an erratum in a later entry and leave the record of what shipped intact.
B is not a strawman: #16671 independently reached for the word "erratum" for exactly this
situation. Along the four axes:

1. Real business need — measured, not "reads like it helps"

The reader is named by AGENTS.md itself, in the breaking-changeset rule: this text "ships to
consumers as CHANGELOG.md inside the npm package and is what an upgrading agent greps after
the tombstone error."
That reader is a machine that arrives at one entry, by grepping the
symbol it was just refused on. It does not browse the file and it has no reason to read
forward. The writers are measured too: the two cards that produced this gap each need exactly
one sentence corrected, now sitting at packages/spec/CHANGELOG.md:1497 (#15058) and at both
packages/lint/CHANGELOG.md:436 and packages/spec/CHANGELOG.md:3641 (#15026).

The measurement that decides it: both routes have identical delivery latency. An already
published tarball is immutable, so neither an amendment nor an erratum reaches a consumer who
installed the affected version — both reach only consumers who install the next one. The
routes therefore do not differ in when the correction arrives, only in where it sits
relative to where the reader lands.
Under A it is the sentence the greper lands on. Under B
the greper lands on the false sentence and there is nothing that points it at the erratum.

2. Long-term soundness

CHANGELOG.md is a generated artifact and its generator appends; it never reconciles
hand-written matter someone added underneath. An erratum is therefore a second, un-generated
layer on a generated file that nothing keeps in step — a workaround in this repo's sense.
Amending is the shape the file already tolerates: one deliberate out-of-band hand edit, in its
own PR, exactly like a content/docs/releases/ correction. Contract-first points the same
way: the contract the breaking-changeset rule states is that the entry which removes a symbol
carries its FROM → TO mapping
. B leaves that contract broken on the entry that owns the
symbol and satisfies it somewhere else; A restores it where it was declared.

The honest cost, stated plainly: under A the file on main stops being byte-identical to what
each version shipped. That property does not vanish — it is held by the published npm tarballs
(immutable, one per version) and by git history — but it stops being held by the working file,
and anyone reading the working file as the shipping record has to go to those two places.

3. Making it structurally harder for an AI to get this wrong

This is the axis with the widest gap. The failure being corrected is an AI acting on a false
migration sentence. B keeps the false sentence and adds a true one elsewhere: two live
readings of one fact, reconciled by the reader. That is the tolerant shape this project rules
against — tolerance is where AI mistakes get covered up in bulk. A leaves one spelling.

B also declares a route the runtime does not honour. Nothing back-links an entry to a later
erratum; building that link is out of scope here by the card's own words (the "nothing notices
when a card's .changeset/ path is consumed" question is filed separately). Declaring an
erratum route is declaring a capability nothing delivers.

And A is mechanical where B is a judgement call: A says correct the sentence where it is, in a
docs-only PR, never as a rider. B additionally asks the author to choose which later entry
and to phrase a back-reference — two decisions an AI will make inconsistently across the
repo's packages/*/CHANGELOG.md files.

4. Startup stage — do not proliferate

B invents a document form that does not exist in this repo today, and to be usable it pulls in
a placement convention, a back-link, and eventually a gate. Nothing is pulling on it: the
demand is two sentences on two cards. A adds no new form at all — it reuses the route already
written in the row immediately above, which any agent reading the table has just read. The
"no gradual transition" ruling points the same way: B is precisely the "both readings stay
live, the reader reconciles" shape that ruling refuses for spellings and retirements.

Where they conflict: axis 1's counter-consideration — the file stops being a record of
what was actually published — is real, and it is the whole of B's case. It does not carry,
because that record is held by npm's immutable tarballs and by git, while the working file's
one measured reader is a greper that B cannot reach. It is a one-line row; flipping it to B is
a one-line edit.

What paid for the line

AGENTS.md sits at the line ratchet's ceiling with headroom 0 (1068/1068), so the row is
paid for by deleting content in the same file, in the same section:

-A `.describe()` string counts — it lands in `content/docs/references/`. Adding one
-export counts — it lands in `api-surface/`. Don't match by hand — one command runs
-**every** gate and reports **all** stale artifacts at once:
+Don't match by hand — one command runs **every** gate and reports **all** stale
+artifacts at once:

119 bytes of prose removed; no rule lost. Every fact in the two deleted sentences has a
home, and all of them are inside this same section:

deleted fact where it already lives
a .describe() string counts as a change the regeneration table three lines above: A .describe() / TSDoc on any schema → check:docs → gen:schema && gen:docs
it lands in content/docs/references/ guardrail row 1 of the table this PR extends: AUTO-GEN, regenerated by packages/spec/scripts/build-docs.ts
adding one export counts as a change the next row of the same regeneration table: A public export (added / removed / renamed) → check:api-surface → gen:api-surface
it lands in api-surface/ the check:exported-any bullet further down this section: "the api-surface/ snapshot records that an export exists, never what it resolves to"

The sentence that survives is the paragraph's operative instruction — and it is the one that
tells you not to hand-match the very rows the deleted sentences were hand-matching. The
paragraph reads better without them.

This is not a re-wrap. 119 bytes of prose were deleted at the paragraph's existing wrap
width (about 82 columns); the surviving 96-byte sentence occupies two lines. The line was
bought by the deletion, not by re-flowing.

Evidence

All readings taken on the final commit, 3f72809c7 (merged origin/main 854639b31).

Line ratchet — the two verdict lines the gate printed, both green:

✓ check-skill-line-ratchet: AGENTS.md: widest table row is 768 bytes (pin 768; headroom 0).
✓ check-skill-line-ratchet: AGENTS.md is 1068 lines (ceiling 1068; headroom 0).

wc -l AGENTS.md = 1068, before and after, and after the merge of origin/main.
The table went from 4 rows to 5; the new row is line 680.

Governed surface — node scripts/pm/check-governed-merges.mjs --test AGENTS.md, exit 3:

governed-surface predicate: 1 of 1 path(s) hit the register (5 surfaces, repo-agnostic).
  ⛔  GOVERNED — a human merge is the review record for this PR (#9495 regime).
      No seat flips it ready, enqueues it, or arms auto-merge (AGENTS.md Prime Directive #14).
      One hit governs the whole PR — 「混合 diff 一条命中即整 PR 分叉」; proportion is not a question.
      AGENTS.md ×1 — the repo-root agent instruction file
        - AGENTS.md

So: draft, human merge, no queue, no auto-merge, no seat approval.

Gates — derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
on the final tree (change set: AGENTS.md, 1 path). All 14 derived families run, all exit 0,
plus pnpm check:doc-authoring run beyond the derivation:

gate exit
node scripts/check-closing-keyword-parity.mjs 0
node scripts/check-closing-keyword-parity.mjs --self-test 0
node scripts/check-comment-mask-corpus.mjs 0
pnpm check:agent-test-spelling 0
pnpm check:docs-audit-scope 0
pnpm check:driver-memory-census 0
pnpm check:nul-bytes 0
pnpm check:pm-governed-merges 0
pnpm check:pm-governed-prose 0
pnpm check:pm-skill-id-lint 0
pnpm check:pm-skill-ratchet 0
pnpm check:refd-timer-probe 0
pnpm check:required-contexts 0
pnpm check:watch-hint-literal 0
pnpm check:doc-authoring (beyond the derivation) 0

Reconciled with --ran:
✓ dispatch-gates --ran: 14 derived famil(ies) accounted for — 14 run, 0 NOT-MEASURED.

Repo-wide lint, narrowed and the narrowing proved. The linted universe is read from
eslint's own config, not guessed: eslint.config.mjs scopes every one of its seven blocks to
{ts,tsx,mts,cts,js,jsx,mjs,cjs}. Markdown is in none of them. Measured with
npx eslint --no-inline-config --format json AGENTS.md: one result, zero files actually
linted
— "File ignored because no matching configuration was supplied.", errorCount: 0.
Invariance: type-aware linting is not what decides this — the diff's single path is outside
every files glob in the config, so it cannot move the verdict on any untouched file. A
repo-wide pnpm lint is CI's run, and this narrowing excludes nothing.

Package build / test: none owed. The diff touches no package, so there is no dependency
closure to build and no affected package to test. The diff edits no gate or tool script, so no
checker's own suite is owed either.

Control bytes: pnpm check:nul-bytes green, plus a self-scan of the changed file —
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' AGENTS.md exits 1 (no match).

skip-changeset: nothing published moves. AGENTS.md is a repo-root instruction file; no
package's files[] can reach outside its own directory, so it ships in no tarball.

维护者速读(草稿)

改了什么 — AGENTS.md 的 Documentation Guardrails 表加了第五行,管 packages/*/CHANGELOG.md。
这一行说三件事:代码 PR ❌ 不改它(它由 changeset version 从 .changeset/*.md 编译出来,
并随 npm 包发运);PR 的输入是 changeset,而这个输入有一条无人看守的硬截止——发版消费它的那一刻,
输入被删掉、句子被发布出去;已发布条目里的事实错误,在原条目上就地改正,走专门的 docs-only PR,
不用「在后续条目里写勘误」。表从 4 行变 5 行,新行 751 字节,放在 content/docs/releases/ 那一行下面。

为什么改 — 这个洞是量出来的,不是预想的。#15058 和 #15026 两张卡当初写的修法是
「改 .changeset/*.md 里的一句话」,这在守则里是明确允许的;然后一次发版把两个 changeset 都消费了,
两句错话从可编辑的输入变成了已发运的 CHANGELOG,卡上写的路线就此不存在,而接替的路线没人写过。
更尴尬的是:AGENTS.md 唯一提到这个文件的地方(破坏性 changeset 那条)恰恰说明它是发给消费者、
被 agent 读
的,而管「什么能改」的那张表里一个字都没有。#16671 是同一类还在窗口期内的活例子。

风险与代价(含回滚) — 代价集中在「就地改正」这个选择上:main 上的 CHANGELOG 文件
不再逐字等于「当时实际发布了什么」。这个属性没有消失,它由 npm 上每个版本不可变的 tarball 和 git
历史承担,但确实不再由工作文件承担——谁以前把工作文件当发运记录读,得改去这两处读。
反过来,勘误方案保住了这个属性,代价是读者得自己找到勘误:而实测的读者是一个照着墓碑错误
grep 符号的升级 agent
,它落在旧条目上,不会往后翻,也没有任何机制把它指过去。两条路线到达消费者的
延迟其实一样(都只影响下一次发布的 tarball),差别只在「更正放在读者落点上,还是放在别处」。
回滚成本极低:这是一行表格,翻成勘误方案是一行改动;整个 PR 只动 AGENTS.md 一个文件,git revert 即可。
另一项代价是付账:棘轮 1068/1068 无余量,新行由同一节里删掉的两句话买单(那两句严格复述了它们上方三行的表格)。

席位意见 —

你要做的 — 只需要回一个字母:A(已选:在原条目上就地改正)还是 B(改成在后续条目加勘误)?


Generated by Claude Code

…ls row

The table said nothing about the one generated, consumer-shipped documentation
file that is neither a `.changeset/` input nor a `content/docs/releases/` page,
so two cards prescribed a correction route that a release had already closed.

The new row states three things: a code PR never edits it (it is compiled by
`changeset version` and ships inside the npm tarball); the PR's input is its
changeset, on a hard, unwatched deadline that the consuming release closes; and
a factual error in a released entry is amended in place, in a dedicated
docs-only PR, rather than corrected by an erratum in a later entry — the reader
is an upgrading agent grepping the tombstoned symbol, and it lands on the old
entry, so a correction anywhere else is one it never reaches.

Paid within the file, per the shrink-only line ratchet (1068/1068, headroom 0):
the two example sentences under the `packages/spec` regeneration table restated
that table's own first two rows, three lines above them, and the sentence that
survives tells you not to hand-match those rows in the first place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLw5aKDPR5RJgyUR7Exkd

Copy link
Copy Markdown
Collaborator Author

维护者速读 — PR #17078(#16849,skills 席终稿,2026-09-09T06:52Z)

改了什么 — AGENTS.md 的 Documentation Guardrails 表新增一行 packages/*/CHANGELOG.md(RELEASE-OWNED):代码 PR 不改它;PR 的输入是 changeset,而这个输入有一条没人看守的截止——发版消费的那一刻它被删掉、句子被发布;已发布条目里的事实错误 ⇒ 在专门的 docs-only PR 里就地修正那条,⛔ 不在后续条目加勘误。为了不超行数上限(1068/1068),同节里删掉了两句对上面再生表格的复述(.describe() 落到 references、新增导出落到 api-surface),事实都还在表格本行里。一文件 +3/−3,行数与最宽表行都不动。

为什么改 — #15058 与 #15026 两张卡都写着「改一行 pending changeset」,守则允许;结果发版把 changeset 消费掉了,错句进了发运的 CHANGELOG,而表格里没有一行说这个文件该怎么办——分诊按更严读法处理了那两张卡并立了本卡。

风险与代价(含回滚) — 纯指令文本;回滚 = revert 本 PR。真正的取舍是 A(就地修正历史条目)vs B(后续勘误,保留发运原文):dev 与本席都选 A——唯一实测的读者是升级中 grep 墓碑符号的 agent,它只会落到那一条旧条目,勘误放在别处它永远读不到;「发运了什么」的记录由 npm 不可变 tarball 与 git 历史承担。B 的代价是真实的(main 上的文件不再逐字节等于发运物),但它保护的记录另有家。

席位意见 — 接受。本席在分支头上复核:棘轮 1068/1068、最宽行 768 未动(新行 752 B);删掉的两句在同节表格里各有家;id-lint 干净;控制字节 0;受管判定 exit 3;mergeable: true;CI 尚在收敛(06:51Z 四个 job 进行中)。

你要做的 — 合并;或由 os-zhuang / hotlong 批准,本席随即入队。只需答一字:A(就地修正,已落地)还是 B(勘误)——选 B 就改那一格。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 9, 2026 07:45
@yinlianghui
yinlianghui added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 9cb6f51 Sep 9, 2026
35 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-16849-agents-changelog-guardrail-row branch September 9, 2026 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs-user-decision size/xs skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants