Uh oh!
There was an error while loading. Please reload this page.
fix(pm): derive a gate family for a card that edits the gate's own check script (#8509) - #8532
Merged
Merged
Conversation
…eck script (#8509) `derive()` resolves every discovered `check:*` family to the script FILES that implement it and stores them on `entry.files`, then compared only `entry.hints` — the path literals scanned out of those files' CONTENTS. So the most direct relationship the tool has was the one it never used: *this gate IS this file*. A card editing `scripts/check-empty-changeset.mjs` derived nothing, because the gate whose whole job is running that script's self-test names it in package.json, not in the script's own source. Measured on this tree: of the 70 gate scripts the workflows resolve to, 8 derived any family when edited — and those eight only because they happen to quote their own filename in their module body, which was never a feature. After this change, 70 of 70. The blind spot was self-shaped: exactly the class of card that edits gate tooling, which is the work most likely to break a gate. Nothing is listed. `entry.files` is already resolved at runtime from package.json, so the identity key keeps the same derived-never-listed contract as the rest of the script. Two pure exported functions carry the judgment, so the self-test can drive it offline: `coveringHint(entry, path)` (identity keys first, then scanned hints — one answer per path, so the two can never double-print) and `classifyEntry(entry, paths)`. The bucket predicate deliberately still reads `entry.hints`: pushing `entry.files` into `entry.hints` would have been the one-line spelling, and it silently empties the "repo-wide / undetermined" bucket for the 16 of 35 hint-less families that resolve to a script file. Self-test 87 -> 101 cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018WuTtyckQa1VcXwgd52JpN
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
hotlong
marked this pull request as ready for review
August 13, 2026 17:49
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#8509
derive()resolves every discoveredcheck:*family to the script files that implement it and stores them onentry.files, then compared onlyentry.hints— the path literals scanned out of those files' contents. So the most direct relationship the tool has was the one it never used: this gate IS this file. A card editingscripts/check-empty-changeset.mjsderived nothing, because the gate whose entire job is running that script's self-test names it in package.json, not in the script's own source.What changed
The card's own direction, and nothing beyond it: each of
entry.filesis now a watch key for its own family, alongside the scanned ones. Two pure exported functions carry the judgment so the self-test can drive it offline:coveringHint(entry, inputPath)— the family's own script files first, then the scanned hints, compared with the samehintCovers. One answer per path, so the two key sets can never double-print; identity goes first only because it is the more specific claim, so it is the better provenance when both fire.classifyEntry(entry, paths)—matched/undetermined/silent, replacing the inline loop inderive.Nothing is listed.
entry.filesis already resolved at runtime from package.json, so the identity key keeps the same derived-never-listed contract as the rest of the script: a gate script added tomorrow is matched by the next run with nothing to update.The
matched viacolumn shows the file path as its own provenance, exactly as the card described:The trap this deliberately avoids
The one-line spelling — push
entry.filesintoentry.hints— also silently empties the "repo-wide / undetermined" bucket. A family whose source names no path whatsoever still resolves to a script file, sohints.length === 0stops being true for it. Measured on this tree: 35 families have no discoverable path literals and 16 of them resolve to a script file, so that spelling moves 16 gates out of the output's honest half and into silence — a gate the derivation cannot mention at all, the one output shape this script's contract forbids.The two questions are different. Matching asks is this family relevant to these paths?, which identity answers. The bucket asks does this family's source name any path at all?, which identity answers for nobody's card but the one editing that very script. So identity decides matching, and the bucket keeps reading
entry.hints. That invariant is pinned, and reverse-verified below.Measured, not reasoned
Re-measured on the current
origin/main(a6231c7), not on the card's numbers — the farm grew to 70 discoverable gate scripts since the card measured 66. "Does editing this gate script derive at least one gate family?":main@a6231c7(red baseline, reproduced)The 8 survivors are the accidental ones the card described — scripts that happen to quote their own filename in their module body (
check-nul-bytes,check-adr-anchors,check-doc-anchors,check-published-files,check-skill-compatibility-version,check-type-check-coverage,docs-audit/affected-docs,git-merge-regen). The count is computed with the tool's real exported judgment, not a copy of it.Probed end to end, before -> after:
scripts/check-empty-changeset.mjscheck:changeset-gate-self-tests+ the direct invocation)scripts/pm/check-dispatch-gates.mjscheck:pm-dispatch-gates, by identity)scripts/pm/dispatch-gates.mjsscripts/check-nul-bytes.mjspackages/spec/src/data/filter.zod.tsTwo consequences worth stating rather than burying:
scripts-shaped inputs. Identity-first replaced several matches that had been landing on data files or prose artifacts:check:query-options-erasurewas attributed toscripts/query-options-erasure-baseline.json,check:type-source-resolutiontoscripts/tool.ts, andcheck:skill-compatibilityto a literal with a trailing dot (...-version.mjs., a sentence end read as a path). All three now name the script that implements the gate.scriptsnow matches ~every gate implemented there (16 -> 70). This is the existing bidirectionalhintCoversrule (input dir covers hint below it, itself a pinned case), not a new one, and it is true: editing all ofscripts/really does move every gate whose implementation lives there. Named here because the number is large enough to surprise a reader.Self-test: 87 -> 101 cases
Both directions, as the card asked. Offline fixtures for the boundary (a gate script derives its own family with the file path as provenance; an unrelated path gains nothing; another gate's script does not match through this one's identity; a family resolving to no file matches nothing by identity; precedence in both directions). Live pins for the population: the thin-gate-file specimen in both of its directions — the gate file now derives its own family by identity, while the tool it runs still derives it through the module-body constant, so the two keys answer different inputs and do not fight — and the card's own specimen resolved through the real root package.json, so the whole chain (resolve -> identity -> cover) is asserted rather than the fixture alone.
Reverse verification, direction predicted first
Ablation A — remove the identity key (
coveringHintfalls back to scanned hints only). Predicted: exactly 6 red — the identity pin, the precedence pin, the live gate-file pin, the end-to-end pin, the matched-not-undetermined pin, the hit-shape pin; the eight "gains nothing / still works" cases stay green because they assertnullor a scanned hint either way. Observed: exactly those 6, and the live repro reverted to the card's red baseline (No check family names the given paths in its own source.).Ablation B — the one-line spelling (let the merged keys answer the bucket question). Predicted: 1 red (the bucket guard) and the live undetermined count collapsing. Observed: 1 red, and
35 -> 19families — the 16 losses measured above, reproduced.Both ablations were run from the committed state and restored with
git checkout claude/issue-8509-own-script-derivation -- scripts/pm/dispatch-gates.mjs, confirmed byte-identical (git diff --stat HEADempty) before the gates below.Gates
pnpm check:pm-dispatch-gates— green, 101/101.pnpm check:nul-bytes— green (7682 files scanned, 0 control bytes), plus a wider control-character self-scan of the changed file (grep -naPover the full C0 set plus DEL) — clean.npx eslint scripts/pm/dispatch-gates.mjs --no-inline-config— clean, exit 0.Re-derived against the actual diff (
scripts/pm/dispatch-gates.mjs): the tool names exactlypnpm check:pm-dispatch-gates— no family the dispatch prompt missed, and the derivation for this card is unchanged by this change (the tool is not itself a gate script; its gate reaches it through the module-body constant).No changeset: a
scripts/-only change releases nothing, so theskip-changesetroute applies — same as the predecessor on this file.Scope
One file,
derive()plus the self-test, as claimed. The residue #8352 names — gates whose population is a runtime-computed git diff and which name no literal at all — is untouched in either direction; it is a different key, not this one.Generated by Claude Code