fix(scripts): the foreign-changeset refusal names both classes, not just the collision - #18162
Merged
Merged
Conversation
…ust the collision `scanForeign()` refused every foreign `M`/`D` with one remedy -- "rename yours; restore theirs from base". That is right for the collision the gate was built for, and wrong for a PR that changed behaviour a PENDING release note describes and corrected that note in the same stroke: following it restores a sentence the same PR has just made false. The refusal now names both classes and routes the second to the existing human confirmation path instead of to the restore. One source, two renderings: the human body indents the block, the `::error` annotation joins it into a line, and the self-test reads the block back out of the rendered body and requires it to equal the annotation's text byte for byte, so neither rendering can drift. Text only -- `--diff-filter=MD`, `--no-renames`, the merge-base derivation and both exemptions are untouched, and the new battery pins the deliberate-correction shape as STILL refused. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 14, 2026
This was referenced Sep 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18160
Ruling D on #17712 (maintainer, 2026-09-14), implemented as ruled: text only.
--diff-filter=MD,--no-renames, the merge-base derivation, thechangeset-release/mainexemption and the job-levelskip-changesetexemption are byte-for-byte unchanged. No label (B refused), no diff-shape allowance (C refused).Clause-②: no.What changed
scanForeign()refused every foreignM/Drow with one remedy — rename yours; restore theirs from base. That is right for the collision the gate was built for, and wrong for a PR that changed behaviour a pending release note describes and corrected that note in the same stroke: following it restores a sentence the same PR has just made false.The refusal now names two classes with opposite remedies, and routes the second to the confirmation path the report already described instead of to the restore.
One source, two renderings:
FOREIGN_TWO_CLASS_LINESis indented into the human body, andFOREIGN_TWO_CLASS_TEXT(the same array joined) is what the::errorannotation carries.Acceptance
1 — the refusal names both classes
Printed body (excerpt, from the run in row 3 below):
The numbered procedure below it is now labelled
Concretely, for the COLLISION class:, and the closing paragraph is the class-2 route: step 1 is named as the one thing not to do, and the author is sent to say so on the PR and get it confirmed. The gate stays red either way — that is what puts the decision in front of a person.2 — annotation and body pinned equal
The self-test does not compare each rendering against its own constant (that passes straight through the drift). It reads the block back out of the rendered body, normalises it to one line, and requires equality with the annotation's text:
Ablation, from the committed state, proving the pin can fail: revert the annotation to
Remedy: ${FOREIGN_REMEDY}.alone.3 —
ed7243d52driven through the gate, from real commitsnode scripts/check-empty-changeset.mjs --base ed7243d52^ --head ed7243d52— both refs are commits, so the scan reads history, not this worktree's files.Still exit 1 — this card does not make it pass — and the text now names the second class. That commit is the load-bearing instance: at
ed7243d52^the note reads "Booleans are refused forsum/avg/min/maxby the ruling's 'every other pair: refused'", and ated7243d52the same PR rewrites it to "Booleans (boolean,toggle) are NOT in the 'every other pair' default". Restoring theirs from base would republish the first sentence.4 — self-test green, new battery registered, roster floor moved
New battery
D (#18160): the refusal names BOTH classes, body and annotation pinned equalat 12 cases;SELF_TEST_BATTERY_FLOOR22 to 23. A battery that never ran stays distinguishable from one that passed — ablation raising that battery's floor to 13, from the committed state:Both ablations restored with
git checkout HEAD -- pathand verified by blob hash againstHEAD, not by an exit code.5 — no net change to the number of PRs the gate refuses (a reading, not an assertion)
Replayed the whole 30-day changeset-touching population of
origin/mainthrough both implementations in one process — the post-changescanForeignfrom this branch and the pre-change one from a detached worktree atorigin/main(0ffb4963eis the commit that last touched the script) — judging each commit atbase = commit^,head = commit:The reading fires rather than reading zero against nothing: 28 commits are refused on both sides, and the refused rows (status letter plus path) are equal commit by commit, not merely the counts.
Whole-CLI leg, exit codes rather than the scan's return value, over the 28 refused commits plus 12 changeset-touching controls:
The 12 zeros are the control that the harness is not simply printing
1.A correction to the card's census
The card's window reading does not reproduce, and the direction is against the card's own modesty.
node scripts/pm/git-history.mjs count --days=30 --path=.changesetonorigin/mainat394ccf549answers 2020 commits (exit 0, floor 2026-08-08), not 429. The card's 429 matches a 7-day window, which answers 446 today.Re-deriving the foreign population over the real 30 days: 28 commits carry a foreign
M/Don a.changeset/*.md, of which 24 are not Version Packages. The class the old remedy misrouted is therefore materially larger than the five the card names, and it includes a recurring shape the card does not — commits whose entire subject is correcting a stale pending changeset (docs(changeset): the STACK_* codes ARE in ERROR_CODE_LEDGER now — correct the stale sentence …,docs(changeset): correct the share-link refusal log-volume sentence, and others). Nothing here re-opens the ruling; it only means D lands on more traffic than it was scoped against.Release surface
skip-changeset. The diff is one file, repo-rootscripts/check-empty-changeset.mjs. Measured rather than assumed: the root manifest isprivate: true, and of the 70 non-private package manifests underpackages/, everyfiles[]entry is package-local (dist,README.md,CHANGELOG.md, plus spec's generated artefacts) — none escapes its own directory and none names ascriptspath, so no tarball can carry this file.Gates
Derived from the real diff with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(34 commands; the tool took the change set from the merge base itself), then run and reconciled with--ran. Results are in the report comment on #18160.Generated by Claude Code