Plan 01-03 summary: strict ESM scaffold and approved toolchain (FOUND-01, QUAL-01)
Status: complete. All four scripts exit 0 on the scaffold, the QUAL-01 probe was demonstrated and removed, and no suppression directive was introduced. No src/ or test/ directory was created. No commit, push, or fetch was performed.
Files changed
| File | Change |
|---|---|
package.json | New. type: module, private: true, engines copied from docs/integration/hosts.md ({"node": ">=22"}), four scripts, six exact devDependencies. |
package-lock.json | New. Generated by npm install, 102 packages, committed-ready. |
tsconfig.json | New. Strict ESM config (see choices below). |
eslint.config.js | New. Flat config with typescript-eslint, no-empty, and the ts-comment ban (see choices below). |
.gitignore | New. node_modules/, dist/, coverage/, *.tsbuildinfo, .env, .env.* (with .env.example allowed). |
.planning/phases/01-scaffold-and-frozen-contracts/01-03-SUMMARY.md | This file. |
Task 1: verified toolchain and approval
Approval was given in the dispatch context (Main, approved list). Before installing, each pin was re-checked against the npm registry with npm view <name>@<version> name version license engines on 2026-10-21. The registry matched the approved list:
| Name | Version | License | Registry engines |
|---|---|---|---|
| typescript | 6.0.3 | Apache-2.0 | >=14.17 |
| eslint | 10.13.0 | MIT | ^20.19.0 || ^22.13.0 || >=24 |
| @eslint/js | 10.0.1 | MIT | ^20.19.0 || ^22.13.0 || >=24 |
| typescript-eslint | 8.72.0 | MIT | ^18.18.0 || ^20.9.0 || >=21.1.0 |
| @types/node | 22.20.5 | MIT | none declared |
| ajv | 8.20.0 | MIT | none declared |
Install command, run once after approval:
npm install --save-dev --save-exact typescript@6.0.3 eslint@10.13.0 @eslint/js@10.0.1 typescript-eslint@8.72.0 @types/node@22.20.5 ajv@8.20.0Result: added 102 packages, exit 0. Exact versions as they appear in package.json devDependencies: @eslint/js 10.0.1, @types/node 22.20.5, ajv 8.20.0, eslint 10.13.0, typescript 6.0.3, typescript-eslint 8.72.0. No ^ or ~ prefixes.
Engines gap (known, not changed)
eslint 10.13.0 and @eslint/js 10.0.1 declare ^20.19.0 || ^22.13.0 || >=24. A Node 22 release below 22.13 satisfies engines: ">=22" but would not satisfy the two ESLint packages. Decision: engines stays ">=22" (copied verbatim from docs/integration/hosts.md) and is not raised. ESLint 10 needs Node ^22.13 for development tooling only, so this is recorded as a development requirement, not a runtime one. The local runtime used for verification is Node v24.18.0, which satisfies all ranges. Node 22.x was not exercised. See open question 1 below.
Task 2 choices
tsconfig.json
targetandlib: ES2023.moduleandmoduleResolution: NodeNext (ESM,.jsextensions required in relative imports later).types:["node"]explicitly. TypeScript 6 defaultstypesto empty, sonode:testtypes need this.- Strictness beyond
strict:noUncheckedIndexedAccess,exactOptionalPropertyTypes,noImplicitOverride,noFallthroughCasesInSwitch,noUnusedLocals,noUnusedParameters,noImplicitReturns,verbatimModuleSyntax,forceConsistentCasingInFileNames. skipLibCheckis not set, so declaration-file errors are not hidden.rootDir: ".",outDir: "dist",sourceMap: true.rootDiris"."so that tests undertest/and source undersrc/both compile todist/.allowJsandcheckJs: see the empty-tree issue below.
Empty-tree issue and the choice made. With include set to src/**/*.ts and test/**/*.ts and no such files present, TypeScript fails with TS18003: No inputs were found. Setting files: [] fails with TS18002. Neither form can pass on an empty tree. The applied choice: include lists eslint.config.js first, with allowJs and checkJs on, so typecheck checks a real file on the empty tree (it resolves the @eslint/js and typescript-eslint imports and the config types). The src/**/*.ts and test/**/*.ts globs remain in include. Plan 01-04 and 01-05 add their files under those globs and need no config change. Side effect: npm run build emits dist/eslint.config.js and its source map. dist/ is ignored by git.
eslint.config.js
- Flat config built with
tseslint.config(...). - Global ignores:
dist/**,coverage/**,node_modules/**. - Base presets:
js.configs.recommendedandtseslint.configs.strict(non-type-aware, so probe files outside the tsconfigincludestill lint). no-emptyset toerrorwithallowEmptyCatch: falsefor**/*.ts(QUAL-01).@typescript-eslint/ban-ts-commentset toerrorwithts-expect-error,ts-ignore, andts-nocheckalltrue. This is stricter than D-08, which bans only blanket@ts-expect-error. Note for suppression grep: the literal stringsts-expect-error,ts-ignore, andts-nocheckappear ineslint.config.jsas keys of this rule. They enforce the ban and are not suppressions.
Task 3: script results and QUAL-01 probe
Run on the final files with Node v24.18.0:
| Script | Command | Exit |
|---|---|---|
| typecheck | npm run typecheck (tsc -p tsconfig.json --noEmit) | 0 |
| lint | npm run lint (eslint .) | 0 (ESLint: No issues found) |
| test | npm test (tsc -p tsconfig.json && node --test "dist/test/**/*.test.js") | 0 (tests 0, fail 0, skipped 0, todo 0) |
| build | npm run build (tsc -p tsconfig.json) | 0 |
Test invocation choice: TypeScript is compiled with tsc first, then Node's built-in runner runs the compiled dist/test/**/*.test.js. Node 22 cannot execute .ts directly without a flag, and native type stripping does not cover every construct in strict mode. No bundler is used. Node's runner exits 0 when the glob matches no files, which is the empty-tree case here.
QUAL-01 probe: a temporary root-level qual01-probe.ts containing try { throw new Error("probe"); } catch {}.
npm run lintwith the probe present: exit 1,4:11 error Empty block statement no-empty.- Probe removed,
npm run lintagain: exit 0. qual01-probe.tsconfirmed absent afterwards.
Verification checks
src/andtest/do not exist.git statusshows only the new root files plus pre-existing untracked planning and docs files.- Suppression grep over the repo's own files, excluding
node_modulesand.planning: the only matches are the three rule keys ineslint.config.jsdescribed above. No@ts-ignore,eslint-disable,skip, ortodoappears in any created file. - No host-local absolute paths (
/root,/home,/Users) inpackage.json,package-lock.json,tsconfig.json,eslint.config.js, or.gitignore. - Phase 1 has no runtime async boundary. The timeout rule (D-08, QUAL-01 timeouts) applies from Phase 2 onward.
Open questions (default applied)
- Engines gap for ESLint 10. Node 22.0 to 22.12 do not satisfy eslint 10 or @eslint/js 10. Decision:
enginesstays">=22"per D-09 and is not raised. ESLint 10 needs Node ^22.13 for development tooling only; this is a development requirement, not a runtime one. ajvin devDependencies. The approved list and the acceptance criteria placeajvin devDependencies. Plan 01-05 imports it at runtime fromsrc/contract/validate.ts, so it should move todependencieswhen that module lands. Default applied: kept in devDependencies now, per the plan.tsconfiginclude on the empty tree. Default applied:eslint.config.jsas the placeholder input, withcheckJs, as described above. Plan 01-04 should confirm itssrc/files compile under this config.dist/contents.npm run buildcurrently emitsdist/eslint.config.js. Default applied: accepted;dist/is ignored.