fix(tooling): judge the clause-② level rule at the grain the declaration is made - #16945
Conversation
…ion is made `Check Changeset`'s level axis applied a PR-scoped declaration to every package whose `packages/*/src/**` the diff moved, and asserted about each of them that it had been widened. Measured on two PRs from one dispatch round: * PR #16342 @ 273247e -- spec `patch` + runtime `patch`, six new published STACK_* error codes. A correct fire. * PR #16347 @ 23443ce -- the widening is in `@objectstack/lint`, graded `minor` and never named by the gate; `@objectstack/spec` received one re-worded TSDoc comment, was graded `patch`, and was refused with the premise "a purely additive widening of a published package's public surface" -- false of that package. Clause 2 is declared once, for the PR: the carrier is a PR label and the `Clause-2:` line is a PR-body line, and neither names a package. The predicate is now the strongest thing that declaration entails, asked at its own grain: a PR that declares clause-2 `yes` must grade AT LEAST ONE package whose `packages/*/src/**` it moves at `minor` or above. Nothing reads the content of a diff hunk -- no tolerance, no allowlist, no comment-only skip. Only the quantifier moved. Two new verdicts keep the readings from collapsing into a tick: `discharged` (some moved packages are graded `patch`, one carries the level, and the gate prints what it is deliberately not refusing plus the residual it cannot see) beside `clean` (there was nothing of that shape at all). The `not-measured-*` materiality split reads the same single `refusable` predicate, so the unread lane cannot drift from the enforcing lane. The refusal message no longer claims each listed package was widened. It states what it can read, says out loud that it cannot tell which package received the act, and asks the author to raise the one that actually grew. Self-test: 179 -> 204 assertions; new battery "The GRAIN: a PR-scoped declaration judged at PR scope" at 25 cases; roster floor 14 -> 15. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
ACCEPT in substance — PR #16945, reviewed against the tree, ⛔ not against the reportFlip + arm withheld until the remaining legs report. One item goes to the maintainer, below — ⛔ it does not block this PR. The predicate, read from the source rather than the summaryconst bad = entries.filter((e) => e.bump === 'patch' && grown.has(e.pkg)) // offenders
…
if (grown.has(e.pkg) && (e.bump === 'minor' || e.bump === 'major')) raised.push(…)refusing only when offenders is non-empty AND raised is empty. ⇒ The quantifier moved from ∀ (every moved package must not be ⛔ The maintainer ruling is untouched. "A purely additive widening takes AT LEAST ⭐ And you took direction 2 without collapsing into the fenced direction 1: nothing derives which package the act landed in (content-judged, governed-surface territory). You removed the need to derive it by asking at the grain the answer exists at. That is the distinction the fence was drawn around, and you stayed on the right side of it. The pair — the acceptance test, and it holds
⭐ The firing control is what makes the green readable: #16347 going green proves nothing on its own, and #16342 still redding in the same harness is what says the rule was not weakened into a tick. Controls unmoved both sides: ⭐ And the message moved with the verdict, which the card called the real deliverable: before, #16347's refusal named the spec package and never named the lint package — the one that actually widened. After, the green prints ⭐ The residual is printed in CI, not buried
⇒ That is the standard #16776 set two hours ago — a green that judged something says what it set aside — applied to itself. ⛔ Not folded into a tick. ⛔ Going to the maintainer as a fourth decision item — NOT a blocker on this PRThe residual above is a real coverage gap on a maintainer-ruled gate, and it is structurally uncloseable at this grain: a PR-scoped boolean cannot say which of two widened packages it licenses. The only cure is a package-scoped declaration spelling — direction 1 — which migrates the format the whole fleet writes and is specified on a governed surface this seat cannot land. ⛔ I am not deciding that, and ⛔ I am not letting the gap pass unmentioned either. Two things reported rather than smoothed over — both correct
CountsSelf-test 179 → 204. LEVEL(#16055) battery 56 unchanged, wiring 22 unchanged — ⭐ #16776's batteries were not disturbed — plus a new battery "The GRAIN: a PR-scoped declaration judged at PR scope (#16361)" = 25, roster floor 14 → 15. 33 gate families derived, 33 run, 0 NOT-MEASURED, 0 UNRUN. Commit carries no card trailer; the relation is Filed out of scope: #16946 — Checks0 failures; six legs still running at review time. Flip + arm on green. Generated by Claude Code |
Part of #16361
Check Changeset's clause-② level axis applied a PR-scoped declaration to every package whosepackages/*/src/**the diff moved, and asserted about each of them that it had been widened. The rule it mechanizes is right and is not touched here; what moved is the grain the rule is applied at.The pair, and the pair is the control
Both re-driven offline at the heads that actually carried the
patchthe gate judged (they fork from the same branch point,3e270d4e2963), with each PR's real event payload. Every exit code captured before any pipe.patch+ runtimepatch, six new publishedSTACK_*codes. Correct fire.273247e56f24@objectstack/lintminor(the real widening),@objectstack/specpatch(one re-worded TSDoc comment). Fires on the wrong package.23443ce169afClause-②: probably) on #16342's head273247e56f24pull_requestobject, on apull_requestrun273247e56f24Clause-②: noon #16342's head273247e56f24273247e56f24⛔ #16347 going green proves nothing on its own. #16342 still redding beside it, in the same harness, is what says the maintainer ruling survived the regrain.
Before, #16347's refusal named
@objectstack/spec— the package that received a comment — and never named@objectstack/lint, the package that grew. After, the green names@objectstack/lint: minoras the package that carries the level, and prints the specpatchit is deliberately not refusing.What changed
Clause ② is declared once, for the PR: the
needs:contract-reviewcarrier is a PR label and theClause-②:line is a PR-body line, and neither names a package. The predicate is now the strongest thing that declaration entails, asked at its own grain:The widened package is one of the packages the diff moved src of, so "the widened package is graded
minor+" implies "some moved package is gradedminor+". The converse does not hold, and that gap is named on the green rather than papered over.⛔ No tolerance, no allowlist, no "skip if the diff is comment-only." Nothing here reads the content of a diff hunk. The inputs are unchanged — the packages whose
src/**moved, the levels the changesets grade them, the PR-scoped declaration. Only the quantifier moved. On a single-package PR the old and new predicates are identical; they diverge only where a PR moves several packages' src, which is exactly the shape that produced the false refusal.The refusal message is a deliverable, not cleanup
The old rendering tagged every listed package with
← this PR moves PKG's packages/*/src/**under a headline saying the PR "grew" them, and a reader took the pair for the finding. It was not one. The message now states only what it can read, and says out loud what it cannot:Two new verdicts keep readings from collapsing into a tick.
discharged(exit 0) is a green that judged something and set it aside: it names the package carrying the level, prints thepatchlines it is not refusing, and names its own residual — a second widening gradedpatchbeside the first one'sminorwould not be seen here.clean(exit 0) stays what it was: there was nothing of that shape at all. Thenot-measured-material/not-measured-mootsplit (#16776) reads the same singlerefusablepredicate as the enforcing lane, so materiality cannot drift from enforcement.Direction taken, and the one that was fenced
Direction 1 (a package-scoped
Clause-②: yes (@objectstack/lint)spelling) is fenced for this seat: it migrates the declaration format the whole fleet writes, specified on a governed surface. Not built toward, and this change does not need it.This is direction 2, done without the collapse the card warned about. It does not derive which package the act landed in — that is content-judged and would collapse into direction 1. It removes the need to derive it, by asking the question at the grain the answer is available at. What it reads: the moved package set, the introduced changeset levels, the PR-scoped declaration. What it cannot see: which package the act landed in, therefore a second mis-graded widening in a multi-package PR. That residual is printed on every
dischargedgreen.Direction 3's cost — «a false premise in a refusal message trains readers to stop checking premises» — is paid here as a consequence of the regrain rather than as a wording patch: when the gate fires, every moved package is graded
patch, so the statement it makes is true as written.Tests
node scripts/check-changeset-no-major.mjs --self-test— 179 → 204 assertions, exit 0. New batteryThe GRAIN: a PR-scoped declaration judged at PR scope (#16361)at 25 cases; roster floorSELF_TEST_BATTERY_FLOOR14 → 15. The#16055LEVEL battery (56) and the wiring battery (22) are unchanged and still green.grep -c1→0, injection 0→1) before the run, restored withgit checkout HEAD -- PATHand verified by blob hashaf4b506a20c30c13cabbc8cb6556be98fc1fec16==HEAD:plus an emptygit diff HEAD:refusable = offenders.length > 0) → self-test exit 1, failing on#16347 fired on the wrong package and must now pass — got enforceand on the exit-code pair assertion. Restored → exit 0, 204 assertions.the refusal must SAY it cannot tell which package was widened. Restored → exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 33 families; all 33 run, 33 green, 0 non-zero.--ranreconciliation: 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN. Re-derived after the commit: identical command set.pnpm exec eslint . --no-inline-config --format jsonatba2efa11e6— the whole population, not a narrowing: 6383 files, 0 errors, 0 warnings, exit 0.grep -naPover the C0 set): no hits.Changeset
skip-changeset. Measured, not assumed: across every non-privatepackages/*/package.json, zerofiles[]entries reachscripts/; positive control — thefilesfield is present and read inpackages/cli,packages/client,packages/client-react.scripts/check-changeset-no-major.mjsis repo-root CI tooling that no published package ships.验收备注
.github/workflows/pr-automation.yml的 “WHICH LEVEL” 散文里有一句This is prose, not a gate -- no check computes it; ... the two remaining levels are yours.自 [finding] No gate answers whether a changeset's LEVEL fits the surface — Check Changeset is green on patch and on minor for the same diff #16055 落地起就已经不成立:对声明了 clause-② 的 PR,check-changeset-no-major.mjs正是在计算它,而且本 PR 的拒绝消息把作者指向这段散文。⛔ 未在本 PR 修改 —— 派发词把该文件圈在“只在判定需要新接线时”才动,而这里不需要接线。已作为范围外发现记录,交由 PM 立卡。🤖 Generated with Claude Code
https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Generated by Claude Code