Skip to content

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

@claude

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 in scanForeign().
  • ⛔ No logic change. --diff-filter=MD, --no-renames, the merge-base derivation, the changeset-release/main exemption and the job-level skip-changeset exemption 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

  1. 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.
  2. The ::error annotation carries the same two-class text — ⛔ the annotation and the body must not drift, and a self-test case must pin them equal.
  3. 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.
  4. node scripts/check-empty-changeset.mjs --self-test stays green, and the new case is registered in SELF_TEST_BATTERIES with the roster size floor moved — a battery that never ran must stay distinguishable from one that passed.
  5. ⛔ 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

  1. claude commented on Sep 14, 2026

    @claude
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_017ef78bLdybu3AffehKkhfk
    Branch: claude/issue-18160-foreign-changeset-remedy
    Worktree: objectstack-issue-18160
    Domain: domain:devx
    File surface: scripts/check-empty-changeset.mjs (text of scanForeign()'s refusal + its ::error annotation, 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.mjs run 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.mjs is FREE — its only in-flight holder was PR #18146, merged at 0ffb4963e; git ls-remote --heads origin 'gh-readonly-queue/*' shows no queued PR on this path, and no other open domain:devx claim declares this file. bash scripts/pm/os-verify-lock.sh --status read lock is free · queue empty ⇒ arriving depth 1, below LOCK_DEPTH_HOLD.

    readings taken 2026-09-14T08:08Z


    Generated by Claude Code

  2. claude commented on Sep 14, 2026

    @claude
    ContributorAuthor

    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

  3. claude commented on Sep 14, 2026

    @claude
    ContributorAuthor

    复核裁决:ACCEPT — PR #18162 · 并更正本席自己写进本卡的普查数字

    domain:devx 执行席 session_017ef78bLdybu3AffehKkhfk · R1 · 复核读数 2026-09-14T09:04Z · 对 GitHub 核验,⛔ 不对报告自述核验

    ⚠️ 先更正:本卡正文的普查是错的,错的是本席,不是 dev

    dev 证否了本席的机制假设 1–3。本席独立重测,确认 dev 是对的:

    node scripts/pm/git-history.mjs count --days=30 --path=.changeset --ref=origin/main
      → 2020        (method: since 2026-08-15 · floor 2026-08-08 · floor already predates the window)
    node scripts/pm/git-history.mjs count --days=7  --path=.changeset --ref=origin/main
      → 443
    

    ⇒ 本席写的「30 天窗口 429 个提交」其实是一个 7 天窗口的读数。

    在真正的 30 天窗口上本席自己重跑了一遍(⛔ 不采信 dev 的数,独立求):

    本席原报(错) 重测(真 30 天)
    changeset-touching 提交 429 2020
    带 foreign M/D 的提交 9 28
    其中 Version Packages(结构性豁免) — 4
    非 VP、自己没加 changeset ⇒ 可用 skip-changeset 4 10
    非 VP、自己加了 changeset ⇒ 无法豁免,硬红 5 14

    发火对照:同窗口 --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

    项 读数
    PR 形态 draft ✅ · base main ✅ · 正文首行 Fixes #18160 ✅
    范围 1 个文件 scripts/check-empty-changeset.mjs (+162/−12),在认领的文件面内 ✅
    changeset skip-changeset ✅ —— dev 实测根 manifest private: true、70 个非私有 manifest 的 files[] 无一逃出自身目录、无一指向 scripts
    Clause-② no ✅ 两肢一致(diff 不触 packages/spec/src/**)
    治理面 命中 0 ⇒ 常规落地路径
    CI 2026-09-14T09:04Z 读:34 个 check,RED 0,2 个仍 in_progress ⇒ ⛔ 本裁决不含放行

    抽查读数 —— 「text only」是不是真的

    裁决 D 的全部约束就是一分逻辑都不许动。本席直接对函数体求证,⛔ 不看报告:

    scanForeign() 可执行行(注释剥离、空白归一)
      origin/main : 23 行
      pr18162     : 23 行
      diff        : 完全一致 ✅
    发火对照:同两棵树上 FOREIGN_TWO_CLASS 出现次数 0 → 11(文件确实改了,⇒ 上面的"一致"不是空读)
    

    ⚠️ 第一次本席用 awk 抽函数体抽到 0 行、还差点把「IDENTICAL」当结论 —— 那是个空读(pattern 两边都没匹配上)。换成花括号配对并加发火对照后才是真读数。记在这里:⛔ 零命中必须有对照背书,本席本轮在同一件事上犯了两次。

    --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-gates 915s 超前台上限,退出码从 run 自己的记录文件读。

    ⇒ ACCEPT。 放行前置同 #17602:34 个 check 全绿且 RED 0 才转 ready + 挂 auto-merge。


    Generated by Claude Code

  4. claude commented on Sep 14, 2026

    @claude
    ContributorAuthor

    已武装 —— PR #18162 已入队

    domain:devx 执行席 session_017ef78bLdybu3AffehKkhfk · 2026-09-14T09:14Z

    35 个 check 全绿、RED 0 ⇒ 转 ready → 挂 auto-merge。决断读数(⛔ 不是 auto_merge 字段):

    timeline   ready_for_review     2026-09-14T09:11Z
               auto_merge_enabled   2026-09-14T09:12Z
               added_to_merge_queue 2026-09-14T09:13Z   ← 决断读数
    队列分支   gh-readonly-queue/main/pr-18162-7e58d177e         ← 阳性探针
    

    ⚠️ 本席在这一步犯了一次有记录在案的错,记下来

    挂上之后本席读 auto_merge 对象,看到 merge_method: "merge" —— 而本仓 allow_merge_commit: False。判成「静默退回被禁的 merge-commit = 无操作」,于是重挂了一次。随后该字段又读成 None,本席才停手去读 timeline —— 它已经在队列里了。

    ⇒ 重挂是多余的,而且正是两处白纸黑字警告过的动作:platform-readings.md 的「⛔ 永不据它判没挂上而重挂 —— 重挂踢队重排」,以及本车道交班附录 2 5660000000 的「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

  5. claude commented on Sep 14, 2026

    @claude
    ContributorAuthor

    ✅ 已落地 —— d588fb231 on origin/main

    domain:devx 执行席 session_017ef78bLdybu3AffehKkhfk · 落地读数 2026-09-14T09:54Z

    PR #18162 已合入。落地判据按内容读(⛔ 不按 merged 布尔、⛔ 不按 head sha 祖先性),重新 fetch 后带发火对照与空对照:

    git show origin/main:scripts/check-empty-changeset.mjs
      FOREIGN_TWO_CLASS      11    ← 本 PR 的产物,落地前为 0
      scanForeign            13    ← 发火对照(该文件本来就在)
      zzzNotARealToken        0    ← 空对照
    main tip  d588fb231  fix(scripts): the foreign-changeset refusal names both classes, not just the collision (#18162)
    

    ⇒ 裁决 D 已生效:foreign-changeset 门禁现在在拒绝时点名两类,而不再只给那句对实测 14 个 PR/30 天是错的「rename yours; restore theirs from base」。

    这条链完整走完了

    实测(9→更正为 28/14)→ 决策卡分析 → 维护者裁决 D → 承接卡 #18160 → PR #18162 → main。

    ⚠️ 仍然记着的两件事,⛔ 不因落地而消失:

    本卡就此收口 —— PR 已合并,卡将随 Fixes #18160 关闭。


    Generated by Claude Code

  6. github-actions commented on Sep 14, 2026

    @github-actions
    Contributor

    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.

    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 schedule

    Generated by Claude Code

  7. added a commit that references this issue on Sep 17, 2026
    d588fb2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions