Skip to content

fix(devx): refuse a pnpm --filter that matches nothing, instead of exiting 0 - #10903

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-10853-filter-zero-match-guard
Aug 22, 2026
Merged

fix(devx): refuse a pnpm --filter that matches nothing, instead of exiting 0#10903
os-zhuang merged 2 commits into
mainfrom
claude/issue-10853-filter-zero-match-guard

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#10853

pnpm --filter <name>exits 0 when the filter matches no project. Re-measured on this branch:

$ pnpm --filter @objectstack/definitely-not-a-package test; echo $?
No projects matched the filters in "/home/user/objectstack-issue-10853"
0

The typo is not the defect; the exit 0 is. Every discipline this repo uses to make a run trustworthy is defeated by it — cmd > log 2>&1; ec=$? captures the 0 faithfully, and a report saying "suite green, exit 0" is then true and worthless because nothing ran. Same family as the ---before-vitest-args trap: exit 0, nothing measured, output that reads like success.

⚠️ Which population each half protects, and what is still uncovered

The card is explicit that its two directions are not substitutes. Both are implemented here, and neither is presented as covering the other.

protectsdoes not protect
os-verify-lock.sh filter preflightevery ad-hoc command routed through the shared verify lock — the entry point every heavy verify in the container is contracted to usean ad-hoc command that does not go through the lock: a bare pnpm --filter … typed at a prompt is still exit 0, still silent
check:pnpm-filter-targetsthe committed population: package.json scripts, .github/workflows/**, scripts/**anything typed ad hoc — which is the population that actually bit

Left uncovered, plainly: a pnpm --filter <typo> typed directly into a shell, outside the lock wrapper, behaves exactly as it does today. Nothing here changes pnpm's exit code. ① narrows that population to "commands that skip the mandated wrapper" rather than eliminating it, and ② cannot see it at all. Direction ③ from the card (the dispatch template's example) is out of scope.claude/skills/** is governed, human-merge-only, and the PM's triage comment already records that no committed template teaches the bad name.

The card's "no guard exists" finding — re-verified, not inherited

Both greps re-run on this tree:

  • targetedgit grep -n "No projects matched the filters" -- scripts .github/workflows package.jsonzero hits (exit 1). The card's precise finding holds.
  • loosegit grep -nE "filter.*exists|verify.*filter" -- scripts .github/workflows8 files now (the card recorded 4). Every hit is Array.prototype.filter next to existsSync, or this wrapper's own usage examples. None guards a zero match. The loose grep is still the wrong instrument; the targeted one still settles it.

scripts/check-ci-filter-parity.mjs was checked by name because it sounds adjacent — it is about ci.yml's dorny path filters, not pnpm package selectors, and does not overlap.

The matching rule was measured, not assumed

--filter resolution is one shared module (scripts/pnpm-filter-targets.mjs) so the two consumers cannot answer the same question differently. pnpm matches the package name, full or scope-stripped, and not the directory — all measured with pnpm --filter <sel> ls --depth -1 --json on pnpm 10.31.0:

selectormatchesnote
@objectstack/hono1
hono1pnpm strips the scope
example-todo1the unscoped name
app-todo0the directoryexamples/app-todo — not a match
@objectstack/adapter-hono0the card's guessed-from-the-path name

The app-todo row is the one that makes the rule non-obvious: a matcher written from the directory intuition would report live selectors as dead. Fidelity check: all 49 judgeable selectors in a corpus built from every committed spelling were resolved against real pnpm and compared with the local matcher — 49/49 agree, 0 divergence.

A selector is judged only when the answer cannot be argued with (a plain name). Globs, path selectors, since-ref selectors and interpolations are refused a verdict by design — deciding globs would need a picomatch-compatible matcher, i.e. a second recogniser, the trade check-ci-filter-parity.mjs already rejected. The unjudged count is printed, so an unjudged population is visible rather than silent.

Ablations — predictions written down before the runs

None of these subjects is consumed through a package exports map: all three are read from disk by node scripts/… / bash scripts/… on every invocation, so there is no dist/ between the edit and the run. Each mutation was still proved on disk by marker counts, never by an editing tool's exit code.

A — the ⭐ direction, both ways, no mutation. Predicted: reds on the card's literal reproduction, silent on the same command with the real name.

$ os-verify-lock.sh -c 'pnpm --filter @objectstack/adapter-hono test --maxWorkers=2' → exit 2
✗ this command would measure NOTHING:
--filter @objectstack/adapter-hono matches NO project in this workspace.
did you mean `@objectstack/hono`?
VERDICT filter-matches-nothing (exit 2) · never acquired · refused before waiting · nothing was built or tested
$ os-verify-lock.sh -c 'pnpm --filter @objectstack/hono ls --depth -1' → exit 0
VERDICT command-exit 0 · held the lock 1s · waited 0s

B — mutation: delete the guard call from mode_run. Marker count filter_preflight 2 → 1 (call gone, definition kept). Predicted direction: red → green (not "more diagnostics", not reversed).

$ os-verify-lock.sh -c 'pnpm --filter @objectstack/adapter-hono test --maxWorkers=2' → exit 0
ACQUIRED after 0s — running: pnpm --filter @objectstack/adapter-hono test --maxWorkers=2
No projects matched the filters in "/home/user/objectstack-issue-10853"
VERDICT command-exit 0 · held the lock 1s · waited 0s

The pre-fix world exactly: exit 0, nothing measured — and it spent the fleet's shared lock to do it. Restore leg: marker count back to 2, guard reds again, git diff --quiet scripts/pm/os-verify-lock.sh0 (byte-identical).

C — mutation: plant a dead filter in the real lint.yml. Marker count adapter-hono in lint.yml 0 → 1. Predicted: gate exits 1, naming file:line and the suggestion.

✗ check:pnpm-filter-targets — 1 filter(s) name no package in this workspace
• .github/workflows/lint.yml:2006: `--filter @objectstack/adapter-hono` names no package in this
workspace. Did you mean `@objectstack/hono`? pnpm EXITS 0 on a filter that matched nothing, so
this command reports success while measuring nothing (#10853) — a red here is a run that was
never happening.

Restore leg: marker count back to 0, gate green, git diff --quiet .github/workflows/lint.yml0 (byte-identical).

Two false positives the gate found in its own first run — both fixed, both pinned

Recorded because they are the reason the discrimination exists, not incidental:

  1. git fetch --unshallow --filter=blob:none — git's partial-clone filter, reported as a dead package. A --filter now has to belong to a pnpm/turbo command word, cut at the last command separator so pnpm build && git fetch --filter=… does not inherit the pnpm.
  2. This gate's own step name:- name: Every committed pnpm --filter names a real package parses as --filter names. A YAML step name is a label, not a command, the same distinction [finding] check-required-contexts' self-test proves its own WIRING with text a comment can supply — two presence assertions that go green on prose #10877 had to make. Both are pinned in --self-test with the live text as the fixture.

Self-tests have real negative controls

  • pnpm-filter-targets.mjs --self-test54 assertions over the 77 real workspace packages; the measured pnpm rule pinned row by row; the preflight observed both refusing and silent.
  • check-pnpm-filter-targets.mjs --self-test36 assertions; a dead filter observed red in all four carriers (workflow, package.json, shell, JS) and the same fixtures observed silent with a real name.
  • os-verify-lock.sh --self-test72 cases, 16 new, including: refused command never ran, exit 2 and not 99 (99 means "never got a turn" and invites a retry that would only reproduce the silent green), a real name sails through and its command actually runs, undecidable selectors proceed, and the escape hatch works.

Three #4690 vacuity refusals: zero workspace packages, zero --filter occurrences found, and zero occurrences judged each fail loudly rather than reading as a clean tree.

Scope, exemptions and registration

  • Foreign workspaces are declared, not silently skipped.@object-ui/ is in FOREIGN_SCOPES with its reason: scripts/build-console.sh and scripts/gen-sdui-manifest.sh run those filters inside a checkout of objectstack-ai/objectui, never against this workspace. A new foreign scope must be added deliberately.
  • Self-exemption is an exact, pinned list of three files whose non-comment filter spellings are negative-control fixtures. A fourth entry fails an assertion, so it cannot grow into a mute button.
  • The preflight is fail-open in every direction. No node, no resolver on disk, a resolver that errors, an undecidable selector, or a command that changes directory — all proceed. The only refusal is a plain name this workspace does not have. It reads pnpm-workspace.yaml and the manifests it names: no network, no pnpm invocation, no lockfile, so it cannot become the unbounded wait that file exists to prevent. Escape hatch: OS_VERIFY_LOCK_NO_FILTER_CHECK=1.
  • Gate registration. A check:pnpm-filter-targets key in root package.json. Read off Migrate the release toolchain to @changesets/cli v3 — one atomic PR carrying the bump, the pre-mode restructure, and the gates that model v2's semantics #9465 directly: that lane's fence names root package.json (the @changesets/cli range and the version script) — a scoping parenthetical, so a new check:* key is allowed. (lint.yml carries fifteen comments asserting a wider claim; [finding] lint.yml states the #9465 fence as covering root package.json — fifteen times, and the over-broad reading is acquirable by copying #10894 tracks those, and none of that phrasing is copied here.)
  • lint.yml step placement. Added at the end of the lint job (~:1971), deliberately away from :184 and :561. fix(ci): collect every self-test verdict in a step, instead of masking after the first failure #10887merged while this was in progress; origin/main is merged in and re-verified on the merged head — the new check-step-collectors.mjs is green on this branch, and it does not flag the new step (one command, no --self-test in the block).

Verification

Gate union derived with node scripts/pm/dispatch-gates.mjs (no paths passed — it reads the changeset from the merge base itself), re-run on the final commit 63ffc88712: 17/17 green, plus os-verify-lock.sh --self-test (72 cases).

One declared narrowing:check:type-check-debt was not completed. Its --re-measure half refuses to run without a fully built workspace closure (a deliberate #6376 refusal — measuring without it would silently measure a different world). Its --self-test half and check:type-check-coverage both pass. This diff adds no TypeScript to any package's tsc program — two .mjs scripts, a bash script, a workflow step and a package.json script key — so it cannot move a DEBT or TEST_DEBT number. CI runs it with the closure built.

No changeset: nothing published changes (scripts/**, .github/workflows/**, root package.json scripts), so this carries skip-changeset.


Generated by Claude Code

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026
@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file labels Aug 21, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 22, 2026 03:57
@os-zhuang
os-zhuang added this pull request to the merge queueAug 22, 2026
Merged via the queue into main with commit 13e04d6Aug 22, 2026
35 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10853-filter-zero-match-guard branch August 22, 2026 04:27
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32551036907 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 4.77s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 79 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

ci/cddependenciesPull requests that update a dependency filesize/xlskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] pnpm --filter <typo> exits 0 with "No projects matched the filters" — a whole verification round can be fictitious and read as green

2 participants

@os-zhuang@claude