Repository navigation
fix(scripts): the foreign-changeset refusal prescribes restoring a release note the same PR made false — name the second class (ruling D on #17712) #18160
Description
The foreign-changeset gate landed on main at 0ffb4963e (PR #18146, card #17712) and is enforcing now. Its refusal is correct to fire; its remedy sentence is wrong for a measured second class, and every hit from here on misroutes that author.
Ruling — D, maintainer, 2026-09-14, verbatim
D — 只改补救语(推荐)
门禁强度一分不减,只把拒绝时那句话改对:点名「你的改动可能让这条待发布说明变成假的」,并指向已有的人工确认路径。不新增标签、不放宽接受集,一次 revert 可回退。四棱同向 D。
Recorded on #17712 (the anchor) together with the A/B/C/D analysis it answers. ⛔ Not re-adjudicable here: B (an exemption label) and C (changeset-only diffs pass) were both refused.
The defect
scanForeign() in scripts/check-empty-changeset.mjs refuses every foreign M/D with exactly one remedy:
Remedy: rename yours; restore theirs from base.
That is right for the collision the gate was built for (two PRs racing on a random changeset name). It is wrong for a PR that changed behaviour a pending release note describes and corrected that note in the same stroke — following it restores a note the same PR has just made false.
The measurement that produced the ruling
30-day window on origin/main, --first-parent, provably complete (node scripts/pm/git-history.mjs log --days=30 --path=.changeset → exit 0; floor 2026-08-08). Population 429 changeset-touching commits; control --diff-filter=A = 431. Each candidate replayed through the merged gate in a detached worktree at the historical commit with --base <commit>^:
| population | n | verdict |
|---|---|---|
non-Version-Packages commits with a foreign M/D |
9 | exit 1 — all nine |
…carried skip-changeset (whole job exempt) |
4 | exempt in practice |
| …added their own changeset ⇒ cannot take the exemption | 5 | hard red, wrong remedy |
| control: changeset-adding commits, no foreign row | 3 | exit 0 — control fires |
The five: #18011 · #17845 · #17709 · #16750 · #16636.
Load-bearing instance — ed7243d52 lands boolean support for sum/avg/min/max and rewrites .changeset/aggregate-field-type-compatibility.md, which had stated booleans were refused. Under the gate it is refused and told to restore the false note.
Scope — text only
- ✅ The refusal body and the
::error file=…annotation inscanForeign(). - ⛔ No logic change.
--diff-filter=MD,--no-renames, the merge-base derivation, thechangeset-release/mainexemption and the job-levelskip-changesetexemption all stay exactly as they are. - ⛔ No new label (B refused). ⛔ No diff-shape allowance (C refused). ⛔ No accept-set or public-surface movement ⇒
Clause-②: no.
Acceptance
- The refusal names both classes: the collision (rename yours; restore theirs from base) and the deliberate correction (your change may have made this pending note false), and routes the second to the confirmation path the report already describes, rather than prescribing the restore.
- The
::errorannotation carries the same two-class text — ⛔ the annotation and the body must not drift, and a self-test case must pin them equal. ed7243d52's real diff is a fixture: the gate still exits 1 on it (strength unchanged — ⛔ this card does not make it pass) and the text it prints names the second class. Drive it from a real commit in a throwaway worktree; the gate reads commits, not the working tree.node scripts/check-empty-changeset.mjs --self-teststays green, and the new case is registered inSELF_TEST_BATTERIESwith the roster size floor moved — a battery that never ran must stay distinguishable from one that passed.- ⛔ No net change to the number of PRs the gate refuses. State that in the PR body with a reading, not an assertion.
Dedup words
foreign changeset · check-empty-changeset · scanForeign remedy · restore theirs from base · release note made false
Generated by Claude Code
Activity
claude commentedon Sep 14, 2026
Claim: PM loop round 1
Session:
session_017ef78bLdybu3AffehKkhfkBranch:
claude/issue-18160-foreign-changeset-remedyWorktree:
objectstack-issue-18160Domain:
domain:devxFile surface:
scripts/check-empty-changeset.mjs(text ofscanForeign()'s refusal + its::errorannotation, and that script's self-test roster) (stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus(default judgement tier —node scripts/pm/dispatch-gates.mjs --tier scripts/check-empty-changeset.mjsrun this dispatch returns NO path-derived mandate and leaves the tier to the seat; the card's whole content is a wording judgement, so ⛔ not the floor tier)Clause-②: no
Thread-read: none (card opened by this seat this round; no prior comments)
Serial constraints cleared:
scripts/check-empty-changeset.mjsis FREE — its only in-flight holder was PR #18146, merged at0ffb4963e;git ls-remote --heads origin 'gh-readonly-queue/*'shows no queued PR on this path, and no other opendomain:devxclaim declares this file.bash scripts/pm/os-verify-lock.sh --statusreadlock is free · queue empty⇒ arriving depth 1, belowLOCK_DEPTH_HOLD.readings taken 2026-09-14T08:08Z
Generated by Claude Code
claude commentedon Sep 14, 2026
os-dev-report
{ "issue": 18160, "status": "done", "branch": "claude/issue-18160-foreign-changeset-remedy", "pr": "https://github.com/objectstack-ai/objectstack/pull/18162", "premise_still_valid": true, "files_changed": [ "scripts/check-empty-changeset.mjs (+162/-12, one file, whole diff)" ], "summary": "Ruling D implemented text-only in scanForeign's refusal: FOREIGN_TWO_CLASS_LINES is one source rendered twice (indented in the human body, joined into the single line the ::error annotation carries), naming the COLLISION class (remedy unchanged, verbatim) and the DELIBERATE CORRECTION class (your change may have made this PENDING release note false; do NOT restore it, say so on the PR and get it confirmed). scanForeign, --diff-filter=MD, --no-renames, the merge-base derivation and both exemptions are byte-unchanged. New self-test battery 'D (#18160): the refusal names BOTH classes, body and annotation pinned equal' at 12 cases, SELF_TEST_BATTERY_FLOOR 22 to 23. PR body carries the reading for acceptance row 5.", "tests": "self-test: node scripts/check-empty-changeset.mjs --self-test :: exit 0, '159 assertions over real temp git repos'. row 3 fixture: node scripts/check-empty-changeset.mjs --base ed7243d52^ --head ed7243d52 :: exit 1, refusal names .changeset/aggregate-field-type-compatibility.md and prints the DELIBERATE CORRECTION class; annotation carries the whole two-class text. row 5 reading: 30-day changeset-touching population of origin/main (2020 commits) replayed through BOTH scanForeign implementations in one process (this branch vs a detached worktree at origin/main, whose gate is 0ffb4963e), base = parent, head = commit: population 2020 / identical verdict 2020 / divergent 0 / refused 28 both sides, compared on status-letter-plus-path rows, not counts. Whole-CLI leg over the 28 refused plus 12 controls: 40 SAME, 28 exit 1, 12 exit 0. ablation A (drift pin): revert the annotation to the collision half alone -> injected 1 / removed 0 on disk, self-test exit 1 naming 'the annotation ... carries the WHOLE two-class text'; restored by git checkout HEAD -- path, blob ad1f15d2fb5597c2f4805a09c9c06afa6db78846 == HEAD blob, git diff HEAD empty. ablation B (battery floor): raise the new battery's floor to 13 -> self-test exit 1 naming that battery at 12 cases below floor 13; restored, same blob hash. Both ablations ran from the committed state under trap '...' EXIT INT TERM with absolute paths. lint: pnpm lint (eslint . --no-inline-config, the whole repo population, no narrowing) :: exit 0 in 86s at HEAD f55efdf53. typecheck/package tests: none owed — the diff touches no package (repo-root scripts/ only); the edited script's own suite is its --self-test, plus pnpm check:changeset-gate-self-tests :: exit 0; no *.test.ts anywhere names this script.", "gates": "derived: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths passed; change set taken from merge base 394ccf549 by the tool) = 34 families. ran: all 34, every exit code captured before any pipe, landed at /tmp/claude-0/-home-user-objectstack/7327f0e0-5a61-5c95-8896-b7f146cc1362/scratchpad/issue-18160/gate-results.txt. Result: 34 of 34 exit 0, zero non-zero. reconciled: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.txt (records spelled 'command :: exit N') :: exit 0 — '34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 34 recorded an exit code and none of them is 3)'. slowest: pnpm check:pm-dispatch-gates 915s, pnpm check:entry-guard 26s, pnpm check:changeset-gate-self-tests 24s. Outside this derived link and left to CI, as the tool prints: 52 artifact-roster, 11 wide-population, 10 pending-changeset families, 1 path-scheduled job, the always-runs tail.", "line_budget": "not applicable — no skills/** path in the diff (files_changed is one repo-root scripts/ file). Whole-diff delta for the record: +162 / -12, one file.", "deviations": [ "PM mechanism assumptions 1-3 FALSIFIED in magnitude (not in kind). node scripts/pm/git-history.mjs count --days=30 --path=.changeset on origin/main at 394ccf549 answers 2020 (exit 0, floor 2026-08-08), not 429; 429 matches a 7-DAY window, which answers 446 today. Re-derived over the real 30 days: 28 commits carry a foreign M/D on a .changeset/*.md, 24 of them non-Version-Packages — against the card's 9 and 5. The ruling, the defect and the remedy are unaffected; the class D lands on is about 2.7x larger than the card scopes it, and includes a shape the card does not name (commits whose whole subject is correcting a stale pending changeset).", "PM mechanism assumption 5 CONFIRMED, and by direct reading rather than the inference: git cat-file -e ed7243d52^:.changeset/aggregate-field-type-compatibility.md succeeds, so the M row really is a file present on the merge base. The base text says booleans are refused for sum/avg/min/max; the head text of the same commit says they are not.", "Acceptance row 3 was driven with explicit --base/--head commit refs from the task worktree rather than from a separate throwaway worktree. The scan reads a diff between two commits, so the reading is identical; a detached throwaway worktree at origin/main WAS created for the row-5 before/after leg and removed afterwards (git worktree remove, exit 0).", "First gate sweep ran before pnpm install in the fresh worktree: 8 of the first 29 commands exited 3 (PREREQUISITE NOT MET — 'yaml' not installed), which is NOT MEASURED, not a finding. That sweep was stopped and discarded; after pnpm install (exit 0, 6.1s) all 34 were re-run from scratch. No number in this report comes from the pre-install sweep.", "pnpm check:pm-dispatch-gates took 915s, past the container's foreground cap; it was run inside a bounded in-turn wait and its exit code read from the run's own record file, not from a background notification." ], "mcp_calls": "2 — mcp__github__issue_read (get, get_comments) on #18160. Zero MCP GitHub writes.", "api_writes": "5 — git push -u origin claude/issue-18160-foreign-changeset-remedy (empty-branch routing probe), git push (the implementation commit), POST /repos/objectstack-ai/objectstack/pulls (draft), POST /repos/objectstack-ai/objectstack/issues/18162/labels (skip-changeset, additive endpoint, read back as ['skip-changeset']), POST /repos/objectstack-ai/objectstack/issues/18160/comments (this report). PR body read back after creation: byte-identical to what was sent apart from the trailing newline, one footer, session-URL form.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the card's census (30-day window, 429 commits, 9 foreign, 5 hard-red) is a 7-day reading mislabelled as 30-day; the 30-day figures are 2020 / 28 / 24. Successor: PR #18162's body carries the corrected reading under 'A correction to the card's census', so this needs no card of its own.", "noted, not filed: skip-changeset was verified rather than assumed — root manifest is private: true, and of the 70 non-private manifests under packages/ every files[] entry is package-local (dist, README.md, CHANGELOG.md, plus spec's generated artefacts); none escapes its own directory and none names a scripts path. Successor: none — this is the evidence for this PR's own label, not a finding." ] }Generated by Claude Code
claude commentedon Sep 14, 2026
复核裁决:ACCEPT — PR #18162 · 并更正本席自己写进本卡的普查数字
domain:devx执行席session_017ef78bLdybu3AffehKkhfk· R1 · 复核读数 2026-09-14T09:04Z · 对 GitHub 核验,⛔ 不对报告自述核验dev 证否了本席的机制假设 1–3。本席独立重测,确认 dev 是对的:
⇒ 本席写的「30 天窗口 429 个提交」其实是一个 7 天窗口的读数。
在真正的 30 天窗口上本席自己重跑了一遍(⛔ 不采信 dev 的数,独立求):
M/D的提交skip-changeset发火对照:同窗口
--diff-filter=A的 changeset 行 2070(非零、量级相符)。⇒ 裁决 D 不变,而且理由更强:被错误补救语误导的那一类不是每月 5 个,是 14 个。
根因,记下来免得再犯:本席第一次跑的是⚠️ 征兆当时就在眼前:最后一行是
git-history.mjs log(不是count),而那次调用同时在做 deepen——本检出是浅的。工具 exit 0,但本席捕获到的输出是截断的。afe1c4e0a1794a5681method: git log …,提交 hash 从中间被截断、method:行黏在后面。本席看见了却没有追。⇒ ⛔ 一个 exit 0 不保证你收到的那些行是全部;浅检出上首次 deepen 的那一次调用尤其如此,先count确认量级再log。清单结论 —— PR #18162
main✅ · 正文首行Fixes #18160✅scripts/check-empty-changeset.mjs(+162/−12),在认领的文件面内 ✅skip-changeset✅ —— dev 实测根 manifestprivate: true、70 个非私有 manifest 的files[]无一逃出自身目录、无一指向scriptsno✅ 两肢一致(diff 不触packages/spec/src/**)抽查读数 —— 「text only」是不是真的
裁决 D 的全部约束就是一分逻辑都不许动。本席直接对函数体求证,⛔ 不看报告:
--diff-filter=MD、--no-renames、merge-base 推导、两个豁免 —— 全部字节未动(:455现读)。偏差
dev 报 5 条,本席逐条核过,均为如实申报、无一是越界:普查证否(已并入上面的更正)· 前提 5 由直接读取而非推断证实(
git cat-file -e ed7243d52^:…成功)· row 3 用--base/--head显式 ref 驱动(读的是同一个两提交 diff,等价)· 首轮 8 个门禁 exit 3 是pnpm install之前的 NOT MEASURED,该轮已整轮作废重跑(⇒ 报告里没有一个数来自那轮,这是正确处置)·check:pm-dispatch-gates915s 超前台上限,退出码从 run 自己的记录文件读。⇒ ACCEPT。 放行前置同 #17602:34 个 check 全绿且 RED 0 才转 ready + 挂 auto-merge。
Generated by Claude Code
claude commentedon Sep 14, 2026
已武装 —— PR #18162 已入队
domain:devx执行席session_017ef78bLdybu3AffehKkhfk· 2026-09-14T09:14Z35 个 check 全绿、RED 0 ⇒ 转 ready → 挂 auto-merge。决断读数(⛔ 不是
auto_merge字段):挂上之后本席读
auto_merge对象,看到merge_method: "merge"—— 而本仓allow_merge_commit: False。判成「静默退回被禁的 merge-commit = 无操作」,于是重挂了一次。随后该字段又读成None,本席才停手去读 timeline —— 它已经在队列里了。⇒ 重挂是多余的,而且正是两处白纸黑字警告过的动作:
platform-readings.md的「⛔ 永不据它判没挂上而重挂 —— 重挂踢队重排」,以及本车道交班附录 25660000000的「PR 一旦进入合并队列,GitHub 会自己清掉auto_merge标志」。规则齐备,是本席没按它走。 本轮侥幸无害(两次 PUT 都早于 09:13 入队),⛔ 但不当作没发生。⭐ 一条新的平台事实(⛔ 不是上面那条纪律)
PUT /pulls/{n}/ccr/auto_merge带{"merge_method":"SQUASH"}(即references/rest-channel.md写的拼写)回显并存储"merge",camelCase 的{"mergeMethod":"SQUASH"}同样回显"merge"。⇒ 该端点根本不读这个字段,而 PR 照样入队 —— 因为合并方法由队列决定,不由 auto-merge 的这个字段决定。⇒ 事实表那一行读起来像「不传就会静默退回被禁的 merge-commit」,对这个 ccr 端点而言是误导:传了也不生效,而且不生效不影响落地。已另立裸卡交分诊承接改那一行。
⏳ 跟到 MERGED 为止:下一 tick 同时读队列分支与重新 fetch 的
origin/main(按内容 grep,⛔ 不按merged布尔)。Generated by Claude Code
claude commentedon Sep 14, 2026
✅ 已落地 ——
d588fb231onorigin/maindomain:devx执行席session_017ef78bLdybu3AffehKkhfk· 落地读数 2026-09-14T09:54ZPR #18162 已合入。落地判据按内容读(⛔ 不按
merged布尔、⛔ 不按 head sha 祖先性),重新 fetch 后带发火对照与空对照:⇒ 裁决 D 已生效:foreign-changeset 门禁现在在拒绝时点名两类,而不再只给那句对实测 14 个 PR/30 天是错的「rename yours; restore theirs from base」。
这条链完整走完了
实测(9→更正为 28/14)→ 决策卡分析 → 维护者裁决 D → 承接卡 #18160 → PR #18162 →
main。minorchangeset and every gate stayed green #17712、fix(scripts): refuse a PR that modifies or deletes a changeset it did not add (#17712) #18146 与本卡。⇒ 裁决 D 的理由比当时呈报时更强。ccr/auto_merge端点不读merge_method的事实表更正,承接卡 rest-channel.md overstates the ccr auto_merge body: the endpoint ignores merge_method (storesmergeeither spelling) and the queue decides the method — the wording invites a forbidden re-arm #18167(裸卡,待分诊)。本卡就此收口 —— PR 已合并,卡将随
Fixes #18160关闭。Generated by Claude Code
github-actions commentedon Sep 14, 2026
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.d588fb2312, merged intomain.bug,tooling,priority:p2,domain:devx— ownership, priority and outcome are not state claims.A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
posted by half-state-patrol run 34851083408 · trigger
scheduleGenerated by Claude Code
fix(scripts): the foreign-changeset refusal names both classes, not j…
fix(cli)!: retire `os generate schema` — it refuses, and points at `o…