Context
The repo currently has three relative-import specifier styles. One is forced by a hard constraint; the other two are an arbitrary split that should be one style.
| Area |
tsconfig |
Style today |
Forced? |
src/** (456 files) |
root — moduleResolution: "Bundler" |
extensionless (from "../db/repositories") |
idiomatic for Bundler |
scripts/** (29 files, 43 import lines) |
the same root tsconfig |
.js (from "./backtest-corpus-export-core.js") |
no |
packages/loopover-engine/** |
own tsconfig — moduleResolution: "NodeNext", published package (main: dist/index.js, exports) |
.js |
yes — cannot change |
src/** and scripts/** compile under the same tsconfig and the same Bundler resolution, yet disagree. There is no technical reason for the split:
- Bundler resolution accepts both forms, so
.js in scripts/** is stylistic, not required.
scripts/** runs under tsx (see .github/workflows/calibration-advisory.yml), and tsx resolves extensionless imports — verified directly:
$ node_modules/.bin/tsx /tmp/exttest/main.ts # main.ts: import { hi } from "./core"
extensionless under tsx: works
The engine package is genuinely different and must stay on .js: it is a published package emitting real ESM, and Node's ESM resolver requires the emitted extension in the specifier. That is a hard constraint of shipping ESM, not a preference — so "one style everywhere" is not achievable, but "one style per resolution zone" is, and today it is violated in exactly one place.
Note that .ts specifiers are not an option in either zone: the root tsconfig does not set allowImportingTsExtensions, so from "./x.ts" fails typecheck with TS5097. A repo-wide grep confirms zero .ts specifiers exist today.
Requirements
- Convert all 43 relative-import specifiers across the 29
scripts/** files from ./foo.js to ./foo (extensionless), matching src/**.
- Mechanical only. No logic, behavior, export-surface, or formatting change of any other kind. Imports from
@loopover/engine and other packages are untouched (they are package specifiers, not relative paths).
- Leave
packages/loopover-engine/** (and any other NodeNext-resolved published package) on .js — changing it breaks the published ESM output.
- Add a drift guard so the split cannot silently return: a check that fails when a relative import inside
src/** or scripts/** carries a .js (or .ts) extension. Wire it into test:ci alongside the existing drift checks.
npm run typecheck green, and every scripts/**-invoking workflow still runs (calibration-advisory.yml is the main one; the CLIs are also spawned by test/unit/*-e2e suites).
Deliverables
- The 29-file conversion in one PR.
- The drift guard + its own unit test (both arms: a conforming import passes, an extensioned relative import fails).
Expected outcome
Two resolution zones, each internally uniform: bundler-resolved app code (src/**, scripts/**) is extensionless; published NodeNext packages keep .js because they must. A new file in either zone has exactly one correct style, and CI enforces it.
Out of scope
Changing moduleResolution anywhere; touching packages/** specifiers; converting apps/**.
Context
The repo currently has three relative-import specifier styles. One is forced by a hard constraint; the other two are an arbitrary split that should be one style.
src/**(456 files)moduleResolution: "Bundler"from "../db/repositories")scripts/**(29 files, 43 import lines).js(from "./backtest-corpus-export-core.js")packages/loopover-engine/**moduleResolution: "NodeNext", published package (main: dist/index.js,exports).jssrc/**andscripts/**compile under the same tsconfig and the same Bundler resolution, yet disagree. There is no technical reason for the split:.jsinscripts/**is stylistic, not required.scripts/**runs undertsx(see.github/workflows/calibration-advisory.yml), and tsx resolves extensionless imports — verified directly:The engine package is genuinely different and must stay on
.js: it is a published package emitting real ESM, and Node's ESM resolver requires the emitted extension in the specifier. That is a hard constraint of shipping ESM, not a preference — so "one style everywhere" is not achievable, but "one style per resolution zone" is, and today it is violated in exactly one place.Note that
.tsspecifiers are not an option in either zone: the root tsconfig does not setallowImportingTsExtensions, sofrom "./x.ts"fails typecheck withTS5097. A repo-wide grep confirms zero.tsspecifiers exist today.Requirements
scripts/**files from./foo.jsto./foo(extensionless), matchingsrc/**.@loopover/engineand other packages are untouched (they are package specifiers, not relative paths).packages/loopover-engine/**(and any other NodeNext-resolved published package) on.js— changing it breaks the published ESM output.src/**orscripts/**carries a.js(or.ts) extension. Wire it intotest:cialongside the existing drift checks.npm run typecheckgreen, and everyscripts/**-invoking workflow still runs (calibration-advisory.ymlis the main one; the CLIs are also spawned bytest/unit/*-e2esuites).Deliverables
Expected outcome
Two resolution zones, each internally uniform: bundler-resolved app code (
src/**,scripts/**) is extensionless; published NodeNext packages keep.jsbecause they must. A new file in either zone has exactly one correct style, and CI enforces it.Out of scope
Changing
moduleResolutionanywhere; touchingpackages/**specifiers; convertingapps/**.