Uh oh!
There was an error while loading. Please reload this page.
Resolve TypeScript-authored gates in the dispatch derivation - #12247
Conversation
`resolveCheckToFiles` matched only `.mjs|.cjs|.js|.sh`, so every gate whose npm script names a `.ts`/`.mts`/`.cts` file resolved to zero files. `discoverFamilies` never opened the source, so those families contributed no watch hints, no first-party import following, no `no-path-population` read, and no `gateFiles` entry -- and scored `undetermined` for every card, indistinguishable from a gate whose author declined to declare a population. Three changes, all in the derivation: - widen the extension alternation to `ts|mts|cts`; - resolve a package manifest's spelling against the manifest's OWN directory (`dir`), so a script that climbs out of its package (`tsx ../../scripts/check-exported-any-returns.mts`) normalises to the tracked repo path instead of being misattributed to the package; - anchor the alternation on its right, so an extension that is a prefix of a longer one (`.tsx`, `.json`) no longer matches as itself. Measured on this tree, both directions, in the docblock: 73278 -> 74481 watch-hint (gate, file) pairs (+1203, 0 lost), zero-file families 24 -> 1, 19 families gaining coverage, 0 losing, 0 import-follow edges suppressed, 0 inherited hints lost, 0 gate files naming a path that does not exist. The one family left zero-file, `check:app-nav-i18n`, is a `pnpm --filter ... run` composite that names no path at all; no extension list can reach it, and the self-test asserts the residue is only ever composites of that shape. Part of #12107 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
yinlianghui
commented
Aug 25, 2026
Body-byte correction (the AGENTS.md "GitHub mutates body BYTES" class, caught on read-back). Item 3 of "What changed" renders as The right-hand boundary added to the alternation is a negative lookahead for a word character or Recorded rather than fixed in place: re-editing the body re-runs the same sanitizer over the same Generated by Claude Code Generated by Claude Code |
…faces Resolving TypeScript-authored gates un-voids the bare-root sweep for their sources, which is what the card asked for -- that sweep walks the gate sources `discoverFamilies` resolves, so a TypeScript gate was never swept. Two genuinely invisible bare roots come with it, both `SKILLS_DIR = 'skills'`. Both recorded REFUSE-UNSPELLABLE, on consistency with the existing `check-skills-token-ratchet.mjs SKILLS_DIR skills` row directly above them: the same bare root, the same readdir-plus-named-file shape, the same scale (11 of 50 there, 12 of 50 here). One root answered one way. Each `why` is measured from its own gate's filter and cites that gate's own file:line -- neither borrows the other's numbers, which the map's docblock forbids by name. They are not the same mechanism: - check:skill-docs DOES enumerate the bare root (build-skill-docs.ts:221) and reconciles it against DISPLAY both ways, but :222 admits a child only if it carries SKILL.md -- so the population is a filename filter, not the root. - check:skill-refs never reads the bare root at all: it iterates the authored SKILL_MAP and manages skills/<name>/references/. Its true population, skills/*/references/**, collapses to a double slash (#12246), which is what leaves the bare root as the only spellable claim. bare-root-worklist --self-test: 45 live row(s), 39 recorded verdict(s) -- none stale, none missing. Part of #12107 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12107
resolveCheckToFilesmatched only.mjs|.cjs|.js|.sh, so every gate whose npm script named a.ts/.mts/.ctsfile resolved to zero files.discoverFamiliesnever opened the source, sothose families contributed no watch hints, no first-party import following, no
declaredNoPathPopulationread, and nogateFilesentry — and scoredundeterminedfor everycard in the tree, indistinguishable from a gate whose author declined to declare a population.
File surface:
scripts/pm/dispatch-gates.mjs(including its--self-test), plusscripts/pm/bare-root-worklist.mjs'sTRIAGEmap — the second file added by an explicit PMsurface widening, for the landing obligation this fix creates (see "The bare-root obligation"
below). No gate script,
package.jsonor workflow is touched.Derived at the final commit
d821f7f25c.What changed
Three changes in the derivation (
dispatch-gates.mjs):ts|mts|cts.resolveCheckToFilestakes
{ dir }and returns tracked paths, replacing the caller's.map((f) => join(base, pkgDirGuess, f)).longer matches as itself. (The literal spelling is a negative lookahead; see the comment below —
GitHub's sanitiser eats it from a PR body.)
Plus two
TRIAGErows inbare-root-worklist.mjs, recording the two bare roots that change 1makes visible.
Premise re-verified on my own base
Base
f11fc61c51. The reproducer was run, not recalled:Same as the seat claim measured at
9abe4e4827. PR #11934 (a187fe612b) was checked by diffrather than by title: its hunks land at
repoIdentity/DERIVATION_SURFACE/baseDrift/driftLines,bannerLines, and the self-test block —resolveCheckToFilesanddiscoverFamiliesare untouchedby it, so this fix sits cleanly on top.
The measurement, in both directions
firstPartyImportTargets' docblock is the standard this is written to, and the same numbers are inthe code. Over 167 families x 6763 tracked files:
The addition is real, not fabricated. Of the hints these 19 families newly contribute, 19 are
live and 128 are inert (they cover zero tracked files, so they cannot put any gate in any
card's matched column). The +1203 is concentrated in three families that declare real corpora —
check:skill-examples+495 (packages/client,packages/client-react,content/docs, spot-checkedagainst that gate's own header, which names
packages/client-react/srcandpackages/client/src),check:docs+440,check:generated+234.The 4 re-attributions are the one number that differs from the precedent's (which reports 0), so
they are reported rather than rounded.
check:liveness,check:empty-state,check:variant-docsand
check:strictness-ledgereach already matched their own source file throughspec-liveness-check.yml'spaths:filter, and now match it through identity instead — the vialabel moves from
CI trigger in spec-liveness-check.ymltogate script. No path enters or leavesany family's matched list. That is
coveringKey's own declared ordering (identity outranks atrigger, because it is the stronger provenance), so these are re-attributions up.
The subtraction direction is zero for a structural reason, not by luck. Admitting these sources
makes them
gateFiles, anddiscoverFamiliesrefuses to follow a module that is itself a gatefile.
firstPartyImportTargetsadmits only relative specifiers resolving inside the rootscripts/dir; 22 of the 23 live underpackages/spec/scripts/, which no first-party follow canreach, and the 23rd (
scripts/check-exported-any-returns.mts) is imported by nothing. The mirrorquestion — opening 23 files also means following their imports, which is how a gate inherits a
population it never reads — is also 0: the 22 are out of reach by the same rule, and the root-dir
one imports
./check-regen-pending.mjsand./invoked-as.mjs, both already gate files. Thelive half of the self-test asserts this rather than trusting it.
#11556's fabrication surface: measured, and the assumption falsified in direction
The dispatch asked whether adding 23 sources to
gateFilesmakes #11556's inheritable-literalsurface worse, on the assumption it is a net subtraction. Measured on this tree,
extractWatchHintsoverscripts/pm/dispatch-gates.mjs:It is neither worse nor a subtraction — it is a no-op on both counts. The assumption's
conclusion (cannot fabricate, safe to land) holds; its direction does not.
scripts/pm/dispatch-gates.mjsis not a gate file before or after this change, so nothing about its followability moves.
Two corrections to the card, both measured
1. The card's table names package-relative spellings, not tracked paths. It lists e.g.
check:generated → scripts/check-generated.ts. The tracked path ispackages/spec/scripts/check-generated.ts— 22 of the 23 resolve through the package-filter branchagainst
packages/spec. The table is the command string's spelling, not the repo's.2. The card's point 1 predicts the wrong mechanism, and the difference decides the fix. The
card expects
packages/client/../../scripts/…— a path that resolves on disk but is not atracked-path spelling. That is not what happens. The old pattern was anchored on the literal
scripts/, so the match silently dropped the../../, leavingjoinnothing to normalise.Measured with the extensions widened and normalisation not yet in place:
The family leaves the honest
undeterminedbucket and stops being nameable as such, whileexistsSynckeeps the file closed so it still reads zero hints — the card's own trigger gate,still silent, now silent behind a confident phantom identity key. That is strictly worse than the
bug, which is why the climb prefix is matched and normalised through
joinagainst the manifest'sown directory rather than stapled on afterwards.
The right-hand boundary — a narrowing that rides along, declared
The alternation was never anchored on its right, so an extension that is a prefix of a longer one
matched as itself:
The
.jsoncase was already live through thejsbranch; admittingtsadds the common one,.tsx— so closing it is part of this change's own correctness, not a tidy-up beside it. Bothproduce the same output as the package-relative case above: a gate file that does not exist. It is
closed in the same edit and pinned by the same live assertion. Measured: adding the boundary
changes nothing on this tree — pairs,
gateFilesand the family map are byte-identical with andwithout it — so it costs zero recall today.
What is deliberately still zero-file
check:app-nav-i18n. Its root script ispnpm --filter @objectstack/cli run check:app-nav-i18n—a composite that names a package and a script name, no path. No extension list can reach it;
resolving it means following a
pnpm --filter … run …hop into another manifest, which is adifferent mechanism. A result of 0 remaining would mean this fix had absorbed a family it cannot
honestly resolve. The self-test asserts the residue is only ever composites of that shape, and
that it is non-empty — never that the count is zero.
What this does NOT buy
check:exported-any-returns, the gate the card was filed from, resolves its source now and stillcontributes no watch hints: that source declares no path literals and carries no
no-path-populationmarker. It scoresundeterminedbefore and after. The difference is the wholepoint of the card and none of it shows in a pair count — before, the declaration was never read;
now it has been read and there is nothing there. The first is a defect in this derivation, the
second is a missing declaration on that gate, and only the second can be acted on by its author.
The bare-root obligation — surfaced, measured, and discharged here
Resolving the TypeScript sources un-voids the bare-root sweep for them. That is the card's own
prediction, not a side effect: the card records that the extension bug "silently voids the bare-root
sweep for these files", because that sweep walks the gate sources
discoverFamiliesresolves. Twogenuinely invisible bare roots come with it, both
SKILLS_DIR = 'skills'.Reverse-verified, and the evidence is kept visible rather than deleted now that it is green:
Both rows are recorded
REFUSE-UNSPELLABLE.⭐ The verdict is chosen on consistency with the existing sibling row, not on a readdir argument.
bare-root-worklist.mjs:301already carries askillsrow —scripts/check-skills-token-ratchet.mjs SKILLS_DIR skills,REFUSE-UNSPELLABLE, "one named fileper child directory, 11 of 50 (22%)". That gate reads the same bare root the same way
(
check-skills-token-ratchet.mjs:213isreaddirSync(dir, { withFileTypes: true }), and its line205 states the same SKILL.md-less exclusion in prose) and reconciles the listing against an authored
per-file roster in both directions. So "it enumerates the root, so a new name is catchable" does not
distinguish these two gates from the one already recorded as unspellable. Both new rows measure
12 of 50 (24%) against that sibling's 22%; every
REFUSE-WIDErow in the map sits at 39–90%.One root, answered one way.
Each
whyis measured from its own gate's filter and cites that gate's own file:line. The map'sdocblock forbids carrying a sibling's numbers into a new row by name — it records the
check-declaration-mirrorsincident, where a copiedwhywas wrong in both terms and--self-testcould not catch it (it audits verdict-set membership and
whylength, never what awhysays).The two gates are not the same mechanism, and the rows say so:
check:skill-docsdoes enumerate the bare root (build-skill-docs.ts:221) and reconciles itagainst
DISPLAYin both directions (:227-228) — but:222admits a child only if it carriesSKILL.md, so the population is that filename filter, not the root. Population: 11objectstack-*/SKILL.md+README.md= 12 of 50.check:skill-refsnever reads the bare root at all. It iterates the authoredSKILL_MAP(
build-skill-references.ts:43-127,304), resolvesskills/<name>/, and enumerates and prunesskills/<name>/references/viamanageDir/ownsReferenceEntry(:288-294). There is noreaddirSync(SKILLS_DIR)in that file. Its true population isskills/*/references/**, whichcollapseHintreduces toskills//references/**— a double slash no tree can hold ([finding] A mid-segment*collapses to a double slash, soskills/*/references/_index.mdnames 9 real files and reaches none — and the family is filed as "THE LAYOUT MOVED" #12246) —and that is what leaves the bare root as the only spellable claim. Population: 12 of 50, 9 of
them the
_index.mdit emits.Self-test
dispatch-gates --self-testgoes 595 → 614 cases, +19, covering every behaviour added:.ts,.mts,.ctsseparately (an alternation regresses one branch at a time), plus amixed
.mjs+.tscommand;.jsonargument and a.tsxfile are not gate scripts, while.tsstill is;stated as a negative, dedupe on the normalised path, the climbs-clear-of-the-repo drop, and the
unchanged root-manifest behaviour;
catches the near-miss above); the climbing family resolves to the tracked root path; the tree
really does resolve TypeScript gates; every remaining zero-file family is a pnpm composite and
there is at least one; and the subtraction direction, written in the same shape as the existing
self-test-family promotion assertion beside it.
bare-root-worklist --self-testneeds no new case: its self-test already asserts that every liverow carries a verdict and that no verdict is stale, which is exactly what the two new rows satisfy.
Gates
Union re-derived at the final commit
d821f7f25cwith the committed tools:⭐ This PR edits the derivation tool itself, so the union was derived both ways. At the first
commit the BASE tool and the committed tool produced a byte-identical local gate list — this
change does not move the union it is judged by. At the final commit the list grows from 9 to 10
families, and the tenth is
node scripts/pm/bare-root-worklist.mjs --self-test, matched via gatescript identity — the derivation naming the very gate whose ledger this PR edits, which is the
mechanism working rather than a change in it.
check:agent-test-spelling--self-test, which drives the same sweep RED over a temp treecheck:cross-package-test-inputsAll 117 self-test cases passed./OK: 16 package(s) read outside themselves, all declaredcheck:entry-guard✓ check:entry-guard: 161 scripts/ file(s) — every entry guard goes through invoked-as.mjscheck:parse-guardcheck:pm-dispatch-gates✓ dispatch-gates self-test: 614 cases pass.check:pnpm-filter-targets✓ check:pnpm-filter-targets: 136/173 --filter occurrence(s) across 28 file(s) resolvescripts/check-ci-filter-parity.mjsOK: all 96 declared cross-package glob(s) (81 unique) are coveredscripts/check-cross-package-test-inputs.mjsOK: 16 package(s) read outside themselves, all declaredscripts/check-self-test-wired.mjs✓ every one of the 132 script(s) CI runs that ship a --self-test has that self-test run by CIscripts/pm/bare-root-worklist.mjs --self-testOK self-test: 45 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) — none stale, none missingscripts/check-nul-bytes.mjsOK (scanned 6757 text file(s) … no raw ASCII control bytes)Every exit code was captured into a variable before any pipe. All eleven ran under
scripts/pm/os-verify-lock.shat the final commit —VERDICT command-exit 0 · held the lock 57s · waited 422s (7m02s). The wait was spent inside theround, not by ending it.
Declared narrowing: no repo-wide
pnpm lintwas run — CI owns that farm run, and this diff istwo root
scripts/files with no eslint config change, so no untouched file's verdict can move.Out-of-scope finding, filed not fixed
#12246 — a mid-segment
*collapses to a double slash, soskills/*/references/_index.mdnames 9 real tracked files and reaches none, and
unreachableClassthen files that family under"THE LAYOUT MOVED … a real miss, worth triaging" while printing three TypeScript module
specifiers as its evidence. Only visible because of this PR (the residue listing goes 2 → 12
families). Unassigned, not fixed here — and now load-bearing for this PR, since it is what makes
skills/*/references/**unspellable in thecheck:skill-refsrow above.Serial queue — deliberately not folded in
Three open cards name
dispatch-gates.mjsand are held behind this PR: #12074 (its option 1,teaching the derivation to name
check:bare-root-worklistfor a new gate script), #11556 (thismodule's own literals being inheritable — measured above as unmoved by this change), #11199
(
check:cross-package-test-inputsnever named for an edited test file). None is touched. #12246joins that queue.
Changeset
None — root
scripts/only, nothing published.skip-changeset.Generated by Claude Code