Correct 1A-1 plan for 4 bugs found during execution

- Task 3: dotfile placeholder (src/.typecheck-placeholder.ts) is ignored
  by TypeScript's glob; use non-dotfile name.
- Task 4: replace legacy .eslintrc.cjs with ESLint 9 flat config.
  ESLint 9 requires flat config natively; the legacy format triggers
  deprecation warnings and needs a FlatCompat shim.
- Task 9: env impl cannot unconditionally assign undefined to optional
  fields under exactOptionalPropertyTypes; build base object then
  conditionally assign the three optional URL/header fields.
- Task 12 Step 5: defer coverage check to 1B, which owns the
  @vitest/coverage-v8 dep per the master plan shared-files table.
This commit is contained in:
2026-04-14 22:12:26 +03:00
parent 9d5898e8d5
commit 5b67aa25fa
@@ -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. **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. **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. 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.) 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/*`. 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. 5. `vitest.config.ts` wired to the `@/` alias.
6. `src/` directory with: 6. `src/` directory with:
- `src/env/index.ts` — Zod-validated `Env` reader (and its test). - `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 | | `package.json` | Root workspace manifest + scripts + deps | 1, 2 |
| `pnpm-lock.yaml` | pnpm lockfile (generated) | 2 | | `pnpm-lock.yaml` | pnpm lockfile (generated) | 2 |
| `tsconfig.json` | TypeScript strict config + path aliases | 3 | | `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 | | `vitest.config.ts` | Vitest runner config with `@/` alias | 5 |
| `src/observability/logger/types.ts` | Type-only `Logger` surface | 6 | | `src/observability/logger/types.ts` | Type-only `Logger` surface | 6 |
| `src/observability/analytics/types.ts` | Type-only `AnalyticsProviders` + friends | 7 | | `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: If TS18003 fires, create an empty placeholder so it goes away:
```bash ```bash
mkdir -p src mkdir -p src
echo "export {};" > src/.typecheck-placeholder.ts echo "export {};" > src/typecheck-placeholder.ts
pnpm typecheck pnpm typecheck
``` ```
Expected: exits `0` silently. 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** - [ ] **Step 3: Commit**
```bash ```bash
git add tsconfig.json 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" 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:** **Files:**
- Create: `.eslintrc.cjs` - Create: `eslint.config.js`
- Create: `.eslintignore`
- [ ] **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 ```javascript
/** @type {import('eslint').Linter.Config} */ import js from "@eslint/js";
module.exports = { import tseslint from "typescript-eslint";
root: true, import unusedImports from "eslint-plugin-unused-imports";
parser: "@typescript-eslint/parser",
parserOptions: { export default [
ecmaVersion: 2023, {
sourceType: "module", ignores: [
project: "./tsconfig.json", "dist/**",
ecmaFeatures: { jsx: true }, "node_modules/**",
}, "ClientApp/**",
plugins: ["@typescript-eslint", "unused-imports"], "wwwroot/**",
extends: [ "**/*.cjs",
"eslint:recommended", "pnpm-lock.yaml",
"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: "^_",
},
], ],
"@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"], js.configs.recommended,
}; ...tseslint.configs.recommended,
``` {
files: ["src/**/*.{ts,tsx}"],
- [ ] **Step 2: Write `.eslintignore`** languageOptions: {
parserOptions: {
``` ecmaVersion: 2023,
dist/ sourceType: "module",
node_modules/ project: "./tsconfig.json",
ClientApp/ ecmaFeatures: { jsx: true },
wwwroot/ },
*.cjs },
pnpm-lock.yaml 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** - [ ] **Step 3: Run lint**
@@ -302,13 +315,29 @@ pnpm-lock.yaml
```bash ```bash
pnpm lint 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 ```bash
git add .eslintrc.cjs .eslintignore echo "const x: any = 1; export default x;" > src/lint-probe.ts
git commit -m "Add baseline ESLint config (no boundary rules yet)" 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; 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, NODE_ENV: raw.NODE_ENV,
BUILD_TARGET: raw.BUILD_TARGET, BUILD_TARGET: raw.BUILD_TARGET,
PROD_ORIGIN: raw.PROD_ORIGIN, PROD_ORIGIN: raw.PROD_ORIGIN,
API_BASE_URL: raw.API_BASE_URL, API_BASE_URL: raw.API_BASE_URL,
SIGNALR_HUB_URL: raw.SIGNALR_HUB_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: { ANALYTICS_ENABLED: {
metrica: raw.ANALYTICS_METRICA, metrica: raw.ANALYTICS_METRICA,
ctm: raw.ANALYTICS_CTM, ctm: raw.ANALYTICS_CTM,
@@ -732,7 +762,17 @@ export function getEnv(): Env {
}, },
VERSION: raw.VERSION, 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** - [ ] **Step 2: Delete the placeholder from Task 3 if it exists**
```bash ```bash
rm -f src/.typecheck-placeholder.ts rm -f src/typecheck-placeholder.ts
``` ```
- [ ] **Step 3: Typecheck and lint** - [ ] **Step 3: Typecheck and lint**
@@ -842,7 +882,7 @@ Expected: both exit `0`.
```bash ```bash
git add src/features/ src/ui/ 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" 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. 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 ```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** - [ ] **Step 6: Verify the sub-plan directory listing matches the deliverables**
@@ -965,7 +1010,7 @@ Expected output shape:
- `src/observability/analytics/types.ts` - `src/observability/analytics/types.ts`
- Four feature directories each with `index.ts` - Four feature directories each with `index.ts`
- `src/ui/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** - [ ] **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: **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 610 (files created), Task 10 (barrels) - src/ layout (§1.3) → Task 5 (vitest.config), Task 610 (files created), Task 10 (barrels)
- `tsconfig.json` → Task 3 - `tsconfig.json` → Task 3
- `.eslintrc.cjs` base → Task 4 - `eslint.config.js` flat config baseline → Task 4
- `package.json` scripts → Task 1 - `package.json` scripts → Task 1
- `package.json` dep ownership (`zod`) → Task 2 - `package.json` dep ownership (`zod`) → Task 2
- `src/env/index.ts` Zod-validated → Task 9 (TDD) - `src/env/index.ts` Zod-validated → Task 9 (TDD)