Skip to content

[finding] dispatch-gates.mjs is a followable non-gate module, so its join bases and tier globs are inheritable — 2553 fabricated pairs for the next gate that imports it #11556

Description

@os-steve

Measured while landing #11404 (PR #11554, the --self-test invocation matcher). Filed unassigned; nothing is red today — the one live instance is closed inside that PR, and this card is about the latent half it does not close.

What was measured

scripts/pm/dispatch-gates.mjs is not a discovered gate FILE. check:pm-dispatch-gates resolves to scripts/pm/check-dispatch-gates.mjs, which reaches the tool by spawnSync, never by import — so discoverFamilies' "never follow a module that is itself a gate file" rule does not protect it.

Its module body yields 9 watch hints under today's extractor:

scripts, .claude/skills/pm-dispatch/SKILL.md, .claude/agents/os-dev.md,
skills/objectstack-pm-dispatch/SKILL.md, packages/spec/src/**,
.github/workflows, packages/plugins, packages/drivers, packages/services

Against 6465 tracked files those cover 2553 of them. Only .github/workflows is something the tool reads as a population; packages/plugins, packages/drivers and packages/services are the bases its package resolver joins, and packages/spec/src/** is a tier glob the file's own self-test pins as inert for gate matching.

So any gate script that statically imports ./dispatch-gates.mjs inherits 2553 (gate, file) pairs of population it never opens. Measured live: scripts/pm/bare-root-worklist.mjs does exactly that, and admitting it as a family handed it all 2553 — 96% of that PR's entire price.

Why the spawn is not a general guard

check-dispatch-gates.mjs's header already records this verdict at length: it exists as a separate file, spawning rather than importing, because "the derivation resolves a check family to its script file and then scans THAT FILE's source for the path literals it operates on", and a directly-wired gate "would print MATCHED for every card under them — a smaller fabrication than the fixture one, of the same kind."

That guard protects exactly one caller — the one that remembered. It is a convention held in one file's prose, not a property of the module, so the next gate that reaches for discoverFamilies or hintCovers as a library gets the fabrication with no warning and every gate green.

PR #11554 closes the live instance narrowly and by measurement, not generally: a --self-test family follows no import, because that invocation runs the script's self-test rather than its work. A check- gate importing the same module is untouched by that rule.

Not asserted

No claim about which remedy is right. Three shapes, in increasing width:

The second is the one that composes with #11511 and #11510, which want the same module shape for the same reason.

Related

#11404 / PR #11554 (where this was measured, and which closes the live instance) · #11511 (the mirror case: a module that IS a gate file, so the follow refuses it and the honest half is lost) · #11510 · #8162 (check-dispatch-gates.mjs's spawn verdict) · #11190 / PR #11512 (the follow this rides on)


Generated by Claude Code

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    domain:devxDomain lane — seat card indexed by label:pm:seat
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 24, 2026
  2. claude commented on Aug 25, 2026

    @claude
    Contributor

    Concentrated triage batch: finding → pm:queue, Task (domain:devx stands) — latent but cheap to close: dispatch-gates.mjs is spawn-reached, not imported, so the "never follow a gate file" rule does not shield it, and its module body yields 9 watch hints covering 2553 files for any future gate that imports it. Fix: make it structurally non-followable (explicit exclusion in discoverFamilies, or move the path strings out of module-body reach per the discoverer's own conventions); self-test pins that importing it fabricates zero pairs.


    Generated by Claude Code

  3. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 25, 2026
  4. added theissue type on Aug 25, 2026
  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 25, 2026
  6. self-assigned this
    on Aug 25, 2026
  7. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    Contributor

    Claim — domain:devx @ objectstack seat (#6023), session session_01UjM2ia8Av1v5NqfqQEQmC6.

    • Branch: claude/issue-11556-followable-non-gate-module
    • Worktree: ../objectstack-11556 (dedicated)
    • Model tier: opus
    • Clause ②: falsify, don't build on, this seat's assumptions.

    Released from a serial hold. This card sat behind scripts/pm/dispatch-gates.mjs until PR #12324 merged (10bd486d62).

    Premise re-verified on current origin/main — the mechanism, which is what the card actually claims:

    scripts/pm/check-dispatch-gates.mjs:142
      const result = spawnSync(process.execPath, [join(ROOT, TOOL), '--self-test'], …)
      — imports only node builtins; still reaches the tool by SPAWN, never by import
    
    scripts/pm/bare-root-worklist.mjs:72
      } from './dispatch-gates.mjs';
      — the live inheritance path is intact
    

    So dispatch-gates.mjs is still not a discovered gate FILE, discoverFamilies' "never follow a module that is itself a gate file" rule still does not protect it, and a real consumer still imports it. The mechanism holds.

    ⚠️ The card's absolute numbers are stale and must be restated, not quoted. It measured 2553 of 6465 tracked files; the tree is now at 6840. Every term has to be re-derived together on one named tree — ⛔ never a denominator refreshed alone.

    ⭐ And take the right function. This lane just made the opposite mistake twice in one shift: #12300 added if (globInNonFinalSegment(hint)) return triggerCovers(hint, inputPath); at hintCovers:1523, so a hint whose glob sits in a non-final segment never reaches collapseHint at all. Of this card's nine hints, packages/spec/src/** has its glob in the final segment and the rest carry no glob — so the branch probably does not move them, but "probably" is not a measurement. Drive hintCovers, not collapseHint.

    Corroborating datum from PR #12324's both-ways derivation, taken independently of this card: that file's own watch-hint set measured 9 before and 9 after, added [], removed [] — matching the card's nine.

    Serial constraint: scripts/pm/dispatch-gates.mjs is single-writer for this claim. #12074 · #12046 · #12173 · #12299 all claim the same file and are held behind this card.


    Generated by Claude Code

  8. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    Contributor
    {
      "issue": 11556,
      "status": "done",
      "branch": "claude/issue-11556-followable-non-gate-module",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12346",
      "premise_still_valid": true,
      "summary": "Mechanism re-verified on my own base c48d46d70a: check-dispatch-gates.mjs:142 still reaches the tool by spawnSync, bare-root-worklist.mjs:72 still statically imports it, and no discovered family's files include the tool — so it is still a followable non-gate module and the inheritance path is live. Numbers restated, never quoted: 9 module-body literals covering 2660 of 6840 tracked files (card said 2553 of 6465). Chose shape 3 done by DECLARATION rather than heuristic: the module now carries a marker ('dispatch-gates: inherited-population PATH ... -- REASON') naming the one population a follower inherits, read fresh on every run by hintsOfModule, in the idiom of the two markers already in this file. It is narrowing-only and enforced: a declared path the module does not spell is REFUSED with a throw, so a declaration can remove leads but never invent one. Rejected shape 1 (respell as unslashed halves) because it protects only the literals someone remembered to respell — the same failure one level down — and would have to mangle MANDATORY_TIER_GLOBS/SUSPECT_TIER_GLOBS, a machine-readable copy of a maintainer ruling. Shape 2 (extract a module) is a file-surface change and was NOT built, per zone 1a; #11511/#11510 remain untouched. Result: a gate importing the tool inherits 1 literal / 28 files instead of 9 / 2660. Zero coverage cut — the .github/workflows population the tool really readdirs is pinned as still fully reachable. One file changed, the declared surface. Two operational notes for the PM, neither a repo defect: (1) the additive POST .../labels endpoint is NOT reachable from this dev seat — raw api.github.com returns 'GitHub access is not enabled for this session' and there is no gh CLI — so skip-changeset went on by the declared fallback (read, union, whole-set write) and is confirmed by read-back; (2) GitHub's body sanitizer ate the angle-bracket placeholders out of the PR body's code fence on creation, and the body was re-written without them.",
      "tests": "All at final commit 2d5d0fc300 (merged origin/main cf99875ea8 after a STALE TREE warning; re-derived after). Exit codes captured before any pipe, verdicts quoted from each gate's own line. Union derived with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' (exit 0, no STALE TREE), 11 matched families, all run: check:agent-test-spelling EXIT=0 · check:bash32-floor EXIT=0 · check:cli-command-ids EXIT=0 ('276 command-id literal(s) across 99 file(s) ... all resolve') · check:cross-package-test-inputs EXIT=0 ('OK: 17 package(s) read outside themselves, all declared') · check:entry-guard EXIT=0 · check:parse-guard EXIT=0 · check:pm-dispatch-gates EXIT=0 ('dispatch-gates self-test: 664 cases pass.', 642 on base) · check:pnpm-filter-targets EXIT=0 · scripts/check-ci-filter-parity EXIT=0 ('OK: all 102 declared cross-package glob(s) ... covered') · scripts/check-cross-package-test-inputs EXIT=0 · scripts/check-self-test-wired EXIT=0. Plus check:nul-bytes EXIT=0 (6834 files, no raw control bytes) and a control-byte self-scan of the diff (grep exit 1, no matches). Repo-wide 'eslint . --no-inline-config' EXIT=0, 83s under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, waited 0s) — a full run, NOT a narrowed one. bare-root-worklist --self-test EXIT=0, output byte-identical to base: '46 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing'. NOT MEASURED: check-ci-filter-parity's first attempt printed 'PREREQUISITE NOT MET — the dependency yaml is not installed' in the fresh worktree; the EXIT=0 above is the real run after pnpm install. BOTH-WAYS DERIVATION (this file judges its own PR): base tool from origin/main cf99875ea8 and head tool run over identical 10 input paths in the same tree — plain, --residue and --tier all BYTE-IDENTICAL (25130/49355/1764 bytes). Census identical both ways: 6841 tracked, 173 families, 70013 (gate,file) pairs, 350 inherited hints tree-wide; this file's own watch-hint set 9 before and 9 after, added [] removed []. Driven through hintCovers, not collapseHint (#12300's globInNonFinalSegment short-circuit); measured that none of the nine hints carries a glob in a non-final segment. Live-inheritor sweep found bare-root-worklist.mjs as the only static importer, with a positive control (same search shape returns 10+ hits for js-comment-mask.mjs). ABLATION, two legs, no build/dist leg needed (the tool is source-resolved and run directly by node): (A) deleted the live marker line — mutation confirmed ON DISK by anchored count (live-marker lines 1 to 0, file hash changed), gate EXIT=1 with 7 of 664 named cases failing, the case names printing the restored fabrication verbatim ('inherits 9 of those 9 literal(s)', 'price of that import drops from 2660 tracked files to 2660'); (B) widened the declaration with packages/objectql/src — confirmed on disk (mutated-marker lines 0 to 1, live-marker 1 to 0), gate EXIT=1 with 'declares 1 path(s) this module does not spell: packages/objectql/src — the declaration may only NARROW what a caller inherits, never invent it'. Both legs restored under trap ... EXIT INT TERM; restore proven byte-identical with git hash-object (97e68e68d1c9208f4ccb42c182a9490b1e46c0cc before and after), working tree clean. A FIRST attempt at leg B was refused by its own anchor assertion (the string occurs twice: live marker and docblock example) and never landed on disk — that run measured nothing and is not counted; the second attempt used a line-anchored pattern. Changeset: none, and the reasoning rather than an assumption — scripts/** is repo-internal PM tooling, in no package's published files list, and the diff changes no package runtime behaviour, exports or docs, so there is no user-visible change for a release note; skip-changeset applied and read back on PR #12346 (labels now size/m, skip-changeset).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  9. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    Contributor

    os-dev-report

    (Re-post of the report above — identical JSON. That comment's HTML-comment marker was eaten by GitHub's body sanitizer on write, confirmed by read-back, so the marker is spelled as literal text here. No edit-comment tool is reachable from this seat; this is the scannable copy.)

    {
      "issue": 11556,
      "status": "done",
      "branch": "claude/issue-11556-followable-non-gate-module",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12346",
      "premise_still_valid": true,
      "summary": "Mechanism re-verified on my own base c48d46d70a: check-dispatch-gates.mjs:142 still reaches the tool by spawnSync, bare-root-worklist.mjs:72 still statically imports it, and no discovered family's files include the tool — so it is still a followable non-gate module and the inheritance path is live. Numbers restated, never quoted: 9 module-body literals covering 2660 of 6840 tracked files (card said 2553 of 6465). Chose shape 3 done by DECLARATION rather than heuristic: the module now carries a marker ('dispatch-gates: inherited-population PATH ... -- REASON') naming the one population a follower inherits, read fresh on every run by hintsOfModule, in the idiom of the two markers already in this file. It is narrowing-only and enforced: a declared path the module does not spell is REFUSED with a throw, so a declaration can remove leads but never invent one. Rejected shape 1 (respell as unslashed halves) because it protects only the literals someone remembered to respell — the same failure one level down — and would have to mangle MANDATORY_TIER_GLOBS/SUSPECT_TIER_GLOBS, a machine-readable copy of a maintainer ruling. Shape 2 (extract a module) is a file-surface change and was NOT built, per zone 1a; #11511/#11510 remain untouched. Result: a gate importing the tool inherits 1 literal / 28 files instead of 9 / 2660. Zero coverage cut — the .github/workflows population the tool really readdirs is pinned as still fully reachable. One file changed, the declared surface. Two operational notes for the PM, neither a repo defect: (1) the additive POST .../labels endpoint is NOT reachable from this dev seat — raw api.github.com returns 'GitHub access is not enabled for this session' and there is no gh CLI — so skip-changeset went on by the declared fallback (read, union, whole-set write) and is confirmed by read-back; (2) GitHub's body sanitizer ate the angle-bracket placeholders out of the PR body's code fence on creation, and the body was re-written without them.",
      "tests": "All at final commit 2d5d0fc300 (merged origin/main cf99875ea8 after a STALE TREE warning; re-derived after). Exit codes captured before any pipe, verdicts quoted from each gate's own line. Union derived with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' (exit 0, no STALE TREE), 11 matched families, all run: check:agent-test-spelling EXIT=0 · check:bash32-floor EXIT=0 · check:cli-command-ids EXIT=0 ('276 command-id literal(s) across 99 file(s) ... all resolve') · check:cross-package-test-inputs EXIT=0 ('OK: 17 package(s) read outside themselves, all declared') · check:entry-guard EXIT=0 · check:parse-guard EXIT=0 · check:pm-dispatch-gates EXIT=0 ('dispatch-gates self-test: 664 cases pass.', 642 on base) · check:pnpm-filter-targets EXIT=0 · scripts/check-ci-filter-parity EXIT=0 ('OK: all 102 declared cross-package glob(s) ... covered') · scripts/check-cross-package-test-inputs EXIT=0 · scripts/check-self-test-wired EXIT=0. Plus check:nul-bytes EXIT=0 (6834 files, no raw control bytes) and a control-byte self-scan of the diff (grep exit 1, no matches). Repo-wide 'eslint . --no-inline-config' EXIT=0, 83s under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, waited 0s) — a full run, NOT a narrowed one. bare-root-worklist --self-test EXIT=0, output byte-identical to base: '46 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing'. NOT MEASURED: check-ci-filter-parity's first attempt printed 'PREREQUISITE NOT MET — the dependency yaml is not installed' in the fresh worktree; the EXIT=0 above is the real run after pnpm install. BOTH-WAYS DERIVATION (this file judges its own PR): base tool from origin/main cf99875ea8 and head tool run over identical 10 input paths in the same tree — plain, --residue and --tier all BYTE-IDENTICAL (25130/49355/1764 bytes). Census identical both ways: 6841 tracked, 173 families, 70013 (gate,file) pairs, 350 inherited hints tree-wide; this file's own watch-hint set 9 before and 9 after, added [] removed []. Driven through hintCovers, not collapseHint (#12300's globInNonFinalSegment short-circuit); measured that none of the nine hints carries a glob in a non-final segment. Live-inheritor sweep found bare-root-worklist.mjs as the only static importer, with a positive control (same search shape returns 10+ hits for js-comment-mask.mjs). ABLATION, two legs, no build/dist leg needed (the tool is source-resolved and run directly by node): (A) deleted the live marker line — mutation confirmed ON DISK by anchored count (live-marker lines 1 to 0, file hash changed), gate EXIT=1 with 7 of 664 named cases failing, the case names printing the restored fabrication verbatim ('inherits 9 of those 9 literal(s)', 'price of that import drops from 2660 tracked files to 2660'); (B) widened the declaration with packages/objectql/src — confirmed on disk (mutated-marker lines 0 to 1, live-marker 1 to 0), gate EXIT=1 with 'declares 1 path(s) this module does not spell: packages/objectql/src — the declaration may only NARROW what a caller inherits, never invent it'. Both legs restored under trap ... EXIT INT TERM; restore proven byte-identical with git hash-object (97e68e68d1c9208f4ccb42c182a9490b1e46c0cc before and after), working tree clean. A FIRST attempt at leg B was refused by its own anchor assertion (the string occurs twice: live marker and docblock example) and never landed on disk — that run measured nothing and is not counted; the second attempt used a line-anchored pattern. Changeset: none, and the reasoning rather than an assumption — scripts/** is repo-internal PM tooling, in no package's published files list, and the diff changes no package runtime behaviour, exports or docs, so there is no user-visible change for a release note; skip-changeset applied and read back on PR #12346 (labels now size/m, skip-changeset).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    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

Labels

domain:devxDomain lane — seat card indexed by label:pm:seattoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions