diff --git a/docs/superpowers/plans/2026-04-14-phase-1a1-skeleton.md b/docs/superpowers/plans/2026-04-14-phase-1a1-skeleton.md index 30725d70..56571541 100644 --- a/docs/superpowers/plans/2026-04-14-phase-1a1-skeleton.md +++ b/docs/superpowers/plans/2026-04-14-phase-1a1-skeleton.md @@ -4,7 +4,7 @@ **Goal:** Bootstrap the empty Modern.js React project skeleton at the repo root with a typechecked, lintable, testable `src/` tree, a Zod-validated env module, a frozen-public-barrel rule for the four feature modules, and the `HostContract` type — producing a baseline that every downstream Phase 1 sub-plan builds on. -**Architecture:** The new React project lives at the **repo root**, alongside the existing ASP.NET files and the Angular `ClientApp/` (both untouched). The root gains a `package.json`, `tsconfig.json`, `.eslintrc.cjs`, `vitest.config.ts`, and an empty `src/` tree. Fate of the ASP.NET host is Phase 0 assumption **A5** (hard blocker resolved before 1A-1 starts); regardless of A5, 1A-1 never deletes or modifies ASP.NET or `ClientApp/` files. +**Architecture:** The new React project lives at the **repo root**, alongside the existing ASP.NET files and the Angular `ClientApp/` (both untouched). The root gains a `package.json`, `tsconfig.json`, `eslint.config.js` (flat config, ESLint 9), `vitest.config.ts`, and an empty `src/` tree. Fate of the ASP.NET host is Phase 0 assumption **A5** (hard blocker resolved before 1A-1 starts); regardless of A5, 1A-1 never deletes or modifies ASP.NET or `ClientApp/` files. **Tech Stack:** Node 24, pnpm, TypeScript 5 strict, Vitest, ESLint 9 (flat config compatible), Zod for runtime env validation. @@ -21,7 +21,7 @@ 1. `.nvmrc` pinned to Node 24. 2. Root `package.json` with scripts `dev`, `build:standalone`, `build:remote`, `build:both`, `test`, `test:coverage`, `lint`, `typecheck`. (Non-build scripts are stubs until 1A-2 wires Modern.js.) 3. `tsconfig.json` — strict, `noUncheckedIndexedAccess`, `isolatedModules`, path aliases `@/*` → `src/*`, `@phase0/*` → `scripts/phase-0/*`. -4. `.eslintrc.cjs` baseline (no boundary rules yet). +4. `eslint.config.js` baseline flat config (no boundary rules yet). ESLint 9 requires flat config natively. 5. `vitest.config.ts` wired to the `@/` alias. 6. `src/` directory with: - `src/env/index.ts` — Zod-validated `Env` reader (and its test). @@ -47,7 +47,7 @@ Files created or modified by this plan, in order of appearance: | `package.json` | Root workspace manifest + scripts + deps | 1, 2 | | `pnpm-lock.yaml` | pnpm lockfile (generated) | 2 | | `tsconfig.json` | TypeScript strict config + path aliases | 3 | -| `.eslintrc.cjs` | Baseline ESLint config (no boundaries yet) | 4 | +| `eslint.config.js` | Baseline ESLint 9 flat config (no boundaries yet) | 4 | | `vitest.config.ts` | Vitest runner config with `@/` alias | 5 | | `src/observability/logger/types.ts` | Type-only `Logger` surface | 6 | | `src/observability/analytics/types.ts` | Type-only `AnalyticsProviders` + friends | 7 | @@ -224,77 +224,90 @@ Expected: exits `0` with no output, OR errors `error TS18003: No inputs were fou If TS18003 fires, create an empty placeholder so it goes away: ```bash mkdir -p src -echo "export {};" > src/.typecheck-placeholder.ts +echo "export {};" > src/typecheck-placeholder.ts pnpm typecheck ``` Expected: exits `0` silently. +**Note on the filename.** Use `src/typecheck-placeholder.ts`, NOT `src/.typecheck-placeholder.ts` — TypeScript's glob matcher skips dotfiles, so a dot-prefixed name won't be picked up by the `include: ["src/**/*.ts"]` pattern and the placeholder won't suppress TS18003. + - [ ] **Step 3: Commit** ```bash git add tsconfig.json -git add src/.typecheck-placeholder.ts 2>/dev/null || true +git add src/typecheck-placeholder.ts 2>/dev/null || true git commit -m "Add strict TypeScript config with @/ and @phase0/ aliases" ``` --- -## Task 4 — Create baseline `.eslintrc.cjs` +## Task 4 — Create baseline `eslint.config.js` (ESLint 9 flat config) **Files:** -- Create: `.eslintrc.cjs` -- Create: `.eslintignore` +- Create: `eslint.config.js` -- [ ] **Step 1: Write `.eslintrc.cjs`** +**Note on format.** ESLint 9 requires flat config natively — legacy `.eslintrc.cjs` is not supported without a compat shim, and shims produce deprecation warnings. This task ships flat config directly. Boundary rules (`eslint-plugin-boundaries`) and `no-restricted-imports` land in **1A-3**. -Baseline only. Layered dependency rules (`eslint-plugin-boundaries`) and `no-restricted-imports` come in **1A-3**. +- [ ] **Step 1: Install flat-config helper packages** + +```bash +pnpm add -D @eslint/js@^9.0.0 typescript-eslint@^8.0.0 +``` + +The `typescript-eslint` meta-package is the recommended flat-config entrypoint — it re-exports the parser and plugin as flat-config-ready objects. `@eslint/js` provides `js.configs.recommended`. + +- [ ] **Step 2: Write `eslint.config.js`** ```javascript -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: 2023, - sourceType: "module", - project: "./tsconfig.json", - ecmaFeatures: { jsx: true }, - }, - plugins: ["@typescript-eslint", "unused-imports"], - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - ], - rules: { - "@typescript-eslint/no-unused-vars": "off", - "unused-imports/no-unused-imports": "error", - "unused-imports/no-unused-vars": [ - "warn", - { - vars: "all", - varsIgnorePattern: "^_", - args: "after-used", - argsIgnorePattern: "^_", - }, +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import unusedImports from "eslint-plugin-unused-imports"; + +export default [ + { + ignores: [ + "dist/**", + "node_modules/**", + "ClientApp/**", + "wwwroot/**", + "**/*.cjs", + "pnpm-lock.yaml", ], - "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }], - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-non-null-assertion": "warn", - "no-console": ["warn", { allow: ["warn", "error"] }], }, - ignorePatterns: ["dist/", "node_modules/", "ClientApp/", "wwwroot/", "*.cjs"], -}; -``` - -- [ ] **Step 2: Write `.eslintignore`** - -``` -dist/ -node_modules/ -ClientApp/ -wwwroot/ -*.cjs -pnpm-lock.yaml + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["src/**/*.{ts,tsx}"], + languageOptions: { + parserOptions: { + ecmaVersion: 2023, + sourceType: "module", + project: "./tsconfig.json", + ecmaFeatures: { jsx: true }, + }, + }, + plugins: { + "unused-imports": unusedImports, + }, + rules: { + "@typescript-eslint/no-unused-vars": "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "warn", + { + vars: "all", + varsIgnorePattern: "^_", + args: "after-used", + argsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }], + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-non-null-assertion": "warn", + "no-console": ["warn", { allow: ["warn", "error"] }], + }, + }, +]; ``` - [ ] **Step 3: Run lint** @@ -302,13 +315,29 @@ pnpm-lock.yaml ```bash pnpm lint ``` -Expected: either `0` errors / `0` warnings with no files to lint, OR a message like "No files matching the pattern 'src/**/*.{ts,tsx}' were found." Both are acceptable — the point is that the config parses and the rule set loads without error. +Expected: exit `0` with no errors, no warnings, and no deprecation notices. If ESLint reports "no files matching the pattern" that's also acceptable — the point is the config parses and rules load without error. -- [ ] **Step 4: Commit** +- [ ] **Step 4: Smoke-test that rules actually fire** + +Create a temporary probe file to confirm `no-explicit-any` fires: ```bash -git add .eslintrc.cjs .eslintignore -git commit -m "Add baseline ESLint config (no boundary rules yet)" +echo "const x: any = 1; export default x;" > src/lint-probe.ts +pnpm lint +``` +Expected: a warning on `any`. Delete the probe and re-run lint: + +```bash +rm src/lint-probe.ts +pnpm lint +``` +Expected: exit `0`. + +- [ ] **Step 5: Commit** + +```bash +git add eslint.config.js package.json pnpm-lock.yaml +git commit -m "Add baseline ESLint 9 flat config (no boundary rules yet)" ``` --- @@ -715,15 +744,16 @@ export function getEnv(): Env { } const raw = parsed.data; - cached = { + // Build with required fields first; the three optional URL/header + // fields are assigned only when defined. Under the tsconfig's + // `exactOptionalPropertyTypes: true`, assigning `undefined` to an + // optional property is a type error — hence the conditional shape. + const result: Env = { NODE_ENV: raw.NODE_ENV, BUILD_TARGET: raw.BUILD_TARGET, PROD_ORIGIN: raw.PROD_ORIGIN, API_BASE_URL: raw.API_BASE_URL, SIGNALR_HUB_URL: raw.SIGNALR_HUB_URL, - OTEL_EXPORTER_OTLP_ENDPOINT: raw.OTEL_EXPORTER_OTLP_ENDPOINT, - OTEL_EXPORTER_OTLP_HEADERS: raw.OTEL_EXPORTER_OTLP_HEADERS, - LOGS_ENDPOINT: raw.LOGS_ENDPOINT, ANALYTICS_ENABLED: { metrica: raw.ANALYTICS_METRICA, ctm: raw.ANALYTICS_CTM, @@ -732,7 +762,17 @@ export function getEnv(): Env { }, VERSION: raw.VERSION, }; - return cached; + if (raw.OTEL_EXPORTER_OTLP_ENDPOINT !== undefined) { + result.OTEL_EXPORTER_OTLP_ENDPOINT = raw.OTEL_EXPORTER_OTLP_ENDPOINT; + } + if (raw.OTEL_EXPORTER_OTLP_HEADERS !== undefined) { + result.OTEL_EXPORTER_OTLP_HEADERS = raw.OTEL_EXPORTER_OTLP_HEADERS; + } + if (raw.LOGS_ENDPOINT !== undefined) { + result.LOGS_ENDPOINT = raw.LOGS_ENDPOINT; + } + cached = result; + return result; } /** @@ -828,7 +868,7 @@ export {}; - [ ] **Step 2: Delete the placeholder from Task 3 if it exists** ```bash -rm -f src/.typecheck-placeholder.ts +rm -f src/typecheck-placeholder.ts ``` - [ ] **Step 3: Typecheck and lint** @@ -842,7 +882,7 @@ Expected: both exit `0`. ```bash git add src/features/ src/ui/ -git rm -f src/.typecheck-placeholder.ts 2>/dev/null || true +git rm -f src/typecheck-placeholder.ts 2>/dev/null || true git commit -m "Seed frozen public barrels for 4 features + UI adapter" ``` @@ -946,12 +986,17 @@ pnpm test ``` Expected: 7 env tests pass, no other test files. -- [ ] **Step 5: Coverage sanity check** +- [ ] **Step 5: Coverage sanity check — DEFERRED to 1B** + +`pnpm test:coverage` requires `@vitest/coverage-v8`, which is owned by **1B** per the master plan's shared-files table. Do NOT install it here — 1B lands the dep and validates coverage end-to-end. Skip this step during 1A-1 execution; the exit gate still holds because the next sub-plan (1B) covers it. + +If you want a smoke-check anyway, install the dep in a throwaway fashion, run, then remove — but this is optional and adds no value over 1B: ```bash -pnpm test:coverage +pnpm add -D @vitest/coverage-v8@^3.0.0 # temporary +pnpm test:coverage # should show src/env near 100% +pnpm remove @vitest/coverage-v8 # revert so 1B owns it ``` -Expected: exit `0`. `src/env/index.ts` reports near-100% coverage. Type-only files (`logger/types.ts`, `analytics/types.ts`, `host-contract.ts`) are excluded per `vitest.config.ts`, so they do not appear in the coverage report. - [ ] **Step 6: Verify the sub-plan directory listing matches the deliverables** @@ -965,7 +1010,7 @@ Expected output shape: - `src/observability/analytics/types.ts` - Four feature directories each with `index.ts` - `src/ui/index.ts` -- No stray files (`.typecheck-placeholder.ts` must be gone). +- No stray files (`typecheck-placeholder.ts` must be gone). - [ ] **Step 7: Verify Phase 0 and other sub-plans are unaffected** @@ -989,7 +1034,7 @@ git status --porcelain | grep -q . && git commit -am "1A-1 exit gate verificatio **Spec coverage.** Every item in the master plan §1A-1 "Exports" bullet list maps to a task: - src/ layout (§1.3) → Task 5 (vitest.config), Task 6–10 (files created), Task 10 (barrels) - `tsconfig.json` → Task 3 -- `.eslintrc.cjs` base → Task 4 +- `eslint.config.js` flat config baseline → Task 4 - `package.json` scripts → Task 1 - `package.json` dep ownership (`zod`) → Task 2 - `src/env/index.ts` Zod-validated → Task 9 (TDD)