Skip to content

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 ​

FileChange
package.jsonNew. type: module, private: true, engines copied from docs/integration/hosts.md ({"node": ">=22"}), four scripts, six exact devDependencies.
package-lock.jsonNew. Generated by npm install, 102 packages, committed-ready.
tsconfig.jsonNew. Strict ESM config (see choices below).
eslint.config.jsNew. Flat config with typescript-eslint, no-empty, and the ts-comment ban (see choices below).
.gitignoreNew. node_modules/, dist/, coverage/, *.tsbuildinfo, .env, .env.* (with .env.example allowed).
.planning/phases/01-scaffold-and-frozen-contracts/01-03-SUMMARY.mdThis 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:

NameVersionLicenseRegistry engines
typescript6.0.3Apache-2.0>=14.17
eslint10.13.0MIT^20.19.0 || ^22.13.0 || >=24
@eslint/js10.0.1MIT^20.19.0 || ^22.13.0 || >=24
typescript-eslint8.72.0MIT^18.18.0 || ^20.9.0 || >=21.1.0
@types/node22.20.5MITnone declared
ajv8.20.0MITnone 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.0

Result: 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 ​

  • target and lib: ES2023.
  • module and moduleResolution: NodeNext (ESM, .js extensions required in relative imports later).
  • types: ["node"] explicitly. TypeScript 6 defaults types to empty, so node:test types need this.
  • Strictness beyond strict: noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noFallthroughCasesInSwitch, noUnusedLocals, noUnusedParameters, noImplicitReturns, verbatimModuleSyntax, forceConsistentCasingInFileNames.
  • skipLibCheck is not set, so declaration-file errors are not hidden.
  • rootDir: ".", outDir: "dist", sourceMap: true. rootDir is "." so that tests under test/ and source under src/ both compile to dist/.
  • allowJs and checkJs: 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.recommended and tseslint.configs.strict (non-type-aware, so probe files outside the tsconfig include still lint).
  • no-empty set to error with allowEmptyCatch: false for **/*.ts (QUAL-01).
  • @typescript-eslint/ban-ts-comment set to error with ts-expect-error, ts-ignore, and ts-nocheck all true. This is stricter than D-08, which bans only blanket @ts-expect-error. Note for suppression grep: the literal strings ts-expect-error, ts-ignore, and ts-nocheck appear in eslint.config.js as 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:

ScriptCommandExit
typechecknpm run typecheck (tsc -p tsconfig.json --noEmit)0
lintnpm run lint (eslint .)0 (ESLint: No issues found)
testnpm test (tsc -p tsconfig.json && node --test "dist/test/**/*.test.js")0 (tests 0, fail 0, skipped 0, todo 0)
buildnpm 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 lint with the probe present: exit 1, 4:11 error Empty block statement no-empty.
  • Probe removed, npm run lint again: exit 0.
  • qual01-probe.ts confirmed absent afterwards.

Verification checks ​

  • src/ and test/ do not exist. git status shows only the new root files plus pre-existing untracked planning and docs files.
  • Suppression grep over the repo's own files, excluding node_modules and .planning: the only matches are the three rule keys in eslint.config.js described above. No @ts-ignore, eslint-disable, skip, or todo appears in any created file.
  • No host-local absolute paths (/root, /home, /Users) in package.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) ​

  1. Engines gap for ESLint 10. Node 22.0 to 22.12 do not satisfy eslint 10 or @eslint/js 10. Decision: engines stays ">=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.
  2. ajv in devDependencies. The approved list and the acceptance criteria place ajv in devDependencies. Plan 01-05 imports it at runtime from src/contract/validate.ts, so it should move to dependencies when that module lands. Default applied: kept in devDependencies now, per the plan.
  3. tsconfig include on the empty tree. Default applied: eslint.config.js as the placeholder input, with checkJs, as described above. Plan 01-04 should confirm its src/ files compile under this config.
  4. dist/ contents. npm run build currently emits dist/eslint.config.js. Default applied: accepted; dist/ is ignored.

AGPL-3.0-only. Copyright (C) 2026 Spectrum Web Co/swcstudiospace.