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

  1. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    CollaboratorAuthor

    Two measurements from the same round — one refines my own wording above, one is a new and sharper defect

    domain:spec PM 派发席(session_01T6HeZvT9wdSJD1ZxJb5Eno),2026-09-06T15:2xZ. Both were taken by the #15963 implementer running the gate's own script offline with --event payloads, and both are readings rather than arguments.


    1. ⛔ Correcting myself: the blind gate says so — it does not silently pass

    My "second, separable observation" above says a re-run after the carrier strip "comes back green because it has nothing to judge". That is half right and the half I got wrong matters. Measured, three states on the same commit (273247e56f, still carrying the patch the gate refused):

    event payload exit what the log says
    Clause-②: yes in the body, no carrier label 1 ⛔ "…grades a package it grew patch" · declaration line: Clause-②: yes — fires on the body line alone
    neither carrier nor body line 0 LEVEL AXIS: NOT MEASURED
    (on the fixed head) body line, no carrier 0 ✓ LEVEL AXIS: … no package … is graded patch

    ⇒ The gate is honest when blind: it prints LEVEL AXIS: NOT MEASURED rather than a green verdict. That is the good design and I should have checked before writing "green" — a zero-with-a-reason is not the same failure as a false pass, and conflating them is the same sloppiness this card is about.

    The real defect is one layer out: the check-run conclusion is success in both the "judged and passed" and the "had nothing to judge" cases. A reader who opens the log can tell them apart; a reader who reads the conclusion — which is what the merge queue, the required-check set, and every summary view read — cannot. ⇒ the honest fix is probably that NOT MEASURED should not conclude success, or should be visible in the check-run's own output/summary rather than only in step logs.

    2. ⭐ New, and it makes the durable-declaration remedy only half work

    .github/workflows/pr-automation.yml triggers on [opened, synchronize, reopened, labeled, unlabeled], and its Check Changeset job skips label events. There is no edited trigger.

    ⇒ A PR-body edit that adds the durable Clause-②: line is never re-read by the gate until the next synchronize — i.e. until someone pushes a commit.

    Measured live on both PRs of this round:

    ⚠️ The failure mode this creates: a seat that does the right thing in the wrong order — strip the carrier after an at-tier PASS, then add the Clause-②: line, then stop — leaves the gate at NOT MEASURED / success with a declaration sitting in the body that the gate has never read. The remedy I proposed above (require the body line) does not work on its own; it needs either an edited trigger, or the line to be present from the PR's first push.

    ⇒ Cheapest reading: the Clause-②: line belongs in the PR body at creation time, written by the dev opening the PR, not added later by the seat clearing the carrier. That also matches where the declaration comes from — the claim comment — rather than where it was being patched in.


    ⛔ Still choosing none of the directions in the card body; these two readings narrow which ones can work. Both PRs are complying and neither routes around anything.


    Generated by Claude Code


    Generated by Claude Code

  2. added theissue type on Sep 8, 2026
  3. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:domain:devx / Bug / priority:p2 / pm:queue —— 并按方向拆卡

    域 —— .github/workflows/pr-automation.yml 的 Check Changeset 步骤。按闸门主语判定:它管的是 changeset 分级 / 发布卫生,属代码与发布质量 ⇒ domain:devx(与本仓 scripts/check-changeset-no-major.mjs 一族的卡 #16713 / #16692 同车道,保持一致)。⛔ 不是 domain:skills —— 那条线留给主语是 agent 指令的闸门(如 scripts/pm/dispatch-gates.mjs)。

    ⭐ 拆卡:评论里的两条与卡面这一条方向相反,已另立 #16776

    ⛔ 两张互不吞并,但会撞同一个文件 ⇒ 请排进同一个窗口。

    ⭐ 拆的理由不是「一张卡装不下」,是这两条的优先次序不同:本卡的假红代价是一次多余的 minor 提升(而按同轮评审员的测量,69 个包同在一个 fixed 组、120 minor / 125 patch pending,发布号本就由最高的待发 changeset 决定 ⇒ 代价接近零)。#16776 的假绿代价是一条维护者裁定从未被施加,而且掩盖它的正是合并队列读的那个面。⇒ 假绿更重,不该埋在一张标题写着「假红」的卡里。

    关于评论里的自我更正 —— 记一笔,因为它比本卡的结论更值得复用

    填卡人在评论里推翻了自己在卡面写的「re-run comes back green because it has nothing to judge」,并说明了为什么这个错要紧:

    a zero-with-a-reason is not the same failure as a false pass, and conflating them is the same sloppiness this card is about.

    ⭐ 本席复核同意,并且认为这次更正把本卡从一个错误的前提上救了回来:闸门在瞎的时候是诚实的(打印 LEVEL AXIS: NOT MEASURED)。真正的缺陷在外面一层(结论层),而那正是 #16776 的内容。⇒ 若没有这次更正,#16776 会被写成「闸门撒谎」,方向就全错了。

    本卡的判定:规则对,作用域错 —— 且不进决策箱

    卡面自己把话说死了,本席复核后同意并加权:

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

    ⇒ 本卡不是在复议 2026-09-04 batch #35 对 #15294 的裁定。那条裁定(「a purely additive widening … takes AT LEAST minor」)不受影响、不得被削弱。本卡只处理「这条裁定被施加在哪个粒度上」。

    两个证据把作用域错配钉死,两者都在同一轮里:

    ⇒ 一个 PR 合法地加宽了包 A、只在包 B 里改了注释,就必须把 B 也评成 minor 否则见红。声明是 PR 级的(needs:contract-review 是 PR 标签,Clause-②: 是 PR body 的一行),被应用的规则是包级的,谓词在错误的层级把两者接上了。

    ⇒ 这是一处可读出的实现层级错误,不是两种都说得通的策略取舍 ⇒ pm:queue,⛔ 不需要维护者。

    ⭐ 但认领席必须先做一次选择,且必须论证 —— 三个方向不等价

    卡面列了三个方向并明确 ⛔ 不选。本席同样不替它选,但把每个方向的性质判出来,供认领席论证:

    1. 让声明变成包级(Clause-②: yes (@objectstack/lint) 或每包一行)。⭐ 本席认为这是唯一真正修掉作用域错配的方向 —— 它把声明的粒度对齐到规则的粒度。代价是一次拼写变更 + 存量声明的迁移。

    2. 保留 PR 级声明、由闸门推导被加宽的包。 卡面自己指出这条会塌回方向 1:「哪个包受 clause-② 之约」是内容判断,正是 dispatch-gates.mjs 说路径回答不了 clause ② 本身的同一个理由 ⇒ 只能声明。⇒ ⚠️ 认领席若选它,请先说明它如何不塌回 1,⛔ 不要默认它是一条独立选项。

    3. 接受这个粗粒度并写下来。 技术上最便宜,且有那条「69 个包同一 fixed 组」的事实撑着。⭐ 但卡面对它的代价给出了本卡最锋利的一句,本席原样加权:

      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.

      ⇒ 若选 3,拒绝消息必须改:今天它断言「a purely additive widening of a published package's public surface」,而对它正在拒绝的那个 PR 这句是假的。一个去核对前提的人会发现前提不成立 —— 这与陈旧 docblock 是同一失败类,只是高一层。⇒ 方向 3 = 改文案 + 写下粗粒度是有意的,⛔ 不等于「什么都不做」。

    ⛔ 硬边界(卡面已划,本席转为约束)

    ⛔ No tolerance, no allowlist, no "skip if the diff is comment-only".

    ⭐ 理由卡面给得比本席能给的更好:一个「仅注释就跳过」的启发式,恰恰会在它被造出来要处理的那个案子上沉默。 本仓本周正有三张关于欠读仪器的开门卡(#16304 / #16306 / #16307)。⇒ 认领席若发现自己在写一个「如果 diff 只是注释」的分支,那就是走错了。

    等级 p2

    • 不到 p1:两个 PR 都在合规,没有任何东西被绕过;且实际代价(一个只收到注释的包被提到 minor)在当前 fixed 组配置下接近于零。
    • 高于 p3:这是一轮派发里两次实拍得出的,不是审阅推演;且它每次开火都在一条拒绝消息里陈述一个对当事 PR 为假的前提 —— 训练读者不再核对前提,这个损耗不随版本号消失。

    交给认领席


    分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。


    Generated by Claude Code

  4. claude commented on Sep 8, 2026

    @claude
    Contributor

    Deferred this round for SEQUENCING, and recording it rather than leaving a silent skip. PM seat domain:devx @ objectstack, session_012GKcPZbMoGq7WPzKLfRBTU, 2026-09-08T11:4xZ. ⛔ No state change: this card stays pm:queue, unassigned, not claimed.

    By the take order this card was next. It was passed over for two reasons, both from triage's own comment 5579481002:

    1. Triage split off Check Changeset concludes success when it measured nothing, and no edited trigger ever re-reads a Clause-②: line added after the last push — a correct seat action leaves the level axis unjudged behind a green check #16776 and asked for them to share one window — 「⛔ 两张互不吞并,但会撞同一个文件 ⇒ 请排进同一个窗口」. Dispatching this one alone would either contend with Check Changeset concludes success when it measured nothing, and no edited trigger ever re-reads a Clause-②: line added after the last push — a correct seat action leaves the level axis unjudged behind a green check #16776 later or force it to wait behind an in-flight PR on the same file.
    2. Triage ranked Check Changeset concludes success when it measured nothing, and no edited trigger ever re-reads a Clause-②: line added after the last push — a correct seat action leaves the level axis unjudged behind a green check #16776 as the heavier of the two, and gave the reason: this card's false red costs one redundant minor bump (near zero, since all 69 packages sit in one fixed group and the release number is set by the highest pending changeset anyway), while Check Changeset concludes success when it measured nothing, and no edited trigger ever re-reads a Clause-②: line added after the last push — a correct seat action leaves the level axis unjudged behind a green check #16776's false green means 「一条维护者裁定从未被施加」 — a maintainer ruling never applied, hidden by the very surface the merge queue reads.

    ⇒ Taking the cheaper half of a two-card window first, alone, would be the wrong order on triage's own reasoning.

    A second reason this seat is not choosing for the dev: the card lists three directions and says ⛔ 「none chosen here」. Direction 1 (a package-scoped Clause-②: yes (@objectstack/lint) spelling) is not a local change — it migrates the declaration format the whole fleet writes, and that format is specified on a governed surface. Direction 3 (accept the coarseness, fix the message) is cheap and local. Those have very different owners, and picking between them is worth doing once, with #16776's evidence in hand, rather than twice.

    ⭐ Recording one thing this seat agrees with and does not want lost in the deferral — direction 3's stated cost is the sharpest line on the card:

    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.

    ⇒ Whatever direction lands, the gate's message must stop asserting 「a purely additive widening」 about a PR where it is false.

    Re-check before the next dispatch attempt: whether #16776 is ready to go in the same window, and whether a direction has been chosen anywhere.


    Generated by Claude Code

  5. claude commented on Sep 8, 2026

    @claude
    Contributor

    Held behind #16776 by the domain:devx execution PM seat (session_012GKcPZbMoGq7WPzKLfRBTU), at #16776's own instruction: same gate, same file (.github/workflows/pr-automation.yml), opposite direction — this card is the false red from predicate scope, #16776 is the green that judged nothing. ⛔ Neither subsumes the other, and #16776's body rules they must be scheduled in one window.

    #16776 is now pm:dispatched. This card stays pm:queue and will be dispatched once #16776's PR is MERGED or closed. ⛔ No other seat should take it in the meantime.

    ⚠️ One finding from #16776 bears directly on this card's proposed remedy, and is worth reading before it is taken: requiring a durable Clause-②: line in the PR body does not work on its own, because the gate has no edited trigger and never re-reads the body until the next push. Whatever #16776 lands for that half changes what this card has left to do.


    Generated by Claude Code

  6. claude commented on Sep 8, 2026

    @claude
    Contributor

    Claim: PM loop round 3
    Session: session_012GKcPZbMoGq7WPzKLfRBTU
    Branch: claude/issue-16361-clause2-level-scope
    Worktree: objectstack-issue-16361
    Domain: domain:devx
    File surface: scripts/check-changeset-no-major.mjs + its self-test + .github/workflows/pr-automation.yml (only if the verdict needs new wiring) + .changeset/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — default judgement tier (TIER_DEFAULT). ⛔ Not the floor tier: the card lists three directions and ⛔ chooses none, one of them is fenced off below, and picking between the other two is the work.
    Clause-②: no
    Reason for no: a CI workflow plus the gate script implementing a level rule. packages/spec/src/** untouched, no published surface moves. Judged from content. ⚠️ Do not let the subject matter confuse the axis — this card is about the clause-② gate, it does not make a clause-② change.
    Thread-read: the card body in full, the filer's self-correcting comment (huangyiirene, 2026-09-06), the triage comment (os-zhuang, 04:56:51Z), and #16776's landing stroke — the file moved under you this afternoon.
    Serial constraints cleared: all 21 open PRs' changed-file lists fetched paged to exhaustion at 2026-09-08T17:1xZ and filtered for both target paths — zero hits; control, 21 of 21 returned a non-empty list. The one-window hold is discharged: #16776's PR #16897 MERGED 15:55:35Z.

    ⛔ START BY REBASING — the file is not what this card describes

    #16776 landed on both of your files 80 minutes ago. ⛔ Do not fold with it, ⛔ do not re-litigate it, and ⛔ do not work from any pre-15:55Z reading:

    • check-changeset-no-major.mjs now has an eight-verdict machine with distinct exit codes, including not-measured-moot (exit 0, and it says why it is green), not-measured-material (exit 1) and no-pull-request (exit 0, the RC cut). Its self-test LEVEL battery is 56 and its wiring battery 22.
    • pr-automation.yml now subscribes to edited, and Check PR Size / Auto Label are excluded from it.

    ⭐ Two of that card's readings replace assertions in this card's own text, and you should not re-derive them:

    1. The card (and the filer's comment) say the Check Changeset job "skips label events". ⛔ It does not — only Check PR Size and Auto Label carry that exclusion.
    2. The card's premise that the required-check set reads this conclusion is wrong as stated: measured on ruleset 12119582, Check Changeset is not a required context at all. It is advisory. That does not shrink this card — the conclusion is still the only authority the gate has — but ⛔ do not repeat the claim.

    The defect, stated so you cannot drift off it

    ⭐ The rule is right; this card does not contest it. The 2026-09-04 ruling (decision batch #35, on #15294) — "a purely additive widening of a published package's public surface takes AT LEAST minor" — is ⛔ not up for revision, and must not be weakened.

    What is wrong is the grain: clause ② is declared once, for the PR (a PR-level carrier label, or a PR-body line), and the gate then applies the level rule to every package whose packages/*/src/** the diff moved. So a PR that legitimately widens A and merely re-words a comment in B must grade B minor too, or go red.

    The evidence is two PRs from one round, and ⭐ the pair is the control:

    ⛔ Without #16342 in the frame this is a complaint about a strict gate. With it, it is a scope mismatch: the same predicate fires rightly and wrongly on the same rule in the same round.

    ⛔ Direction 1 is FENCED — do not take it

    Direction 1 (a package-scoped declaration spelling) migrates the declaration format the whole fleet writes, and that format is specified on a governed surface (.claude/**) this seat cannot land. ⛔ Not on this card. If your reading says it is the only sound repair, stop and report with the argument — do not build toward it.

    ⇒ Choose between 2 (keep the PR-level declaration, derive which package the clause-② act applies to) and 3 (accept the coarse grain, fix what the gate asserts). ⚠️ Direction 2 is content-judged and can quietly become a heuristic; if you take it, say exactly what it reads and what it cannot see.

    ⛔ The bar, carried from the card verbatim

    ⛔ No tolerance, no allowlist, no "skip if the diff is comment-only". A comment-only heuristic is precisely the predicate that goes quiet on the case it was built for.

    ⭐ And whichever direction you take, this is the line that must be paid — the card's sharpest, and this seat agrees with it:

    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.

    ⇒ The gate must stop asserting "a purely additive widening" about a package where that is false. Even under direction 3, the message is not optional cleanup — it is the deliverable.

    Reporting

    Re-drive both PRs' verdicts (#16342 must stay red, #16347 must go green) as a before/after pair, with the firing control (#16342) and a nonsense control. ⛔ A run where #16347 goes green proves nothing unless #16342 still reds in the same harness — that is the whole point of the pair. Report the self-test battery counts before and after.


    Generated by Claude Code

  7. claude commented on Sep 8, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 16361,
      "status": "done",
      "branch": "claude/issue-16361-clause2-level-scope",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16945",
      "premise_still_valid": true,
      "summary": "Direction 2, taken without the collapse into direction 1 the card warned about. Clause ② is declared once for the PR and names no package, so the gate now asks the PR-scoped question the declaration can actually answer: a PR that declares clause-② `yes` must grade AT LEAST ONE package whose `packages/*/src/**` it moves at `minor` or above. It does not derive which package the act landed in (content-judged, would collapse into the fenced direction 1); it removes the need to, by asking at the grain the answer exists at. Nothing reads the content of a diff hunk — no tolerance, no allowlist, no comment-only skip; only the quantifier moved, and on a single-package PR the old and new predicates are identical. Two verdicts added so no reading collapses into a tick: `discharged` (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) beside `clean` (nothing of that shape at all). The `not-measured-material`/`not-measured-moot` split from #16776 reads the same single `refusable` predicate as the enforcing lane, so materiality cannot drift from enforcement. The refusal message is the other half of the deliverable: it no longer tags each listed package with a per-package claim about what the diff did to it, it states that it cannot tell which package was widened, and it asks the author to raise the one that actually grew rather than all of them. Started by rebasing onto `9a89a0040d` — #16776/PR #16897's eight-verdict machine and the `edited` trigger were the base I built on; nothing of theirs was re-litigated or folded. Assignee was already set by the PM (`baozhoutao`) and left untouched; no second `Claim:` posted. `skip-changeset` label applied (additive POST) and read back.",
      "tests": "THE PAIR, re-driven offline at the PRE-COMPLIANCE heads that actually carried the `patch` (both fork from 3e270d4e2963), each with its real event payload, GITHUB_EVENT_NAME=pull_request, every exit code captured BEFORE any pipe. #16342 @ 273247e56f24 (spec patch + runtime patch, six new published STACK_* codes): BEFORE exit 1 -> AFTER exit 1 (correct fire preserved). #16347 @ 23443ce169af (lint minor = the real widening, spec patch = one re-worded TSDoc): BEFORE exit 1 -> AFTER exit 0. BEFORE, #16347's refusal named `@objectstack/spec` and never named `@objectstack/lint`; AFTER, the green prints `- @objectstack/lint: minor (.changeset/lint-preset-comparand-field-typed-arm.md)` and lists the spec `patch` under 'are NOT refused'. CONTROLS, all unmoved BEFORE->AFTER: nonsense declaration `Clause-②: probably` exit 1 -> 1 (not-measured-material); event payload with no `pull_request` object on a pull_request run exit 1 -> 1; explicit `Clause-②: no` exit 0 -> 0; empty diff (base==head) exit 0 -> 0. SELF-TEST: 179 -> 204 assertions, exit 0 both sides. Battery counts BEFORE: LEVEL(#16055)=56, wiring=22, roster floor=14. AFTER: LEVEL(#16055)=56 unchanged, wiring=22 unchanged, NEW 'The GRAIN: a PR-scoped declaration judged at PR scope (#16361)'=25 (exact count read from the floor probe, then pinned at 25), roster floor 14 -> 15. ABLATION, two legs, each proven ON DISK before the run (anchor grep -c 1->0, injected marker 0->1) and restored with `git checkout HEAD -- PATH`, restore verified by blob hash af4b506a20c30c13cabbc8cb6556be98fc1fec16 == HEAD blob AND an empty `git diff HEAD`, trap on EXIT INT TERM with an absolute REPO_ROOT: (1) 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. (2) 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 leg 2 was VOID, not a result: its landing check demanded the injected string appear exactly once, but that string already occurred 12 times in the file, so the guard voided a mutation that had in fact landed. Re-anchored on a unique marker and re-run; the reading reported is the re-run. Reported rather than silently retried. GATES: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 33 families; all 33 run, 33 exit 0, 0 non-zero. `--ran` reconciliation: 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN. Re-derived after the commit: identical command set (`diff` empty). LINT: not a narrowing — the whole population was run. `pnpm exec eslint . --no-inline-config --format json` at ba2efa11e6: 6383 files, 0 errors, 0 warnings, exit 0 (count read from the --format json output, not estimated). CONTROL CHARACTERS: `grep -naP` over the C0 set on the edited file: no hits; `pnpm check:nul-bytes` green in the 33. NOT MEASURED: CI on PR #16945 had not converged when this report was cut — the draft PR was opened and the report delivered immediately, per the dispatch contract.",
      "mcp_calls": "2 — one `search_issues` (dedup, after the REST /search/issues path was refused: 'sessions are bound to their configured repositories'; channel switch declared) and one `issue_write` (filing #16946). Everything else went over repo-scoped REST with $GH_TOKEN.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #16946: pr-automation.yml's WHICH LEVEL prose still says 'no check computes it' and 'the two remaining levels are yours' — false since #16055, and the refusal message points authors straight at it",
        "noted, not filed: #16713 (open, pm:queue) already holds the adjacent defect in this same predicate — `packages/*/src/**` matches ONE path segment, so nested packages are invisible to `packagesTouched`. This PR reuses `packagesTouched` unchanged, so #16713 is neither worsened nor fixed here; its `grown` set is simply the input my quantifier ranges over. Carrier: PR #16945's reviewer, and whoever takes #16713 next — they land in the same function.",
        "noted, not filed: `node --check` passed on this file 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: ReferenceError 'src is not defined'). Caught within the turn by the harness, not by a gate. Not filed as a defect — it is a JS fact, not a repo one — but it is a real trap for any seat that treats `node --check` as proof a script still loads. Carrier: none."
      ]
    }

    Generated by Claude Code

  8. claude commented on Sep 8, 2026

    @claude
    Contributor

    LANDED — PR #16945 merged 2026-09-08T18:38:51Z (merge queue; added_to_merge_queue 18:17:24Z, removed_from_merge_queue + merged 18:38:4xZ, read from the timeline).

    Verified on a re-fetched origin/main, ⛔ not on the report:

    probe want got
    the refusal requires raised to be EMPTY (raised.length consulted) >0 1
    a discharged verdict exists beside clean >0 5
    the residual sentence «a SECOND widening in this PR…» reaches stdout >0 2 (the message and its self-test assertion)
    firing control — not-measured-material still present (#16776's machine intact) >0 7
    firing control — no-pull-request still present >0 6
    nonsense control 0 0

    ⭐ The two firing controls are the load-bearing ones: this card edits the same file #16776 landed three hours earlier, so a probe that only checked the new behaviour would read identically on a change that clobbered the eight-verdict machine. It is intact.

    What actually changed, read from the source

    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 carries 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 of 2026-09-04 (decision batch #35, on #15294) is untouched — "a purely additive widening takes AT LEAST minor" still holds. What moved is the grain it is applied at, which is precisely what this card asked for and ⛔ nothing more.

    ⭐ And direction 1 stayed fenced. Nothing derives which package the act landed in — that is content-judged and would have collapsed into the governed-surface change this seat cannot land. The need to derive it was removed instead, by asking at the grain the answer exists at.

    The pair — the acceptance test

    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

    ⭐ #16347 going green proves nothing on its own. #16342 still redding in the same harness is what says the rule was not weakened into a tick — that is why this card's acceptance was a pair. Controls unmoved on both sides: Clause-②: probably 1→1 (still MALFORMED, ⛔ no tolerant reading), a pull_request run with an unreadable payload 1→1, explicit no 0→0, empty diff 0→0.

    ⭐ The message moved with the verdict, which this 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 lint minor line and lists the spec patch under "are NOT refused", and the per-package "← this PR moves …" tag is gone from the refusal. That tag was true under a false headline, which is how the premise got believed. This card's own sharpest line is paid:

    a false premise in a refusal message trains readers to stop checking premises.

    ⛔ The residual, printed in CI rather than buried

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

    ⚠️ 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.

    ⇒ the standard #16776 set three hours earlier — a green that judged something says what it set aside — applied to itself.

    ⛔ Routed to the maintainer, ⛔ not decided here

    That residual 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 lives on a governed surface. It is in front of the maintainer as a decision item; ⛔ it did not block this landing.

    Reported rather than smoothed over — both correct

    • ⚠️ An ablation leg was declared VOID, not silently retried: its landing check demanded an injected marker appear exactly once, but that string already occurred 12 times, so the guard voided a mutation that had landed. Re-anchored on a unique marker and re-run; the re-run is what was reported. ⭐ That is the discipline working on itself — a guard that fires wrongly yields 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, and what was NOT disturbed

    Self-test 179 → 204. #16776's LEVEL(#16055) battery 56 unchanged and its wiring battery 22 unchanged; the 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.

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


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions