Uh oh!
There was an error while loading. Please reload this page.
fix(pm): follow a dropped module extension in hintCovers, so nine packages/spec gate families can be MATCHED - #12794
Conversation
…kages/spec gate families can be MATCHED An ESM/TypeScript relative import spells its target without the extension, so a gate importing '../src/migrations/registry' yields the hint packages/spec/src/migrations/registry while the tree holds registry.ts. hintCovers compares whole segments, so the hint missed the file it names by exactly its extension, and nine packages/spec families were unreachable entirely for that reason: no path derivation could name them, and a dev editing the file was never told they owed check:spec-changes. Silent under-derivation, exit 0. The extension is followed at COMPARISON time, not at extraction: the hint text stays what the source says, extractWatchHints stays a pure string function, and the residue rows land untouched. The rule is an equality against MODULE_SPECIFIER_EXTENSIONS, so it can name one file per extension and neither a subtree nor a stem sibling. Measured through hintCovers and nothing else, over 176 families x 829 distinct hints x 7129 tracked files: 83775 -> 83830 (gate, file) pairs (+55, zero lost), 18 families gaining coverage and zero losing, unreachable families 11 -> 2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
os-zhuang
commented
Aug 27, 2026
复核:ACCEPT —— 但 ⛔ 尚未武装,CI 还在跑
人工地板:不成立,本席可裁这是 MATCHED 列的加宽——每张卡从此被告知更多它欠的门禁,是严格更多的覆盖,⛔ 不是削弱。没有降阈值、没有删必需 check、没有抬 ratchet 上限、没有 skip 任何测试; 生产改动实测:五行我不看 diffstat 判断范围,直接读了 hunk。 MODULE_SPECIFIER_EXTENSIONS.some((ext)=>inputPath===plain+ext)等值,不是前缀——注释里写明了理由:掉扩展名只可能指向说明符解析到的那一个文件。而且用的是 #12780 落下的共享常量,⛔ 不是第二份拷贝。第一个 hunk 的另外 ~100 行全是解释代价的 docblock。⭐ 一个改一个函数五行、把一百行用来说明代价的 PR,是这条车道该有的样子。 四棱
⭐⭐ 三处我特别要记名的
|
Uh oh!
There was an error while loading. Please reload this page.
Closes#12514
hintCoverscompares whole path segments, and an ESM/TypeScript relative importspells its target without the extension. So
check-spec-changes.tsimporting../src/migrations/registryyields the hintpackages/spec/src/migrations/registry— correct, resolved against the writingscript — while the tree holds
…/registry.ts. The hint missed the file it namesby exactly its extension.
Nine
packages/specfamilies were unreachable entirely for that reason, sono path derivation could ever name them: a dev editing
packages/spec/src/migrations/registry.tswas never told they owedcheck:spec-changes. Silent under-derivation, exit 0 — measured four times inone day on this board as red CI after a dev had run a complete-looking union
(
check:api-surfaceon #12585,check:docson #12591,check:adr-anchorson#12652). PR #12780 repaired the residue printer only;
hintCoverswas untouchedthere, and this is the remaining half.
Where the extension is followed: COMPARISON, not extraction — chosen deliberately
The other place to follow it is
extractWatchHints, emitting…/registry.tsasthe hint. Refused on three counts, and what that choice costs is stated rather
than left implicit:
import
../src/migrations/registry, extensionless.resolveModuleRelativeHint'sown docblock calls resolving-rather-than-forgiving the contract-first repair;
"which file does this specifier mean" is a different question, and the
comparison is where it is asked.
extractWatchHintsis a pure string function over one script's source.Following an extension there needs the tracked-file set, which couples
extraction to a git checkout and hands the file a second answer to a question
extensionlessModuleTargetalready owns.residue rows fix(pm): stop the residue calling an extensionless module specifier a layout move #12780 landed the day before, for no gain in the column this card
is about.
What extraction-side would have bought and this does not:
deepestTrackedPrefixand the escapable-literal rows still see the extensionless spelling. That is
deliberate — those readers describe what the author wrote, and they are correct
about it.
Pair counts, both directions, driven through
hintCoversand nothing else176 families × 829 distinct hints × 7129 tracked files, at base
ead731756.Counted by importing
discoverFamilies,trackedFilesandhintCoversfromthe module under test — not through the residue printer, not through a
hand-rolled regex.
Per-family matched-file maps were diffed file by file: zero pairs disappear,
and the ablated map is byte-identical to the base map. Each newly live
(check, hint) pair contributes exactly one file, which is why the two
+55figures coincide — the rule is an equality against one derived name.
The specimen the card was filed for, end to end through the real tool:
That path goes from 18 matched families to 21.
Ablation, proved on disk
The extension disjunct was removed from
hintCoverson top of the committedimplementation, under an
EXIT INT TERMtrap with absolute paths.e67d5024…equals the HEAD blob (measuring theright tree, proved before mutating, not assumed);
grep -c1 before / 0after, the injected marker 0 before / 1 after, worktree hash moved to
be023885…. A zero-hit edit would have failed the run rather than reading asa clean ablation;
this PR adds or repairs go red;
git checkout HEAD -- ABSOLUTE_PATH,then
git diff HEADempty, worktree hash back toe67d5024…, marker back to1 and injected text back to 0.
No build step is involved:
scripts/pm/dispatch-gates.mjsis plain ESM readdirectly by node, with no
dist/artifact between the edit and the run, sothere is no stale-artifact leg to prove.
The
.test.tstrap, re-measured here rather than taken on trustA rule accepting any suffix in the same directory selects the same 38 hints
(so the narrowing costs no lead) and names a
.test.tssibling for 4 ofthem —
protocol-version,metadata-type-schemas,react-blocks,manifest-collection-spelling. All 38 resolve through.ts.That trade was decided in #12780 for one sentence in a listing. It is now
load-bearing in a second place: the same list decides which files the MATCHED
column names, so the loose rule would put a gate's test sibling into dispatch
prompts. A new case pins that the matcher reaches the module and not the
test sibling for every one of the four.
The boundary with the
content/docsvscontent/docs.site.jsonrefusal (#8534)holds for the same reason:
.site.jsonis not a module extension. Pinned againhere, because this is the card that would have taken it back.
Provenance: 39 added equalities, zero fabricated
38 are the module specifiers above, each naming the file its own gate imports.
The 39th is
scripts/adr-anchorsvsscripts/adr-anchors.mjs— the only hintin the fleet whose collapsed form is a tracked directory that also has a
module-extension sibling. Checked at the declaration site rather than assumed:
scripts/check-adr-anchors.mjs:239imports./adr-anchors.mjs, and that gate'sown header sends a reader editing the anchor layout to that module's header
first. A true lead.
The fabrication direction — the acceptance requirement re-homed from #12299
Re-measured on this base, not cited: 41 distinct top-level roots sit one
directory away from converting 259 inert hints into MATCHED pairs (
srcgates 20 of them,
data9). The number matches the PM's figure independently.The guard is three cases. The mechanism is shown with a synthetic pair
(
hintCovers('src/kernel/protocol-version', 'src/kernel/protocol-version.ts')is true — the follow is root-agnostic), the derived root set is asserted absent
from the tree with a failure message telling the reader what just happened, and
src/dataare held out by name so the guard cannot evaporate if the derivedset empties for an unrelated reason. A repo that grows a top-level
src/redsthis case instead of quietly minting pairs into prompts.
One structural consequence, stated rather than buried
The residue's extensionless-module reason is no longer reachable from the live
sweep for a hint that carries a separator — such a hint is MATCHED now, so it
never enters
deadto be described. #12780'sextensionlessModuleTargetandits rendered sentence are not touched; the fixture case that used to reach
them through the sweep now asserts the arrival (the family is no longer
unreachable) and the renderer stays pinned by handing it the same
deadentrydirectly. The sweep fixture's unreachable count moves 4 to 3 for that reason,
and the listing-heading pin moves with it.
A departure pin cannot see an arrival — every new case here pins where a hint
lands.
Verification
All at the final head
ab51cccfe, each read from the gate's own printedverdict line (exit codes captured after a redirect, never across a pipe).
check:pm-dispatch-gatesdispatch-gates self-test: 752 cases pass.dispatch-gates.mjs --self-test752 cases pass.(738 before this PR)check:self-test-wired--self-testare all run by CIcheck:agent-test-spellingcheck:bash32-floor130 cases pass· 22 tracked shell filescheck:cli-command-ids39 cases pass· 287 literals resolvecheck:cross-package-test-inputsAll 117 self-test cases passed· OKcheck:entry-guard54 cases pass· 170 filescheck:parse-guard46 cases pass· 169 filescheck:pnpm-filter-targets54+40assertions · 140/177 resolvecheck:nul-bytes75 assertions· 7122 files, no raw control bytesscripts/check-ci-filter-parity.mjsOK: all 109 declared cross-package glob(s)…scripts/check-cross-package-test-inputs.mjsOK: 20 package(s)…scripts/pm/bare-root-worklist.mjs --self-testOK self-test: 46 live row(s)…pnpm lint(whole repo,--no-inline-config)The union was re-derived from the real change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackat thishead; it names the same 11 families the dispatch did and adds none.
Third instrument, beyond the two the dispatch named:
check:entry-guardandcheck:parse-guardboth derive a scan surface that explicitly includes thisfile ("plus the dispatch-gates scan surface, derived from the walked root"), so
either can fail on an edit here. Both green.
Scope
One file, one function, and that function's tests. Nothing from the queue held
behind this card (#12470 · #12411 · #12410 · #12500 · #12749) is touched.
scripts/**publishes nothing, so this takes theskip-changesetlabel ratherthan a changeset.
Generated by Claude Code