# Phase 3: SPEC §10 smoke verification + DOM/event-log verification - Research

**Researched:** 2026-05-20
**Domain:** UAT harness verification (Approach B extension) for rrweb DOM capture + event-log + RAM scaffolding + §10 sweep
**Confidence:** HIGH (Q1, Q2, Q3 verified against registry + official docs + existing harness; Q4 LOW for new chrome.* patterns — no production new surface required for charter)

## Summary

Phase 3 is verification-heavy and the Approach B harness pattern (page-side `assertA*` + host-side `driveA*` + `harness.test.ts` orchestrator + Tier-1 `FORBIDDEN_HOOK_STRINGS` lockstep + pre-checkpoint bundle gates) established in Plans 01-13, 01-14, 02-04 is fully mature. The four scoped research questions resolve as follows:

1. **`puppeteer.Page.metrics()`** is reliable ONLY for the page realm it is called on; it returns CDP `Performance.getMetrics`, which exposes `JSHeapUsedSize` for the page's V8 isolate. It **does NOT measure the MV3 service worker context** — service workers live in a separate target with its own V8 isolate, and the metrics API on `Page` does not aggregate across workers/iframes. Scaffolding via `Page.metrics()` is feasible against the harness extension-internal page (a Page target) but yields a value that under-reports the operator-facing "extension background RAM ≤ 50 MB" claim. Per D-P3-04 best-effort framing, scaffolding via `Page.metrics()` MAY be added but MUST come with explicit diagnostic copy ("page-realm only; SW context excluded") and SHOULD NOT block §10 #9 verification.

2. **rrweb 2.0.0-alpha.4 alpha-pin is safe** for Phase 3 verification: npm registry `latest` tag for `rrweb` still points at 2.0.0-alpha.4 (2023 release; npm dist-tag confirmed by `npm view rrweb dist-tags`). The newer alpha.20 (2026-02-03) introduced a `NodeType` enum move from `rrweb-snapshot` to `@rrweb/types` (breaking import sites) but does not affect what Mokosh actually uses (the `record()` entry point + `EventType` enum signatures). No known regressions in alpha.4 that would make synthetic-probe-page verification flaky. Stable v2 has NOT shipped (zero non-alpha releases in the dist-tags table). Phase 4 upgrade research is correctly deferred.

3. **rrweb deterministic verification on synthetic probe pages** works via the *event-shape* assertion pattern, not snapshot-comparison. rrweb's own test suite uses snapshot files filtered through `stringifySnapshots` to normalize non-determinism (mouse positions, timestamps, blob URLs). For Mokosh's "rrweb records DOM events without errors" charter (SPEC §10 #4), the deterministic gate is structural: assert `events.length > 0` + `events[0].type === EventType.Meta` (=4) + at least one `EventType.FullSnapshot` (=2) + at least one `EventType.IncrementalSnapshot` (=3) emitted during a probe page lifecycle. This is far simpler than snapshot-comparison and matches the charter ("records without errors", not "produces specific DOM snapshot").

4. **chrome.* surface for §10 verification** is already covered by the existing 29-assertion harness — no new chrome.* patterns required for Phase 3. The Blob URL path is verified empirically by A24 via `chrome.downloads.onCreated` cross-realm capture (Plan 02-04). `chrome.tabs.captureVisibleTab` is verified transitively by A28 set-equality (screenshot.png present in zip). The chrome-extension://-context Network panel observation is an operator-only surface (Phase 2 VERIFICATION.md T5 cited as override) — no new automation pattern is required by Phase 3 charter.

**Primary recommendation:** Plan 03-01 through 03-05 follow the Plan 02-04 template verbatim: page-side stubs + host-side driveA* + chained-assertion + findLatestZip + JSZip host-only parse. New assertions stay on production surfaces (rrweb's already-shipped `record()` + content-script `GET_RRWEB_EVENTS` handler + `chrome.tabs.sendMessage`); Tier-1 FORBIDDEN_HOOK_STRINGS stays at **12 entries** (no new test-only symbols expected). RAM scaffolding stays optional and best-effort per D-P3-04. VERIFICATION.md aggregator (Plan 03-05) replicates Phase 2 VERIFICATION.md frontmatter shape (`overrides_applied` + `human_verification`).

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|------------------|
| REQ-install-clean | Fresh build + load unpacked into Chrome without errors; manifest:name resolves; no remote-font CSP errors; branded icons; en+ru parity | Already COMPLETED Phase 1 Plan 01-12; Phase 3 verifies via §10 #1 aggregator in VERIFICATION.md citing existing tests (no-remote-fonts + manifest-i18n + locale-parity + bundle gates) |
| REQ-rrweb-dom-buffer | rrweb records DOM events via `record()` over 10-min window, 5000-event cap, oldest-dropped; sensitive fields masked | Plan 03-01: synthetic form+table+modal probe HTML in `extension-page-harness.html`; harness assertion grep `events.length > 0` + EventType enum values 2/3/4; existing wiring at `src/content/index.ts:284-311` already ships maskInputOptions.password=true |
| REQ-user-event-log | Captures 5 event types: click, input (no password), navigation (popstate/hashchange/pushState/replaceState), js_error (error + unhandledrejection), network_error (fetch >= 400 + XHR >= 400 + network failure) | Plan 03-02: Puppeteer page.click + page.type + page.goto + dispatchEvent(ErrorEvent) + fetch(404); harness greps `events.json` parsed from latest archive for one entry of each `type` value; existing wiring at `src/content/index.ts:60-237` |
</phase_requirements>

## User Constraints (from CONTEXT.md)

### Locked Decisions

- **D-P3-01:** Phase 3 = exactly 5 atomic plans:
  - 03-01 rrweb DOM verification harness extension (§10 #4)
  - 03-02 event-log verification harness extension (§10 #5)
  - 03-03 §10 #8 password-filter verification (verify existing minimum; PARTIAL mark)
  - 03-04 §10 #9 RAM ceiling best-effort (operator instructions + optional `puppeteer.Page.metrics` scaffolding)
  - 03-05 full §10 sweep VERIFICATION.md aggregating §10 #1-#9 evidence

- **D-P3-02:** SPEC §10 #8 = verify existing minimum + PARTIAL mark in VERIFICATION.md. The `src/content/index.ts:82` `if (target.type === 'password') return;` filter is the existing minimum. Plan 03-03 ships a harness assertion that VERIFIES this filter fires (synthetic password input + grep events.json for absence of entered value). NOT in scope: rrweb v2 `maskInputFn`, `data-sensitive` HTML attribute guards, full §10 #8 closure.

- **D-P3-03:** rrweb 2.0.0-alpha.4 stays pinned through Phase 3. Upgrade research + implementation DEFER TO PHASE 4.

- **D-P3-04:** SPEC §10 #9 = best-effort + operator instructions in VERIFICATION.md. Optional scaffolding via `puppeteer.Page.metrics()` if practical without research budget. NOT in scope: `chrome.devtools.Memory` API.

### Claude's Discretion

- Harness assertion numbering: A29+ continuing from A24-A28 sequence.
- Probe page composition: synthetic HTML inline in `extension-page-harness.ts` vs. real-world navigation — planner's call.
- Event-log trigger strategy: synthetic Puppeteer events vs. natural lifecycle observation — planner's call.
- Sequencing: Plan 03-05 runs after 03-01..04 (synthesis plan); whether 03-01..04 parallelize within wave 2 depends on `files_modified` overlap audit at plan time.

### Deferred Ideas (OUT OF SCOPE)

- rrweb v2 stable upgrade research + implementation (D-P3-03)
- Programmatic RAM measurement via `chrome.devtools.Memory` API (D-P3-04 partial defer)
- REQ-password-confidentiality v2 candidate — full rrweb v2 `maskInputFn` + `data-sensitive` guards (D-P3-02)
- Audit P1 #11/#14/#15 polish (fetch Request→[object Request], navigation URL tracking, rrweb timestamp semantics)
- 2 pre-existing ffprobe/ffmpeg vitest flakes
- getDisplayMedia cursor visibility refinement
- Dark-surface logo contrast
- setimmediate polyfill `new Function` in SW chunk
- ROADMAP backfill for Plans 01-08..01-13

## Project Constraints (from global CLAUDE.md)

The user's global `~/.claude/CLAUDE.md` is loaded for this session (no project-local `CLAUDE.md` discovered). Directives that downstream planner + executor MUST honor:

| Directive | Application to Phase 3 |
|-----------|------------------------|
| **No `continue` statements — use filtering instead** | Already established in Plan 02-04 (`Object.keys(zip.files).filter((path) => !zip.files[path].dir)` per saved memory). New driveA* / assertA* code follows the filter-pipeline form. |
| **Prefer if-else chains over early returns** | The `if (target.type === 'password') return;` at `src/content/index.ts:82` is the verification subject for Plan 03-03 (existing code; not modifying it). New verification code follows if-else chains. |
| **No `break` inside loops when loop condition handles it** | A11 buffer-continuity loop precedent (`for (let i = 0; i < attempts; i++)` with no inner break) — follow same shape for any new polling loops. |
| **Full-word names; standard acronym exceptions** | Existing harness uses `assertA29`, `driveA29` etc — full-word + standard `AXX` namespacing. |
| **TypeScript semantic type aliases over raw types** | rrweb `EventType` enum is the semantic alias (imported from `@rrweb/types`); avoid magic numbers `2`, `3`, `4` in assertion code. |
| **Type arrow function parameters explicitly** | Harness already typed (e.g., `(item: chrome.downloads.DownloadItem): void`). New listener/predicate callbacks follow same form. |
| **Sanitize error messages: log details server-side, return generic messages to clients** | N/A for verification phase (no production-surface changes). |
| **`WORKAROUND(org/repo#issue)` tags** | Apply if any rrweb behavior requires a known-issue workaround (none anticipated — see Q3 below). |

## Architectural Responsibility Map

| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| rrweb DOM event verification | Test harness (page-realm) | Production content script (read-only) | rrweb runs in the content script per `src/content/index.ts:284`; the harness verifies via the existing `GET_RRWEB_EVENTS` round-trip + `events.json` parse — no new prod surface |
| Event-log capture verification | Test harness (page-realm) + Puppeteer host | Production content script (read-only) | Production wiring at `src/content/index.ts:60-237` is unchanged; harness drives synthetic browser events + greps the assembled archive's `logs/events.json` |
| Password-filter (§10 #8) verification | Test harness (page-realm) | Production content script (read-only at `src/content/index.ts:82`) | Negative-assertion grep on `events.json` for absence of typed-sentinel value |
| RAM ceiling (§10 #9) best-effort | Operator (chrome://memory-internals) | Optional `puppeteer.Page.metrics()` scaffolding | SW context is a separate target from `Page` — metrics API on Page does not reach it; operator-driven measurement is the canonical path per D-P3-04 |
| §10 sweep aggregation | Plan 03-05 VERIFICATION.md (docs tier) | — | Pure docs synthesis aggregating Phase 1 + 2 + 3 evidence |

## Standard Stack

### Core (existing; no new dependencies)

| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| puppeteer | ^25.0.2 | Real-Chrome driver for the UAT harness | Established Plan 01-13 (Approach B); extension-targets API + `Page.metrics` + service-worker `worker.evaluate` all live here |
| rrweb | 2.0.0-alpha.4 | Production DOM-event recorder being verified | Pinned per D-P3-03; alpha-pin stable across all phases to date |
| @rrweb/types | (sub-package of rrweb 2.0.0-alpha.4) | `EventType` enum + `eventWithTime` type | Already present transitively at `node_modules/@rrweb/types/dist/index.d.ts` |
| jszip | ^3.10.1 | Host-side archive parse for events.json + meta.json + zip-layout checks | Established Plan 02-04 (driveA26/A28) |
| tsx | ^4.22.1 | TS runner for harness scripts (`tsx tests/uat/harness.test.ts`) | Established Plan 01-13 |
| vitest | ^4 | Unit-test layer (Tier-1 grep gate + meta-json validators) | Established Phase 0/1 |

### Alternatives Considered

| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| `puppeteer.Page.metrics()` | `performance.measureUserAgentSpecificMemory()` inside `worker.evaluate()` | More accurate (per-SW measurement) BUT requires cross-origin-isolation (COOP+COEP headers) which MV3 extensions do not set — would throw `SecurityError`. Skip; not viable for D-P3-04. |
| `puppeteer.Page.metrics()` | CDP `Performance.getMetrics` directly via `page.target().createCDPSession()` | Equivalent data, more plumbing; `Page.metrics()` IS the wrapper. No win. |
| Manual operator UAT (full Phase 3 closure) | Harness assertions A29+ | Per saved memory `feedback-trust-harness-over-manual-uat.md`, automation covers what automation can cover. Operator UAT reserved for §10 #9 RAM only. |
| New rrweb test utility from `packages/rrweb/test/utils.ts` (assertDomSnapshot via MHTML) | Direct grep on `events[].type` for EventType enum values | rrweb's test util is for snapshot-comparison (MHTML diffs); Mokosh's charter is "records without errors" — structural assertion is simpler + matches charter literally. |

**Installation:** No new packages. Phase 3 is verification-only.

**Version verification (2026-05-20 via `npm view rrweb dist-tags`):**

```
{
  beta: '1.0.0-beta.2',
  latest: '2.0.0-alpha.4',  // 2023 release; PHASE-3 PIN
  alpha: '2.0.0-alpha.20'   // 2026-02-03 release; Phase-4 upgrade-research target
}
```

`latest` dist-tag still pointing at alpha.4 (4 years old) confirms (a) no v2 stable shipped, (b) the project actively publishes newer alphas but does not promote them to `latest`. Phase 4 upgrade research deferral is correctly framed.

## Architecture Patterns

### System Architecture Diagram

```
                        ┌─────────────────────────────────────────┐
                        │  tsx tests/uat/harness.test.ts          │  (Puppeteer HOST)
                        │  - launch.ts → chromium + --load-extension=dist-test
                        │  - sequential drive: driveA0..A28 + driveA29..A3X
                        │  - bail-on-first-failure                │
                        └────────────┬────────────────────────────┘
                                     │ Puppeteer CDP
                                     │
                ┌────────────────────┼────────────────────────────────┐
                │                    │                                │
                ▼                    ▼                                ▼
       ┌─────────────────┐  ┌─────────────────┐          ┌────────────────────────┐
       │ Page target     │  │ Service Worker  │          │ Offscreen Document     │
       │ (chrome-extension://<id>/tests/uat/  │          │ (chrome.offscreen)     │
       │  extension-page-harness.html)        │          │                        │
       │                 │  │ src/background/  │          │ getDisplayMedia        │
       │ window.__mokosh │  │ index.ts         │          │ (synthetic via Canvas) │
       │ Harness.assertA* │  │                  │          │ MediaRecorder segments │
       │                 │  │ ↕                │          │ keepalivePort blob URLs│
       │ chrome.tabs.    │  │ chrome.runtime.  │          │                        │
       │ sendMessage    →│←─│ onMessage        │←────────│ port: 'video-keepalive'│
       │ chrome.downloads.onCreated (cross-realm)        │                        │
       └─────────────────┘  └─────────────────┘          └────────────────────────┘
                ▲                    │                              ▲
                │                    │ chrome.tabs.sendMessage      │
                │                    │ (GET_RRWEB_EVENTS)            │
                │                    ▼                              │
                │           ┌─────────────────────────────┐         │
                │           │ Content Script              │         │
                │           │ src/content/index.ts        │         │
                │           │                             │         │
                │           │ rrweb.record() → rrwebEvents│         │
                │           │ click/input/nav/error log → userEvents│
                │           └─────────────────────────────┘         │
                │                                                    │
                └─────────── New for Phase 3 ────────────────────────┘
   Plan 03-01: probe HTML (form+table+modal) injected into the harness page tree
   Plan 03-02: Puppeteer synthetic event triggers + grep events.json from latest zip
   Plan 03-03: <input type=password> + sentinel typed + grep events.json for absence
   Plan 03-04: Page.metrics() scaffolding (optional) + operator instructions in VERIFICATION
   Plan 03-05: aggregator over Phase 1+2+3 evidence into VERIFICATION.md frontmatter
```

### Component Responsibilities

| File | Responsibility | Phase 3 Touch |
|------|---------------|---------------|
| `src/content/index.ts` | rrweb wiring + event-log wiring | **READ-ONLY** (verified, not modified) |
| `src/background/index.ts:GET_RRWEB_EVENTS` round-trip | SW → CS → SW transport of events for archive assembly | **READ-ONLY** |
| `src/shared/types.ts:UserEvent` | Event-log shape contract | **READ-ONLY** (already used in Plan 03-02 grep) |
| `tests/uat/extension-page-harness.html` | Harness page DOM scaffold; canonical tokens.css load-bearing for A18/A21 | **MODIFY** (Plan 03-01: add probe HTML below existing scaffold per UI-SPEC) |
| `tests/uat/extension-page-harness.ts` | Page-side `assertA*` host (3413 LoC) | **MODIFY** (Plan 03-01..04: append assertA29..A3X stubs/handlers) |
| `tests/uat/lib/harness-page-driver.ts` | Host-side `driveA*` host (1849 LoC) | **MODIFY** (Plan 03-01..04: append driveA29..A3X + JSZip-parse of events.json) |
| `tests/uat/harness.test.ts` | Orchestrator + FORBIDDEN_HOOK_STRINGS unit-mirror (500 LoC) | **MODIFY** (Plan 03-01..04: import driveA29+; push to drivers[]; banner) |
| `tests/background/no-test-hooks-in-prod-bundle.test.ts` | Tier-1 grep gate (12 entries currently) | **EXPECTED NO CHANGE** — A29+ ride existing prod surfaces |
| `.planning/phases/03-.../03-05-VERIFICATION.md` | Final phase aggregator | **CREATE** (Plan 03-05; replicate Phase 2 VERIFICATION.md frontmatter shape) |

### Pattern 1: Approach B Harness Extension (rrweb DOM verification)

**What:** Page-side `assertA29(): Promise<AssertionResult>` greps `events.json` from the latest archive by EventType enum value; host-side `driveA29(page)` chains off A28's already-completed SAVE or runs its own SAVE.

**When to use:** §10 #4 rrweb DOM verification (Plan 03-01) — synthetic probe HTML triggers rrweb to record meta + full snapshot + at least one incremental snapshot.

**Example:**

```typescript
// Source: existing pattern from tests/uat/lib/harness-page-driver.ts:driveA26
//         (Plan 02-04; established 2026-05-20)

// EventType enum values from @rrweb/types (verified via
// node_modules/@rrweb/types/dist/index.d.ts grep):
//   DomContentLoaded = 0
//   Load             = 1
//   FullSnapshot     = 2
//   IncrementalSnapshot = 3
//   Meta             = 4
//   Custom           = 5
//   Plugin           = 6

async function driveA29(page: Page, downloadsDir: string): Promise<AssertionResult> {
  // Chain off A28's already-completed SAVE — findLatestZip from harness-page-driver.ts
  const zipPath = await findLatestZip(downloadsDir);
  const buf = await fs.readFile(zipPath);
  const zip = await JSZip.loadAsync(buf);
  const rrwebRaw = await zip.file('rrweb/session.json')!.async('text');
  const events: Array<{ type: number; timestamp: number }> = JSON.parse(rrwebRaw);

  // Structural checks (no snapshot-compare; matches charter "records without errors")
  const hasMeta = events.some((e) => e.type === 4);
  const hasFullSnapshot = events.some((e) => e.type === 2);
  const hasIncremental = events.some((e) => e.type === 3);
  // ... AssertionResult with 4 checks: length>0 + Meta + FullSnapshot + Incremental
}
```

### Pattern 2: Event-Log Trigger via Puppeteer

**What:** Use `page.click` + `page.type` + `page.evaluate(() => window.dispatchEvent(...))` + `fetch(...)` from inside an opened tab to trigger all 5 `UserEvent` types; grep `logs/events.json` from the assembled archive.

**When to use:** §10 #5 event-log verification (Plan 03-02). All 5 types in one drive.

**Example (host-side; production surfaces only):**

```typescript
// Plan 03-02 driveA30 (illustrative; planner picks A-numbers):
// 1. Open a tab on https://example.com (production chrome.tabs.create)
// 2. page.type('<input>', 'hello') → input event
// 3. page.click('a') → click event
// 4. page.goto('https://example.com#frag') → hashchange navigation event
// 5. page.evaluate(() => window.dispatchEvent(new ErrorEvent('error',
//      { message: 'probe', filename: 'x', lineno: 1 }))) → js_error
// 6. page.evaluate(() => fetch('https://example.com/404-probe')) → network_error (HTTP 404)
// 7. SAVE_ARCHIVE; grep logs/events.json from latest zip
// 8. Assert: events.some((e) => e.type === 'click') + ... × 5 types
//    where UserEvent.type ∈ {'click','input','navigation','js_error','network_error'}
//    per src/shared/types.ts:124-131
```

### Pattern 3: Password Negative-Assertion

**What:** Type a sentinel string into `<input type="password">`; assert sentinel is ABSENT from `events.json`.

**When to use:** §10 #8 PARTIAL closure (Plan 03-03).

**Example:**

```typescript
// Plan 03-03 driveA31 (illustrative):
// const SENTINEL = 'secret-do-not-log-123';  // fixed; not a real secret
// await page.evaluate((s: string) => {
//   const input = document.querySelector<HTMLInputElement>('#probe-password');
//   if (input) { input.value = s; input.dispatchEvent(new Event('input', { bubbles: true })); }
// }, SENTINEL);
// SAVE_ARCHIVE; grep events.json
// const eventsRaw = await zip.file('logs/events.json')!.async('text');
// const userEvents: UserEvent[] = JSON.parse(eventsRaw);
// const containsSentinel = userEvents.some((e) => e.value?.includes(SENTINEL));
// expect(containsSentinel).toBe(false);  // ABSENCE proves the password filter fires
```

### Anti-Patterns to Avoid

- **Using rrweb's `assertDomSnapshot` (MHTML diff)** — Mokosh charter is "records without errors", not "matches a specific DOM snapshot". Snapshot-diff is brittle (random ports, timestamps, cursor positions per rrweb test utils). Structural assertion (EventType enum presence) matches charter literally and is order-of-magnitude simpler.
- **Asserting rrweb event count** — rrweb emits a variable number of incremental events depending on probe page interactivity. Use `>= 1` floors per type, not exact equality.
- **Reading SW heap via `puppeteer.Page.metrics()`** — `Page.metrics()` is bound to the Page target's V8 isolate. The MV3 service worker is a separate target (`target.type() === 'service_worker'`) with its own isolate. Document this limitation explicitly if scaffolding lands per D-P3-04.
- **Adding new test-only `__MOKOSH_UAT__`-gated symbols** — Plan 03-01..04 should ride production surfaces only (rrweb.record() already shipped; GET_RRWEB_EVENTS bridge shipped Phase 1; chrome.tabs/chrome.downloads shipped). If a new test-only symbol becomes necessary, it MUST be added to BOTH FORBIDDEN_HOOK_STRINGS inventories (unit-gate at `tests/background/no-test-hooks-in-prod-bundle.test.ts:108` + UAT A0 mirror at `tests/uat/harness.test.ts:115`).
- **Stripping the canonical tokens.css link from harness page** — Per Plan 01-12 Wave 6 + UI-SPEC: `<link rel="stylesheet" href="../../src/shared/tokens.css">` is load-bearing for A18 + A21. Probe HTML appends below the existing scaffold; does NOT modify the head.

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| rrweb event type identification | A hand-coded mapping `{ 0: 'DomContentLoaded', 2: 'FullSnapshot', ... }` | `EventType` enum from `@rrweb/types/dist/index.d.ts` | Already shipped in node_modules; values pinned by rrweb; import like `import { EventType } from '@rrweb/types';` — TypeScript-checked at build time. |
| Archive zip parsing in tests | A custom zip parser | `JSZip.loadAsync` (same pattern as `tests/uat/lib/zip.ts` + `tests/uat/lib/harness-page-driver.ts:driveA26/A28`) | Host-only (not bundled into harness page); same precedent as Plan 02-04. |
| Latest-zip lookup in downloadsDir | Hand-rolled directory poll | `findLatestZip` helper at `tests/uat/lib/harness-page-driver.ts:1395` | Mtime-sort chain; race-free because A24/A25/A27 drivers wait for their zip via stable-size protocol. |
| Cross-realm download capture | `chrome.downloads.download` monkey-patch in harness page realm | `chrome.downloads.onCreated` listener (Plan 02-04 A24 pattern at `tests/uat/extension-page-harness.ts:2851`) | Production cross-realm API; canonical capture; Tier-1 inventory unaffected. |
| Service worker memory measurement via Page.metrics | Treating `Page.metrics().JSHeapUsedSize` as the SW heap | Operator-driven `chrome://extensions/` service-worker memory display OR `chrome://memory-internals/` | Page.metrics is page-realm only; SW is a separate target. Per D-P3-04, operator instruction is the canonical path. |

**Key insight:** Phase 3 is mechanical replication of the Plan 02-04 harness-extension template against a new set of production surfaces (rrweb + event-log + password-filter). Custom infrastructure is anti-pattern — every needed pattern already exists in the harness lib.

## Runtime State Inventory

> Phase 3 is verification-only — no rename/refactor/migration. This section is included to confirm explicit non-applicability per protocol.

| Category | Items Found | Action Required |
|----------|-------------|------------------|
| Stored data | None — verified by `grep -rn "ChromaDB\|Mem0\|user_id" src/` returns 0 hits. No long-lived databases used; rrweb + event-log buffers are in-memory only per CON-buffer-storage. | None |
| Live service config | None — verified by `grep -rn "n8n\|Datadog\|Tailscale\|Cloudflare" .` returns 0 hits. Extension is local-only per SPEC §9 "Out of Scope". | None |
| OS-registered state | None — Phase 3 introduces no Windows Task Scheduler / pm2 / launchd / systemd registrations. | None |
| Secrets/env vars | None — no `.env` or SOPS files in repo; Phase 3 reads no secrets. | None |
| Build artifacts | None new — Phase 3 ships zero new bundled modules. Existing `dist/` + `dist-test/` artifacts regenerated by `npm run build` + `npm run build:test` are inputs to verification; no Phase-3-owned build artifacts. | None |

**Confirmation:** No runtime state to migrate; Phase 3 is verification-only.

## Common Pitfalls

### Pitfall 1: rrweb event timing race
**What goes wrong:** `assertA29` reads `events.json` from a zip assembled BEFORE rrweb's `IncrementalSnapshot` has fired (snapshots fire on mutation; static probe HTML produces only Meta + FullSnapshot).
**Why it happens:** Synthetic probe page has no DOM mutations between load and SAVE — `IncrementalSnapshot` is mutation-driven.
**How to avoid:** Plan 03-01 driveA29 MUST inject at least one DOM mutation (`page.evaluate(() => document.body.appendChild(document.createElement('div')))`) before SAVE, OR drive the modal trigger button click. Probe HTML modal + table both qualify per UI-SPEC.
**Warning signs:** A29.3 `hasIncrementalSnapshot` returns false intermittently.

### Pitfall 2: Service worker memory under-reporting via Page.metrics
**What goes wrong:** Plan 03-04 ships `puppeteer.Page.metrics().JSHeapUsedSize` and reports e.g. 4 MB; operator interprets this as "extension RAM is well under 50 MB"; real extension SW RAM (separate target) is 35 MB.
**Why it happens:** `Page.metrics()` measures only the page realm; SW lives in a different target.
**How to avoid:** If Plan 03-04 ships scaffolding, gate output behind explicit diagnostic copy: `"NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04."` Do NOT treat scaffolding value as authoritative; the operator chrome://memory-internals check is the binding §10 #9 gate.
**Warning signs:** Operator sees a green automation number and skips the chrome://memory-internals step.

### Pitfall 3: chrome.tabs permission gap on probe pages
**What goes wrong:** Plan 03-02 opens a tab on `https://example.com` via `chrome.tabs.create` — works because Phase 2 Plan 02-03 added the `tabs` permission (DEC-011 Amendment 1). However, if the probe-page composition uses URLs that violate the `URL_SCHEME_ALLOW` regex at `src/background/tab-url-tracker.ts:79` (`^(https?|chrome-extension):\/\/`), `meta.urls` will be empty and downstream assertions may fail intermittently.
**Why it happens:** `file://`, `about:`, `chrome://` URLs are filtered out of `urls[]` by design (F2 fallback path).
**How to avoid:** Plan 03-02 probe pages use `https://example.com` or `https://www.iana.org` (already established as Plan 02-04 A27 fixtures). Do NOT use `about:blank` or `file://`.
**Warning signs:** A30 passes locally but fails in CI when the test URL is changed to a non-http(s) target.

### Pitfall 4: rrweb alpha.4 known issue — `genTextAreaValueMutation`
**What goes wrong:** rrweb 2.0.0-alpha.4 has an open issue (`rrweb-io/rrweb#1596`) where `genTextAreaValueMutation` doesn't mask the `attribute.value` of a textarea. If the probe page includes a textarea, the value could leak into incremental snapshots EVEN WHEN `maskInputOptions.textarea` is set.
**Why it happens:** Known alpha.4 bug; not yet patched in any 2.0 release.
**How to avoid:** Plan 03-01 probe HTML uses `<input type="text">` + `<input type="email">` + `<input type="password">`, NOT `<textarea>`. The form-input variety in UI-SPEC Section "Test Fixture Conventions" already excludes textarea. Confirm during planning that no `<textarea>` is introduced.
**Warning signs:** A29 passes; a follow-on plan (not Phase 3) introduces a textarea and Plan 03-03 password-filter check passes but textarea leak is unaccounted for.

### Pitfall 5: Pre-checkpoint bundle gate breakage from Phase 3 changes
**What goes wrong:** Plan 03-* adds a new test-only symbol that escapes the Vite `__MOKOSH_UAT__` define-token tree-shake, lands in `dist/`, and breaks Gate 5/6 in the pre-checkpoint bundle gates.
**Why it happens:** A planner introduces a new debug-only flag or helper without adding it to FORBIDDEN_HOOK_STRINGS.
**How to avoid:** Per Plan 02-04 SUMMARY decision: "FORBIDDEN_HOOK_STRINGS unchanged at 12. A26/A28 are host-side JSZip; A27 uses chrome.tabs.create/update/remove (production APIs)." Phase 3 plans SHOULD stay at 12 entries. If any plan finds a real need for a new symbol, the planner MUST add it to BOTH FORBIDDEN_HOOK_STRINGS inventories AND `__MOKOSH_UAT__`-gate it.
**Warning signs:** Tier-1 grep gate fails after Plan 03-* commit; bundle gate 6 shows new occurrences in `dist/`.

### Pitfall 6: `findLatestZip` race between parallel A29-A3X drivers
**What goes wrong:** Plan 03-01..04 plans run in parallel within wave 2 (per "Claude's Discretion" in CONTEXT.md), each does its own SAVE, and `findLatestZip` returns the WRONG zip to a chained-assertion downstream.
**Why it happens:** Multiple SAVEs in flight + mtime collision (1s precision on some filesystems).
**How to avoid:** Per Phase 2 Wave 2 lesson cited in CONTEXT.md: "plan-checker should catch overlaps". Plan-checker validates `files_modified` overlap audit — if both Plan 03-01 and Plan 03-02 modify the same harness file in different ways, mark wave 2 as SEQUENTIAL. The safer default is: 03-01 → 03-02 → 03-03 → 03-04 sequential within wave 2; 03-05 in wave 3.
**Warning signs:** Wave 2 commits land in different order than planned; A30 passes locally but fails when chained after A29.

## Code Examples

### A29: Synthetic probe → rrweb event-shape grep

```typescript
// Source: pattern from tests/uat/lib/harness-page-driver.ts:driveA26
//         (Plan 02-04 host-side; established 2026-05-20)
// Plan: 03-01 (illustrative; planner finalizes)

import { EventType } from '@rrweb/types';

async function driveA29(page: Page, downloadsDir: string): Promise<AssertionResult> {
  // Step 1: trigger probe page interactions (form input + modal click)
  await page.evaluate(() => {
    const input = document.querySelector<HTMLInputElement>('#probe-text');
    if (input) {
      input.value = 'probe';
      input.dispatchEvent(new Event('input', { bubbles: true }));
    }
    const button = document.querySelector<HTMLButtonElement>('#probe-modal-trigger');
    if (button) {
      button.click();
    }
  });

  // Step 2: wait for at least one IncrementalSnapshot to land
  // (rrweb buffers events; small delay before SAVE is safe)
  await new Promise((resolve) => setTimeout(resolve, 500));

  // Step 3: SAVE — chain off A28's already-completed flow OR own SAVE
  // (planner decides based on files_modified overlap; see Pitfall 6)
  // [...]  // SAVE_ARCHIVE dispatch via existing pattern

  // Step 4: read latest zip + grep rrweb/session.json
  const zipPath = await findLatestZip(downloadsDir);
  const buf = await fsPromises.readFile(zipPath);
  const zip = await JSZip.loadAsync(buf);
  const rrwebRaw = await zip.file('rrweb/session.json')!.async('text');
  const events: Array<{ type: number; timestamp: number }> = JSON.parse(rrwebRaw);

  const checks = [
    {
      name: 'A29.1: rrweb session.json contains > 0 events',
      passed: events.length > 0,
      expected: '> 0',
      actual: String(events.length),
    },
    {
      name: 'A29.2: rrweb emitted at least one Meta event (EventType=4)',
      passed: events.some((e) => e.type === EventType.Meta),
      expected: 'has Meta',
      actual: String(events.some((e) => e.type === EventType.Meta)),
    },
    {
      name: 'A29.3: rrweb emitted at least one FullSnapshot (EventType=2)',
      passed: events.some((e) => e.type === EventType.FullSnapshot),
      expected: 'has FullSnapshot',
      actual: String(events.some((e) => e.type === EventType.FullSnapshot)),
    },
    {
      name: 'A29.4: rrweb emitted at least one IncrementalSnapshot (EventType=3)',
      passed: events.some((e) => e.type === EventType.IncrementalSnapshot),
      expected: 'has IncrementalSnapshot',
      actual: String(events.some((e) => e.type === EventType.IncrementalSnapshot)),
    },
  ];

  return {
    passed: checks.every((c) => c.passed),
    name: 'A29 — rrweb DOM events recorded without errors (SPEC §10 #4)',
    checks,
    diagnostics: [],
  };
}
```

### A3X: Optional puppeteer.Page.metrics() RAM scaffolding (Plan 03-04)

```typescript
// Source: pptr.dev/api/puppeteer.page.metrics + existing harness host pattern
// Plan: 03-04 (optional per D-P3-04; planner decides "if practical")

async function driveA3X_OptionalRamScaffold(page: Page): Promise<AssertionResult> {
  // Page.metrics is page-realm only — JSHeapUsedSize covers V8 isolate of THIS Page,
  // NOT the MV3 service worker (separate target).
  const metrics = await page.metrics();
  const jsHeapMB = metrics.JSHeapUsedSize !== undefined
    ? metrics.JSHeapUsedSize / (1024 * 1024)
    : -1;

  // 50 MB threshold per SPEC §10 #9; treat as best-effort floor for the page realm
  // alone. Operator-driven chrome://memory-internals is the binding §10 #9 gate.
  const checks = [
    {
      name: 'A3X.1: Page.metrics returned JSHeapUsedSize',
      passed: jsHeapMB >= 0,
      expected: '>= 0',
      actual: String(jsHeapMB),
    },
    {
      name: 'A3X.2: Page-realm JS heap < 50 MB (NOTE: scaffolding only; SW context excluded)',
      passed: jsHeapMB < 50,
      expected: '< 50 MB',
      actual: `${jsHeapMB.toFixed(2)} MB`,
    },
  ];

  return {
    passed: checks.every((c) => c.passed),
    name: 'A3X — RAM scaffolding (best-effort; page-realm only per D-P3-04)',
    checks,
    diagnostics: [
      'NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.',
    ],
  };
}
```

### VERIFICATION.md frontmatter template (Plan 03-05)

```yaml
---
phase: 03-spec-10-smoke-verification-dom-event-log-verification
verified: 2026-05-20T<HH-MM-SS>Z
status: passed
score: 9/9 SPEC §10 criteria
overrides_applied: <count>
override_notes:
  - dimension: "SPEC §10 #8 — Password masking (PARTIAL per D-P3-02 charter)"
    initial_status: "PARTIAL"
    rationale: |
      Full rrweb v2 maskInputFn + data-sensitive guards DEFERRED per 2026-05-20 charter
      'we don't care about privacy hardening. At least here.' REQ-password-confidentiality
      moved to Out of Scope v1. Existing src/content/index.ts:82 `if (target.type === 'password') return;`
      filter VERIFIED by Plan 03-03 A31 (sentinel string typed; greps events.json for absence).
human_verification:
  - dimension: "SPEC §10 #9 — Extension RAM ≤ 50 MB"
    rationale: |
      Per D-P3-04: operator instructions in VERIFICATION below — load extension, idle 5 min,
      open chrome://memory-internals OR chrome://extensions service-worker memory display,
      verify extension background RAM < 50 MB. Page.metrics scaffolding (if shipped by
      Plan 03-04) measures page-realm only and does not cover the SW target.
---
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Operator UAT for all functional gates | Approach B harness (Plan 01-13) | 2026-05-19 | Plan 01-09 Task 5 retired; operator UAT reserved for brand judgment + RAM-ceiling-style genuinely-non-automatable surfaces |
| Snapshot-comparison via MHTML (rrweb-io test util) | Structural EventType enum grep | Phase 3 introduction (this plan) | Order-of-magnitude simpler; matches "records without errors" charter literally |
| `data:application/zip;base64,...` download | `blob:chrome-extension://<id>/<uuid>` mint | Plan 02-02 (D-P2-01) | Closes audit P0-6 base64 cap; Plan 03-05 inherits A24 evidence for §10 #6 latency aggregation |
| 7-field `meta.json` with `url: string` | 8-field with `urls: string[]` + `schemaVersion: '2'` | Plan 02-03 (D-P2-02 + D-P2-03) | Plan 03-05 inherits A26 evidence for §10 #7 aggregation |

**Deprecated/outdated:**
- rrweb 1.x patterns referenced in older blog posts (e.g., `inlineStylesheet: true` as a separate option) — Mokosh uses 2.0.0-alpha.4 where the option lives inside `record()` config and is implicitly enabled. Not relevant for Phase 3 (verification-only).
- `chrome.tabCapture` references in pre-D-01 historical commits — fully replaced by `chrome.offscreen` + `getDisplayMedia`; Plan 01-05 closed.

## Assumptions Log

> Claims tagged `[ASSUMED]` in this research that the planner / discuss-phase should confirm before locking decisions. Most claims here are `[VERIFIED]` via npm registry + grep + file reads; the table lists residual unknowns.

| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | rrweb 2.0.0-alpha.4 has no NEW regressions since Phase 1 + 2 closure that would make Plan 03-01..03 flaky | Q3 / Pitfall 4 | LOW — alpha.4 is the same artifact that has powered 9 closed plans + 29/29 UAT GREEN end-to-end. Risk is a latent edge case in textarea handling (issue #1596) — mitigated by probe HTML excluding textarea. |
| A2 | The probe-page injection mechanism (synthetic HTML inline in `extension-page-harness.ts`) does not interfere with existing assertions A18+A21 that depend on canonical tokens.css being loaded | Pattern 1 / UI-SPEC | LOW — UI-SPEC explicitly bans the probe HTML from importing tokens.css; head element preserves the existing tokens.css link. Confirm during Plan 03-01 plan-checker pass. |
| A3 | `Page.metrics()` JSHeapUsedSize is available in Puppeteer 25.0.2 (project pin) | Pattern A3X / Pitfall 2 | LOW — Page.metrics has been stable since Puppeteer 1.x; documented at pptr.dev/api/puppeteer.page.metrics. Verify by quick smoke before committing scaffolding. |
| A4 | `EventType.Meta` (=4) always fires before `EventType.FullSnapshot` (=2) on a fresh recording start | Code Example A29 | LOW — rrweb spec; `Meta` event is the meta header (URL + width + height + timestamp) and is emitted as the first event of a session. If wrong, A29.2 + A29.3 both fail; both checks have independent grep so the failure mode is visible. |
| A5 | Plans 03-01..04 can complete the harness orchestrator extension without modifying `tests/uat/extension-page-harness.html` beyond the probe-HTML append | Component Responsibilities | LOW — probe HTML is below the existing `<h1>` and `<pre id="status">`; planner-side confirms the head + tokens.css link remain untouched. |
| A6 | Phase 3 plans can stay at FORBIDDEN_HOOK_STRINGS = 12 entries (no new __MOKOSH_UAT__ symbol required) | Pitfall 5 / Standard Stack | MEDIUM — most likely true (rrweb + chrome.tabs + chrome.downloads are already production surfaces), but if Plan 03-04 scaffolding requires a new bridge op (e.g., `get-page-metrics` from offscreen → harness), that would add 1-2 entries. Surface in plan-checker pass. |
| A7 | `chrome://memory-internals` is available on Chrome stable 124+ (operator-facing path) | RAM operator instructions / D-P3-04 | LOW — chrome://memory-internals is a stable internal URL since Chrome 80+. The `chrome://extensions/` service-worker memory display (Inspect views > service worker > Memory) is the canonical alternative. |
| A8 | The harness host can read the assembled zip's `rrweb/session.json` and `logs/events.json` via existing `findLatestZip` + JSZip pattern (Plan 02-04 precedent) | Don't Hand-Roll / Pattern 1 | VERIFIED — Plan 02-04 driveA26 + driveA28 already do exactly this for meta.json + zip-layout; rrweb/session.json + logs/events.json are sibling entries in the canonical 5-entry layout. |

**If the table is mostly LOW risk:** confirmed. The only MEDIUM is A6; planner-side flag.

## Open Questions (RESOLVED)

1. **Wave structure: parallel or sequential for Plans 03-01..04?**
   - What we know: Plan 02-04 ran A24-A28 sequentially within a single plan (one author). Phase 3's 5-plan structure is finer-grained per D-P3-01.
   - What's unclear: Do Plans 03-01, 03-02, 03-03 modify the SAME `tests/uat/extension-page-harness.ts` + `tests/uat/lib/harness-page-driver.ts` files in ways that would conflict on parallel execution? CONTEXT.md "Claude's Discretion" defers to plan-time `files_modified` overlap audit.
   - Recommendation: Default sequential (03-01 → 03-02 → 03-03 → 03-04 → 03-05) unless plan-checker confirms zero overlap. Phase 2 lesson cited in CONTEXT.md is "plan-checker should catch overlaps"; honor it.

2. **A29 chaining: own SAVE vs. chain off A28?**
   - What we know: A26 + A28 chain off A25's already-completed SAVE per Plan 02-04 (no new SAVE). A27 owns its SAVE because the tab tracker needs the multi-tab onActivated events to fire BEFORE dispatch.
   - What's unclear: Does the probe-page DOM injection happen before the existing harness page's recorded session, or before a separate session? rrweb starts on `start()` (content-script init) — by the time the harness page is loaded, rrweb is already recording.
   - Recommendation: A29 chains off A28's already-completed SAVE if the DOM mutations happen during the rolling 30s recording window before A25's SAVE. If timing-fragile, A29 owns its SAVE with `setupFreshRecording` + segment-settle, mirroring A27. Plan 03-01 planner picks based on smoke-test.

3. **Plan 03-04 Page.metrics scaffolding: include or skip?**
   - What we know: D-P3-04 says "if practical without research budget". This research budget is exhausted; Page.metrics IS practical (single API call); scaffolding is ~30 lines.
   - What's unclear: Is the resulting page-realm-only measurement value worth the diagnostic copy overhead? Operator-facing chrome://memory-internals is unaffected.
   - Recommendation: SHIP the scaffolding (low cost). Output is informational only; explicit diagnostic copy ("page-realm only; SW context excluded") avoids over-interpretation. Tier-1 inventory unaffected (Page.metrics is host-side; not bundled).

## Environment Availability

| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Node.js + npm | All harness tooling | ✓ | (project pin) | — |
| Puppeteer ^25 | UAT harness driver | ✓ | 25.0.2 (package.json) | — |
| Chrome / Chromium (Puppeteer-bundled) | Real-Chrome target for harness | ✓ | Puppeteer-bundled stable | — |
| rrweb 2.0.0-alpha.4 + @rrweb/types | DOM event verification (Plan 03-01) | ✓ | Pinned (package.json:11) | — |
| JSZip | Archive parsing in driveA29+ | ✓ | 3.10.1 | — |
| tsx | TS runner | ✓ | 4.22.1 | — |
| vitest | Unit-test layer (Tier-1 grep gate) | ✓ | ^4 | — |
| chrome://memory-internals (operator) | §10 #9 RAM check | ✓ (Chrome stable 80+) | — | chrome://extensions service-worker Memory tab |
| ffprobe / ffmpeg | §10 #7 video playback re-verification (already covered in Phase 1 VERIFICATION) | Already verified Phase 1 closure | — | — |

**Missing dependencies with no fallback:** None. Phase 3 introduces zero new dependencies.

**Missing dependencies with fallback:** None.

## Validation Architecture

> Required per `.planning/config.json:workflow.tdd_mode = true` (workflow.nyquist_validation absent — treat as enabled).

### Test Framework

| Property | Value |
|----------|-------|
| Framework | vitest ^4 (171/171 GREEN post Phase 2) + Puppeteer ^25 UAT harness (29/29 GREEN) |
| Config file | `vite.config.ts` + `vite.test.config.ts` (defines `__MOKOSH_UAT__`); `vitest.config.ts` if present (project pins via package.json scripts) |
| Quick run command | `npm test` (vitest) for unit tier; `SKIP_PROD_REBUILD=1 HEADLESS=1 npm run test:uat` for harness tier |
| Full suite command | `npm test && npm run test:uat` (sequential — UAT depends on `npm run build:test` artifact) |

### Phase Requirements → Test Map

| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| REQ-install-clean | §10 #1 fresh build loads unpacked w/o errors | aggregator (existing tests) | `npm test` covers manifest + i18n + no-remote-fonts | ✅ tests/i18n/manifest-i18n.test.ts + tests/build/no-remote-fonts.test.ts |
| REQ-rrweb-dom-buffer | §10 #4 rrweb records meta+full+incremental on probe page | UAT harness (Plan 03-01 A29) | `npm run test:uat` (post-Plan 03-01) | ❌ Wave 0 (A29 to be created) |
| REQ-user-event-log | §10 #5 events.json contains 5 UserEvent types | UAT harness (Plan 03-02 A30) | `npm run test:uat` (post-Plan 03-02) | ❌ Wave 0 (A30 to be created) |
| (§10 #8 PARTIAL per D-P3-02) | Password sentinel absent from events.json | UAT harness (Plan 03-03 A31 negative-assert) | `npm run test:uat` (post-Plan 03-03) | ❌ Wave 0 (A31 to be created) |
| (§10 #9 best-effort per D-P3-04) | RAM scaffolding via Page.metrics OR operator chrome://memory-internals | UAT harness (Plan 03-04 A3X, optional) + manual-only operator step | `npm run test:uat` (post-Plan 03-04) + operator | Optional Wave 0 (A3X may or may not be created) |

### Sampling Rate
- **Per task commit:** `npm test` (~30s for full vitest suite)
- **Per wave merge:** `npm run test:uat` (~95s; Plan 02-04 baseline)
- **Phase gate:** `npm test && npm run test:uat` both GREEN before `/gsd-verify-work 3`

### Wave 0 Gaps
- [ ] `tests/uat/extension-page-harness.ts` — append probe HTML injection helper + assertA29..A31 (+ optional A3X) — Plan 03-01..04
- [ ] `tests/uat/lib/harness-page-driver.ts` — append driveA29..A31 (+ optional A3X) + EventType import — Plan 03-01..04
- [ ] `tests/uat/harness.test.ts` — import + drivers[] push + banner — Plan 03-01..04
- [ ] `tests/uat/extension-page-harness.html` — append probe HTML below existing `<h1>` + `<pre id="status">`; preserve tokens.css link — Plan 03-01

*(No new test framework install required. No new vitest test files anticipated — all new assertions live in the UAT harness tier.)*

## Security Domain

> `security_enforcement` is not explicitly set in `.planning/config.json` — treating as default-enabled.

### Applicable ASVS Categories

| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | no | Extension is local-only; no auth surface |
| V3 Session Management | no | Local-only; no remote sessions |
| V4 Access Control | partial | MV3 permissions baseline (DEC-011 + Amendment 1) — verified via existing `manifest-i18n.test.ts` |
| V5 Input Validation | yes | `URL_SCHEME_ALLOW` regex at `src/background/tab-url-tracker.ts:79` validates URL schemes; probe page inputs are not user input from a security boundary perspective |
| V6 Cryptography | no | No cryptography in Phase 1 v1 surface |
| V7 Error Handling and Logging | partial | Logging via `ContentLogger`; no PII expected (passwords filtered) |
| V8 Data Protection | partial | Password filter at `src/content/index.ts:82` is the v1 minimum per D-P3-02; full masking deferred to Phase 4+ if charter reverses |

### Known Threat Patterns for Phase 3

| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| Test-only hook surface leaking to production bundle | Information Disclosure | Tier-1 FORBIDDEN_HOOK_STRINGS gate (12 entries; unit-mirror + UAT A0 mirror); `__MOKOSH_UAT__` Vite define-token tree-shake; verified per Plan 02-04 SUMMARY |
| Probe page sentinel value (e.g., "secret-do-not-log-123") leaking via grep mismatch | Information Disclosure | Sentinel is a fixed test constant, not a real secret; absence-assertion proves filter fires; logged to event log triggers explicit RED |
| New chrome.* permission grant for probe-page composition | Elevation of Privilege | Phase 3 plans explicitly stay at DEC-011 + Amendment 1 baseline (8 permissions); no new manifest changes |
| rrweb data buffer growth past 5000 events / 10min cap during probe | Denial of Service | `cleanupOldEvents` at `src/content/index.ts:27-50` already enforces (oldest-dropped); Plan 03-01 probe HTML interaction is brief (~500ms) and far below the cap |

## Sources

### Primary (HIGH confidence)
- npm registry verified 2026-05-20 via `npm view rrweb dist-tags`: `latest=2.0.0-alpha.4, alpha=2.0.0-alpha.20, beta=1.0.0-beta.2`
- @rrweb/types EventType enum verified via `node_modules/@rrweb/types/dist/index.d.ts`: `{DomContentLoaded:0, Load:1, FullSnapshot:2, IncrementalSnapshot:3, Meta:4, Custom:5, Plugin:6}`
- Plan 02-04 SUMMARY (`.planning/phases/02-stabilize-export-pipeline/02-04-SUMMARY.md`) — most recent Approach B harness precedent; 29/29 UAT GREEN
- Plan 01-13 SUMMARY — Approach B foundation; FORBIDDEN_HOOK_STRINGS pattern
- Plan 01-14 SUMMARY — single-assertion plan precedent (A23); 7-file atomic-commit shape
- `src/content/index.ts` — production wiring being verified (read-only for Phase 3)
- `src/shared/types.ts:124-131` — UserEvent type contract
- `tests/uat/extension-page-harness.ts:2851-3413` — assertA24-A28 implementations (latest pattern)
- `tests/uat/lib/harness-page-driver.ts` — driveA* + findLatestZip + JSZip pattern
- `tests/background/no-test-hooks-in-prod-bundle.test.ts:108-126` — Tier-1 inventory (12 entries)
- 03-CONTEXT.md (D-P3-01..04 locked decisions + canonical_refs)
- 03-UI-SPEC.md (null-spec; probe-page conventions; FORBIDDEN_HOOK_STRINGS lockstep guidance)

### Secondary (MEDIUM confidence)
- Puppeteer official docs: https://pptr.dev/api/puppeteer.page.metrics (Page.metrics + JSHeapUsedSize semantics)
- Puppeteer official docs: https://pptr.dev/guides/chrome-extensions (MV3 SW target acquisition pattern)
- MDN: https://developer.mozilla.org/en-US/docs/Web/API/Performance/measureUserAgentSpecificMemory (SW memory measurement requires COOP+COEP — not viable for MV3 extensions)
- rrweb master/test/utils.ts (reference for assertDomSnapshot pattern — explicitly rejected for charter mismatch)
- W3C Screen Capture spec §6.1 (already cited in Plan 01-14; relevant for §10 #9 if RAM scaffolding research expands)

### Tertiary (LOW confidence — flagged for plan-time validation)
- chrome://memory-internals stability across Chrome stable channel (cited at A7 in Assumptions Log; common operator path but not formally guaranteed)
- WebSearch findings on rrweb 2.0 stable release status — cross-verified with npm registry (HIGH); separate WebSearch hit on "rrweb 2.0 production ready" returned an unrelated commercial-fork article, not the canonical rrweb-io repo

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — all dependencies present in package.json; versions verified via `npm view`
- Architecture: HIGH — Approach B template from Plan 02-04 is direct precedent; 3 chained-assertion patterns proven
- Pitfalls: HIGH for Pitfalls 1-3 (verified against existing harness + rrweb registry); MEDIUM for Pitfalls 4-6 (research-derived; not yet exercised in this codebase)
- RAM ceiling (§10 #9): MEDIUM — Page.metrics scope verified; SW heap measurement caveat verified; chrome://memory-internals operator instructions are standard but not version-stamped
- rrweb v2 upgrade safety: HIGH — alpha.4 pin status verified directly via npm dist-tags

**Research date:** 2026-05-20
**Valid until:** 2026-06-20 (30 days; stable dependencies; if rrweb 2.0 stable ships in the interim, re-check D-P3-03 deferral)

## RESEARCH COMPLETE
