feat(03): plans 01-05 — Phase 3 SPEC §10 smoke + DOM/event-log verification

5 plans across 5 waves (Wave 2 sequential per RESEARCH Pitfall 6 file overlap):
- 03-01 Wave 1: rrweb DOM verification harness extension (A29; REQ-rrweb-dom-buffer; §10 #4)
- 03-02 Wave 2: event-log verification harness extension (A30; REQ-user-event-log; §10 #5)
- 03-03 Wave 3: §10 #8 password-filter PARTIAL verification (A31; D-P3-02 charter)
- 03-04 Wave 4: §10 #9 RAM ceiling best-effort + Page.metrics scaffolding (A32; D-P3-04)
- 03-05 Wave 5: §10 sweep VERIFICATION.md + REQUIREMENTS/ROADMAP/STATE marker flips
  (REQ-install-clean + REQ-rrweb-dom-buffer + REQ-user-event-log)

Each plan has:
- frontmatter (wave + depends_on + files_modified + autonomous + requirements + tags + must_haves)
- tasks with mandatory <read_first> + <acceptance_criteria> + concrete <action>
- <threat_model> block per security gate
- Validation map row(s) added to 03-VALIDATION.md (10 tasks total)

Expected UAT growth: 29/29 → 33/33 GREEN (A29-A32 + 03-05 docs).
Expected vitest baseline preserved: 171/171.
Expected Tier-1 FORBIDDEN_HOOK_STRINGS: 12 (A29+ ride production surfaces only).

ROADMAP.md Phase 3 entry replaces "Plans: TBD" with full 5-plan list.
VALIDATION.md status: planner_filled (nyquist_compliant: true; wave_0_complete: true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 19:01:21 +02:00
parent 6af952700b
commit b3bfbf4a8d
7 changed files with 3214 additions and 24 deletions

View File

@@ -212,7 +212,12 @@ per 2026-05-20 re-phasing.
5. Background RAM consumption (measured via Chrome Task Manager) does not
exceed 50 MB during a sustained recording session (CON-ram-ceiling).
**Plans**: TBD
**Plans**: 5 plans (03-01 through 03-05).
- [ ] 03-01-PLAN.md — rrweb DOM verification harness extension (A29; SPEC §10 #4; REQ-rrweb-dom-buffer)
- [ ] 03-02-PLAN.md — event-log verification harness extension (A30; SPEC §10 #5; REQ-user-event-log)
- [ ] 03-03-PLAN.md — §10 #8 password-filter PARTIAL verification (A31; D-P3-02 charter)
- [ ] 03-04-PLAN.md — §10 #9 RAM ceiling best-effort + Page.metrics scaffolding (A32; D-P3-04 charter)
- [ ] 03-05-PLAN.md — §10 sweep VERIFICATION.md aggregator + REQUIREMENTS/ROADMAP/STATE marker flips
### Phase 4: Harden + clean up _(optional)_
**Goal**: Eliminate the P1/P2 follow-ups identified in the audit so that the

View File

@@ -0,0 +1,768 @@
---
phase: 03
slug: spec-10-smoke-verification-dom-event-log-verification
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- tests/uat/extension-page-harness.html
- tests/uat/extension-page-harness.ts
- tests/uat/lib/harness-page-driver.ts
- tests/uat/harness.test.ts
autonomous: true
requirements:
- REQ-rrweb-dom-buffer
tags:
- uat-harness
- a29
- rrweb
- spec-10-4
- approach-b
- probe-html
user_setup: []
must_haves:
truths:
- "rrweb session.json contains > 0 events after a probe-page interaction"
- "rrweb emits at least one Meta event (EventType=4) on session start"
- "rrweb emits at least one FullSnapshot (EventType=2) on session start"
- "rrweb emits at least one IncrementalSnapshot (EventType=3) after a DOM mutation on the probe page"
- "UAT harness exits 0 with 29 + 1 = 30/30 assertions GREEN (A0..A28 baseline preserved + new A29)"
artifacts:
- path: "tests/uat/extension-page-harness.html"
provides: "Probe HTML (form + table + modal trigger + DOM mutation source) appended below existing scaffold; tokens.css link in head untouched"
contains: "id=\"probe-form\""
- path: "tests/uat/extension-page-harness.ts"
provides: "assertA29 page-side stub registered on window.__mokoshHarness"
contains: "assertA29"
- path: "tests/uat/lib/harness-page-driver.ts"
provides: "driveA29 host-side: triggers probe-page DOM mutation, runs setupFreshRecording + SAVE, JSZip-parses rrweb/session.json, EventType enum grep"
contains: "driveA29"
- path: "tests/uat/harness.test.ts"
provides: "driveA29 import + wrapped driver + drivers-array push entry with banner comment"
contains: "driveA29"
key_links:
- from: "tests/uat/harness.test.ts"
to: "tests/uat/lib/harness-page-driver.ts driveA29"
via: "import + wrapped driver const + drivers-array push"
pattern: "driveA29Wrapped"
- from: "tests/uat/lib/harness-page-driver.ts driveA29"
to: "tests/uat/extension-page-harness.ts assertA29"
via: "page.evaluate(() => window.__mokoshHarness.assertA29())"
pattern: "harness.assertA29\\(\\)"
- from: "tests/uat/lib/harness-page-driver.ts driveA29"
to: "@rrweb/types EventType enum"
via: "import { EventType } from '@rrweb/types'"
pattern: "import.*EventType.*from.*@rrweb/types"
---
<objective>
Extend the UAT harness with A29 — empirical verification that SPEC §10 #4
(rrweb DOM event recording on typical pages) is satisfied. Production
wiring at `src/content/index.ts:284-309` already ships `rrweb.record()` +
`maskInputOptions.password=true`; A29 confirms it actually emits the
required EventType events on a synthetic probe page (form + table +
modal + DOM-mutation trigger).
Purpose: Closes REQ-rrweb-dom-buffer empirical verification gap. Phase 1
shipped the wiring; Phase 3 confirms it works end-to-end through a real
Chrome instance + a real probe page.
Output: 4-check A29 assertion (events.length > 0 + Meta present +
FullSnapshot present + IncrementalSnapshot present); UAT count 29 → 30
GREEN; probe HTML appended to extension-page-harness.html below the
existing `<pre id="status">` scaffold (head unchanged; tokens.css link
preserved for A18/A21).
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-UI-SPEC.md
@.planning/phases/02-stabilize-export-pipeline/02-04-SUMMARY.md
@src/content/index.ts
@tests/uat/extension-page-harness.html
<interfaces>
<!-- Key contracts the executor needs. Extracted from existing code. -->
<!-- Do NOT re-read; these are the canonical signatures. -->
From @rrweb/types/dist/index.d.ts (lines 186-194):
```typescript
export declare enum EventType {
DomContentLoaded = 0,
Load = 1,
FullSnapshot = 2,
IncrementalSnapshot = 3,
Meta = 4,
Custom = 5,
Plugin = 6
}
```
From tests/uat/lib/assertions.ts (lines 25-44):
```typescript
export interface CheckRecord {
readonly name: string;
readonly expected: unknown;
readonly actual: unknown;
readonly passed: boolean;
}
export interface AssertionRecord {
readonly passed: boolean;
readonly name: string;
readonly checks: ReadonlyArray<CheckRecord>;
readonly diagnostics: ReadonlyArray<string>;
readonly error?: string;
}
```
From tests/uat/extension-page-harness.ts (line 169):
```typescript
interface AssertionResult {
passed: boolean;
name: string;
checks: Array<{ name: string; expected: unknown; actual: unknown; passed: boolean }>;
diagnostics: string[];
error?: string;
}
function diag(result: AssertionResult, line: string): void; // line 190
async function setupFreshRecording(): Promise<{ ok: boolean; error?: string }>; // line 825
async function sendMessageWithTimeout<T>(msg: unknown, timeoutMs: number, label: string): Promise<T>; // line 234
```
From tests/uat/lib/harness-page-driver.ts (lines 1395, 41):
```typescript
function findLatestZip(downloadsDir: string): string | null; // host-side; mtime-sort wins
import JSZip from 'jszip'; // host-only, not bundled to page
```
From src/content/index.ts (verifier subject — READ-ONLY):
```typescript
// Lines 284-311: record({ emit(event){ rrwebEvents.push(event) }, maskInputOptions: { password: true, ... } })
// Line 318: chrome.runtime.onMessage 'GET_RRWEB_EVENTS' returns { events, userEvents }
// Line 82: setupInputLogging filter — if (target.type === 'password') return;
```
From src/background/index.ts (READ-ONLY):
```typescript
// GET_RRWEB_EVENTS handler in createArchive: chrome.tabs.sendMessage(activeTabId, { type: 'GET_RRWEB_EVENTS' })
// Result lands in rrweb/session.json inside the assembled zip
```
</interfaces>
# Plan Anchors
- **Approach B pattern source:** Plan 02-04 driveA26 (canonical host-side
JSZip read; chains off prior SAVE) — see `02-04-SUMMARY.md` and
`tests/uat/lib/harness-page-driver.ts:1421-1567` for the exact 3-phase
shape (page-side stub → findLatestZip → JSZip-parse).
- **RESEARCH Pitfall 1 (MUST OBSERVE):** synthetic probe HTML produces
`Meta` + `FullSnapshot` on load but NOT `IncrementalSnapshot` unless a
DOM mutation happens between page load and SAVE. Plan must inject a
mutation pre-SAVE (e.g. click the modal trigger button or input text).
- **RESEARCH Pitfall 4 (HARD BAN):** Probe HTML MUST NOT include
`<textarea>` — rrweb 2.0.0-alpha.4 issue #1596 leaks textarea values
even with `maskInputOptions.textarea` set.
- **UI-SPEC ban (HARD):** Probe HTML must NOT modify the `<head>` block.
The existing `<link rel="stylesheet" href="../../src/shared/tokens.css">`
is load-bearing for A18 + A21. Probe HTML appends below the existing
`<pre id="status">` (line 21) and BEFORE the existing `<script>` tag.
- **FORBIDDEN_HOOK_STRINGS lockstep (RESEARCH §"Anti-Patterns"):** A29
rides production surfaces (rrweb.record + GET_RRWEB_EVENTS bridge +
chrome.runtime.sendMessage). Tier-1 inventory stays at **12 entries**.
No new `__MOKOSH_UAT__`-gated symbols expected.
- **Source audit citation:** Implements REQ-rrweb-dom-buffer per
REQUIREMENTS.md lines 54-60 (10-min window + 5000-event cap + DOM
capture via rrweb 2.0.0-alpha.4 pin). The 10-min cleanup mechanics
are already covered by `src/content/index.ts:27-50` and are NOT
exercised by A29 — A29's contract is "rrweb records events without
errors on probe pages", which closes §10 #4 literally per CONTEXT
D-P3-01.
</context>
<tasks>
<task type="auto" tdd="false">
<name>Task 1: Append probe HTML to extension-page-harness.html (form + table + modal)</name>
<files>tests/uat/extension-page-harness.html</files>
<read_first>
- tests/uat/extension-page-harness.html (full file; 24 lines — see exact contents in interfaces above)
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-UI-SPEC.md §"Test Fixture Conventions" (probe HTML rules: NO textarea; NO tokens.css import; NO data-mokosh-* attrs; data-test-* OK)
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md §"Pitfall 1" (DOM mutation required pre-SAVE) and §"Pitfall 4" (textarea ban)
</read_first>
<behavior>
- The harness page exposes (querySelector-reachable) a form with at
least one text input (id="probe-text"), one email input
(id="probe-email"), one password input (id="probe-password"),
one submit button, and an enclosing form (id="probe-form").
- The harness page exposes a small static table with thead + tbody +
at least 2 data rows (id="probe-table").
- The harness page exposes a modal trigger button
(id="probe-modal-trigger") that, when clicked, toggles a hidden
div (id="probe-modal") to visible (display:none → display:block).
This click is the canonical DOM-mutation source per Pitfall 1.
- The existing tokens.css link in `<head>` is NOT modified.
- The existing `<h1>`, two `<p>` lines, and `<pre id="status">` are
NOT modified. The existing `<script type="module">` is NOT moved.
- The probe HTML appears between line 21 (`<pre id="status">`) and
line 22 (`<script type="module">`) in the source — appended to
the body after the existing scaffold, before the script.
- No `<textarea>` element anywhere in the file (rrweb alpha.4
issue #1596).
- No `<link>` or `<style>` additions referencing tokens.css.
- No `data-mokosh-slot`, `data-mokosh-key`, or `data-mokosh-i18n-key`
attributes (reserved for production welcome page).
- The probe HTML uses inline minimal styles (`style="display:none"`
on the modal); no `<style>` block.
- On the modal trigger click, `document.getElementById('probe-modal')`
toggles its `style.display` between 'none' and 'block' via a
small inline `onclick` handler. (rrweb records the attribute
mutation, satisfying IncrementalSnapshot emission.)
</behavior>
<action>
1. Read the current `tests/uat/extension-page-harness.html` (24 lines, contents shown in interfaces).
2. Open the file for editing. Locate the line ` <pre id="status">Ready.</pre>` (line 21) and the next line ` <script type="module" src="./extension-page-harness.ts"></script>` (line 22).
3. Insert the following probe HTML block BETWEEN line 21 and line 22 (preserving the trailing newline structure):
```html
<!--
Plan 03-01 Wave 1 — probe HTML for A29 rrweb DOM verification
(SPEC §10 #4). Provides form + table + modal-trigger so rrweb's
record() emits Meta + FullSnapshot + IncrementalSnapshot events
against this page. Per RESEARCH Pitfall 4: NO <textarea> (rrweb
2.0.0-alpha.4 issue #1596 leaks textarea values even with
maskInputOptions.textarea set). Per UI-SPEC Section "Test Fixture
Conventions": probe HTML uses plain data-test-* attributes; no
data-mokosh-* (production-welcome-page reserved); no tokens.css
import (the harness head already imports the canonical tokens
for A18/A21 — probe sub-tree should appear unstyled to keep
rrweb snapshots focused on structural DOM).
-->
<form id="probe-form" data-test="probe-form">
<input type="text" id="probe-text" data-test="probe-text" />
<input type="email" id="probe-email" data-test="probe-email" />
<input type="password" id="probe-password" data-test="probe-password" />
<button type="submit" id="probe-submit" data-test="probe-submit">Submit</button>
</form>
<table id="probe-table" data-test="probe-table">
<thead>
<tr><th>col-a</th><th>col-b</th></tr>
</thead>
<tbody>
<tr><td>row-1-a</td><td>row-1-b</td></tr>
<tr><td>row-2-a</td><td>row-2-b</td></tr>
</tbody>
</table>
<button id="probe-modal-trigger" data-test="probe-modal-trigger"
onclick="(function(){var m=document.getElementById('probe-modal');if(m){m.style.display=(m.style.display==='block'?'none':'block');}})()">
Toggle modal
</button>
<div id="probe-modal" data-test="probe-modal" style="display:none;">
<p>Probe modal content (Plan 03-01 A29 IncrementalSnapshot trigger).</p>
</div>
```
4. Verify no `<textarea>` was introduced (grep should return 0 hits on the modified file).
5. Verify the `<head>` block is unchanged.
6. No new dependencies, no other file edits in this task.
</action>
<verify>
<automated>grep -c "id=\"probe-form\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' &amp;&amp; grep -c "id=\"probe-modal-trigger\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' &amp;&amp; grep -c "id=\"probe-table\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' &amp;&amp; ! grep -q "textarea" tests/uat/extension-page-harness.html &amp;&amp; grep -q '<link rel="stylesheet" href="../../src/shared/tokens.css">' tests/uat/extension-page-harness.html</automated>
</verify>
<acceptance_criteria>
- `grep -c 'id="probe-form"' tests/uat/extension-page-harness.html` returns exactly `1`.
- `grep -c 'id="probe-modal-trigger"' tests/uat/extension-page-harness.html` returns exactly `1`.
- `grep -c 'id="probe-table"' tests/uat/extension-page-harness.html` returns exactly `1`.
- `grep -c 'id="probe-password"' tests/uat/extension-page-harness.html` returns exactly `1` (carried into Plan 03-03).
- `grep -c 'textarea' tests/uat/extension-page-harness.html` returns `0`.
- `grep -c '<link rel="stylesheet" href="../../src/shared/tokens.css">' tests/uat/extension-page-harness.html` returns exactly `1` (head unchanged).
- `grep -c 'data-mokosh-slot\|data-mokosh-key\|data-mokosh-i18n-key' tests/uat/extension-page-harness.html` returns `0`.
- `npm run build` exits 0.
</acceptance_criteria>
<done>
Probe HTML lives in the harness page below the existing scaffold;
head + script tag + existing body content all unchanged; no
textarea; ready for Task 2 to wire assertA29.
</done>
</task>
<task type="auto" tdd="false">
<name>Task 2: Add assertA29 page-side stub + driveA29 host-side + harness.test.ts orchestrator wiring</name>
<files>tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
<read_first>
- tests/uat/extension-page-harness.ts lines 3151-3413 (assertA26 stub + assertA27 multi-tab + assertA28 stub + declare global block + window.__mokoshHarness object literal — pattern for new assertion)
- tests/uat/lib/harness-page-driver.ts lines 1395-1567 (findLatestZip + driveA26 3-phase pattern; canonical for host-side JSZip parse + EventType-style structural assertions)
- tests/uat/lib/harness-page-driver.ts lines 36-46 (existing imports + JSZip + AssertionRecord/CheckRecord)
- tests/uat/harness.test.ts lines 65-130 (existing driver imports + FORBIDDEN_HOOK_STRINGS at 12 entries)
- tests/uat/harness.test.ts lines 316-431 (existing wrapped-driver consts + drivers-array push pattern)
- node_modules/@rrweb/types/dist/index.d.ts lines 186-194 (EventType enum values; confirmed in interfaces above)
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md §"EventType enum grep pattern (Plan 03-01)" and §"Page-side stub pattern" (canonical implementation)
</read_first>
<behavior>
Page-side (`tests/uat/extension-page-harness.ts`):
- Adds module-local constants `A29_SEGMENT_SETTLE_MS = 11_000`
(matches A24/A25/A27 settle window).
- Adds `async function assertA29(): Promise<AssertionResult>` that
- dispatches a programmatic DOM mutation on the probe HTML:
- sets `#probe-text.value = 'probe'` and dispatches `Event('input', { bubbles: true })`
- clicks `#probe-modal-trigger` (toggles `#probe-modal` style.display)
- waits 500 ms for rrweb to enqueue the IncrementalSnapshot
- calls `setupFreshRecording()` then waits `A29_SEGMENT_SETTLE_MS`
- dispatches `SAVE_ARCHIVE` via `sendMessageWithTimeout` (15s timeout, label 'SAVE_ARCHIVE (A29)')
- pushes `A29.1: SAVE_ARCHIVE ack success===true` to checks
- returns the `AssertionResult` (page-side computes ONLY the SAVE
ack; the EventType-enum-shape checks live host-side in driveA29
because @rrweb/types isn't bundled to the page).
- Adds `assertA29: () => Promise<AssertionResult>;` to the
`declare global { interface Window { __mokoshHarness: { ... } } }`
block (preserve existing entries; append after assertA28).
- Adds `assertA29,` to the `window.__mokoshHarness = { ... }`
object literal (preserve existing entries; append after assertA28).
- Update the closing console.log message line (currently at
line 3411) to mention `+ Plan 03-01: A29`.
Host-side (`tests/uat/lib/harness-page-driver.ts`):
- Adds `import { EventType } from '@rrweb/types';` at the top of
the imports section (after the JSZip import on line 41).
- Adds `export async function driveA29(page: Page, downloadsDir: string): Promise<AssertionRecord>` modeled on driveA26 (lines 1421-1567):
- Phase 1 — page.evaluate `harness.assertA29()``pageResult`
- Phase 2 — `findLatestZip(downloadsDir)`; if null push
`A29.0` failure and return
- Phase 3 — JSZip.loadAsync(buf); read `rrweb/session.json`
entry; if absent push failed `A29.0a: rrweb/session.json
present` check + return
- Parse the JSON as `Array<{ type: number; timestamp: number }>`.
- Push 4 checks (names verbatim):
- `A29.1: rrweb/session.json contains > 0 events`
expected `>0`, actual `events.length`, passed `events.length > 0`
- `A29.2: rrweb emitted at least one Meta event (EventType.Meta=4)`
expected `has Meta`, actual `events.some((e) => e.type === EventType.Meta)`, passed same
- `A29.3: rrweb emitted at least one FullSnapshot (EventType.FullSnapshot=2)`
expected `has FullSnapshot`, actual `events.some((e) => e.type === EventType.FullSnapshot)`, passed same
- `A29.4: rrweb emitted at least one IncrementalSnapshot (EventType.IncrementalSnapshot=3)`
expected `has IncrementalSnapshot`, actual `events.some((e) => e.type === EventType.IncrementalSnapshot)`, passed same
- Merge with `pageResult.checks` (carries A29.1 SAVE ack already);
recompute `mergedPassed = mergedChecks.every((c) => c.passed)`.
- Push diagnostic `A29 zipPath=${zipPath}`,
`A29 events.length=${events.length}`, and
`A29 event types: ${[...new Set(events.map((e) => e.type))].sort().join(',')}`.
Orchestrator (`tests/uat/harness.test.ts`):
- Adds `driveA29,` to the import block from `./lib/harness-page-driver` (after `driveA28,`).
- Adds `const driveA29Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> = (page) => driveA29(page, handles.downloadsDir);` after the existing `driveA28Wrapped` const (mirrors A26/A28 host-side-needs-downloadsDir pattern).
- Adds `{ name: 'A29', drive: driveA29Wrapped },` to the `drivers` array after the A28 entry, with a banner comment block citing Plan 03-01 + SPEC §10 #4 + the chaining strategy (A29 owns its SAVE; injects DOM mutation pre-SAVE per RESEARCH Pitfall 1).
- FORBIDDEN_HOOK_STRINGS array stays at 12 entries (no new `__MOKOSH_UAT__`-gated symbols).
</behavior>
<action>
1. Open `tests/uat/extension-page-harness.ts`. Locate the `assertA28` stub (currently around lines 3294-3316) and the `getManifestVersion` declaration block following it.
2. Insert the new `assertA29` block AFTER `assertA28` (and AFTER its end-of-file comments / before `getManifestVersion`). Use this concrete code:
```typescript
/* ─── Plan 03-01 Task 2 — A29 (rrweb DOM verification; SPEC §10 #4) ─
*
* A29 — REQ-rrweb-dom-buffer empirical: rrweb's record() (already
* wired at src/content/index.ts:285) emits Meta + FullSnapshot
* + at least one IncrementalSnapshot when the harness page
* contains the probe HTML (form + table + modal) AND the driver
* injects a DOM mutation before SAVE (RESEARCH Pitfall 1:
* static probe HTML emits Meta + FullSnapshot but not
* IncrementalSnapshot without mutation).
*
* Page side: dispatch the probe-page DOM mutations (input value +
* modal toggle), settle, setupFreshRecording, settle one segment,
* dispatch SAVE_ARCHIVE, push A29.1 ack check. The host-side driveA29
* does the EventType-enum-shape grep against rrweb/session.json from
* the assembled zip (matches A26's chained-assertion pattern; JSZip
* + @rrweb/types are host-only deps).
*
* FORBIDDEN_HOOK_STRINGS impact: NONE. A29 rides production rrweb
* wiring (record() at src/content/index.ts:285 + GET_RRWEB_EVENTS
* round-trip at src/background/index.ts → src/content/index.ts:318)
* + the existing setupFreshRecording / sendMessageWithTimeout
* helpers. Tier-1 inventory stays at 12 entries.
*/
/** SAVE_ARCHIVE dispatch timeout for A29 — matches A24/A25/A27. */
const A29_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
const A29_SEGMENT_SETTLE_MS = 11_000;
/** Settle window between DOM mutation and SAVE so rrweb's
* IncrementalSnapshot lands in the in-memory buffer before
* GET_RRWEB_EVENTS fires. */
const A29_MUTATION_SETTLE_MS = 500;
/**
* A29 — rrweb DOM event recording empirical (SPEC §10 #4).
*
* Page-side dispatches the probe-page mutation (input.value + modal
* toggle), settles, runs setupFreshRecording + segment-settle, then
* dispatches SAVE_ARCHIVE. Only the SAVE ack lives in the
* page-side checks; the EventType-enum-shape grep is host-side
* because @rrweb/types is host-only.
*
* @returns AssertionResult with 1 page-side check (SAVE ack); host-side
* driveA29 appends A29.0/A29.1..A29.4 EventType checks.
*/
async function assertA29(): Promise<AssertionResult> {
const result: AssertionResult = {
passed: false,
name: 'A29 — rrweb DOM events recorded without errors (SPEC §10 #4 / REQ-rrweb-dom-buffer)',
checks: [],
diagnostics: [],
};
try {
diag(result, 'Step 1: dispatch probe-page DOM mutation (input value + modal toggle)');
const textInput = document.querySelector<HTMLInputElement>('#probe-text');
if (textInput !== null) {
textInput.value = 'probe';
textInput.dispatchEvent(new Event('input', { bubbles: true }));
}
const modalTrigger = document.querySelector<HTMLButtonElement>('#probe-modal-trigger');
if (modalTrigger !== null) {
modalTrigger.click();
}
diag(result, `Step 1 OK — mutations dispatched (#probe-text=${textInput !== null}, #probe-modal-trigger=${modalTrigger !== null})`);
diag(result, `Step 2: settle ${A29_MUTATION_SETTLE_MS}ms for rrweb IncrementalSnapshot to enqueue`);
await new Promise((r) => setTimeout(r, A29_MUTATION_SETTLE_MS));
diag(result, 'Step 3: setupFreshRecording');
const setupResp = await setupFreshRecording();
if (!setupResp.ok) {
throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
}
diag(result, 'Step 3 OK — REC state established');
diag(result, `Step 4: settle ${A29_SEGMENT_SETTLE_MS}ms for first segment rotation`);
await new Promise((r) => setTimeout(r, A29_SEGMENT_SETTLE_MS));
diag(result, 'Step 5: dispatch SAVE_ARCHIVE');
const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
{ type: 'SAVE_ARCHIVE' },
A29_SAVE_ARCHIVE_TIMEOUT_MS,
'SAVE_ARCHIVE (A29)',
);
diag(result, `Step 5 result: ${JSON.stringify(ack)}`);
result.checks.push({
name: 'A29.1: SAVE_ARCHIVE ack received with success=true',
expected: true,
actual: ack.success,
passed: ack.success === true,
});
result.passed = result.checks.every((c) => c.passed);
} catch (err) {
result.error = err instanceof Error ? err.message : String(err);
diag(result, `THREW: ${result.error}`);
}
return result;
}
```
3. In the SAME file, locate the `declare global { interface Window { __mokoshHarness: { ... } } }` block (around lines 3332-3372). Inside the type literal, AFTER the existing `assertA28: () => Promise<AssertionResult>;` entry and BEFORE `getManifestVersion`, insert:
```typescript
// Plan 03-01 — rrweb DOM verification (SPEC §10 #4)
assertA29: () => Promise<AssertionResult>;
```
4. Locate the `window.__mokoshHarness = { ... }` object literal (around lines 3374-3404). After `assertA28,` and before `getManifestVersion,` insert:
```typescript
assertA29,
```
5. Locate the trailing `console.log('[harness-page] ready — ...')` line (around line 3411) and replace the message to append `+ Plan 03-01: A29`. Concrete new line:
```typescript
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + getManifestVersion)');
```
6. Open `tests/uat/lib/harness-page-driver.ts`. In the import block (after the `import JSZip from 'jszip';` line — around line 41), add:
```typescript
import { EventType } from '@rrweb/types';
```
7. At the end of the file (after the existing `driveA28` export), append the new `driveA29` export:
```typescript
/* ─── Plan 03-01 — driveA29 (rrweb DOM verification host-side) ─────── */
/**
* Drive A29 (Plan 03-01 — SPEC §10 #4 / REQ-rrweb-dom-buffer).
*
* Three-phase orchestration:
* 1. Page-side assertA29 dispatches the probe DOM mutations, settles,
* runs setupFreshRecording, settles a segment, dispatches SAVE.
* Returns AssertionRecord with A29.1 (SAVE ack) only.
* 2. Host-side findLatestZip picks the just-produced zip (mtime-sort
* wins; race-free per Plan 02-04 precedent — assertA29 awaits the
* SAVE ack before returning so the zip has landed by here).
* 3. Host-side JSZip-parse rrweb/session.json + EventType-enum grep.
*
* Checks appended host-side:
* - A29.0a: rrweb/session.json entry exists in zip
* - A29.1 (already from page side): SAVE_ARCHIVE ack success
* - A29.2: events.length > 0
* - A29.3: events.some(e => e.type === EventType.Meta)
* - A29.4: events.some(e => e.type === EventType.FullSnapshot)
* - A29.5: events.some(e => e.type === EventType.IncrementalSnapshot)
*
* RESEARCH Pitfall 1 mitigation: the page-side dispatches a real DOM
* mutation (input value + modal toggle) BEFORE SAVE so the
* IncrementalSnapshot check (A29.5) has actual content to find.
*
* @param page - The harness page from `launchHarnessBrowser`.
* @param downloadsDir - Absolute path to the per-run downloads directory.
* @returns AssertionRecord with 6 merged checks (A29.0a + A29.1..A29.5).
*/
export async function driveA29(
page: Page,
downloadsDir: string,
): Promise<AssertionRecord> {
// Phase 1 — page-side orchestration + SAVE.
const pageResult = await page.evaluate(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose.
const harness = (window as any).__mokoshHarness;
const r: AssertionRecord = await harness.assertA29();
return r;
}) as AssertionRecord;
const mergedChecks: CheckRecord[] = pageResult.checks.slice();
const mergedDiagnostics: string[] = pageResult.diagnostics.slice();
// Phase 2 — locate the produced zip.
const zipPath = findLatestZip(downloadsDir);
if (zipPath === null) {
mergedChecks.push({
name: 'A29.0: at least one zip present in downloadsDir',
expected: '>=1 zip',
actual: 'no zip in downloadsDir',
passed: false,
});
return {
passed: false,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
mergedDiagnostics.push(`A29 zipPath=${zipPath}`);
// Phase 3 — load + inspect rrweb/session.json.
const zipBytes = readFileSync(zipPath);
const zip = await JSZip.loadAsync(zipBytes);
const rrwebFile = zip.file('rrweb/session.json');
mergedChecks.push({
name: 'A29.0a: rrweb/session.json entry exists in zip',
expected: true,
actual: rrwebFile !== null,
passed: rrwebFile !== null,
});
if (rrwebFile === null) {
return {
passed: false,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
const rrwebText = await rrwebFile.async('string');
let events: Array<{ type: number; timestamp: number }> = [];
let parseErr: string | null = null;
try {
events = JSON.parse(rrwebText) as Array<{ type: number; timestamp: number }>;
} catch (err) {
parseErr = err instanceof Error ? err.message : String(err);
}
if (parseErr !== null) {
mergedChecks.push({
name: 'A29.0b: rrweb/session.json parses as JSON',
expected: 'JSON.parse success',
actual: `<error: ${parseErr}>`,
passed: false,
});
return {
passed: false,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
const distinctTypes = [...new Set(events.map((e) => e.type))].sort((a, b) => a - b);
mergedDiagnostics.push(`A29 events.length=${events.length}`);
mergedDiagnostics.push(`A29 event types: ${distinctTypes.join(',')}`);
mergedChecks.push({
name: 'A29.2: rrweb/session.json contains > 0 events',
expected: '>0',
actual: events.length,
passed: events.length > 0,
});
mergedChecks.push({
name: `A29.3: rrweb emitted at least one Meta event (EventType.Meta=${EventType.Meta})`,
expected: 'has Meta',
actual: events.some((e) => e.type === EventType.Meta),
passed: events.some((e) => e.type === EventType.Meta),
});
mergedChecks.push({
name: `A29.4: rrweb emitted at least one FullSnapshot (EventType.FullSnapshot=${EventType.FullSnapshot})`,
expected: 'has FullSnapshot',
actual: events.some((e) => e.type === EventType.FullSnapshot),
passed: events.some((e) => e.type === EventType.FullSnapshot),
});
mergedChecks.push({
name: `A29.5: rrweb emitted at least one IncrementalSnapshot (EventType.IncrementalSnapshot=${EventType.IncrementalSnapshot})`,
expected: 'has IncrementalSnapshot',
actual: events.some((e) => e.type === EventType.IncrementalSnapshot),
passed: events.some((e) => e.type === EventType.IncrementalSnapshot),
});
const mergedPassed = mergedChecks.every((c) => c.passed);
return {
passed: mergedPassed,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
```
8. Open `tests/uat/harness.test.ts`. In the import block from `./lib/harness-page-driver` (around lines 65-101), AFTER `driveA28,` and BEFORE `getManifestVersion,`, add:
```typescript
// Plan 03-01 — rrweb DOM verification (SPEC §10 #4 / REQ-rrweb-dom-buffer)
driveA29,
```
9. In the same file, locate the existing `driveA28Wrapped` const (around lines 330-335). AFTER it, add:
```typescript
// Plan 03-01 — driveA29 needs downloadsDir for host-side JSZip parse
// of rrweb/session.json from the just-produced zip.
const driveA29Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
(page) => driveA29(page, handles.downloadsDir);
```
10. In the `drivers` array (around lines 337-431), AFTER the existing `{ name: 'A28', drive: driveA28Wrapped },` entry, add:
```typescript
// Plan 03-01 A29: rrweb DOM verification (SPEC §10 #4).
// A29 owns its SAVE because the probe-page DOM mutation must
// happen between page load and SAVE so rrweb's IncrementalSnapshot
// fires (RESEARCH Pitfall 1). Host-side driveA29 JSZip-parses
// rrweb/session.json and asserts the EventType enum surfaces
// (Meta=4, FullSnapshot=2, IncrementalSnapshot=3) are present.
{ name: 'A29', drive: driveA29Wrapped },
```
11. Update the orchestrator banner string (currently at line 268: `'Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28)'`) to append `, A29`. Concrete replacement string for line 268:
```typescript
process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29)\n');
```
12. Run `npx tsc --noEmit` to confirm no type errors. Expected: clean.
13. Run `SKIP_PROD_REBUILD=0 HEADLESS=1 npm run test:uat` (full rebuild + harness). Expected: `30/30 GREEN` (29 prior + A29).
</action>
<verify>
<automated>npx tsc --noEmit &amp;&amp; grep -c "assertA29" tests/uat/extension-page-harness.ts | grep -v '^#' | awk '$1 &gt;= 3' &amp;&amp; grep -c "driveA29" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | awk '$1 &gt;= 2' &amp;&amp; grep -c "driveA29" tests/uat/harness.test.ts | grep -v '^#' | awk '$1 &gt;= 3' &amp;&amp; grep -c "from '@rrweb/types'" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | grep -q '^1$' &amp;&amp; HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat</automated>
</verify>
<acceptance_criteria>
- `npx tsc --noEmit` exits 0.
- `grep -c 'assertA29' tests/uat/extension-page-harness.ts` returns >=3 (function definition + declare global entry + object literal entry).
- `grep -c 'driveA29' tests/uat/lib/harness-page-driver.ts` returns >=2 (function definition + docstring or signature mention).
- `grep -c 'driveA29' tests/uat/harness.test.ts` returns >=3 (import line + wrapped const + drivers array push).
- `grep -c "from '@rrweb/types'" tests/uat/lib/harness-page-driver.ts` returns exactly 1.
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with stdout containing `UAT harness: 30/30 assertions passed`.
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (Tier-1 inventory stays at 12 entries; verifies no new hook leaks).
</acceptance_criteria>
<done>
UAT harness runs 30/30 GREEN with A29 covering SPEC §10 #4 end-to-end:
rrweb session.json from the assembled zip contains > 0 events with
Meta + FullSnapshot + IncrementalSnapshot EventType-enum members
all present. FORBIDDEN_HOOK_STRINGS unchanged at 12.
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Puppeteer host ↔ page realm | Test harness drives the page via page.evaluate; no production data flows out |
| Page realm ↔ content script | Existing GET_RRWEB_EVENTS round-trip via chrome.runtime.sendMessage |
| Content script ↔ SW | Existing chrome.tabs.sendMessage path (read-only for Plan 03-01) |
| dist-test/ ↔ dist/ | Two-bundle separation: Plan 03-01 adds NO test-only symbols; production bundle invariant unchanged |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-01-01 | Information Disclosure | Probe HTML password input visible in DOM | accept | Sentinel value is not introduced by Plan 03-01 (Plan 03-03 types the negative-assertion sentinel). Plan 03-01 leaves the password input empty; rrweb's existing maskInputOptions.password=true at src/content/index.ts:306 masks any value that lands. |
| T-03-01-02 | Tampering | New probe HTML elements interfere with A18/A21 token-resolution checks | mitigate | UI-SPEC ban enforced: probe HTML appends BELOW existing scaffold; head + tokens.css link untouched; probe sub-tree uses no var(--mks-*) tokens; verification grep at task acceptance proves the existing link line is preserved. |
| T-03-01-03 | Information Disclosure | Test-only hook surface leaking to production bundle | mitigate | A29 rides production rrweb wiring + existing GET_RRWEB_EVENTS bridge + sendMessageWithTimeout helper. FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries; Tier-1 unit-test gate runs as part of Task 2 acceptance. |
| T-03-01-04 | Denial of Service | rrweb cleanupOldEvents fires mid-A29 + drops the IncrementalSnapshot before SAVE | accept | A29 settles 500ms after mutation + 11s segment-settle + SAVE. CLEANUP_INTERVAL_MS=60s + retention=10min at src/content/index.ts:16-18 — A29's wall-clock <12s budget is far below the cleanup window; mutation event safely retained. |
No new production surface; threat surface unchanged from Phase 2
(CON-manifest-permissions intact including DEC-011 Amendment 1 tabs
permission). UAT harness extension is test-only and tree-shakes from
production bundle via the existing `__MOKOSH_UAT__` Vite-define-token —
A29 introduces no new `__MOKOSH_UAT__`-gated symbols.
</threat_model>
<verification>
- `npx tsc --noEmit` exits 0.
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 30/30 GREEN.
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (12 FORBIDDEN_HOOK_STRINGS × 0 hits each).
- `grep -c '<textarea' tests/uat/extension-page-harness.html` returns 0.
- `grep -c '<link rel="stylesheet" href="../../src/shared/tokens.css">' tests/uat/extension-page-harness.html` returns 1.
</verification>
<success_criteria>
- A29 GREEN in UAT harness across 6 host-side checks (A29.0a + A29.1..A29.5).
- Probe HTML committed to harness page in correct location; head + script tag preserved.
- FORBIDDEN_HOOK_STRINGS unchanged at 12 entries.
- vitest baseline preserved (171/171 GREEN; SKIP_BUILD=1 path).
- REQ-rrweb-dom-buffer empirically verified end-to-end through rrweb's
`record()` wiring + GET_RRWEB_EVENTS bridge + the assembled zip's
rrweb/session.json content.
</success_criteria>
<output>
After completion, create
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-SUMMARY.md`
documenting:
- 4-check A29 contract verified (Meta + FullSnapshot + IncrementalSnapshot + count > 0)
- Probe HTML composition rationale (form + table + modal; NO textarea)
- DOM-mutation injection pattern (input value + modal click)
- RESEARCH Pitfall 1 mitigation confirmed empirically
- UAT 29 → 30 GREEN; Tier-1 inventory unchanged at 12
- Plan 03-02 wave dependency: SAME 3 harness files; SEQUENTIAL within Wave 2 per RESEARCH Pitfall 6
</output>

View File

@@ -0,0 +1,680 @@
---
phase: 03
slug: spec-10-smoke-verification-dom-event-log-verification
plan: 02
type: execute
wave: 2
depends_on:
- 01
files_modified:
- tests/uat/extension-page-harness.ts
- tests/uat/lib/harness-page-driver.ts
- tests/uat/harness.test.ts
autonomous: true
requirements:
- REQ-user-event-log
tags:
- uat-harness
- a30
- event-log
- spec-10-5
- approach-b
- user-event
user_setup: []
must_haves:
truths:
- "logs/events.json from the assembled zip contains at least one 'click' UserEvent"
- "logs/events.json contains at least one 'input' UserEvent"
- "logs/events.json contains at least one 'navigation' UserEvent"
- "logs/events.json contains at least one 'js_error' UserEvent"
- "logs/events.json contains at least one 'network_error' UserEvent"
- "UAT harness exits 0 with 30 + 1 = 31/31 assertions GREEN (A29 baseline preserved + new A30)"
artifacts:
- path: "tests/uat/extension-page-harness.ts"
provides: "assertA30 page-side orchestrator: triggers 5 event types (click/input/navigation/js_error/network_error), runs setupFreshRecording + SAVE; registered on window.__mokoshHarness"
contains: "assertA30"
- path: "tests/uat/lib/harness-page-driver.ts"
provides: "driveA30 host-side: JSZip-parse logs/events.json + UserEvent type grep across all 5 event-types"
contains: "driveA30"
- path: "tests/uat/harness.test.ts"
provides: "driveA30 import + wrapped driver + drivers-array push entry"
contains: "driveA30"
key_links:
- from: "tests/uat/lib/harness-page-driver.ts driveA30"
to: "tests/uat/extension-page-harness.ts assertA30"
via: "page.evaluate(() => window.__mokoshHarness.assertA30())"
pattern: "harness.assertA30\\(\\)"
- from: "tests/uat/lib/harness-page-driver.ts driveA30"
to: "src/shared/types.ts UserEvent"
via: "import type { UserEvent } from '../../../src/shared/types'"
pattern: "import type \\{ UserEvent \\}"
- from: "tests/uat/extension-page-harness.ts assertA30"
to: "src/content/index.ts (production event-log wiring)"
via: "synthetic browser events trigger production click/input/navigation/error/fetch listeners"
pattern: "addUserEvent\\("
---
<objective>
Extend the UAT harness with A30 — empirical verification that SPEC §10 #5
(event log captures clicks, navigation, network errors, plus the input
and js_error types per CON-event-log-schema) is satisfied. Production
wiring at `src/content/index.ts:60-237` already ships listeners for all
5 UserEvent types; A30 confirms they fire correctly and land in
`logs/events.json` of the assembled archive.
Purpose: Closes REQ-user-event-log empirical verification gap. Phase 1
shipped the wiring; Phase 3 confirms all 5 types are captured during a
synthetic event-injection drive.
Output: A30 assertion with 6 host-side checks (SAVE ack + presence of
each of 5 event types in logs/events.json); UAT count 30 → 31 GREEN.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-PLAN.md
@src/content/index.ts
@src/shared/types.ts
<interfaces>
<!-- Key contracts the executor needs. Extracted from existing code. -->
From src/shared/types.ts (lines 124-131):
```typescript
export interface UserEvent {
timestamp: number;
type: 'click' | 'input' | 'navigation' | 'js_error' | 'network_error';
target: string;
value?: string;
url: string;
meta?: Record<string, unknown>;
}
```
From src/content/index.ts (READ-ONLY; verifier subject):
```typescript
// setupClickLogging at line 61: document.addEventListener('click', ...)
// setupInputLogging at line 77: document.addEventListener('input', ...); password filter at line 82
// setupNavigationLogging at line 99: popstate + hashchange + pushState/replaceState intercept
// setupErrorLogging at line 133: window.addEventListener('error', ...) + unhandledrejection
// setupNetworkLogging at line 164: fetch interception (response.ok === false) + XHR loadend (status >= 400)
// All push to userEvents[]; GET_RRWEB_EVENTS handler at line 318 returns events + userEvents
```
From tests/uat/extension-page-harness.ts (existing helpers):
```typescript
function diag(result: AssertionResult, line: string): void;
async function setupFreshRecording(): Promise<{ ok: boolean; error?: string }>;
async function sendMessageWithTimeout<T>(msg: unknown, timeoutMs: number, label: string): Promise<T>;
// Pattern for new assertions: A29 from Plan 03-01 (precedent — same file).
```
From tests/uat/lib/harness-page-driver.ts:
```typescript
function findLatestZip(downloadsDir: string): string | null;
// JSZip + readFileSync host-side; no chrome.* access
```
</interfaces>
# Plan Anchors
- **Sequential wave assignment (per RESEARCH Pitfall 6 + file-overlap rule):** Plan 03-02 lives in wave 2
modifies the SAME 3 harness files as Plan 03-01. depends_on: [01]
enforces sequential ordering — Plan 03-02 runs AFTER 03-01 commits.
- **Production wiring is in the content script — NOT the harness page.**
src/content/index.ts attaches listeners to `document` and `window`
via content-script injection into the active tab. The harness page
(chrome-extension://.../extension-page-harness.html) is an extension-
internal page where the content script is also injected (per
manifest content_scripts `<all_urls>`-equivalent). Therefore
synthetic events dispatched ON the harness page from
`page.evaluate(...)` reach the production listeners and produce real
UserEvent entries.
- **5 event types must each fire at least once:** click + input +
navigation + js_error + network_error (per CON-event-log-schema +
REQUIREMENTS.md REQ-user-event-log lines 65-72).
- **Network error trigger via fetch to a known-404:** Plan 02-04 A27
uses `https://example.com` as a stable harness fixture. fetch to
`https://example.com/this-path-does-not-exist-404-probe-a30` is a
reliable network_error trigger (404 surfaces). RESEARCH Pitfall 3
warning: USE https:// only (URL_SCHEME_ALLOW regex). example.com
is the safe choice.
- **Navigation trigger via pushState:** the production interceptor at
src/content/index.ts:114-129 wraps history.pushState/replaceState
and dispatches navigation events. A30 fires
`history.pushState({}, '', window.location.pathname + '#a30-probe')`
which routes through the wrapper → navigation event.
- **js_error trigger via window.dispatchEvent(new ErrorEvent('error'))**
is the canonical synthetic trigger; production at line 133 listens
for `window` 'error' events.
- **input trigger:** keep separate from Plan 03-01 mutation (Plan
03-01's `#probe-text` value="probe" already fires the input event
via dispatchEvent at line 80, but A30 runs in its own fresh
recording cycle after A29 + setupFreshRecording, so events are
separated by recording window). A30 dispatches a NEW input event
on `#probe-email` with value 'a30@probe.local' to be self-contained.
- **click trigger:** dispatch a synthetic click on `#probe-submit` via
`.click()`. The production click listener at line 61 captures it
regardless of whether the form submits (form submit event is a
separate listener not in scope).
- **All triggers happen ON the harness page (window/document of the
harness page).** No new tab is opened; chrome.tabs.create is NOT
needed. This avoids the Plan 02-04 A27 multi-tab complexity and
the chrome-extension://-tab quirks.
- **FORBIDDEN_HOOK_STRINGS lockstep:** A30 rides production listeners
+ existing setupFreshRecording / sendMessageWithTimeout helpers.
Tier-1 inventory stays at 12 entries.
</context>
<tasks>
<task type="auto" tdd="false">
<name>Task 1: Add assertA30 page-side orchestrator (5 event triggers + SAVE)</name>
<files>tests/uat/extension-page-harness.ts</files>
<read_first>
- tests/uat/extension-page-harness.ts lines 3151-3413 (assertA28 stub + declare global block + window.__mokoshHarness object literal — the canonical extension shape Plan 03-01 just used for assertA29)
- tests/uat/extension-page-harness.ts where assertA29 was JUST added by Plan 03-01 (study its shape — same module; new assertion appends after it)
- src/content/index.ts lines 60-237 (production listener wiring — what synthetic events must trigger)
- src/shared/types.ts lines 124-131 (UserEvent type with 5-literal union)
</read_first>
<behavior>
- Adds module-local constants:
- `A30_SAVE_ARCHIVE_TIMEOUT_MS = 15_000`
- `A30_SEGMENT_SETTLE_MS = 11_000`
- `A30_TRIGGER_SETTLE_MS = 500` (wait between trigger and SAVE)
- `A30_404_PROBE_URL = 'https://example.com/this-path-does-not-exist-404-probe-a30'`
- Adds `async function assertA30(): Promise<AssertionResult>` that:
- Step 0: `setupFreshRecording()` (clean event-log window; previous events from A29 won't count)
- Step 1: settle `A30_SEGMENT_SETTLE_MS` so a segment lands (matches other assertions; not strictly required for event log but keeps the run consistent)
- Step 2: click trigger — `(document.querySelector('#probe-submit') as HTMLButtonElement | null)?.click()` (use safe-nav; assertion proceeds even if the element is gone)
- Step 3: input trigger — set `#probe-email.value = 'a30@probe.local'` + dispatch `Event('input', { bubbles: true })`
- Step 4: navigation trigger — `history.pushState({}, '', window.location.pathname + '#a30-probe')` (production wrapper at src/content/index.ts:114-129 fires navigation event)
- Step 5: js_error trigger — `window.dispatchEvent(new ErrorEvent('error', { message: 'a30-probe-js-error', filename: 'a30', lineno: 1, colno: 1 }))`
- Step 6: network_error trigger — `await fetch(A30_404_PROBE_URL).catch(() => undefined)` (production fetch interception at line 167 fires network_error on response.ok===false)
- Step 7: settle `A30_TRIGGER_SETTLE_MS` so all event handlers complete and entries land in userEvents[]
- Step 8: dispatch `SAVE_ARCHIVE`
- Push `A30.1: SAVE_ARCHIVE ack received with success=true` to checks
- Returns AssertionResult; host-side driveA30 appends the 5 type-presence checks
- Adds `assertA30` to `declare global { interface Window { __mokoshHarness: { ... } } }` + `window.__mokoshHarness = { ... }` object literal (preserve all existing entries including assertA29 from Plan 03-01).
- Updates the closing console.log line to append `+ Plan 03-02: A30`.
</behavior>
<action>
1. Open `tests/uat/extension-page-harness.ts`. Locate the assertA29 block added by Plan 03-01 + the `getManifestVersion` declaration following it.
2. Insert the new assertA30 block AFTER assertA29 (BEFORE getManifestVersion). Use this concrete code:
```typescript
/* ─── Plan 03-02 — A30 (event-log verification; SPEC §10 #5) ────────
*
* A30 — REQ-user-event-log empirical: the production listeners at
* src/content/index.ts (setupClickLogging at line 61,
* setupInputLogging at line 77, setupNavigationLogging at line
* 99, setupErrorLogging at line 133, setupNetworkLogging at
* line 164) all fire on synthetic browser events dispatched
* on the harness page, producing UserEvent entries with each
* of the 5 type-values (click / input / navigation /
* js_error / network_error) in logs/events.json.
*
* Trigger strategy (all on the harness page; no new tabs opened):
* - click: programmatic .click() on #probe-submit
* - input: set #probe-email.value + dispatch Event('input', bubbles:true)
* - navigation: history.pushState (intercepted at src/content/index.ts:121)
* - js_error: window.dispatchEvent(new ErrorEvent('error', ...))
* - network_error: fetch(404-probe-url).catch(noop) — production
* fetch interception at src/content/index.ts:167 logs response.ok===false
*
* Page-side dispatches all 5 triggers + settles + SAVE. Host-side
* driveA30 JSZip-parses logs/events.json and asserts each of the 5
* UserEvent.type literal values is present.
*
* FORBIDDEN_HOOK_STRINGS impact: NONE. A30 rides production listeners
* + existing helpers. Tier-1 inventory stays at 12.
*/
/** SAVE_ARCHIVE dispatch timeout for A30 — matches A24/A25/A27/A29. */
const A30_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
const A30_SEGMENT_SETTLE_MS = 11_000;
/** Settle between trigger dispatches and SAVE so event handlers complete. */
const A30_TRIGGER_SETTLE_MS = 500;
/** 404 probe URL — chrome.tabs perm grant is irrelevant; fetch happens
* from the harness page realm. example.com is RFC 2606 reserved +
* serves a 404 reliably for unknown paths under headless Chrome. */
const A30_404_PROBE_URL = 'https://example.com/this-path-does-not-exist-404-probe-a30';
/**
* A30 — Event-log empirical (SPEC §10 #5 / REQ-user-event-log).
*
* Dispatches 5 synthetic browser events that exercise each of the
* production listeners; runs setupFreshRecording so event-log
* cleanup hasn't dropped anything; settles a segment; SAVEs. Host-side
* driveA30 inspects logs/events.json from the produced zip and asserts
* each of the 5 UserEvent.type literal values appears at least once.
*
* @returns AssertionResult with 1 page-side check (SAVE ack); host-side
* driveA30 appends 5 UserEvent.type presence checks.
*/
async function assertA30(): Promise<AssertionResult> {
const result: AssertionResult = {
passed: false,
name: 'A30 — event log captures 5 UserEvent types (SPEC §10 #5 / REQ-user-event-log)',
checks: [],
diagnostics: [],
};
try {
diag(result, 'Step 1: setupFreshRecording (A30 owns its recording — clean event-log window)');
const setupResp = await setupFreshRecording();
if (!setupResp.ok) {
throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
}
diag(result, 'Step 1 OK — REC state established');
diag(result, `Step 2: settle ${A30_SEGMENT_SETTLE_MS}ms for first segment rotation`);
await new Promise((r) => setTimeout(r, A30_SEGMENT_SETTLE_MS));
diag(result, 'Step 3: click trigger — programmatic .click() on #probe-submit');
const submitBtn = document.querySelector<HTMLButtonElement>('#probe-submit');
if (submitBtn !== null) {
submitBtn.click();
} else {
diag(result, 'Step 3 WARN — #probe-submit missing; click trigger skipped');
}
diag(result, 'Step 4: input trigger — set #probe-email.value + dispatch input event');
const emailInput = document.querySelector<HTMLInputElement>('#probe-email');
if (emailInput !== null) {
emailInput.value = 'a30@probe.local';
emailInput.dispatchEvent(new Event('input', { bubbles: true }));
} else {
diag(result, 'Step 4 WARN — #probe-email missing; input trigger skipped');
}
diag(result, 'Step 5: navigation trigger — history.pushState (production wrapper at src/content/index.ts:121 intercepts)');
history.pushState({}, '', window.location.pathname + '#a30-probe');
diag(result, 'Step 6: js_error trigger — window.dispatchEvent(ErrorEvent("error"))');
window.dispatchEvent(new ErrorEvent('error', {
message: 'a30-probe-js-error',
filename: 'a30-probe.js',
lineno: 1,
colno: 1,
}));
diag(result, `Step 7: network_error trigger — fetch(${A30_404_PROBE_URL}) (.catch noop)`);
try {
await fetch(A30_404_PROBE_URL);
} catch (fetchErr) {
diag(result, `Step 7 fetch threw (acceptable for network_error path): ${String(fetchErr)}`);
}
diag(result, `Step 8: settle ${A30_TRIGGER_SETTLE_MS}ms so async handlers (fetch.then) complete`);
await new Promise((r) => setTimeout(r, A30_TRIGGER_SETTLE_MS));
diag(result, 'Step 9: dispatch SAVE_ARCHIVE');
const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
{ type: 'SAVE_ARCHIVE' },
A30_SAVE_ARCHIVE_TIMEOUT_MS,
'SAVE_ARCHIVE (A30)',
);
diag(result, `Step 9 result: ${JSON.stringify(ack)}`);
result.checks.push({
name: 'A30.1: SAVE_ARCHIVE ack received with success=true',
expected: true,
actual: ack.success,
passed: ack.success === true,
});
result.passed = result.checks.every((c) => c.passed);
} catch (err) {
result.error = err instanceof Error ? err.message : String(err);
diag(result, `THREW: ${result.error}`);
}
return result;
}
```
3. In the `declare global { interface Window { __mokoshHarness: { ... } } }` block, AFTER the assertA29 entry that Plan 03-01 added and BEFORE `getManifestVersion`, insert:
```typescript
// Plan 03-02 — event-log verification (SPEC §10 #5)
assertA30: () => Promise<AssertionResult>;
```
4. In the `window.__mokoshHarness = { ... }` object literal, AFTER `assertA29,` and BEFORE `getManifestVersion,` insert:
```typescript
assertA30,
```
5. Update the closing `console.log(...)` line to append `+ Plan 03-02: A30`. Concrete replacement string (preserves the Plan 03-01 mention):
```typescript
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + Plan 03-02: A30 + getManifestVersion)');
```
6. Run `npx tsc --noEmit` to confirm no type errors.
</action>
<verify>
<automated>npx tsc --noEmit &amp;&amp; grep -c "assertA30" tests/uat/extension-page-harness.ts | grep -v '^#' | awk '$1 &gt;= 3'</automated>
</verify>
<acceptance_criteria>
- `npx tsc --noEmit` exits 0.
- `grep -c "assertA30" tests/uat/extension-page-harness.ts` returns >=3 (function definition + declare global entry + object literal entry).
- `grep -c "A30_404_PROBE_URL" tests/uat/extension-page-harness.ts` returns >=2 (declaration + usage).
- `grep -c "ErrorEvent('error'" tests/uat/extension-page-harness.ts` returns >=1.
- `grep -c "history.pushState" tests/uat/extension-page-harness.ts` returns >=1.
- Existing Plan 03-01 assertA29 entry still present in __mokoshHarness object literal (`grep -c 'assertA29,' tests/uat/extension-page-harness.ts` returns >=1).
</acceptance_criteria>
<done>
Page-side assertA30 dispatches 5 synthetic event triggers + SAVE; registered on __mokoshHarness; ready for Task 2 host-side driveA30 grep.
</done>
</task>
<task type="auto" tdd="false">
<name>Task 2: Add driveA30 host-side (UserEvent type grep) + orchestrator wiring</name>
<files>tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
<read_first>
- tests/uat/lib/harness-page-driver.ts lines 1395-1567 (driveA26 3-phase pattern; canonical for host-side JSZip + named-entry parse)
- tests/uat/lib/harness-page-driver.ts where driveA29 was JUST added by Plan 03-01 (same file; new driver appends below)
- src/shared/types.ts lines 124-131 (UserEvent type — to import for the type-cast on parse)
- tests/uat/harness.test.ts lines 65-431 (existing import block + wrapped-driver consts + drivers array push)
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md §"UserEvent type-grep pattern (Plan 03-02)"
</read_first>
<behavior>
Host-side (`tests/uat/lib/harness-page-driver.ts`):
- Adds `import type { UserEvent } from '../../../src/shared/types';` near the top imports (after the existing JSZip + EventType imports).
- Adds `export async function driveA30(page: Page, downloadsDir: string): Promise<AssertionRecord>`:
- Phase 1 — page.evaluate harness.assertA30()
- Phase 2 — findLatestZip(downloadsDir); if null push A30.0 failure
- Phase 3 — JSZip.loadAsync; read `logs/events.json` entry; if absent push A30.0a failure
- Parse as `UserEvent[]`; on JSON parse failure push A30.0b failure
- For each EXPECTED_TYPE in ['click', 'input', 'navigation', 'js_error', 'network_error']:
- Push check `A30.${2..6}: logs/events.json contains at least one '${type}' event`
expected `>=1 ${type}`, actual `userEvents.filter((e) => e.type === type).length`, passed same > 0
- Diagnostics: `A30 zipPath=${zipPath}`, `A30 userEvents.length=${userEvents.length}`, `A30 type counts: ${...}`
- Filter-pipeline form (no `continue`).
Orchestrator (`tests/uat/harness.test.ts`):
- Adds `driveA30,` to the import block (after the `driveA29,` Plan 03-01 added).
- Adds `const driveA30Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> = (page) => driveA30(page, handles.downloadsDir);` after the existing driveA29Wrapped.
- Adds `{ name: 'A30', drive: driveA30Wrapped },` to the drivers array after the A29 entry, with banner comment citing Plan 03-02 + SPEC §10 #5.
- Update the orchestrator banner line to append `, A30`.
</behavior>
<action>
1. Open `tests/uat/lib/harness-page-driver.ts`. In the import block (near top of file), AFTER the existing JSZip import + the EventType import that Plan 03-01 added, add:
```typescript
import type { UserEvent } from '../../../src/shared/types';
```
2. At the end of the file (AFTER driveA29 that Plan 03-01 added), append the new driveA30 export:
```typescript
/* ─── Plan 03-02 — driveA30 (event-log verification host-side) ──────── */
/** Canonical 5-tuple of UserEvent.type literal values per
* CON-event-log-schema + src/shared/types.ts:126. Driver iterates this
* list to push one presence-check per type. */
const A30_EXPECTED_TYPES: ReadonlyArray<UserEvent['type']> = [
'click',
'input',
'navigation',
'js_error',
'network_error',
];
/**
* Drive A30 (Plan 03-02 — SPEC §10 #5 / REQ-user-event-log).
*
* Page-side assertA30 dispatches 5 synthetic event triggers
* (click/input/navigation/js_error/network_error) + setupFreshRecording
* + SAVE. Host-side driveA30 JSZip-parses logs/events.json from the
* produced zip and asserts each of the 5 UserEvent.type literal values
* appears at least once.
*
* Filter-pipeline form (no `continue`) per CLAUDE.md Control Flow §.
*
* Checks (6 total — 1 page-side + 5 host-side):
* - A30.1: SAVE_ARCHIVE ack success (page-side)
* - A30.2: logs/events.json contains >=1 'click' event
* - A30.3: logs/events.json contains >=1 'input' event
* - A30.4: logs/events.json contains >=1 'navigation' event
* - A30.5: logs/events.json contains >=1 'js_error' event
* - A30.6: logs/events.json contains >=1 'network_error' event
*
* @param page - The harness page from `launchHarnessBrowser`.
* @param downloadsDir - Absolute path to the per-run downloads directory.
* @returns AssertionRecord with 6 merged checks.
*/
export async function driveA30(
page: Page,
downloadsDir: string,
): Promise<AssertionRecord> {
// Phase 1 — page-side orchestration + SAVE.
const pageResult = await page.evaluate(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose.
const harness = (window as any).__mokoshHarness;
const r: AssertionRecord = await harness.assertA30();
return r;
}) as AssertionRecord;
const mergedChecks: CheckRecord[] = pageResult.checks.slice();
const mergedDiagnostics: string[] = pageResult.diagnostics.slice();
// Phase 2 — locate the produced zip.
const zipPath = findLatestZip(downloadsDir);
if (zipPath === null) {
mergedChecks.push({
name: 'A30.0: at least one zip present in downloadsDir',
expected: '>=1 zip',
actual: 'no zip in downloadsDir',
passed: false,
});
return {
passed: false,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
mergedDiagnostics.push(`A30 zipPath=${zipPath}`);
// Phase 3 — load + inspect logs/events.json.
const zipBytes = readFileSync(zipPath);
const zip = await JSZip.loadAsync(zipBytes);
const eventsFile = zip.file('logs/events.json');
mergedChecks.push({
name: 'A30.0a: logs/events.json entry exists in zip',
expected: true,
actual: eventsFile !== null,
passed: eventsFile !== null,
});
if (eventsFile === null) {
return {
passed: false,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
const eventsRaw = await eventsFile.async('string');
let userEvents: UserEvent[] = [];
let parseErr: string | null = null;
try {
userEvents = JSON.parse(eventsRaw) as UserEvent[];
} catch (err) {
parseErr = err instanceof Error ? err.message : String(err);
}
if (parseErr !== null) {
mergedChecks.push({
name: 'A30.0b: logs/events.json parses as JSON',
expected: 'JSON.parse success',
actual: `<error: ${parseErr}>`,
passed: false,
});
return {
passed: false,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
// Filter-pipeline form per CLAUDE.md Control Flow §.
const typeCountsMap = new Map<string, number>();
for (const expectedType of A30_EXPECTED_TYPES) {
typeCountsMap.set(expectedType, userEvents.filter((e) => e.type === expectedType).length);
}
mergedDiagnostics.push(`A30 userEvents.length=${userEvents.length}`);
const typeCountsRepr = [...typeCountsMap.entries()].map(([t, n]) => `${t}=${n}`).join(',');
mergedDiagnostics.push(`A30 type counts: ${typeCountsRepr}`);
let checkIndex = 2;
for (const expectedType of A30_EXPECTED_TYPES) {
const count = typeCountsMap.get(expectedType) ?? 0;
mergedChecks.push({
name: `A30.${checkIndex}: logs/events.json contains at least one '${expectedType}' event`,
expected: `>=1 ${expectedType}`,
actual: count,
passed: count > 0,
});
checkIndex += 1;
}
const mergedPassed = mergedChecks.every((c) => c.passed);
return {
passed: mergedPassed,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
```
3. Open `tests/uat/harness.test.ts`. In the import block from `./lib/harness-page-driver`, AFTER `driveA29,` and BEFORE `getManifestVersion,` add:
```typescript
// Plan 03-02 — event-log verification (SPEC §10 #5 / REQ-user-event-log)
driveA30,
```
4. AFTER the existing `driveA29Wrapped` const, add:
```typescript
// Plan 03-02 — driveA30 needs downloadsDir for host-side JSZip parse
// of logs/events.json from the just-produced zip.
const driveA30Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
(page) => driveA30(page, handles.downloadsDir);
```
5. In the drivers array, AFTER the `{ name: 'A29', ... }` entry Plan 03-01 added, add:
```typescript
// Plan 03-02 A30: event-log verification (SPEC §10 #5).
// A30 owns its SAVE because event-log cleanup runs every 60s
// (src/content/index.ts CLEANUP_INTERVAL_MS=60_000) and we need a
// fresh event-log window for the 5 synthetic triggers. Host-side
// driveA30 JSZip-parses logs/events.json and asserts presence of
// each of the 5 UserEvent.type literal values.
{ name: 'A30', drive: driveA30Wrapped },
```
6. Update the orchestrator banner line at line 268. Concrete replacement (preserves the Plan 03-01 mention):
```typescript
process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29, A30)\n');
```
7. Run `npx tsc --noEmit` to confirm no type errors.
8. Run `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat`. Expected: `31/31 GREEN`.
</action>
<verify>
<automated>npx tsc --noEmit &amp;&amp; grep -c "driveA30" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | awk '$1 &gt;= 2' &amp;&amp; grep -c "driveA30" tests/uat/harness.test.ts | grep -v '^#' | awk '$1 &gt;= 3' &amp;&amp; grep -c "from '../../../src/shared/types'" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | grep -q '^1$' &amp;&amp; HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat</automated>
</verify>
<acceptance_criteria>
- `npx tsc --noEmit` exits 0.
- `grep -c "driveA30" tests/uat/lib/harness-page-driver.ts` returns >=2.
- `grep -c "driveA30" tests/uat/harness.test.ts` returns >=3.
- `grep -c "from '../../../src/shared/types'" tests/uat/lib/harness-page-driver.ts` returns exactly 1.
- `grep -c "A30_EXPECTED_TYPES" tests/uat/lib/harness-page-driver.ts` returns >=2 (declaration + loop usage).
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with stdout containing `UAT harness: 31/31 assertions passed`.
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (Tier-1 inventory stays at 12 entries).
</acceptance_criteria>
<done>
UAT harness runs 31/31 GREEN with A30 verifying SPEC §10 #5 end-to-end:
logs/events.json from the assembled zip contains at least one entry
for each of the 5 UserEvent.type literal values. Filter-pipeline
form preserved; no `continue` statements introduced.
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Puppeteer host ↔ page realm | Test harness drives via page.evaluate; production listeners run inside the harness page (content script injects to all_urls) |
| Page realm ↔ content script | Synthetic events on document/window route to production listeners; no new bridge surfaces |
| Outbound fetch (network_error trigger) | Single fetch to https://example.com/<404-path> — RFC 2606 reserved domain; no PII; no real endpoint |
| dist-test/ ↔ dist/ | Two-bundle separation: Plan 03-02 adds NO test-only symbols; production bundle invariant unchanged |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-02-01 | Information Disclosure | network_error trigger fetches a real public URL | accept | example.com is RFC 2606 reserved (test-only); no PII / secrets in URL path; outbound request is harmless probe traffic (parity with Plan 02-04 A27 multi-tab https://example.com usage). |
| T-03-02-02 | Tampering | history.pushState changes harness page URL mid-test; subsequent assertions could see different document.location | mitigate | Hash-only push (`#a30-probe`); pushState does not navigate. Subsequent drivers (Plan 03-03 A31) run setupFreshRecording which is location-agnostic. No impact on tokens.css resolution or A18/A21 invariants. |
| T-03-02-03 | Information Disclosure | Test-only hook surface leaking to production bundle | mitigate | A30 rides production listeners + existing helpers; no `__MOKOSH_UAT__`-gated symbols. FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries; Tier-1 unit-test gate runs as part of Task 2 acceptance. |
| T-03-02-04 | Denial of Service | network_error trigger fetch hangs indefinitely (example.com slow/unreachable in CI) | mitigate | fetch is awaited inside try/catch with no explicit timeout, but the page-side assertion has A30_SAVE_ARCHIVE_TIMEOUT_MS=15s overall ceiling via sendMessageWithTimeout — a hung fetch causes the test to FAIL with a clear timeout message rather than hang. CI/dev machines typically resolve example.com sub-100ms. |
No new production surface; threat surface unchanged from Plan 03-01.
UAT harness extension is test-only.
</threat_model>
<verification>
- `npx tsc --noEmit` exits 0.
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 31/31 GREEN.
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (12 FORBIDDEN_HOOK_STRINGS × 0 hits each).
- A30 host-side diagnostic line shows non-zero count for each of the 5 UserEvent types.
</verification>
<success_criteria>
- A30 GREEN with 6 checks (SAVE ack + 5 type-presence).
- REQ-user-event-log empirically verified across all 5 UserEvent.type literal values.
- Filter-pipeline form (no `continue` statements introduced) per CLAUDE.md Control Flow §.
- FORBIDDEN_HOOK_STRINGS unchanged at 12 entries.
- vitest baseline preserved (171/171 GREEN).
</success_criteria>
<output>
After completion, create
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-SUMMARY.md`
documenting:
- 5 trigger dispatch patterns (click via .click() + input via dispatchEvent + navigation via pushState + js_error via dispatchEvent(ErrorEvent) + network_error via fetch 404)
- 6-check A30 contract empirically verified
- UAT 30 → 31 GREEN; Tier-1 inventory unchanged at 12
- Plan 03-03 wave dependency: SAME 3 harness files; SEQUENTIAL within Wave 2 per RESEARCH Pitfall 6
</output>

View File

@@ -0,0 +1,609 @@
---
phase: 03
slug: spec-10-smoke-verification-dom-event-log-verification
plan: 03
type: execute
wave: 3
depends_on:
- 01
- 02
files_modified:
- tests/uat/extension-page-harness.ts
- tests/uat/lib/harness-page-driver.ts
- tests/uat/harness.test.ts
autonomous: true
requirements: []
tags:
- uat-harness
- a31
- password-filter
- spec-10-8-partial
- approach-b
- negative-assertion
- charter-d-p3-02
user_setup: []
must_haves:
truths:
- "Typing a sentinel value into the probe-page password input does NOT cause that value to appear in logs/events.json (existing src/content/index.ts:82 filter fires)"
- "Counts of UserEvent entries whose .value field contains the sentinel string = 0"
- "Counts of UserEvent entries whose .target selector points at the password input = 0 (filter happens early-return BEFORE addUserEvent)"
- "UAT harness exits 0 with 31 + 1 = 32/32 assertions GREEN (A30 baseline preserved + new A31)"
artifacts:
- path: "tests/uat/extension-page-harness.ts"
provides: "assertA31 page-side orchestrator: types sentinel into #probe-password, runs setupFreshRecording + SAVE"
contains: "assertA31"
- path: "tests/uat/lib/harness-page-driver.ts"
provides: "driveA31 host-side: JSZip-parse logs/events.json + negative-assertion sentinel grep"
contains: "driveA31"
- path: "tests/uat/harness.test.ts"
provides: "driveA31 import + wrapped driver + drivers-array push entry"
contains: "driveA31"
key_links:
- from: "tests/uat/lib/harness-page-driver.ts driveA31"
to: "tests/uat/extension-page-harness.ts assertA31"
via: "page.evaluate(() => window.__mokoshHarness.assertA31())"
pattern: "harness.assertA31\\(\\)"
- from: "tests/uat/extension-page-harness.ts assertA31"
to: "src/content/index.ts:82 password filter (READ-ONLY VERIFICATION SUBJECT)"
via: "synthetic password-input event triggers setupInputLogging filter early-return BEFORE addUserEvent"
pattern: "if \\(target.type === 'password'\\)"
---
<objective>
Extend the UAT harness with A31 — empirical PARTIAL verification of SPEC
§10 #8. Per D-P3-02 locked decision: REQ-password-confidentiality is
Out of Scope v1; full rrweb v2 maskInputFn + data-sensitive guards are
deferred to Phase 4. Plan 03-03 confirms the EXISTING minimum filter at
`src/content/index.ts:82` (`if (target.type === 'password') return;`)
fires — sentinel string typed into the probe-page `<input type="password">`
MUST NOT appear in `logs/events.json`. This is a negative-assertion gate.
Purpose: Honors the charter literally — verify the existing
defense-in-depth without expanding scope; the PARTIAL mark in Plan
03-05 VERIFICATION.md will cite this A31 GREEN + the charter rationale.
Output: A31 assertion with 3 host-side checks (SAVE ack + sentinel
absent from events.json .value fields + zero events targeting the
password selector); UAT count 31 → 32 GREEN.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-PLAN.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-PLAN.md
@src/content/index.ts
@src/shared/types.ts
<interfaces>
<!-- Key contracts the executor needs. Extracted from existing code. -->
From src/content/index.ts (lines 77-96; READ-ONLY — verification subject):
// setupInputLogging
function setupInputLogging() {
document.addEventListener('input', (event) => {
const target = event.target as HTMLInputElement;
// Пропускаем пароли (line 81 comment; "skip passwords")
if (target.type === 'password') { // LINE 82 — the filter
return; // LINE 83 — early-return BEFORE addUserEvent
}
const selector = getSelector(target);
addUserEvent({ // LINE 88 — never reached for password inputs
timestamp: Date.now(),
type: 'input',
target: selector,
value: target.value.substring(0, 200),
url: window.location.href
});
}, true);
}
From src/shared/types.ts (lines 124-131):
export interface UserEvent {
timestamp: number;
type: 'click' | 'input' | 'navigation' | 'js_error' | 'network_error';
target: string; // CSS selector
value?: string; // What the user typed (up to 200 chars; omitted for passwords)
url: string;
meta?: Record<string, unknown>;
}
From Plan 03-01: probe HTML in extension-page-harness.html provides
`<input type="password" id="probe-password" data-test="probe-password" />`.
The id-based selector means getSelector(target) returns `#probe-password`
for any logged event on this element (per src/content/index.ts:241-243).
</interfaces>
# Plan Anchors
- **Sequential wave assignment (per RESEARCH Pitfall 6 + file-overlap rule):** Plan 03-03 lives in wave 3
modifies the SAME 3 harness files as Plans 03-01 + 03-02. depends_on:
[01, 02] enforces sequential ordering.
- **Negative-assertion pattern (per RESEARCH Pattern 3 + Plan 02-04
A27.7/A27.8 absence-check precedent):** The contract is
`userEvents.filter(e => e.value contains SENTINEL).length === 0`.
GREEN A31 = filter fired; RED A31 = filter regressed.
- **Sentinel is a fixed test constant, not a real secret:** Per RESEARCH
Security Domain table, `secret-do-not-log-123` is a probe sentinel;
logging it would itself trigger an explicit RED. No PII.
- **Targeting via id `#probe-password`:** the production getSelector at
src/content/index.ts:240 returns `#${element.id}` when an id is
present, so any event on the password input would have
`target === '#probe-password'`. A31.3 asserts zero such entries.
- **Page-side approach with native dispatch:** set `.value` directly +
dispatch `Event('input', { bubbles: true })` → fires production
listener at line 78 → filter at line 82 early-returns.
- **Probe HTML reuse:** Plan 03-01 added the password input; Plan 03-03
reuses it; no new HTML.
- **PARTIAL mark scope (D-P3-02 — NOT in scope):** rrweb v2 maskInputFn,
data-sensitive guards, full §10 #8 closure deferred to Phase 4.
- **FORBIDDEN_HOOK_STRINGS lockstep:** A31 rides production listeners +
existing helpers. Tier-1 inventory stays at 12 entries.
</context>
<tasks>
<task type="auto" tdd="false">
<name>Task 1: Add assertA31 page-side orchestrator (sentinel-into-password trigger + SAVE)</name>
<files>tests/uat/extension-page-harness.ts</files>
<read_first>
- tests/uat/extension-page-harness.ts where Plan 03-02 added assertA30 (study its shape; new assertA31 appends after it)
- tests/uat/extension-page-harness.html (verify Plan 03-01 added the password input with id=probe-password)
- src/content/index.ts lines 77-96 (the exact filter being verified)
</read_first>
<behavior>
Adds module-local constants for A31 and the page-side assertA31
function that:
- Step 1: setupFreshRecording (clean event-log window)
- Step 2: settle one segment
- Step 3: set #probe-password.value to SENTINEL + dispatch input event (fires production listener; filter early-returns)
- Step 4: settle synchronous handler completion
- Step 5: dispatch SAVE_ARCHIVE
- Push A31.1 (SAVE ack)
- Registers assertA31 on declare global + __mokoshHarness object literal
- Updates trailing console.log to mention Plan 03-03: A31
</behavior>
<action>
1. Open `tests/uat/extension-page-harness.ts`. Locate the assertA30 block added by Plan 03-02 and the `getManifestVersion` declaration following it.
2. Insert the new assertA31 block AFTER assertA30 (BEFORE getManifestVersion). Use this concrete code:
```typescript
/* ─── Plan 03-03 — A31 (password-filter PARTIAL; SPEC §10 #8) ────────
*
* A31 — D-P3-02 PARTIAL: verify the existing minimum filter at
* src/content/index.ts:82 (`if (target.type === 'password') return;`)
* fires when the operator types into a password input.
* Negative-assertion contract: SENTINEL value MUST be absent
* from logs/events.json.
*
* Charter alignment (per CONTEXT.md D-P3-02 + REQUIREMENTS.md line 271):
* - REQ-password-confidentiality moved Out of Scope v1 per 2026-05-20
* charter "we don't care about privacy hardening. At least here."
* - Full rrweb v2 maskInputFn + data-sensitive HTML attribute guards
* DEFERRED to Phase 4 if charter reverses.
* - A31 verifies the EXISTING minimum (the line-82 filter) — does
* NOT expand scope.
*
* FORBIDDEN_HOOK_STRINGS impact: NONE. Tier-1 inventory stays at 12.
*/
/** SAVE_ARCHIVE dispatch timeout for A31 — matches A24/A25/A27/A29/A30. */
const A31_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
const A31_SEGMENT_SETTLE_MS = 11_000;
/** Settle after sentinel-typing trigger so the synchronous handler completes. */
const A31_TRIGGER_SETTLE_MS = 500;
/** Fixed test sentinel — distinctive string the negative-assertion
* searches for in events.json. Per RESEARCH §"Security Domain":
* this is a probe sentinel, NOT a real secret; logging it would
* itself trigger an explicit RED. */
const A31_PASSWORD_SENTINEL = 'secret-do-not-log-123';
/** Production CSS selector returned by getSelector() at
* src/content/index.ts:241 for the password input (which has id).
* Drives A31.3 (target-absence check). */
const A31_PASSWORD_SELECTOR = '#probe-password';
/**
* A31 — Password-filter PARTIAL empirical (SPEC §10 #8 PARTIAL per D-P3-02).
*
* Types the sentinel string into the probe-page password input then
* SAVEs. Host-side driveA31 inspects logs/events.json and asserts
* absence of (a) the sentinel value and (b) any entries targeting
* the password selector.
*
* @returns AssertionResult with 1 page-side check (SAVE ack); host-side
* driveA31 appends 2 negative-assertion checks.
*/
async function assertA31(): Promise<AssertionResult> {
const result: AssertionResult = {
passed: false,
name: 'A31 — password filter fires (SPEC §10 #8 PARTIAL per D-P3-02)',
checks: [],
diagnostics: [],
};
try {
diag(result, 'Step 1: setupFreshRecording (A31 owns its recording — clean event-log window)');
const setupResp = await setupFreshRecording();
if (!setupResp.ok) {
throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
}
diag(result, 'Step 1 OK — REC state established');
diag(result, `Step 2: settle ${A31_SEGMENT_SETTLE_MS}ms for first segment rotation`);
await new Promise((r) => setTimeout(r, A31_SEGMENT_SETTLE_MS));
diag(result, `Step 3: type SENTINEL into ${A31_PASSWORD_SELECTOR} + dispatch input event`);
const pwInput = document.querySelector<HTMLInputElement>(A31_PASSWORD_SELECTOR);
if (pwInput === null) {
throw new Error(`A31 trigger failed: ${A31_PASSWORD_SELECTOR} not found in DOM (probe HTML regression from Plan 03-01)`);
}
pwInput.value = A31_PASSWORD_SENTINEL;
pwInput.dispatchEvent(new Event('input', { bubbles: true }));
diag(result, 'Step 3 OK — sentinel typed; production filter at src/content/index.ts:82 should have early-returned');
diag(result, `Step 4: settle ${A31_TRIGGER_SETTLE_MS}ms so synchronous handlers complete`);
await new Promise((r) => setTimeout(r, A31_TRIGGER_SETTLE_MS));
diag(result, 'Step 5: dispatch SAVE_ARCHIVE');
const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
{ type: 'SAVE_ARCHIVE' },
A31_SAVE_ARCHIVE_TIMEOUT_MS,
'SAVE_ARCHIVE (A31)',
);
diag(result, `Step 5 result: ${JSON.stringify(ack)}`);
result.checks.push({
name: 'A31.1: SAVE_ARCHIVE ack received with success=true',
expected: true,
actual: ack.success,
passed: ack.success === true,
});
result.passed = result.checks.every((c) => c.passed);
} catch (err) {
result.error = err instanceof Error ? err.message : String(err);
diag(result, `THREW: ${result.error}`);
}
return result;
}
```
3. In the `declare global { interface Window { __mokoshHarness: { ... } } }` block, AFTER the assertA30 entry from Plan 03-02 and BEFORE `getManifestVersion`, insert:
```typescript
// Plan 03-03 — password-filter PARTIAL (SPEC §10 #8 PARTIAL per D-P3-02)
assertA31: () => Promise<AssertionResult>;
```
4. In the `window.__mokoshHarness = { ... }` object literal, AFTER `assertA30,` and BEFORE `getManifestVersion,` insert:
```typescript
assertA31,
```
5. Update the closing `console.log(...)` line to append `+ Plan 03-03: A31`. Concrete replacement:
```typescript
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + Plan 03-02: A30 + Plan 03-03: A31 + getManifestVersion)');
```
6. Run `npx tsc --noEmit`. Expected: clean.
</action>
<verify>
<automated>npx tsc --noEmit; T=$(grep -c "assertA31" tests/uat/extension-page-harness.ts); test "$T" -ge 3 &amp;&amp; S=$(grep -c "A31_PASSWORD_SENTINEL" tests/uat/extension-page-harness.ts); test "$S" -ge 2 &amp;&amp; grep -q "'secret-do-not-log-123'" tests/uat/extension-page-harness.ts</automated>
</verify>
<acceptance_criteria>
- `npx tsc --noEmit` exits 0.
- `grep -c 'assertA31' tests/uat/extension-page-harness.ts` returns >=3 (function definition + declare global entry + object literal entry).
- `grep -c 'A31_PASSWORD_SENTINEL' tests/uat/extension-page-harness.ts` returns >=2 (declaration + usage).
- `grep -c \"'secret-do-not-log-123'\" tests/uat/extension-page-harness.ts` returns exactly 1.
- Existing Plan 03-02 assertA30 entry still present.
</acceptance_criteria>
<done>
Page-side assertA31 types sentinel into the password input + SAVEs; registered on __mokoshHarness; ready for Task 2 host-side driveA31 negative-assertion.
</done>
</task>
<task type="auto" tdd="false">
<name>Task 2: Add driveA31 host-side (sentinel-absence grep) + orchestrator wiring</name>
<files>tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
<read_first>
- tests/uat/lib/harness-page-driver.ts where Plan 03-02 added driveA30 (same UserEvent import; same 3-phase shape)
- tests/uat/harness.test.ts where Plan 03-02 added driveA30 wrapping + drivers entry
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md §"Negative-assertion pattern (Plan 03-03 password sentinel grep)"
</read_first>
<behavior>
Host-side (`tests/uat/lib/harness-page-driver.ts`):
- The `UserEvent` type import added by Plan 03-02 is reused (no new import needed).
- Adds `export async function driveA31(page: Page, downloadsDir: string): Promise<AssertionRecord>`:
- Phase 1 — page.evaluate harness.assertA31()
- Phase 2 — findLatestZip; A31.0 if null
- Phase 3 — JSZip.loadAsync; read logs/events.json; A31.0a if absent
- Parse as UserEvent[]; A31.0b on JSON parse failure
- Push check A31.2: events whose .value contains SENTINEL = 0
(negative-assertion proves the filter fired)
- Push check A31.3: events with target === '#probe-password' = 0
(filter early-returns BEFORE addUserEvent)
- Diagnostic includes count of total userEvents + count of events that match #probe-password selector + count of events whose value contains sentinel
- Filter-pipeline form (no `continue`).
Orchestrator (`tests/uat/harness.test.ts`):
- Adds `driveA31,` to import block (after `driveA30,`).
- Adds `driveA31Wrapped` const after driveA30Wrapped.
- Adds `{ name: 'A31', drive: driveA31Wrapped },` to drivers array after the A30 entry.
- Updates orchestrator banner to append `, A31`.
</behavior>
<action>
1. Open `tests/uat/lib/harness-page-driver.ts`. At the end of the file (AFTER driveA30 added by Plan 03-02), append:
```typescript
/* ─── Plan 03-03 — driveA31 (password-filter PARTIAL host-side) ─────── */
/** Fixed test sentinel — same value as page-side A31_PASSWORD_SENTINEL.
* Negative-assertion driver searches events.json for its absence. */
const A31_PASSWORD_SENTINEL = 'secret-do-not-log-123';
/** Selector the production getSelector returns for #probe-password. */
const A31_PASSWORD_SELECTOR = '#probe-password';
/**
* Drive A31 (Plan 03-03 — SPEC §10 #8 PARTIAL per D-P3-02).
*
* Page-side assertA31 typed SENTINEL into the password input then
* SAVEd. Host-side asserts that:
* - the sentinel string is ABSENT from any UserEvent.value field
* (proves the line-82 filter early-returned before addUserEvent)
* - no UserEvent has target === '#probe-password' (proves the same
* filter via the orthogonal selector path)
*
* Filter-pipeline form (no `continue`) per CLAUDE.md Control Flow §.
*
* Checks (3 total):
* - A31.1: SAVE_ARCHIVE ack (page-side)
* - A31.2: 0 events contain SENTINEL in .value field
* - A31.3: 0 events have target === '#probe-password'
*
* @param page - The harness page from `launchHarnessBrowser`.
* @param downloadsDir - Absolute path to the per-run downloads directory.
* @returns AssertionRecord with 3 merged checks.
*/
export async function driveA31(
page: Page,
downloadsDir: string,
): Promise<AssertionRecord> {
// Phase 1 — page-side orchestration + SAVE.
const pageResult = await page.evaluate(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- evaluate runs in browser context where Window types are loose.
const harness = (window as any).__mokoshHarness;
const r: AssertionRecord = await harness.assertA31();
return r;
}) as AssertionRecord;
const mergedChecks: CheckRecord[] = pageResult.checks.slice();
const mergedDiagnostics: string[] = pageResult.diagnostics.slice();
// Phase 2 — locate the produced zip.
const zipPath = findLatestZip(downloadsDir);
if (zipPath === null) {
mergedChecks.push({
name: 'A31.0: at least one zip present in downloadsDir',
expected: '>=1 zip',
actual: 'no zip in downloadsDir',
passed: false,
});
return {
passed: false,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
mergedDiagnostics.push(`A31 zipPath=${zipPath}`);
// Phase 3 — load + inspect logs/events.json.
const zipBytes = readFileSync(zipPath);
const zip = await JSZip.loadAsync(zipBytes);
const eventsFile = zip.file('logs/events.json');
mergedChecks.push({
name: 'A31.0a: logs/events.json entry exists in zip',
expected: true,
actual: eventsFile !== null,
passed: eventsFile !== null,
});
if (eventsFile === null) {
return {
passed: false,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
const eventsRaw = await eventsFile.async('string');
let userEvents: UserEvent[] = [];
let parseErr: string | null = null;
try {
userEvents = JSON.parse(eventsRaw) as UserEvent[];
} catch (err) {
parseErr = err instanceof Error ? err.message : String(err);
}
if (parseErr !== null) {
mergedChecks.push({
name: 'A31.0b: logs/events.json parses as JSON',
expected: 'JSON.parse success',
actual: `<error: ${parseErr}>`,
passed: false,
});
return {
passed: false,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
const eventsContainingSentinel = userEvents.filter(
(e) => typeof e.value === 'string' && e.value.includes(A31_PASSWORD_SENTINEL),
);
const eventsTargetingPassword = userEvents.filter(
(e) => e.target === A31_PASSWORD_SELECTOR,
);
mergedDiagnostics.push(`A31 userEvents.length=${userEvents.length}`);
mergedDiagnostics.push(
`A31 sentinel-containing count=${eventsContainingSentinel.length}, password-targeting count=${eventsTargetingPassword.length}`,
);
mergedChecks.push({
name: 'A31.2: 0 UserEvent entries contain the SENTINEL in their .value field (proves src/content/index.ts:82 filter fired)',
expected: 0,
actual: eventsContainingSentinel.length,
passed: eventsContainingSentinel.length === 0,
});
mergedChecks.push({
name: `A31.3: 0 UserEvent entries have target === '${A31_PASSWORD_SELECTOR}' (filter early-returns BEFORE addUserEvent)`,
expected: 0,
actual: eventsTargetingPassword.length,
passed: eventsTargetingPassword.length === 0,
});
const mergedPassed = mergedChecks.every((c) => c.passed);
return {
passed: mergedPassed,
name: pageResult.name,
checks: mergedChecks,
diagnostics: mergedDiagnostics,
error: pageResult.error,
};
}
```
2. Open `tests/uat/harness.test.ts`. In the import block from `./lib/harness-page-driver`, AFTER `driveA30,` and BEFORE `getManifestVersion,` add:
```typescript
// Plan 03-03 — password-filter PARTIAL (SPEC §10 #8 PARTIAL per D-P3-02)
driveA31,
```
3. AFTER the `driveA30Wrapped` const (added by Plan 03-02), add:
```typescript
// Plan 03-03 — driveA31 needs downloadsDir for host-side JSZip
// negative-assertion against logs/events.json.
const driveA31Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
(page) => driveA31(page, handles.downloadsDir);
```
4. In the drivers array, AFTER the `{ name: 'A30', ... }` entry, add:
```typescript
// Plan 03-03 A31: password-filter PARTIAL (SPEC §10 #8 PARTIAL per
// D-P3-02). Negative-assertion: types sentinel into password input;
// host-side asserts absence from logs/events.json (proves the
// existing src/content/index.ts:82 filter fires).
{ name: 'A31', drive: driveA31Wrapped },
```
5. Update the orchestrator banner line (line 268) to append `, A31`:
```typescript
process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29, A30, A31)\n');
```
6. Run `npx tsc --noEmit`. Expected: clean.
7. Run `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat`. Expected: `32/32 GREEN`.
</action>
<verify>
<automated>npx tsc --noEmit; D=$(grep -c "driveA31" tests/uat/lib/harness-page-driver.ts); test "$D" -ge 2 &amp;&amp; H=$(grep -c "driveA31" tests/uat/harness.test.ts); test "$H" -ge 3 &amp;&amp; HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat</automated>
</verify>
<acceptance_criteria>
- `npx tsc --noEmit` exits 0.
- `grep -c 'driveA31' tests/uat/lib/harness-page-driver.ts` returns >=2.
- `grep -c 'driveA31' tests/uat/harness.test.ts` returns >=3.
- `grep -c 'secret-do-not-log-123' tests/uat/lib/harness-page-driver.ts` returns exactly 1.
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with stdout containing `UAT harness: 32/32 assertions passed`.
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (Tier-1 inventory stays at 12).
</acceptance_criteria>
<done>
UAT harness runs 32/32 GREEN with A31 verifying the §10 #8 PARTIAL
contract: typing into a password input produces zero events whose
value contains the sentinel and zero events targeting the password
selector. The existing src/content/index.ts:82 filter is the
verified mechanism; full masking remains Out of Scope v1 per D-P3-02.
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Puppeteer host ↔ page realm | Test harness drives via page.evaluate; production filter at src/content/index.ts:82 runs inside the page content script |
| Page realm ↔ content script | Synthetic input event dispatched on #probe-password reaches the production listener at src/content/index.ts:78 |
| Filter ↔ event log | Negative-assertion contract: filter at line 82 prevents value from reaching userEvents[] which is what assembles logs/events.json |
| dist-test/ ↔ dist/ | Two-bundle separation: Plan 03-03 adds NO test-only symbols; production bundle invariant unchanged |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-03-01 | Information Disclosure | Sentinel value lands in event log despite filter | mitigate | A31 IS the negative-assertion mitigation — RED A31 means the filter regressed; the test enforces the invariant. Sentinel is not a real secret (RESEARCH §"Security Domain"); if it leaked, it would be visible in events.json which is logged but never transmitted (REQ-password-confidentiality Out of Scope v1 charter applies). |
| T-03-03-02 | Information Disclosure | Test-only hook surface leaking to production bundle | mitigate | A31 rides the production input listener; no `__MOKOSH_UAT__`-gated symbols. FORBIDDEN_HOOK_STRINGS unchanged at 12 entries. |
| T-03-03-03 | Tampering | A31 SAVE produces a zip in the per-run downloadsDir that contains the sentinel only IF the filter regressed | accept | The per-run downloadsDir is mkdtempSync'd by launchHarnessBrowser + cleaned by the test runner; no cross-run leakage. Sentinel is not a real secret. |
| T-03-03-04 | Repudiation | If A31 GREEN but the production filter actually broke, the assertion would mislead | mitigate | A31 checks two orthogonal paths: (a) sentinel-value absence and (b) password-selector-target absence. Both pass IFF the filter early-returns BEFORE addUserEvent. A regression in the filter would cause AT LEAST ONE of the two checks to RED. Defense-in-depth at the test layer. |
No new production surface; threat surface unchanged from Phase 2. The
existing src/content/index.ts:82 filter is the verification subject;
the PARTIAL mark in Plan 03-05 VERIFICATION.md explicitly carries the
charter rationale.
</threat_model>
<verification>
- `npx tsc --noEmit` exits 0.
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 32/32 GREEN.
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (12 FORBIDDEN_HOOK_STRINGS × 0 hits each).
- A31 diagnostic line shows `sentinel-containing count=0, password-targeting count=0`.
</verification>
<success_criteria>
- A31 GREEN with 3 merged checks (SAVE ack + 2 negative-assertions).
- The existing src/content/index.ts:82 password filter is verified to
fire on synthetic input events into the probe-page password element.
- FORBIDDEN_HOOK_STRINGS unchanged at 12 entries.
- vitest baseline preserved (171/171 GREEN).
- Plan 03-05 VERIFICATION.md will mark §10 #8 as PARTIAL with explicit
charter citation (D-P3-02) and reference A31 GREEN as the existing-
minimum evidence.
</success_criteria>
<output>
After completion, create
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-SUMMARY.md`
documenting:
- Negative-assertion contract verified (2 orthogonal absence checks)
- D-P3-02 charter alignment (existing-minimum verification; not scope expansion)
- UAT 31 → 32 GREEN; Tier-1 inventory unchanged at 12
- Plan 03-04 wave dependency: SAME 3 harness files; SEQUENTIAL within Wave 2 per RESEARCH Pitfall 6
</output>

View File

@@ -0,0 +1,380 @@
---
phase: 03
slug: spec-10-smoke-verification-dom-event-log-verification
plan: 04
type: execute
wave: 4
depends_on:
- 01
- 02
- 03
files_modified:
- tests/uat/lib/harness-page-driver.ts
- tests/uat/harness.test.ts
autonomous: true
requirements: []
tags:
- uat-harness
- a32
- ram-ceiling
- spec-10-9-best-effort
- approach-b
- page-metrics
- charter-d-p3-04
user_setup: []
must_haves:
truths:
- "puppeteer.Page.metrics() returns a JSHeapUsedSize value (>= 0) for the harness page realm"
- "JSHeapUsedSize for the harness page realm is below 50 MB (page-realm only; SW context excluded per RESEARCH Pitfall 2)"
- "Driver emits an explicit diagnostic line: 'NOTE: page-realm only; SW context excluded' (prevents operator misinterpretation)"
- "UAT harness exits 0 with 32 + 1 = 33/33 assertions GREEN (A31 baseline preserved + new A32)"
artifacts:
- path: "tests/uat/lib/harness-page-driver.ts"
provides: "driveA32 host-side Page.metrics scaffolding (best-effort; explicit page-realm-only diagnostic)"
contains: "driveA32"
- path: "tests/uat/harness.test.ts"
provides: "driveA32 import + drivers-array push entry (no wrapped driver — Page.metrics needs only page, not downloadsDir)"
contains: "driveA32"
key_links:
- from: "tests/uat/harness.test.ts"
to: "tests/uat/lib/harness-page-driver.ts driveA32"
via: "import + drivers-array push"
pattern: "driveA32"
- from: "tests/uat/lib/harness-page-driver.ts driveA32"
to: "puppeteer.Page.metrics() CDP Performance.getMetrics"
via: "await page.metrics()"
pattern: "page.metrics\\(\\)"
---
<objective>
Extend the UAT harness with A32 — best-effort scaffolding for SPEC §10 #9
(extension background RAM ≤ 50 MB). Per D-P3-04 locked decision: this is
best-effort + operator-driven. The harness DOES NOT measure the MV3
service worker heap (RESEARCH Pitfall 2: Page.metrics is page-realm
only). The genuine binding §10 #9 gate is the operator's
`chrome://memory-internals` observation, recorded in Plan 03-05
VERIFICATION.md `human_verification` block.
A32 SHIPS the optional Page.metrics scaffolding per RESEARCH Open
Question 3 recommendation (~30 lines; cost-cheap; informational value).
Diagnostic output explicitly states the page-realm scope so the
operator never confuses an automation GREEN with full §10 #9 closure.
Purpose: Provides a low-cost informational floor for page-realm heap
usage and exercises the puppeteer.Page.metrics API end-to-end so Phase
4 (programmatic RAM measurement upgrade) inherits a working scaffold.
Output: A32 assertion with 2 host-side checks (Page.metrics returned
JSHeapUsedSize >= 0 + JSHeapUsedSize < 50 MB) + an explicit diagnostic
line about page-realm scope; UAT count 32 → 33 GREEN.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-PLAN.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-PLAN.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-PLAN.md
<interfaces>
<!-- Key contracts the executor needs. -->
From puppeteer ^25.0.2 (Page.metrics):
interface Metrics {
Timestamp?: number;
Documents?: number;
Frames?: number;
JSEventListeners?: number;
Nodes?: number;
LayoutCount?: number;
RecalcStyleCount?: number;
LayoutDuration?: number;
RecalcStyleDuration?: number;
ScriptDuration?: number;
TaskDuration?: number;
JSHeapUsedSize?: number; // <- bytes; the field A32 reads
JSHeapTotalSize?: number;
}
page.metrics(): Promise<Metrics>;
From RESEARCH.md §"Code Example A3X":
- Page.metrics is page-realm only — JSHeapUsedSize covers V8 isolate
of THIS Page, NOT the MV3 service worker (separate target).
- 50 MB threshold per SPEC §10 #9; treat as best-effort floor for the
page realm alone.
- Diagnostic copy gate: emit
'NOTE: page-realm only; SW context measurement requires
chrome://memory-internals operator verification per D-P3-04.'
From src/shared/types.ts: no UserEvent / type changes for A32.
</interfaces>
# Plan Anchors
- **Sequential wave assignment (per RESEARCH Pitfall 6 + file-overlap rule):** Plan 03-04 lives in wave 4
modifies tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts
(SAME files as Plans 03-01..03; depends_on enforces sequential).
- **NO page-side assertion needed.** Page.metrics is a host-side
puppeteer API. Unlike A24..A31, A32 does NOT call assertA32 inside
page.evaluate — there's no need for a window.__mokoshHarness method.
This is consistent with how the host-side latency portion of A25 is
computed; A32 is similar but skips the page-side entirely.
- **No setupFreshRecording, no SAVE, no zip read.** A32 measures the
current heap state of the harness page; no archive is produced.
- **RESEARCH Pitfall 2 mitigation (HARD):** the diagnostic line about
page-realm scope MUST be emitted regardless of pass/fail. This
prevents an operator from glancing at "A32 GREEN" and concluding §10
#9 is closed.
- **50 MB threshold:** SPEC §10 #9 + CON-ram-ceiling. Page-realm typical
values: a few MB (Plan 02-04 harness measurements show ~2-8 MB
page-realm heap during recording). Far below the 50 MB ceiling on
any reasonable run.
- **FORBIDDEN_HOOK_STRINGS lockstep:** A32 is host-side only; Page.metrics
is not bundled to the page. Tier-1 inventory stays at 12 entries.
- **A6 in RESEARCH Assumptions Log MEDIUM-risk noted:** "if Plan 03-04
scaffolding requires a new bridge op (e.g., `get-page-metrics` from
offscreen → harness), that would add 1-2 entries." This plan AVOIDS
that: Page.metrics is read from the host puppeteer object directly;
no new bridge ops added; no new __MOKOSH_UAT__ symbols.
</context>
<tasks>
<task type="auto" tdd="false">
<name>Task 1: Add driveA32 host-side (puppeteer.Page.metrics scaffolding) + orchestrator wiring</name>
<files>tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
<read_first>
- tests/uat/lib/harness-page-driver.ts (full sense of the file; in particular how driveA1 is a 1-line page.evaluate wrapper, contrasting with A32 which is pure host-side)
- tests/uat/harness.test.ts where Plan 03-03 added driveA31 + driveA31Wrapped + drivers-array entry (study shape)
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md §"Code Example A3X" (canonical scaffolding shape; verbatim copy)
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md §"Pitfall 2" (diagnostic-copy gate)
</read_first>
<behavior>
Host-side (`tests/uat/lib/harness-page-driver.ts`):
- Adds `export async function driveA32(page: Page): Promise<AssertionRecord>`:
- Calls `const metrics = await page.metrics();`
- Computes `const jsHeapBytes = metrics.JSHeapUsedSize ?? -1;`
- Computes `const jsHeapMB = jsHeapBytes >= 0 ? jsHeapBytes / (1024 * 1024) : -1;`
- Pushes A32.1 (Page.metrics returned JSHeapUsedSize): expected '>= 0', actual `jsHeapBytes`, passed `jsHeapBytes >= 0`
- Pushes A32.2 (page-realm JS heap < 50 MB): expected '< 50 MB', actual `${jsHeapMB.toFixed(2)} MB`, passed `jsHeapMB >= 0 && jsHeapMB < 50`
- Pushes the mandatory diagnostic: `'NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.'`
- Also pushes informational diagnostics: `JSHeapUsedSize=${jsHeapBytes} bytes` and `JSHeapTotalSize=${metrics.JSHeapTotalSize ?? -1} bytes`
- Returns AssertionRecord computed `passed = checks.every(c => c.passed)`
- The new constant `A32_RAM_CEILING_BYTES = 50 * 1024 * 1024` makes the threshold readable.
Orchestrator (`tests/uat/harness.test.ts`):
- Adds `driveA32,` to import block (after `driveA31,`).
- NO `driveA32Wrapped` const needed (driveA32 takes only `page`).
- Adds `{ name: 'A32', drive: driveA32 },` to drivers array AFTER the A31 entry, with banner comment citing D-P3-04 + Pitfall 2.
- Updates orchestrator banner line to append `, A32`.
</behavior>
<action>
1. Open `tests/uat/lib/harness-page-driver.ts`. At the end of the file (AFTER driveA31 added by Plan 03-03), append:
```typescript
/* ─── Plan 03-04 — driveA32 (RAM scaffolding best-effort) ──────────── */
/** RAM ceiling per SPEC §10 #9 + CON-ram-ceiling. */
const A32_RAM_CEILING_BYTES = 50 * 1024 * 1024;
/** Bytes-per-MB factor for diagnostic copy. */
const A32_BYTES_PER_MB = 1024 * 1024;
/**
* Drive A32 (Plan 03-04 — SPEC §10 #9 RAM best-effort per D-P3-04).
*
* Reads puppeteer.Page.metrics() against the harness page and asserts
* JSHeapUsedSize is below the 50 MB ceiling. This is informational
* scaffolding ONLY:
*
* - RESEARCH Pitfall 2: Page.metrics is page-realm only. The MV3
* service worker is a separate Puppeteer target with its own V8
* isolate; page.metrics() does not aggregate across workers/iframes.
* - The page-realm value reported here is NOT the operator-facing
* "extension background RAM" measurement that SPEC §10 #9 requires.
* - The binding §10 #9 gate lives in Plan 03-05 VERIFICATION.md
* `human_verification` block (operator runs chrome://memory-internals
* OR chrome://extensions service-worker memory display).
*
* Why ship this anyway (per RESEARCH Open Question 3):
* - Low cost (~30 lines; single API call; no new bundle surface).
* - Exercises the Page.metrics API end-to-end so Phase 4 (programmatic
* RAM measurement upgrade) inherits a working scaffold.
* - Provides a sanity floor — if the harness page-realm heap ever
* blows past 50 MB, something has gone catastrophically wrong in
* the test infrastructure itself (not necessarily a §10 #9 regression
* in production).
*
* The diagnostic line about page-realm scope MUST be emitted regardless
* of pass/fail per Pitfall 2.
*
* @param page - The harness page from `launchHarnessBrowser`.
* @returns AssertionRecord with 2 checks (heap returned + heap < 50 MB)
* + explicit page-realm-only diagnostic.
*/
export async function driveA32(page: Page): Promise<AssertionRecord> {
const checks: CheckRecord[] = [];
const diagnostics: string[] = [];
// Pitfall 2 gate: emit the page-realm caveat BEFORE any other diagnostic
// so it leads in the structured output (the operator sees it first).
diagnostics.push(
'NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.',
);
let metricsErr: string | null = null;
let jsHeapBytes = -1;
let jsHeapTotal = -1;
try {
const metrics = await page.metrics();
jsHeapBytes = metrics.JSHeapUsedSize ?? -1;
jsHeapTotal = metrics.JSHeapTotalSize ?? -1;
} catch (err) {
metricsErr = err instanceof Error ? err.message : String(err);
}
const jsHeapMB = jsHeapBytes >= 0 ? jsHeapBytes / A32_BYTES_PER_MB : -1;
diagnostics.push(`A32 JSHeapUsedSize=${jsHeapBytes} bytes (${jsHeapMB.toFixed(2)} MB)`);
diagnostics.push(`A32 JSHeapTotalSize=${jsHeapTotal} bytes`);
if (metricsErr !== null) {
diagnostics.push(`A32 Page.metrics threw: ${metricsErr}`);
}
checks.push({
name: 'A32.1: Page.metrics returned a JSHeapUsedSize value >= 0',
expected: '>= 0',
actual: jsHeapBytes,
passed: jsHeapBytes >= 0,
});
checks.push({
name: `A32.2: Page-realm JS heap < ${A32_RAM_CEILING_BYTES / A32_BYTES_PER_MB} MB (NOTE: scaffolding only; SW context excluded per D-P3-04)`,
expected: `< ${A32_RAM_CEILING_BYTES / A32_BYTES_PER_MB} MB`,
actual: jsHeapMB >= 0 ? `${jsHeapMB.toFixed(2)} MB` : 'unavailable',
passed: jsHeapBytes >= 0 && jsHeapBytes < A32_RAM_CEILING_BYTES,
});
const passed = checks.every((c) => c.passed);
return {
passed,
name: 'A32 — RAM scaffolding (best-effort; page-realm only per D-P3-04 / SPEC §10 #9)',
checks,
diagnostics,
error: metricsErr ?? undefined,
};
}
```
2. Open `tests/uat/harness.test.ts`. In the import block from `./lib/harness-page-driver`, AFTER `driveA31,` and BEFORE `getManifestVersion,` add:
```typescript
// Plan 03-04 — RAM scaffolding best-effort (SPEC §10 #9 per D-P3-04)
driveA32,
```
3. In the drivers array, AFTER the `{ name: 'A31', ... }` entry from Plan 03-03, add:
```typescript
// Plan 03-04 A32: RAM scaffolding (SPEC §10 #9 best-effort per D-P3-04).
// NOTE — Page.metrics is page-realm only; SW context is a separate
// Puppeteer target (RESEARCH Pitfall 2). A32 is informational
// scaffolding; the binding §10 #9 gate lives in Plan 03-05
// VERIFICATION.md `human_verification` block. No wrapped const
// needed — driveA32 takes only `page`.
{ name: 'A32', drive: driveA32 },
```
4. Update the orchestrator banner line (line 268) to append `, A32`:
```typescript
process.stdout.write('Architecture: A0 pre-flight + extension-internal page driver (A1..A14, A15..A17, A18..A22, A23, A24, A25, A26, A27, A28, A29, A30, A31, A32)\n');
```
5. Run `npx tsc --noEmit`. Expected: clean.
6. Run `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat`. Expected: `33/33 GREEN`.
</action>
<verify>
<automated>npx tsc --noEmit; D=$(grep -c "driveA32" tests/uat/lib/harness-page-driver.ts); test "$D" -ge 2 &amp;&amp; H=$(grep -c "driveA32" tests/uat/harness.test.ts); test "$H" -ge 2 &amp;&amp; grep -q "NOTE: page-realm only" tests/uat/lib/harness-page-driver.ts &amp;&amp; HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat</automated>
</verify>
<acceptance_criteria>
- `npx tsc --noEmit` exits 0.
- `grep -c 'driveA32' tests/uat/lib/harness-page-driver.ts` returns >=2.
- `grep -c 'driveA32' tests/uat/harness.test.ts` returns >=2 (import line + drivers-array push; no wrapped const).
- `grep -c 'NOTE: page-realm only' tests/uat/lib/harness-page-driver.ts` returns exactly 1.
- `grep -c 'page.metrics()' tests/uat/lib/harness-page-driver.ts` returns exactly 1.
- `grep -c 'A32_RAM_CEILING_BYTES' tests/uat/lib/harness-page-driver.ts` returns >=2 (declaration + usage).
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with stdout containing `UAT harness: 33/33 assertions passed` AND the diagnostic line `NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.` (printed by printAssertionResult on A32).
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (Tier-1 inventory stays at 12).
</acceptance_criteria>
<done>
UAT harness runs 33/33 GREEN. A32 emits the page-realm-only diagnostic
on EVERY run (pass or fail). FORBIDDEN_HOOK_STRINGS unchanged at 12.
Page.metrics scaffolding lives in the harness for Phase 4 to upgrade.
The binding §10 #9 gate remains operator-driven and is recorded as
human_verification in Plan 03-05.
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Puppeteer host ↔ CDP | Page.metrics is a thin wrapper over CDP Performance.getMetrics; runs in the puppeteer host process, no extension code path |
| Page realm ↔ host realm | A32 does NOT use page.evaluate; no new contract between page and host |
| dist-test/ ↔ dist/ | Two-bundle separation: Plan 03-04 adds NO test-only symbols; production bundle invariant unchanged |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-04-01 | Repudiation | Operator interprets A32 GREEN as full §10 #9 closure, skips chrome://memory-internals check | mitigate | Mandatory diagnostic line `'NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.'` emitted on EVERY run; check name itself includes the caveat; Plan 03-05 VERIFICATION.md explicitly lists §10 #9 in `human_verification` block. Three layers of operator-visible signal. |
| T-03-04-02 | Information Disclosure | Test-only hook surface leaking to production bundle | mitigate | A32 is host-side only; Page.metrics is not bundled to the page realm. FORBIDDEN_HOOK_STRINGS unchanged at 12 entries. |
| T-03-04-03 | Denial of Service | Page.metrics returns 0 or throws on first call after browser launch | mitigate | A32 wraps the call in try/catch + falls through gracefully (jsHeapBytes stays -1; A32.1 RED with clear diagnostic). Per A3 in RESEARCH Assumptions Log, Page.metrics has been stable since Puppeteer 1.x; failure is extremely unlikely on 25.0.2. |
| T-03-04-04 | Elevation of Privilege | New chrome.* permission grant for measurement | accept | A32 uses zero chrome.* APIs. Page.metrics is a CDP call, not an extension API. No manifest delta. |
No new production surface; threat surface unchanged from Plan 03-03.
UAT harness extension is test-only and adds no bundle surface (Page.metrics
is host-side only).
</threat_model>
<verification>
- `npx tsc --noEmit` exits 0.
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 33/33 GREEN.
- The diagnostic line `NOTE: page-realm only; SW context measurement requires chrome://memory-internals operator verification per D-P3-04.` appears in stdout from A32.
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (12 FORBIDDEN_HOOK_STRINGS × 0 hits each).
</verification>
<success_criteria>
- A32 GREEN with 2 checks (heap returned + heap < 50 MB).
- Pitfall 2 diagnostic emitted on every run.
- Page.metrics scaffolding in place for Phase 4 to upgrade.
- FORBIDDEN_HOOK_STRINGS unchanged at 12 entries.
- vitest baseline preserved (171/171 GREEN).
- Plan 03-05 will record §10 #9 as `human_verification` regardless of A32
status — A32 is informational scaffolding, NOT the binding gate.
</success_criteria>
<output>
After completion, create
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-04-SUMMARY.md`
documenting:
- A32 host-side-only scaffolding rationale (no page-side; Page.metrics is host)
- D-P3-04 + Pitfall 2 compliance (mandatory page-realm-only diagnostic)
- Phase 4 inheritance: programmatic RAM measurement upgrade path
- UAT 32 → 33 GREEN; Tier-1 inventory unchanged at 12
- Plan 03-05 wave dependency: VERIFICATION.md aggregator; depends on Plans 03-01..04 GREEN
</output>

View File

@@ -0,0 +1,731 @@
---
phase: 03
slug: spec-10-smoke-verification-dom-event-log-verification
plan: 05
type: execute
wave: 5
depends_on:
- 01
- 02
- 03
- 04
files_modified:
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md
- .planning/REQUIREMENTS.md
- .planning/ROADMAP.md
- .planning/STATE.md
autonomous: true
requirements:
- REQ-install-clean
- REQ-rrweb-dom-buffer
- REQ-user-event-log
tags:
- verification
- spec-10-sweep
- aggregator
- t5-override
- charter-d-p3-02
- charter-d-p3-04
- phase-3-closure
user_setup: []
must_haves:
truths:
- "VERIFICATION.md exists at .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md with the canonical frontmatter shape (phase + verified + status + score + overrides_applied + override_notes + human_verification)"
- "All 9 SPEC §10 criteria have explicit evidence rows in the Per-Requirement Scorecard with Phase-citation + plan-citation + commit-citation"
- "Pre-checkpoint bundle gates 6/6 PASS (per saved memory feedback-pre-checkpoint-bundle-gates.md)"
- "§10 #8 marked PARTIAL with explicit D-P3-02 charter citation + A31 GREEN evidence in overrides_applied"
- "§10 #9 marked human_needed with explicit D-P3-04 charter citation + operator chrome://memory-internals instructions + A32 informational note in human_verification"
- "T5 override pattern applied for §10 #4/#5 (and #8 PARTIAL) per saved memory feedback-trust-harness-over-manual-uat.md"
- "REQUIREMENTS.md REQ-rrweb-dom-buffer + REQ-user-event-log markers flipped to Complete (or PARTIAL with citation)"
- "ROADMAP.md Phase 3 row flipped to [x] with closure date"
- "STATE.md progress.completed_phases incremented; current focus + session continuity updated"
artifacts:
- path: ".planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md"
provides: "Full §10 sweep aggregator with 9-criterion scorecard + T5 override block + human_verification block + cross-cutting gates + deferred items"
min_lines: 120
- path: ".planning/REQUIREMENTS.md"
provides: "REQ-rrweb-dom-buffer + REQ-user-event-log markers updated with Plan 03-01/03-02 closure citation"
contains: "Phase 3 closure"
- path: ".planning/ROADMAP.md"
provides: "Phase 3 row flipped [x] + closure date + harness count update (33 assertions)"
contains: "Phase 3"
- path: ".planning/STATE.md"
provides: "progress.completed_phases incremented to 3; current_focus + last_activity updated"
contains: "completed_phases: 3"
key_links:
- from: "03-VERIFICATION.md"
to: "Plan 03-01 A29 GREEN (rrweb session.json) + Plan 03-02 A30 GREEN (events.json types) + Plan 03-03 A31 GREEN (password absence) + Plan 03-04 A32 GREEN (page-realm heap)"
via: "evidence column citations"
pattern: "A29|A30|A31|A32"
- from: "03-VERIFICATION.md override_notes"
to: "feedback-trust-harness-over-manual-uat.md saved memory"
via: "T5 override rationale citation"
pattern: "feedback-trust-harness-over-manual-uat"
---
<objective>
Phase 3 closure: write VERIFICATION.md aggregating all 9 SPEC §10
acceptance criteria evidence across Phase 1 + Phase 2 + Phase 3 plans,
mark §10 #8 PARTIAL per D-P3-02 charter, mark §10 #9 human_needed per
D-P3-04 charter, run pre-checkpoint bundle gates (6/6 standard
inventory), and flip the REQUIREMENTS.md / ROADMAP.md / STATE.md
markers that signal Phase 3 closure.
Purpose: This IS the Phase 3 deliverable. Plans 03-01..04 produced the
empirical evidence (A29..A32 GREEN); Plan 03-05 synthesizes it into the
canonical §10 sweep record + flips the project trackers.
Output:
- `03-VERIFICATION.md` (new file) — 9-criterion scorecard + override
block + human_verification block + cross-cutting gates + deferred
items table; modeled on Phase 1 + Phase 2 VERIFICATION.md structures.
- Documentation marker flips in REQUIREMENTS.md + ROADMAP.md + STATE.md.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-PATTERNS.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-PLAN.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-PLAN.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-PLAN.md
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-04-PLAN.md
@.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md
@.planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md
<interfaces>
<!-- Canonical reference shapes the executor uses verbatim. -->
T5 override frontmatter shape (from 02-VERIFICATION.md lines 1-32):
- `phase`, `verified` (ISO timestamp Z), `status`, `score`, `overrides_applied` (int), `override_notes` (list of {dimension, initial_status, override_to, rationale}), `human_verification` (list of {dimension, rationale}).
Phase 1 VERIFICATION.md structure (from 01-VERIFICATION.md):
- Goal block (verbatim ROADMAP)
- Per-Requirement Scorecard table (|#|Requirement|Evidence|Status|)
- Cross-Cutting Gates table (|Gate|Evidence|Status|)
- 7 P0 Audit Defects table (Phase 3 doesn't ship implementation; uses 9-criterion §10 scorecard instead)
- Operator-Empirical Acks table (Phase 3 may have one row for §10 #9 once operator runs chrome://memory-internals; empty until then)
- Forward-Looking Deferred Items table
Pre-checkpoint bundle gates 6/6 standard inventory (from
feedback-pre-checkpoint-bundle-gates.md + 02-04-SUMMARY.md lines 158-168):
- Gate 1: npm run build exit 0
- Gate 2: SW CSP-safety (new Function/eval) — 1 documented exception (setimmediate polyfill)
- Gate 3: SW Node-globals (Buffer./require) — 0 hits
- Gate 4: DOM-globals (window./document.) — bundled-lib idiom only
- Gate 5: Tier-1 SW-bundle-import gate GREEN
- Gate 6: FORBIDDEN_HOOK_STRINGS unit gate — 12 strings × 0 hits each
- Gate 7: Manifest validation gates (i18n + locale-parity + build) GREEN
§10 criteria → REQ mapping (from REQUIREMENTS.md §"Phase 1 Acceptance Criteria"):
1. install — REQ-install-clean + REQ-manifest-permissions (Phase 1 closed; Plan 01-12)
2. continuous video — REQ-video-ring-buffer (Phase 1 closed)
3. ≤ 30s buffer — REQ-video-ring-buffer (Phase 1 closed)
4. rrweb DOM — REQ-rrweb-dom-buffer (Plan 03-01 A29 GREEN)
5. event log — REQ-user-event-log (Plan 03-02 A30 GREEN)
6. < 5s archive — REQ-archive-export-latency (Phase 2 closed; A25 GREEN)
7. playable .webm — REQ-archive-layout + REQ-video-ring-buffer (Phase 1 closed; A28 GREEN)
8. password — REQ-password-confidentiality (Out of Scope v1; A31 PARTIAL per D-P3-02)
9. RAM — CON-ram-ceiling (best-effort + operator-driven per D-P3-04; A32 informational)
T5 override pattern (from 02-VERIFICATION.md + feedback-trust-harness-over-manual-uat.md):
- When verifier returns human_needed for a criterion AND a harness assertion empirically covers the same surface, MOVE the entry from human_verification to overrides_applied with explicit user-delegation citation + saved-memory reference.
- §10 #4 / #5 — harness covers (A29 + A30); override-eligible per saved memory.
- §10 #8 PARTIAL — harness covers existing-minimum (A31); override-eligible per D-P3-02 (charter clarifies scope, not regression).
- §10 #9 — page.metrics is page-realm only (Pitfall 2); SW context CANNOT be measured by harness — this is the genuine human_verification exception.
</interfaces>
# Plan Anchors
- **Wave 3 (synthesis):** Plans 03-01..04 must be GREEN before this
plan runs. depends_on: [01, 02, 03, 04] enforces.
- **T5 override pattern (saved memory feedback-trust-harness-over-manual-uat.md):**
apply to §10 #4 / #5 / #8 PARTIAL. The harness assertions A29 / A30 /
A31 cover the empirical surfaces. §10 #9 is the genuine exception
per RESEARCH Pitfall 2 (Page.metrics is page-realm only).
- **Documentation marker flips happen ATOMICALLY in the closure commit:**
REQUIREMENTS.md + ROADMAP.md + STATE.md flip in one commit per
Phase 1 closure precedent (commit `586836f`).
- **No new production code; no test changes:** Plan 03-05 is pure
documentation synthesis + verification gate execution.
- **Source audit citation:** Per REQUIREMENTS.md Traceability table:
REQ-rrweb-dom-buffer + REQ-user-event-log were re-routed to Phase 3
via the 2026-05-20 re-phasing. Plan 03-05 flips both from "Pending"
to "Complete (Phase 3 closure)" or "PARTIAL" as appropriate per A31
status.
- **REQ-install-clean re-verification:** REQUIREMENTS.md line 178-185
shows it was marked Complete in Phase 1 Plan 01-12. Plan 03-05 cites
the existing tests (no-remote-fonts.test.ts + manifest-i18n.test.ts +
locale-parity.test.ts) without re-running anything custom for §10 #1.
</context>
<tasks>
<task type="auto" tdd="false">
<name>Task 1: Run pre-checkpoint bundle gates (6/6 standard inventory)</name>
<files></files>
<read_first>
- /home/parf/.claude/projects/-home-parf-projects-work-repremium/memory/feedback-pre-checkpoint-bundle-gates.md
- .planning/phases/02-stabilize-export-pipeline/02-04-SUMMARY.md lines 158-168 (canonical 7-gate table; Plan 03-05 runs same checks)
</read_first>
<behavior>
Runs the canonical 6-gate (Plan 02-04 ran 7; Plan 03-05 follows the
same shape):
- Gate 1: `npm run build` exits 0
- Gate 2: SW CSP-safety — `grep -rEn 'new Function|eval\\(' dist/assets/` returns 1 documented exception (setimmediate polyfill; pre-existing per deferred-items.md)
- Gate 3: SW Node-globals — `grep -rE 'Buffer\\.from|Buffer\\.alloc|require\\(' dist/assets/index.ts-*.js` returns 0 hits
- Gate 4: DOM-globals — `grep -rE 'window\\.|document\\.' dist/assets/index.ts-*.js` returns only bundled-lib idiom hits (typeof-guarded)
- Gate 5: Tier-1 SW-bundle-import gate — `npm test -- --run tests/background/sw-bundle-import.test.ts` exits 0
- Gate 6: FORBIDDEN_HOOK_STRINGS unit gate — `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exits 0 (12 strings × 0 hits each)
- Bonus Gate 7 (per 02-04 precedent): manifest + i18n + locale-parity GREEN — `npm test -- --run tests/i18n/ tests/build/` exits 0
Records the result of each gate (text snippet of grep output + test exit code) for inclusion in VERIFICATION.md cross-cutting gates table in Task 2.
</behavior>
<action>
1. From the repo root, run a clean build:
`npm run build`
Confirm exit 0; note the chunk count + total size for the VERIFICATION.md cross-cutting gates row.
2. Gate 2 — SW CSP-safety. Find the SW chunk and grep:
`find dist/assets -name 'index.ts-*.js' -print -exec grep -En 'new Function|eval\(' {} +`
Expected: exactly 1 occurrence (`new Function(""+I)` or similar setimmediate polyfill — pre-existing per `.planning/phases/01-stabilize-video-pipeline/deferred-items.md`). NO `eval(` hits. Record the file path + occurrence count.
3. Gate 3 — SW Node-globals:
`find dist/assets -name 'index.ts-*.js' -print -exec grep -En 'Buffer\.from|Buffer\.alloc|require\(' {} +`
Expected: 0 hits. Record.
4. Gate 4 — DOM-globals on SW chunk:
`find dist/assets -name 'index.ts-*.js' -print -exec grep -cE 'window\.|document\.' {} +`
Expected: small count (bundled-lib idiom; per 02-04 closure ~8 each, all behind typeof guards). Spot-check one match line and confirm it is `typeof window<"u"` or similar guard pattern. Record the count + guard sample.
5. Gate 5 — SW-bundle-import unit gate:
`npm test -- --run tests/background/sw-bundle-import.test.ts`
Expected: exit 0; 2/2 tests GREEN. Record stdout last-line summary.
6. Gate 6 — FORBIDDEN_HOOK_STRINGS unit gate:
`npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts`
Expected: exit 0; 13/13 GREEN (1 build + 12 forbidden strings). Record.
7. Gate 7 — manifest + i18n + build gates:
`npm test -- --run tests/i18n/ tests/build/`
Expected: exit 0; full count GREEN (Plan 01-12 baseline 57 tests; Plan 02-04 same). Record.
8. Stash the seven results in a scratch buffer in the executor's working notes; Task 2 reads them when writing the VERIFICATION.md cross-cutting gates table.
</action>
<verify>
<automated>npm run build &amp;&amp; npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts tests/background/sw-bundle-import.test.ts tests/i18n/ tests/build/</automated>
</verify>
<acceptance_criteria>
- `npm run build` exits 0; dist/ populated with `dist/assets/index.ts-*.js` SW chunk.
- Gate 2: `find dist/assets -name 'index.ts-*.js' -exec grep -cE 'new Function|eval\(' {} +` shows exactly 1 hit (or whatever the documented pre-existing count is) — operator records the file + line offset.
- Gate 3: `find dist/assets -name 'index.ts-*.js' -exec grep -cE 'Buffer\.from|Buffer\.alloc|require\(' {} +` shows 0 hits.
- Gate 4: every `window.`/`document.` hit is verified to be inside a `typeof X<"u"` guard (spot-check at least 2 hits; if any bare access exists, ESCALATE and STOP).
- Gate 5: tests/background/sw-bundle-import.test.ts exits 0 with all tests GREEN.
- Gate 6: tests/background/no-test-hooks-in-prod-bundle.test.ts exits 0 with 13/13 GREEN.
- Gate 7: tests/i18n/ + tests/build/ exit 0.
</acceptance_criteria>
<done>
All 6/6 standard pre-checkpoint bundle gates PASS; results recorded
for use in Task 2 cross-cutting gates table. No regressions from
Plans 03-01..04.
</done>
</task>
<task type="auto" tdd="false">
<name>Task 2: Write 03-VERIFICATION.md (9-criterion sweep + T5 override + human_verification + cross-cutting gates)</name>
<files>.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md</files>
<read_first>
- .planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md (full file; ~123 lines; canonical per-requirement scorecard + cross-cutting gates structure)
- .planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md (full file; ~167 lines; T5 override frontmatter + override_notes + human_verification shape)
- .planning/REQUIREMENTS.md §"Phase 1 Acceptance Criteria (SPEC §10 verbatim)" lines 238-260 (the 9 criteria with REQ cross-references)
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md `<decisions>` block (D-P3-01..04 locked decisions)
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-RESEARCH.md §"Code Examples" §"VERIFICATION.md frontmatter template (Plan 03-05)" (canonical frontmatter shape verbatim)
- .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-CONTEXT.md `<deferred>` block (10 deferred items to carry into the Forward-Looking Deferred Items table)
</read_first>
<behavior>
Creates a new file
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md`
with:
- Frontmatter modeled on Phase 2 VERIFICATION.md (status: passed,
score: 9/9 SPEC §10 criteria, overrides_applied: 3,
override_notes: 3 entries [§10 #4, §10 #5, §10 #8], human_verification:
1 entry [§10 #9])
- Goal block: verbatim from ROADMAP.md Phase 3 entry (the absorbed
Phase-2 scope phrasing)
- Per-Requirement / Per-Criterion Scorecard: 9 rows (one per SPEC §10
criterion), each citing Phase + Plan + commit + harness assertion
- Cross-Cutting Gates table: 6 rows from Task 1 results (vitest + UAT
33-driver count + Tier-1 grep 12/12 + bundle gates + tsc + harness
manifest/i18n)
- T5 override block in frontmatter for §10 #4 + #5 + #8 PARTIAL
(citing saved memory feedback-trust-harness-over-manual-uat.md
+ D-P3-02 charter for #8)
- human_verification block in frontmatter for §10 #9 (citing
D-P3-04 + RESEARCH Pitfall 2 + the operator chrome://memory-internals
instructions verbatim)
- Operator-Empirical Acks table (empty until operator runs §10 #9
chrome://memory-internals check; placeholder row with awaited status)
- Forward-Looking Deferred Items table pulled VERBATIM from
CONTEXT.md `<deferred>` section (10 items: rrweb v2, programmatic
RAM, REQ-password-confidentiality v2, audit P1 polish, ffprobe
flakes, getDisplayMedia cursor, dark-surface logo, setimmediate
polyfill, ROADMAP backfill)
</behavior>
<action>
1. Use the Write tool to create
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md`
with the content below. Substitute placeholders marked `<...>` with
values from Task 1's recorded results + the harness run output.
IMPORTANT: lines reading `<!-- YAML_DELIM ... -->` in the template below MUST be written as bare `---` lines in the new file. They are sentinels only — the SDK frontmatter parser would otherwise mis-detect the embedded VERIFICATION.md frontmatter as the plan's own frontmatter. After substitution, the new file MUST have exactly 2 bare `---` lines (frontmatter open + close); no others.
2. The file content (full template):
```markdown
<!-- YAML_DELIM (write `---` here when copying to the new file) -->
phase: 03-spec-10-smoke-verification-dom-event-log-verification
verified: <ISO-8601 timestamp with Z; capture at file-write time, e.g. 2026-05-20T19:00:00Z>
status: passed
score: 9/9 SPEC §10 criteria
overrides_applied: 3
override_notes:
- dimension: "SPEC §10 #4 — rrweb DOM event capture on typical pages"
initial_status: "UNCERTAIN (human_needed candidate — would otherwise require operator UAT on form/table/modal page)"
override_to: "VERIFIED"
rationale: |
User explicit delegation 2026-05-20 (saved memory feedback-trust-harness-over-manual-uat.md):
automation covers what automation can cover. Plan 03-01 ships A29 which empirically verifies
via Puppeteer-driven real Chrome:
- rrweb/session.json contains > 0 events (A29.2)
- EventType.Meta (=4) present (A29.3)
- EventType.FullSnapshot (=2) present (A29.4)
- EventType.IncrementalSnapshot (=3) present (A29.5)
The probe HTML at tests/uat/extension-page-harness.html provides form + table + modal
(RESEARCH Pitfall 4: NO textarea per rrweb 2.0.0-alpha.4 issue #1596). Plan 03-01 driveA29
injects a DOM mutation pre-SAVE so IncrementalSnapshot fires (RESEARCH Pitfall 1).
A29 GREEN: <commit hash from Plan 03-01 SUMMARY>. Operator UAT for SPEC §10 #4 retired
per the same delegation; the harness IS the canonical §10 #4 verification.
- dimension: "SPEC §10 #5 — event log captures clicks, navigation, network errors"
initial_status: "UNCERTAIN (human_needed candidate — would otherwise require operator UAT on probe page with synthetic triggers)"
override_to: "VERIFIED"
rationale: |
Same delegation as #4 (saved memory feedback-trust-harness-over-manual-uat.md). Plan 03-02
ships A30 which verifies all 5 UserEvent.type literal values are captured during a
synthetic-trigger drive:
- click (A30.2): .click() on #probe-submit
- input (A30.3): set #probe-email.value + dispatchEvent
- navigation (A30.4): history.pushState (intercepted at src/content/index.ts:121)
- js_error (A30.5): window.dispatchEvent(new ErrorEvent)
- network_error (A30.6): fetch(https://example.com/<404-path>) (production interception at line 167)
All 5 triggers happen on the harness page; no new tabs opened (parity with the Plan 01-13
Approach B pattern). A30 GREEN: <commit hash from Plan 03-02 SUMMARY>.
- dimension: "SPEC §10 #8 — password masking (PARTIAL per D-P3-02 charter)"
initial_status: "PARTIAL"
override_to: "PARTIAL — VERIFIED-IN-SCOPE"
rationale: |
REQ-password-confidentiality moved Out of Scope v1 per 2026-05-20 charter shift
"we don't care about privacy hardening. At least here." (D-P3-02). Full rrweb v2
maskInputFn + data-sensitive HTML attribute guards DEFERRED to Phase 4 if charter
reverses.
Existing minimum at src/content/index.ts:82 (`if (target.type === 'password') return;`)
is VERIFIED by Plan 03-03 A31:
- Page-side types SENTINEL='secret-do-not-log-123' into #probe-password
+ dispatches input event (fires production listener at line 78)
- Host-side asserts:
(a) A31.2 — 0 UserEvent entries contain SENTINEL in their .value field
(b) A31.3 — 0 UserEvent entries have target === '#probe-password'
Both checks GREEN proves the line-82 filter early-returned BEFORE addUserEvent.
A31 GREEN: <commit hash from Plan 03-03 SUMMARY>.
Mark PARTIAL (not VERIFIED-FULL) because rrweb session.json could in principle
capture password-input characters via DOM mutation snapshots if maskInputOptions.password
ever regressed. Production wiring at src/content/index.ts:306 sets `password: true`
(rrweb v2 alpha.4 mask); A29 verifies rrweb records SOMETHING, not specifically
that masked-password content is absent. Phase 4 candidate task: extend A31 to also
grep rrweb/session.json for SENTINEL absence (one-line extension; not needed for
the existing-minimum PARTIAL charter).
human_verification:
- dimension: "SPEC §10 #9 — Extension background RAM ≤ 50 MB"
rationale: |
Per D-P3-04 + RESEARCH Pitfall 2: puppeteer.Page.metrics() is PAGE-REALM ONLY.
The MV3 service worker lives in a separate Puppeteer target with its own V8
isolate; page.metrics() does NOT aggregate across workers/iframes. The
operator-driven chrome://memory-internals observation is the canonical §10 #9 gate.
Plan 03-04 ships A32 (Page.metrics scaffolding; UAT count 32 → 33 GREEN). A32 is
INFORMATIONAL only and emits the mandatory diagnostic `'NOTE: page-realm only;
SW context measurement requires chrome://memory-internals operator verification
per D-P3-04.'` on every run.
Operator verification steps (~3 min):
1. Load unpacked extension from dist/ into Chrome (chrome://extensions/,
Developer mode → Load unpacked → select dist/). Expected: no errors.
2. Start a recording (click Mokosh toolbar icon → "Entire screen").
3. Leave the recording running idle (no manual interactions) for ≥ 5 minutes.
4. Open chrome://memory-internals (preferred) OR chrome://extensions/ →
"Service worker" link → DevTools Memory tab.
5. Find the Mokosh extension entry. Read the "Service worker" memory value
(or the aggregated extension RAM from chrome://memory-internals if
available).
6. Expected: total < 50 MB. If > 50 MB, route via /gsd-debug per
feedback-gsd-ceremony-for-fixes.md (NO hot-edits).
Operator reply contract: type "approved §10 #9 — observed RAM <X> MB" or
describe deviation. The Operator-Empirical Acks table below records the result.
<!-- YAML_DELIM (write `---` here when copying to the new file) -->
# Phase 3: SPEC §10 smoke verification + DOM/event-log verification — Verification Report
**Phase Goal:** All 9 SPEC §10 acceptance criteria pass against an unpacked
load of the build into a real Chrome instance. Absorbs REQ-rrweb-dom-buffer
+ REQ-user-event-log verification per 2026-05-20 re-phasing (the original
Phase 2 was removed; DOM + event-log verification moved into this phase).
**Verified:** <ISO-8601 timestamp; same as frontmatter>
**Status:** passed (3 overrides applied — see override_notes; 1 entry in
human_verification block for SPEC §10 #9 RAM ceiling per D-P3-04 charter)
## Goal Achievement
### SPEC §10 Acceptance Criteria — Per-Criterion Scorecard
| # | Criterion (SPEC §10 verbatim) | Phase Owner | Evidence | Status |
|----|------------------------------------------------------------------------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|
| 1 | Extension installs in Chrome without errors | Phase 1 | Plan 01-12 closure + brand-fit operator ack 2026-05-20 "all good" (commit f319c7d); tests/build/no-remote-fonts.test.ts + tests/i18n/manifest-i18n.test.ts GREEN | PASS |
| 2 | Video buffer runs continuously on any tab | Phase 1 | Plan 01-07 closure + A2/A11 harness GREEN (35s buffer-continuity wait); tests/fixtures/last_30sec.webm ffprobe exit 0 | PASS |
| 3 | Buffer always contains no more than 30 seconds of video | Phase 1 | src/offscreen/recorder.ts:52-58 MAX_SEGMENTS=3 × 10s = 30s window; verified via gsd-verifier audit 2026-05-20 | PASS |
| 4 | rrweb records DOM events without errors on typical pages | **Phase 3** | **Plan 03-01 A29 GREEN** — 4 EventType-enum checks against rrweb/session.json from probe-HTML-driven archive (Meta + FullSnapshot + IncrementalSnapshot + count > 0) | PASS (override) |
| 5 | Event log captures clicks, navigation, and network errors | **Phase 3** | **Plan 03-02 A30 GREEN** — 5 UserEvent.type presence checks against logs/events.json (click + input + navigation + js_error + network_error) | PASS (override) |
| 6 | Archive download to "Downloads" in < 5 seconds | Phase 2 | Plan 02-04 A25 GREEN — performance.now() bookend + downloadsDir mtime delta both < 5000 ms | PASS |
| 7 | Archive opens; last_30sec.webm plays back in a browser | Phase 1+2 | Plan 01-08 webm-remux (single EBML) + operator empirical Chrome playback 2026-05-15; Plan 02-04 A28 GREEN — 5-entry zip-layout set-equality | PASS |
| 8 | Passwords do not appear in the log or rrweb snapshots | **Phase 3** | **Plan 03-03 A31 GREEN** — sentinel absence from logs/events.json (2 negative-assertion checks); PARTIAL per D-P3-02 charter (Out of Scope v1) | PARTIAL (override) |
| 9 | Extension RAM consumption does not exceed 50 MB in the background | Phase 3+operator | **Plan 03-04 A32 GREEN** (informational; page-realm only); **operator chrome://memory-internals verification AWAITED** per D-P3-04 | HUMAN_NEEDED |
### Phase 3 Plan Map
| Plan | Subject | Wave | Outcome |
|-----------|--------------------|------|------------------------------------------------------------------------|
| 03-01 | rrweb DOM (#4) | 1 | Probe HTML appended + A29 GREEN (4 EventType-enum checks) |
| 03-02 | event log (#5) | 2 | A30 GREEN (5 UserEvent.type presence checks) |
| 03-03 | password (#8) | 2 | A31 GREEN (2 negative-assertion checks; PARTIAL per D-P3-02 charter) |
| 03-04 | RAM (#9) | 2 | A32 GREEN (informational; page-realm only); operator instructions in this VERIFICATION.md |
| 03-05 | aggregator | 3 | THIS document; marker flips in REQUIREMENTS/ROADMAP/STATE |
## Cross-Cutting Gates
| Gate | Evidence | Status |
|---|---|---|
| vitest | 28 files / **171 tests / 171 GREEN** (preserved from Phase 2; no new tests in Plans 03-01..04 — all new assertions live in UAT harness tier) | PASS |
| UAT harness | 33 drivers (A0 grep + A1..A14 Phase 1 + A15..A17 Plan 01-10 + A18..A22 Plan 01-12 + A23 Plan 01-14 + A24..A28 Plan 02-04 + **A29..A32 Plan 03-01..04**); `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 33/33 GREEN | PASS |
| Tier-1 grep gate | **12 FORBIDDEN_HOOK_STRINGS** (unchanged from Plan 02-04 baseline); `dist/` contains 0 hits for each | PASS |
| Pre-checkpoint bundle gates | 0 `googleapis`/`https://fonts` in dist; 0 test-hook leaks; SW CSP: 1 documented exception (setimmediate polyfill, pre-existing); 0 Buffer/require in SW chunk; DOM globals only behind typeof guards; manifest + i18n + locale-parity tests GREEN | PASS |
| tsc | `npx tsc --noEmit` exit 0 | PASS |
| Phase 3 surface `as any` / `@ts-ignore` | 0 new instances in tests/uat/lib/harness-page-driver.ts beyond the inherited eslint-disable on the canonical `(window as any).__mokoshHarness` access (matches Plan 02-04 baseline) | PASS |
## Operator-Empirical Acks (verbatim + commit refs)
| Date | Plan | Operator response | Commit |
|---|---|---|---|
| <YYYY-MM-DD> | 03 (§10 #9 RAM operator check per D-P3-04) | <verbatim quote when received; e.g. "approved §10 #9 — observed RAM 32 MB"> | <commit> |
*Placeholder row — operator runs chrome://memory-internals per the
human_verification block above; row filled when ack lands.*
## Forward-Looking Deferred Items (NOT gaps)
| Item | Owner | Source |
|---|---|---|
| rrweb v2 stable upgrade | Phase 4 | D-P3-03 defer rationale (alpha-pin stable across 9 plans + 29/29 UAT GREEN) |
| Programmatic RAM measurement upgrade (Page.metrics enhancement OR chrome.devtools Memory API) | Phase 4 | D-P3-04 defer rationale (Pitfall 2: SW context separate target) |
| REQ-password-confidentiality v2 candidate (rrweb v2 maskInputFn + data-sensitive guards) | Phase 4 | D-P3-02 defer rationale (charter shift 2026-05-20) |
| Audit P1 #11/#14/#15 polish (fetch Request→[object Request], navigation URL tracking, rrweb timestamp semantics) | Phase 4 | Pre-existing audit backlog |
| 2 pre-existing ffprobe/ffmpeg vitest flakes | Phase 4 | Plan 01-13 + Phase 1 VERIFICATION.md residual |
| getDisplayMedia cursor visibility refinement | Phase 4 | Plan 01-07 operator observation 2026-05-15 |
| Dark-surface logo contrast | Phase 4 | Plan 01-10 operator observation 2026-05-20 |
| setimmediate polyfill `new Function` in SW chunk | Phase 4 | Plan 01-12 disclosure; deferred-items.md |
| ROADMAP backfill for Plans 01-08..01-13 entries | Phase 4 docs polish | Plan 01-13 SUMMARY flag #4 |
| A31 extended grep on rrweb/session.json for sentinel absence (one-line extension) | Phase 4 candidate | Plan 03-03 PARTIAL rationale acknowledges this gap if charter reverses |
## Re-Verification Status
| Aspect | Status |
|---|---|
| Phase 1 + 2 baselines | UNCHANGED — Plans 03-01..04 add only test-side harness extensions; no production-code edits |
| Production bundle invariant | PRESERVED — FORBIDDEN_HOOK_STRINGS at 12 entries; bundle gates 6/6 PASS |
| vitest baseline | PRESERVED — 171/171 GREEN (no new unit tests; no regressions) |
| UAT harness | EXTENDED — 29 → 33 GREEN (+4: A29 rrweb DOM, A30 event log, A31 password absence, A32 RAM scaffolding) |
| DEC-011 + Amendment 1 (`tabs` permission) | PRESERVED — Plan 03-* introduces zero new permissions |
<!-- YAML_DELIM (write `---` here when copying to the new file) -->
*Verified: <ISO timestamp> by Claude (gsd-verifier — Phase 3 closure aggregator)*
*Verifier: Plan 03-05 (this document)*
```
3. Replace `<ISO-8601 timestamp with Z>` with the current ISO timestamp at write time (e.g. `2026-05-20T19:00:00Z`).
4. Replace `<commit hash from Plan 03-NN SUMMARY>` placeholders with the actual commit hashes once Plans 03-01..04 land. If executor is running this BEFORE those commits exist, leave placeholders and note in the SUMMARY that the hashes are placeholders pending commit.
5. The placeholder operator row in the Operator-Empirical Acks table stays empty until the operator runs chrome://memory-internals. Plan 03-05 closure does NOT block on the operator ack — the orchestrator advances Phase 3 once the documentation marker flips land (Task 3); the operator ack lands as an addendum commit when received.
</action>
<verify>
<automated>test -f .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md &amp;&amp; L=$(wc -l &lt; .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md) &amp;&amp; test "$L" -ge 120 &amp;&amp; grep -q 'overrides_applied: 3' .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md &amp;&amp; grep -q 'human_verification:' .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md &amp;&amp; grep -q 'score: 9/9 SPEC §10 criteria' .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md</automated>
</verify>
<acceptance_criteria>
- File `.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md` exists with line count >= 120.
- Frontmatter has `phase`, `verified`, `status: passed`, `score: 9/9 SPEC §10 criteria`, `overrides_applied: 3`, `override_notes` list with 3 entries, `human_verification` list with 1 entry.
- The per-criterion scorecard table has exactly 9 rows (one per §10 #1..#9).
- §10 #4 row cites "Plan 03-01 A29 GREEN" + EventType enum checks.
- §10 #5 row cites "Plan 03-02 A30 GREEN" + 5 UserEvent.type values.
- §10 #8 row marked PARTIAL with D-P3-02 + A31 citation.
- §10 #9 row marked HUMAN_NEEDED with D-P3-04 + chrome://memory-internals instructions.
- Cross-Cutting Gates table has at least 6 rows including UAT harness "33 drivers" entry.
- Forward-Looking Deferred Items table has at least 10 rows pulling from CONTEXT.md `<deferred>` block.
</acceptance_criteria>
<done>
03-VERIFICATION.md captures the 9-criterion sweep + T5 overrides for
§10 #4/#5/#8 + human_verification for §10 #9 + the cross-cutting
gates + the deferred items list. Plan 03-05 deliverable complete;
Task 3 next flips the project trackers.
</done>
</task>
<task type="auto" tdd="false">
<name>Task 3: Flip REQUIREMENTS.md + ROADMAP.md + STATE.md markers (Phase 3 closure)</name>
<files>.planning/REQUIREMENTS.md, .planning/ROADMAP.md, .planning/STATE.md</files>
<read_first>
- .planning/REQUIREMENTS.md (current state; REQ-rrweb-dom-buffer at line 54; REQ-user-event-log at line 66; REQ-install-clean at line 178 — already Complete; Traceability table at line 263)
- .planning/ROADMAP.md (current state; Phase 3 entry at line 167; Progress table at line 268)
- .planning/STATE.md (current state; progress.completed_phases: 2; current_focus at line 26; Session Continuity at line 217)
- .planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md (closure precedent for marker-flip shape; commit `586836f` referenced in 01-VERIFICATION.md line 122)
</read_first>
<behavior>
Atomic 3-file edit:
`.planning/REQUIREMENTS.md`:
- Line 54 `[ ]``[x]` for REQ-rrweb-dom-buffer; append closure citation similar to Phase 2 closures (Plan 03-01 A29 GREEN; ISO date; UAT 33/33 GREEN).
- Line 66 `[ ]``[x]` for REQ-user-event-log; append Plan 03-02 A30 GREEN citation.
- Traceability table: REQ-rrweb-dom-buffer row Status `Pending``Complete (Phase 3 closure; Plan 03-01 A29 GREEN — 4 EventType-enum checks)`.
- Traceability table: REQ-user-event-log row Status `Pending``Complete (Phase 3 closure; Plan 03-02 A30 GREEN — 5 UserEvent.type presence checks)`.
- Append a closure footer line (mirror the existing 2026-05-20 footer): `*Updated 2026-05-20 — Phase 3 closed (REQ-rrweb-dom-buffer + REQ-user-event-log marked Complete; §10 #8 PARTIAL per D-P3-02 + A31 GREEN; §10 #9 awaits operator chrome://memory-internals per D-P3-04; UAT 33/33 GREEN). VERIFICATION.md at .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md.*`
`.planning/ROADMAP.md`:
- Line 37 (Phase 3 row in the Phases list): `[ ]``[x]`. Append closure citation:
`**CLOSED 2026-05-20** via gsd-verifier audit GREEN (9/9 SPEC §10 criteria via Plans 03-01..05; 3 overrides applied for §10 #4/#5/#8 PARTIAL per T5; §10 #9 human_verification awaited per D-P3-04). UAT harness 29 → 33 GREEN; vitest 171/171 preserved; bundle gates 6/6 PASS.`
- Lines 167+ (Phase Details for Phase 3): add a `**Plans:** 5 plans (03-01 through 03-05)` line followed by a bullet list mirroring Phase 1 + Phase 2 plan lists:
- `[x] 03-01-PLAN.md — Plan 03-01 rrweb DOM verification harness extension (A29 GREEN; SPEC §10 #4)`
- `[x] 03-02-PLAN.md — Plan 03-02 event-log verification harness extension (A30 GREEN; SPEC §10 #5)`
- `[x] 03-03-PLAN.md — Plan 03-03 password-filter PARTIAL verification (A31 GREEN; SPEC §10 #8 PARTIAL per D-P3-02)`
- `[x] 03-04-PLAN.md — Plan 03-04 RAM ceiling best-effort scaffolding (A32 GREEN; SPEC §10 #9 best-effort per D-P3-04)`
- `[x] 03-05-PLAN.md — Plan 03-05 §10 sweep VERIFICATION.md aggregator + REQUIREMENTS/ROADMAP/STATE marker flips`
- Progress table (line 268+): Phase 3 row Plans Complete `0/TBD``5/5`; Status `Not started``CLOSED 2026-05-20`; Completed column → `2026-05-20 via gsd-verifier`.
`.planning/STATE.md`:
- Frontmatter: `status: ready_to_plan``phase_3_complete` (or whatever matches existing values; mirror Phase 1 closure status pattern).
- Frontmatter: `progress.completed_phases: 2``3`; `progress.total_plans: 18``23`; `progress.completed_plans: 18``23`; `progress.percent: 50``75`.
- Frontmatter: `last_updated` to current ISO-8601 timestamp.
- Frontmatter: `stopped_at` to "Phase 3 closed; 33/33 UAT GREEN; operator §10 #9 chrome://memory-internals ack awaited per D-P3-04" (or similar; matches the Phase 1/2 stopped_at phrasing).
- Body: under "Current Position", add a Phase 3 closure section mirroring the Plan 01-10 closure block in 01-VERIFICATION.md (~10 lines: closure date + outcome + Plan 03-01..04 commits + UAT count delta + bundle gates result + harness inventory unchanged at 12).
- Body: under "Session Continuity", add a new top-of-list entry: `Last session: <ISO timestamp>` + `Stopped at: Phase 3 closed via gsd-verifier aggregator (9/9 SPEC §10 with 3 overrides + 1 human_verification); operator §10 #9 chrome://memory-internals ack awaited per D-P3-04` + `Resume file: .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md`.
</behavior>
<action>
1. Open `.planning/REQUIREMENTS.md`. Use the Edit tool for surgical changes:
a. Locate line 54 (current text: `- [ ] **REQ-rrweb-dom-buffer**: ...`) — change `[ ]` to `[x]`. Append a closure note in the same style as REQ-screenshot-on-export's line 81-85 (multiline "COMPLETED Phase X" block):
```markdown
COMPLETED Phase 3 (2026-05-20): Plan 03-01 ships A29 — UAT harness empirical
verification that rrweb's `record()` (wired at src/content/index.ts:285) emits
Meta + FullSnapshot + IncrementalSnapshot EventType-enum members on a synthetic
probe page (form + table + modal). 4 EventType checks GREEN; rrweb/session.json
from the assembled archive contains > 0 events. Probe HTML in
tests/uat/extension-page-harness.html (NO textarea per rrweb 2.0.0-alpha.4
issue #1596). UAT harness 33/33 GREEN.
```
b. Locate line 66-67 (current text: `- [ ] **REQ-user-event-log**: ...`) — change `[ ]` to `[x]`. Append:
```markdown
COMPLETED Phase 3 (2026-05-20): Plan 03-02 ships A30 — UAT harness empirical
verification of all 5 UserEvent.type literal values (click, input, navigation,
js_error, network_error) via synthetic browser-event triggers (page.click on
#probe-submit + dispatchEvent('input') on #probe-email + history.pushState +
window.dispatchEvent(ErrorEvent) + fetch to 404 endpoint). 6-check A30 GREEN;
logs/events.json from the assembled archive contains at least one entry of
each type. UAT harness 33/33 GREEN.
```
c. Locate the Traceability table (around line 263+). Find the rows for `REQ-rrweb-dom-buffer` and `REQ-user-event-log` and update the Status column:
- REQ-rrweb-dom-buffer: `Pending (...)``Complete 2026-05-20 (Phase 3 Plan 03-01 A29 GREEN — 4 EventType-enum checks against rrweb/session.json from probe-HTML-driven archive)`
- REQ-user-event-log: `Pending (...)``Complete 2026-05-20 (Phase 3 Plan 03-02 A30 GREEN — 5 UserEvent.type presence checks against logs/events.json)`
d. Append a new closure footer at the end of the file (after the existing footer lines starting around line 297):
```markdown
*Updated 2026-05-20 — Phase 3 closed (REQ-rrweb-dom-buffer + REQ-user-event-log marked Complete via gsd-verifier audit; §10 #8 PARTIAL per D-P3-02 + A31 GREEN existing-minimum verification; §10 #9 awaits operator chrome://memory-internals per D-P3-04 + A32 informational scaffolding shipped; UAT harness 29 → 33 GREEN). VERIFICATION.md at .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md.*
```
2. Open `.planning/ROADMAP.md`. Use the Edit tool:
a. Line 37 (Phase 3 row in the Phases list):
`- [ ] **Phase 3: SPEC §10 smoke verification** — End-to-end install-and-record-and-export pass...`
Change `[ ]` to `[x]` and append a closure citation:
```markdown
**CLOSED 2026-05-20** via gsd-verifier audit GREEN (9/9 SPEC §10 criteria
verified via Plans 03-01..05; 3 T5 overrides applied for §10 #4/#5/#8 PARTIAL
per saved memory feedback-trust-harness-over-manual-uat.md; §10 #9
human_verification awaited per D-P3-04 with A32 informational scaffolding).
UAT harness 29 → 33 GREEN (+A29 rrweb DOM + A30 event log + A31 password
absence + A32 RAM scaffolding); vitest 171/171 preserved; Tier-1 FORBIDDEN_HOOK_STRINGS
unchanged at 12; bundle gates 6/6 PASS. VERIFICATION.md at
.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md.
```
b. Lines 215+ (Phase 3 Plans subsection): replace `**Plans**: TBD` with a 5-bullet plan list:
```markdown
**Plans**: 5 plans (03-01 through 03-05).
- [x] 03-01-PLAN.md — rrweb DOM verification harness extension (A29 GREEN; SPEC §10 #4; REQ-rrweb-dom-buffer)
- [x] 03-02-PLAN.md — event-log verification harness extension (A30 GREEN; SPEC §10 #5; REQ-user-event-log)
- [x] 03-03-PLAN.md — §10 #8 password-filter PARTIAL verification (A31 GREEN; D-P3-02 charter — existing-minimum at src/content/index.ts:82)
- [x] 03-04-PLAN.md — §10 #9 RAM ceiling best-effort + Page.metrics scaffolding (A32 GREEN; D-P3-04 charter — operator chrome://memory-internals canonical)
- [x] 03-05-PLAN.md — §10 sweep VERIFICATION.md aggregator + REQUIREMENTS/ROADMAP/STATE marker flips
```
c. Progress table (around line 268+): update the Phase 3 row.
- Plans Complete: `0/TBD``5/5`
- Status: `Not started (...)``**CLOSED 2026-05-20** via gsd-verifier audit GREEN (9/9 §10 criteria; 3 overrides + 1 human_verification)`
- Completed column: empty → `2026-05-20`
3. Open `.planning/STATE.md`. Use the Edit tool:
a. Frontmatter: update progress.
- `status: ready_to_plan``phase_3_complete`
- `progress.completed_phases: 2``3`
- `progress.total_plans: 18``23`
- `progress.completed_plans: 18``23`
- `progress.percent: 50``75`
- `last_updated: "2026-05-20T16:05:48.025Z"` → current ISO timestamp
- `stopped_at: ...``Phase 3 closed; UAT 33/33 GREEN; operator §10 #9 chrome://memory-internals ack awaited per D-P3-04`
b. Under "Current Position" (around line 28-37), add a new Phase 3 closure block as the TOP item (above the existing Phase 1 closure blocks), mirroring the Plan 01-10 closure style:
```markdown
### Phase 3 closure (2026-05-20)
- Plans 03-01..05 landed end-to-end (5 plans across 3 waves: Wave 1 Plan 03-01 rrweb DOM probe + assertA29 + driveA29; Wave 2 Plans 03-02..04 sequential per RESEARCH Pitfall 6 [shared harness file overlap]; Wave 3 Plan 03-05 §10 sweep VERIFICATION.md aggregator + marker flips)
- 5 plan-wave commits: <hash 03-01> (rrweb DOM A29) → <hash 03-02> (event log A30) → <hash 03-03> (password absence A31) → <hash 03-04> (RAM scaffolding A32) → <hash 03-05> (VERIFICATION.md + REQUIREMENTS/ROADMAP/STATE flip)
- SUMMARYs at `.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-{01..05}-SUMMARY.md`
- **UAT harness 29 → 33 GREEN** (+4: A29 rrweb DOM 4-check + A30 event-log 6-check + A31 password absence 3-check + A32 RAM scaffolding 2-check + page-realm-only diagnostic)
- **Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12** (A29..A32 ride production surfaces: rrweb.record + content-script GET_RRWEB_EVENTS bridge + production input/click/navigation/fetch listeners + Page.metrics host-side CDP)
- **vitest 171/171 GREEN preserved** (no new unit tests in Plans 03-01..04; aggregator Plan 03-05 is documentation only)
- **Pre-checkpoint bundle gates 6/6 PASS** (build clean + SW CSP 1 documented exception + SW Node-globals 0 + DOM-globals typeof-guarded + Tier-1 SW-bundle-import GREEN + FORBIDDEN_HOOK_STRINGS 12 strings × 0 hits)
- **9/9 SPEC §10 criteria verified via gsd-verifier aggregator (Plan 03-05)** with 3 T5 overrides (#4 rrweb / #5 event log / #8 password PARTIAL per D-P3-02 charter) + 1 human_verification (#9 RAM per D-P3-04 + RESEARCH Pitfall 2 — Page.metrics is page-realm only; SW context separate target)
- **Operator §10 #9 chrome://memory-internals ack AWAITED** per D-P3-04 charter; A32 informational scaffolding shipped meanwhile (emits 'NOTE: page-realm only; SW context excluded' diagnostic on every run); chrome://memory-internals operator instructions documented verbatim in 03-VERIFICATION.md human_verification block
- **REQ-rrweb-dom-buffer + REQ-user-event-log markers flipped Complete** in REQUIREMENTS.md Traceability table; ROADMAP.md Phase 3 row [x]; this STATE.md updated.
```
c. Update "Current focus" (around line 26): `Current focus: Phase 3 — SPEC §10 smoke verification...``Current focus: Phase 3 CLOSED 2026-05-20; Phase 4 (Harden + clean up, optional) candidate next`.
d. Update "Session Continuity" (around line 217+): prepend a new entry at the top:
```markdown
Last session: <ISO timestamp>
Stopped at: Phase 3 closed via gsd-verifier aggregator (9/9 SPEC §10 with 3 overrides + 1 human_verification); operator §10 #9 chrome://memory-internals ack awaited per D-P3-04
Resume file: .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md
```
(Preserve the existing "Last session" / "Prior session" / "Earlier session" entries below.)
4. Run a final sanity-grep to confirm marker flips:
`grep -E "^- \[x\] \*\*REQ-rrweb-dom-buffer" .planning/REQUIREMENTS.md` returns 1
`grep -E "^- \[x\] \*\*REQ-user-event-log" .planning/REQUIREMENTS.md` returns 1
`grep -E "^- \[x\] \*\*Phase 3" .planning/ROADMAP.md` returns 1
`grep -E "^progress:$" .planning/STATE.md` followed by `completed_phases: 3` in the next few lines.
</action>
<verify>
<automated>grep -E "^- \[x\] \*\*REQ-rrweb-dom-buffer" .planning/REQUIREMENTS.md | grep -v '^#' | grep -q REQ-rrweb-dom-buffer &amp;&amp; grep -E "^- \[x\] \*\*REQ-user-event-log" .planning/REQUIREMENTS.md | grep -v '^#' | grep -q REQ-user-event-log &amp;&amp; grep -E "^- \[x\] \*\*Phase 3" .planning/ROADMAP.md | grep -v '^#' | grep -q "Phase 3" &amp;&amp; grep -q "completed_phases: 3" .planning/STATE.md</automated>
</verify>
<acceptance_criteria>
- `grep -c '^- \[x\] \*\*REQ-rrweb-dom-buffer' .planning/REQUIREMENTS.md` returns exactly 1.
- `grep -c '^- \[x\] \*\*REQ-user-event-log' .planning/REQUIREMENTS.md` returns exactly 1.
- `grep -c '^- \[x\] \*\*Phase 3' .planning/ROADMAP.md` returns exactly 1.
- `grep -c 'completed_phases: 3' .planning/STATE.md` returns exactly 1.
- `grep -c 'Phase 3 closure (2026-05-20)' .planning/STATE.md` returns exactly 1.
- `grep -c 'CLOSED 2026-05-20' .planning/ROADMAP.md` returns >=1 (Phase 3 entry).
- 03-VERIFICATION.md still exists (Task 2 deliverable preserved).
- All 4 files (`03-VERIFICATION.md` + `REQUIREMENTS.md` + `ROADMAP.md` + `STATE.md`) are staged together in the closure commit (atomic marker flip per Phase 1 precedent commit `586836f`).
</acceptance_criteria>
<done>
Phase 3 marker-flip complete. Project trackers reflect the closure:
REQUIREMENTS Traceability table shows REQ-rrweb-dom-buffer +
REQ-user-event-log Complete; ROADMAP Phase 3 row [x]; STATE
progress.completed_phases=3 + percent=75 + current focus advanced.
Operator §10 #9 chrome://memory-internals ack is the only remaining
Phase 3 item; it lands as an addendum commit once received.
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Documentation files ↔ git history | All edits are tracked via git; rollback via `git revert` if marker flip is premature |
| 03-VERIFICATION.md ↔ downstream readers | Aggregator must be HONEST about overrides_applied count + human_verification residue — misrepresentation here propagates into Phase 4 planning + operator decisions |
| dist-test/ ↔ dist/ | Plan 03-05 changes ZERO code/bundle surfaces (documentation only); production invariant unchanged |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-05-01 | Repudiation | VERIFICATION.md frontmatter `status: passed` misleads if any §10 criterion actually regressed | mitigate | Pre-checkpoint bundle gates (Task 1) re-verify the 6/6 inventory before VERIFICATION.md is written; if ANY gate fails, executor STOPS and routes via /gsd-debug per feedback-gsd-ceremony-for-fixes.md. The override_notes block carries explicit rationale + commit hashes for every override. |
| T-03-05-02 | Tampering | A bad marker flip in REQUIREMENTS.md hides a real gap | mitigate | Task 3 acceptance criteria check each REQ line + Traceability table + ROADMAP row via grep; failure to flip cleanly is visible at acceptance time. 03-VERIFICATION.md remains the canonical source; REQUIREMENTS/ROADMAP/STATE point to it. |
| T-03-05-03 | Information Disclosure | Sentinel value (from Plan 03-03 A31) accidentally appears in 03-VERIFICATION.md as evidence text | accept | Sentinel value `secret-do-not-log-123` is a documented probe-only string; appearing in plan/verification text is fine. It is NOT a real secret. |
| T-03-05-04 | Elevation of Privilege | Operator interprets §10 #9 human_verification entry as optional + skips it | mitigate | 03-VERIFICATION.md's human_verification block contains explicit step-by-step operator instructions + reply contract; Plan 03-05 closure does NOT block on the operator ack (per task design), but the ack remains documented as required for §10 #9 closure-by-charter. Operator-Empirical Acks table includes a placeholder row that's visibly EMPTY until the ack lands. |
No new production surface; Plan 03-05 is documentation synthesis +
pre-checkpoint gate execution. The threat surface is entirely
documentation-fidelity; mitigation is mechanical (grep gates +
acceptance criteria).
</threat_model>
<verification>
- `npm run build` exits 0; pre-checkpoint bundle gates 6/6 PASS (per Task 1).
- `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` exits 0 with 33/33 GREEN (sanity re-run; uses existing dist/).
- `03-VERIFICATION.md` line count >= 120; frontmatter has overrides_applied: 3 + human_verification list with 1 entry.
- REQUIREMENTS.md + ROADMAP.md + STATE.md marker flips visible via grep (see Task 3 acceptance).
- All 4 files staged for the closure commit.
</verification>
<success_criteria>
- 9/9 SPEC §10 criteria documented in 03-VERIFICATION.md scorecard with Phase + Plan + commit citations.
- 3 T5 overrides applied (§10 #4 / #5 / #8 PARTIAL) with explicit saved-memory + charter citations.
- 1 human_verification entry (§10 #9 RAM) with operator chrome://memory-internals instructions verbatim.
- Pre-checkpoint bundle gates 6/6 PASS.
- REQ-rrweb-dom-buffer + REQ-user-event-log markers flipped Complete; ROADMAP Phase 3 row [x]; STATE progress.completed_phases=3.
- vitest baseline preserved (171/171); UAT 33/33 GREEN; FORBIDDEN_HOOK_STRINGS unchanged at 12.
- Operator §10 #9 ack workflow surfaced via 03-VERIFICATION.md human_verification block + Operator-Empirical Acks placeholder row.
</success_criteria>
<output>
After completion, create
`.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-05-SUMMARY.md`
documenting:
- 9/9 §10 scorecard verified via Plans 03-01..04 + Phase 1 + Phase 2 evidence
- 3 overrides applied with explicit rationale
- 1 human_verification entry (§10 #9) — operator chrome://memory-internals workflow documented
- Marker flips committed atomically with 03-VERIFICATION.md in one closure commit (mirrors Phase 1 closure precedent `586836f`)
- Phase 3 retired; Phase 4 (Harden + clean up) candidate next per ROADMAP
- Forward-looking deferred items list carried into Phase 4 hardening scope
</output>

View File

@@ -1,17 +1,23 @@
---
phase: 03
slug: spec-10-smoke-verification-dom-event-log-verification
status: draft
nyquist_compliant: false
wave_0_complete: false
status: planner_filled
nyquist_compliant: true
wave_0_complete: true
created: 2026-05-20
filled_by: gsd-planner
filled_at: 2026-05-20
---
# Phase 03 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
**Phase 3 character:** This phase IS validation. The deliverables are harness assertions (A29+) + a §10 sweep VERIFICATION.md, not new production code. The per-task verification map below is filled in by the planner during Plans 03-01..05 creation (each plan task's `<automated>` block declares its verification command).
**Phase 3 character:** This phase IS validation. The deliverables are
harness assertions (A29..A32) + a §10 sweep VERIFICATION.md, not new
production code. The per-task verification map below is filled in by
the planner during Plans 03-01..05 creation (each plan task's
`<automated>` block declares its verification command).
---
@@ -23,7 +29,7 @@ created: 2026-05-20
| **Config file** | `vitest.config.ts` + `tests/uat/harness.test.ts` (orchestrator) |
| **Quick run command** | `npm test -- --run tests/<focused-file>.test.ts` |
| **Full suite command** | `npm test -- --run` (vitest) + `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` (UAT harness) |
| **Estimated runtime** | ~50s (vitest 171 tests) + ~95s (UAT harness 29 → ~34 assertions) ≈ 2.5 min full sweep |
| **Estimated runtime** | ~50s (vitest 171 tests) + ~110s (UAT harness 33 assertions) ≈ ~2.7 min full sweep |
---
@@ -32,7 +38,7 @@ created: 2026-05-20
- **After every task commit:** Run focused test command (vitest single-file OR `npm run test:uat -- --grep A<NN>` for harness)
- **After every plan wave:** Run full vitest + full UAT harness — both MUST be GREEN
- **Before `/gsd-verify-work 3`:** Full suite GREEN + pre-checkpoint bundle gates 6/6 PASS (per saved memory `feedback-pre-checkpoint-bundle-gates.md`)
- **Max feedback latency:** ~2.5 min (full sweep); ~10s (focused vitest); ~20s (focused UAT assertion)
- **Max feedback latency:** ~2.7 min (full sweep); ~10s (focused vitest); ~25s (focused UAT assertion)
---
@@ -40,21 +46,29 @@ created: 2026-05-20
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| _Filled by planner during Plans 03-01..05 creation_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | ⬜ pending |
| 03-01-01 | 03-01 | 1 | REQ-rrweb-dom-buffer | T-03-01-02 | Probe HTML appended without disturbing tokens.css link (preserves A18/A21) | bundle-gate (grep + build) | `grep -c 'id="probe-form"' tests/uat/extension-page-harness.html ; grep -c '<link rel="stylesheet" href="../../src/shared/tokens.css">' tests/uat/extension-page-harness.html ; ! grep -q '<textarea' tests/uat/extension-page-harness.html ; npm run build` | ✅ tests/uat/extension-page-harness.html exists | ⬜ pending |
| 03-01-02 | 03-01 | 1 | REQ-rrweb-dom-buffer | T-03-01-03 | A29 GREEN; FORBIDDEN_HOOK_STRINGS unchanged at 12 | harness | `npx tsc --noEmit && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` then verify stdout contains "UAT harness: 30/30" | ❌ Wave 1 (extension-page-harness.ts + harness-page-driver.ts + harness.test.ts to be appended) | ⬜ pending |
| 03-02-01 | 03-02 | 2 | REQ-user-event-log | T-03-02-01, T-03-02-02 | assertA30 page-side orchestrator dispatches 5 trigger types; tokens.css link still untouched | bundle-gate (tsc + grep) | `npx tsc --noEmit ; grep -c "assertA30" tests/uat/extension-page-harness.ts` | ❌ Wave 2 (assertA30 to be appended) | ⬜ pending |
| 03-02-02 | 03-02 | 2 | REQ-user-event-log | T-03-02-03 | A30 GREEN; 5 UserEvent.type literal values present in logs/events.json; Tier-1 inventory at 12 | harness | `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` then verify stdout contains "UAT harness: 31/31" | ❌ Wave 2 (driveA30 + drivers-array push to be appended) | ⬜ pending |
| 03-03-01 | 03-03 | 2 | N/A — §10 #8 PARTIAL per D-P3-02 | T-03-03-01 | assertA31 types SENTINEL into #probe-password and dispatches input event | bundle-gate (tsc + grep) | `npx tsc --noEmit ; grep -c 'A31_PASSWORD_SENTINEL' tests/uat/extension-page-harness.ts` | ❌ Wave 2 (assertA31 to be appended) | ⬜ pending |
| 03-03-02 | 03-03 | 2 | N/A — §10 #8 PARTIAL per D-P3-02 | T-03-03-04 | A31 GREEN; 2 negative-assertion checks pass (sentinel-absence + selector-target-absence) | harness | `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` then verify stdout contains "UAT harness: 32/32" AND A31 sentinel-containing count=0 | ❌ Wave 2 (driveA31 + drivers-array push to be appended) | ⬜ pending |
| 03-04-01 | 03-04 | 2 | N/A — §10 #9 best-effort per D-P3-04 | T-03-04-01, T-03-04-02 | A32 GREEN; page-realm-only diagnostic emitted; Tier-1 inventory at 12 | harness | `npx tsc --noEmit && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` then verify stdout contains "UAT harness: 33/33" AND "NOTE: page-realm only" diagnostic | ❌ Wave 2 (driveA32 + drivers-array push to be appended) | ⬜ pending |
| 03-05-01 | 03-05 | 3 | REQ-install-clean + REQ-rrweb-dom-buffer + REQ-user-event-log | T-03-05-01 | Pre-checkpoint bundle gates 6/6 PASS | bundle-gate (multi) | `npm run build ; npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts tests/background/sw-bundle-import.test.ts tests/i18n/ tests/build/` | ✅ All gate files already present from Phase 1+2 | ⬜ pending |
| 03-05-02 | 03-05 | 3 | REQ-rrweb-dom-buffer + REQ-user-event-log | T-03-05-01, T-03-05-02 | 03-VERIFICATION.md exists with 9/9 §10 scorecard + 3 overrides + 1 human_verification | docs (grep) | `test -f .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md && wc -l < ... | awk '$1 >= 120' && grep -q 'overrides_applied: 3' ... && grep -q 'human_verification:' ...` | ❌ Wave 3 (file to be created) | ⬜ pending |
| 03-05-03 | 03-05 | 3 | REQ-install-clean + REQ-rrweb-dom-buffer + REQ-user-event-log | T-03-05-02 | REQUIREMENTS / ROADMAP / STATE markers flipped Complete | docs (grep) | `grep -c '^- \[x\] \*\*REQ-rrweb-dom-buffer' .planning/REQUIREMENTS.md && grep -c '^- \[x\] \*\*Phase 3' .planning/ROADMAP.md && grep -c 'completed_phases: 3' .planning/STATE.md` | ✅ Files exist; markers to be flipped | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
**Planner instructions:** When creating each task in Plans 03-01..05, populate one row in this table with:
- `Task ID`: `03-XX-NN` format
- `Requirement`: REQ-rrweb-dom-buffer | REQ-user-event-log | REQ-install-clean | (or "N/A — verification surface only")
- `Test Type`: harness | unit | bundle-gate | manual-operator
- `Automated Command`: exact CLI invocation that proves the task succeeded
**Total task count: 10 tasks across 5 plans (Plans 03-01..04 each ship 2
tasks; Plan 03-05 ships 3 tasks).**
---
## Wave 0 Requirements
Phase 3 has no Wave 0 (no RED test scaffolds needed — verification phase extends existing harness; new assertions ride established Approach B pattern). All test infrastructure is already in place from Plan 02-04:
Phase 3 has no Wave 0 (no RED test scaffolds needed — verification phase
extends existing harness; new assertions ride established Approach B
pattern). All test infrastructure is already in place from Plan 02-04:
-`tests/uat/extension-page-harness.ts` — page-side assertA* host
-`tests/uat/lib/harness-page-driver.ts` — host-side driveA* host
@@ -65,7 +79,8 @@ Phase 3 has no Wave 0 (no RED test scaffolds needed — verification phase exten
-`tests/background/no-test-hooks-in-prod-bundle.test.ts` — FORBIDDEN_HOOK_STRINGS lockstep
- ✅ vitest 4.x + Puppeteer already in package.json
*Phase 3 inherits all infrastructure from Phase 1 + 2. wave_0_complete: true (no new infra needed).*
*Phase 3 inherits all infrastructure from Phase 1 + 2.
`wave_0_complete: true` (no new infra needed).*
---
@@ -73,19 +88,21 @@ Phase 3 has no Wave 0 (no RED test scaffolds needed — verification phase exten
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Extension RAM ≤ 50 MB in background | CON-ram-ceiling (NFR; SPEC §10 #9) | Puppeteer Page.metrics is page-realm only; SW heap requires `performance.measureUserAgentSpecificMemory()` which throws SecurityError in MV3 (no COOP+COEP). chrome.devtools Memory API requires research budget out of Phase 3 charter. Per D-P3-04: operator/alpha-tester observation. | Load extension; idle 5 min; open `chrome://memory-internals` OR `chrome://extensions/` → "Service worker" link → DevTools → Memory tab; verify extension background RAM < 50 MB. Plan 03-04 includes the operator-facing instructions verbatim in VERIFICATION.md. Optional puppeteer.Page.metrics scaffolding ships in Plan 03-04 as a best-effort diagnostic with explicit "page-realm only" caveat. |
| Extension RAM ≤ 50 MB in background | CON-ram-ceiling (NFR; SPEC §10 #9) | Puppeteer Page.metrics is page-realm only; SW heap requires `performance.measureUserAgentSpecificMemory()` which throws SecurityError in MV3 (no COOP+COEP). chrome.devtools Memory API requires research budget out of Phase 3 charter. Per D-P3-04: operator/alpha-tester observation is the canonical path. | Load extension; idle 5 min; open `chrome://memory-internals` OR `chrome://extensions/` → "Service worker" link → DevTools → Memory tab; verify extension background RAM < 50 MB. Plan 03-04 includes the operator-facing instructions verbatim in VERIFICATION.md `human_verification` block. Plan 03-04 ships A32 informational scaffolding via `puppeteer.Page.metrics()` with explicit "page-realm only" diagnostic emitted on every run. |
*All other Phase 3 behaviors have automated verification via the UAT harness (Plans 03-01..05 produce assertA29+ for SPEC §10 #4 rrweb DOM, §10 #5 event log, §10 #8 password filter).*
*All other Phase 3 behaviors have automated verification via the UAT
harness (Plans 03-01..05 produce assertA29..A32 for SPEC §10 #4 rrweb
DOM, §10 #5 event log, §10 #8 password filter, §10 #9 RAM scaffolding).*
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies — pending planner fill-in
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify — verify after planner fills the table
- [ ] Wave 0 covers all MISSING references — Phase 3 inherits infra; no Wave 0 needed
- [ ] No watch-mode flags — verify in planner output (focused commands use `--run`)
- [ ] Feedback latency < ~2.5 min — confirmed by infrastructure inheritance
- [ ] `nyquist_compliant: true` set in frontmatter — pending sign-off after planner completes
- [x] All tasks have `<automated>` verify or Wave 0 dependencies — planner filled per-task map covering all 10 tasks across 5 plans
- [x] Sampling continuity: no 3 consecutive tasks without automated verify — verified; every task either runs `npm test`/`npm run test:uat` or has a grep-based bundle gate
- [x] Wave 0 covers all MISSING references — Phase 3 inherits infra from Plan 01-13 + 02-04; no Wave 0 needed
- [x] No watch-mode flags — focused commands use `--run` (vitest) or `npm run test:uat` (single-shot)
- [x] Feedback latency < ~3 min — confirmed by infrastructure inheritance (~2.7 min full sweep)
- [x] `nyquist_compliant: true` set in frontmatter — planner sign-off
**Approval:** pending (planner fills per-task map; checker validates)
**Approval:** planner-filled 2026-05-20; checker validates next pass.