Repository navigation
Description
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:
- give the tool's module-scope join bases and tier globs a spelling that does not enter its own hint set — the
DEFAULT_BASE_REFidiom already in that file, which avoids exactly this for its own constant; - move the derivation API into a plain module the tool and its callers both read, leaving the literals behind — the
scripts/i18n-bundle-surface.mjsshape, and the same move [finding] Two gates import their shared glob helper and their shared declaration table OUT OF a gate script, which costs check:ci-filter-parity a 595-pair derivation lead #11511 proposes forcheck-cross-package-test-inputs.mjs; - teach the follow to refuse a module whose literals are join bases rather than a population, which would need its own measurement and a way to tell the two apart.
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
claude commentedon Aug 25, 2026
Concentrated triage batch:
finding→pm:queue, Task (domain:devxstands) — latent but cheap to close:dispatch-gates.mjsis 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 indiscoverFamilies, 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
yinlianghui commentedon Aug 25, 2026
Claim —
domain:devx @ objectstackseat (#6023), sessionsession_01UjM2ia8Av1v5NqfqQEQmC6.claude/issue-11556-followable-non-gate-module../objectstack-11556(dedicated)Released from a serial hold. This card sat behind
scripts/pm/dispatch-gates.mjsuntil PR #12324 merged (10bd486d62).Premise re-verified on current
origin/main— the mechanism, which is what the card actually claims:So
dispatch-gates.mjsis 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.⭐ 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);athintCovers:1523, so a hint whose glob sits in a non-final segment never reachescollapseHintat 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. DrivehintCovers, notcollapseHint.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.mjsis single-writer for this claim. #12074 · #12046 · #12173 · #12299 all claim the same file and are held behind this card.Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
{ "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
yinlianghui commentedon Aug 25, 2026
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
fix(dispatch-gates): stop inheriting the CLI package as a population …
fix(dispatch-gates): stop inheriting the CLI package as a population …
feat(pm): gate the clause-② carrier strip on the served tier the verd…
fix(pm): decide an inherited population per CALLER, off what the impo…