/** * `slowcook check surface-parity` — mock/prod drift detector (#261). * * Design-first apps ship ONE source tree whose surfaces carry a canned branch * (the reviewable mock) and a live branch (the real product), selected by * build-time flags. The two silently diverge: a dead CTA fixed only in one * mode, copy updated in one branch, or an entire live branch dead-code- * eliminated because a flag was never set at build time (the 2026-07 dash * wallet page shipped CANNED to prod for weeks that way). * * Two sub-checks, both static (no browser): * * 1. FLAG COMPLETENESS — every `import.meta.env.…` the source reads * must be *declared* in each profile marked `require_all_flags` (the value * may be "" = deliberately off; UNDECLARED means nobody ever considered it, * which is how live branches vanish). * * 2. NODE PARITY — the universe of review nodes is parsed from source * (`rn("…")` / `data-review-node`, the @slowcook-ai/review-overlay * convention). Each profile is built with its env; a node's id LITERAL * surviving into the bundle means its branch was not DCE'd. Divergent * presence across profiles = drift. Known/deliberate divergence lives in a * BASELINE (`.slowcook/parity-baseline.yaml`) with a reason + direction; * new unwaived divergence fails (a ratchet), healed entries are flagged * for cleanup. * * v1 limit (documented in #261): runtime-gated modes (localStorage flags) * don't DCE, so literal survival can't see them — the rendered-profile pass * is the v2 follow-up. * * Config (`.slowcook/parity.yaml` at the repo root): * app: mock # build cwd, relative to root * src: mock/src # scanned for flags + node ids * build: npx vite build --outDir {outDir} --emptyOutDir * env_prefix: VITE_ * markers: [data-review-node] # or e.g. [data-testid] for repos without rn() * baseline: .slowcook/parity-baseline.yaml # per-app in multi-app repos * profiles: * mock: { env: {} } * prod: * env: { VITE_DATA_BACKEND: "1", … } * require_all_flags: true * allow_unset: [VITE_REVIEW_TOOLS] # deliberately absent (qa-only) */ export interface ParityProfile { env: Record; require_all_flags?: boolean; allow_unset?: string[]; /** markers that must NOT survive into this profile's bundle (e.g. fixture * emails, preview gates, review tooling in the real/prod profile) — * the prod-bundle sever philosophy, per profile. */ forbid?: string[]; } export interface ParityConfig { app: string; /** one or several source dirs (shared component packages count too). */ src: string | string[]; build: string; env_prefix: string; /** attribute names harvested as node ids (besides rn() and *node= props). * Default: data-review-node. Repos without review nodes can point this at * their own stable-marker convention, e.g. data-testid. */ markers: string[]; /** baseline path (relative to root) — lets multi-app repos keep one per app. */ baseline: string; profiles: Record; } export interface BaselineEntry { node: string; absent_from: string[]; reason?: string; owner?: string; /** who is truth for this surface: mock-first (greenfield) | prod-first (as-built) */ direction?: string; } export interface FlagViolation { flag: string; profile: string; readAt: string; } export interface NodeDrift { node: string; absent_from: string[]; presentIn: string[]; } export interface ForbidViolation { marker: string; profile: string; } export interface ParityResult { flagViolations: FlagViolation[]; /** forbidden markers found in a profile's bundle — always failing. */ forbidViolations: ForbidViolation[]; /** divergent nodes NOT covered by the baseline — these fail the check. */ newDrift: NodeDrift[]; /** divergent nodes covered by the baseline — reported, never failing. */ waivedDrift: NodeDrift[]; /** baseline entries whose drift no longer exists — prune them. */ healed: BaselineEntry[]; /** node ids absent from EVERY profile bundle — likely dead code. */ deadNodes: string[]; nodesChecked: number; flagsChecked: number; profilesBuilt: string[]; } /** every `import.meta.env.X` / `["X"]` read, with a source location. */ export declare function scanFlagReads(root: string, srcRel: string | string[], prefix: string): Map; /** * review-node ids from source: rn("id", …), literal data-review-node="id", * and ids passed through props whose NAME mentions node (e.g. * `` — the wrapper calls * rn() internally). The prop form requires the slashed `area/name` shape so * ordinary strings don't false-positive. */ export declare function scanNodeIds(root: string, srcRel: string | string[], markers?: string[]): Set; export declare function loadConfig(root: string, path?: string): ParityConfig; export declare function loadBaseline(root: string, path?: string): BaselineEntry[]; export declare function runSurfaceParityCheck(root: string, config: ParityConfig, baseline: BaselineEntry[], opts?: { build?: (profile: string, env: Record, outDir: string) => void; }): ParityResult; export declare function writeBaseline(root: string, drift: NodeDrift[], prior: BaselineEntry[], path?: string): void; export declare function runSurfaceParityCli(argv: string[]): void; //# sourceMappingURL=surface-parity.d.ts.map