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

53 KiB
Raw Blame History

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:

// 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):

// 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:

// 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

// 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)

// 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)

---
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)

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