Skip to content

Commit 1167b4c

Browse files
claude[bot]claude
andauthored
ci(lint): run each scoped Lint & Repo Gates family only on the paths it reads (#16754)
* ci(lint): run each scoped gate family only on the paths it reads (#16496) Lint & Repo Gates is the merge queue's critical path after #16453 (median 1468 s over the last ten merge-group runs; the PM dispatch-gates self-test alone 597 s), and none of its expensive steps read a merge group's file surface. A selector under scripts/ci/ -- the shape select-shard-packages.sh set -- classifies the changed paths on merge_group and pull_request and skips a family only when every path is one that family provably never reads. Every doubt runs every family: an unscoped event, an unresolvable base, an empty or failed diff, an unclassified path, a deletion or rename. The workflow steps spell `!= 'skip'`, so an absent output runs the step too. Five families are scoped, each with its read-set derived from the gate's own source: the PM dispatch-gates self-test, the query-options erasure ratchet, the slot-lookup ratchet, the verify-lock self-test and the comment-mask corpus walk. ESLint is not scoped. push on main and any scheduled run keep the full battery. The self-test drives every branch offline and also reads lint.yml to pin the YAML half of the contract: the selector step's id and env, the `!= 'skip'` spelling on every scoped step, and that the scoped set equals the decided set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU * ci(lint): keep the selector's hint surface honest for the dispatch derivation The self-test now spells its fixture paths unquoted and pins the verify-lock step by its script name, so scripts/pm/dispatch-gates.mjs extracts exactly the two files it reads (the selector and lint.yml) and check-self-test-wired does not read a pinned command fragment as a flag of this file. Header paths in both scripts are spelled without backticks for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU * ci(lint): widen two read-sets the contract review measured narrower than their gates pm_dispatch_gates now runs on a modification of any JS/TS source anywhere and of any nested .gitignore: the dispatch-gates self-test reads the content of every source file (compound-anchor census, exposed-scratch-dir sweep) and consults nested ignore rules, so a workspace source edit could have reddened the step the selector skipped. verify_lock now runs on a modification of any top-level scripts/*.mjs and any workspace package.json: case (h) of its suite runs the real entry point from the repo root, which preflights through scripts/pnpm-filter-targets.mjs and enumerates the workspace. Headers, the lint.yml notes and the self-test cases say so; two new cases pin the nested .gitignore and the non-source workspace file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8b37a09 commit 1167b4c

4 files changed

Lines changed: 1118 additions & 11 deletions

File tree

.github/workflows/lint.yml

Lines changed: 115 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,45 @@ jobs:
174174
- name: Install dependencies
175175
run: pnpm install --frozen-lockfile
176176

177+
# ── Gate-family selection (#16496) ───────────────────────────────────
178+
# After #16453 this job is the merge queue's critical path (median 1468 s
179+
# over the ten merge-group runs measured for #16496; the PM dispatch-gates
180+
# self-test alone 597 s), and none of its expensive steps read a
181+
# merge group's file surface: a docs-only group paid the full battery.
182+
# So on `merge_group` and `pull_request` the FIVE scoped families below
183+
# -- each step carrying `if: steps.gate-families.outputs.<id> != 'skip'`
184+
# -- run only when the changed paths touch the files that family reads.
185+
# `push` on main and the scheduled full run keep the whole battery: the
186+
# script runs everything for any event it does not scope.
187+
#
188+
# ⭐ The invariant is FAIL-OPEN, and it holds at both layers. The script
189+
# runs every family when the base cannot be resolved, the diff fails or
190+
# is empty, a path is one it does not classify (a new top-level
191+
# directory, an unlisted root file), or any change is a deletion, rename
192+
# or type change; a family is skipped ONLY when every changed path is
193+
# positively classified into a class that family provably never reads.
194+
# The `!= 'skip'` spelling means an ABSENT output -- the selector never
195+
# ran, or wrote nothing -- also runs the step. Both halves are pinned by
196+
# `scripts/ci/select-gate-families.selftest.sh` (`check:select-gate-
197+
# families`, further down this job), which also reads THIS file and
198+
# refuses any other `if:` spelling and any family the script does not
199+
# decide. The per-family read-sets, and how each was measured, are in
200+
# the script's header.
201+
#
202+
# The selection itself lives in scripts/ci/select-gate-families.sh:
203+
# environment in, `$RUNNER_TEMP/gate-families.txt` + `$GITHUB_OUTPUT`
204+
# out, and a ran/skipped table in the job summary (the seat reads it).
205+
# This step only exports the event's fields, so the same script -- every
206+
# branch of it -- runs locally and under its self-test.
207+
- name: Select the gate families this run pays for
208+
id: gate-families
209+
env:
210+
OS_GATE_EVENT_NAME: ${{ github.event_name }}
211+
OS_GATE_PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
212+
OS_GATE_MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
213+
run: |
214+
bash scripts/ci/select-gate-families.sh
215+
177216
# #7484 docs anchors: `lychee.toml` sets `include_fragments = "none"`, so
178217
# the `Check Documentation Links` job resolves a link's FILE and never its
179218
# `#anchor` — measured with the pinned lychee 0.24.2 under the CI argv, a
@@ -251,7 +290,12 @@ jobs:
251290
# file is ignored COMPLETELY, so new erasures added to one ride the old
252291
# entry in silence. This re-measures those files with the grandfathering
253292
# lifted and holds them to a per-file count, so the list can only shrink.
293+
#
294+
# Scoped (#16496): reads `packages/**/*.{ts,tsx,mts,cts}`, its baseline,
295+
# `eslint.config.mjs` and the top-level `scripts/*.mjs` it imports; a
296+
# group touching none of those skips it (selection step above).
254297
- name: Slot-lookup ratchet
298+
if: steps.gate-families.outputs.slot_lookup != 'skip'
255299
run: pnpm check:slot-lookup
256300

257301
# Engine query-options erasure ratchet (#4918). The slot-lookup rule above
@@ -269,7 +313,11 @@ jobs:
269313
# slot-lookup for the non-test residual, plus one aggregate decrease-only
270314
# number for test code (a test whose subject IS off-contract engine input
271315
# must be able to build it). Runs its own --self-test first.
316+
#
317+
# Scoped (#16496): the same population and inputs as the slot-lookup
318+
# ratchet above, with `scripts/query-options-erasure-baseline.json`.
272319
- name: Engine query-options erasure ratchet
320+
if: steps.gate-families.outputs.query_options_erasure != 'skip'
273321
run: pnpm check:query-options-erasure
274322

275323
# @objectstack/verify stand-in erasure guard (#6399). The third and
@@ -620,7 +668,13 @@ jobs:
620668
# Cost, measured on a contended 4-vCPU container: 4,741 files, 72 MB of
621669
# source, 45–48 s, of which the parser is 45 s. The step prints CI's own
622670
# number on every run.
671+
#
672+
# Scoped (#16496): the walk reads every `.ts .tsx .mts .cts .js .mjs
673+
# .cjs .jsx` file in the tree, so a group that changes no file of those
674+
# extensions (and no root configuration) skips it -- selection step at
675+
# the top of this job.
623676
- name: Comment mask agrees with a real parser over the whole corpus
677+
if: steps.gate-families.outputs.comment_mask_corpus != 'skip'
624678
run: node scripts/check-comment-mask-corpus.mjs
625679

626680
# Stack-collection enumerations vs the schema (#6242). `stack.zod.ts`
@@ -745,9 +799,28 @@ jobs:
745799
# green and surfaced later as a prompt naming the wrong gate families —
746800
# output that reads as correct, produced by the one tool whose purpose is
747801
# to stop gate lists being memory-shaped. Same family as the
748-
# `Changeset-family gate self-tests` step above, and like that one this
749-
# step is deliberately UNCONDITIONAL: no `if:`, no label read, no paths
750-
# filter. An exemption is what a self-test must not have, or the gap moves.
802+
# `Changeset-family gate self-tests` step above. Until #16496 this step
803+
# was deliberately UNCONDITIONAL -- no `if:`, no label read, no paths
804+
# filter -- on the argument that an exemption is what a self-test must
805+
# not have, or the gap moves. That argument was about an exemption a
806+
# PATH FILTER or a LABEL would grant: a paths: trigger or a label read
807+
# skips the step on a card that never touched the paths someone once
808+
# listed, with no reader of what the test actually reads. What runs
809+
# here now is different in kind and was the maintainer's call (#16496,
810+
# 「同意你的建议,你负责执行派发所有可行的优化」): the selection step at
811+
# the top of this job classifies every changed path against this
812+
# self-test's MEASURED read-set (every workflow, every gate source under
813+
# `scripts/**` and `packages/*/scripts/**`, every `package.json`,
814+
# `.claude/**`, `skills/**`, `AGENTS.md`, `CLAUDE.md`, `tsconfig.json`,
815+
# every `.gitignore` -- nested ones included -- the CONTENT of every
816+
# JS/TS file in the tree (the compound-anchor census of
817+
# `function ...SelfTest...(` declarations and the exposed-scratch-dir
818+
# sweep of every mkdtempSync/mkdirSync caller both assert over it), and
819+
# the tracked NAME set it sweeps -- so any added, deleted or renamed
820+
# file runs it too), and skips this step only when every path is one
821+
# the test provably never reads: a modified doc, changeset or non-source
822+
# workspace file. Every doubt runs it, the self-test of the selector
823+
# pins that, and `push` on main keeps it unconditional.
751824
#
752825
# The gate runs the SELF-TEST only. The live derivation
753826
# (`node scripts/pm/dispatch-gates.mjs <path>`) answers a question about a
@@ -775,13 +848,17 @@ jobs:
775848
# counts stand here either, for the same reason — they rot the same way,
776849
# and it is the SHAPE of the work, not its size, that the argument needs.
777850
#
778-
# ⛔ Cost was never what makes this step unconditional. The FIRST
779-
# paragraph above is, and it stands without this half: an exemption is
780-
# what a self-test must not have, or the gap moves. Losing the estimate is
781-
# therefore NOT an argument for an `if:`, a label read or a paths filter —
782-
# weakening this gate is a maintainer call, and no reading of this note
783-
# authorises one.
851+
# ⛔ Cost was never what decided this step's conditionality, and the
852+
# measured medians that motivated #16496 (597 s here, over ten
853+
# merge-group runs) live in that card and its PR, not in this note.
854+
# Losing the estimate is NOT an argument for a label read or a paths
855+
# filter, and the read-set-scoped `if:` below is not one of those: it
856+
# was a maintainer call (the first paragraph names it), and widening the
857+
# skip -- any change to the classes the selector treats as inert for
858+
# this family -- is again a maintainer call, taken in
859+
# scripts/ci/select-gate-families.sh under its self-test.
784860
- name: PM dispatch-gates self-test
861+
if: steps.gate-families.outputs.pm_dispatch_gates != 'skip'
785862
run: pnpm check:pm-dispatch-gates
786863

787864
# Every ROOT_DIR_WATCH_HINTS declaration stays READABLE BY A TEXT SCANNER
@@ -1318,9 +1395,21 @@ jobs:
13181395
#
13191396
# It runs entirely on a private lock under a temp dir; it never touches
13201397
# /tmp/os-heavy-verify.lock, so a runner and an agent container behave the
1321-
# same. Unconditional and un-`if:`-ed, like the self-tests above — an
1322-
# exemption is what a self-test must not have. No network, ~40 s.
1398+
# same. No network; median 100 s over the ten merge-group runs measured
1399+
# for #16496.
1400+
#
1401+
# Scoped (#16496): the suite reads the script itself (`$SELF`) and a
1402+
# private temp dir, and its case (h) runs the real entry point from the
1403+
# repo root, which routes through filter_preflight ->
1404+
# scripts/pnpm-filter-targets.mjs --preflight and so reads the top-level
1405+
# scripts/*.mjs helpers that imports, pnpm-workspace.yaml and every
1406+
# workspace package.json. A group touching none of those skips it. That
1407+
# is not the exemption the self-tests above refuse (a label or a paths
1408+
# filter with no reader behind it): the selection step at the top of
1409+
# this job runs this step on every doubt, and `push` on main keeps it
1410+
# unconditional.
13231411
- name: Verify-lock entry-point self-test
1412+
if: steps.gate-families.outputs.verify_lock != 'skip'
13241413
run: bash scripts/pm/os-verify-lock.sh --self-test
13251414

13261415
# ci-failure self-test (#9898). Fifth member of the PM self-test family
@@ -3299,6 +3388,21 @@ jobs:
32993388
- name: Test Core package selection self-test
33003389
run: pnpm check:select-shard-packages
33013390

3391+
# Gate-family selection self-test (#16496). The "Select the gate families
3392+
# this run pays for" step at the top of this job is a thin call into
3393+
# scripts/ci/select-gate-families.sh, and its `merge_group` branch cannot
3394+
# run before a PR is already in the queue, so it is proved here: every
3395+
# event, the card's four cases (a scripts/pm change, a docs-only group, a
3396+
# workflow change, an unknown path), and every fail-open branch (empty
3397+
# diff, unresolvable base, deletion / rename, unspellable path), pinning
3398+
# which families ran and skipped, for which reason, and which
3399+
# `::warning::` lines fired. It also reads THIS file and pins the YAML
3400+
# half: the selector step's id and env, the `!= 'skip'` spelling on every
3401+
# scoped step, and that the scoped set equals the decided set. Offline;
3402+
# ~3 s.
3403+
- name: Gate-family selection self-test
3404+
run: pnpm check:select-gate-families
3405+
33023406
# Aggregator roster gate (#10490). Three required contexts are aggregate
33033407
# jobs standing in for a set of real jobs — `Test Core` and `Dogfood
33043408
# Regression Gate` in ci.yml, `TypeScript Type Check` in this file — and

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
"check:pnpm-acquisition": "node scripts/check-pnpm-acquisition.mjs --self-test && node scripts/check-pnpm-acquisition.mjs",
139139
"check:workflow-status-functions": "node scripts/check-workflow-status-functions.mjs --self-test && node scripts/check-workflow-status-functions.mjs",
140140
"check:select-shard-packages": "bash scripts/ci/select-shard-packages.selftest.sh",
141+
"check:select-gate-families": "bash scripts/ci/select-gate-families.selftest.sh",
141142
"check:shard-attestation": "node scripts/check-shard-attestation.mjs --self-test && node scripts/check-shard-attestation.mjs",
142143
"check:required-contexts": "node scripts/check-required-contexts.mjs --self-test && node scripts/check-required-contexts.mjs",
143144
"check:cross-package-test-inputs": "node scripts/check-cross-package-test-inputs.mjs --self-test && node scripts/check-cross-package-test-inputs.mjs",

0 commit comments

Comments
 (0)