# Phase 1 — Foundation MASTER Plan

> **This document is a plan INDEX, not an executable plan.** It lists the Phase 1 sub-plans, their dependency order, the contracts each sub-plan exports for downstream sub-plans to consume, and the shared files that cross sub-plan boundaries.
>
> **Do not execute this document directly.** Each sub-plan is a separate file under `docs/superpowers/plans/` with its own TDD-granular tasks. They are written on demand by re-invoking the `superpowers:writing-plans` skill with a sub-plan-specific prompt.

**Goal of Phase 1:** Build the complete Modern.js + Module Federation 2.0 foundation on which all four feature migrations (Phases 2–5) will be implemented. Nothing in Phase 1 ships to production users — the output is a working dual-build artifact deployed to the `testing` environment with all observability, security, and CI pipelines live.

**Phase 1 exit gate** (must pass before Phase 2 starts):

- Both build targets (standalone SSR + MF 2.0 remote) produce valid artifacts in CI.
- `mf-manifest.json` is served from the `testing` environment and consumable by a test host.
- The smoke route (`/ru/smoke`) renders via SSR in `testing` with all observability pipelines (logger, metrics, analytics) emitting correctly, verified by inspecting the log / metrics / analytics capture endpoints.
- All Phase 1 CI gates pass on `main`: lint, typecheck, unit (70%+ coverage on `src/features/` + `src/shared/` + `src/ui/` + `src/observability/`), bundle size, security scan.
- Security hardening live: CSP with per-request nonce (including the stream-transform nonce injection workaround for React issue #24883), HTTP security headers, dependency scanning green.
- Canary deploy pipeline functional (smoke route deployed via canary path with auto-rollback on health-check failure).
- Operational runbook published in `docs/superpowers/phase-1/runbook.md`.
- Responsive baseline assertions passing on root layout + error pages at 320 / 768 / 1280 / 1920 widths.

**Reference spec:** `docs/superpowers/specs/2026-04-14-aeroflot-flights-react-rewrite-design.md`. Phase 1 implements sections §1–§8 of the spec (everything except the feature ports in §9.2 Phase 2+).

**Phase 0 prerequisites — split gate.** Phase 0 produces a customer-confirmation checklist answering assumptions A1–A9. Phase 1 splits the gate:

- **Hard blockers** (1A-1 does not start until all resolved): **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 — new).
- **Stub-allowed** (Phase 1 ships stubs, swap task pending customer response): **A1** (module template), **A4** (log format), **A7** (analytics vendor credentials).

---

## Sub-plan inventory

| ID | Sub-plan | Spec section | Estimated size | File |
|---|---|---|---|---|
| **1A-1** | Project skeleton (src tree, tsconfig, eslint base, env, package.json, zod) | §1.3, §2.1 | Medium | `2026-04-14-phase-1a1-skeleton.md` (TBW) |
| **1A-2** | MF 2.0 + dual build targets + RemoteLoader + MF spike | §2.1, §2.2, §2.3, §2.4, §2.5 | Medium | `2026-04-14-phase-1a2-mf-builds.md` (TBW) |
| **1A-3** | ESLint boundaries + layered dependency rules | §1.2 | Small | `2026-04-14-phase-1a3-eslint-boundaries.md` (TBW) |
| **1B** | CI pipeline | §8.5 | Medium | `2026-04-14-phase-1b-ci.md` (TBW) |
| **1C** | i18n runtime + locale port | §6.1–§6.4 | Medium | `2026-04-14-phase-1c-i18n.md` (TBW) |
| **1D** | API client + caches + circuit breaker | §4.1, §4.2 | Medium | `2026-04-14-phase-1d-api-client.md` (TBW) |
| **1E** | SignalR wrapper + `useLiveFlights` hook | §4.4 | Medium | `2026-04-14-phase-1e-signalr.md` (TBW) |
| **1F-layout** | Root layout + locale layout + error routes + smoke route + ErrorBoundary + error→HTTP mapper | §1.3, §3.1, §3.3 | Medium | `2026-04-14-phase-1f-layout.md` (TBW) |
| **1F-seo** | SeoHead + hreflang builder + JsonLdRenderer | §3.6, §6.5, §6.6, §6.7, §6.8 | Small | `2026-04-14-phase-1f-seo.md` (TBW) |
| **1G-logger** | Logger types + JSON-lines transport + console transport + provider | §7.1, §7.2 | Medium | `2026-04-14-phase-1g-logger.md` (TBW) |
| **1G-metrics** | OpenTelemetry init (server/browser) + custom metric instruments | §7.3, §7.6, §7.7 | Medium | `2026-04-14-phase-1g-metrics.md` (TBW) |
| **1G-analytics** | Analytics facade + four stub adapters (Yandex.Metrica, CTM, Variocube, Dynatrace) | §7.4 | Small | `2026-04-14-phase-1g-analytics.md` (TBW) |
| **1H** | Security hardening (CSP + nonce stream transform + headers + storage) | §8.1 | Small | `2026-04-14-phase-1h-security.md` (TBW) |
| **1I** | Deploy pipeline + health + graceful shutdown + runbook | §8.3, §8.5 | Medium | `2026-04-14-phase-1i-deploy.md` (TBW) |

Sizes: **Small** ≈ 5–10 tasks, **Medium** ≈ 10–20 tasks. (No "Large" sub-plans after the 1A/1F/1G splits.)

Parity harnesses (URL / SEO / VRT) and real parity tests are **deferred to Phase 2**, to be designed against the first real feature migration rather than against a synthetic smoke route.

---

## Dependency graph

```
                     ┌─────────────────┐
                     │  1A-1 Skeleton  │◄── every sub-plan depends on 1A-1
                     └────────┬────────┘
                              ▼
                     ┌─────────────────┐
                     │  1A-2  MF 2.0   │
                     │  + builds + MF  │
                     │  spike first    │
                     └────────┬────────┘
                              ▼
                     ┌─────────────────┐
                     │ 1A-3  ESLint    │
                     │ boundaries      │
                     └────────┬────────┘
   ┌──────────────────────────┼──────────────────┬──────────────────┐
   ▼                          ▼                  ▼                  ▼
┌──────┐  ┌────────┐  ┌───────────┐  ┌──────────┐  ┌──────────────┐  ┌─────────┐
│ 1B   │  │ 1C i18n │  │  1D API  │  │ 1E Sig-  │  │ 1G-logger    │  │ 1F-seo  │
│ CI   │  │         │  │  client  │  │ nalR     │  │ (type-only   │  │ (pure   │
│      │  │         │  │          │  │          │  │  file first) │  │ funcs)  │
└──┬───┘  └────┬───┘  └─────┬────┘  └──────────┘  └──────┬───────┘  └────┬────┘
   │           │            │                             │                │
   │           │            │       ┌─────────────────────┘                │
   │           │            │       ▼                                      │
   │           │            │  ┌──────────────┐                            │
   │           │            │  │ 1G-metrics   │                            │
   │           │            │  │ (depends on  │                            │
   │           │            │  │  1G-logger)  │                            │
   │           │            │  └──────┬───────┘                            │
   │           │            │         │                                    │
   │           │            │         ▼                                    │
   │           │            │  ┌──────────────┐                            │
   │           │            │  │ 1G-analytics │                            │
   │           │            │  │ (depends on  │                            │
   │           │            │  │  1G-logger)  │                            │
   │           │            │  └──────┬───────┘                            │
   │           │            │         │                                    │
   │           └────────────┴─────────┴────────────────────────────────────┤
   │                                  ▼                                    │
   │                     ┌──────────────────────────────┐                  │
   │                     │  1F-layout (root layout +    │◄─────────────────┘
   │                     │  error routes + smoke route) │
   │                     │  (consumes 1C + 1D +         │
   │                     │   1G-logger/metrics/analytics │
   │                     │   + 1F-seo)                  │
   │                     └──────┬───────────────────────┘
   │                            ▼
   │                     ┌──────────────────────────────┐
   │                     │  1H  Security hardening      │
   │                     │  (middleware + nonce stream  │
   │                     │  transform into 1F-layout)   │
   │                     └──────┬───────────────────────┘
   │                            │
   ▼                            ▼
   ┌───────────────────────────────────────┐
   │  1I  Deploy pipeline + runbook        │
   │  (consumes 1A-2 + 1B + 1H)            │
   └───────────────────────────────────────┘
```

### Execution order

**Serial (1 engineer):** 1A-1 → 1A-2 → 1A-3 → 1C → 1F-seo → 1G-logger → 1D → 1G-metrics → 1G-analytics → 1F-layout → 1H → 1I → 1B → 1E.

Rationale:
- 1A-1/2/3 unlock everything.
- 1C is cheap and 1F-layout depends on it.
- 1F-seo is pure functions, no runtime deps on 1C/1D/1G — slot it in early.
- 1G-logger's type-only file (`Logger`, `LogFields`, `LogLevel`) must ship early since 1A-1's `HostContract` depends on it. (See "Logger type extraction" below.)
- 1D depends on 1G-logger's types (for request-scoped child loggers).
- 1G-metrics and 1G-analytics depend on 1G-logger.
- 1F-layout consumes 1C + 1D + all three 1G sub-plans + 1F-seo — it's the integration point.
- 1H modifies files 1F-layout creates, so it follows.
- 1I consumes 1A-2 + 1B + 1H.
- 1B can be slotted earlier if CI-enforced gates become blocking; the order above assumes PR-quality gates are enough until 1I.
- 1E can come last because its only consumer is Phase 2 Online Board.

**Parallel (2+ engineers):** After 1A-3 ships, the following can proceed in parallel: **1B, 1C, 1D, 1E, 1F-seo, 1G-logger** (and its dependents 1G-metrics/1G-analytics). 1F-layout and later remain sequential because of shared-file constraints.

### Critical path

**1A-1 → 1A-2 → 1A-3 → 1C → 1F-layout → 1H → 1I → exit gate** is the critical path for one engineer. 1G-logger / 1G-metrics / 1G-analytics / 1D / 1F-seo all feed 1F-layout but sit off the critical path once 1C ships (they run in parallel with 1C if there's more than one engineer, or in series but interleaved if there's only one).

---

## Logger type extraction (cross-cutting)

`HostContract` in 1A-1 has an optional `logger?: Logger` field (design spec §2.4). `Logger` is defined in 1G-logger. To avoid a plan-dependency cycle:

- **1G-logger's first task** ships `src/observability/logger/types.ts` containing **only the type definitions** (`Logger`, `LogFields`, `LogLevel`, `LogRecord`, `LogTransport`). No runtime code, no transports.
- **1A-1** imports `Logger` from `@/observability/logger/types` in its `HostContract` definition.
- Runtime logger implementation (transports, provider, factories) lands later in 1G-logger and does not retroactively affect 1A-1.

This "type-only file first" pattern is the canonical workaround for plan-order cycles in this master plan; any other sub-plan hitting a similar cycle follows the same pattern.

---

## Contracts — what each sub-plan exports

This is the section that lets sub-plans be written and reviewed independently. Every sub-plan must produce its contracts without breaking changes once another sub-plan depends on them. Contracts are enforced via TypeScript types — any change to an exported type is a cross-sub-plan review gate.

### 1A-1 — Project skeleton contracts

**Exports:**

- **Project layout** — the `src/` directory tree from design spec §1.3. Every other sub-plan adds files inside this tree. No sub-plan is allowed to create top-level directories outside `src/` / `tests/` / `scripts/` / `docs/` without explicit call-out.
- **`tsconfig.json`** — strict mode, path aliases (`@/` → `src/`, `@phase0/` → `scripts/phase-0/`), `noUncheckedIndexedAccess`, `isolatedModules`.
- **`.eslintrc.cjs` base** — the baseline config. 1A-3 adds the boundaries rules on top.
- **`package.json` scripts** — `dev`, `build:standalone`, `build:remote`, `build:both`, `test`, `test:coverage`, `lint`, `typecheck`. Pinned **Node 24** via `engines` and `.nvmrc`.
- **`package.json` dependencies owned by 1A-1** — `zod` (for env validation and `storage.ts` schema validation — used by both 1A-1 `src/env/` and 1H `src/shared/storage.ts`, so it lives in the common base).
- **`src/env/index.ts`** — runtime env-var reader returning a Zod-validated typed `Env` object. Other sub-plans read env vars exclusively through this module.
- **`src/host-contract.ts`** — the `HostContract` type, reproduced byte-for-byte from design spec §2.4:

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

    export interface HostContract {
      locale: string;                                        // "ru", "en", ...
      canonicalOrigin: string;                               // "https://flights.aeroflot.ru"
      navigate?: (path: string) => void;                     // optional deep-link nav override
      consent?: { analytics: boolean; telemetry: boolean };  // optional, else assumed true
      logger?: Logger;                                       // optional host logger merge
    }
    ```

- **Empty feature and UI barrel files** — `src/features/{online-board,schedule,flights-map,popular-requests}/index.ts` and `src/ui/index.ts` exist but export nothing. Phase 2+ populates them. **Exit-gate rule: the public barrel surface is frozen — no other sub-plan creates new cross-boundary imports outside these barrels.**

**TypeScript contracts:**

```ts
// src/env/index.ts
export interface Env {
  NODE_ENV: "development" | "testing" | "staging" | "production";
  BUILD_TARGET: "standalone" | "remote";
  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;      // imported from src/observability/analytics/types
  VERSION: string;                            // git sha, injected at build time
}
export function getEnv(): Env;
```

**Exit gate for 1A-1:**
- `pnpm typecheck` and `pnpm lint` green on an empty src tree plus the env module.
- `src/env/index.ts` round-trips a test `Env` object through Zod validation and surfaces a readable error on malformed input.
- **Frozen barrel rule** documented in 1A-1's deliverables — subsequent sub-plans may add exports *to* these barrels but may not create parallel public surfaces.
- **Rename-pass rework task** attached to this sub-plan's exit gate: if A1 (customer module template) resolves after 1A-1 ships, a bounded rename pass moves the src tree to match the template without rewriting internals. The frozen barrel rule ensures this rename is a mechanical operation.

---

### 1A-2 — MF 2.0 + dual build targets + RemoteLoader contracts

**First task (gate):** **Modern.js + MF 2.0 spike.** A 2–4 hour timeboxed experiment that boots the simplest possible Modern.js + Rspack + `@module-federation/modern-js` end-to-end example (a single exposed hello-world component), documents gotchas in `docs/superpowers/phase-1/modernjs-mf-spike.md`, and produces a **pinned version matrix** (Modern.js X.Y.Z + `@module-federation/modern-js` A.B.C + Rspack P.Q.R). If the spike fails or reveals a hard blocker, 1A-2 halts and the issue escalates to the customer before committing to Modern.js.

**Exports:**

- **`modern.config.ts`** — the Modern.js build config with `BUILD_TARGET=standalone|remote` branching. Later sub-plans (1G-metrics, 1H, 1I) modify this file via explicit "modify" tasks — 1A-2 owns the base structure.
- **Dual build targets** — `pnpm build:standalone` produces `dist/standalone/` (Node server + client bundle); `pnpm build:remote` produces `dist/remote/` (static chunks + `mf-manifest.json`).
- **`src/mf/remote-loader.ts`** — Module Federation 2.0 runtime API wrapper so Phase 2+ can consume other customer remotes without touching `modern.config.ts` again:

    ```ts
    export interface RemoteModuleRef<T = unknown> {
      name: string;   // remote name (e.g. "customer-ui")
      module: string; // exposed module path (e.g. "./Header")
    }

    export function loadRemoteModule<T>(ref: RemoteModuleRef<T>): Promise<T>;
    export function registerRemote(entry: { name: string; entry: string }): void;
    ```

- **`src/mf/host-entry.ts`** — the entry point for the remote build target that consumes `HostContract` and bootstraps the React subtree into a host-provided mount point.

**TypeScript contracts:** `RemoteModuleRef`, `loadRemoteModule`, `registerRemote` (above). `HostContract` is imported from 1A-1 (`src/host-contract.ts`).

**Exit gate for 1A-2:**
- Spike doc committed with pinned version matrix.
- `pnpm build:both` produces `dist/standalone/` (Node server + client bundle) and `dist/remote/` (static chunks + `mf-manifest.json`) with zero type errors.
- A minimal integration test loads a test remote via `loadRemoteModule` and asserts the returned module.

---

### 1A-3 — ESLint boundaries contracts

**Exports:**

- **`.eslintrc.cjs` additions** — `eslint-plugin-boundaries` rules enforcing the layered dependency direction from design spec §1.2:
  - `features/` cannot import `routes/` or `mf/`
  - `ui/` cannot import `features/`
  - `shared/` cannot import `features/`, `routes/`, `mf/`, `observability/`
  - `observability/` cannot import `features/`, `routes/`, `mf/`
- **`no-restricted-imports` rules:**
  - `@opentelemetry/sdk-metrics` — allowed only in `src/observability/metrics/otel.ts` (keeps module-level instrument exports safe by forcing meter acquisition through `@opentelemetry/api`'s proxy meter).
  - `window.localStorage` / `window.sessionStorage` — allowed only in `src/shared/storage.ts`.
  - `@microsoft/signalr` — forbidden in any file that's part of the SSR bundle (enforced via file-path pattern).
  - `react-i18next` — forbidden outside `src/i18n/provider.tsx` (feature code goes through the re-export).

**Exit gate for 1A-3:** Each rule has a fabricated violation test in `tests/eslint/` that asserts the rule fires.

---

### 1B — CI pipeline contracts

**Exports:**

- **`.github/workflows/ci.yml`** (or equivalent for the CI provider chosen via A3). Runs on every PR and every push to `main`: install, lint, typecheck, unit tests, build both targets, bundle-size gate, security scan.
- **`.github/workflows/nightly.yml`** — nightly-only: contract tests, load test (stub until Phase 2), Lighthouse CI.
- **`scripts/ci/bundle-size-gate.ts`** — reads the Rspack build stats and compares against budgets in `docs/superpowers/phase-1/bundle-budgets.json`. Fails the build if any budget is exceeded.
- **`scripts/ci/check-coverage-delta.ts`** — reads Vitest coverage JSON and the prior-commit's coverage JSON (from the base branch), fails if coverage decreases. Uses `git show <base>:coverage-summary.json` to read the baseline; tolerates missing baseline (first run).

**TypeScript contracts:** none exported (scripts are CI-internal).

**Exit gate for 1B:** A PR with a trivially-broken test fails CI; a PR with a green test passes CI in under 20 minutes. Bundle-size gate flags a fabricated regression.

---

### 1C — i18n runtime contracts

**Exports:**

- **`src/i18n/config.ts`** — factory that creates a request-scoped `i18next` instance configured with `i18next-icu`, loaded with a single locale's bundle:

    ```ts
    export function createI18nInstance(options: {
      locale: Language;
      initialResources?: Record<string, Record<string, unknown>>;
    }): Promise<i18n>;
    ```

- **`src/i18n/resolver.ts`** — locale resolution from URL prefix:

    ```ts
    export type Language = "ru"|"en"|"es"|"fr"|"it"|"ja"|"ko"|"zh"|"de";
    export const LANGUAGES: readonly Language[];
    export function isLanguage(x: string): x is Language;
    export function resolveLocaleFromPath(pathname: string): Language | null;
    export function stripLocaleFromPath(pathname: string): { locale: Language; rest: string } | null;
    ```

- **`src/i18n/locales/{lang}/common.json`** — 9 files, ported from `ClientApp/src/assets/i18n/*.json` using the Phase 0 translation-key inventory to drop dead keys (optional; by default, all keys port). ICU MessageFormat syntax preserved byte-for-byte.
- **`src/i18n/serializer.ts`** — helpers to serialize the loaded locale bundle into the SSR HTML payload under `window.__I18N__` and rehydrate it on the client:

    ```ts
    export function serializeI18nForHydration(i18n: i18n): string;   // emits a JSON string
    export function hydrateI18nFromWindow(): Promise<i18n>;          // reads window.__I18N__
    ```

- **`src/i18n/provider.tsx`** — React Context provider + `<I18nProvider i18n={...}>` component + `useI18n()` accessor. **Re-exports `useTranslation` from `react-i18next`** so feature code never imports `react-i18next` directly (enforced by 1A-3's ESLint rule).

**Exit gate for 1C:** Vitest test renders a component with `<I18nProvider>` loaded with `ru` and asserts `t("common.someKey")` returns the Russian value. SSR + hydration roundtrip test using `renderToString` verifies no client-side re-fetch or flash.

---

### 1D — API client contracts

**Exports:**

- **`src/shared/api/client.ts`** — the `ApiClient` class:

    ```ts
    export interface ApiClientRetryOptions {
      maxRetries?: number;          // default 3 (idempotent only)
      timeoutFactor?: number;       // default 2 (exponential backoff)
      statusCodes?: number[];       // default [408, 429, 500, 502, 503, 504]
    }

    export interface ApiClientOptions {
      baseUrl: string;
      locale: Language;
      traceId?: string;
      fetchImpl?: typeof fetch;          // for tests (ignored on server path where undici is used)
      defaultTimeoutMs?: number;          // default 5000
      retry?: ApiClientRetryOptions;
      logger?: Logger;
    }

    export class ApiClient {
      constructor(options: ApiClientOptions);
      get<T>(path: string, query?: Record<string, string | number | boolean>): Promise<T>;
      post<T>(path: string, body: unknown): Promise<T>;
    }
    ```

  **Server path** uses `undici.RetryAgent` under the hood (statusCodes + timeoutFactor wired into the agent). **Client path** uses `globalThis.fetch` with a thin hand-rolled retry wrapper that applies the same config.

- **`src/shared/api/errors.ts`** — typed error classes:

    ```ts
    export class ApiError extends Error { constructor(message: string); }
    export class ApiHttpError extends ApiError { status: number; body?: unknown; }
    export class ApiTimeoutError extends ApiError { timeoutMs: number; }
    export class ApiNetworkError extends ApiError { cause?: Error; }
    ```

- **`src/shared/api/cache.ts`** — three distinct cache types:

    ```ts
    // (1) SSR request-scoped dedup: one-shot, discarded after response
    export class RequestScopedCache {
      get<T>(key: string): Promise<T> | undefined;
      set<T>(key: string, promise: Promise<T>): void;
    }

    // (2) Client-side per-tab in-memory TTL cache (count-capped)
    export class ClientMemoryCache<T> {
      constructor(options: { max: number; defaultTtlMs: number });
      get(key: string): T | undefined;
      set(key: string, value: T, ttlMs?: number): void;
      delete(key: string): void;
      clear(): void;
      size: number;
    }

    // (3) Shared per-VM LRU cache with BYTE cap (~100MB), backed by lru-cache@^10
    export class ServerLruCache<T> {
      constructor(options: {
        maxBytes: number;                         // e.g. 100 * 1024 * 1024
        defaultTtlMs: number;
        sizeCalculation?: (value: T, key: string) => number; // default: JSON.stringify length
      });
      get(key: string): T | undefined;
      set(key: string, value: T, ttlMs?: number): void;
      delete(key: string): void;
      clear(): void;
      calculatedSize: number;                     // current bytes used
    }

    // Key convention used by all three caches
    export function cacheKey(endpoint: string, query: Record<string, unknown>, locale: Language): string;
    ```

  Default TTLs from design spec §4.2: **30s for live data, 5 min for static reference data** (wired via call-site overrides).

- **`src/shared/api/circuit-breaker.ts`**:

    ```ts
    export interface CircuitBreakerOptions {
      failureThreshold?: number;  // default 5
      openDurationMs?: number;    // default 30_000
    }

    export class CircuitBreaker {
      constructor(options?: CircuitBreakerOptions);
      exec<T>(fn: () => Promise<T>): Promise<T>;
      reset(): void;
      state: "closed" | "open" | "half-open";
    }
    ```

- **`src/shared/api/cached-client.ts`** — **caching decorator** layered above `ApiClient` (not inside it):

    ```ts
    export interface CachedClientOptions {
      client: ApiClient;
      requestScoped?: RequestScopedCache;   // SSR only
      clientMemory?: ClientMemoryCache<unknown>;
      serverLru?: ServerLruCache<unknown>;
      ttlMs?: number;
    }

    export class CachedApiClient {
      constructor(options: CachedClientOptions);
      get<T>(path: string, query?: Record<string, string | number | boolean>): Promise<T>;
    }
    ```

  Feature code opts into caching by wrapping `ApiClient` in `CachedApiClient`; uncached calls go through the raw `ApiClient`.

- **`src/shared/api/provider.tsx`** — React Context provider + `useApiClient()` hook. SSR-aware: on the server, the client is constructed per-request with the resolved locale; on the client, a single instance is shared across the tab.

**Dependency on 1G-logger:** `ApiClientOptions.logger?: Logger` consumes the type-only import from `src/observability/logger/types`. 1D must not ship before 1G-logger's type-only file.

**Package additions (1D):** `undici` (explicit dep, even though Node 24 includes it — pin for deterministic behavior), `lru-cache@^10`.

**Exit gate for 1D:** Vitest tests cover: success response deserialization; retry on `[408, 429, 500, 502, 503, 504]`; no retry on other 4xx; timeout; `Retry-After` honored on 429/503; circuit breaker open/half-open/closed transitions; request-scoped cache dedup; client-memory TTL eviction; server LRU byte cap eviction under load.

---

### 1E — SignalR wrapper contracts

**Exports:**

- **`src/shared/signalr/connection.ts`** — the reference-counted connection wrapper:

    ```ts
    export interface HubOptions {
      hubUrl: string;
      reconnectDelaysMs?: number[];  // default [0, 2000, 10000, 30000]
      gracePeriodMs?: number;        // default 5000
    }

    export class SignalRConnection {
      constructor(options: HubOptions);
      subscribe(channel: string, handler: (message: unknown) => void): () => void;
      onStatusChange(handler: (status: ConnectionStatus) => void): () => void;
      get status(): ConnectionStatus;
    }

    export type ConnectionStatus = "idle" | "connecting" | "live" | "reconnecting" | "offline";

    export function getSharedConnection(options: HubOptions): SignalRConnection;
    ```

- **`src/shared/hooks/useLiveFlights.ts`** — **generic** live-data hook (not hardcoded to flight-search params):

    ```ts
    export function useLiveFlights<TParams, TData>(
      params: TParams,
      initialData: TData[],
      config: {
        hubUrl: string;
        channelKey: (params: TParams) => string;
      },
    ): { data: TData[]; connectionStatus: ConnectionStatus };
    ```

  SSR-safe: during SSR, returns `{ data: initialData, connectionStatus: "idle" }` without importing `@microsoft/signalr` (enforced by 1A-3's ESLint SSR-bundle guard).

**Exit gate for 1E:**
- Two rapid `useEffect` mounts (Strict Mode double-invoke simulation) result in exactly one `HubConnection.start()` call.
- Unmount + remount within the grace period reuses the connection; unmount + remount after the grace period creates a fresh one.
- SSR render path does not import `@microsoft/signalr` (asserted by inspecting the SSR bundle stats for the absence of the package).

---

### 1F-layout — Root layout + routes + error mapper contracts

**Exports:**

- **`src/routes/layout.tsx`** — root HTML shell: `<html>`, `<head>`, `<Scripts>`, `<Links>`, root `<I18nProvider>`, root `<ApiClientProvider>`, root `<ErrorBoundary>`, root `<AnalyticsLoader>` (from 1G-analytics). Wrapped by `<LoggerProvider>` (from 1G-logger).

- **`src/routes/[lang]/layout.tsx`** — locale-scoped layout: validates `params.lang`, creates the request-scoped i18next instance (via 1C's `createI18nInstance`), builds the canonical URL + hreflang set (via 1F-seo's `buildHreflangSet`), passes them into `<SeoHead>`. Creates a request-scoped logger child and an OTel span (from 1G-metrics).

- **`src/routes/error/[code]/page.tsx`** — error page rendered for `code ∈ {404, 500, 503}`. Ports the existing Angular error component layout. Fully responsive (assertions below).

- **`src/routes/[lang]/smoke/page.tsx`** — smoke route that exercises every foundation subsystem: emits a log at `info`, emits a metric counter, calls `track("smoke.pageview")`, renders `{t("smoke.heading")}`, fetches a dummy API endpoint via `ApiClient.get`, renders `<SeoHead>` with canonical + hreflang. Visible in `testing` env at `/ru/smoke` and `/en/smoke`. Uses `React.lazy()` + `<Suspense>` + a server loader as a deliberate stress test of the React 18 concurrent + streaming path.

- **`src/routes/error/map.ts`** — error-to-HTTP mapper consumed by the SSR loader path:

    ```ts
    export interface ErrorResponse {
      status: 404 | 500 | 503;
      headers?: Record<string, string>;
      errorCode: "not_found" | "internal" | "unavailable";
    }

    export function errorToResponse(error: unknown): ErrorResponse;
    ```

  Mapping rules (design spec §4.6):
  - `ApiHttpError` with `status === 404` → `{ status: 404, errorCode: "not_found" }`
  - `ApiHttpError` with `status` in 500–599 → `{ status: 500, errorCode: "internal" }`
  - `ApiTimeoutError` → `{ status: 503, headers: { "Retry-After": "30" }, errorCode: "unavailable" }`
  - Unknown → `{ status: 500, errorCode: "internal" }`

- **`src/ui/errors/ErrorBoundary.tsx`** — React error boundary component. Logs the error via `useLogger()`, emits the `flights.react.error` metric, shows a fallback UI with a "Retry" button that resets the boundary's state.

**Shared file ownership flag:** `src/routes/layout.tsx` is owned by 1F-layout. Sub-plans 1G-analytics (analytics loader mount), 1G-logger (provider wrap), and 1H (CSP nonce propagation + stream transform) modify this file via explicit tasks, referencing the 1F-layout-shipped version as the base.

**Exit gate for 1F-layout:**
- `/ru/smoke` and `/en/smoke` render via SSR in the `testing` env.
- `<head>` contains title, description, canonical, 9 hreflang alternates + `x-default`, OG tags, one JSON-LD block, one `<meta>` description (generated by 1F-seo).
- Missing `lang` URLs redirect 301 to `/ru/smoke`.
- 404 route returns HTTP 404 with the error page body.
- **Responsive baseline (R2):** Playwright renders `/ru/smoke` and `/ru/error/404` at widths **320, 768, 1280, 1920** — no horizontal scroll, all critical content visible, error page layout stable. Baselines committed under `tests/fixtures/phase-1/responsive/`.

---

### 1F-seo — SeoHead + hreflang + JsonLdRenderer contracts

**Exports:**

- **`src/ui/seo/SeoHead.tsx`** — the `<SeoHead>` component from design spec §6.5:

    ```ts
    export interface SeoHeadProps {
      title: string;
      description: string;
      canonical: string;
      hreflang: Array<{ lang: Language | "x-default"; href: string }>;
      og: {
        title: string; description: string; url: string;
        image: string; type: "website" | "article";
        locale: string; siteName: string;
      };
      twitter?: {
        card: "summary" | "summary_large_image";
        title?: string; description?: string; image?: string;
      };
      jsonLd?: unknown | unknown[];
      noindex?: boolean;
    }
    export function SeoHead(props: SeoHeadProps): JSX.Element;
    ```

- **`src/shared/seo/hreflang.ts`** — reusable reciprocal-hreflang builder:

    ```ts
    export function buildHreflangSet(args: {
      canonicalOrigin: string;
      pathWithoutLocale: string;   // e.g. "/onlineboard/flight/SU100-2025-01-15"
    }): Array<{ lang: Language | "x-default"; href: string }>;
    ```

  Per design spec §1.4: `x-default` points to the Russian (`ru`) variant.

- **`src/shared/seo/json-ld.tsx`** — `schema-dts`-typed JSON-LD renderer helper:

    ```ts
    import type { Thing } from "schema-dts";

    export interface JsonLdRendererProps {
      data: Thing | Thing[];
    }

    export function JsonLdRenderer(props: JsonLdRendererProps): JSX.Element;
    export function serializeJsonLd(data: Thing | Thing[]): string;
    ```

  Phase 2+ feature sub-plans ship typed builders (`buildFlightJsonLd`, `buildFlightSearchResultsJsonLd`, etc.) that consume `JsonLdRenderer` — no infrastructure work needed per feature.

**Exit gate for 1F-seo:** Unit tests cover `buildHreflangSet` for the 9 languages + `x-default`, `SeoHead` emits the full `<head>` shape, and `JsonLdRenderer` round-trips a typed `Thing` through `serializeJsonLd` → DOM string.

---

### 1G-logger — Logger contracts

**Sequencing:** The **first task** of 1G-logger is `src/observability/logger/types.ts` shipping **only type definitions**, so 1A-1's `HostContract` can import `Logger` without waiting on the runtime implementation.

**Exports:**

- **`src/observability/logger/types.ts`** (type-only, ships first):

    ```ts
    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 LogTransport {
      write(record: LogRecord): void;
      flush(): Promise<void>;
    }

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

- **`src/observability/logger/json-lines-transport.ts`** — default `JsonLinesHttpTransport` with batching, backpressure drop, redaction, `sendBeacon` flush.
- **`src/observability/logger/console-transport.ts`** — dev-mode transport.
- **`src/observability/logger/root.ts`** — `createRootLogger()` factory that reads env config and picks a transport.
- **`src/observability/logger/provider.tsx`** — React context + `useLogger()` hook. Server: request-scoped child logger. Client: shared root logger.

**A4-trigger task (from requirements gap R3):** When the customer provides the log format (A4 resolution), a follow-on task creates `src/observability/logger/customer-format-transport.ts` implementing the customer-specified format, and updates `createRootLogger()` to pick this transport by default. **No consumer code changes** — the `Logger` interface is stable. This task is attached to 1G-logger's exit gate and fires on A4 resolution (could be during Phase 1, or deferred to early Phase 2).

**Exit gate for 1G-logger:**
- Type-only file ships first, verified by 1A-1 successfully importing `Logger`.
- Vitest tests cover: batching + flush; redaction of sensitive fields; transport backpressure drops old records; dev-mode console transport; `child()` context propagation.

---

### 1G-metrics — OpenTelemetry + custom instruments contracts

**Exports:**

- **`src/observability/metrics/otel.ts`** — OpenTelemetry setup (the **only** file allowed to import from `@opentelemetry/sdk-metrics`, enforced by 1A-3):

    ```ts
    export function initServerOtel(env: Env): void;    // called once per Node process
    export function initBrowserOtel(env: Env): void;   // called once per tab
    export function getMeter(name: string): Meter;     // thin re-export of @opentelemetry/api
    export function getTracer(name: string): Tracer;
    ```

- **`src/observability/metrics/custom.ts`** — minimum-set custom metrics from design spec §7.3 as exported module-level instruments. **This pattern is safe** because the instruments are created against `@opentelemetry/api`'s proxy meter, which lazy-resolves to the real meter after `initServerOtel`/`initBrowserOtel` runs:

    ```ts
    import { metrics } from "@opentelemetry/api";

    const meter = metrics.getMeter("flights");

    export const flightsSsrRequestDuration = meter.createHistogram("flights.ssr.request.duration");
    export const flightsApiRequestDuration = meter.createHistogram("flights.api.request.duration");
    export const flightsApiError = meter.createCounter("flights.api.error");
    export const flightsSignalRConnected = meter.createUpDownCounter("flights.signalr.connected");
    export const flightsSignalRMessageReceived = meter.createCounter("flights.signalr.message.received");
    export const flightsSignalRDisconnect = meter.createCounter("flights.signalr.disconnect");
    export const flightsFeatureRender = meter.createCounter("flights.feature.render");
    export const flightsReactError = meter.createCounter("flights.react.error");
    // web-vitals histograms created at init time inside initBrowserOtel, not exported statically
    ```

**Shared file ownership flag:** `modern.config.ts` (owned by 1A-2) — 1G-metrics modifies it to wire OTel SDK init + request tracing plugin into the Modern.js middleware chain.

**Exit gate for 1G-metrics:**
- Integration test: `initServerOtel` runs, a counter is incremented, and the test reader observes the recorded value (proves the proxy meter resolved correctly).
- ESLint rule from 1A-3 blocks a fabricated `import { MeterProvider } from "@opentelemetry/sdk-metrics"` in a file outside `otel.ts`.

---

### 1G-analytics — Analytics facade contracts

**Exports:**

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

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

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

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

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

- **`src/observability/analytics/facade.ts`**:

    ```ts
    export function createAnalytics(options: {
      enabled: AnalyticsProviders;
      consent: { analytics: boolean; telemetry: boolean };
      logger: Logger;
    }): Analytics;
    ```

- **`src/observability/analytics/adapters/{metrica,ctm,variocube,dynatrace}.ts`** — four adapters implementing:

    ```ts
    export interface AnalyticsAdapter {
      name: string;
      load(): Promise<void>;
      track(event: string, props?: AnalyticsProps): void;
      page(url: string, props?: AnalyticsProps): void;
    }
    ```

  **Phase 1 ships these as structured stubs** (R8): each stub's `load()`/`track()`/`page()` emits an `AnalyticsEvent` to a **test-observable sink** (`src/observability/analytics/sink.ts`) with the `provider` field set to the adapter's name. Real vendor scripts wire in Phase 2A (alongside the Online Board migration) after A7 resolves.

- **`src/observability/analytics/sink.ts`** — test-observable event sink; exports `getRecordedEvents()` and `resetEvents()` for integration tests. In production, the sink is a no-op ring buffer.

- **`src/observability/analytics/loader.tsx`** — `<AnalyticsLoader>` component that mounts in the root layout (owned by 1F-layout, modified by 1G-analytics to add the mount). Waits for `requestIdleCallback`, then imports enabled adapters and calls `.load()`.

- **`src/observability/analytics/provider.tsx`** — React context + `useAnalytics()` hook. Server: returns a `NoopAnalytics`. Client: returns the instance from `<AnalyticsLoader>`.

**Exit gate for 1G-analytics:**
- Integration test: smoke route calls `track("smoke.pageview")`, all four stub adapters emit exactly one `AnalyticsEvent` to the sink with matching props.
- `consent.analytics = false` short-circuits before any adapter is invoked.
- Adapter load failure emits `flights.analytics.load_failed` counter.

---

### 1H — Security hardening contracts

**Exports:**

- **`src/server/middleware/csp.ts`** — Modern.js middleware that generates a per-request nonce, sets the `Content-Security-Policy` header (per design spec §8.1), and exposes the nonce to the React render tree via a request-scoped context.

    ```ts
    export function cspMiddleware(options: { reportOnly?: boolean }): ModernMiddleware;
    export const CspNonceContext: React.Context<string>; // default ""; components reading client-side no-op on empty
    ```

- **`src/server/middleware/nonce-stream-transform.ts`** — **workaround for React issue #24883.** React 18's `renderToPipeableStream({ nonce })` only applies the nonce to inline `bootstrapScriptContent`, **not to external `bootstrapScripts` src URLs.** This middleware post-processes the SSR HTML stream to inject `nonce="{nonce}"` on every `<script>` tag that doesn't already have one.

    ```ts
    export function wrapSsrStreamWithNonce(
      stream: NodeJS.ReadableStream,
      nonce: string,
    ): NodeJS.ReadableStream;
    ```

  Integration test (required for the exit gate): every `<script>` tag in the SSR output of `/ru/smoke` carries the per-request nonce.

- **`src/server/middleware/security-headers.ts`** — Modern.js middleware setting HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy (per design spec §8.1).

- **`src/shared/storage.ts`** — the only module allowed to touch `window.localStorage` / `window.sessionStorage` (enforced by 1A-3). Namespaced keys + Zod schema validation on read:

    ```ts
    import type { ZodSchema } from "zod";

    export const storage = {
      get<T>(key: string, schema: ZodSchema<T>): T | null,
      set<T>(key: string, value: T, schema: ZodSchema<T>): void,
      delete(key: string): void,
      clear(): void,
    };
    ```

**Shared file ownership flags:**

- `modern.config.ts` (owned by 1A-2) — 1H modifies to register CSP, nonce-stream-transform, and security-headers middlewares.
- `src/routes/layout.tsx` (owned by 1F-layout) — 1H modifies to propagate the CSP nonce into `<Head>`-emitted inline scripts.

**Exit gate for 1H:**
- SSR render emits a CSP header with a unique nonce per request; **every** `<script>` in the output (both inline and external `bootstrapScripts`) carries the nonce.
- `storage.get` with a mismatching schema returns `null` rather than corrupt data.
- ESLint: `window.localStorage` usage outside `src/shared/storage.ts` fails lint (rule from 1A-3).

---

### 1I — Deploy pipeline + runbook contracts

**Exports:**

- **`Dockerfile`** at repo root — multi-stage build: **Node 24** base, installs pnpm, runs `pnpm build:standalone`, produces a minimal production image with the standalone server as the entrypoint.
- **`Dockerfile.remote`** — static-file server image for the remote-mode artifact (nginx base, copies `dist/remote/` into `/usr/share/nginx/html`).
- **`src/server/routes/health.ts`** — health endpoint registered at `/health` via Modern.js's middleware API:

    ```ts
    export function healthMiddleware(options: {
      apiClient: ApiClient;
      upstreamTimeoutMs?: number;
    }): ModernMiddleware;
    ```

  Returns 200 if the last successful upstream REST ping is within 60s, 503 otherwise.

- **`src/server/shutdown.ts`** — graceful shutdown handler: on SIGTERM, stops accepting new requests, drains in-flight for 30s, flushes the log buffer, exits.
- **`.github/workflows/deploy.yml`** — reuses PR-built artifacts, builds Docker images, pushes to the customer's registry, triggers a canary deploy to the `testing` env via the customer's deployment tool, runs post-deploy smoke test, auto-rolls back on health-check failure.
- **`docs/superpowers/phase-1/runbook.md`** — operational runbook (requirements gap R1) covering:
  1. Incident response decision tree (user impact → severity → who pages whom)
  2. Canary rollout procedure with monitoring checklist
  3. Rollback procedure (auto and manual)
  4. Health-check interpretation (`/health` 200/503 meanings, common causes)
  5. Log query cookbook (how to find errors in the customer log aggregator)
  6. Known-failure playbooks for: upstream API down, SignalR hub offline, CSP violation spike, analytics adapter load failure, OTel exporter unreachable.

**Shared file ownership flag:** `modern.config.ts` (owned by 1A-2) — 1I modifies to register the health middleware and the graceful shutdown hook.

**Exit gate for 1I:**
- Merge to `main` triggers the deploy workflow; `testing` env shows a new revision; `/health` returns 200.
- SIGTERM drains in-flight and exits 0 within 31s.
- Forced health-check failure auto-rolls back.
- Runbook reviewed by at least one engineer and one ops-adjacent reader; 6-hour recovery SLA walked through step-by-step.

---

## Shared files — cross-sub-plan modification table

| File | Primary owner | Also modified by | What the modifiers add |
|---|---|---|---|
| `modern.config.ts` | 1A-2 | 1G-metrics | OTel SDK init + request tracing plugin |
|  |  | 1H | CSP + nonce-stream-transform + security-headers middleware |
|  |  | 1I | Health middleware + graceful shutdown hook |
| `src/routes/layout.tsx` | 1F-layout | 1G-logger | `<LoggerProvider>` wrap |
|  |  | 1G-analytics | `<AnalyticsLoader>` mount |
|  |  | 1H | CSP nonce propagation into `<Head>` inline scripts |
| `src/routes/[lang]/layout.tsx` | 1F-layout | 1G-logger | Per-request logger child creation |
|  |  | 1G-metrics | OTel span attachment |
| `.eslintrc.cjs` | 1A-1 (base) | 1A-3 | Boundaries plugin rules + `no-restricted-imports` rules |
| `package.json` | 1A-1 | 1A-2 | `@module-federation/modern-js`, Modern.js, Rspack (pinned from spike) |
|  |  | 1B | CI-related dev deps (`@vitest/coverage-v8`, size-limit, osv-scanner wrappers) |
|  |  | 1C | `i18next`, `react-i18next`, `i18next-icu`, `i18next-resources-to-backend` |
|  |  | 1D | `lru-cache@^10`, `undici` (pinned) |
|  |  | 1E | `@microsoft/signalr` |
|  |  | 1F-seo | `schema-dts` |
|  |  | 1G-logger | — (stdlib only) |
|  |  | 1G-metrics | `@opentelemetry/api`, `@opentelemetry/sdk-node`, `@opentelemetry/sdk-metrics`, `@opentelemetry/sdk-web`, `@opentelemetry/exporter-trace-otlp-http`, `@opentelemetry/exporter-metrics-otlp-http`, `web-vitals` |
|  |  | 1G-analytics | — (stubs only in Phase 1) |
|  |  | 1I | — (Node 24 native `undici`; additional runtime deps TBD) |
| `tsconfig.json` | 1A-1 | — | No modifications after 1A-1 |

**Modification protocol.** When a downstream sub-plan modifies a file owned by another, its task must:
1. Explicitly reference the primary owner ("modifying `modern.config.ts`, which was created in 1A-2 Task N").
2. Quote the expected pre-modification state of the file (from the primary owner's exit gate).
3. Show the full post-modification file, not just a diff.
4. Re-run the primary owner's exit-gate test to prove the modification didn't break it.

---

## Spec-coverage matrix

| Spec section | Topic | Sub-plan(s) |
|---|---|---|
| §1.1, §1.2 | Runtime topology, dependency direction | 1A-1 + 1A-3 |
| §1.3 | `src/` tree | 1A-1 |
| §1.4 | URL language-prefix policy | 1C + 1F-layout |
| §2.1 | Two build targets | 1A-2 |
| §2.2 | `mf-manifest.json` + exposed modules | 1A-2 |
| §2.3 | Shared dependencies (React singleton) | 1A-2 |
| §2.4 | `HostContract` | 1A-1 (type defined; `RemoteLoader` consumer path in 1A-2) |
| §2.5 | Build artifacts + deploy shape | 1A-2 + 1I |
| §3.1 | SSR request lifecycle | 1F-layout + 1G-logger + 1G-metrics + 1H |
| §3.2 | Two SSR invariants | 1A-3 (ESLint) + 1E (client-only dynamic import) |
| §3.3 | File-based routing | 1F-layout |
| §3.4 | Loaders, Suspense, `React.lazy` | 1F-layout (smoke route demonstrates the pattern) |
| §3.5 | URL parity — ported serializers | Phase 2 (deferred with 1J) |
| §3.6 | Canonical / hreflang / redirects | 1F-seo + 1F-layout |
| §4.1 | REST client | 1D |
| §4.2 | Caching strategy (three caches, `lru-cache@^10` byte cap) | 1D |
| §4.3 | SSR → client data handoff | 1F-layout (smoke route demonstrates pattern) |
| §4.4 | SignalR wrapper + hook | 1E |
| §4.5 | State management | (no Phase 1 work) |
| §4.6 | Error handling path | 1D (`ApiError` types) + 1F-layout (`errorToResponse` + `ErrorBoundary`) |
| §5 | UI adapter + styling | (Phase 2) |
| §6.1–§6.4 | i18n | 1C |
| §6.5 | `<SeoHead>` | 1F-seo |
| §6.6 | JSON-LD schema coverage | 1F-seo (`JsonLdRenderer` helper; feature-specific builders in Phase 2+) |
| §6.7 | OG images (static default) | 1F-seo |
| §6.8 | Canonical/hreflang correctness | 1F-seo + 1F-layout |
| §7.1, §7.2 | Logging | 1G-logger |
| §7.3 | Metrics | 1G-metrics |
| §7.4 | Analytics | 1G-analytics |
| §7.5 | Error handling layers | 1F-layout + 1G-logger + 1G-metrics |
| §7.6 | Correlation | 1G-metrics (trace IDs) + 1G-logger (propagation) |
| §7.7 | Performance budgets | 1B (bundle gate) + 1G-metrics (web-vitals reporting) |
| §7.8 | Dev-mode ergonomics | 1G-logger (console transport) |
| §8.1 | Security / isolation | 1H |
| §8.2 | Performance / 100 RPS | 1A-1 (Node 24 tuning) + 1B (bundle gate) + 1I (CDN headers) |
| §8.3 | Reliability / geo-distribution | 1I |
| §8.4 | Testing strategy | 1B (parity harnesses deferred to Phase 2) |
| §8.5 | CI/CD | 1B + 1I |

---

## Phase 1 global exit gate — checklist

- [ ] **1A-1:** `pnpm typecheck` + `pnpm lint` green on baseline src tree; `src/env/` Zod validation passes; frozen-barrel rule documented; `HostContract` type in place.
- [ ] **1A-2:** MF spike doc + pinned version matrix committed; `pnpm build:both` produces valid `dist/standalone/` + `dist/remote/` + `mf-manifest.json`; `loadRemoteModule` integration test green.
- [ ] **1A-3:** Every ESLint boundary + restricted-import rule has a passing fabricated-violation test.
- [ ] **1B:** Per-PR CI runs green under 20 min. Nightly pipeline runs successfully at least once.
- [ ] **1C:** 9 locale JSON files in place; i18n SSR + hydration roundtrip test passes; zero missing-key warnings on the smoke route.
- [ ] **1D:** `ApiClient` unit tests green (retry via `undici.RetryAgent`, timeout, `Retry-After`, circuit breaker, three caches including 100MB byte-cap LRU); isomorphic usage verified SSR + client.
- [ ] **1E:** SignalR wrapper tests green (Strict Mode double-mount, grace-period close, SSR import absence); `useLiveFlights<TParams, TData>` generic signature verified by a typed test consumer.
- [ ] **1F-layout:** `/ru/smoke` + `/en/smoke` render SSR in `testing` env with full `<head>`; `errorToResponse` mapper round-trips all five error shapes; **responsive baseline** passes at 320/768/1280/1920.
- [ ] **1F-seo:** `buildHreflangSet` covers 9 langs + `x-default`; `JsonLdRenderer` round-trips a typed `Thing`.
- [ ] **1G-logger:** Type-only file shipped first (consumed by 1A-1); batching + flush + redaction + `child()` tests green; A4-trigger task documented.
- [ ] **1G-metrics:** OTel init + custom instruments emit through proxy meter; ESLint guard on `@opentelemetry/sdk-metrics` proven.
- [ ] **1G-analytics:** Four stub adapters fan out to structured sink; consent short-circuit verified; smoke route emits one log + one metric + one analytics event, observed at the sink.
- [ ] **1H:** CSP header on `testing` env contains per-request nonce; **every** `<script>` tag (inline + external) carries the nonce via the stream transform; security headers present on every response; storage schema validation verified.
- [ ] **1I:** Merge to `main` auto-deploys to `testing` via canary; `/health` returns 200; SIGTERM drains within 31s; forced health failure auto-rolls back; **runbook** reviewed and 6-hour SLA walked through.
- [ ] **Security scan:** `osv-scanner` + `npm audit` green on `main`.
- [ ] **Bundle-size gate:** all Phase 1 budgets from design spec §8.2 met on the smoke route.
- [ ] **Coverage:** ≥ 70% line coverage on `src/shared/`, `src/observability/`, `src/ui/`, `src/i18n/`.
- [ ] **Documentation:** `docs/superpowers/phase-1/README.md` indexes what Phase 1 shipped + how to run each subsystem locally + how to debug failures + pointer to the runbook.

---

## Risks + open questions for Phase 1

1. **Customer template (A1) still unknown.** If it arrives during Phase 1, 1A-1's rename-pass rework task executes. Frozen-barrel rule keeps the rename bounded.

2. **Customer log format (A4) still unknown.** 1G-logger ships `JsonLinesHttpTransport`. A4-trigger task creates `CustomerFormatTransport` when the format arrives. No consumer changes.

3. **Analytics vendor credentials (A7) still unknown.** 1G-analytics ships structured stubs. Real scripts land in Phase 2A.

4. **CI provider (A3) is a hard blocker for 1A-1.** Assumed GitHub Actions in the master plan; if the customer uses GitLab CI or TeamCity, 1B's workflow YAML is rewritten but the underlying scripts are portable.

5. **Modern.js + MF 2.0 interaction (T1).** De-risked by the 1A-2 spike as its first task — escalation path defined if the spike fails.

6. **Node 24 availability on customer VMs (A9, new).** Hard blocker for 1A-1. Node 20 LTS ends April 2026; running Phase 1 on Node 20 would bake in an immediate upgrade debt.

7. **React issue #24883 (CSP nonce gap on bootstrapScripts).** Known React limitation. 1H ships a stream-transform workaround with an integration test; if React fixes this upstream before Phase 1 exits, the workaround can be removed in 1H's follow-up.

8. **`lru-cache@^10` byte-sizing cost.** `sizeCalculation` runs on every `set()`. For 100MB cap with `JSON.stringify(value).length` as the default sizer, this is O(n) per cache write. Mitigation: pass a custom `sizeCalculation` that uses pre-computed size hints from the API response (e.g., `content-length` header) where available.

9. **SSR + `<Suspense>` + streaming + React 18 concurrent mode** has known interop issues with some libraries. 1F-layout's smoke route uses `React.lazy()` + `<Suspense>` + a data loader as a deliberate stress test.

---

## How to write each sub-plan

When the user is ready to execute a sub-plan, re-invoke `superpowers:writing-plans` with a specific prompt like:

> "Write sub-plan 1A-1 (project skeleton) from `docs/superpowers/plans/2026-04-14-phase-1-foundation-master.md`. Target file: `docs/superpowers/plans/2026-04-14-phase-1a1-skeleton.md`. Follow the contracts defined in the master plan §1A-1 exactly; reference the design spec §1.3, §2.1 as source material."

The sub-plan writer must:

1. Read this master plan in full for the dependency + contract context.
2. Read the relevant design spec sections.
3. Read any upstream sub-plans that have already been written (their exit gates lock in file/API shapes).
4. Produce a fully TDD-granular plan at the shape of `docs/superpowers/plans/2026-04-14-phase-0-preflight.md`.
5. Match the contracts in this master plan byte-for-byte on type signatures. Any contract change requires updating this master plan first.

---

## Self-review

**Spec coverage.** Every design-spec section §1–§8 maps to at least one sub-plan in the spec-coverage matrix. §9 (Phase 2–6 migration) and parity harnesses (§3.5, §8.4) are explicitly deferred to Phase 2.

**Placeholder scan.** No `TBD` / `TODO` / `FIXME` outside of the "TBW" markers on sub-plan filenames (deliberate indicators of un-written sub-plans).

**Internal consistency.** Cross-checked: `Logger` extracted as type-only file in 1G-logger's first task → used by 1A-1, 1D, 1F-layout; `Language` in 1C → used by 1F-seo, 1F-layout; `ApiClient` + caches in 1D → used by 1F-layout, 1I; `ConnectionStatus` in 1E → Phase 2 consumer; `AnalyticsProviders` in 1G-analytics → used by `Env` in 1A-1 (via type-only import from `src/observability/analytics/types`).

**Plan-order cycle resolution.** The `HostContract` → `Logger` dependency between 1A-1 and 1G-logger is resolved by 1G-logger shipping `src/observability/logger/types.ts` as its first task. The `Env` → `AnalyticsProviders` dependency is resolved by 1G-analytics owning `src/observability/analytics/types.ts` as a type-only file that 1A-1 imports.

**Execution order correctness.** The dependency graph has no cycles after the type-only extractions. Critical path: 1A-1 → 1A-2 → 1A-3 → 1C → 1F-layout → 1H → 1I (seven serial sub-plans with one engineer).

---

## Next step

- **If you approve this master plan:** say so, and I'll write sub-plan **1A-1** (project skeleton) fully in the next session. Then we iterate.
- **If you want changes:** tell me, I revise.
