chore(ts-migration): resolve source-text reads across .js/.jsx/.ts/.tsx

The check suite consumes source files through two entirely separate channels,
and Phase 0 only hardened one of them.

`withSourceResolution` wraps `ssrLoadModule`, which covers the 159 module loads.
It does not and cannot see the other 30 sites, which read source as TEXT through
`readFileSync` to assert structural facts - "the panel imports no local
suggestion ranker", "no runtime path writes a definition". Renaming
`base44Client.js` to `.ts` is what surfaced the difference: ENOENT in the middle
of a suite that had been passing, from a line no grep for `ssrLoadModule` would
ever have found.

`resolveSourcePath()` in `ssr-resolve.mjs` reuses the existing `candidatesFor()`
ordering - the written path first, then `.ts`, then `.tsx` - and returns a path
relative to the root, so every call site keeps its `join(ROOT, ...)` as it was.
An unresolvable path comes back unchanged, which keeps the two absence
assertions honest: a check proving `store.js` is GONE still asks about the path
it means, and now also notices if the file returns under another extension.

Thirty-seven lines change, each a one-for-one replacement. No assertion text, no
record() message, no ordering, no logic.

The audit found the reads in four shapes, and two of them a path grep cannot
see:

  - direct       readFileSync(join(ROOT, 'src/x.jsx'), 'utf8')
  - via a const  const P = join(ROOT, 'src/x.jsx')
  - dir + name   ['node.js', 'patch.js'].map((f) => readFileSync(join(ROOT, 'src/lib/ui', f)))
  - path array   for (const f of files) readFileSync(join(ROOT, f))

The third and fourth hide thirteen filenames in adjacent arrays, which is why
the first estimate of this work was twenty-two files and the real number is
thirty-five.

One directory scan also filtered `/\.jsx?$/` over `src/pages/admin` and
`src/components/agents`. That one does not crash - it quietly matches nothing
once those directories are TypeScript, and the check passes having inspected an
empty set. Widened to `/\.[jt]sx?$/`. A silent shrink is worse than a failure,
and CI's FLOOR of 900 would not have caught it.

Deliberately NOT touched, because they are correctly extension-specific: the
`dist/assets` filter reads built bundles, which are `.js` whatever the source
was; `scripts/stubs/react-hot-toast.js` and `scripts/browser-flows.js` are
scripts, not migrated source; and every comment that mentions a `.js` filename
says something true about a file that still has that name.

Proven rather than assumed. `npm test` reports 1684/1691 before and after, the
same seven failures. Then `src/api/base44Client.js` - the exact file whose
rename broke the suite - was renamed to `.ts`, the suite re-run, and the check
that reads it as text passed: "the app does not import the seed fixture". The
rename was reverted and the file verified byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
2026-09-17 22:56:45 +05:30
parent dca184289e
commit 02a2ab05ef
2 changed files with 65 additions and 37 deletions

View File

@@ -78,3 +78,31 @@ export function withSourceResolution(server, root = process.cwd()) {
return server;
}
/**
* The same resolution, for source read as TEXT rather than loaded as a module.
*
* `skill-check.mjs` asserts structural facts by reading source files and
* matching against their contents — "the panel imports no local suggestion
* ranker", "no runtime path writes a definition". Those reads go through
* `readFileSync`, not `ssrLoadModule`, so `withSourceResolution` above never
* sees them: it wraps the loader, and this is a second, entirely separate
* channel. Renaming `base44Client.js` to `.ts` is what surfaced the difference,
* as an ENOENT in the middle of a suite that had been passing.
*
* Takes and returns a path RELATIVE to the project root, so the call site keeps
* its `join(ROOT, …)` exactly as it was:
*
* readFileSync(join(ROOT, resolveSourcePath('src/api/base44Client.js')), 'utf8')
*
* Unresolvable paths come back unchanged, so the resulting error still names
* the file the caller asked for rather than a candidate it invented. That also
* keeps the deliberately absent ones honest: a check asserting a file is GONE
* gets the path it asked about, and `existsSync` still answers false.
*/
export function resolveSourcePath(path, root = process.cwd()) {
for (const candidate of candidatesFor(path)) {
if (existsSync(join(root, candidate))) return candidate;
}
return path;
}