feat(devx): the changeset gate answers whether the LEVEL fits the surface, not only whether it is major - #16264
Merged
Conversation
…face, not only whether it is `major` `Check Changeset` was green on `patch` and green on `minor` for the same diff: measured on PR #16044, heads `e0938d3fdce` and `98179cae022`, a source tree that `git diff` reports as differing in exactly one changeset path and, inside it, one word. Every level-sensitive gate passed both. On that axis the job's green could not fail, so it carried no information about the level while looking exactly like a green that does. `check-changeset-no-major.mjs` now reads a second axis beside the major guard: when the PR DECLARES clause ② -- the `needs:contract-review` carrier, or the fixed `Clause-②: yes` line, read through `check-clause2-carriers.mjs`'s own `readClause2Line` rather than a second spelling -- a `patch` entry for any package whose `packages/*/src/**` the diff moves is refused. Nothing new is asked of an author: both declarations already existed in the same PR and were never compared, and the refusal can only fire while the carrier already holds the PR outside the merge queue. The declaration is read from the `pull_request` payload CI already has on disk, so the gate makes no API call and needs no token; `--event FILE` names that payload for an offline run. No workflow file changes: the real scan is already wired per PR in pr-automation.yml's `Check Changeset` job. Driven on the two real heads, not only on fixtures: `patch` exits 1 naming `@objectstack/cli` and the changeset, `minor` exits 0, and the same `patch` head with the carrier stripped reads NOT MEASURED -- a missing reading, never a pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
This was referenced Sep 6, 2026
baozhoutao
marked this pull request as ready for review
September 6, 2026 08:56
baozhoutao
enabled auto-merge
September 6, 2026 08:56
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 #16055
Check Changesetwas green onpatchand green onminorfor the same diff. Measured on PR #16044's two real heads —e0938d3fdcegrading@objectstack/clipatch,98179cae022grading itminor—git diffbetween the two commits returns one changeset path and nothing else, and inside it one word. Every level-sensitive gate passed both. On that axis the job's green could not fail, so it carried no information about the level while looking exactly like a green that does.The change — one axis added to
scripts/check-changeset-no-major.mjs, no new script, no workflow editThe gate now answers a second question beside "is this bump
major": when this PR DECLARES clause ② — a new key on a published payload — is it grading a package it grewpatch? If so it refuses, naming the changeset file, the package, and the level to raise to.needs:contract-review, and the fixedClause-②: yes/noline — read throughcheck-clause2-carriers.mjs's own exportedreadClause2Line, andCONTRACT_REVIEW_LABELimported fromcheck-half-states.mjs, so there is no second spelling here that can drift from the gate the PM protocol runs. That module is read as a library and is not edited.packages/*/src/**this diff moves, resolved to its name frompackage.jsonin the HEAD tree so the reading works on commits that are not checked out.LEVEL AXIS: NOT MEASUREDand exits 0 — neither a pass nor a failure. A declaration that readsnois a separate verdict with separate text, because a decision and a missing reading must not look the same; that confusion is the class this card is about.No
.github/workflows/**file changes. The real scan is already wired per PR:pr-automation.yml'sCheck Changesetjob runsnode scripts/check-changeset-no-major.mjs --base "$MERGE_BASE"as its last step. The declaration comes from thepull_requestpayload the job already has on disk at$GITHUB_EVENT_PATH— no API call, no token, no network.--event FILEnames that payload for an offline run and makes it the whole declaration input.The PM's mechanism assumption A1 is falsified, and that is what made this reachable
The dispatch recorded that
check-changeset-no-major.mjs/check-empty-changeset.mjs/check-adr-0087-registration.mjsrun per PR only as--self-test, and for real only incut-rc.yml. Measured on this tree instead:check-changeset-no-major.mjs--base "$MERGE_BASE"pr-automation.yml,Check Changesetjob (last step)check-empty-changeset.mjs--base "$MERGE_BASE"check-adr-0087-registration.mjs--base "$MERGE_BASE"--self-testlint.yml, viapnpm check:changeset-gate-self-testscut-rc.ymlSo placement (b) needed no
lint.ymledit and no new script, and theneeds_decisionfork the dispatch offered was not taken.A2's question, answered by measurement rather than assumed
The dispatch asked which declaration the #16044 heads actually carried. Both were checked:
cardDeclaration()over cardos lint --eval --json's generator-load failure emits a bare{error}— nocode, nohttpStatus— while the same command's project-lint catch-all emits the full ADR-0112 envelope #15549's real comment thread returns{ state: 'absent' }— its claim comment is a## Claimheading with noClause-②:line at all. A gate keyed only on that line would have answered NOT MEASURED on the acceptance case and never gone red.os lint --eval --json's generator-load exit #16044's label event stream:labeled needs:contract-reviewat 2026-09-05T21:43:58Z,unlabeledat 22:38:26Z.e0938d3fdcewas HEAD from 21:43:58Z until 22:18:39Z;98179cae022from then until the strip.That is why the carrier is read, and why the reader is a disjunction over both carriers rather than the line alone.
Both precedent citations in the card were re-checked at source before being relied on
098a08ffafa:.changeset/cli-json-error-envelope-adr-0112-code.mdline 2 reads"@objectstack/cli": minorfor feat(cli): carry the ADR-0112 code and httpStatus in--format jsonfailure envelopes #13510 — the same two keys,codeandhttpStatus, on a CLI--format jsonfailure envelope, with its own body stating "Whyminorand notpatch. Maintainer-set … this is a shape change to an already-published error envelope."8aaa118d1f55a299f3c9bfdf75d75d76753bc323(fix(cli): os lint surfaces ADR-0087 conversion notices — console notice + a conversions key in --json #13741) rules it in its own words: "Additive member on a published machine-readable surface; no existing key changes meaning and no count moves."Both hold. The card's premise stands.
Acceptance — driven on #16044's two real heads, not only on fixtures
A scratch clone fetched the two commits plus their branch point
2024eca4ff2, checked each head out, overlaid this branch's gate, and ran it with the event payload built from the label state measured above. Exit codes captured before any pipe.e0938d3fdce@objectstack/cli: patch98179cae022@objectstack/cli: minorLEVEL AXIS: … no package … graded patche0938d3fdce@objectstack/cli: patchLEVEL AXIS: NOT MEASUREDRow 1 against row 2 is the card's hard condition: byte-identical source tree, one word apart, opposite verdicts. Row 3 is the control on the other axis — the same offending tree goes quiet when the declaration is not readable, so row 1's red is about the level and the declaration, not about the diff.
The refusal, verbatim from row 1:
Tests
--self-testgains one battery,The LEVEL axis: #16044's two heads, one word apart (#16055), 41 assertions, registered inSELF_TEST_BATTERIESwith the roster floor raised 13 → 14. Total 116 → 157 assertions. The two head fixtures are built from one shared body constant with the bump word substituted, so they are byte-identical by construction — a typo cannot make the negative pass for the wrong reason. It covers the pure judgement, the declaration reader against #16044's real PR-body prose, the event-payload reader, and six end-to-end cases over real temp git repositories (including the#7005exemption on this axis and the downgrade direction that must still fire).Reverse verification — two ablation legs, each proven on disk and restored. Mutation confirmed by removed-text and injected-text counts plus a
git hash-objectthat moved off the HEAD blob; restore confirmed by an emptygit diff HEADand the hash back tob9c431b736e750490ed0f6bf74e1fcd7acd3649a. Atrap … EXIT INT TERMwith absolute paths carried the restore.entry.bump === 'patch'→ a bump word nothing declares): self-test exits 1 with 7 failures, and they are exactly the seven positive assertions —Every negative and control stayed green — the discriminating direction, not merely "it goes red".
yes: self-test exits 1 with 4 failures, the NOT MEASURED /no/ near-miss controls. So the declaration reading is load-bearing too.Gates
Union derived at
07103b4f9withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --changed --commands, reconciled with--ran: 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN — all exit 0.pnpm check:pm-dispatch-gatesran underscripts/pm/os-verify-lock.sh(VERDICT command-exit 0, held 677s, waited 469s). The two imported modules' own self-tests are green as well (check-clause2-carriers190 cases,check-half-states2535 cases), neither file edited.Two readings that are NOT MEASURED rather than green, stated so they are not mistaken for coverage:
*.test.tsanywhere namescheck-changeset-no-major.mjs—git grep -lover the test globs returns nothing. The script's--self-testis its suite, and it is wired inlint.yml.pnpm lintis CI's. Locally eslint ran over the one changed file as a declared narrowing: population read from eslint's own resolution (--print-configexits 0, so the file is not ignored), file count read from--format json(1 file, 0 errors, 0 warnings), andeslint.config.mjsstates in its own text that it "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file" — so this diff cannot move the verdict of any file it does not touch.grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the changed file returns nothing, beside a greencheck:nul-bytes.Residuals, recorded in the file's own header rather than implied
skip-changesetandallow-major, closed the same way: this workflow triggers onlabeled/unlabeled, so hanging the carrier fires a run that does see it. On fix(cli): carry the ADR-0112 error carriers onos lint --eval --json's generator-load exit #16044 theopenedrun at 21:40Z would have read NOT MEASURED and thelabeledrun three minutes later refuses thepatch.os lint --eval --json's generator-load exit #16044 the review's own verdict comment concurred with theminorgrading explicitly — but it makes this a pre-review reading, never a landing-time one.allow-majorlabel skips the whole step, this axis included, because the step it lives in is the launch-window major guard. A PR granted a whole-stack major that also grades a clause-②-declared packagepatchis not caught. Two conditions with no motive; the alternative — a second step — is refused bycheck-empty-changeset.mjs's pin on this job's failable step count.Scope
This card only. No out-of-scope defects were found, so no issues were filed. Sketch 2 (binding the level to an ADR-0087 disposition) did not fall out for free and was not attempted; sketch 3 stays refused for this card.
.github/workflows/**is untouched, so PRs #15331 / #15392 are not contended.Publishes nothing — the whole diff is one file under
scripts/— soskip-changesetis applied.Opened as a draft; not flipped ready, auto-merge not armed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
Generated by Claude Code