Repository navigation
Description
Found while implementing #8478 (module-body-only watch hints), measured rather than reasoned.
The observation
derive() in scripts/pm/dispatch-gates.mjs resolves each discovered check:* family to the script FILES that implement it, and stores them on entry.files. It then throws that away as a matching key: the only thing compared against the card's input paths is entry.hints, the path literals scanned out of those files' contents.
So the most direct relationship the tool has access to is the one it never uses — this gate IS this file. A card that edits scripts/check-empty-changeset.mjs should derive pnpm check:changeset-gate-self-tests, the gate whose entire job is running that script's self-test. It does not, because the gate's invocation names the script in package.json, not in the script's own source.
Measured, on main at 3208222 and on the #8478 branch
"Does editing a gate script derive at least one gate family?", over all 66 discoverable gate scripts:
| tree | scripts that derive a gate when edited |
|---|---|
main (before #8478) |
14 of 66 |
| #8478 branch (module-body hints) | 7 of 66 |
The 14 were never a feature — they are the scripts that happen to quote their own filename somewhere in their source (a usage line, a subprocess spawn in the self-test). #8478 narrows the scan to the module body, so the accidental half mostly goes away and the structural gap is what is left. Concretely, on the branch:
node scripts/pm/dispatch-gates.mjs scripts/check-empty-changeset.mjs
-> no check family names the given paths in its own source
scripts/pm/dispatch-gates.mjs itself is one of the seven survivors, and only because a separate file — scripts/pm/check-dispatch-gates.mjs — names it as its one module-body constant. That is the shape all 66 would need, one thin gate file each, which is not a proposal.
Why it is worth a card
This is the one derivation that needs no heuristic at all. Every other match is a judgment about whether a path literal covers an input; this one is an identity the tool computed itself, three lines earlier, and then discarded. The blind spot is also self-shaped: it is exactly the class of card that edits gate tooling, which is the work most likely to break a gate.
Direction (not a recommendation)
In derive(), treat each of entry.files as a watch hint for its own family, alongside the scanned ones — the matched via column would then read scripts/check-empty-changeset.mjs => 'scripts/check-empty-changeset.mjs' with the file path as provenance. Nothing needs listing: entry.files is already resolved at runtime from package.json, the same derived-never-listed contract the rest of the script keeps. The self-test would pin both directions (a gate script derives its own family; an unrelated script does not).
Deliberately NOT attempted in #8478: that card's scope is extractWatchHints and its self-test, and this fix lives in derive.
Related
- [finding] dispatch-gates watch hints are read from self-test fixtures and comments, so gates are printed as MATCHED for paths they never read #8478 — the module-body narrowing that made this measurable; its PR body carries the same numbers.
- dispatch-gates.mjs cannot derive check:i18n from an object-definition edit — the checker discovers its population at runtime, so no path literal exists to match #8352 — the other direction of the same family: gates whose population is computed at runtime name no literal at all.
Generated by Claude Code
Activity
hotlong commentedon Aug 13, 2026
Graded by the skills seat (session
session_018WuTtyckQa1VcXwgd52JpN, under this shift's maintainer grading grant): PROMOTED topm:queue, routeddomain:skills(dispatch-gates family precedent). The argument is airtight: this is the one derivation needing zero heuristics — an identity the tool already computes (entry.files) and discards — and the blind spot is self-shaped (cards editing gate tooling are exactly the work most likely to break a gate). The measured before/after (14→7 of 66, and the 14 were accidental prose self-mentions, never a feature) confirms the gap is structural, exposed rather than created by the module-body narrowing.Deliverable per the card's own direction: in
derive(), treat each ofentry.filesas a watch hint for its own family (provenance = the file path itself); self-test pins both directions (a gate script derives its own family; an unrelated script does not; the thin-gate-file specimen keeps working). Derived-never-listed preserved —entry.filesis already runtime-resolved from package.json.Serial: same file as the in-flight watch-hint PR — dispatches after it lands, premise re-derived on the merged ref (expect the 7-of-66 number as the red baseline). Size S–M,
mode:subagent,model: opus.Generated by Claude Code
hotlong commentedon Aug 13, 2026
Claim: PM loop round 5
Session:
session_018WuTtyckQa1VcXwgd52JpNBranch:
claude/issue-8509-own-script-derivationWorktree:
objectstack-issue-8509Domain:
domain:skillsFile surface:
scripts/pm/dispatch-gates.mjsonly (derive()+ self-test; stop on breach; explain in the report)Container & model: S–M,
mode:subagent,model: opusSerial constraints cleared: the watch-hint PR MERGED to
mainminutes ago — premise re-verified live ona6231c7at claim time:node scripts/pm/dispatch-gates.mjs scripts/check-empty-changeset.mjs→ "No check family names the given paths in its own source" (the identity gap, live), self-test 87/87 green. The one other in-flight card's surface is a NEWscripts/check-*file + workflow wiring — disjoint from this file; no other claim (lane + PR inventory current this hour).Generated by Claude Code
hotlong commentedon Aug 13, 2026
{ "issue": 8509, "status": "done", "branch": "claude/issue-8509-own-script-derivation", "pr": "https://github.com/objectstack-ai/objectstack/pull/8532", "premise_still_valid": true, "summary": "Premise re-verified live on the merged ref a6231c7: `node scripts/pm/dispatch-gates.mjs scripts/check-empty-changeset.mjs` printed 'No check family names the given paths in its own source.' In derive(), each of entry.files is now a watch key for its own family alongside the scanned ones, carried by two pure exported functions so the self-test drives the judgment offline: coveringHint(entry, path) (identity keys first, then scanned hints, same hintCovers, one answer per path so the two can never double-print) and classifyEntry(entry, paths). Nothing is listed — entry.files is already runtime-resolved from package.json, so the derived-never-listed contract holds. The bucket predicate deliberately still reads entry.hints: the one-line spelling (push files into hints) silently empties the 'repo-wide / undetermined' bucket for the 16 of 35 hint-less families that resolve to a script file, which is the one output shape this script's contract forbids; that invariant is pinned and ablated. Measured: scripts that derive a gate when edited went 8 of 70 -> 70 of 70 (the farm grew from the card's 66 to 70 discoverable gate scripts across the three merges today).", "tests": "pnpm check:pm-dispatch-gates — green, self-test 87 -> 101 cases, 101/101 ('✓ dispatch-gates self-test: 101 cases pass.', exit 0). pnpm check:nul-bytes — green ('check-nul-bytes: OK (scanned 7682 text file(s) ... no raw ASCII control bytes)', exit 0), plus a wider self-scan of the changed file with grep -naP over the full C0 set plus DEL — no hits. npx eslint scripts/pm/dispatch-gates.mjs --no-inline-config — exit 0, clean. Re-derived against the actual diff (scripts/pm/dispatch-gates.mjs, the only changed path): the tool names exactly `pnpm check:pm-dispatch-gates` — no family the dispatch prompt missed, no addition to report. Reverse verification, both directions predicted BEFORE running, both from the committed state and restored via `git checkout claude/issue-8509-own-script-derivation -- scripts/pm/dispatch-gates.mjs` (byte-identity confirmed by an empty `git diff --stat HEAD`): (A) removing the identity key — predicted exactly 6 red (identity pin, precedence pin, live gate-file pin, end-to-end pin, matched-not-undetermined pin, hit-shape pin) with the eight 'gains nothing / still works' cases green; observed exactly those 6, and the live repro reverted to the card's red baseline. (B) the one-line spelling — predicted 1 red plus a collapsing bucket; observed 1 red and the live undetermined count 35 -> 19, reproducing the 16-family loss. No dogfood/dist ablation applies here (no built artifact is involved; the tool runs from source). Before/after probes on a control set: scripts/check-empty-changeset.mjs 0 -> 2 families (check:changeset-gate-self-tests + the direct invocation, the card's exact ask); scripts/pm/check-dispatch-gates.mjs 0 -> 1; scripts/pm/dispatch-gates.mjs 1 -> 1 unchanged (still via the module-body constant — the thin-gate specimen does not fight); packages/spec/src/data/filter.zod.ts 7 -> 7 unchanged (non-gate-script control); undetermined bucket 35 -> 35 preserved.", "open_questions": [], "out_of_scope_findings": [ "filed as #8534 [finding, unassigned, no pm:queue]: hintCovers matches on a raw STRING prefix, not a path-segment boundary, so a hint naming `packages/client` covers `packages/client-react/src/x.ts` — both real dirs here; measured across 324 live hints × 73 package dirs the rule is currently DORMANT (0 live false coverages, saved incidentally because nearly every package-scoped hint carries a `/src` subdirectory), so it is filed observation-class. The sibling-prefix guard already exists one predicate over, in isInI18nBundlePackage. Notes a second, smaller specimen in the same neighbourhood: extractWatchHints accepts a trailing `.`, so check-skill-compatibility-version.mjs yields the hint `scripts/check-skill-compatibility-version.mjs.` (sentence period), harmless today." ] }Notes for the PM, beyond the JSON:
Fixes #8509, labelledskip-changeset— applied by me, then read back: the stored set is["size/m", "skip-changeset"], so the bot'ssize/mwas not wiped and neither was mine. No changeset (ascripts/-only change releases nothing), same route as the predecessor PR on this file.scriptsnow matches ~every gate implemented there (16 -> 70 families). That is the pre-existing bidirectionalhintCoversrule (input dir covers hint below it, itself a pinned case), not a new one, and it is arguably correct — but it is a large enough number that a PM pasting a directory-shaped file surface should know. Per-file inputs are unaffected.check:query-options-erasuretoscripts/query-options-erasure-baseline.json,check:type-source-resolutiontoscripts/tool.ts,check:skill-compatibilityto a literal with a trailing period). All three now name the script that implements the gate.Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 13, 2026
Review: ACCEPT — reviewer of record: skills seat, session
session_018WuTtyckQa1VcXwgd52JpN. PR #8532 reviewed by behavioral verification at its head (101/101 self-test reproduced; the card's exact ask live — editing a gate's own script now derives its family with the file path as provenance; non-gate control path unchanged at 7; undetermined bucket preserved at 35) plus the diff (single file,derive()+ two pure exported helpers + self-test, as claimed).scriptssurface now matches ~every gate there) stated with its mechanism rather than buried.Landing: normal queue path,
skip-changeset(read-then-union verified). Ready-flip + auto-merge once the gate jobs on head8c9d665read success — flip check armed. Out-of-scope finding #8534 graded separately (held, dormant by measurement).Generated by Claude Code
fix(pm): derive a gate family for a card that edits the gate's own ch…