Skip to content

fix(pm): stop the residue calling an extensionless module specifier a layout move - #12780

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-12299-residue-recount
Aug 27, 2026
Merged

fix(pm): stop the residue calling an extensionless module specifier a layout move#12780
os-zhuang merged 2 commits into
mainfrom
claude/issue-12299-residue-recount

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#12299

Option B of the #12371 fork, sized to what the recount found rather than to what
the card described.

The measurement came first, and it disagrees with the dispatch — in the card's favour

Recounted on origin/main @ 1246b4cf2 (176 families, 7125 tracked files, 11
unreachable), using the tool's own exported functions rather than a second
implementation of them.

On the question as asked, the dispatch's relayed number is exactly right and
the answer is zero.
Families still printing "never was a repo path": 2 of
11
check:driver-memory-census and scripts/check-test-completeness.mjs.
Every one of their dead hints is a bare package specifier (@objectstack/types,
@objectstack/driver-memory, ...). Zero of them resolve to anything. That is
the honest class (c), and by the dispatch's own stated criterion the card was
done.

It is not done, because the false statement moved instead of leaving. Ruling
A resolved the module-relative literal against its writing script — correctly,
and it stays. What it also did was carry those nine families out of one wrong
sentence into another, and out of the inert bucket into the actionable one:

 before A after A
families printing a false reason 9 of 11 9 of 11
...filed BY CONSTRUCTION (inert) 9 0
...filed LAYOUT MOVED (triage me) 0 9

The mechanism is one missing extension. hintCovers compares whole segments and
an ESM/TypeScript relative import drops its extension, so the resolved hint
packages/spec/scripts/lib/dist-freshness misses the tracked-prefix set by
exactly .ts while the tree holds
packages/spec/scripts/lib/dist-freshness.ts. deepestTrackedPrefix therefore
stops one segment short, at packages/spec/scripts/lib, and unreachableClass
reads a short prefix as a move. All nine families were being advertised under
"THE LAYOUT MOVED under a gate that still spells the old path — a real miss,
worth triaging"
about directories nothing moved out of.

globInNonFinalSegment's docblock already calls that row "the worst row this
output can print"
, and #12329 landed partly to take layout moved from 1 to 0.
It came back at 9.

How it landed green. Ruling A shipped this pin:

t('a resolved dead hint has a tracked prefix, so the residue stops calling it "never a repo path"',Boolean(deepestTrackedPrefix('packages/spec/scripts/lib/dist-freshness', ...)),);

It asserts only that the hint LEFT the "never" branch. Leaving that branch is
precisely what puts a hint into the "layout moved" one, so the pin was green for
an arrival it never looked at. A departure pin cannot see an arrival. It is
widened here to assert both ends on the same live specimen.

What this changes

Residue-side only. No verdict moves: the hints stay dead, the families stay
unreachable, hintCovers is untouched, and the MATCHED column is byte-identical.

  • extensionlessModuleTarget — names the tracked file a dead hint means when the
    hint is a module specifier with its extension dropped. It refuses a hint the
    tree already HAS as a path, so the "too generic" case keeps its own, more
    useful message (measured: the two cases never overlap on this tree).
  • unreachableReason — a fourth case, checked first because it is the strongest
    statement available: the sweep knows the actual file, while every other branch
    reasons from a prefix and for this shape every one of them lands on something
    false.
  • unreachableClass — such a hint no longer votes for layout moved.

Result on this tree: layout moved9 to 1, and the survivor
(check:entry-nameability) is honest — it keeps
packages/node_modules/typescript/bin/tsc and
packages/spec/scripts/dist/other.d.ts, hints that genuinely name nothing
tracked. The narrowing is exactly as narrow as it claims.

The extension list is priced, not guessed

MODULE_SPECIFIER_EXTENSIONS is compared against the obvious alternative — "any
suffix in the same directory" — over the live fleet (829 distinct hints, 385
inert). Both rules select the same 38 hints, so the narrowing costs no lead.
They name a different file for 4 of them, because a test sibling sorts first:

packages/spec/src/kernel/protocol-version.test.ts the loose rule picks THIS
packages/spec/src/kernel/protocol-version.ts what the import actually means

(likewise metadata-type-schemas, react-blocks,
manifest-collection-spelling.) Reporting a gate's test sibling as "the file
this specifier means" would be a new false sentence in place of the old one —
the exact failure this repair exists to undo. Both halves are pinned, and so is
the live disagreement that justifies the narrowing, with a re-point instruction
rather than a delete instruction if those test files ever go.

Verification — all on the final commit dea0c190d

Gate union re-derived for the ACTUAL diff with node scripts/pm/dispatch-gates.mjs
(no paths, merge-base semantics); it returned exactly the dispatch's list, no
additions. No STALE TREE warning. Every exit code captured before any pipe.

check:nul-bytes check:agent-test-spelling check:bash32-floor check:cli-command-ids
check:cross-package-test-inputs check:entry-guard check:parse-guard
check:pnpm-filter-targets check:pm-dispatch-gates
scripts/check-ci-filter-parity.mjs scripts/check-cross-package-test-inputs.mjs
scripts/check-self-test-wired.mjs
scripts/pm/bare-root-worklist.mjs --self-test all exit=0

Verdict lines quoted from the gates themselves:

  • dispatch-gates self-test: 736 cases pass. — 2 of my new pins failed on the
    first run and both were real findings: one caught the .test.ts sibling above,
    the other a fixture-count pin my new sweep family moved.
  • check-self-test-wired: every one of the 140 script(s) CI runs that ship a --self-test has that self-test run by CI.
  • bare-root-worklist OK self-test: 46 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing, none contradicted.
  • check-nul-bytes: OK (scanned 7118 text file(s) ... no raw ASCII control bytes).

Repo-wide pnpm lint is narrowed, and the narrowing is declared rather than
skipped.
(1) The population is read from eslint's own config, which states —
measured there with a positive control — that this repo runs one
eslint.config.mjs that never enables type-aware linting for ANY file. (2)
--format json reports 1 file linted, 0 errors, 0 warnings. (3) With no
type-aware linting and a one-file diff, this change cannot move the verdict on
any untouched file. CI runs the full farm regardless.

skip-changeset: internal PM tooling, in no published package; this publishes
nothing.

Generated by Claude Code


Generated by Claude Code

… as a layout move
`hintCovers` compares whole segments and an ESM/TS relative import drops its
extension, so a resolved hint like `packages/spec/scripts/lib/dist-freshness`
misses the tracked-prefix set by exactly `.ts`. `deepestTrackedPrefix` then
stops one segment short and `unreachableClass` filed the family under "THE
LAYOUT MOVED ... a real miss, worth triaging" -- about a directory nothing
moved out of, for a file sitting right there.
Residue-side only: `extensionlessModuleTarget` names the file the tree really
has, `unreachableReason` says so instead, and such a hint no longer votes for
the layout-moved class. No verdict moves; `hintCovers` is untouched.
Measured on 1246b4c: unreachable families 11 (unchanged), "layout moved"
9 -> 1, and the survivor keeps hints that genuinely name nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
…nsion narrowing
The measured reason the extension list is explicit rather than "any suffix":
over the live fleet both rules select the same 38 hints, but the loose one
names a `.test.ts` sibling for 4 of them -- a new false sentence in place of
the old one. Pinned as a cost-free narrowing plus the live disagreement that
justifies it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 27, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 19:26
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 19:26
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit a70f6d2Aug 27, 2026
34 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12299-residue-recount branch August 27, 2026 19:49
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-zhuang@claude