Skip to content

[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

@os-zhuang

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.mjs derives 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 parsing pnpm-workspace.yaml names no literal at all — so it is not "silent", it is undetermined, 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:

families whose gate script reads pnpm-workspace.yaml at runtime: 12
  ... naming NONE of the four specimens:                          7
  ... of those 7, rescued by CHANGE_KIND_GATES:                   2   (check:type-check-coverage, check:type-check-debt)
  ... UNRESCUED:                                                  5

The five unrescued:

family hints it does carry
pnpm check:published-files 4, none covering a workspace path
pnpm check:published-readme-exports 2, likewise
pnpm check:release-body 0
node scripts/check-dev-prereqs.mjs 1
node scripts/check-prerelease-pin-watch.mjs 0

Reproduce: build the family table the way derive() does, then ask coveringKey(entry, specimen) for each. The five come back null for all four layouts. check:test-source-alias and check:type-source-resolution were 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-exports really 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-watch is different: what it watches is pnpm-workspace.yaml itself — a repo-root file spelled as a bare filename, which is the second documented refusal in hintCovers, not this one. Declaring packages/** for it would be a fabricated lead.
  • check:release-body carries zero hints for an unrelated reason already recorded in the isNonPathNamespace docblock.

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.yaml parser, 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.mjs and check-type-source-resolution.mjs — carried byte-identical 11-entry WORKSPACE_PARENT_DIRS arrays, and the blind spot in #9955 existed identically in both while being reported for only one. A shared enumerator (the shape scripts/i18n-bundle-surface.mjs already 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 hintCovers refuses bare literals, with the +139084 measurement), #9883 / #10097 (the unreachable verdict, which does not fire for these because they declare nothing at all).

Activity

  1. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    Triage: routed finding + domain:skills — landing surface is scripts/pm/dispatch-gates.mjs coverage 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

  2. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    ContributorAuthor

    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 declaring packages/** 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

  3. self-assigned this
    on Aug 23, 2026
  4. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    CLAIM — session 757f0dc0-5dae-51d7-9053-c15c57ebfe3b(skills 席),branch claude/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

  5. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor
    {
      "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/code form 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

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions