# Phase 1A-1 — Project Skeleton Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**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`, `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.

**Scope boundaries (what 1A-1 does NOT do):**
- No Modern.js / Rspack / MF 2.0 config — that's **1A-2**.
- No ESLint boundary rules or restricted-imports — that's **1A-3**.
- No feature code, no routes, no middlewares — Phase 1B through 1I own those.
- No runtime logger/metrics/analytics implementations — only **type-only seeds** that 1G sub-plans extend.

**Assumptions resolved before this plan starts (from Phase 0 hard-blocker gate):**
- **A2** CDN vendor, **A3** CI provider, **A5** ASP.NET host fate, **A6** metrics endpoint, **A8** prod URL / access logs, **A9** Node 24 available on customer deploy VMs.

**Deliverables (contract with downstream Phase 1 sub-plans):**
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. `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).
   - `src/host-contract.ts` — the `HostContract` interface.
   - `src/observability/logger/types.ts` — type-only `Logger`, `LogFields`, `LogLevel`, `LogRecord`, `LogTransport` (seeded here, owned by 1G-logger going forward).
   - `src/observability/analytics/types.ts` — type-only `AnalyticsProviders`, `AnalyticsProps`, `AnalyticsEvent`, `Analytics` (seeded here, owned by 1G-analytics going forward).
   - `src/features/{online-board,schedule,flights-map,popular-requests}/index.ts` — empty barrels.
   - `src/ui/index.ts` — empty barrel.
7. `docs/superpowers/phase-1/frozen-barrels.md` — documents the frozen public-surface rule.
8. `docs/superpowers/phase-1/rename-pass-plan.md` — stub rework plan attached to A1 resolution.

**Exit gate:** `pnpm install && pnpm typecheck && pnpm lint && pnpm test` all green. Committed to `plan/react-rewrite`.

---

## File structure reference

Files created or modified by this plan, in order of appearance:

| File | Responsibility | Task |
|---|---|---|
| `.nvmrc` | Pin Node 24 | 1 |
| `package.json` | Root workspace manifest + scripts + deps | 1, 2 |
| `pnpm-lock.yaml` | pnpm lockfile (generated) | 2 |
| `tsconfig.json` | TypeScript strict config + path aliases | 3 |
| `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 |
| `src/host-contract.ts` | `HostContract` interface | 8 |
| `src/env/index.ts` | Zod-validated `getEnv()` | 9 (TDD) |
| `src/env/env.test.ts` | Env module tests | 9 (TDD) |
| `src/features/online-board/index.ts` | Empty barrel | 10 |
| `src/features/schedule/index.ts` | Empty barrel | 10 |
| `src/features/flights-map/index.ts` | Empty barrel | 10 |
| `src/features/popular-requests/index.ts` | Empty barrel | 10 |
| `src/ui/index.ts` | Empty barrel | 10 |
| `docs/superpowers/phase-1/frozen-barrels.md` | Frozen public-surface rule | 11 |
| `docs/superpowers/phase-1/rename-pass-plan.md` | A1-trigger rework stub | 11 |

**Decomposition rationale.** Every file has one responsibility. The env reader and its test live next to each other (`src/env/`). The two type-only observability files are deliberately small — they exist only to break the plan-order cycle with 1A-1's `HostContract` and `Env`, and are extended by 1G-logger / 1G-analytics later. `host-contract.ts` lives at `src/` root because both `src/routes/` and `src/mf/` consume it and neither should own it.

---

## Task 1 — Pin Node 24 and create root `package.json`

**Files:**
- Modify: `.nvmrc`
- Create: `package.json`

- [ ] **Step 1: Bump `.nvmrc` to Node 24**

Replace the single line in `.nvmrc` (currently `16`) with the pinned Node 24 LTS version:

```
24.2.0
```

- [ ] **Step 2: Verify Node 24 is available locally**

Run:
```bash
node --version
```
Expected: `v24.2.0` or newer 24.x. If older, install Node 24 via nvm/volta/fnm before continuing.

- [ ] **Step 3: Create root `package.json`**

Create `package.json` at the repo root:

```json
{
  "name": "@aeroflot/flights-web",
  "version": "0.0.0",
  "private": true,
  "description": "Aeroflot Flights — Modern.js + MF 2.0 React remote component (Phase 1 foundation)",
  "license": "UNLICENSED",
  "type": "module",
  "engines": {
    "node": ">=24.0.0",
    "pnpm": ">=9.0.0"
  },
  "packageManager": "pnpm@9.15.0",
  "scripts": {
    "dev": "echo \"dev script wired in 1A-2\" && exit 1",
    "build:standalone": "echo \"build:standalone wired in 1A-2\" && exit 1",
    "build:remote": "echo \"build:remote wired in 1A-2\" && exit 1",
    "build:both": "pnpm build:standalone && pnpm build:remote",
    "test": "vitest run",
    "test:coverage": "vitest run --coverage",
    "lint": "eslint \"src/**/*.{ts,tsx}\" --max-warnings 0",
    "typecheck": "tsc --noEmit"
  }
}
```

Rationale for the `"echo ... exit 1"` stubs: the master-plan contract for 1A-1 requires these script names to exist, but the implementations come from 1A-2 (Modern.js/Rspack). Explicit non-zero exit prevents silent false-success if someone runs them before 1A-2 lands.

- [ ] **Step 4: Commit**

```bash
git add .nvmrc package.json
git commit -m "Pin Node 24 and seed root package.json for Phase 1A-1"
```

---

## Task 2 — Install base dev dependencies via pnpm

**Files:**
- Modify: `package.json` (dependencies/devDependencies sections)
- Create: `pnpm-lock.yaml`

- [ ] **Step 1: Install runtime dep**

```bash
pnpm add zod@^3.23.0
```

Rationale: `zod` is used for env-var validation in Task 9 and will later be consumed by `src/shared/storage.ts` in 1H. It lives at 1A-1's level because both future consumers need it.

- [ ] **Step 2: Install dev deps (TypeScript, Vitest, ESLint, Node types)**

```bash
pnpm add -D \
  typescript@^5.5.0 \
  @types/node@^24.0.0 \
  vitest@^2.0.0 \
  @vitest/ui@^2.0.0 \
  eslint@^9.0.0 \
  @typescript-eslint/parser@^8.0.0 \
  @typescript-eslint/eslint-plugin@^8.0.0 \
  eslint-plugin-unused-imports@^4.0.0
```

- [ ] **Step 3: Verify `node_modules/` populated and lockfile exists**

```bash
ls node_modules/.bin/tsc node_modules/.bin/vitest node_modules/.bin/eslint
test -f pnpm-lock.yaml && echo "lockfile present"
```
Expected: three binary paths print, and `lockfile present` echoes.

- [ ] **Step 4: Commit**

```bash
git add package.json pnpm-lock.yaml
git commit -m "Install TypeScript, Vitest, ESLint, Zod base toolchain"
```

---

## Task 3 — Create `tsconfig.json` with strict settings and path aliases

**Files:**
- Create: `tsconfig.json`

- [ ] **Step 1: Write `tsconfig.json`**

```json
{
  "compilerOptions": {
    "target": "ES2023",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noEmit": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@phase0/*": ["scripts/phase-0/*"]
    },
    "types": ["node", "vitest/globals"]
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "vitest.config.ts"],
  "exclude": ["node_modules", "dist", "ClientApp", "wwwroot"]
}
```

- [ ] **Step 2: Verify `pnpm typecheck` runs on empty src (will fail — no files yet)**

```bash
pnpm typecheck
```
Expected: exits `0` with no output, OR errors `error TS18003: No inputs were found` because `src/` does not exist yet. Either outcome is acceptable at this step — the point is that the command runs and the config parses.

If TS18003 fires, create an empty placeholder so it goes away:
```bash
mkdir -p src
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 commit -m "Add strict TypeScript config with @/ and @phase0/ aliases"
```

---

## Task 4 — Create baseline `eslint.config.js` (ESLint 9 flat config)

**Files:**
- Create: `eslint.config.js`

**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**.

- [ ] **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
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",
    ],
  },
  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**

```bash
pnpm lint
```
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: Smoke-test that rules actually fire**

Create a temporary probe file to confirm `no-explicit-any` fires:

```bash
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)"
```

---

## Task 5 — Create `vitest.config.ts`

**Files:**
- Create: `vitest.config.ts`

- [ ] **Step 1: Write `vitest.config.ts`**

```typescript
import { defineConfig } from "vitest/config";
import path from "node:path";

export default defineConfig({
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
      "@phase0": path.resolve(__dirname, "./scripts/phase-0"),
    },
  },
  test: {
    environment: "node",
    globals: true,
    include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
    coverage: {
      provider: "v8",
      reporter: ["text", "json-summary", "lcov"],
      include: ["src/**/*.ts", "src/**/*.tsx"],
      exclude: [
        "src/**/*.test.ts",
        "src/**/*.test.tsx",
        "src/**/types.ts",
        "src/host-contract.ts",
      ],
    },
  },
});
```

Rationale for coverage excludes: type-only files and interface declarations have no runtime code to cover; including them would skew the coverage delta gate added in 1B.

- [ ] **Step 2: Run `pnpm test` on empty test suite**

```bash
pnpm test
```
Expected: Vitest prints `No test files found` and exits with code `0` (vitest's default when `passWithNoTests` is not explicitly false — if it fails with exit 1, add `passWithNoTests: true` under `test:` in the config).

If `passWithNoTests` is required, update the config:

```typescript
  test: {
    environment: "node",
    globals: true,
    passWithNoTests: true,
    // ... rest unchanged
  },
```

Re-run `pnpm test` — expected exit `0`.

- [ ] **Step 3: Commit**

```bash
git add vitest.config.ts
git commit -m "Configure Vitest with @/ alias and v8 coverage"
```

---

## Task 6 — Seed type-only logger file

**Files:**
- Create: `src/observability/logger/types.ts`

**Ownership note:** This file is **owned by 1G-logger** going forward. 1A-1 seeds it now to break the plan-order cycle: `src/host-contract.ts` (created in Task 8) imports `Logger` from this file, so the type must exist before `HostContract` can typecheck. 1G-logger extends this file with runtime transports and the provider later. Do not add anything runtime-related here.

- [ ] **Step 1: Write `src/observability/logger/types.ts`**

```typescript
// Ownership: src/observability/logger/ is owned by Phase 1 sub-plan 1G-logger.
// This type-only seed exists because Phase 1A-1's HostContract depends on Logger.
// Sub-plan 1G-logger extends this file with runtime transports.

export type LogLevel = "debug" | "info" | "warn" | "error";

export type LogFields = Record<string, string | number | boolean | null | undefined>;

export interface Logger {
  debug(msg: string, fields?: LogFields): void;
  info(msg: string, fields?: LogFields): void;
  warn(msg: string, fields?: LogFields): void;
  error(msg: string, fields?: LogFields & { err?: Error }): void;
  child(context: LogFields): Logger;
}

export interface LogRecord {
  ts: string;
  level: LogLevel;
  msg: string;
  fields: LogFields;
}

export interface LogTransport {
  write(record: LogRecord): void;
  flush(): Promise<void>;
}
```

- [ ] **Step 2: Typecheck**

```bash
pnpm typecheck
```
Expected: exit `0`.

- [ ] **Step 3: Commit**

```bash
git add src/observability/logger/types.ts
git commit -m "Seed type-only Logger surface for HostContract dependency"
```

---

## Task 7 — Seed type-only analytics file

**Files:**
- Create: `src/observability/analytics/types.ts`

**Ownership note:** Owned by **1G-analytics** going forward. Seeded here so `src/env/index.ts` (Task 9) can type `ANALYTICS_ENABLED: AnalyticsProviders` without a forward reference.

- [ ] **Step 1: Write `src/observability/analytics/types.ts`**

```typescript
// Ownership: src/observability/analytics/ is owned by Phase 1 sub-plan 1G-analytics.
// This type-only seed exists because Phase 1A-1's Env depends on AnalyticsProviders.
// Sub-plan 1G-analytics extends this file with the facade and adapter implementations.

export interface AnalyticsProviders {
  metrica: boolean;
  ctm: boolean;
  variocube: boolean;
  dynatrace: boolean;
}

export interface AnalyticsProps {
  [key: string]: unknown;
}

export interface AnalyticsEvent {
  kind: "track" | "page";
  name: string;
  props: AnalyticsProps;
  provider: "metrica" | "ctm" | "variocube" | "dynatrace";
  ts: string;
}

export interface Analytics {
  track(event: string, props?: AnalyticsProps): void;
  page(url: string, props?: AnalyticsProps): void;
}
```

- [ ] **Step 2: Typecheck**

```bash
pnpm typecheck
```
Expected: exit `0`.

- [ ] **Step 3: Commit**

```bash
git add src/observability/analytics/types.ts
git commit -m "Seed type-only AnalyticsProviders for Env dependency"
```

---

## Task 8 — Create `src/host-contract.ts`

**Files:**
- Create: `src/host-contract.ts`

- [ ] **Step 1: Write `src/host-contract.ts`**

Reproduce the shape byte-for-byte from master plan §1A-1 contracts (which itself matches design spec §2.4):

```typescript
import type { Logger } from "@/observability/logger/types";

/**
 * Contract a host application must implement to embed this component
 * as an MF 2.0 remote. Both standalone SSR and remote modes satisfy
 * this interface; features depend only on HostContract and never on
 * the hosting mode.
 */
export interface HostContract {
  /** Resolved locale, e.g. "ru", "en". */
  locale: string;
  /** Canonical origin for SEO (e.g. "https://flights.aeroflot.ru"). */
  canonicalOrigin: string;
  /** Optional deep-link navigation override from the host router. */
  navigate?: (path: string) => void;
  /** Optional consent flags; absent == assumed true. */
  consent?: { analytics: boolean; telemetry: boolean };
  /** Optional host logger for log stream merging. */
  logger?: Logger;
}
```

- [ ] **Step 2: Typecheck — verify the `@/observability/...` alias resolves**

```bash
pnpm typecheck
```
Expected: exit `0`. If TypeScript cannot resolve `@/observability/logger/types`, check that `tsconfig.json` `paths` matches Task 3 exactly.

- [ ] **Step 3: Commit**

```bash
git add src/host-contract.ts
git commit -m "Add HostContract interface per design spec §2.4"
```

---

## Task 9 — TDD `src/env/index.ts` with Zod validation

**Files:**
- Create: `src/env/env.test.ts`
- Create: `src/env/index.ts`

This is the only non-trivial runtime code in 1A-1; it gets full TDD treatment.

**Behavioral contract (from master plan §1A-1):**
- `getEnv()` reads from `process.env`, validates via Zod, returns a typed `Env` object.
- On malformed input, throws a readable error listing every failing field.
- Subsequent calls should be cached (idempotent, cheap) — `getEnv()` is called from many places.
- Tests override `process.env` and reset the cache between cases.

- [ ] **Step 1: Write the failing test**

Create `src/env/env.test.ts`:

```typescript
import { afterEach, beforeEach, describe, expect, it } from "vitest";

describe("getEnv", () => {
  const originalEnv = { ...process.env };

  beforeEach(() => {
    // Clear env to a clean slate, then set a minimum valid shape.
    for (const key of Object.keys(process.env)) {
      if (key.startsWith("VITEST_")) continue;
      delete process.env[key];
    }
    process.env["NODE_ENV"] = "testing";
    process.env["BUILD_TARGET"] = "standalone";
    process.env["PROD_ORIGIN"] = "https://flights.aeroflot.ru";
    process.env["API_BASE_URL"] = "https://platform.aeroflot.ru";
    process.env["SIGNALR_HUB_URL"] = "wss://platform.aeroflot.ru/hub";
    process.env["ANALYTICS_METRICA"] = "true";
    process.env["ANALYTICS_CTM"] = "false";
    process.env["ANALYTICS_VARIOCUBE"] = "false";
    process.env["ANALYTICS_DYNATRACE"] = "true";
    process.env["VERSION"] = "abc1234";
  });

  afterEach(async () => {
    process.env = { ...originalEnv };
    // Reset the module cache so getEnv re-reads the updated env.
    const mod = await import("./index.js");
    mod.__resetEnvCacheForTests();
  });

  it("returns a typed Env object for valid input", async () => {
    const { getEnv } = await import("./index.js");
    const env = getEnv();
    expect(env.NODE_ENV).toBe("testing");
    expect(env.BUILD_TARGET).toBe("standalone");
    expect(env.PROD_ORIGIN).toBe("https://flights.aeroflot.ru");
    expect(env.API_BASE_URL).toBe("https://platform.aeroflot.ru");
    expect(env.SIGNALR_HUB_URL).toBe("wss://platform.aeroflot.ru/hub");
    expect(env.ANALYTICS_ENABLED).toEqual({
      metrica: true,
      ctm: false,
      variocube: false,
      dynatrace: true,
    });
    expect(env.VERSION).toBe("abc1234");
    expect(env.OTEL_EXPORTER_OTLP_ENDPOINT).toBeUndefined();
  });

  it("caches the result across calls (same object identity)", async () => {
    const { getEnv } = await import("./index.js");
    const a = getEnv();
    const b = getEnv();
    expect(a).toBe(b);
  });

  it("throws a readable error when a required field is missing", async () => {
    delete process.env["API_BASE_URL"];
    const { getEnv, __resetEnvCacheForTests } = await import("./index.js");
    __resetEnvCacheForTests();
    expect(() => getEnv()).toThrow(/API_BASE_URL/);
  });

  it("throws when NODE_ENV is not one of the allowed values", async () => {
    process.env["NODE_ENV"] = "banana";
    const { getEnv, __resetEnvCacheForTests } = await import("./index.js");
    __resetEnvCacheForTests();
    expect(() => getEnv()).toThrow(/NODE_ENV/);
  });

  it("throws when BUILD_TARGET is neither standalone nor remote", async () => {
    process.env["BUILD_TARGET"] = "hybrid";
    const { getEnv, __resetEnvCacheForTests } = await import("./index.js");
    __resetEnvCacheForTests();
    expect(() => getEnv()).toThrow(/BUILD_TARGET/);
  });

  it("accepts optional OTEL_EXPORTER_OTLP_ENDPOINT when provided", async () => {
    process.env["OTEL_EXPORTER_OTLP_ENDPOINT"] = "https://otel.example/v1/traces";
    const { getEnv, __resetEnvCacheForTests } = await import("./index.js");
    __resetEnvCacheForTests();
    const env = getEnv();
    expect(env.OTEL_EXPORTER_OTLP_ENDPOINT).toBe("https://otel.example/v1/traces");
  });

  it("rejects OTEL_EXPORTER_OTLP_ENDPOINT that is not a URL", async () => {
    process.env["OTEL_EXPORTER_OTLP_ENDPOINT"] = "not-a-url";
    const { getEnv, __resetEnvCacheForTests } = await import("./index.js");
    __resetEnvCacheForTests();
    expect(() => getEnv()).toThrow(/OTEL_EXPORTER_OTLP_ENDPOINT/);
  });
});
```

- [ ] **Step 2: Run the test to verify it fails**

```bash
pnpm test src/env
```
Expected: FAIL — `Cannot find module './index.js'` or similar. The file does not exist yet.

- [ ] **Step 3: Write minimal implementation**

Create `src/env/index.ts`:

```typescript
import { z } from "zod";
import type { AnalyticsProviders } from "@/observability/analytics/types";

const boolish = z
  .enum(["true", "false", "1", "0"])
  .transform((v) => v === "true" || v === "1");

const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "testing", "staging", "production"]),
  BUILD_TARGET: z.enum(["standalone", "remote"]),
  PROD_ORIGIN: z.string().url(),
  API_BASE_URL: z.string().url(),
  SIGNALR_HUB_URL: z.string().url(),
  OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),
  OTEL_EXPORTER_OTLP_HEADERS: z.string().optional(),
  LOGS_ENDPOINT: z.string().url().optional(),
  ANALYTICS_METRICA: boolish.default("false"),
  ANALYTICS_CTM: boolish.default("false"),
  ANALYTICS_VARIOCUBE: boolish.default("false"),
  ANALYTICS_DYNATRACE: boolish.default("false"),
  VERSION: z.string().min(1),
});

type RawEnv = z.infer<typeof EnvSchema>;

export interface Env {
  NODE_ENV: RawEnv["NODE_ENV"];
  BUILD_TARGET: RawEnv["BUILD_TARGET"];
  PROD_ORIGIN: string;
  API_BASE_URL: string;
  SIGNALR_HUB_URL: string;
  OTEL_EXPORTER_OTLP_ENDPOINT?: string;
  OTEL_EXPORTER_OTLP_HEADERS?: string;
  LOGS_ENDPOINT?: string;
  ANALYTICS_ENABLED: AnalyticsProviders;
  VERSION: string;
}

let cached: Env | undefined;

export function getEnv(): Env {
  if (cached) return cached;

  const parsed = EnvSchema.safeParse(process.env);
  if (!parsed.success) {
    const details = parsed.error.issues
      .map((i) => `${i.path.join(".")}: ${i.message}`)
      .join("; ");
    throw new Error(`Invalid environment configuration: ${details}`);
  }

  const raw = parsed.data;
  // 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,
    ANALYTICS_ENABLED: {
      metrica: raw.ANALYTICS_METRICA,
      ctm: raw.ANALYTICS_CTM,
      variocube: raw.ANALYTICS_VARIOCUBE,
      dynatrace: raw.ANALYTICS_DYNATRACE,
    },
    VERSION: raw.VERSION,
  };
  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;
}

/**
 * Test-only: resets the module-level cache so tests can mutate process.env
 * and re-read. Do NOT call this from production code.
 */
export function __resetEnvCacheForTests(): void {
  cached = undefined;
}
```

- [ ] **Step 4: Run the tests to verify they pass**

```bash
pnpm test src/env
```
Expected: all 7 tests PASS.

If tests fail with module-resolution errors on `./index.js`, check the `vitest.config.ts` alias and TypeScript `moduleResolution: "Bundler"` setting from Task 3.

- [ ] **Step 5: Typecheck and lint the new files**

```bash
pnpm typecheck && pnpm lint
```
Expected: both exit `0`.

- [ ] **Step 6: Commit**

```bash
git add src/env/index.ts src/env/env.test.ts
git commit -m "Add Zod-validated getEnv() reader with module-level cache"
```

---

## Task 10 — Create empty feature + UI barrels (frozen public surface)

**Files:**
- Create: `src/features/online-board/index.ts`
- Create: `src/features/schedule/index.ts`
- Create: `src/features/flights-map/index.ts`
- Create: `src/features/popular-requests/index.ts`
- Create: `src/ui/index.ts`

- [ ] **Step 1: Create all five barrel files, each with an identical stub**

Run for each feature:

```bash
mkdir -p src/features/online-board src/features/schedule src/features/flights-map src/features/popular-requests src/ui
```

Write `src/features/online-board/index.ts`:

```typescript
// Public barrel for the online-board feature.
// This file is the ONLY public surface — other sub-plans and features
// must import exclusively from "@/features/online-board", never from
// deeper paths. See docs/superpowers/phase-1/frozen-barrels.md for the rule.
export {};
```

Write `src/features/schedule/index.ts`:

```typescript
// Public barrel for the schedule feature. See frozen-barrels.md.
export {};
```

Write `src/features/flights-map/index.ts`:

```typescript
// Public barrel for the flights-map feature. See frozen-barrels.md.
export {};
```

Write `src/features/popular-requests/index.ts`:

```typescript
// Public barrel for the popular-requests feature. See frozen-barrels.md.
export {};
```

Write `src/ui/index.ts`:

```typescript
// Public barrel for the UI adapter layer. See frozen-barrels.md.
// Feature code imports UI primitives exclusively through this barrel.
export {};
```

- [ ] **Step 2: Delete the placeholder from Task 3 if it exists**

```bash
rm -f src/typecheck-placeholder.ts
```

- [ ] **Step 3: Typecheck and lint**

```bash
pnpm typecheck && pnpm lint
```
Expected: both exit `0`.

- [ ] **Step 4: Commit**

```bash
git add src/features/ src/ui/
git rm -f src/typecheck-placeholder.ts 2>/dev/null || true
git commit -m "Seed frozen public barrels for 4 features + UI adapter"
```

---

## Task 11 — Document the frozen-barrel rule and rename-pass rework plan

**Files:**
- Create: `docs/superpowers/phase-1/frozen-barrels.md`
- Create: `docs/superpowers/phase-1/rename-pass-plan.md`

- [ ] **Step 1: Write `docs/superpowers/phase-1/frozen-barrels.md`**

```markdown
# Frozen public barrels

**Rule.** Cross-module imports inside `src/` go through exactly these five public entries:

- `@/features/online-board`
- `@/features/schedule`
- `@/features/flights-map`
- `@/features/popular-requests`
- `@/ui`

No file outside `src/features/<feature>/` may import from `src/features/<feature>/components/...` or any deeper path. No file outside `src/ui/` may import `src/ui/primitives/Button`. Enforcement lands in sub-plan **1A-3** via `eslint-plugin-boundaries` + `no-restricted-imports`.

**Why this is frozen.** Phase 0 assumption **A1** (customer's standard remote-frontend module template) may arrive after 1A-1 ships. When it does, the rename pass documented in `rename-pass-plan.md` must be a mechanical move: rename directories, update import paths at the five barrels, done. If cross-module imports fan out through deep paths, the rename becomes a surgery across dozens of files.

**What this unblocks.** Every Phase 1 sub-plan can add *internal* files to a feature/UI directory without coordinating with other sub-plans, because nothing outside the barrel depends on internals. The barrel file itself is the review gate.

**What this costs.** Small friction when a sub-plan wants to expose a new symbol — it must update the barrel. Acceptable cost for the refactor safety it buys.
```

- [ ] **Step 2: Write `docs/superpowers/phase-1/rename-pass-plan.md`**

```markdown
# A1 rename-pass rework plan

**Trigger.** Phase 0 assumption **A1** — "customer's standard remote-frontend module template" — resolves to a directory layout that differs from the one this repo uses.

**Scope.** Move/rename directories inside `src/` to match the customer template. Update import paths *only at the five frozen public barrels* (see `frozen-barrels.md`). Do not restructure feature internals.

**Preconditions.**
- The frozen-barrel rule has been enforced since 1A-1 (Task 11) and 1A-3 ESLint rules are passing on `main`.
- Customer template document is in hand and reviewed for explicit directory conventions.

**Steps (to be fleshed out when A1 resolves).**
1. Create a target-layout scratch file mapping current path → new path for every file under `src/`.
2. Run the rename as a single automated pass (`git mv`) inside an isolated worktree.
3. Update `tsconfig.json` `paths` aliases if the top-level segments change.
4. Update `vitest.config.ts` aliases to match.
5. Update the five barrel files — this is the *only* hand-edit needed for consumer code.
6. Run `pnpm typecheck && pnpm lint && pnpm test` — green before commit.
7. Run all Phase 1 exit-gate checks (from master plan) — green before PR.
8. Single commit: `Rename src/ layout to match customer module template (A1)`.

**Escape valve.** If the rename touches more than the five barrels, something violated the frozen-barrel rule between 1A-1 and now. Fix the violation first (move the cross-boundary import through a barrel), then retry the rename.

**Owner.** This task is attached to 1A-1's exit gate and fires on A1 resolution, whether that happens during Phase 1 or early Phase 2.
```

- [ ] **Step 3: Commit**

```bash
git add docs/superpowers/phase-1/
git commit -m "Document frozen-barrel rule and A1 rename-pass rework plan"
```

---

## Task 12 — Full exit-gate verification

**Files:**
- None (verification only).

- [ ] **Step 1: Fresh install from lockfile**

```bash
rm -rf node_modules && pnpm install --frozen-lockfile
```
Expected: install succeeds, lockfile unchanged.

- [ ] **Step 2: Typecheck**

```bash
pnpm typecheck
```
Expected: exit `0`, no output.

- [ ] **Step 3: Lint**

```bash
pnpm lint
```
Expected: exit `0`, no errors or warnings.

- [ ] **Step 4: Tests**

```bash
pnpm test
```
Expected: 7 env tests pass, no other test files.

- [ ] **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 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
```

- [ ] **Step 6: Verify the sub-plan directory listing matches the deliverables**

```bash
ls -la src/ src/env/ src/observability/logger/ src/observability/analytics/ src/features/ src/ui/
```
Expected output shape:
- `src/env/index.ts` and `src/env/env.test.ts`
- `src/host-contract.ts`
- `src/observability/logger/types.ts`
- `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).

- [ ] **Step 7: Verify Phase 0 and other sub-plans are unaffected**

```bash
git status
```
Expected: clean working tree on `plan/react-rewrite`. `ClientApp/`, `Startup.cs`, `Program.cs`, `Aeroflot.Flights.Web.csproj`, `wwwroot/` untouched.

- [ ] **Step 8: Final commit (exit-gate marker)**

If there are any stray uncommitted files from verification steps, commit them. Otherwise no-op:

```bash
git status --porcelain | grep -q . && git commit -am "1A-1 exit gate verification" || echo "nothing to commit"
```

---

## Self-review

**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
- `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)
- `src/host-contract.ts` → Task 8
- Empty feature + UI barrels → Task 10
- Frozen barrel rule documented → Task 11
- Rename-pass rework task attached → Task 11
- Node 24 pinned → Task 1

Design spec §1.3 (src tree) coverage: the top-level directories actually materialized by 1A-1 are `src/env/`, `src/features/<four>/`, `src/observability/logger/`, `src/observability/analytics/`, `src/ui/`, plus `src/host-contract.ts`. Other directories from §1.3 (`src/routes/`, `src/mf/`, `src/shared/`, `src/i18n/`, `src/observability/metrics/`) are **not** created by 1A-1 — they're created by the sub-plans that own them (1A-2, 1C, 1D, 1G-metrics, 1F-layout). This is consistent with the master plan's sub-plan ownership table.

**Placeholder scan.** No `TBD` / `TODO` / `FIXME` / "fill in later" in this plan. The `"echo ... exit 1"` stubs in `package.json` scripts are deliberate placeholders guarded by explicit error exits, not forgotten work.

**Type consistency.**
- `Logger` interface: defined once in Task 6, imported by `HostContract` in Task 8. Same shape in both places.
- `AnalyticsProviders` interface: defined once in Task 7, imported by `Env` in Task 9. Same shape.
- `Env` interface: defined in Task 9, returned by `getEnv()`. Properties match the master plan §1A-1 contract.
- `HostContract` interface: Task 8 shape matches master plan §1A-1 and design spec §2.4 byte-for-byte.

**Test-plan scan.** The only runtime module with logic in 1A-1 is `src/env/index.ts`. Its test file covers: happy path, cache identity, missing required field, enum violations (two), optional field present, optional field malformed. Seven tests, all behavior branches touched. Config files, type-only files, and barrels have no logic to test.

**Commit hygiene.** Twelve tasks produce 11 commits (Task 12 is verification-only). Each commit message is focused on *why* not *what*, in English, no Co-Authored-By. Commits are small, logical, and independently revertible.

---

## Execution handoff

**Plan complete and saved to `docs/superpowers/plans/2026-04-14-phase-1a1-skeleton.md`. Two execution options:**

**1. Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration.

**2. Inline Execution** — Execute tasks in this session using `superpowers:executing-plans`, batch execution with checkpoints.

**Which approach?**
