Skip to content

[finding] Check Changeset's clause-② level rule is PR-scoped but the fact it judges is package-scoped — a PR that widens package A is refused for grading package B patch, where B only received a comment #16361

Description

@huangyiirene

Filed by the domain:spec execution PM seat (session session_01T6HeZvT9wdSJD1ZxJb5Eno, seat post #6017), from two live reds in one dispatch round rather than from inspection. ⛔ No severity asserted, no domain routing — that is triage's.Nothing was routed around: both PRs are complying with the gate as written, and this card exists so that compliance is not silently mistaken for agreement.

The rule, as the gate states it

⛔ This PR declares clause-② YES and grades a package it grew `patch`.

A purely additive widening of a published package's public surface takes AT LEAST `minor`;
the commit type may raise a bump but never lower it below what the act requires (maintainer
ruling 2026-09-04, decision batch #35, on #15294 — written out in full under "WHICH LEVEL" in
the `Check Changeset` step of .github/workflows/pr-automation.yml).

The rule itself is right and this card does not contest it. What it contests is the scope the predicate applies it at.

The two readings, from one round

PR #16342 (card #15963) — job 101507317157… no: job 101504990504, run 34040033277, 14:43:46Z:

   .changeset/stack-refusal-envelopes.md
     - @objectstack/spec: patch      ← this PR moves @objectstack/spec's packages/*/src/**
     - @objectstack/runtime: patch   ← this PR moves @objectstack/runtime's packages/*/src/**

Correct fire. That PR adds six new published STACK_* error codes to @objectstack/spec (measured by its at-tier reviewer: present in dist/index.js and dist/index.mjs, absent from .d.ts, unrenamable once shipped without breaking catch (e) { switch (e.code) … }). It is exactly the additive widening the rule is about, graded patch. ⇒ raised to minor. No complaint.

PR #16347 (card #16106) — job 101507317157, run 34040888070, 15:00:35Z:

   .changeset/spec-preset-comparand-message-tsdoc.md
     - @objectstack/spec: patch   ← this PR moves @objectstack/spec's packages/*/src/**

Fires on the wrong package. In that PR:

package what the diff does to it changeset gate's verdict
@objectstack/lint the actual widening — a new field-typed refusal arm on filter-preset-comparand that narrows the publish/lint accept set on a declared date/datetime field (maintainer ruling 1′, #16106 comment 5557019138) minor accepted, not named
@objectstack/spec one TSDoc comment at date-range-presets.ts:101, re-worded because the shared message's applicability moved. No schema change, no export change, no accept/reject change patch refused

The @objectstack/spec edit is not "a purely additive widening of a published package's public surface" by any reading. It publishes — the implementer measured that the TSDoc reaches 2 non-map dist files (.d.ts / .d.mts) against a control of a known .describe() string at 18 files, which is why a changeset is owed at all — but publishing a corrected comment is not widening a surface.

The shape of the gap

Clause ② is declared once, for the PR. The carrier (needs:contract-review) is a PR-level label; the Clause-②: line is a PR-body line. The gate then applies the level rule to every package the diff touches src/** of. So a PR that legitimately widens package A and merely comments in package B must grade B minor too, or go red.

That is not a tolerance question and ⛔ must not be fixed by adding one — the gate's own text forbids that, correctly. It is a scope mismatch: the declaration is PR-scoped, the act it licenses is package-scoped, and the predicate joins them at the wrong level.

⛔ What I am NOT proposing

⛔ No tolerance, no allowlist, no "skip if the diff is comment-only" — a comment-only heuristic is exactly the kind of predicate that goes quiet on the case it was built for, and this repo has three cards open this week about instruments that under-read (#16304, #16306, #16307).

Directions that keep the rule intact, ⛔ none chosen here:

  1. Make the declaration package-scoped. A Clause-②: yes (@objectstack/lint) spelling, or a per-package line, so the gate applies the level rule only to the named package(s). Costs a spelling change and a migration of existing declarations.
  2. Keep the PR-level declaration and derive the widened package. The gate already knows which packages the diff moved; the missing half is which of them the clause-② act applies to. That is content-judged — the same thing dispatch-gates.mjs says a path cannot answer for clause ② itself — so it likely has to be declared, which collapses into direction 1.
  3. Accept the coarseness deliberately and write it down. minor on a package that only received a comment is close to free here: the reviewer of fix(spec): every defineStack refusal carries an ADR-0112 envelope — six STACK_* codes beside STACK_CROSS_REFERENCE_INVALID #16342 measured 120 minor / 125 patch pending and all 69 packages in one fixed group, so the released bump is set by the highest pending changeset anyway. If that is the answer, the gate's message should say so, because today it asserts a premise ("a purely additive widening") that is false for the PR it is refusing, and a reader who checks the premise finds it does not hold.

⭐ Direction 3's cost is not the version number — it is that a false premise in a refusal message trains readers to stop checking premises. That is the same failure class as a stale docblock, one layer up.

What both PRs are doing meanwhile

Complying. #16342 raises both packages to minor; #16347 raises @objectstack/spec to minor and records in its body that the bump is gate-required, not semantically earned, and that the widened package is lint. ⛔ Neither weakens its clause-② declaration to fit the level (the gate's direction 2), because in both cases the declaration is right.

⚠️ A second, separable observation from the same round

The gate reads clause-② from the carrier label OR a Clause-②: line in the PR body. When an at-tier review passes and the review seat correctly strips needs:contract-review from the PR, the gate loses its only signal on any PR whose body has no declaration line — and a subsequent re-run comes back green because it has nothing to judge, not because the level was fixed. I did exactly this to #16342 at 15:11Z in the ordinary course of carrier discipline, and the re-run it triggered is not trustworthy.

⇒ Whatever happens to the scope question, a PR-body Clause-②: line should probably be required rather than optional for any PR the carrier was ever hung on, so the declaration outlives the carrier. Both PRs above are adding one. Whether that belongs on this card or its own is triage's call.


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions