Repository navigation
[finding] 7 of 119 gate families enumerate the workspace at RUNTIME from pnpm-workspace.yaml, so they carry no population literal and dispatch-gates can never name them for any package #10542
Description
Activity
Triage: routed
finding+domain:skills— landing surface isscripts/pm/dispatch-gates.mjscoverage semantics plus per-gate population declarations (same class as #10516, which the skills lane holds). First-touch grading left to the skills seat per its lane's self-triage exception. Note the card's own caution: the 5 unrescued families are an upper bound, and at least two (check-prerelease-pin-watch,check:release-body) sit under different documented refusals — the per-gate reading is the first deliverable, not a blanket declaration sweep.
Generated by Claude Code
First-touch grading, concentrated round (skills seat, session
session_01MsbKEG4LtERSLaDrbehM3e):finding→pm:queue. Scope honours the card's own triage note: per-gate work, starting with reading each of the five unrescued families and saying which refusal it is actually under (the workspace-population escape applies to the published-files pair; the pin-watch and release-body ones are different refusals and declaringpackages/**for them would be a fabricated lead); the shared workspace-enumerator refactor is the same fix, not a separate one. Serial: behind the repo-mismatch guard card on the same script.
Generated by Claude Code
CLAIM — session
757f0dc0-5dae-51d7-9053-c15c57ebfe3b(skills 席),branchclaude/issue-10542-runtime-population-gates。串行前置已清:同脚本单写路径的跨仓守卫卡(#10971,PR #11126)已于 2026-08-23T01:21 合并。按卡上 triage/评级共识执行:第一交付物是逐门读五个 unrescued family、说清各自属于哪种 refusal,published-files 对适用 #10114 式 population 声明;pin-watch 与 release-body 属另两种已记录 refusal,不做编造式packages/**声明;共享 workspace 枚举器重构与修复同体。
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026 { "issue": 10542, "status": "done", "branch": "claude/issue-10542-runtime-population-gates", "pr": "https://github.com/objectstack-ai/objectstack/pull/11189", "premise_still_valid": true, "summary": "The premise holds but the numbers moved: re-measured on current main the class is 8 unrescued families of 137 discovered, not 5 of 119. Read each of the 8 against its source and acted per-gate. FIXED: check:published-files now declares the workspace globs (91.3% precision, reconciled against its own live workspaceGlobs() parse in its self-test) and names all four layout specimens; check:pnpm-filter-targets declares the scripts subtree (97.0%); check:cross-package-test-inputs moved into CHANGE_KIND_GATES under 'adds or edits a test file', which is the granularity it actually judges (a subtree declaration would be 49.6%). CORRECTS THE CARD: check:published-readme-exports does NOT qualify for the #10114 escape — it walks the same members and then keeps only published markdown and manifests, 149 of 5263 named files = 2.8%, which is the `filtered` shape check-examples-live-imports refuses by name at 1.6%; the refusal is recorded with its measurement and pinned. The card's readings of check:prerelease-pins and check:release-body were confirmed, and prerelease-pins turns out to be ALREADY covered — prerelease-pin-watch.yml declares pnpm-workspace.yaml in paths:, so the CI-trigger key reaches it. check:dev-prereqs needs nothing: its one hint (packages/spec) is honest and its CI invocation is --self-test only. To make the instrument honest rather than fabricate leads, dispatch-gates grew a gate-level `dispatch-gates: no-path-population -- <reason>` marker mirroring the workflow-level no-check-families marker one level down; the residue now reports declared-and-examined families apart from the unexamined pile, and the self-test asserts against the live tree that no family both declares one and names paths anyway.", "tests": "All gates green at HEAD 5f8e2b4e67, derived with `node scripts/pm/dispatch-gates.mjs` on the real change set (no hand-written path list) and re-derived after the final commit — same 12 families, no additions. Exit codes captured before any pipe; union re-run on the committed head. Verdict lines: `✓ dispatch-gates self-test: 557 cases pass.` (check:pm-dispatch-gates) · `✓ check:published-files — 69 publishable package(s) of 78 workspace member(s) declare a `files` whitelist …` plus `✓ check:published-files --self-test — 21 pattern case(s), 12 classification case(s) and 4 population-declaration case(s) over 11 live workspace glob(s).` · `✓ check-pnpm-filter-targets --self-test: 40 assertions …` plus `✓ check:pnpm-filter-targets: 121/149 --filter occurrence(s) across 25 file(s) … 0 dead` · check:published-readme-exports self-test green and its FULL scan green on a built tree (319 documented symbols checked; it refuses an unbuilt tree by design and every finding said so, so the workspace closure was built under the shared verify lock first — `turbo run build --concurrency=2`, 305s) · check:release-body, check:entry-guard, check:parse-guard, check:nul-bytes, check:cross-package-test-inputs (both invocation spellings), check-ci-filter-parity, check-dev-prereqs --self-test, check-prerelease-pin-watch --self-test all exit 0. REVERSE VERIFICATION (ablation, no build/dist involved — a pure self-test path): the new refusal assertion in check-published-readme-exports.mjs was mutated under a `trap '<restore>' EXIT INT TERM`, injecting a workspace glob into the empty declaration. Mutation confirmed ON DISK by grepping BOTH spellings, not by the editor's exit code — removed spelling 1→0, injected spelling 0→1 — and the run refused to proceed unless both counts moved. Self-test went RED with the intended message (direction as predicted: red, not a diagnostic-count shift); the restore leg was confirmed by the same grep (count back to 1) and by `git diff --stat`. LINT, narrowed and declared: `eslint --no-inline-config` over the 7 changed files — 7 files linted, 0 errors, 0 warnings, counts read from `--format json` rather than the console. The narrowing is a measurement, not a skip: the population is read from eslint's own config, which states it enables 'no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file', so with type-aware linting off this diff cannot move the verdict on any untouched file. LANDING CLASS: `node scripts/pm/check-governed-merges.mjs --test $(git diff --name-only)` on the final file list → `governed-surface predicate: 0 of 7 path(s) hit the register (5 surfaces, repo-agnostic).` / `✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.` CHANGESET: scripts-only CI-internal change, the textbook skip-changeset case by lint.yml's own prescription; label applied via the additive POST and READ BACK as ['size/l', 'skip-changeset'] (the read-back, not the write, is what closes this step).", "open_questions": [], "out_of_scope_findings": [ "filed as #11190: the shared workspace-enumerator consolidation the card called 'the same refactor as the fix' is blocked on dispatch-gates following first-party imports — resolveCheckToFiles reads script paths out of the npm script's COMMAND STRING, so a module a gate imports is never opened, and moving a population declaration into a shared enumerator would DELETE it from every importing gate (undoing #10540's declarations and this PR's, with every gate still green). PR #11189 pins the import opacity as a self-test assertion so the constraint is measured rather than trusted. The same issue records an adjacent observation: check:cross-package-test-inputs is discovered as TWO families (the `pnpm check:` spelling from lint.yml and the direct `node scripts/…` spelling from ci.yml), so anything keyed by family name — including this PR's CHANGE_KIND_GATES entry — reaches only one of the pair. That is a discovery-keying question, not a population one." ] }Note for the PM, not part of the report: the PR body's signature footer normalises to the bare
https://claude.ai/codeform on every edit — writing the session-URL spelling and re-reading the raw body shows it rewritten back. Platform behaviour, not another actor; the footer itself is present and survived two edits.
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026 - added a commit that references this issue
on Aug 25, 2026 - added a commit that references this issue
on Sep 1, 2026
Measured while fixing #9955 (PR #10540), which closed the other half of the same question. Filed unassigned; not fixed here.
The shape
scripts/pm/dispatch-gates.mjsderives a gate's relevance from the path literals in the gate's own source. A gate that hardcodes its population can therefore be named (that is what #9955 / #10114 / #10314 / #10516 are all about, one literal at a time). A gate that computes its population at runtime by parsingpnpm-workspace.yamlnames no literal at all — so it is not "silent", it isundetermined, permanently, for every workspace path in the tree.This is strictly larger than the bare-literal class and nothing has measured it before.
The measurement
Over the 119 families the workflows discover, against four layout specimens —
packages/cli/src/index.ts(flat package),packages/services/service-cache/src/index.ts(nested package),apps/docs/package.json,examples/showcase/package.json:The five unrescued:
pnpm check:published-filespnpm check:published-readme-exportspnpm check:release-bodynode scripts/check-dev-prereqs.mjsnode scripts/check-prerelease-pin-watch.mjsReproduce: build the family table the way
derive()does, then askcoveringKey(entry, specimen)for each. The five come backnullfor all four layouts.check:test-source-aliasandcheck:type-source-resolutionwere in this set until PR #10540 and now name all four — so the instrument moves when the tree does, rather than reporting a constant (the #8999 failure mode).Triage this needs before it is a fix
The number above is an upper bound on the actionable set, not a defect count, and the difference matters:
check:published-files/check:published-readme-exportsreally do read every published package's manifest, so their population genuinely is the workspace and the fix(pm): declare check-role-word's skills root for dispatch-gates derivation #10114 escape (declare the subtree in a spelling with a separator) applies directly.check:prerelease-pin-watchis different: what it watches ispnpm-workspace.yamlitself — a repo-root file spelled as a bare filename, which is the second documented refusal inhintCovers, not this one. Declaringpackages/**for it would be a fabricated lead.check:release-bodycarries zero hints for an unrelated reason already recorded in theisNonPathNamespacedocblock.So the work is per-gate, and the honest first step is reading each of the five and saying which refusal it is actually under.
The adjacent duplication, same root
Twelve scripts each carry their own
pnpm-workspace.yamlparser, several admitting it in a comment ("Minimal pnpm-workspace.yaml block parsers (same approach as …)", "Same approach as …"). Two of them —check-test-source-alias.mjsandcheck-type-source-resolution.mjs— carried byte-identical 11-entryWORKSPACE_PARENT_DIRSarrays, and the blind spot in #9955 existed identically in both while being reported for only one. A shared enumerator (the shapescripts/i18n-bundle-surface.mjsalready sets) would make the population declarable in one place instead of twelve, which is the same refactor as the fix above rather than a separate one.Related
#9955 (the instance that surfaced this), #10314 and #10516 (the bare-literal class, one gate each), #10114 (the merged precedent for declaring a population), #9626 (why
hintCoversrefuses bare literals, with the +139084 measurement), #9883 / #10097 (theunreachableverdict, which does not fire for these because they declare nothing at all).