Skip to content

ci(lint): tooling self-tests run on a PR only when their own inputs change (#19498) - #19511

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-19498-self-tests-off-pr-path
Sep 21, 2026
Merged

os-project-manager merged 1 commit into
mainfrom
claude/issue-19498-self-tests-off-pr-path

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #19498

Clause-②: no

Gate weakening is a maintainer floor. The sentence that authorizes this one, verbatim (ruling #208 on #19491, part R4):

19491 接受你的建议,并立刻派发处理相关任务。

What this changes

Lint & Repo Gates set the wall clock of PR #19314's CI — 27.4 minutes over 184 steps for a three-file packages/spec diff, above the longest test shard — and 18.6 of those minutes were the tooling's own self-tests, corpora and censuses. The single PM dispatch-gates self-test step was 11.8 of them, on a PR that changes no PM tool: that family's read-set included the whole-tree censuses its battery runs — the content of every JS/TS and .sh file, the nested .gitignore files, and the tracked NAME set, which made an ADDED path anywhere run it.

  1. pm_dispatch_gates is narrowed to the tool's own inputs: the workflow tree and composite actions its discovery reads, every gate source it resolves under scripts/ and a workspace package's own scripts/, the package.json that names a check:* script, the agent configuration its live cases read (.claude/**, skills/**, AGENTS.md, CLAUDE.md), and root configuration. The self-test's own refusal semantics are untouched — an unreadable population still refuses.
  2. Four more tooling steps go behind the selector, each as a family with a read-set of its own inputs plus the matching if: line in lint.yml.
  3. push to main and the hourly scheduled run are unchanged and keep the whole battery: the selector runs everything for any event it does not scope.

The census: every unconditional step at or above ~0.3 min

Measured on run 35506407130, job Lint & Repo Gates (check-run 106066910262) at head 7d67e1ee4136aee8f8e6ea838950c7fb71520be2, read step by step from GET /repos/{owner}/{repo}/actions/jobs/106066910262. 184 steps, 27.4 min.

step min subject disposition
PM dispatch-gates self-test 11.80 tooling self-test already scoped — read-set narrowed
Engine query-options erasure ratchet 2.07 product ratchet unchanged (scoped by its real read-set)
ESLint 1.27 product lint stays unconditional (#16496 card ruling 2)
Slot-lookup ratchet 1.07 product ratchet unchanged (scoped by its real read-set)
Comment mask agrees with a real parser over the whole corpus 0.90 corpus agreement unchanged (already a family)
scripts/ entry guards go through one predicate 0.62 tooling corpus over scripts/** movedentry_guard
Engine test-double contract gate 0.50 product gate stays unconditional
Self-test workflow-command gate 0.48 tooling self-test movedself_test_workflow_commands
A declared gate population reaches the tree 0.40 tooling gate over the derivation moveddeclared_population_live
Checkout repository 0.40 runner infrastructure not a gate
ADR anchors + number uniqueness 0.37 docs/ADR gate stays unconditional
Tenant-audit census matches the tree 0.35 product census stays unconditional (named in the card)
PM bare-root worklist self-test 0.32 tooling self-test movedbare_root_worklist

Below the line, left unconditional because the list decides and not the principle: scripts/ shared-module self-tests 0.27, Cross-package test inputs 0.25, Declared registry log level 0.25, Platform-object tenancy census 0.20, Merge-driver wiring gate 0.20, Documented HTTP status matches the status the runtime emits 0.17, ObjectQL double limit gate 0.17, Changeset-family gate self-tests 0.15. Every other step in the job measured under 0.17 min.

Product ratchets and censuses stay unconditional throughout: query_options_erasure, slot_lookup, the tenancy and tenant-audit censuses, the engine gates.

The four families added, and what each reads

family step command read-set
entry_guard pnpm check:entry-guard scripts/** — its own ROOT_DIR_WATCH_HINTS, held against the root it walks by its own self-test
declared_population_live pnpm check:declared-population-live imports discoverFamilies + trackedFiles: the workflow tree, every gate source discovery resolves, the tracked NAME set (only a name that DISAPPEARS moves its verdict, and deletions already run everything)
bare_root_worklist node scripts/pm/bare-root-worklist.mjs (self-test only) the same derivation, the same read-set
self_test_workflow_commands node scripts/check-self-test-workflow-commands.mjs its declared scripts/** population of .mjs, .mts and .sh files, plus the workflow tree and .github/actions it discovers the runnable self-tests from

What is weaker now, said out loud

A ratchet's skip says: no changed path is one this family reads. A tooling self-test's skip now says something weaker: no changed path is one the tool's own inputs name, while the battery behind it may still read that path through a whole-tree census. So a defect these five would have caught can first appear on main instead of on the PR that wrote it.

Three things bound that, and none of them changed here: every doubt still runs everything (unresolvable base, empty diff, unclassified path, any deletion, rename or type change); push on main and the hourly schedule run the whole battery; and widening the skip further is again a maintainer call. The selector's header carries this paragraph beside the read-sets, and lint.yml carries it on the steps themselves — including the three steps whose prose used to say "unconditional, like every self-test around it", which this change would otherwise have made false.

Two previously pinned cases are given up deliberately and are now pinned in the other direction, so the loss is legible: an ADDED file anywhere no longer runs pm_dispatch_gates (the tracked-NAME sweep), and neither does a nested .gitignore or a workspace .sh outside scripts/ (the #16769 case).

Acceptance — the four dry runs, verbatim

(a) pull_request, changed files = PR #19314's list. Reproduced in a throwaway detached worktree off origin/main (c9b23cd066), driven with this branch's selector; the worktree was removed afterwards and nothing under scripts/pm/ is touched by this PR.

-- (a) changed files --
A	.changeset/19150-declares-collection-pipe-authorable-side.md
A	packages/spec/src/compose-stacks-collection-pipe-arm.test.ts
M	packages/spec/src/stack.zod.ts
Gate-family diff base: c9b23cd066284a87d4cbe6fb53bd8f24fad2898e  (merge-base of origin/main and HEAD)
Gate families: 3 run, 6 skipped  (event: pull_request; changed paths: 3)
  run   slot_lookup            reads packages/spec/src/compose-stacks-collection-pipe-arm.test.ts (A, workspace)
  run   query_options_erasure  reads packages/spec/src/compose-stacks-collection-pipe-arm.test.ts (A, workspace)
  skip  entry_guard            no changed path is in its read-set
  run   comment_mask_corpus    reads packages/spec/src/compose-stacks-collection-pipe-arm.test.ts (A, workspace)
  skip  pm_dispatch_gates      no changed path is in its read-set
  skip  declared_population_live no changed path is in its read-set
  skip  bare_root_worklist     no changed path is in its read-set
  skip  self_test_workflow_commands no changed path is in its read-set
  skip  verify_lock            no changed path is in its read-set
VERDICT command-exit=0

(b) pull_request, changed file scripts/pm/dispatch-gates.mjs — same throwaway worktree:

-- (b) changed files --
M	scripts/pm/dispatch-gates.mjs
Gate-family diff base: c9b23cd066284a87d4cbe6fb53bd8f24fad2898e  (merge-base of origin/main and HEAD)
Gate families: 6 run, 3 skipped  (event: pull_request; changed paths: 1)
  skip  slot_lookup            no changed path is in its read-set
  skip  query_options_erasure  no changed path is in its read-set
  run   entry_guard            reads scripts/pm/dispatch-gates.mjs (M, scripts)
  run   comment_mask_corpus    reads scripts/pm/dispatch-gates.mjs (M, scripts)
  run   pm_dispatch_gates      reads scripts/pm/dispatch-gates.mjs (M, scripts)
  run   declared_population_live reads scripts/pm/dispatch-gates.mjs (M, scripts)
  run   bare_root_worklist     reads scripts/pm/dispatch-gates.mjs (M, scripts)
  run   self_test_workflow_commands reads scripts/pm/dispatch-gates.mjs (M, scripts)
  skip  verify_lock            no changed path is in its read-set
VERDICT command-exit=0

(c) push — every family runs:

Gate families: 9 run, 0 skipped  (event: push; changed paths: 0)
  run   slot_lookup            event 'push' is not scoped -- the full battery runs
  run   query_options_erasure  event 'push' is not scoped -- the full battery runs
  run   entry_guard            event 'push' is not scoped -- the full battery runs
  run   comment_mask_corpus    event 'push' is not scoped -- the full battery runs
  run   pm_dispatch_gates      event 'push' is not scoped -- the full battery runs
  run   declared_population_live event 'push' is not scoped -- the full battery runs
  run   bare_root_worklist     event 'push' is not scoped -- the full battery runs
  run   self_test_workflow_commands event 'push' is not scoped -- the full battery runs
  run   verify_lock            event 'push' is not scoped -- the full battery runs
VERDICT command-exit=0

(d) the selector's self-test (pnpm check:select-gate-families), which also pins the YAML half against the real lint.yml:

  ok    the workflow scopes exactly the families the script decides (9)
  ok    each family gates exactly one step
  ok    pm_dispatch_gates gates the step running: pnpm check:pm-dispatch-gates
  ok    query_options_erasure gates the step running: pnpm check:query-options-erasure
  ok    slot_lookup gates the step running: pnpm check:slot-lookup
  ok    entry_guard gates the step running: pnpm check:entry-guard
  ok    declared_population_live gates the step running: pnpm check:declared-population-live
  ok    bare_root_worklist gates the step running: node scripts/pm/bare-root-worklist.mjs
  ok    self_test_workflow_commands gates the step running: node scripts/check-self-test-workflow-commands.mjs
  ok    verify_lock gates the step running: bash scripts/pm/os-verify-lock.sh
  ok    comment_mask_corpus gates the step running: node scripts/check-comment-mask-corpus.mjs
all 44 cases passed (228 checks)
VERDICT command-exit=0

The battery grew from 30 cases / 120 checks at its floor to 44 / 228, and the floor moves with it (42 / 220). New cases: the nine ids in job order, an ADDED path inside a read-set, a composite action, the PR #19314 shape under both merge_group and pull_request, and one pin_step per new family.

For completeness, this PR's own diff under pull_request — a change to the selector and the workflow runs all five tooling families:

Gate-family diff base: 2cac3636cab1cecef8f7a0453e4963dd1d01fb21  (merge-base of origin/main and HEAD)
Gate families: 5 run, 4 skipped  (event: pull_request; changed paths: 3)
  skip  slot_lookup            no changed path is in its read-set
  skip  query_options_erasure  no changed path is in its read-set
  run   entry_guard            reads scripts/ci/select-gate-families.selftest.sh (M, scripts)
  skip  comment_mask_corpus    no changed path is in its read-set
  run   pm_dispatch_gates      reads .github/workflows/lint.yml (M, workflow)
  run   declared_population_live reads .github/workflows/lint.yml (M, workflow)
  run   bare_root_worklist     reads .github/workflows/lint.yml (M, workflow)
  run   self_test_workflow_commands reads .github/workflows/lint.yml (M, workflow)
  skip  verify_lock            no changed path is in its read-set
VERDICT command-exit=0

Expected wall clock

The five steps carry 13.62 min of the 27.4-min job (11.80 + 0.62 + 0.48 + 0.40 + 0.32). On a diff of #19314's shape all five skip and nothing else changes, so Lint & Repo Gates should read about 13.8 min — arithmetic on one run's step timings, on that runner with its cache state, not a prediction of the next run. The card's bar is ≤ 16 min, and the director seat measures the real number on the first product PR after this lands.

Tier S — not on the governed register

$ node scripts/pm/check-governed-merges.mjs --branch claude/issue-19498-self-tests-off-pr-path
derived from `git diff --name-only --no-renames 2cac3636ca 9a1ca2bab2` (three-dot): 3 path(s).
    origin/main = 48c39e0023, claude/issue-19498-self-tests-off-pr-path = 9a1ca2bab2, merge-base = 2cac3636ca.
    size: +333 / -111 over 3 file(s) (0 binary, counted 0) — `git diff --numstat --no-renames` on the same range.
governed-surface predicate: 0 of 3 path(s) hit the register (6 surfaces, repo-agnostic).
  ✅  NOT governed — ordinary queue landing applies to a PR with exactly this file list.
  size: 444 changed line(s) (+333 / -111) ≤ 5000 — under the human-merge threshold (generated files included in the count).
VERDICT command-exit=0

Gates

skip-changeset: nothing under packages/**, nothing published.

Every family node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives for this diff was run locally, each exit code captured before any pipe — 51 commands, 50 exit 0, including check:self-test-wired, check:self-test-workflow-commands, check:step-collectors, check:declared-population-live, check:entry-guard, check:watch-hint-literal, check:required-contexts, check:workflow-status-functions, check:ci-filter-parity, check:bash32-floor and check:nul-bytes. pnpm check:pm-dispatch-gates was run detached per its own header and passed: ✓ dispatch-gates self-test: 1883 cases pass. / the battery took 1056.1s on this box.

The one command that did not return a verdict: pnpm check:type-check-debt exits 3 = PREREQUISITE NOT MET in a fresh worktree (--re-measure cannot run: 31 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk), which its own remedy text declares is neither a pass nor a finding. This diff touches no TypeScript, and CI builds the closure before that step.


Generated by Claude Code

…hange

`Lint & Repo Gates` was the wall clock of a product PR's CI (27.4 min over
184 steps on PR 19314's head, above the longest test shard), and 18.6 of
those minutes were the tooling's own self-tests, corpora and censuses. The
single `PM dispatch-gates self-test` step was 11.8 min on a three-file
`packages/spec` diff, because that family's read-set included the
whole-tree censuses its battery runs: the content of every JS/TS and .sh
file, the nested .gitignore files, and the tracked NAME set that made any
ADDED path anywhere run it.

Narrow that family to the tool's own inputs — the workflow tree and
composite actions it discovers, every gate source it resolves under
scripts/ and a workspace package's own scripts/, the package.json that
names a check:* script, the agent configuration its live cases read, and
root configuration — and put four more tooling steps behind the same
selector with a read-set each:

  entry_guard                   0.62 min   scripts/**
  self_test_workflow_commands   0.48 min   scripts/** + the workflow tree
  declared_population_live      0.40 min   the derivation's read-set
  bare_root_worklist            0.32 min   the derivation's read-set

Product ratchets and censuses stay unconditional: the two engine ratchets,
the tenancy and tenant-audit censuses and everything else that judges
product code is untouched, as is ESLint. Steps under ~0.3 min stay
unconditional too — the scripts/ shared-module self-tests (0.27) and the
changeset-family gate self-tests (0.15) among them.

The skip these five carry is a WEAKER claim than a ratchet's and the code
and the workflow both say so: a ratchet skips only where it provably reads
nothing, while a tooling self-test skips where the tool's own inputs are
untouched and its battery may still read the changed path through a
census. Gate weakening is a maintainer floor; the authorizing sentence is
quoted where the read-sets are declared. What bounds it is unchanged:
`push` to main and the hourly scheduled run take the unscoped branch and
run the whole battery.

The selector's self-test grows with it — the nine ids, the per-family
read-sets, an ADDED path inside a read-set, the shape of PR 19314 under
both `merge_group` and `pull_request`, and a `pin_step` for each new
family against the real lint.yml.

Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE
Co-Authored-By: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Contract review

Independent at-tier contract review of PR #19511 (card #19498) at head 9a1ca2bab200fd924323fa4b2af5164641d03359, ruling #208 R5 shape: the diff and the card read, CI taken from the head's check-runs over REST, no gate family re-run. The only local commands run were the subject itself — bash scripts/ci/select-gate-families.selftest.sh and select-gate-families.sh dry runs — in a fresh worktree at origin/main d114d4c40b with the PR branch fetched into a ref of its own. Written 2026-09-21T05:13Z.

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9a1ca2bab200fd924323fa4b2af5164641d03359

① Derived judgments

Adversarial brief: find what is wrong with the diff. Judged item by item.

1. Scope — RIGHT. git diff origin/main...9a1ca2ba --name-status is exactly M .github/workflows/lint.yml, M scripts/ci/select-gate-families.selftest.sh, M scripts/ci/select-gate-families.sh; one commit; nothing under scripts/pm/, packages/, or the patrol workflow. Six commits landed on main since the merge-base 2cac3636ca, none touching these three files (git log 2cac3636..origin/main -- the three paths is empty); the PR reports mergeable.

2. The pm_dispatch_gates read-set — RIGHT, honest against what the self-test reads. Why #19314 matched the OLD arm: [ "$status" = M ] || return 0 ran the family for ANY non-M status, so its two ADDED paths ran it by status alone before any class was consulted, and its modified stack.zod.ts ran it a second way through is_masked_source (the whole-tree compound-anchor / mkdtemp census) — three paths, three hits, exactly as the dev says. The NEW arm keeps the tool's own inputs: reads_gate_tree (workflow tree incl. .github/actions, scripts/** at any depth, a workspace package's own scripts/, any */package.json) plus the agent-config class (.claude/**, skills/**, AGENTS.md, CLAUDE.md), and unknown/root-config still run everything. I opened scripts/pm/dispatch-gates.mjs (28,769 lines; pnpm check:pm-dispatch-gates runs check-dispatch-gates.mjs, whose production spawn is dispatch-gates.mjs --self-test, confirmed at its line 334) and enumerated every fixed-path read: .github/workflows (readdir + lint.yml, ci.yml, pr-automation.yml, spec-liveness-check.yml), .github/actions/*/action.yml (the composite reader), root package.json scripts and each workspace package.json it resolves a check:* through, every gate source under scripts/** and packages/*/scripts/** (check-generated.ts, build-schemas.ts, check-llms-txt.ts, packages/lint/scripts/check-doc-formula-expressions.mjs, bump-objectui.sh, …), .claude/skills/pm-dispatch/SKILL.md and references/review-checklist.md, .claude/agents/os-dev.md, .claude/settings.json, skills/**, AGENTS.md, tsconfig.json and root .gitignore (both root-config). Every one is inside the new read-set. The i18n live walks (findExtractConfigs / findMetadataFormModules over packages/) read config CONTENT only under packages/*/scripts/ (inside the set) and pin the form-module walk as length above zero and no duplicates — an ADDED .form.ts cannot move them, a deleted one is structural and runs everything. What the new read-set omits is exactly what the ruling took off the PR path and nothing else: the CONTENT census of every JS/TS and .sh file (trackedFiles() + readFileSync in the compound-anchor census and the scratch-dir sweep), nested .gitignore, and the tracked-NAME sweep — declared in the selector header, on the step, and in the PR body. turbo.json, which the card guessed at, is not read by the tool at all (no non-comment read); it is root-config anyway. No omitted input found.

3. The four new families — RIGHT on all four.

  • entry_guardpnpm check:entry-guard = scripts/check-entry-guard.mjs: walks SCRIPTS = HERE (scripts/), ROOT_DIR_WATCH_HINTS = ['scripts/**'], imports only ./invoked-as.mjs and ./js-comment-mask.mjs. Arm: scripts|verify-lock run; docs|changeset|workflow|agent-config|workspace skip; * run. Matches.
  • declared_population_livescripts/check-declared-population-live.mjs: imports discoverFamilies, trackedFiles, hintCovers, watchHintTree from the derivation; reads the workflow tree, the resolved gate sources, the tracked NAME set. Only a name that disappears moves live→refused; deletions are structural and run everything. Matches reads_gate_tree.
  • bare_root_worklistnode scripts/pm/bare-root-worklist.mjs --self-test: imports the same derivation; its self-test reads trackedFiles(), discoverFamilies().byCheck, each family's gate source, and its own source. I read its live pins on the name set (srcSegmentHit.length above zero, srcSegmentHit.length below underPkg.length, existence claims): every one is an inequality an ADDED name cannot flip. Matches.
  • self_test_workflow_commandsscripts/check-self-test-workflow-commands.mjs: population ROOT_DIR_WATCH_HINTS = ['scripts/**/*.mjs','scripts/**/*.mts','scripts/**/*.sh'] via collectPopulation from check-self-test-wired.mjs, corpus roots WORKFLOW_DIR = '.github/workflows' and ACTION_DIR = '.github/actions'; it spawns the selected self-tests and reads their OUTPUT. The spawned self-tests' transitive reads are the stated weaker claim, not a hidden one. Matches.
  • lint.yml carries exactly nine if: steps.gate-families.outputs.X != 'skip' lines on nine distinct steps (awk over the head: lines 382, 404, 607, 808, 1001, 1048, 1091, 1798, 1845); the four new ones sit on scripts/ entry guards go through one predicate, A declared gate population reaches the tree, PM bare-root worklist self-test, Self-test workflow-command gate and nowhere else. ESLint (pnpm lint, line 368) has no if:. The query_options_erasure|slot_lookup, verify_lock and comment_mask_corpus arms are untouched by the diff; the tenancy / tenant-audit census steps carry no if:. FAMILIES now lists the nine ids in the job's real run order (the old line listed pm_dispatch_gates first while the job ran slot_lookup first — repaired here).

4. Dry runs — reproduced, all RIGHT. Run with THIS PR's selector (absolute path, RUNNER_TEMP=/tmp/rt-review), every exit 0.

(a) pull_request against PR #19314's REAL head 7d67e1ee41 (fetched as refs/pull/19314/head, merge-base with origin/main e6a03e6491 — the dev used a throwaway shape; same verdicts):

A	.changeset/19150-declares-collection-pipe-authorable-side.md
A	packages/spec/src/compose-stacks-collection-pipe-arm.test.ts
M	packages/spec/src/stack.zod.ts
Gate-family diff base: e6a03e649154252740b8e3534804e7abc453fb5c  (merge-base of origin/main and HEAD)
Gate families: 3 run, 6 skipped  (event: pull_request; changed paths: 3)
  run   slot_lookup            reads packages/spec/src/compose-stacks-collection-pipe-arm.test.ts (A, workspace)
  run   query_options_erasure  reads packages/spec/src/compose-stacks-collection-pipe-arm.test.ts (A, workspace)
  skip  entry_guard            no changed path is in its read-set
  run   comment_mask_corpus    reads packages/spec/src/compose-stacks-collection-pipe-arm.test.ts (A, workspace)
  skip  pm_dispatch_gates      no changed path is in its read-set
  skip  declared_population_live no changed path is in its read-set
  skip  bare_root_worklist     no changed path is in its read-set
  skip  self_test_workflow_commands no changed path is in its read-set
  skip  verify_lock            no changed path is in its read-set
VERDICT command-exit=0

(b) pull_request, only scripts/pm/dispatch-gates.mjs modified (throwaway branch off origin/main d114d4c40b):

Gate families: 6 run, 3 skipped  (event: pull_request; changed paths: 1)
  skip  slot_lookup            no changed path is in its read-set
  skip  query_options_erasure  no changed path is in its read-set
  run   entry_guard            reads scripts/pm/dispatch-gates.mjs (M, scripts)
  run   comment_mask_corpus    reads scripts/pm/dispatch-gates.mjs (M, scripts)
  run   pm_dispatch_gates      reads scripts/pm/dispatch-gates.mjs (M, scripts)
  run   declared_population_live reads scripts/pm/dispatch-gates.mjs (M, scripts)
  run   bare_root_worklist     reads scripts/pm/dispatch-gates.mjs (M, scripts)
  run   self_test_workflow_commands reads scripts/pm/dispatch-gates.mjs (M, scripts)
  skip  verify_lock            no changed path is in its read-set
VERDICT command-exit=0

(c) push:

Gate families: 9 run, 0 skipped  (event: push; changed paths: 0)
  run   slot_lookup            event 'push' is not scoped -- the full battery runs
  run   query_options_erasure  event 'push' is not scoped -- the full battery runs
  run   entry_guard            event 'push' is not scoped -- the full battery runs
  run   comment_mask_corpus    event 'push' is not scoped -- the full battery runs
  run   pm_dispatch_gates      event 'push' is not scoped -- the full battery runs
  run   declared_population_live event 'push' is not scoped -- the full battery runs
  run   bare_root_worklist     event 'push' is not scoped -- the full battery runs
  run   self_test_workflow_commands event 'push' is not scoped -- the full battery runs
  run   verify_lock            event 'push' is not scoped -- the full battery runs
VERDICT command-exit=0

(d) bash scripts/ci/select-gate-families.selftest.sh at the head: exit 0, all 44 cases passed (228 checks), the nine pin_step lines all ok, 2.2 s.

(e) pull_request, only .github/workflows/lint.yml modified:

Gate families: 4 run, 5 skipped  (event: pull_request; changed paths: 1)
  skip  slot_lookup            no changed path is in its read-set
  skip  query_options_erasure  no changed path is in its read-set
  skip  entry_guard            no changed path is in its read-set
  skip  comment_mask_corpus    no changed path is in its read-set
  run   pm_dispatch_gates      reads .github/workflows/lint.yml (M, workflow)
  run   declared_population_live reads .github/workflows/lint.yml (M, workflow)
  run   bare_root_worklist     reads .github/workflows/lint.yml (M, workflow)
  run   self_test_workflow_commands reads .github/workflows/lint.yml (M, workflow)
  skip  verify_lock            no changed path is in its read-set
VERDICT command-exit=0

entry_guard skipping on a workflow-only change is correct — it walks scripts/ and nothing else.

(f) pull_request, only packages/spec/src/stack.zod.ts modified: Gate families: 3 run, 6 skippedslot_lookup, query_options_erasure, comment_mask_corpus run (M, workspace); entry_guard, pm_dispatch_gates, declared_population_live, bare_root_worklist, self_test_workflow_commands and verify_lock skip. The five tooling families all skip.

(g) This PR's own diff under pull_request: 5 run, 4 skipped — all five tooling families run (entry_guard via the selftest .sh, the other four via lint.yml), matching the PR body.

5. The selftest's own hazard — RIGHT with one precision. The floor is two literals, [ "$cases" -lt 42 ] || [ "$checks" -lt 220 ], judged against a 44-case / 228-check battery. What it refuses is a loss of three or more cases or nine or more checks; a single lost case (three to eight checks) passes the floor. That is the pinned-TOTAL shape the dev flagged as pre-existing and out of scope under R6, and it is TIGHTER than before, not looser: I ran the selftest at origin/main and it reports all 40 cases passed (192 checks) against the old floor 30/120 — ten cases and 72 checks of slack, now two and eight. What refuses the loss of a scoped step is the YAML pin, and I reproduced the dev's ablation in the worktree: deleting the entry_guard if: line (git hash-object of lint.yml 01a87d77b5316506, one deletion) turns the selftest RED — exit 1, FAIL the workflow scopes exactly the families the script decides, FAIL each family gates exactly one step, FAIL entry_guard gates the step running: pnpm check:entry-guard, SELFTEST FAILED (44 cases, 228 checks); git checkout -- .github/workflows/lint.yml restored blob 01a87d77, git status --porcelain empty, selftest GREEN again at 44/228.

One prose inaccuracy, PR body and dev report only, no code change needed: 「The battery grew from 30 cases / 120 checks at its floor to 44 / 228」 — the prior battery was 40 cases / 192 checks (the FLOOR was 30/120). The direction of the claim holds; the number does not.

6. lint.yml header prose — RIGHT. The on: block now names #19498 and the four families as part of the push-on-main full-battery argument. The job header cites 「ruling #208 on #19491 (R4) and the maintainer's sentence quoted in the selector's header」 and repeats ⛔ product ratchets and censuses stay unconditional. The pm_dispatch_gates step keeps its 「Until #16496 this step was deliberately UNCONDITIONAL … was the maintainer's call (#16496, 「同意你的建议,你负责执行派发所有可行的优化」)」 paragraph and adds a ⚠️ paragraph stating the read-set is now NARROWER than the battery reads, what the battery also reads, the 11.8 / 27.4 measurement on #19314, and 「ruling #208 on #19491 (R4) took those whole-tree reads off the PR path on the maintainer's sentence, quoted in the selector's header. ⛔ A defect in that wider set can now first appear on main」. The verbatim sentence 「19491 接受你的建议,并立刻派发处理相关任务。」 is quoted in select-gate-families.sh's header section 「The self-test families are POLICY-scoped, not read-set-scoped (#19498)」 and in the PR body; lint.yml cites the card and the ruling and points at the quote — the brief's 「quoted or the card cited」 is met. The weaker claim is stated honestly in all three places, and the three steps whose prose read 「unconditional, like every self-test around it」 were rewritten to name the ruled exception rather than left false.

7. Expected saving — RIGHT. GET actions/jobs/106066910262 (the Lint & Repo Gates check-run of head 7d67e1ee41, run 35506407130): job wall 27.45 min over 184 steps; the five now-scoped steps measure PM dispatch-gates self-test 11.80, scripts/ entry guards go through one predicate 0.62, Self-test workflow-command gate 0.48, A declared gate population reaches the tree 0.40, PM bare-root worklist self-test 0.32 — sum 13.62; 27.45 − 13.62 = 13.83, the dev's 「about 13.8 min」. Steps are serial, so the subtraction is a wall-clock claim. Under the card's ≤ 16 bar on arithmetic; the real number is the director seat's measurement after landing.

8. Nothing else wrong found. The removed [ "$status" = M ] || return 0 is the deliberate half of the narrowing (an ADDED path now runs the family only inside its read-set; pinned both ways by the selftest — scripts/pm/new-tool.mjs (A, scripts) runs it, docs/new-page.md (A, docs) does not). Fail-open is intact: structural changes, empty diff, unresolvable base, unspellable path, unknown class and root-config all still run everything; every new case ends in a run arm.

② Semver level

Carriers: none hung. Clause-②: no. Nothing under packages/**; skip-changeset is on the PR; CI-only change with no published surface.

③ Boundary flags

Tier S — node scripts/pm/check-governed-merges.mjs --branch claude/issue-19498-self-tests-off-pr-path: 「governed-surface predicate: 0 of 3 path(s) hit the register (6 surfaces, repo-agnostic). ✅ NOT governed」; size 444 changed lines (+333 / −111) ≤ 5000. The director seat lands it after PASS and green contexts.

Required contexts on head 9a1ca2ba, read from the check-runs 2026-09-21T05:13Z (PR is draft, no auto-merge, labels ci/cd, size/m, skip-changeset):

context conclusion note
TypeScript Type Check success run id 106221125572
Test Core success run id 106220505778
Dogfood Regression Gate success run id 106219043796
Build Core skipped run id 106219044437
Temporal Conformance (live PG + MySQL) skipped run id 106219044002
Lint & Repo Gates in_progress — NOT MEASURED run id 106218990342
Governed Surface Queue Guard success run id 106218990101

skipped on Build Core and Temporal Conformance is the path filter on a diff with nothing under packages/**. No failure anywhere among the head's check-runs. The one context still running at the poll, Lint & Repo Gates, is NOT MEASURED here and is the director seat's to read green before landing — it is the context that carries this change (it runs check:select-gate-families and, on this PR's own diff, all five tooling families).

Implemented-by: claude/issue-19498-self-tests-off-pr-path
Reviewed-by: session_012GcsUbuqFGBibkEDMRC1eE

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Provenance — director seat, summon #25 (session_012GcsUbuqFGBibkEDMRC1eE), 2026-09-21T05:15Z


Generated by Claude Code

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

Labels

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

Projects

None yet

2 participants