plan/react-rewrite #1
@@ -0,0 +1,725 @@
|
||||
# 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 (1A through 1J) 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, URL parity harness (infra only, no real URLs yet), SEO parity harness (infra only), VRT harness wired to Phase 0 baselines.
|
||||
- Security hardening live: CSP with per-request nonce, HTTP security headers, dependency scanning green.
|
||||
- Canary deploy pipeline functional (smoke route deployed via canary path with auto-rollback on health-check failure).
|
||||
|
||||
**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+).
|
||||
|
||||
---
|
||||
|
||||
## Sub-plan inventory
|
||||
|
||||
| ID | Sub-plan | Spec section | Estimated size | File |
|
||||
|---|---|---|---|---|
|
||||
| **1A** | Project skeleton + dual build targets | §1.3, §2.1, §2.2, §2.5 | Large | `docs/superpowers/plans/2026-04-14-phase-1a-skeleton.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** | Root layout + error routes + smoke route + SeoHead | §1.3, §3.1, §3.3, §3.6, §6.5, §6.8 | Medium | `2026-04-14-phase-1f-layout.md` (TBW) |
|
||||
| **1G** | Observability (logger, OTel, analytics) | §7.1–§7.8 | Large | `2026-04-14-phase-1g-observability.md` (TBW) |
|
||||
| **1H** | Security hardening (CSP, headers, storage) | §8.1 | Small | `2026-04-14-phase-1h-security.md` (TBW) |
|
||||
| **1I** | Deploy pipeline + health + graceful shutdown | §8.3, §8.5 | Medium | `2026-04-14-phase-1i-deploy.md` (TBW) |
|
||||
| **1J** | Parity harnesses (URL / SEO / VRT) | §3.5, §8.4 | Medium | `2026-04-14-phase-1j-parity-harness.md` (TBW) |
|
||||
|
||||
Sizes: **Small** ≈ 5–10 tasks, **Medium** ≈ 10–20 tasks, **Large** ≈ 20–40 tasks.
|
||||
|
||||
---
|
||||
|
||||
## Dependency graph
|
||||
|
||||
```
|
||||
┌────────────────────┐
|
||||
│ 1A Skeleton │◄── everything depends on 1A
|
||||
└─────┬──────────────┘
|
||||
┌──────────────────┬┴──────┬────────────┬────────────────┐
|
||||
▼ ▼ ▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────┐ ┌──────────┐ ┌──────────────┐
|
||||
│ 1B CI │ │ 1C i18n │ │1D API│ │1E SignalR│ │1G Observabil.│
|
||||
└─────┬────┘ └─────┬────┘ └───┬──┘ └──────────┘ └──────┬───────┘
|
||||
│ │ │ │
|
||||
│ └──────┬───┘ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────┐ │
|
||||
│ │ 1F Root layout + routes │◄─────┘
|
||||
│ │ (consumes 1C + 1D + 1G) │
|
||||
│ └──────┬───────────────────────┘
|
||||
│ ▼
|
||||
│ ┌──────────────────────────────┐
|
||||
│ │ 1H Security hardening │
|
||||
│ │ (middleware into 1F) │
|
||||
│ └──────┬───────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────────────────────────────┐
|
||||
│ 1I Deploy pipeline │
|
||||
│ (consumes 1A + 1B + 1H) │
|
||||
└───────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────┐
|
||||
│ 1J Parity harnesses │
|
||||
│ (consumes 1A + 1B + 1F) │
|
||||
└───────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Execution order
|
||||
|
||||
**Serial (1 engineer):** 1A → 1B → 1C → 1D → 1G → 1E → 1F → 1H → 1I → 1J
|
||||
Rationale: 1A unlocks everything. 1B unlocks running tests in CI. 1C and 1D unlock 1F. 1G is independent but 1F imports its hooks, so 1G comes before 1F. 1E can come after 1F because it's only consumed in Phase 2. 1H modifies files 1F creates, so it follows 1F. 1I consumes 1A+1B+1H. 1J consumes 1F.
|
||||
|
||||
**Parallel (2+ engineers):** After 1A ships, 1B / 1C / 1D / 1E / 1G can proceed in parallel. 1F and later are sequential because of shared-file constraints.
|
||||
|
||||
### Critical path
|
||||
|
||||
**1A → 1C → 1F → 1H → 1I → exit gate** is the critical path with one engineer. Optimizing this path (by finishing 1A fast and prioritizing 1C + 1F + 1H as a single block) is the only way to shorten the Phase 1 calendar meaningfully.
|
||||
|
||||
---
|
||||
|
||||
## 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 — 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.
|
||||
- **`modern.config.ts`** — the Modern.js build config with `BUILD_TARGET=standalone|remote` branching. Later sub-plans (1G, 1H, 1I) modify this file via explicit "modify" tasks — 1A owns the base structure.
|
||||
- **`tsconfig.json`** — strict mode, path aliases (`@/` → `src/`, `@phase0/` → `scripts/phase-0/`), no-unchecked-indexed-access, isolatedModules.
|
||||
- **`.eslintrc.cjs`** — including the `boundaries` plugin rules enforcing the layered dependency direction from design spec §1.2 (`features/` cannot import `routes/` or `mf/`; `ui/` cannot import `features/`; etc.).
|
||||
- **`package.json` scripts** — `dev`, `build:standalone`, `build:remote`, `build:both`, `test`, `test:coverage`, `lint`, `typecheck`.
|
||||
- **`src/env/index.ts`** — runtime env-var reader returning a typed `Env` object. Other sub-plans read env vars exclusively through this module.
|
||||
- **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.
|
||||
|
||||
**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: { metrica: boolean; ctm: boolean; variocube: boolean; dynatrace: boolean };
|
||||
VERSION: string; // git sha, injected at build time
|
||||
}
|
||||
export function getEnv(): Env;
|
||||
```
|
||||
|
||||
**Exit gate for 1A:** `pnpm build:both` produces `dist/standalone/` (Node server + client bundle) and `dist/remote/` (static chunks + `mf-manifest.json`) with zero type errors and zero ESLint errors. The smoke build (no features wired yet) renders a blank `<div id="root" />` on both targets.
|
||||
|
||||
---
|
||||
|
||||
### 1B — CI pipeline contracts
|
||||
|
||||
**Exports:**
|
||||
|
||||
- **`.github/workflows/ci.yml`** (or equivalent for the CI provider chosen in Phase 0 assumption 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.
|
||||
|
||||
**TypeScript contracts** (the above — downstream sub-plans import from `@/i18n`).
|
||||
|
||||
**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 ApiClientOptions {
|
||||
baseUrl: string;
|
||||
locale: Language;
|
||||
traceId?: string;
|
||||
fetchImpl?: typeof fetch; // for tests
|
||||
defaultTimeoutMs?: number; // default 5000
|
||||
circuitBreaker?: CircuitBreakerOptions;
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
```
|
||||
|
||||
- **`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`** — two cache implementations:
|
||||
|
||||
```ts
|
||||
export class RequestScopedCache {
|
||||
get<T>(key: string): Promise<T> | undefined;
|
||||
set<T>(key: string, promise: Promise<T>): void;
|
||||
}
|
||||
|
||||
export class TtlCache<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;
|
||||
}
|
||||
```
|
||||
|
||||
- **`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/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.
|
||||
|
||||
**TypeScript contracts:** the above. All ported feature code (Phase 2+) imports exclusively from `@/shared/api`.
|
||||
|
||||
**Exit gate for 1D:** Vitest tests cover: success response deserialization; retry on 5xx; no retry on 4xx; timeout; circuit breaker open/half-open/closed state transitions; request-scoped cache dedup; TTL eviction.
|
||||
|
||||
---
|
||||
|
||||
### 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; // returns unsubscribe
|
||||
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`**:
|
||||
|
||||
```ts
|
||||
export function useLiveFlights<T>(
|
||||
params: { date: string; departure?: string; arrival?: string },
|
||||
initialData: T[],
|
||||
config: { hubUrl: string; channelKey: (params: object) => string },
|
||||
): { data: T[]; connectionStatus: ConnectionStatus };
|
||||
```
|
||||
|
||||
SSR-safe: during SSR, returns `{ data: initialData, connectionStatus: "idle" }` without importing `@microsoft/signalr`.
|
||||
|
||||
**TypeScript contracts:** the above. Phase 2 Online Board consumes `useLiveFlights` and the `ConnectionStatus` type.
|
||||
|
||||
**Exit gate for 1E:**
|
||||
|
||||
- Unit test: two rapid `useEffect` mounts (Strict Mode double-invoke simulation) result in exactly one `HubConnection.start()` call.
|
||||
- Unit test: unmount + remount within the grace period reuses the connection; unmount + remount after the grace period creates a fresh one.
|
||||
- Unit test: SSR render path does not import `@microsoft/signalr` (asserted by inspecting the SSR bundle stats for the absence of the package).
|
||||
|
||||
---
|
||||
|
||||
### 1F — Root layout + routes + SeoHead contracts
|
||||
|
||||
**Exports:**
|
||||
|
||||
- **`src/routes/layout.tsx`** — root HTML shell: `<html>`, `<head>`, `<Scripts>`, `<Links>`, root `<I18nProvider>`, root `<ApiClientProvider>`, root `<ErrorBoundary>`, root `<AnalyticsLoader>` (from 1G).
|
||||
|
||||
- **`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, passes them into `<SeoHead>`.
|
||||
|
||||
- **`src/routes/error/[code]/page.tsx`** — error page rendered for `code ∈ {404, 500, 503}`. Ports the existing Angular error component layout.
|
||||
|
||||
- **`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`.
|
||||
|
||||
- **`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 }>;
|
||||
```
|
||||
|
||||
- **`src/ui/errors/ErrorBoundary.tsx`** — React error boundary component. Logs the error (via 1G's `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. Sub-plans 1G (analytics loader mount) and 1H (CSP nonce propagation) **modify** this file via explicit tasks in their own plans, referencing the 1F-shipped version as the base.
|
||||
|
||||
**TypeScript contracts:** the above. Phase 2+ features import `SeoHead` + `buildHreflangSet` + `ErrorBoundary`.
|
||||
|
||||
**Exit gate for 1F:** `/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. Missing `lang` URLs redirect 301 to `/ru/smoke`. 404 route returns HTTP 404 with the error page body.
|
||||
|
||||
---
|
||||
|
||||
### 1G — Observability contracts
|
||||
|
||||
**Exports:**
|
||||
|
||||
- **`src/observability/logger/types.ts`** — the `Logger` interface from design spec §7.2:
|
||||
|
||||
```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 that pipes records to `console[level]`.
|
||||
|
||||
- **`src/observability/logger/root.ts`** — `createRootLogger()` factory that reads env config and picks a transport.
|
||||
|
||||
- **`src/observability/logger/provider.tsx`** — React context + `useLogger()` hook. On the server, the hook returns the request-scoped child logger; on the client, it returns the shared root logger.
|
||||
|
||||
- **`src/observability/metrics/otel.ts`** — OpenTelemetry setup:
|
||||
|
||||
```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`** — the minimum-set custom metrics from design spec §7.3 as exported instruments:
|
||||
|
||||
```ts
|
||||
export const flightsSsrRequestDuration: Histogram;
|
||||
export const flightsApiRequestDuration: Histogram;
|
||||
export const flightsApiError: Counter;
|
||||
export const flightsSignalRConnected: UpDownCounter;
|
||||
export const flightsSignalRMessageReceived: Counter;
|
||||
export const flightsSignalRDisconnect: Counter;
|
||||
export const flightsFeatureRender: Counter;
|
||||
export const flightsReactError: Counter;
|
||||
// web-vitals histograms created at init time, not exported statically
|
||||
```
|
||||
|
||||
- **`src/observability/analytics/facade.ts`**:
|
||||
|
||||
```ts
|
||||
export interface AnalyticsProps { [k: string]: unknown; }
|
||||
|
||||
export interface Analytics {
|
||||
track(event: string, props?: AnalyticsProps): void;
|
||||
page(url: string, props?: AnalyticsProps): void;
|
||||
}
|
||||
|
||||
export function createAnalytics(options: {
|
||||
enabled: Env["ANALYTICS_ENABLED"];
|
||||
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 **stubs that simulate loading and log calls instead of hitting real vendor scripts**. Real vendor scripts are wired in Phase 2A (alongside the Online Board migration) after the customer provides credentials (see Phase 0 task 14, assumption A7).
|
||||
|
||||
- **`src/observability/analytics/loader.tsx`** — `<AnalyticsLoader>` component that mounts in the root layout (owned by 1F, modified by 1G to add the mount). Waits for `requestIdleCallback`, then imports enabled adapters and calls `.load()`.
|
||||
|
||||
- **`src/observability/analytics/provider.tsx`** — React context + `useAnalytics()` hook. On the server, returns a `NoopAnalytics`. On the client, returns the instance created by `<AnalyticsLoader>`.
|
||||
|
||||
**TypeScript contracts:** the above. `Logger`, `Analytics`, `getMeter`, `getTracer` are the four surfaces every feature imports from.
|
||||
|
||||
**Exit gate for 1G:**
|
||||
|
||||
- Vitest tests cover: logger batching + flush; redaction of sensitive fields; transport backpressure drops old records; dev-mode console transport; analytics facade fans out to all four stub adapters; consent=false short-circuits; adapter load failure emits `flights.analytics.load_failed` metric.
|
||||
- Integration test: smoke route (1F) emits one log, one metric, and one analytics event — all three observable in a test harness capturing the pipeline outputs.
|
||||
|
||||
---
|
||||
|
||||
### 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 with the nonce, 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>;
|
||||
```
|
||||
|
||||
- **`src/server/middleware/security-headers.ts`** — Modern.js middleware that sets HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy.
|
||||
|
||||
- **`src/shared/storage.ts`** — the only module allowed to touch `window.localStorage` / `window.sessionStorage`. Namespaced keys + redaction on write:
|
||||
|
||||
```ts
|
||||
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,
|
||||
};
|
||||
```
|
||||
|
||||
A zero-dep ESLint rule in `.eslintrc.cjs` forbids `window.localStorage` references anywhere except `src/shared/storage.ts`.
|
||||
|
||||
- **`.eslintrc.cjs` additions** — `no-eval`, `no-implied-eval`, custom rule banning raw `innerHTML` assignments in TSX (handled by the `react/no-danger` rule + a custom augmentation).
|
||||
|
||||
**Shared file ownership flags:**
|
||||
|
||||
- `modern.config.ts` (owned by 1A) — 1H modifies it to register the two middlewares.
|
||||
- `src/routes/layout.tsx` (owned by 1F) — 1H modifies it to propagate the CSP nonce into `<Head>`-emitted inline scripts.
|
||||
|
||||
**TypeScript contracts:** the middleware signatures + `storage` API.
|
||||
|
||||
**Exit gate for 1H:**
|
||||
|
||||
- Test: an SSR render emits a CSP header containing a unique nonce per request, and every inline `<script>` in the output carries that nonce.
|
||||
- Test: `storage.get` with a schema that doesn't match returns `null` rather than corrupt data.
|
||||
- ESLint: `window.localStorage` usage outside `src/shared/storage.ts` fails lint.
|
||||
|
||||
---
|
||||
|
||||
### 1I — Deploy pipeline contracts
|
||||
|
||||
**Exports:**
|
||||
|
||||
- **`Dockerfile`** at repo root — multi-stage build: Node 20 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. Registered during server init.
|
||||
|
||||
- **`.github/workflows/deploy.yml`** — deploy workflow: reuses PR-built artifacts, builds Docker images, pushes to the customer's registry (registry URL is a secret), 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.
|
||||
|
||||
**Shared file ownership flag:** `modern.config.ts` (owned by 1A) — 1I modifies to add the health middleware registration and the graceful shutdown hook.
|
||||
|
||||
**TypeScript contracts:** `healthMiddleware` signature.
|
||||
|
||||
**Exit gate for 1I:** A merge to `main` triggers the deploy workflow. `testing` env shows a new revision. `/health` returns 200. Killing the server with SIGTERM drains in-flight requests and exits with code 0 within 31s. A forced health-check failure auto-rolls back.
|
||||
|
||||
---
|
||||
|
||||
### 1J — Parity harness contracts
|
||||
|
||||
**Exports:**
|
||||
|
||||
- **`tests/parity/url/harness.ts`** — framework for URL parity tests. Feature sub-plans in Phase 2+ register a serializer + a fixture file; the harness runs table-driven tests + `fast-check` fuzz tests automatically.
|
||||
|
||||
```ts
|
||||
export interface UrlParityConfig<TQuery> {
|
||||
feature: RouteFeature;
|
||||
fixturePath: string; // e.g. "tests/fixtures/phase-0/url-corpus/onlineboard.json"
|
||||
parse(raw: string): TQuery;
|
||||
build(query: TQuery): string;
|
||||
fuzzArbitrary: fc.Arbitrary<TQuery>;
|
||||
}
|
||||
|
||||
export function defineUrlParityTests<T>(config: UrlParityConfig<T>): void;
|
||||
```
|
||||
|
||||
- **`tests/parity/seo/harness.ts`** — framework for SEO parity tests. Feature sub-plans register a route-renderer callback; the harness compares the SSR `<head>` against the Phase 0 SEO baseline fixtures.
|
||||
|
||||
```ts
|
||||
export interface SeoParityConfig {
|
||||
slug: string;
|
||||
fixturePath: string;
|
||||
render(lang: Language): Promise<string>; // returns full HTML
|
||||
// Assertions: title, canonical, hreflang set, OG, JSON-LD shape
|
||||
}
|
||||
|
||||
export function defineSeoParityTests(config: SeoParityConfig): void;
|
||||
```
|
||||
|
||||
- **`tests/parity/vrt/playwright.config.ts`** — Playwright config that resolves baselines from `tests/fixtures/phase-0/vrt-baselines/` and compares against the React build.
|
||||
|
||||
- **Infrastructure only — no real tests yet.** Phase 1J ships with one smoke test per harness proving the mechanism works: the URL harness has a trivial registered test; the SEO harness renders the smoke route and asserts its `<head>`; the VRT harness compares a single placeholder baseline against the smoke route's screenshot. Real tests are registered by Phase 2+ feature sub-plans.
|
||||
|
||||
**TypeScript contracts:** `defineUrlParityTests`, `defineSeoParityTests`.
|
||||
|
||||
**Exit gate for 1J:** One trivially-broken URL parity test on a fake serializer fails CI; the real smoke-route tests pass. VRT harness compares the smoke route against a committed baseline and passes.
|
||||
|
||||
---
|
||||
|
||||
## Shared files — cross-sub-plan modification table
|
||||
|
||||
This table is the single source of truth for who owns each shared file and which sub-plans modify it. Any modification not listed here is a plan bug.
|
||||
|
||||
| File | Primary owner | Also modified by | What the modifiers add |
|
||||
|---|---|---|---|
|
||||
| `modern.config.ts` | 1A | 1G | OTel SDK init + request tracing plugin |
|
||||
| | | 1H | CSP + security-headers middleware registration |
|
||||
| | | 1I | Health middleware + graceful shutdown hook |
|
||||
| `src/routes/layout.tsx` | 1F | 1G | `<AnalyticsLoader>` mount + `<LoggerProvider>` wrap |
|
||||
| | | 1H | CSP nonce propagation into `<Head>` inline scripts |
|
||||
| `src/routes/[lang]/layout.tsx` | 1F | 1G | Per-request logger child creation + OTel span attachment |
|
||||
| `.eslintrc.cjs` | 1A | 1H | `no-eval`, `no-implied-eval`, custom `innerHTML` rule, custom `window.localStorage` rule |
|
||||
| `package.json` | 1A | 1B | CI-related dev deps (`@vitest/coverage-v8`, size-limit, osv-scanner wrappers) |
|
||||
| | | 1C | `i18next`, `react-i18next`, `i18next-icu`, `i18next-resources-to-backend` |
|
||||
| | | 1D | `@isaacs/ttlcache`, `zod` |
|
||||
| | | 1E | `@microsoft/signalr` |
|
||||
| | | 1F | `schema-dts` |
|
||||
| | | 1G | `@opentelemetry/api`, `@opentelemetry/sdk-node`, `@opentelemetry/sdk-web`, `@opentelemetry/exporter-trace-otlp-http`, `@opentelemetry/exporter-metrics-otlp-http`, `web-vitals` |
|
||||
| | | 1I | `undici` (for keep-alive HTTP agent) |
|
||||
| `tsconfig.json` | 1A | — | No modifications after 1A |
|
||||
| `pnpm-workspace.yaml` | (created in Phase 0) | — | No modifications in Phase 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 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, so the engineer has the authoritative version.
|
||||
4. Re-run the primary owner's exit-gate test to prove the modification didn't break it.
|
||||
|
||||
---
|
||||
|
||||
## Spec-coverage matrix
|
||||
|
||||
Every design-spec section §1–§8 maps to at least one sub-plan.
|
||||
|
||||
| Spec section | Topic | Sub-plan(s) |
|
||||
|---|---|---|
|
||||
| §1.1, §1.2 | Runtime topology, dependency direction | 1A |
|
||||
| §1.3 | `src/` tree | 1A |
|
||||
| §1.4 | URL language-prefix policy | 1C + 1F |
|
||||
| §2.1 | Two build targets | 1A |
|
||||
| §2.2 | `mf-manifest.json` + exposed modules | 1A |
|
||||
| §2.3 | Shared dependencies (React singleton) | 1A |
|
||||
| §2.4 | `HostContract` | 1A (type defined, consumers wire in Phase 2) |
|
||||
| §2.5 | Build artifacts + deploy shape | 1A + 1I |
|
||||
| §3.1 | SSR request lifecycle | 1F + 1G + 1H |
|
||||
| §3.2 | Two SSR invariants (no body side effects, no server SignalR) | 1A (ESLint) + 1E (client-only dynamic import) |
|
||||
| §3.3 | File-based routing | 1F |
|
||||
| §3.4 | Loaders, Suspense, `React.lazy` | 1F (smoke route demonstrates the pattern) |
|
||||
| §3.5 | URL parity — ported serializers | 1J (harness only; real serializers in Phase 2+) |
|
||||
| §3.6 | Canonical / hreflang / redirects | 1F (`<SeoHead>` + `buildHreflangSet`) |
|
||||
| §4.1 | REST client | 1D |
|
||||
| §4.2 | Caching strategy | 1D |
|
||||
| §4.3 | SSR → client data handoff | 1F (smoke route demonstrates pattern) |
|
||||
| §4.4 | SignalR wrapper + hook | 1E |
|
||||
| §4.5 | State management | (no Phase 1 work — no features exist) |
|
||||
| §4.6 | Error handling path | 1D (`ApiError` types) + 1F (`ErrorBoundary`) |
|
||||
| §5 | UI adapter + styling | (no Phase 1 work — real UI ports start in Phase 2) |
|
||||
| §6.1–§6.4 | i18n | 1C |
|
||||
| §6.5 | `<SeoHead>` | 1F |
|
||||
| §6.6 | JSON-LD schema coverage | 1F (component; feature-specific builders in Phase 2+) |
|
||||
| §6.7 | OG images (static default) | 1F |
|
||||
| §6.8 | Canonical/hreflang correctness | 1F |
|
||||
| §7.1–§7.3 | Logging + metrics | 1G |
|
||||
| §7.4 | Analytics | 1G |
|
||||
| §7.5 | Error handling layers | 1F + 1G |
|
||||
| §7.6 | Correlation | 1G |
|
||||
| §7.7 | Performance budgets | 1B (bundle gate) + 1G (web-vitals reporting) |
|
||||
| §7.8 | Dev-mode ergonomics | 1G |
|
||||
| §8.1 | Security / isolation | 1H |
|
||||
| §8.2 | Performance / 100 RPS | 1A (Node tuning) + 1B (bundle gate) + 1I (CDN headers) |
|
||||
| §8.3 | Reliability / geo-distribution | 1I |
|
||||
| §8.4 | Testing strategy | 1B + 1J |
|
||||
| §8.5 | CI/CD | 1B + 1I |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 global exit gate — checklist
|
||||
|
||||
Phase 1 is complete when **every one of these** is true:
|
||||
|
||||
- [ ] **1A:** `pnpm build:both` produces valid `dist/standalone/` + `dist/remote/` + `mf-manifest.json`.
|
||||
- [ ] **1B:** Per-PR CI pipeline runs green end-to-end in under 20 minutes. 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, timeout, circuit breaker, caches); isomorphic usage verified in SSR and client.
|
||||
- [ ] **1E:** SignalR wrapper tests green (Strict Mode double-mount, grace-period close, SSR import absence).
|
||||
- [ ] **1F:** `/ru/smoke` + `/en/smoke` render SSR in `testing` env with full `<head>` (title, description, canonical, 9 hreflang + x-default, OG, JSON-LD).
|
||||
- [ ] **1G:** Smoke route emits at least one log, one metric, and one analytics event, all captured at the sink.
|
||||
- [ ] **1H:** CSP header on `testing` env contains per-request nonce; every inline `<script>` has the nonce; security headers present on every response.
|
||||
- [ ] **1I:** Merge to `main` auto-deploys to `testing` via canary path; `/health` returns 200; graceful shutdown drains + exits within 31s; forced health failure auto-rolls back.
|
||||
- [ ] **1J:** URL parity, SEO parity, and VRT parity harnesses are wired to Phase 0 baselines and run in CI; each has one passing smoke test.
|
||||
- [ ] **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. (Feature budgets are tracked per phase.)
|
||||
- [ ] **Coverage:** ≥ 70% line coverage on `src/shared/`, `src/observability/`, `src/ui/`, `src/i18n/`. (No `src/features/` coverage in Phase 1 — there are no features yet.)
|
||||
- [ ] **Documentation:** `docs/superpowers/phase-1/README.md` indexes what Phase 1 shipped + how to run each subsystem locally + how to debug failures in each.
|
||||
|
||||
---
|
||||
|
||||
## Risks + open questions for Phase 1
|
||||
|
||||
1. **Customer template (assumption A1) still unknown.** If it arrives during Phase 1, directory layout may need a rename pass. Mitigation: feature barrels (`src/features/*/index.ts`) are the only cross-module public surface, so a rename can be absorbed with an alias + a migration task added to the affected sub-plan.
|
||||
|
||||
2. **Customer log format (assumption A4) still unknown.** 1G ships `JsonLinesHttpTransport`. If the format arrives before Phase 1 completes, a single `CustomerFormatTransport` task is added to 1G's sub-plan. If it arrives after, it's a standalone task in Phase 2.
|
||||
|
||||
3. **Analytics vendor credentials (assumption A7) still unknown.** 1G ships stub adapters. Real script URLs + IDs land in Phase 2A. A disabled-but-present stub does not block Phase 1 exit.
|
||||
|
||||
4. **CI provider (assumption A3) assumed GitHub Actions.** If the customer uses GitLab CI or TeamCity, 1B's workflow YAML is rewritten but the underlying scripts (`bundle-size-gate.ts`, `check-coverage-delta.ts`) are portable.
|
||||
|
||||
5. **Modern.js + Module Federation 2.0 interaction.** `@module-federation/modern-js` is newer than either Modern.js or MF 2.0 in isolation. Risk: undocumented edge cases with Rspack's shared-dependency resolution across `BUILD_TARGET`. Mitigation: 1A's first tasks build the simplest possible end-to-end example (a single exposed hello-world component) and iterate from there; every subsequent task in 1A adds one capability at a time.
|
||||
|
||||
6. **SSR + `<Suspense>` + streaming + React 18 concurrent mode** is a combination that has known interop issues with some libraries. Mitigation: 1F's smoke route uses a `React.lazy()` + `<Suspense>` + data loader as a deliberate stress test.
|
||||
|
||||
7. **Detached HEAD state from spec/plan authoring.** The engineer running Phase 0 Task 1 attaches these commits to a branch; if anything else checks out first, commits become unreachable. Mitigation: Phase 0 Task 1 is the very first task and is a no-op if the branch already exists.
|
||||
|
||||
---
|
||||
|
||||
## 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 (project skeleton + dual build targets) from `docs/superpowers/plans/2026-04-14-phase-1-foundation-master.md`. Target file: `docs/superpowers/plans/2026-04-14-phase-1a-skeleton.md`. Follow the contracts defined in the master plan §1A exactly; reference the design spec §1.3, §2.1, §2.2, §2.5 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 §7 of this document. Section §9 (Phase 2–6 migration) is explicitly out of scope for Phase 1.
|
||||
|
||||
**Placeholder scan.** No `TBD` / `TODO` / `FIXME` in this master plan outside of the "TBW" ("to be written") markers on sub-plan filenames, which are deliberate indicators of which sub-plan documents don't exist yet.
|
||||
|
||||
**Internal consistency.** Every contract exported by one sub-plan and consumed by another uses the same type name + shape in both locations (cross-checked: `Logger` in 1G → used by 1D/1F; `Language` in 1C → used by 1F/1J; `ApiClient` in 1D → used by 1F/1I; `ConnectionStatus` in 1E → Phase 2 consumer).
|
||||
|
||||
**Scope.** Ten sub-plans is a lot, but each is small to medium and maps to one subsystem with a well-defined contract surface. Sub-plan 1A is the only "large" one — it could be split further (1A-1 skeleton, 1A-2 MF + builds, 1A-3 ESLint boundaries) if the writer finds the single plan runs long.
|
||||
|
||||
**Execution order correctness.** The dependency graph has no cycles (verified by hand). The critical path (1A → 1C → 1F → 1H → 1I) is 5 sub-plans; everything else is off the critical path and can slip without impacting Phase 1 calendar if parallelized.
|
||||
|
||||
---
|
||||
|
||||
## Next step
|
||||
|
||||
- **If you approve this master plan shape:** say so, and I'll write sub-plan **1A** (project skeleton + dual build targets) fully in the next session. Then we iterate: 1B, 1C, … as the team is ready.
|
||||
- **If you want changes** (re-bundle sub-plans, adjust contracts, change execution order): tell me, I revise.
|
||||
Reference in New Issue
Block a user