Files
mokosh/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md
Mark de3f14722f docs(03): plan-phase closure — checker WARNING resolved + preferences consumed + state synced
Plan-checker iter-1 VERIFICATION PASSED with 1 cosmetic WARNING (Dimension 11
Research Resolution: Open Questions section heading lacked (RESOLVED) suffix
convention). Fixed inline: heading now reads "## Open Questions (RESOLVED)".

.plan-phase-preferences.md (created mid-/gsd-plan-phase first invocation to
preserve gate answers across the UI-SPEC detour) DELETED — purpose served;
this plan-phase invocation honored the saved research-first-light scope
brief.

state.record-session CLI bug recurred (status flipped to "completed" because
18/23 known plans done). Restored: status=ready_to_execute. percent: 78 is
correct now (5 Phase 3 plans counted; was 18/18=100 stale).

Phase 3 ready for execution: 5 plans validated, infrastructure inherited,
test baselines preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:06:00 +02:00

656 lines
53 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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