Repository navigation
fix(scripts): refuse a PR that modifies or deletes a changeset it did not add (#17712) - #18146
Conversation
… not add (#17712) A changeset filename is content-free, so a collision looks like nothing: the changesets default word-pair names were designed for one human running the CLI at a time, and this repository runs many agents in parallel drawing from the same small name space. Overwriting somebody else's changeset produces a perfectly valid changeset file, so every parse-shaped gate stays green on both sides -- the sibling PR's release note is silently replaced, its own CI never re-runs, and the loss surfaces at release time in the generated CHANGELOG with the authoring PR long merged. check-empty-changeset.mjs now answers a second question in the same run: a `.changeset/*.md` that exists on the merge base and was not added by this PR may be neither modified nor deleted. "Added by this PR" is the file's absence on the merge base, which is what git's status letters already say, so the rule is content-blind and legacy names are untouched. Rename detection is off for this pass (the opposite of rule 1's `AMR`): renaming somebody else's changeset deletes their release note at its path, and with detection on that deletion is folded into an `R` row and disappears. Wired into check-empty-changeset.mjs rather than a new script so it inherits the `changeset-check` job's merge base, its `changeset-release/main` exemption -- `changeset version` deletes every changeset, so the release PR would be structurally unsatisfiable otherwise -- and its self-test's place in lint.yml, with no workflow diff at all. Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU Co-authored-by: Claude <noreply@anthropic.com>
复核通过 —— 已 undraft 并武装(
|
| 腿 | 构造 | 结果 |
|---|---|---|
foreign M |
改写 .changeset/12271-published-entry-no-auto-transpile.md(base 上已存在、非本 PR 所加) |
exit 1 ✅ —— 点名该文件,并逐字打出裁决的补救语 Remedy: rename yours; restore theirs from base.,还带 ::error file= 注记 |
own A → own M 跨提交 |
提交1 新增自己的 changeset,提交2 改写它 | exit 0 ✅ —— 且输出写着「1 declaring changeset(s) added」,证明夹具真的生效了 |
foreign D |
git rm 掉 base 上别人的那份 |
exit 1 ✅,同一条补救语 |
git commit -- <path> 对一个未 git add 的新文件直接失败,夹具没进提交,门禁看到「0 added」而绿 —— 那个绿 ⛔ 不是读数,是夹具没生效。加了 git add 重跑才得到上表。一次没落地的构造不是一次测量,本班又一次。
还原按 hash 结算,⛔ 不靠 trap:HEAD 回到 eeb4012a,git diff HEAD 干净,git status --porcelain 0 行。
其余核过的
node scripts/check-empty-changeset.mjs --self-test→ exit 0,147 assertions over real temp git repos;新增 29 例电池并把 roster floor 由 21 抬到 22 ⇒ ⭐ 删不掉而不被发现check-clause2-carriers --pair 18146→ 两载体一致、无放宽征兆 ✅- CI:33 项,RED: none;未挂
needs:contract-review✅;skip-changeset打的是标签 ✅ - 关闭关系:PR body 带
Closes #17712✅
⭐ dev 对卡的一处更正,本席采纳并记账
卡里写「两个 PR 都绿着过去了」——多说了一行。实测:一个只覆盖别人的 changeset 而自己不加的 PR,ADDED=0,pr-automation.yml 的「Require a changeset」那步今天就会红 —— 但它叫错了问题的名字,补救语是"加个 changeset",而覆盖本身原封不动留在那里。真正全绿到 main 的是另外两种形状(覆盖别人 且 加自己 / 删别人 且 加自己,ADDED=1)。⇒ 卡的结论成立,理由要收窄一行。
⚠️ ⚠️ 本席不裁的一问,交给接手席位 —— 这是本车道现在唯一一条会丢的东西
dev 提出且本席认为确实悬空:裁决没有给"故意去更正别人已在 main 上的 release note"留任何机械出口(写错了 bump、写错了包)。门禁按设计是内容盲的,所以连这种也一并拒。三个选项与 dev 的建议:
- A —— 不留机械出口。改一条已在 main 上的 release note 是发布决定,走人工确认。(dev 建议 A;本席同意其理由:A′ 之所以压过 B,正因为"散文级保护"就是刚刚失效的那一类,加一个豁免标签等于把那一类原样请回来。)
- B —— 专用标签(如
edits-foreign-changeset)只豁免这条轴。 - C —— 仅当 PR 的 diff 只含
.changeset/*.md时放行。(dev 判 C 最弱,本席同意:"只动 changeset"对它要抓的那次碰撞同样成立,只要肇事者顺手删掉自己那份。)
⛔ 本席停席中,不代裁。 而且它不阻塞本 PR:门禁在 A 下就是正确的,已武装。
needs-user-decision 标签的问题 —— 正是本车道巡检里 H52 那一族(「一个只有维护者能答的问题,立了却谁也够不着」)。⇒ 接手席位就座后的第一批动作里应当包含给它一个归宿(送分诊/总监,或打上 needs-user-decision),⛔ 不要让它跟着 PR 一起被 Closes 关掉就此消失。本席已把这条写进 #6023 的交班附录。
Generated by Claude Code
Counter-fact measured on the ruled behaviour — please read before this merges
The incoming seat re-took this PR's blast radius against real history rather than against fixtures. The gate is correct against its ruling and its self-test is strong. What follows is not a defect in the implementation — it is a reading about the ruled rule itself that the ruling did not have. What was measuredWindow: 30 days of 9 non-Version-Packages commits carry an Each was replayed through this PR's actual gate: a throwaway worktree detached at the historical commit, this branch's
The five that would be hard-redded: #18011 · #17845 · #17709 · #16750 · #16636. Why this is not just frictionThe 4 exempt ones are exactly the "deliberately correcting somebody else's release note" case the PR body's 验收备注 already records — and in practice every one of them carried The 5 are a different and unrecorded class: a PR that changes behaviour a pending release note describes, and corrects that note in the same stroke. Read This is the repo's own recorded defect class (a diagnosis naming a cause that did not occur), met from the remedy side. What this seat is and is not doing
Nothing above asks the author for a change. The one question it raises — whether the remedy text should name the legitimate co-edit, and whether the 5-per-30-days class needs a route at all — is a gate-strength decision and goes to the maintainer. Generated by Claude Code |
|
| 原报(错) | 真 30 天窗口 | |
|---|---|---|
| changeset-touching 提交 | 429 | 2020 |
foreign M/D 提交 |
9 | 28 |
| Version Packages(结构性豁免) | — | 4 |
可用 skip-changeset 豁免 |
4 | 10 |
| 无法豁免 ⇒ 硬红 | 5 | 14 |
发火对照:同窗口 --diff-filter=A 的 changeset 行 2070。
结论方向不变,只是更强
裁决 D(只改补救语)不受影响 —— 被那句错误补救语误导的不是每月 5 个 PR,而是 14 个。承重实例 ed7243d52 也不受影响,dev 用直接读取(git cat-file -e ed7243d52^:.changeset/aggregate-field-type-compatibility.md 成功)替换了本席的推断,结论相同。
根因
本席第一次跑的是 git-history.mjs log 而非 count,而那一次调用同时在给浅检出做 deepen。工具 exit 0,但本席收到的输出是截断的。afe1c4e0a1794a5681method: git log … —— 提交 hash 从中间断掉、method: 行黏在其后。
⇒ ⛔ exit 0 不保证你收到的行是全部,浅检出上触发 deepen 的那一次调用尤其如此。先 count 定量级,再 log 取清单。
⛔ 本更正不改本卡任何标签与状态。
Generated by Claude Code
…validate --strict` narrowing an at-tier review exhibited (objectstack-ai#18867) Fixes objectstack-ai#18823 Clause-②: no `.changeset/18677-validate-per-package-authoring-pass.md` — PR objectstack-ai#18769's still-pending, still-unreleased entry — declined its `**BREAKING**` banner on the strength of a negative, verbatim: *"⛔ **not** declared breaking, because the narrowing could not be exhibited and is bounded by an existing gate"*, alongside *"**No newly-refused input could be exhibited on any fixture**"*. The negative is false. This PR edits that one file: it adds the banner, the measured table, the mechanism that explains why the union fold cannot see the finding, and the ADR-0087 disposition the banner owes — and it narrows the sentence an upgrader would have been misled by. ⛔ A forward edit to an **unpublished** file. ⛔ Not a rewrite of landed history, and ⛔ not a ruling on objectstack-ai#18677's landed `Clause-②: no` — see "Boundaries" below. ## 1. The clock, re-confirmed at this branch's base `be7aeb8275` — ⛔ not inherited The card is `priority:p1` because the entry is unconsumed. Three readings, all taken here: | reading | value | |:--|:--| | the file on `origin/main` | present (`git ls-tree`) | | `npm view @objectstack/cli version` | **17.4.0** | | `packages/cli/package.json` version on `origin/main` | **17.4.0** — equal, so no release has bumped it | | the entry's own distinctive sentence in `packages/cli/CHANGELOG.md` | **0** hits | ⇒ unconsumed. Amending it now costs a diff; amending it after the release that consumes it costs a published version number that cannot be recalled. ## 2. ⭐ The reviewer's table, REPRODUCED here — ⛔ not copied The measurement is PR objectstack-ai#18813's isolated at-tier contract review (record `5722342660`, finding **F2**). The card's first instruction is to reproduce it rather than build on it. Driven in this worktree, on the `CONFIG_FLIP` two-package fixture planted verbatim from `packages/cli/test/lint-per-package-authoring-parity.test.ts` (lines 115-166, sha256 `d5fb835ac5…`), through `packages/cli/bin/run-dev.js` with tsx: | `os validate --json --strict` on `CONFIG_FLIP` | exit | warnings | |:--|:--|:--| | `packages/cli/src/commands/validate.ts` restored to its pre-objectstack-ai#18769 blob `bafa54b07f` | **0** | 0 | | at head (blob `340cec6253`) | **1** | 1 | **It reproduces.** The one warning is `field-no-consumers` at `package 'com.example.ppflip.core' — object "pp_account" · field "industry"`. Ablation hygiene, because a mutation that never reached disk reads exactly like a clean run: - the mutation was proven on disk before the CLI was driven — `git hash-object` on the file returned `bafa54b07f…`, equal to the target blob, and the marker count `runPerPackageAuthoringRules` moved **3 → 0** in that file; - restore is `git checkout HEAD -- packages/cli/src/commands/validate.ts` from a `trap … EXIT INT TERM` with an absolute path, verified by hash equality back to `340cec6253…` **and** by `git diff HEAD` being empty, not by an exit code; - `git status --porcelain` is empty after the run. ⛔ No landed code is modified by this PR — the ablation is a one-off measurement, and `validate.ts` is untouched in the diff. Extra reading this PR took that the record did not, and the changeset now states: the **default (non-strict)** face of `os validate --json` on the same fixture at head is **exit 0 with 1 warning**. So on this fixture only the `--strict` door moved. ## 3. What the file now says - `**BREAKING**` banner naming the door that moved: `os validate --strict` can now fail a project it passed before. - The before/after table above, plus the named finding and the fact that `os build` already reports it — so the narrowing is still bounded by the command that ships. - The remedy an upgrader owes: drop `--strict` to keep the old verdict, or fix what the per-package pass reports. - ⭐ The mechanism, which is what replaces the false negative: `packageBodyAsStack` hands each package the artifact's whole `packages[]` as **resolution context**, so a cross-package *reference* still resolves and the reference-integrity rules stay quiet — but a **reachability** rule asks what the *stack* reads, and per package the stack is that one package's own body. A field whose only consumer lives in a sibling package is live to the union run and inert to the per-package run. That is the shape the earlier fixtures could not produce, and it is why "could not be exhibited" was a statement about the fixtures rather than about the property. - An `adr-0087:` disposition marker, in the HTML-comment form the gate reads, claiming `not-required (no-migration-prescription)`: nothing an author writes changes, so `objectstack migrate meta` has nothing to rewrite. - The sentence "nothing that builds today stops validating" is removed. It was true of the default face and false of `--strict`, and it is the sentence the card names as the one that would mislead an upgrader. - Level is untouched at `minor`. ⛔ Nothing here asks for `major`; the launch window refuses it and the banner plus the disposition are what carry breaking-ness. ## 4. This PR's own changeset — MEASURED, with controls both ways The question "does a PR that only edits a changeset file publish anything?" was answered by running `changeset version` in a throwaway comparison worktree at this branch's base and reading `packages/cli/CHANGELOG.md`, which `packages/cli`'s `files[]` ships (`["dist","README.md","CHANGELOG.md"]`). Four legs: | leg | resulting `@objectstack/cli` version | `packages/cli/CHANGELOG.md` | |:--|:--|:--| | base, untouched | 17.5.0 | sha256 `d132f18a05…` | | **negative control** — base + an edit to a file no package ships (`scripts/check-adr-0087-registration.mjs`) | 17.5.0 | **byte-identical** to base | | **this PR** — base + the changeset amendment only | 17.5.0 | sha256 `10ccd96910…`, 20 diff lines, **all inside the entry objectstack-ai#18677 already schedules** | | **positive control** — base + a NEW changeset (`'@objectstack/cli': patch`) | 17.5.0 | one **new bullet** appears: a release entry of its own | ⇒ Two facts, and they point in different directions, so both are stated: 1. This PR **does** move bytes that ship. ⛔ It is not true that editing a changeset publishes nothing. 2. This PR **declares no release of its own** — no new entry, no version movement — which is exactly the wording the `changeset-check` job uses for the `skip-changeset` exemption, and it is what the positive control above makes visible rather than assumed. ⇒ **Route taken: `skip-changeset`.** Of the three routes the `Check Changeset` log itself names, route 3 (an empty-frontmatter changeset) is closed — newly added ones are rejected (objectstack-ai#5471) because an all-empty set makes `changesets/action` return green while publishing nothing (objectstack-ai#4898). Between the other two, the positive control above is what decides it: adding a real changeset would add **one new published CHANGELOG bullet** — a user-facing release note announcing a correction to the release note directly above it — while moving no version. This PR amends the prose of a bump that is **already declared**; it adds none. That is the exemption's own wording.⚠️ **The label is not on this PR yet.** `node scripts/pm/label-write.mjs --issue 18867 --repo objectstack-ai/objectstack --add skip-changeset` was refused by this session's local permission classifier (reason: `[CI Bypass]`) before any request was issued — ⛔ not by GitHub, and ⛔ no status code was reached. This dev did ⛔ not route around that refusal through a second channel. **The measurement is above and the route is declared; applying the label is left to the dispatching seat.** Until it is applied, the `Check Changeset` red below stands. ## 5.⚠️ The pending-note correction still needs a person's word — ⛔ and the red on this PR is NOT the gate that asks for it Run locally, `node scripts/check-empty-changeset.mjs --base origin/main` exits **1** here, naming this file and this class: > DELIBERATE CORRECTION -- your change may have made this PENDING release note false, and you rewrote it in the same stroke. Remedy: do NOT restore it -- say so on the PR and get it confirmed; restoring it from the base would put the false sentence back. and closing: > Correcting a pending release note is a decision about a release rather than a refactor -- say so on the PR, naming the note and what changed under it, and get it confirmed. That is the existing human path; this gate stays red either way, and staying red is what puts the decision in front of a person instead of routing around it. **So, saying it, as the gate asks:** - **The note:** `.changeset/18677-validate-per-package-authoring-pass.md`, PR objectstack-ai#18769's, pending and unreleased. - **What changed under it:** ⛔ nothing in the code. What changed is the **evidence**: a fixture that did not exist when that note was written (`CONFIG_FLIP`, shipped by PR objectstack-ai#18813) exhibits the newly-refused input the note declared unexhibitable. The note's runtime claims are otherwise untouched and undisputed. - **What is asked:** confirmation that this pending release note may be corrected in place. This PR stays **draft** until then.⚠️ **A correction to an earlier reading in this very PR, stated rather than quietly dropped.** This section first argued that `skip-changeset` must be withheld so the refusal above would stay red on CI and summon a person. **Measured on this PR's own failing run** (job 105457719184, step list read from the Actions API, ⛔ not inferred from the check name), that argument is false: | step | outcome | |:--|:--| | 11 · Require a changeset (or the skip-changeset label) | **failure** | | 12 · Reject an empty-frontmatter changeset added by this PR — where `check-empty-changeset --base` runs | **skipped** | | 13 · Require an ADR-0087 disposition on a declared-breaking changeset | **skipped** | | 14-15 · allow-major re-read, major guard | **skipped** | Step 11 short-circuits the job, so the foreign-changeset refusal **never executes on CI at all** — labelled or not. Withholding the label therefore hides nothing and reveals nothing; what it does instead is leave a *misleading* headline red ("This PR adds no changeset ... run `pnpm changeset`") on a PR that correctly adds none. ⇒ the refusal's "say so on the PR and get it confirmed" is a **prose-and-person** requirement, discharged by this section, ⛔ not by a CI red that does not happen.⚠️ **What the label costs, so nobody reads a green board as more than it is:** with `skip-changeset` on, the whole `changeset-check` job is exempt, so steps 12 and 13 do not run here either. Both were run locally at `1056c00195`: `check-empty-changeset --base origin/main` exit **1** (by design, the refusal quoted above) and `check-adr-0087-registration --base origin/main` exit **0**, reading `.changeset/18677-…md [BREAKING] not-required (no-migration-prescription)`. The live ADR-0087 check also runs over the whole pending stock at RC-cut time (`cut-rc.yml`, `--base $SNAPSHOT_SHA`), so the disposition is still checked before any release consumes this entry — just not on this PR.⚠️ For context, the two landed precedents for this act — objectstack-ai#18126 (the same repair, BREAKING banner + ADR-0087 disposition onto a pending entry) and objectstack-ai#17851 — both carried `skip-changeset`. Measured, not recalled: the foreign-changeset refusal landed in objectstack-ai#18146 at `0ffb4963e5` **2026-09-14T06:56:58Z** and objectstack-ai#18126 merged at `f3b41e87d4` **2026-09-14T04:04:11Z**; `git merge-base --is-ancestor 0ffb496 f3b41e8` exits **1**, with a control leg (`f3b41e87d4^` against `f3b41e87d4`) at exit **0** on a non-shallow checkout. So the refusal post-dates both by about three hours and ⛔ neither is precedent for it — which is exactly why the reading above was measured on this PR rather than borrowed from them. ## 6. Verification | what | result | |:--|:--| | `node scripts/check-adr-0087-registration.mjs --base origin/main` | **exit 0** — `.changeset/18677-…md [BREAKING] not-required (no-migration-prescription)` | | `node scripts/check-changeset-no-major.mjs --base origin/main` | exit 0 | | `node scripts/check-empty-changeset.mjs --base origin/main` | **exit 1 — by design, see §5; it does not run on this PR's CI, labelled or not** | | the other 15 commands from `scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (self-tests, `check:nul-bytes`, `check:published-files`, `check:objectui-changeset`, `check:pm-changeset-deadline-census`, …) | all **exit 0** | | `pnpm lint` — the whole repo, `eslint . --no-inline-config`, ⛔ not narrowed | **exit 0** at `1056c00195` | | control characters | `grep -naP` over the changed file for `[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]`: no hits | `dispatch-gates.mjs` does not name the `pnpm lint` family; it was run anyway, unnarrowed, so no narrowing argument is owed. Its own provenance line reads `objectstack-ai/objectstack` at `1056c00195`, and `--repo` was asserted and held. No package test or typecheck is owed: the diff touches no package source, no `exports`, no spec contract and no built artefact. `packages/cli`'s dependency closure was built only to drive the CLI for §2. ## Boundaries — what this PR deliberately does not do - ⛔ **It does not touch `validate.ts` or any landed code.** The diff is one file. - ⛔ **It adds no `Clause-②:` line to the changeset body**, though the sibling `.changeset/18778-lint-per-package-authoring-pass.md` carries one. Writing `yes (narrowing)` into objectstack-ai#18677's note would be a retro-correction of a landed declaration, and the card marks that "Not asserted" and routes it above this seat. The banner and the ADR-0087 disposition are release-facing and are what the card prescribes; the governance declaration is not. - ⛔ **It does not rule on what a landed `yes (narrowing)` that shipped declared `no` owes beyond this file**, nor on whether objectstack-ai#18677's mandatory contract review is now owed. That is the maintainer's. - ⛔ It does not touch `content/docs/releases/`, any `packages/*/CHANGELOG.md`, `packages/cli/src/commands/compile.ts`, `packages/qa/vitest-filter-preflight/**` or `packages/cli/vitest.config.ts`. ## Acceptance notes - **Noted, not filed:** `.changeset/18778-lint-per-package-authoring-pass.md` carries its `Clause-②: yes (narrowing)` line inside the changeset body, i.e. in text that ships verbatim into the published CHANGELOG. Whether that governance token belongs in a user-facing release note is a question about changeset convention, not a defect: no gate reads it there, nothing is falsified by it, and it is out of this card's one-file surface. Carrier: the next PR to touch changeset conventions; no PR is in flight on it. - **Noted, not filed:** the ⭐ generalization this card turns on — *a property that could not be exhibited with the fixtures on hand is UNEXHIBITED, ⛔ not absent* — is currently recorded only in card prose (objectstack-ai#18823, and triage `5722459190` which asks for it on the discriminant list). It has no home in `AGENTS.md` or any gate. Placing it is a governed-surface edit and so is not this PR's to make. Carrier: the triage seat that asked for it. Authored by Claude Code in session `session_01DvvamiacK328idtBYJBxV3`; the branch is `claude/issue-18823-pending-changeset-breaking-banner`. (Attribution is stated here in prose on purpose: this body is edited through a channel measured to store only a bare footer, which carries no session id.) --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: Claude <noreply@anthropic.com>
Closes #17712
Clause-②: no— this card only makes a gate refuse MORE. It loosens no accept set and widens no published surface.Implements item 1 (A′) of the ruling on #17712 (director seat, decision batch #130 item 3, 2026-09-13; maintainer's verbatim reply 「同意」). Item 2 (the
ISSUE-SLUG.mdnaming line in governed dev-round guidance) is #17748's and is not touched here. Item 3 — legacy names untouched, diff shape only — is honoured: nothing in this diff reads a changeset filename.The rule
A PR may not MODIFY or DELETE a
.changeset/*.mdthat exists on the merge base and was not added by this PR. Refused by name, one remedy: rename yours; restore theirs from base."Added by this PR" is the file's absence on the merge base — which is exactly what git's status letters already say, so the rule needs no second reading and no filename predicate at all:
AMDWhere it is wired, and why
Into
scripts/check-empty-changeset.mjs, as a second axis answered in the same run — the shapecheck-changeset-no-major.mjsalready uses for its two axes. The PR touches no workflow file at all, so it can be armed normally.Three things that come for free from that placement, each of which a standalone script would have had to re-derive:
changeset-release/mainexemption.changeset versionDELETES every pending changeset, so the Version Packages PR is all-Drows. Outsidechangeset-check's job-levelif:, this gate would be structurally unsatisfiable on every release PR — the chore: version packages (rc) #4422 / 发布 PR(changeset-release/main)常红两处 CI:Check Changeset 结构性必失败 + Scaffold E2E 在 RC 窗口协议大版本错配 #4894 shape, pointed at the release train.changeset-checkjob already derivesMERGE_BASEand hands it to this script. That matters more for this rule than for the others: run two-dot against a moving base tip instead of the merge base and every changesetmaingained while the PR sat open reports as aDon this branch — an author refused by name for deleting files they never touched. The self-test pins that false red as a firing control beside the real reading.check-empty-changeset.mjs --self-testruns unconditionally inlint.yml(观察单:changeset 家族三个 check-*.mjs 的 --self-test 只在可被 skip-changeset 整体豁免的 job 里跑,改这些脚本的 PR 恰好豁免自己的自检 #6509), outside theskip-changesetexemption — which is where this gate's own fixtures need to be, since a PR editing a CI-internal script is the textbookskip-changesetcase.The file name still names rule 1 only. Renaming it would mean editing the workflow steps that spawn it; the header says so rather than leaving it to be noticed.
Rename detection is OFF for this pass (
--no-renames), the opposite of theAMRchoice rule 1 makes in #7045 — and for the same underlying reason, one letter over. Renaming somebody else's changeset DELETES their release note at its path; with detection on, that deletion folds into anRrow and disappears. With it off the same edit reportsD theirs+A yoursand theDis refused. It costs nothing in the other direction: a PR renaming its OWN changeset across commits still shows oneArow, because the old path was never on the merge base either.The four ruled cases, end to end on this repository
Driven on a throwaway worktree at this branch's tip, against real commits (the gate reads commits, never the working tree):
M— overwrite a sibling's changesetM .changeset/12271-published-entry-no-auto-transpile.mdD— delete theirs, add mineD .changeset/12271-...md+A .changeset/17712-demo-only.mdMacross commits — add in commit 1, reword in commit 2A .changeset/17712-demo-only.mdAA .changeset/17712-demo-only.mdThe refusal, verbatim:
plus a
::error file=...annotation carrying the same remedy, so it lands on the diff rather than only in a log.Ablation — the currently-false sentence this makes true
Same two fixtures, with
scripts/check-empty-changeset.mjsrestored to itsorigin/mainblob (1c5638ace2, marker count forscanForeign0 on disk; restored to24739e0fad, marker count 12,git diff HEADempty):Reverse-read, both directions
Made false. "
check-empty-changeset.mjsexits 0 on any diff whose.changeset/*.mdrows are all non-empty at head." It no longer does — caseMabove shows rule 1 printing its own green tick on the same run that exits 1.Made true. "A PR that changes or deletes another PR's release note is refused by name, with a remedy." Previously false in every member of the family: the only filename predicate in the changeset gates is prefix + extension, and
scan()'s--diff-filter=AMRhas noDat all, so a deletion was invisible to all three. Pinned as an assertion, not just asserted here: the foreign-Dbattery case also runsscan()on the same fixture and requires zero rule-1 violations — the control that this rule is not redundant with the one beside it.One premise in the card is not exactly right, measured
The card says the collision would pass "with every gate green on both sides". Measured on real fixtures against
pr-automation.yml's own--diff-filter=Acount:ADDEDSo the narrowest shape of the incident was already refused — by the wrong gate, naming the wrong problem, and with a remedy ("add a changeset") that leaves the overwrite in place. The two shapes that actually reach
mainwere green. The card's conclusion stands; its "every gate" is one row too strong, and the correction is recorded here rather than left for the next reader of that thread.Self-test
New battery
A' (#17712): a changeset the PR did not add is neither modified nor deleted— 29 cases, pinned inSELF_TEST_BATTERIESand reached (the roster's own size floor moves 21 → 22). The battery-floor machinery (#13489) is what makes "every case held" distinguishable from "the cases never ran".Beyond the four ruled cases it covers: renaming a foreign changeset (with the rename-detection-ON control proving the
Rrow really does swallow theD); renaming your own changeset across commits;.changeset/README.md; a nonsense control (a diff touching no changeset); #6129 in this rule's direction, firing control first; #4690 (no merge base throws, never falls back); and that the rendered report names the file and carries the remedy verbatim in both the body and the annotation.Every green in the battery carries a control that the fixture did what it claims — a
0from a diff that was empty for an unrelated reason is a reading taken against nothing, not a pass.Verification
At
eeb4012a17:node scripts/pm/dispatch-gates.mjs --ran— 34 derived families, 34 run, 0 NOT-MEASURED (derived, every entry carrying its exit code), 0 UNRUN. All 34 exit 0. Re-derived aftergit fetch origin main; the set did not move.npx eslint . --no-inline-config --format json— exit 0 over 6751 files, 0 errors, 0 warnings. The whole-repo run, not a narrowing: the population is eslint's own count from its--format jsonoutput, and this file is in it.node scripts/check-empty-changeset.mjs --base origin/mainon this branch: both axes green.No package is touched, so there is no dependency closure to build and no package test suite affected;
scripts/is not TypeScript and no*.test.tsin the tree names this script (control:check-nul-bytesnames three).No changeset —
skip-changesetMeasured rather than asserted: 0 published (non-
private) packages have afiles[]entry that would ship anything underscripts/. Firing control: 70 published packages shipdist/. Nothing published moves, so the label is the correct route and an empty-frontmatter changeset would be refused by rule 1 of this very script.验收备注
Two cells are recorded rather than implied, both inherited from the job this step lives in and neither newly opened by it:
skip-changesetis exempt from the wholechangeset-checkjob, so it is exempt from this rule too. Identical in shape to the cellcheck-empty-changeset.mjsalready records for rule 1, and with the same reasoning: a consistent exemption beats a gate that reds one PR and greens an identical one. Motive is thin — a PR with the label adds no changeset of its own, so it has nothing to collide with — but deletion is still reachable under it.pr-automation.ymlruns onpull_requestonly, so this gate does not re-run in the merge queue. There is a recorded ⛔ inlint.yml'son:block against addingmerge_groupto advisory workflows, so closing that would be a decision, not a wiring fix. What the ruling's "re-evaluated against the current base" needs is satisfied on the PR leg: the base is recomputed on everysynchronize, and it is a merge base, never a branch tip.One maintainer-only question, deliberately left un-decided here: the ruling names no escape hatch for deliberately correcting somebody else's release note on
main. The report tells such an author to say so on the PR and get it confirmed. If a mechanical route is wanted instead, that is a decision about release integrity and belongs to you, not to this PR.Noted, not filed:
scan()andscanForeign()now hold two near-identical merge-base preambles, including the same throw text. Whoever next touches a third axis in this family will want one helper — no PR or person is queued on this file today, so there is no carrier: 承接者:无.Generated by Claude Code