From 3bf0e985ec230297a0a1541db23e34fcd25e3ea8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 14:16:49 +0000 Subject: [PATCH 1/2] ci(lint): key the PM dispatch-gates family on the files its battery opens The pm_dispatch_gates arm of select-gate-families.sh ran the PM self-test for every change under scripts/ and every piece of agent configuration. One full battery run under an fs/child-process read hook shows the battery never opens a data or prose file under scripts/ outside scripts/pm/, nor skills/**, AGENTS.md, CLAUDE.md or the .claude/ prose and JSON outside the pm-dispatch rulebook. Those now skip the step on pull_request and merge_group; scripts/pm/** whole, every code file under any scripts/, the rulebook, .claude code, the workflow tree and every package manifest still run it, and a scripts/ data file a relative import specifier names runs it (the one edge by which the discovery could open one). push on main and the scheduled run keep the whole battery. Self-test: 12 new or re-pinned cases, floor raised to 68 cases / 354 checks. Claude-Session: https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw Co-authored-by: Claude --- scripts/ci/select-gate-families.selftest.sh | 120 +++++++++++++++- scripts/ci/select-gate-families.sh | 143 ++++++++++++++++---- 2 files changed, 235 insertions(+), 28 deletions(-) diff --git a/scripts/ci/select-gate-families.selftest.sh b/scripts/ci/select-gate-families.selftest.sh index 9b9b462a130..f745655d559 100644 --- a/scripts/ci/select-gate-families.selftest.sh +++ b/scripts/ci/select-gate-families.selftest.sh @@ -20,6 +20,14 @@ # edit, an added test, an added changeset) must skip every tooling self-test, # while the tool's own inputs must still run them. # +# Since #22076 the same section pins the pm_dispatch_gates read-set to the +# files the battery OPENS, as measured: a ratchet baseline, prose under +# scripts/ and agent prose outside the pm-dispatch rulebook skip it, while +# scripts/pm/** whole, the rulebook, a `.claude/**` shell script, a data file a +# relative specifier names and a scripts/ file of an unlisted kind still run +# it; a pull request touching only packages/*/src/** runs no PM self-test, and +# `schedule` and `push` still run it over the very changes the PR path skips. +# # Since #19753 it covers migration_registry, the one family that ADDS a gate # to the PR path: an entry, the generated registry, the generator and the two # package files that decide how it runs must each select it; a spec source @@ -96,7 +104,20 @@ printf '#!/usr/bin/env bash\necho ci\n' > "$UP/scripts/ci/tool.sh" printf 'name: lint\n' > "$UP/.github/workflows/lint.yml" printf '# agent\n' > "$UP/.claude/agents/os-dev.md" printf '# skill\n' > "$UP/skills/x/SKILL.md" +# The pm_dispatch_gates read-set as measured (#22076): the rulebook its live +# cases open, a hook shell script the shell-mask census opens, and a data file +# under scripts/ that a module imports through a relative specifier. The +# specifier is assembled from an unquoted word on purpose, for the reason the +# two repo paths at the top are spelled as they are. +mkdir -p "$UP/.claude/skills/pm-dispatch/references" "$UP/.claude/hooks" +printf '# rules\n' > "$UP/.claude/skills/pm-dispatch/references/rules.md" +printf '#!/usr/bin/env bash\necho guard\n' > "$UP/.claude/hooks/guard.sh" +printf '{}\n' > "$UP/.claude/settings.json" +printf '{"rows":[]}\n' > "$UP/scripts/table.json" +TABLE_SPECIFIER=./table.json +printf "import table from '%s' with { type: 'json' };\nexport const rows = table.rows;\n" "$TABLE_SPECIFIER" > "$UP/scripts/reads-table.mjs" printf '# rules\n' > "$UP/AGENTS.md" +printf '# claude\n' > "$UP/CLAUDE.md" printf '# readme\n' > "$UP/README.md" printf -- '---\n"a": patch\n---\nchange\n' > "$UP/.changeset/first.md" # The migration_registry read-set (#19753), and its nearest neighbours outside it. @@ -297,6 +318,27 @@ run_case 'push: the event decides, not the variables that happen to be set' "$RE expect_rc 0 expect_all_run +# The backstop, held over the very change set the PR path now lets the PM +# self-test skip (#22076): an unscoped event never reads the diff. +S=$(scenario M:scripts/slot-lookup-baseline.json M:AGENTS.md M:skills/x/SKILL.md) +run_case 'schedule: a change the PR path skips the PM self-test for still runs it -- the hourly full run is the backstop (#22076 control)' "$REPO" schedule '' '' +expect_rc 0 +expect_warnings '' '' +expect_all_run +expect_reason pm_dispatch_gates "event 'schedule' is not scoped" + +run_case 'push: the same change on main still runs the PM self-test (#22076 control)' "$REPO" push main "$C0" +expect_rc 0 +expect_warnings '' '' +expect_all_run +expect_reason pm_dispatch_gates "event 'push' is not scoped" + +run_case 'merge_group: the same change, scoped, skips the PM self-test -- so the two controls above are not vacuous' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_warnings '' '' +expect_verdicts slot_lookup query_options_erasure entry_guard declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason pm_dispatch_gates 'no changed path is in its read-set' + # ── merge_group: the card's four cases ────────────────────────────────────── S=$(scenario M:scripts/pm/tool.mjs) run_case 'merge_group: a scripts/pm change runs every tooling self-test (and, for a .mjs, the corpus walk)' "$REPO" merge_group '' "$C0" @@ -516,9 +558,38 @@ expect_reason verify_lock scripts/helper.mjs expect_reason migration_registry 'no changed path is in its read-set' S=$(scenario M:scripts/slot-lookup-baseline.json) -run_case 'merge_group: a ratchet baseline runs the ratchets and every family that reads scripts/' "$REPO" merge_group '' "$C0" +run_case 'merge_group: a ratchet baseline runs the ratchets and the families that walk scripts/, and not the PM self-test -- the battery opens no data file (#22076)' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_warnings '' '' +expect_verdicts slot_lookup query_options_erasure entry_guard declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason pm_dispatch_gates 'no changed path is in its read-set' +expect_reason entry_guard '(M, scripts)' + +S=$(scenario M:scripts/table.json) +run_case 'merge_group: a data file a relative import specifier names still runs the PM self-test -- the one edge by which the discovery opens a data file' "$REPO" merge_group '' "$C0" expect_rc 0 +expect_warnings '' '' expect_verdicts slot_lookup query_options_erasure entry_guard pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason pm_dispatch_gates 'scripts/table.json (M, scripts)' + +S=$(scenario A:scripts/notes.md A:scripts/ci/fixtures/job-log.txt) +run_case 'merge_group: prose and a fixture log under scripts/ run the families that walk scripts/, and not the PM self-test (#22076)' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_warnings '' '' +expect_verdicts entry_guard declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason pm_dispatch_gates 'no changed path is in its read-set' + +S=$(scenario A:scripts/ci/matrix.yaml) +run_case 'merge_group: a scripts/ file of a kind the arm does not list still runs the PM self-test -- fail-open on the extension' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_verdicts entry_guard pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason pm_dispatch_gates 'scripts/ci/matrix.yaml (A, scripts)' + +S=$(scenario A:scripts/pm/ledger.json) +run_case 'merge_group: a data file under scripts/pm still runs the PM self-test -- scripts/pm is in its read-set whole' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_verdicts entry_guard pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason pm_dispatch_gates 'scripts/pm/ledger.json (A, scripts)' S=$(scenario M:scripts/ci/tool.sh) run_case 'merge_group: a scripts/ subdirectory script is a gate source only' "$REPO" merge_group '' "$C0" @@ -526,9 +597,29 @@ expect_rc 0 expect_verdicts entry_guard pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands S=$(scenario M:.claude/agents/os-dev.md M:skills/x/SKILL.md M:AGENTS.md) -run_case 'merge_group: agent configuration runs the PM self-test alone -- it is the only battery here that reads it' "$REPO" merge_group '' "$C0" +run_case 'merge_group: agent prose the battery never opens (an agent definition, a published skill, AGENTS.md) runs no family (#22076)' "$REPO" merge_group '' "$C0" expect_rc 0 +expect_warnings '' '' +expect_verdicts +expect_reason pm_dispatch_gates 'no changed path is in its read-set' + +S=$(scenario M:CLAUDE.md M:.claude/settings.json) +run_case 'merge_group: CLAUDE.md and the .claude settings JSON run no family (#22076)' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_verdicts + +S=$(scenario M:.claude/skills/pm-dispatch/references/rules.md) +run_case 'merge_group: the pm-dispatch rulebook runs the PM self-test alone -- its live cases open it' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_warnings '' '' expect_verdicts pm_dispatch_gates +expect_reason pm_dispatch_gates '(M, agent-config)' + +S=$(scenario M:.claude/hooks/guard.sh) +run_case 'merge_group: a .claude hook shell script still runs the PM self-test alone -- only prose and JSON left the read-set' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_verdicts pm_dispatch_gates +expect_reason pm_dispatch_gates '.claude/hooks/guard.sh (M, agent-config)' S=$(scenario M:package.json) run_case 'merge_group: root configuration runs every family' "$REPO" merge_group '' "$C0" @@ -649,6 +740,29 @@ expect_verdicts slot_lookup query_options_erasure comment_mask_corpus expect_reason pm_dispatch_gates 'no changed path is in its read-set' expect_line 'Gate families: 3 run, 7 skipped' +# The card's pin and triage's (#22076): product code alone runs no PM +# self-test; scripts/pm/** runs it, prose included. +git_q -C "$REPO" checkout -q -B feature-22076-src "$C0" +printf 'export const a = 3;\n' > "$REPO/packages/a/src/index.ts" +printf 'export const index = 2;\n' > "$REPO/packages/spec/src/index.ts" +git_q -C "$REPO" commit -q -am 'F4: product source in two packages, nothing else' +run_case 'pull_request: a PR touching only packages/*/src/** runs no PM self-test (#22076 pin)' "$REPO" pull_request main '' +expect_rc 0 +expect_warnings '' '' +expect_verdicts slot_lookup query_options_erasure comment_mask_corpus +expect_reason pm_dispatch_gates 'no changed path is in its read-set' +expect_changed 'two package sources' "M packages/a/src/index.ts +M packages/spec/src/index.ts" + +git_q -C "$REPO" checkout -q -B feature-22076-pm "$C0" +printf '# pm, revised on a feature branch\n' > "$REPO/scripts/pm/README.md" +git_q -C "$REPO" commit -q -am 'F5: prose under scripts/pm' +run_case 'pull_request: a PR touching scripts/pm/** runs the PM self-test, prose included (#22076 pin)' "$REPO" pull_request main '' +expect_rc 0 +expect_warnings '' '' +expect_verdicts entry_guard pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason pm_dispatch_gates 'scripts/pm/README.md (M, scripts)' + git_q -C "$REPO" checkout -q -B feature-19753 "$C0" printf 'export const entry = 2;\n' > "$REPO/packages/spec/src/migrations/entries/semantic/17.x.ts" git_q -C "$REPO" commit -q -am 'F3: an entry edited on a feature branch, registry not regenerated' @@ -753,7 +867,7 @@ pin_step migration_registry 'pnpm --filter @objectstack/spec check:migration-reg # ── Verdict ───────────────────────────────────────────────────────────────── # #4690: a battery that ran nothing is a failure, never a pass. -if [ "$cases" -lt 56 ] || [ "$checks" -lt 293 ]; then +if [ "$cases" -lt 68 ] || [ "$checks" -lt 354 ]; then echo "SELFTEST FAILED: only $cases case(s) / $checks check(s) ran -- the battery is short" exit 1 fi diff --git a/scripts/ci/select-gate-families.sh b/scripts/ci/select-gate-families.sh index e6c2f3c27b0..c3fb5c084b1 100644 --- a/scripts/ci/select-gate-families.sh +++ b/scripts/ci/select-gate-families.sh @@ -52,26 +52,45 @@ # the self-test, which reads lint.yml and refuses any other spelling. # # pm_dispatch_gates `pnpm check:pm-dispatch-gates`, which spawns -# `scripts/pm/dispatch-gates.mjs --self-test`. Its -# self-test discovers every workflow file, resolves -# every check:* script through the root and package -# `package.json`s, reads each gate's source for its -# watch hints (so ALL of scripts/** and every -# packages/*/scripts/**), and reads .claude/**, -# skills/** (the frame-sync COPIES table), -# `AGENTS.md`, `CLAUDE.md`. Those are the tool's OWN -# inputs and they are this family's read-set. +# `scripts/pm/dispatch-gates.mjs --self-test`. This +# read-set is MEASURED, not inferred (#22076): one +# full battery run on 3d9188502e under a read hook on +# every fs open and every child process, its node +# children included. Outside the whole-tree censuses +# named below, the files it opens are: every workflow +# file and the composite actions they use; the source +# of every gate its discovery resolves (a CODE file +# under scripts/ or a workspace package's scripts/ -- +# `resolveCheckToFiles` admits no other extension) +# and every scripts/ module those import; the root +# `package.json` and the manifest of each package a +# `--filter` row names; scripts/pm/**; and the +# pm-dispatch rulebook, .claude/skills/pm-dispatch/**. +# Those are the tool's OWN inputs and they are this +# family's read-set. +# It opens NO data or prose file beyond them: not a +# `.json`, `.md` or `.txt` under scripts/ outside +# scripts/pm/, and nothing of skills/**, `AGENTS.md`, +# `CLAUDE.md` or the `.claude/**` prose and JSON +# outside the rulebook. A few of those it only tests +# for EXISTENCE (the frame-sync COPIES table names +# skills/** files, the governed read floor names +# .claude/agents/os-dev.md), and a modification +# cannot move an existence test. # ⛔ The battery reads MORE than that -- the CONTENT -# of every JS/TS and shell (`.sh`) file in the tree -# (the compound-anchor census of -# `function ...SelfTest...(` declarations, the -# exposed-scratch-dir sweep of every -# mkdtempSync/mkdirSync caller and the nested -# .gitignore files it consults) and the tracked NAME -# set (hint reachability, test-file residue), which is -# why an ADDED path anywhere used to run it. Those -# reads are real and they are DELIBERATELY off the PR -# path: see "The self-test families" below. +# of every JS/TS file in the tree twice (the +# compound-anchor census of `function ...SelfTest...(` +# declarations, and the exposed-scratch-dir sweep of +# every mkdtempSync/mkdirSync caller with the +# .gitignore files `git check-ignore` consults), of +# every non-test TypeScript file (the error-code +# literal census), of every tracked `.sh` file (the +# shell-mask census), of a handful of named product +# files it holds as live specimens, and the tracked +# NAME set (hint reachability, test-file residue), +# which is why an ADDED path anywhere used to run it. +# Those reads are real and they are DELIBERATELY off +# the PR path: see "The self-test families" below. # query_options_erasure `pnpm check:query-options-erasure`. Lints # packages/**/*.{ts,tsx,mts,cts} under # `eslint.config.mjs`, reads its baseline @@ -166,6 +185,37 @@ # behind the selector, or making one of these read-sets smaller -- is again a # maintainer call, taken here, under this script's self-test. # +# ## pm_dispatch_gates keys on what the battery OPENS (#22076) +# +# That call was taken once more, for this family alone: the maintainer asked +# for the CI cost tasks, verbatim 「CI 优化按照你的建议创建任务」, and triage +# ruled the direction on the card -- narrow the read-set to the inputs that can +# change the battery's verdicts, keep the backstop, say what coverage moves. +# The measured read-set above found two classes the arm still ran on only +# because their DIRECTORY was in the old read-set, while the battery opens no +# file in them: +# +# - a data or prose file under scripts/ outside scripts/pm/ (`.json`, `.md`, +# `.txt`: a ratchet baseline, a pinned ledger, a fixture log), UNLESS some +# file spells a relative specifier ending in its name. An import a gate +# source makes is the one edge by which the discovery could open such a +# file (it follows `./` and `../` specifiers into scripts/), so a name no +# specifier spells is a file it cannot reach; +# - agent prose and JSON outside the pm-dispatch rulebook: skills/**, +# `AGENTS.md`, `CLAUDE.md`, .claude/agents/**, the .claude/ settings and +# the other skills under .claude/skills/. +# +# For a MODIFICATION that is the ratchet-grade claim, not #19498's weaker one: +# no case reads these files' bytes or anything computed from them, so no case +# can change verdict. What moves from PR time to push-on-main and the hourly +# run is the NAME half -- an ADDED file in these two classes, which the +# tracked-name sweep sees and which, like an added file in every other class +# since #19498, is judged here by its class and not its status. A deletion or +# a rename still runs every family. Everything else the arm ran on, it still +# runs on: scripts/pm/**, every code file under any scripts/, the rulebook, +# every `.claude/**` code file (the hooks, which the shell-mask census reads), +# the workflow tree and every package manifest. +# # ## migration_registry ADDS a gate to the PR path; it scopes nothing away # # The other nine families were steps that ran on every PR before the selector @@ -381,6 +431,26 @@ is_masked_source() { return 1 } +# named_by_relative_specifier -- exit 0 when some file in the HEAD +# tree spells a quoted relative specifier (`./...` or `../...`) whose last +# segment is this path's basename, and ALSO when git grep cannot answer: +# fail-open. It exists for one edge. The dispatch derivation opens a file that +# is not a gate source only by following an import out of one, and it follows +# only a quoted specifier that starts `./` or `../`, so a basename no such +# specifier spells is a file the derivation cannot reach. The search is a +# superset of that edge on purpose -- the whole tree, comments unmasked, a +# reference that is no import at all -- and every extra hit answers "named", +# which runs the family. +named_by_relative_specifier() { + local base pattern rc + base=${1##*/} + pattern=$(printf '%s' "$base" | sed -e 's/[][\.*^$+?(){}|]/\\&/g') + rc=0 + git grep -q -E -e "['\"]\\.\\.?/([^'\"]*/)?${pattern}['\"]" HEAD -- > /dev/null 2>&1 || rc=$? + if [ "$rc" -eq 1 ]; then return 1; fi + return 0 +} + # reads_gate_tree -- exit 0 when this change is inside the # read-set the dispatch derivation shares with the gates built on it: the # workflow tree it discovers (.github/**, workflows and composite actions), @@ -413,17 +483,40 @@ family_reads() { case "$class" in unknown|root-config) return 0 ;; esac case "$id" in pm_dispatch_gates) - # NARROWED to the tool's own inputs (#19498). What it keeps: the - # derivation's own read-set above, plus the agent configuration the - # battery's live cases read (.claude/**, skills/**, AGENTS.md, - # CLAUDE.md). - # ⛔ What it gives up, on the ruling this script's header quotes: the + # NARROWED to the tool's own inputs (#19498), and those to the files the + # battery OPENS (#22076). What it keeps: the derivation's own read-set + # above, scripts/pm/** whole, and the pm-dispatch rulebook its live + # cases read. + # ⛔ What it gives up, on the rulings this script's header quotes: the # whole-tree censuses -- every masked source and every `.sh` file read # for the compound-anchor sweep, the nested .gitignore files the # exposed-scratch-dir sweep consults, and the tracked NAME sweep that # made an ADDED path anywhere run it. Those reads are real; they run on # push-on-main and on the hourly full run, and not on a PR. - case "$class" in agent-config) return 0 ;; esac + case "$path" in + scripts/pm/*|.claude/skills/pm-dispatch/*) return 0 ;; + esac + case "$class" in + agent-config) + # Prose and JSON the battery never opens. A `.claude/**` file of any + # other kind -- a hook's shell, a workflow script -- still runs it. + case "$path" in + *.md|*.json) return 1 ;; + esac + return 0 + ;; + scripts) + # Data and prose under scripts/: no gate resolves to one, and no + # import reaches one unless a relative specifier spells its name. An + # extension not listed here runs it, whatever it is. + case "$path" in + *.json|*.md|*.txt) + if named_by_relative_specifier "$path"; then return 0; fi + return 1 + ;; + esac + ;; + esac if reads_gate_tree "$path" "$class"; then return 0; fi return 1 ;; From 1ed9df228538e861e417eb354c68f2ef2fda5423 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 14:30:43 +0000 Subject: [PATCH 2/2] ci(lint): a test under a package scripts/ that no gate wiring names skips the PM self-test The PM dispatch-gates discovery opens a file under a workspace package's scripts/ only as a gate source, which is a file a check:* command in a manifest or a workflow step spells; it follows no import into a package's scripts/. A test there that no package.json and nothing under .github/ names is therefore read only by the two whole-tree censuses, as a test under src/ is, and it now skips the step on pull_request and merge_group. A basename either place spells still runs it (superset grep, fail-open on any git grep error). Self-test: three more cases, floor 71 cases / 369 checks. Claude-Session: https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw Co-authored-by: Claude --- scripts/ci/select-gate-families.selftest.sh | 43 +++++++++++++++++-- scripts/ci/select-gate-families.sh | 46 +++++++++++++++++++-- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/scripts/ci/select-gate-families.selftest.sh b/scripts/ci/select-gate-families.selftest.sh index f745655d559..7422005c5f0 100644 --- a/scripts/ci/select-gate-families.selftest.sh +++ b/scripts/ci/select-gate-families.selftest.sh @@ -25,8 +25,10 @@ # scripts/ and agent prose outside the pm-dispatch rulebook skip it, while # scripts/pm/** whole, the rulebook, a `.claude/**` shell script, a data file a # relative specifier names and a scripts/ file of an unlisted kind still run -# it; a pull request touching only packages/*/src/** runs no PM self-test, and -# `schedule` and `push` still run it over the very changes the PR path skips. +# it; a test under a package's scripts/ skips it unless a manifest or a +# workflow names it; a pull request touching only packages/*/src/** runs no PM +# self-test, and `schedule` and `push` still run it over the very changes the +# PR path skips. # # Since #19753 it covers migration_registry, the one family that ADDS a gate # to the PR path: an entry, the generated registry, the generator and the two @@ -85,13 +87,17 @@ mkdir -p "$UP/packages/a/scripts" "$UP/packages/a/src" "$UP/apps/site/src" "$UP/ "$UP/packages/spec/src/migrations/entries/semantic" "$UP/packages/spec/scripts" printf '{"name":"fixture","private":true}\n' > "$UP/package.json" printf 'packages:\n - packages/*\n' > "$UP/pnpm-workspace.yaml" -printf '{"name":"a"}\n' > "$UP/packages/a/package.json" +printf '{"name":"a","scripts":{"check:gate":"vitest run scripts/gate.test.ts"}}\n' > "$UP/packages/a/package.json" printf 'export const a = 1;\n' > "$UP/packages/a/src/index.ts" printf 'export const t = 1;\n' > "$UP/packages/a/src/index.test.ts" printf '{"rows":[]}\n' > "$UP/packages/a/src/data.json" printf '#!/usr/bin/env bash\necho foo\n' > "$UP/packages/a/foo.sh" printf 'dist/\n' > "$UP/packages/a/.gitignore" printf 'console.log(1);\n' > "$UP/packages/a/scripts/build.mjs" +# Two tests under a package's scripts/: one a check:* command in its manifest +# names (a gate source), one nothing names (#22076). +printf 'export const g = 1;\n' > "$UP/packages/a/scripts/gate.test.ts" +printf 'export const b = 1;\n' > "$UP/packages/a/scripts/build.test.ts" printf 'export const site = 1;\n' > "$UP/apps/site/src/page.tsx" printf '# guide\n' > "$UP/docs/guide.md" printf '# page\n' > "$UP/content/docs/page.mdx" @@ -128,6 +134,7 @@ printf 'export const registry = [];\n' > "$UP/packages/spec/src/migrations/regis printf 'export const entry = 1;\n' > "$UP/packages/spec/src/migrations/entries/semantic/17.x.ts" printf 'export const gen = 1;\n' > "$UP/packages/spec/scripts/build-migration-registry.ts" printf 'export const schemas = 1;\n' > "$UP/packages/spec/scripts/build-schemas.ts" +printf 'export const merge = 1;\n' > "$UP/packages/spec/scripts/conversions-merge.test.ts" git_q -C "$UP" add -A git_q -C "$UP" commit -q -m 'C0: root' C0=$(git_q -C "$UP" rev-parse HEAD) @@ -544,6 +551,20 @@ run_case 'merge_group: a package-local script is a gate source (the derivation f expect_rc 0 expect_verdicts comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands +S=$(scenario M:packages/a/scripts/build.test.ts) +run_case 'merge_group: a test under a package scripts/ that no manifest or workflow names is product test code to the PM self-test, which skips (#22076)' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_warnings '' '' +expect_verdicts slot_lookup query_options_erasure comment_mask_corpus declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason pm_dispatch_gates 'no changed path is in its read-set' + +S=$(scenario M:packages/a/scripts/gate.test.ts) +run_case 'merge_group: a test under a package scripts/ that a check script names is a gate source and still runs the PM self-test' "$REPO" merge_group '' "$C0" +expect_rc 0 +expect_warnings '' '' +expect_verdicts slot_lookup query_options_erasure comment_mask_corpus pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason pm_dispatch_gates 'packages/a/scripts/gate.test.ts (M, workspace)' + S=$(scenario M:scripts/pm/os-verify-lock.sh) run_case 'merge_group: the lock script runs its own self-test and every family that reads scripts/' "$REPO" merge_group '' "$C0" expect_rc 0 @@ -763,6 +784,20 @@ expect_warnings '' '' expect_verdicts entry_guard pm_dispatch_gates declared_population_live bare_root_worklist self_test_workflow_commands expect_reason pm_dispatch_gates 'scripts/pm/README.md (M, scripts)' +# The shape the card measured paying the step: a spec source, tests under +# packages/spec/scripts/ that nothing wires as a gate, and a changeset. +git_q -C "$REPO" checkout -q -B feature-22076-spec-tests "$C0" +printf 'export const merge = 2;\n' > "$REPO/packages/spec/scripts/conversions-merge.test.ts" +printf 'export const index = 3;\n' > "$REPO/packages/spec/src/index.ts" +printf -- '---\n"spec": patch\n---\nthe shared entry\n' > "$REPO/.changeset/shared-entry.md" +git_q -C "$REPO" add -A +git_q -C "$REPO" commit -q -m 'F6: a spec source, an unwired test under packages/spec/scripts, a changeset' +run_case 'pull_request: a spec source, an unwired test under packages/spec/scripts and a changeset run no PM self-test (#22076)' "$REPO" pull_request main '' +expect_rc 0 +expect_warnings '' '' +expect_verdicts slot_lookup query_options_erasure comment_mask_corpus declared_population_live bare_root_worklist self_test_workflow_commands +expect_reason pm_dispatch_gates 'no changed path is in its read-set' + git_q -C "$REPO" checkout -q -B feature-19753 "$C0" printf 'export const entry = 2;\n' > "$REPO/packages/spec/src/migrations/entries/semantic/17.x.ts" git_q -C "$REPO" commit -q -am 'F3: an entry edited on a feature branch, registry not regenerated' @@ -867,7 +902,7 @@ pin_step migration_registry 'pnpm --filter @objectstack/spec check:migration-reg # ── Verdict ───────────────────────────────────────────────────────────────── # #4690: a battery that ran nothing is a failure, never a pass. -if [ "$cases" -lt 68 ] || [ "$checks" -lt 354 ]; then +if [ "$cases" -lt 71 ] || [ "$checks" -lt 369 ]; then echo "SELFTEST FAILED: only $cases case(s) / $checks check(s) ran -- the battery is short" exit 1 fi diff --git a/scripts/ci/select-gate-families.sh b/scripts/ci/select-gate-families.sh index c3fb5c084b1..76f368b76de 100644 --- a/scripts/ci/select-gate-families.sh +++ b/scripts/ci/select-gate-families.sh @@ -210,9 +210,20 @@ # can change verdict. What moves from PR time to push-on-main and the hourly # run is the NAME half -- an ADDED file in these two classes, which the # tracked-name sweep sees and which, like an added file in every other class -# since #19498, is judged here by its class and not its status. A deletion or -# a rename still runs every family. Everything else the arm ran on, it still -# runs on: scripts/pm/**, every code file under any scripts/, the rulebook, +# since #19498, is judged here by its class and not its status. +# +# A third class takes #19498's weaker claim, and only that one: a TEST file +# under a workspace package's scripts/ that no package manifest and nothing +# under .github/ names. The discovery opens a package's scripts/ file only as +# a gate source, and a gate source is a file a check:* command or a workflow +# step spells; such a test is spelled by neither, so the one battery reader +# left is the pair of whole-tree censuses -- which read it exactly as they +# read a test under src/, a class that has skipped this family since #19498. +# That census coverage is what moves for it. +# +# A deletion or a rename still runs every family. Everything else the arm ran +# on, it still runs on: scripts/pm/**, every code file under the root +# scripts/, every non-test file under a package's scripts/, the rulebook, # every `.claude/**` code file (the hooks, which the shell-mask census reads), # the workflow tree and every package manifest. # @@ -451,6 +462,23 @@ named_by_relative_specifier() { return 0 } +# named_in_gate_wiring -- exit 0 when this path's basename is spelled +# anywhere in a package manifest or anywhere under .github/ in the HEAD tree, +# and ALSO when git grep cannot answer: fail-open. A file under a workspace +# package's scripts/ is a gate source only when a check:* command in a +# manifest or a workflow step names it (`resolveCheckToFiles` reads the +# command text; the discovery follows no import into a package's scripts/), +# so a basename spelled in neither place names no gate source. A comment, a +# lookalike name, a step that is no gate: every extra hit answers "named". +named_in_gate_wiring() { + local base rc + base=${1##*/} + rc=0 + git grep -q -F -e "$base" HEAD -- '*package.json' '.github/' > /dev/null 2>&1 || rc=$? + if [ "$rc" -eq 1 ]; then return 1; fi + return 0 +} + # reads_gate_tree -- exit 0 when this change is inside the # read-set the dispatch derivation shares with the gates built on it: the # workflow tree it discovers (.github/**, workflows and composite actions), @@ -516,6 +544,18 @@ family_reads() { ;; esac ;; + workspace) + # A test under a package's scripts/ is product test code to this + # battery -- the two whole-tree censuses read it as they read every + # source file -- unless gate wiring names it. A package manifest and + # every other file under a scripts/ keep running it below. + case "$path" in + */scripts/*.test.ts|*/scripts/*.test.tsx|*/scripts/*.test.mts|*/scripts/*.test.cts|*/scripts/*.test.js|*/scripts/*.test.mjs|*/scripts/*.test.cjs) + if named_in_gate_wiring "$path"; then return 0; fi + return 1 + ;; + esac + ;; esac if reads_gate_tree "$path" "$class"; then return 0; fi return 1