Skip to content

fix(tooling): judge the clause-② level rule at the grain the declaration is made - #16945

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-16361-clause2-level-scope
Sep 8, 2026
Merged

fix(tooling): judge the clause-② level rule at the grain the declaration is made#16945
baozhoutao merged 1 commit into
mainfrom
claude/issue-16361-clause2-level-scope

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Part of #16361

Check Changeset's clause-② 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. 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 patch the gate judged (they fork from the same branch point, 3e270d4e2963), with each PR's real event payload. Every exit code captured before any pipe.

harness case head before after
#16342 — spec patch + runtime patch, six new published STACK_* codes. Correct fire. 273247e56f24 exit 1 exit 1
#16347@objectstack/lint minor (the real widening), @objectstack/spec patch (one re-worded TSDoc comment). Fires on the wrong package. 23443ce169af exit 1 exit 0
control: nonsense declaration (Clause-②: probably) on #16342's head 273247e56f24 exit 1 exit 1
control: event payload with no pull_request object, on a pull_request run 273247e56f24 exit 1 exit 1
control: explicit Clause-②: no on #16342's head 273247e56f24 exit 0 exit 0
control: empty diff (base == head) 273247e56f24 exit 0 exit 0

#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: minor as the package that carries the level, and prints the spec patch it is deliberately not refusing.

What changed

Clause ② is declared once, for the PR: the needs:contract-review carrier is a PR label and the Clause-②: 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-② yes must grade at least one package whose packages/*/src/** it moves at minor or above.

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 graded minor+". 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:

⛔ This PR declares clause-② YES, and it grades NO package whose `packages/*/src/**` it moves
   at `minor` or above.

   The packages this PR moves `packages/*/src/**` of, and the level each is graded:
   .changeset/stack-refusal-envelopes.md
     - @objectstack/spec: patch
     - @objectstack/runtime: patch
   ⇒ none of them is graded `minor` or above.
...
⚠️ WHICH of the packages above received that widening is NOT something this gate can read, and it
does not claim to. ... So the finding above is not "each of these was widened"; it is the whole of
what a PR-scoped declaration entails: THE WIDENED PACKAGE IS ONE OF THEM, AND NONE OF THEM CARRIES
THE LEVEL. Raise the one that actually grew. Raising a package that only received a comment is not
asked for here...

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 the patch lines it is not refusing, and names its own residual — a second widening graded patch beside the first one's minor would not be seen here. clean (exit 0) stays what it was: there was nothing of that shape at all. The not-measured-material / not-measured-moot split (#16776) reads the same single refusable predicate 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 discharged green.

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-test179 → 204 assertions, exit 0. New battery The GRAIN: a PR-scoped declaration judged at PR scope (#16361) at 25 cases; roster floor SELF_TEST_BATTERY_FLOOR 14 → 15. The #16055 LEVEL battery (56) and the wiring battery (22) are unchanged and still green.
  • Ablation, both legs, each proven on disk (anchor grep -c 1→0, injection 0→1) before the run, restored with git checkout HEAD -- PATH and verified by blob hash af4b506a20c30c13cabbc8cb6556be98fc1fec16 == HEAD: plus an empty git diff HEAD:
    • predicate reverted to the old per-package form (refusable = offenders.length > 0) → self-test exit 1, failing on #16347 fired on the wrong package and must now pass — got enforce and on the exit-code pair assertion. Restored → exit 0, 204 assertions.
    • the honest sentence deleted from the refusal message → self-test exit 1, failing on the refusal must SAY it cannot tell which package was widened. Restored → exit 0.
    • ⚠️ The first attempt at the second leg was void, not a result: its landing check demanded the injected string appear exactly once, but the injected string already occurred 12 times in the file. Re-anchored on a unique marker and re-run; the reading above is the re-run.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 33 families; all 33 run, 33 green, 0 non-zero. --ran reconciliation: 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN. Re-derived after the commit: identical command set.
  • pnpm exec eslint . --no-inline-config --format json at ba2efa11e6 — the whole population, not a narrowing: 6383 files, 0 errors, 0 warnings, exit 0.
  • Control-character scan over the edited file (grep -naP over the C0 set): no hits.

Changeset

skip-changeset. Measured, not assumed: across every non-private packages/*/package.json, zero files[] entries reach scripts/; positive control — the files field is present and read in packages/cli, packages/client, packages/client-react. scripts/check-changeset-no-major.mjs is repo-root CI tooling that no published package ships.

验收备注


🤖 Generated with Claude Code

https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU


Generated by Claude Code

…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
@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT in substance — PR #16945, reviewed against the tree, ⛔ not against the report

Flip + 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 summary

const 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 patch) to (at least one moved package must carry minor+). ⭐ And the justification is the strong one, not the convenient one: clause ② is a single PR-scoped boolean that names no package, so the most it can entail is that one of the moved packages carries the level. The old predicate was over-reading the declaration; the new one reads exactly what it says.

The maintainer ruling is untouched. "A purely additive widening takes AT LEAST minor" still holds; what changed is the grain the gate applies it at — which is precisely what the card asked for and ⛔ nothing more.

⭐ 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

before after
#16342 @ 273247e56f24 (spec + runtime patch, six new published STACK_* codes) exit 1 exit 1 — correct fire preserved
#16347 @ 23443ce169af (lint minor = the real widening; spec patch = one re-worded TSDoc) exit 1 exit 0

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: Clause-②: probably 1→1 (still not-measured-material, ⛔ no tolerant reading), a pull_request run with no payload 1→1, explicit no 0→0, empty diff 0→0.

⭐ 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 - @objectstack/lint: minor (…) and lists the spec patch under "are NOT refused". The per-package "← this PR moves …" tag is gone from the refusal, because true-under-a-false-headline is how the premise got believed.

⭐ The residual is printed in CI, not buried

discharged is its own verdict beside clean, and its stdout carries, verbatim:

⚠️ Because clause ② is declared once FOR THE PR and names no package, this gate cannot read WHICH package the act landed in. … The residual, named rather than left silent: a SECOND widening in this PR, graded patch beside the minor above, would not be seen here.

⇒ 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 PR

The 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

  • ⚠️ An ablation leg was declared VOID rather than silently retried. Its landing check demanded an injected marker appear exactly once; that string already occurred 12 times, so the guard voided a mutation that had in fact landed. Re-anchored on a unique marker and re-run, and the re-run is what was reported. ⭐ That is the discipline working on itself — a guard that fires wrongly is a void reading, not a result.
  • ⚠️ node --check passed while the module was broken at runtime: a packages/*/src/** written inside a JSDoc block closed the comment at its */ and the tail parsed as live statements. ⛔ Correctly not filed as a repo defect — it is a JS fact — but a real trap for anyone treating node --check as proof a script still loads.

Counts

Self-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 Part of #16361 in the body.

Filed out of scope: #16946pr-automation.yml's WHICH LEVEL prose still says "no check computes it", false since #16055, and the refusal message points authors straight at it.

Checks

0 failures; six legs still running at review time. Flip + arm on green.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants