Skip to content

pm(check-expected-skips): derive the expected-skip roster from the workflows' job-level if: gates - #26648

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-ct165q
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-ct165q

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #26439

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:check-expected-skips 不再手抄「允许 skip 的 job 名单」,改为直接读各 workflow 里每个 job 的 if: 推出名单;手抄的 16 行全部删除。
  • 为什么改:手抄名单一漏就红(check-expected-skips: Build UI Deps (new in #25963) is missing from the roster, so every PR whose ui scope is empty reads exit 4 #26111 就是一次),新增或改名一个带门控的 job 都得来这里补一行;本 PR 实测 main 上已经又漏了两个(Plan Test Core shards、Delete the reapable branches)。
  • 风险与回滚:只动一个 scripts/pm 脚本,不发布、不改 workflow;判错的方向是「多红」不是「放过」——不认识的写法一律拒收并点名。回滚即 revert 本 PR。
  • 先看:scripts/pm/check-expected-skips.mjs 的 gateTerm / jobGate(认哪几种写法)、filterTermProblem(filter 契约三条),再看自测里 ADD / RENAME / UN-GATE 几条。
  • 两分钟验证:node scripts/pm/check-expected-skips.mjs --roster,预期首行写 20 expected-skip name(s), every one derived … none listed by hand,且无 ⚠️ refused 行。
  • 需要你做的:无,席位落地。

What changes

scripts/pm/check-expected-skips.mjs only.

  • The hand roster is gone. EXPECTED_SKIPS (16 rows), GATE_KINDS, gateSpellings, the reason constants and auditRoster are deleted. The roster is now derived from the tree on every run: rosterFromTree = deriveGatedSkips (new) + deriveNonPrEventSkips (existing shape), off one shared scanWorkflowJobs scan.
  • deriveGatedSkips reads each job's job-level if:, splits it at top-level && (outside parentheses and literals), and admits the job when at least one conjunct is a recognised gate term:
    • filter-output: needs.filter.outputs.OUTPUT != 'false', or a parenthesised || whose every alternative is one;
    • pr-event: github.event_name != 'pull_request' or != 'merge_group' (the per-PR events; the full-battery jobs);
    • event-action: github.event.action != 'ACTION' (a labeled / unlabeled / edited run of the same head);
    • label: contains(github.event.pull_request.labels.*.name, 'LABEL') (opt-in) or its ! negation (opt-out).
  • The name is the job's name: verbatim (raw matrix template included), so the hard-coded /3, /8, /16 shard sizes disappear with the list: GitHub reports a pre-expansion skip under the raw name:, never a size read from matrix:.
  • Refusals (the skip stays exit 4, and --roster names it): a filter term whose output lacks the || 'true' widening, is not declared by the filter job, or sits on a job without needs: filter; an if: that reads needs.filter.outputs. in any spelling the recogniser does not read; a name two jobs carry; a name that cannot be told. A job with no gate term at all is never admitted.
  • Each derived row's reason is built from the terms the job spells (queue-widened vs queue-read outputs, full battery, opt-in / opt-out label, unread conjunct count, raw template).
  • judgeCheckRuns and renderRoster now take the roster explicitly (no hand-list default); --roster prints gate: job-gate filter core|crosspkg + pr-event pull_request|merge_group-style lines.
  • The header's "what the roster answers" section is rewritten for the derivation at the same line count, so the self-test-reads declaration stays at :137 (pinned by dispatch-gates.self-test.mjs) — no edit outside this file.

Why

The card: the roster was a hand copy of ci.yml's if: gates and went red on every new or renamed gated job until someone added a row (#26111: Build UI Deps). Measured on origin/main 7c34e5dc2, the class had already recurred twice more on main:

skipped check-run job main's roster this PR
Plan Test Core shards ci.yml › test-plan (gated on core || crosspkg, added with the shard planner) exit 4 exit 0
Delete the reapable branches merged-branch-reaper.yml › reap (!= 'pull_request', the workflow runs on PRs touching itself) exit 4 exit 0

Tests

The self-test is rewritten in this file (no other test file moves): 173 → 206 cases.

Acceptance notes

  • Hypothesis 1 (the card): holds, with one correction. Every one of the 16 hand rows falls out of the rule — measured by --roster before/after on 7c34e5dc2: the 18 names before (16 listed + 2 derived) are all present after, plus the two live misses above, 20 in all, 0 refused. The correction: shard sizes are not read from matrix: blocks; the skipped check-run carries the raw name:, and ci.yml's test matrix is fromJSON(needs.test-plan.outputs.shards …), which has no static size to read.
  • Hypothesis 2: holds. The gate's real question survives: TypeScript Type Check skipped still reds on both trees (below), and the self-test keeps the un-gated cases.
  • reap is now expected. Its if: conjoins github.event_name != 'pull_request', so its skip on a PR head is by construction; the non-PR-event shape still refuses it (that case is kept).
  • Boundary kept: the derivation is a recogniser of closed spellings, never an evaluator; it answers the NAME question only, as before.
  • The file's self-test carries the verdict handshake but not the battery-name floor described in AGENTS.md § Writing a --self-test; unchanged here (not this card), noted only.
  • node scripts/measure-self-test-floor.mjs --self-test exits 1 on this head naming scripts/pm/check-prior-rulings.mjs and scripts/pm/dispatch-gates.mjs (handshake spelling census); neither file is touched here and the script is not a wired check, so it reads the same on main.

Verification

All readings on head 98a3dc80.

Acceptance proof on disk: add + rename a gated ci.yml job (one-off, scratch worktrees)

In two scratch worktrees (main 7c34e5dc2 and this head), ci.yml was edited: build-docs renamed to Build Docs Site, and a new job Brand New Gated Job added with if: ${{ !cancelled() && needs.filter.outputs.core != 'false' }}. Then --self-test and --check-runs-json on one-skip payloads:

main this PR
--self-test exit 1 — Build Docs: ci.yml job build-docs is named "Build Docs Site" — the check-run name moved; correct the row exit 0, 206 cases
Brand New Gated Job skipped exit 4 exit 0
Build Docs Site skipped exit 4 exit 0
Build Docs (the old name) skipped exit 0 (false OK) exit 4

No file under scripts/pm was edited for the PR-side column.

The check still reds on the defect it was written for

One-skip payloads, --check-runs-json, current tree:

skipped main this PR
TypeScript Type Check (no gate) exit 4 exit 4
Plan Test Core shards exit 4 exit 0
Delete the reapable branches exit 4 exit 0
Build UI Deps (the #26111 job) exit 0 exit 0
Local gates (dispatch-gates --commands, 31 families)
  • pnpm check:pm-expected-skips → ✓ check-expected-skips self-test: 206 cases pass
  • the 30 other derived commands: exit 0 each (list in the card report)
  • pnpm check:pm-dispatch-gates → exit 0, ✓ dispatch-gates self-test (fast tier): 1935 cases pass
  • dispatch-gates --ran → 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED (a DERIVED zero)
  • pnpm check:ci-filter-parity exit 0, pnpm check:required-contexts exit 0 — neither reads this file (git grep for its name in both scripts: 0 hits), so their output is unchanged by construction
  • eslint --no-inline-config scripts/pm/check-expected-skips.mjs exit 0; check:nul-bytes exit 0

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint on the file locally; repo-wide in CI)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset — nothing publishes (scripts/pm/** only), skip-changeset; both declaration lines are filled in above.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the required Chinese summary).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ADHkmTBKk6ks5yi6zYDatg


Generated by Claude Code

…rkflows' job-level if: gates

The roster of jobs allowed to skip was a hand copy of ci.yml's `if:` gates,
so a gated job added or renamed in a workflow read exit 4 on every PR its gate
said no to until someone added a row. The hand list (EXPECTED_SKIPS) and its
audit are gone: deriveGatedSkips reads every job's job-level `if:` and admits
it when a top-level `&&` conjunct is a recognised gate term (a `filter` output
carrying the `|| 'true'` widening, a per-PR event excluded, a pull_request
action excluded, a PR label read), under the raw name its skipped check-run
carries. A job with no gate term still reds when it skips; an unwidened,
undeclared or un-needed filter output, an unread filter spelling, a shared or
untellable name are refused and named.

The self-test judges the measured heads against a frozen fixture tree, so no
live gated job name is pinned here.

Claude-Session: https://claude.ai/code/session_01ADHkmTBKk6ks5yi6zYDatg
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 27, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:skills#1 · session_01ADHkmSrRBdu9MdC6fc33Uu · head 98a3dc80 · 2026-10-27T11:43Z

  1. 形态 — one file, scripts/pm/check-expected-skips.mjs (+654/−490); check-governed-merges --pr 26648: NOT governed. The hand roster (EXPECTED_SKIPS, GATE_KINDS, gateSpellings, auditRoster) is gone; deriveGatedSkips reads each job's job-level if: and admits only a recognised top-level && gate term (filter output, non-PR event, PR action, PR label) — a recogniser, never an evaluator; an unread filter spelling is refused by name, not skipped.
  2. 前提实测 — seat read the derivation and its refusal paths on the head. Dev's on-disk proof: renaming plus adding a gated ci.yml job reds main's self-test and leaves this head green with no scripts/pm edit; a skipped TypeScript Type Check still exits 4 (the check still reds on what it was written for). All 16 hand rows reproduce; the two extra rows (Plan Test Core shards, Delete the reapable branches) are gated jobs main's hand list missed.
  3. 门禁 check-runs — head 98a3dc80: 26 success, 17 skipped, 0 failing; Lint · repo & tooling gates (which runs check:pm-expected-skips) green.
  4. 接受的偏差 — the card's hypothesis "shard sizes from matrix:" was corrected: the raw name: carries them; noted in the report. Two stray empty files in the dev's container root are outside the repo.
  5. 范围外去向 — none. Skills-docs: none.
  6. 落地条款 — not governed: ready + auto-merge through the queue by this seat.

Generated by Claude Code

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

Labels

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

Projects

None yet

2 participants