Skip to content

ci(lint): add a .sh arm to select-gate-families's pm_dispatch_gates read-set - #16986

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-16769-gate-selector-sh-arm
Sep 9, 2026
Merged

ci(lint): add a .sh arm to select-gate-families's pm_dispatch_gates read-set#16986
baozhoutao merged 1 commit into
mainfrom
claude/issue-16769-gate-selector-sh-arm

Conversation

@baozhoutao

@baozhoutao baozhoutao commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Part of #16769.

Half 1 only — half 2 stays open, the card stays queued

This PR lands only the first of the card's two halves: the one-line *.sh arm
in pm_dispatch_gates's read-set, plus a self-test case that drives the real
window. It closes nothing. Half 2 — pinning the selector's key sets to the
gates' live read-sets so this class of drift cannot recur — is a design
question the card explicitly declines to prescribe a shape for, and stays
unaddressed here. objectstack-ai/objectstack#16769 remains open and queued
after this merges.

What changed

family_reads's pm_dispatch_gates branch classified a modified (M) shell
script that is not masked source, not a .gitignore, and sits directly under
packages/** / apps/** / examples/** (not inside a */scripts/*
subdirectory) into the workspace class, whose arm returns 1 (skip) for
anything but */package.json or */scripts/*. But scripts/pm/dispatch-gates.mjs's
self-test reads the CONTENT of every tracked .sh file (a live census
asserting the shell-comment mask never adds a watch hint), so such a path
should always run pm_dispatch_gates.

Added case "$path" in *.sh) return 0 ;; esac beside the existing
*/.gitignore arm, before the class switch — so any .sh file runs the
family whatever class it sits in, matching how masked source and
.gitignore are already handled. Updated both comments describing this
family's read-set in the same voice.

This is superset-only by construction: it can only make pm_dispatch_gates
run more often on a path that previously skipped, never less.

Self-test case, and the before/after ablation the card requires

Added packages/a/foo.sh (workspace class, not under */scripts/*) to the
self-test's fixture tree, and a case modifying it at status M:

S=$(scenario M:packages/a/foo.sh)
run_case 'merge_group: a workspace shell script outside scripts/ is still gate source, not a skipped non-source workspace file (#16769)' "$REPO" merge_group '' "$C0"
expect_rc 0
expect_warnings '' ''
expect_verdicts pm_dispatch_gates
expect_reason pm_dispatch_gates packages/a/foo.sh

Run against the unmodified script (the .sh arm reverted, self-test
otherwise identical) — the new case fails, demonstrating the real hole:

FAIL  pm_dispatch_gates reason mentions: packages/a/foo.sh
      got: pm_dispatch_gates skip no changed path is in its read-set
      -- output --
      | Gate families: 0 run, 5 skipped  (event: merge_group; changed paths: 1)
      |   skip  pm_dispatch_gates      no changed path is in its read-set
      |   skip  query_options_erasure  no changed path is in its read-set
      ...
SELFTEST FAILED (40 cases, 192 checks)

Run against the fixed script — the case passes and the full battery is
green:

all 40 cases passed (192 checks)

Negative control (mandatory): the pre-existing case at
M:packages/a/src/data.json ("a non-source, non-manifest workspace file
skips every family") still asserts expect_verdicts (empty — all skip) and
passes post-fix, confirming the new arm did not escape its intended scope
into an early, over-broad return 0. The pre-existing merge_group docs-only
case (docs/guide.md, status M) likewise still asserts expect_verdicts
empty (all skip) post-fix.

Re-check commands, re-derived before and after (on origin/main 3030369e94)

grep -n 'gitignore' scripts/ci/select-gate-families.sh
grep -c 'shellGrew' scripts/pm/dispatch-gates.mjs
git ls-files '*.sh' | wc -l
command before (this branch's base) after (this PR)
.gitignore arm line :320 case "$path" in */.gitignore) return 0 ;; esac :323 case "$path" in */.gitignore) return 0 ;; esac (the new *.sh arm now sits at :322, immediately above it)
shellGrew in dispatch-gates.mjs 4 4 (unchanged — out of scope, read-only)
tracked .sh files 31 31 (unchanged by this PR)

The card said 27 at filing, triage said 29 sixteen hours later; this run
re-derives 31. That drift is the card's own argument for half 2 — three
data points now, not two. Control: all 31 tracked .sh files still live
under scripts/** or .claude/hooks/** (classes where pm_dispatch_gates
already ran before this fix); files matching
^(packages|apps|examples|docs|content)/ and not under */scripts/*0
— the hole was real but its firing window was, and remains, shut by where
people happen to put shell scripts, not by any rule. This PR closes that
rule-level gap for the one class this half addresses.

Local checks run (this file surface has no package.json; not a package)

  • pnpm check:select-gate-families (= this file's own self-test): 40/40
    cases, 192/192 checks — before-fix red demonstrated above, after-fix green.
  • pnpm check:nul-bytes: OK (8403 tracked text files, 0 raw control bytes).
  • pnpm check:scripts-symbol-anchors: OK (3041 anchors, 234 scripts).
  • node scripts/check-self-test-wired.mjs: OK.
  • node scripts/check-self-test-workflow-commands.mjs: OK.
  • node scripts/check-comment-mask-corpus.mjs: OK (6389 files, 0 disagree).
  • pnpm check:pm-dispatch-gates (scripts/pm/dispatch-gates.mjs's own
    self-test, run detached per that gate's own header instruction since it
    exceeds a practical foreground budget): PASS — dispatch-gates self-test: 1561 cases pass (~530s wall time, this run).

No changeset: nothing in packages/**'s published files[] references
scripts/ci/**; this is unpublished, repo-root CI tooling under a private
package, so no user-visible surface moved.


Generated by Claude Code

… read-set

The dispatch-gates self-test grew a live census that reads the CONTENT
of every tracked `.sh` file (comment-masked watch-hint extraction), but
the selector's pm_dispatch_gates read-set had no `*.sh` rule. A
modified shell script under packages/**, apps/**, examples/**, docs/**
or content/** that does not sit in a scripts/ subdirectory classified
to a class whose arm skips the gate, silently disarming a required
merge check for the one path shape `*/scripts/*` does not already
cover.

Add `case "$path" in *.sh) return 0 ;; esac` beside the existing
`.gitignore` arm, before the class switch, so any `.sh` file runs the
family whatever class it sits in -- a strict superset of what already
ran, so it can only widen coverage, never narrow it.

Add a self-test case that drives the window directly: a
`packages/a/foo.sh` modified at status M now selects pm_dispatch_gates
(asserted red against the unmodified script, green after), with a
negative control confirming a docs-class file at status M still
skips every family.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
@github-actions github-actions Bot added the size/s label Sep 8, 2026
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 8, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 8, 2026 23:53
@baozhoutao
baozhoutao enabled auto-merge September 8, 2026 23:53
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit bb7d91f Sep 9, 2026
45 of 48 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-16769-gate-selector-sh-arm branch September 9, 2026 00:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants