diff --git a/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md new file mode 100644 index 0000000..87af2a4 --- /dev/null +++ b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md @@ -0,0 +1,790 @@ +# Phase 3: SPEC §10 smoke verification + DOM/event-log verification - Pattern Map + +**Mapped:** 2026-05-20 +**Files analyzed:** 5 anticipated (4 modifications + 1 new VERIFICATION.md) + optional 6th (Page.metrics scaffolding) +**Analogs found:** 5 / 5 (4 exact, 1 role-match; +1 partial for optional Page.metrics) +**Phase character:** Verification-heavy; Approach B harness extension (Plan 02-04 is direct precedent for 4 of 5 files). + +## File Classification + +| Anticipated File (Plans 03-01..05) | Role | Data Flow | Closest Analog | Match Quality | +|-------------------------------------|------|-----------|----------------|---------------| +| `tests/uat/extension-page-harness.ts` (page-side `assertA29..A3X`) | test (page-side assertion host) | request-response (chrome.runtime.sendMessage round-trip) + event-driven (chrome.downloads.onCreated listener for A24 echo, if needed by Plan 03-02) | `tests/uat/extension-page-harness.ts:2851-3413` (assertA24..assertA28 block; Plan 02-04) | EXACT (same file; append-only) | +| `tests/uat/lib/harness-page-driver.ts` (host-side `driveA29..A3X` + `JSZip` reads) | test (host-side driver + zip inspection) | file-I/O (downloadsDir polling + JSZip-parse) + request-response (page.evaluate wrapper) | `tests/uat/lib/harness-page-driver.ts:1202-1849` (driveA24..driveA28 + `findLatestZip` + `A28_EXPECTED_PATHS`; Plan 02-04) | EXACT (same file; append-only) | +| `tests/uat/harness.test.ts` (orchestrator wiring: imports + wrapped drivers + drivers array push + banner) | test (orchestrator) | request-response (sequential driver dispatch + bail-on-first-failure) | `tests/uat/harness.test.ts:93-99 + 316-335 + 406-430` (driveA24-A28 imports + wrappers + push; Plan 02-04) | EXACT (same file; append-only) | +| `tests/uat/extension-page-harness.html` (probe HTML append) | test (DOM scaffold) | static markup | `tests/uat/extension-page-harness.html:17-22` (existing `

` + `
`; Plan 01-12 Wave 6 tokens.css preserved) | EXACT (same file; append-only) |
+| `.planning/phases/03-.../03-VERIFICATION.md` (NEW; aggregator) | docs (verification aggregator with frontmatter) | docs synthesis | `.planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md` (T5 override pattern; 5/5 must-haves; Plan 02-04 closure) PLUS `.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md` (per-requirement scorecard; cross-cutting gates table; operator empirical acks; deferred items section) | EXACT (T5 override) + ROLE-MATCH (scorecard structure) |
+| `tests/uat/lib/ramMetrics.ts` OR inline in `harness-page-driver.ts` (OPTIONAL — D-P3-04 best-effort `puppeteer.Page.metrics()` scaffolding) | test (host-side utility) | request-response (CDP `Performance.getMetrics` via `Page.metrics()`) | NONE (research-derived; no existing analog) — closest scaffolding shape lives in `tests/uat/lib/harness-page-driver.ts:1255-1334` (driveA25 host-side latency measurement + diagnostic copy pattern) | NEW PATTERN (cite RESEARCH §"Code Examples" A3X) |
+
+## Pattern Assignments
+
+### `tests/uat/extension-page-harness.ts` (page-side `assertA29..A3X` for Plans 03-01..04)
+
+**Analog:** `tests/uat/extension-page-harness.ts:2851-3413` (Plan 02-04 assertA24-A28)
+
+**Existing imports / module context** — already present at top of file; no new imports expected:
+- The harness uses ambient chrome.* types (`chrome.downloads.DownloadItem`, `chrome.tabs.create`, etc.); production surfaces only.
+- Helper utilities `setupFreshRecording`, `sendMessageWithTimeout`, `diag` (file-local) — all reusable for Plan 03-01..04.
+
+**Module constants block** — Plan 02-04 precedent for per-assertion tuning constants (lines 2977-2982):
+
+```typescript
+/** SAVE_ARCHIVE dispatch timeout for A25 — matches A24's 15s. */
+const A25_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
+/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
+const A25_SEGMENT_SETTLE_MS = 11_000;
+/** Hard latency ceiling per SPEC §10 #6 + CON-archive-export-latency. */
+const A25_LATENCY_CEILING_MS = 5_000;
+```
+
+Phase 3 plans should follow the same `const A__MS = NNNN_NNN;` form (`A29_SEGMENT_SETTLE_MS`, `A30_TRIGGER_PAUSE_MS`, `A31_PASSWORD_SENTINEL`, etc.).
+
+**Page-side stub pattern (for host-side-only work)** — `assertA26` lines 3164-3173 + `assertA28` lines 3307-3316:
+
+```typescript
+/**
+ * A26 — D-P2-02 + D-P2-03 empirical (page-side stub).
+ *
+ * Returns the assertion name + a sentinel diagnostic. All real work
+ * happens host-side in driveA26 (JSZip-parse the latest zip + assert
+ * meta.json shape). The page-side stub exists purely so the orchestrator's
+ * single-method-per-assertion contract (window.__mokoshHarness.assertA26)
+ * is uniform across all 29 assertions.
+ *
+ * Chaining: A26 reads the zip produced by A25 (host-side driver picks
+ * the most-recently-modified zip in downloadsDir). No new SAVE dispatch.
+ *
+ * @returns Stub AssertionResult with empty checks; driveA26 fills them.
+ */
+async function assertA26(): Promise {
+  return {
+    passed: true,
+    name: 'A26 — meta.json 8-field shape (D-P2-02 + D-P2-03)',
+    checks: [],
+    diagnostics: [
+      'assertA26 page-side stub; host-side driveA26 inspects latest zip + asserts meta.json shape',
+    ],
+  };
+}
+```
+
+**Use for:** Plan 03-01 (rrweb session.json inspection is host-side JSZip), Plan 03-02 (events.json grep is host-side), Plan 03-03 (sentinel grep is host-side). Page-side stubs for A29 / A30 / A31; host-side drivers do the work.
+
+**Page-side orchestration pattern (when chrome.* APIs are required pre-SAVE)** — `assertA24` lines 2851-2962 (chrome.downloads.onCreated cross-realm capture) and `assertA27` lines 3202-3292 (chrome.tabs.create + activate + cleanup):
+
+```typescript
+async function assertA24(): Promise {
+  const result: AssertionResult = {
+    passed: false,
+    name: 'A24 — D-P2-01 empirical: chrome.downloads.download receives blob: URL (closes P0-6)',
+    checks: [],
+    diagnostics: [],
+  };
+
+  // Capture stored in closure so the onCreated listener can populate it
+  // across the async SAVE_ARCHIVE dispatch + post-ack poll.
+  let capturedUrl: string | null = null;
+  let listenerInstalled = false;
+  const onCreatedListener = (item: chrome.downloads.DownloadItem): void => {
+    if (capturedUrl === null) {
+      capturedUrl = item.url;
+    }
+  };
+
+  try {
+    diag(result, 'Step 1: setupFreshRecording (A24 owns its recording — onCreated listener installed pre-SAVE)');
+    const setupResp = await setupFreshRecording();
+    if (!setupResp.ok) {
+      throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
+    }
+    // ... Step 2: settle, Step 3: install listener, Step 4: dispatch SAVE_ARCHIVE, Step 5: poll for capturedUrl
+  } catch (err) {
+    result.error = err instanceof Error ? err.message : String(err);
+    diag(result, `THREW: ${result.error}`);
+  } finally {
+    // T-02-04-01 mitigation: always remove the listener (idempotent; no-op if never added).
+    if (listenerInstalled) {
+      try { chrome.downloads.onCreated.removeListener(onCreatedListener); }
+      catch (rmErr) { diag(result, `(listener cleanup ignored: ${String(rmErr)})`); }
+    }
+  }
+
+  return result;
+}
+```
+
+**Use for:** Plan 03-02 if event-log triggers (page.click / page.type / dispatchEvent) require pre-SAVE setup beyond what RESEARCH §"Code Examples" pattern 2 describes; Plan 03-03 password-sentinel typing pre-SAVE.
+
+**Page-side extended-result shape (when host needs page data beyond AssertionResult)** — `A25Result` lines 2997-3002 + `A27Result` lines 3145-3148:
+
+```typescript
+interface A25Result extends AssertionResult {
+  t0: number;        // performance.now() at SAVE dispatch
+  tAck: number;      // performance.now() at SAVE ack
+  t0Wall: number;    // Date.now() at SAVE dispatch (cross-realm bracket anchor)
+  ackSuccess: boolean;
+}
+
+interface A27Result extends AssertionResult {
+  tabAUrl: string;
+  tabBUrl: string;
+}
+```
+
+**Use for:** Plan 03-04 OPTIONAL Page.metrics scaffolding — `A3XResult extends AssertionResult { jsHeapUsedBytes: number; jsHeapTotalBytes: number; }` if Plan 03-04 ships scaffolding (D-P3-04 "if practical"; see RESEARCH §"Code Example A3X").
+
+**`__mokoshHarness` registration pattern** — lines 3332-3372 (TypeScript ambient interface) + lines 3374-3404 (object literal):
+
+```typescript
+declare global {
+  interface Window {
+    __mokoshHarness: {
+      assertA1: () => Promise;
+      // ... (24 baseline assertions A1..A23)
+      assertA24: () => Promise;
+      assertA25: () => Promise;
+      assertA26: () => Promise;
+      assertA27: () => Promise;
+      assertA28: () => Promise;
+      getManifestVersion: () => Promise;
+    };
+  }
+}
+
+window.__mokoshHarness = {
+  assertA1, assertA2, /* ... */ assertA27, assertA28, getManifestVersion,
+};
+```
+
+**Use for:** Plans 03-01..04 each append their own `assertA` to BOTH the `declare global` interface AND the `window.__mokoshHarness = {...}` object literal. Lockstep is critical; missing one side breaks the orchestrator's `await harness.assertA()` call.
+
+---
+
+### `tests/uat/lib/harness-page-driver.ts` (host-side `driveA29..A3X` + `JSZip`)
+
+**Analog:** `tests/uat/lib/harness-page-driver.ts:1202-1849` (Plan 02-04 driveA24-A28)
+
+**Existing imports (already present in file)** — lines 36-45:
+
+```typescript
+import { spawnSync } from 'node:child_process';
+import { existsSync, mkdtempSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join, resolve as resolvePath } from 'node:path';
+
+import JSZip from 'jszip';
+import type { Page } from 'puppeteer';
+
+import type { AssertionRecord, CheckRecord } from './assertions';
+import { assertArchiveShape, extractEntryToFile } from './zip';
+```
+
+Plan 03-01 only needs to ADD a single import for the rrweb EventType enum:
+```typescript
+import { EventType } from '@rrweb/types';   // ← NEW for Plan 03-01 (verified at node_modules/@rrweb/types/dist/index.d.ts:186-194)
+```
+
+Plan 03-02 / 03-03 may add an import for `UserEvent` from `src/shared/types.ts`:
+```typescript
+import type { UserEvent } from '../../../src/shared/types';   // ← NEW for Plan 03-02 events.json grep
+```
+
+**Standard `page.evaluate` wrapper for page-orchestrated assertions** — driveA24 lines 1202-1209:
+
+```typescript
+export async function driveA24(page: Page): Promise {
+  return await page.evaluate(async () => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose.
+    const harness = (window as any).__mokoshHarness;
+    const r: AssertionRecord = await harness.assertA24();
+    return r;
+  }) as AssertionRecord;
+}
+```
+
+**Use for:** Plan 03-02 driveA30 (if Plan 03-02 chooses page-side orchestration for event triggers — but RESEARCH recommends Puppeteer-side `page.click` / `page.type` via the page, not chrome.runtime.sendMessage round-trip; planner picks).
+
+**Two-phase pattern (page-side stub + host-side JSZip read)** — driveA26 lines 1421-1567 (canonical 6-check meta.json shape gate; chains off A25):
+
+```typescript
+export async function driveA26(
+  page: Page,
+  downloadsDir: string,
+): Promise {
+  // Phase 1 — page-side stub (uniform orchestrator shape).
+  const pageResult = await page.evaluate(async () => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose.
+    const harness = (window as any).__mokoshHarness;
+    const r: AssertionRecord = await harness.assertA26();
+    return r;
+  }) as AssertionRecord;
+
+  const mergedChecks: CheckRecord[] = pageResult.checks.slice();
+  const mergedDiagnostics: string[] = pageResult.diagnostics.slice();
+
+  // Phase 2 — locate A25's zip.
+  const zipPath = findLatestZip(downloadsDir);
+  if (zipPath === null) {
+    mergedChecks.push({
+      name: 'A26.0: at least one zip present in downloadsDir (chain-off A25)',
+      expected: '>=1 zip',
+      actual: 'no zip in downloadsDir',
+      passed: false,
+    });
+    return { passed: false, name: pageResult.name, checks: mergedChecks,
+             diagnostics: mergedDiagnostics, error: pageResult.error };
+  }
+  mergedDiagnostics.push(`A26 zipPath=${zipPath}`);
+
+  // Phase 3 — load + inspect target file inside zip.
+  const zipBytes = readFileSync(zipPath);
+  const zip = await JSZip.loadAsync(zipBytes);
+  const metaFile = zip.file('meta.json');
+  // ... A26.1..A26.6 structural checks
+  const mergedPassed = mergedChecks.every((c) => c.passed);
+  return { passed: mergedPassed, name: pageResult.name, checks: mergedChecks,
+           diagnostics: mergedDiagnostics, error: pageResult.error };
+}
+```
+
+**Use for:** All four Plans 03-01..04 — same 3-phase shape. Plan 03-01 reads `rrweb/session.json`, Plan 03-02 reads `logs/events.json`, Plan 03-03 reads `logs/events.json` for sentinel grep.
+
+**`findLatestZip` chain-off helper** — lines 1395-1406 (must be present; if Plan 03-* runs in a fresh wave the helper is already shipped by Plan 02-04):
+
+```typescript
+function findLatestZip(downloadsDir: string): string | null {
+  const candidates = readdirSync(downloadsDir).filter(isZipFilename);
+  if (candidates.length === 0) {
+    return null;
+  }
+  const withMtimes = candidates.map((name) => ({
+    name,
+    mtimeMs: statSync(resolvePath(downloadsDir, name)).mtimeMs,
+  }));
+  withMtimes.sort((a, b) => b.mtimeMs - a.mtimeMs);
+  return resolvePath(downloadsDir, withMtimes[0].name);
+}
+```
+
+**Use for:** Every Plan 03-01..04 driver that reads from downloadsDir. Already present in file (no re-implementation).
+
+**EventType enum grep pattern (Plan 03-01)** — derived from RESEARCH §"Code Examples A29" (verified against `@rrweb/types/dist/index.d.ts:186-194`):
+
+```typescript
+// Plan 03-01 driveA29 — structural rrweb event-shape grep (RESEARCH-cited pattern; no codebase analog)
+import { EventType } from '@rrweb/types';   // FullSnapshot=2, IncrementalSnapshot=3, Meta=4
+
+const rrwebRaw = await zip.file('rrweb/session.json')!.async('text');
+const events: Array<{ type: number; timestamp: number }> = JSON.parse(rrwebRaw);
+
+mergedChecks.push({
+  name: 'A29.1: rrweb/session.json contains > 0 events',
+  expected: '>0',
+  actual: events.length,
+  passed: events.length > 0,
+});
+mergedChecks.push({
+  name: `A29.2: rrweb emitted at least one Meta event (EventType.Meta=${EventType.Meta})`,
+  expected: 'has Meta',
+  actual: events.some((e) => e.type === EventType.Meta),
+  passed: events.some((e) => e.type === EventType.Meta),
+});
+mergedChecks.push({
+  name: `A29.3: rrweb emitted at least one FullSnapshot (EventType.FullSnapshot=${EventType.FullSnapshot})`,
+  expected: 'has FullSnapshot',
+  actual: events.some((e) => e.type === EventType.FullSnapshot),
+  passed: events.some((e) => e.type === EventType.FullSnapshot),
+});
+mergedChecks.push({
+  name: `A29.4: rrweb emitted at least one IncrementalSnapshot (EventType.IncrementalSnapshot=${EventType.IncrementalSnapshot})`,
+  expected: 'has IncrementalSnapshot',
+  actual: events.some((e) => e.type === EventType.IncrementalSnapshot),
+  passed: events.some((e) => e.type === EventType.IncrementalSnapshot),
+});
+```
+
+**Use for:** Plan 03-01 only. RESEARCH §"Pitfall 1" warns: synthetic probe HTML needs at least one DOM mutation between load and SAVE for `IncrementalSnapshot` to fire — Plan 03-01 driver MUST trigger one via `page.evaluate(() => document.body.appendChild(...))` or `page.click('#probe-modal-trigger')`.
+
+**UserEvent type-grep pattern (Plan 03-02)** — derived from RESEARCH §"Code Examples" pattern 2 + `src/shared/types.ts:124-131`:
+
+```typescript
+// Plan 03-02 driveA30 — events.json grep for 5 UserEvent.type values
+import type { UserEvent } from '../../../src/shared/types';
+
+const eventsRaw = await zip.file('logs/events.json')!.async('text');
+const userEvents: UserEvent[] = JSON.parse(eventsRaw);
+
+const EXPECTED_TYPES = ['click', 'input', 'navigation', 'js_error', 'network_error'] as const;
+for (const expectedType of EXPECTED_TYPES) {
+  mergedChecks.push({
+    name: `A30.: logs/events.json contains at least one '${expectedType}' event`,
+    expected: `>=1 ${expectedType}`,
+    actual: userEvents.filter((e) => e.type === expectedType).length,
+    passed: userEvents.some((e) => e.type === expectedType),
+  });
+}
+```
+
+**Negative-assertion pattern (Plan 03-03 password sentinel grep)** — derived from RESEARCH §"Pattern 3" (no codebase analog; closest analog is driveA27's A27.7/A27.8 absence assertions at harness-page-driver.ts:1724-1737):
+
+```typescript
+// Plan 03-03 driveA31 — sentinel absence from logs/events.json
+const SENTINEL = 'secret-do-not-log-123';   // fixed; not a real secret
+const eventsRaw = await zip.file('logs/events.json')!.async('text');
+const userEvents: UserEvent[] = JSON.parse(eventsRaw);
+
+const eventsContainingSentinel = userEvents.filter(
+  (e) => e.value !== undefined && e.value.includes(SENTINEL),
+);
+mergedChecks.push({
+  name: 'A31.1: password sentinel ABSENT from logs/events.json (existing src/content/index.ts:82 filter VERIFIED)',
+  expected: 'absent (0 events containing sentinel)',
+  actual: `${eventsContainingSentinel.length} events containing sentinel`,
+  passed: eventsContainingSentinel.length === 0,
+});
+```
+
+**Filter-pipeline form (no `continue`)** — driveA28 lines 1808-1810 (project style; CLAUDE.md "Control Flow" §):
+
+```typescript
+const actualPaths: string[] = Object.keys(zip.files)
+  .filter((path) => !zip.files[path].dir)
+  .sort();
+```
+
+**Use for:** Any enumeration loop in Plans 03-01..04. No `for...of` + `if (...) continue;` shape.
+
+**Tab-cleanup safety pattern (T-02-04-04)** — assertA27 lines 3271-3289 (try/catch silent-ignore on already-closed):
+
+```typescript
+} finally {
+  // T-02-04-04 mitigation: cleanup tabs with silent-ignore on already-closed.
+  if (tabAId !== undefined) {
+    try { await chrome.tabs.remove(tabAId); }
+    catch (rmErr) { diag(result, `(tabA cleanup ignored: ${String(rmErr)})`); }
+  }
+  if (tabBId !== undefined) {
+    try { await chrome.tabs.remove(tabBId); }
+    catch (rmErr) { diag(result, `(tabB cleanup ignored: ${String(rmErr)})`); }
+  }
+}
+```
+
+**Use for:** Plan 03-02 if event-log trigger strategy involves opening a probe tab (e.g., `https://example.com` for js_error/network_error triggers).
+
+---
+
+### `tests/uat/harness.test.ts` (orchestrator wiring)
+
+**Analog:** `tests/uat/harness.test.ts:65-130 + 316-335 + 406-430` (Plan 02-04)
+
+**Import section pattern** — lines 92-100 (add A24-A28 imports per plan):
+
+```typescript
+  // Plan 02-04 Task 1 — D-P2-01 empirical Blob URL verification
+  driveA24,
+  // Plan 02-04 Task 2 — REQ-archive-export-latency (5s ceiling)
+  driveA25,
+  // Plan 02-04 Task 3 — meta.json 8-field + multi-tab strict + REQ-archive-layout
+  driveA26,
+  driveA27,
+  driveA28,
+  getManifestVersion,
+} from './lib/harness-page-driver';
+```
+
+**Use for:** Each Plan 03-01..04 appends ONE `driveA` import + a section banner comment naming the plan and assertion purpose.
+
+**Wrapped-driver pattern (when driver needs `downloadsDir` beyond the standard `Page` arg)** — lines 322-335 (Plan 02-04 wrappers):
+
+```typescript
+  // Plan 02-04 Task 2 — driveA25 needs downloadsDir for the host-side
+  // dispatch→file-on-disk latency check (mirrors A5/A12/A13 wrapping).
+  const driveA25Wrapped: (page: import('puppeteer').Page) => Promise =
+    (page) => driveA25(page, handles.downloadsDir);
+  // Plan 02-04 Task 3 — driveA26/A27/A28 need downloadsDir for host-side
+  // zip inspection (JSZip-parse meta.json + zip-layout enumeration). A26
+  // chains off A25's zip (no new SAVE); A27 owns its SAVE (multi-tab);
+  // A28 chains off A27's zip (no new SAVE).
+  const driveA26Wrapped: (page: import('puppeteer').Page) => Promise =
+    (page) => driveA26(page, handles.downloadsDir);
+  const driveA27Wrapped: (page: import('puppeteer').Page) => Promise =
+    (page) => driveA27(page, handles.downloadsDir);
+  const driveA28Wrapped: (page: import('puppeteer').Page) => Promise =
+    (page) => driveA28(page, handles.downloadsDir);
+```
+
+**Use for:** Plans 03-01..03 (all need `downloadsDir` to find the latest zip). Same wrapping pattern; one wrapper per driver.
+
+**Drivers-array push pattern with explanatory banner comment** — lines 397-430 (Plan 02-04):
+
+```typescript
+    // Plan 02-04 Task 1 A24: D-P2-01 empirical Blob URL verification.
+    // Installs chrome.downloads.onCreated listener cross-realm, dispatches
+    // SAVE_ARCHIVE, captures the download URL, asserts the `blob:` prefix
+    // (closes audit P0-6 end-to-end through a real Chrome instance +
+    // the offscreen mint round-trip + chrome.downloads platform call).
+    { name: 'A24', drive: driveA24 },
+    // Plan 02-04 Task 2 A25: REQ-archive-export-latency / SPEC §10 #6.
+    { name: 'A25', drive: driveA25Wrapped },
+    // Plan 02-04 Task 3 A26: D-P2-02 + D-P2-03 meta.json 8-field shape.
+    { name: 'A26', drive: driveA26Wrapped },
+    // Plan 02-04 Task 3 A27: STRICT multi-tab urls[] post DEC-011 Amendment 1.
+    { name: 'A27', drive: driveA27Wrapped },
+    // Plan 02-04 Task 3 A28: REQ-archive-layout strict 5-entry zip-layout.
+    { name: 'A28', drive: driveA28Wrapped },
+  ];
+```
+
+**Use for:** Plans 03-01..04 each append ONE `{ name: 'A', drive: driveAWrapped }` entry with a banner comment citing the plan, the SPEC §10 # being verified, and the chaining strategy (own SAVE vs chain off prior).
+
+**FORBIDDEN_HOOK_STRINGS UAT A0 mirror** — lines 115-130 (12 entries):
+
+```typescript
+const FORBIDDEN_HOOK_STRINGS: ReadonlyArray = [
+  '__mokoshTest',
+  'setCurrentStream',
+  'setSegmentCountGetter',
+  'installFakeDisplayMedia',
+  'uninstallFakeDisplayMedia',
+  'dispatchEndedOnTrack',
+  'getSegmentCount',
+  '__mokoshOffscreenQuery',
+  'get-display-surface',
+  'get-segment-count',
+  // Plan 01-14 A23 surface — lockstep with unit-gate inventory at
+  // tests/background/no-test-hooks-in-prod-bundle.test.ts:105.
+  'lastGetDisplayMediaConstraints',
+  'get-last-getDisplayMedia-constraints',
+];
+```
+
+**Lockstep requirement:** ANY new test-only `__MOKOSH_UAT__`-gated symbol introduced by Plan 03-* MUST be added to BOTH this UAT A0 mirror AND the unit-gate at `tests/background/no-test-hooks-in-prod-bundle.test.ts:108-126`. RESEARCH §"Anti-Patterns" + A6 in Assumptions Log: Phase 3 plans are EXPECTED to stay at 12 entries (rrweb + chrome.downloads + chrome.tabs are production surfaces). The MEDIUM-risk exception (A6) is Plan 03-04 Page.metrics scaffolding — Page.metrics is host-side (not bundled into page-realm); no new symbol expected.
+
+---
+
+### `tests/uat/extension-page-harness.html` (probe HTML append for Plan 03-01)
+
+**Analog:** Current state of the file (lines 17-22) — append below, do NOT touch the head.
+
+**Existing scaffold** (full file, 24 lines):
+
+```html
+
+
+  
+    
+    Mokosh UAT Harness (extension-internal page)
+    
+    
+  
+  
+    

Mokosh UAT — extension-internal page harness

+

This page lives at chrome-extension://<id>/tests/uat/extension-page-harness.html.

+

Puppeteer navigates a tab here and drives assertions via window.__mokoshHarness.*.

+
Ready.
+ + + +``` + +**Probe-HTML append target:** between `
` (line 21) and `