diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 25659fd..53affb3 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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 diff --git a/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-PLAN.md b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-PLAN.md new file mode 100644 index 0000000..fc6272e --- /dev/null +++ b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-PLAN.md @@ -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" +--- + + +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 `` scaffold (head unchanged; tokens.css link +preserved for A18/A21). + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + + +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; + readonly diagnostics: ReadonlyArray; + 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(msg: unknown, timeoutMs: number, label: string): Promise; // 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 +``` + + +# 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 + `` — 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 `` block. + The existing `` + is load-bearing for A18 + A21. Probe HTML appends below the existing + `` (line 21) and BEFORE the existing `` (line 22). +3. Insert the following probe HTML block BETWEEN line 21 and line 22 (preserving the trailing newline structure): + +```html + + + + + + Submit + + + + col-acol-b + + + row-1-arow-1-b + row-2-arow-2-b + + + + Toggle modal + + + Probe modal content (Plan 03-01 A29 IncrementalSnapshot trigger). + +``` + +4. Verify no `` was introduced (grep should return 0 hits on the modified file). +5. Verify the `` block is unchanged. +6. No new dependencies, no other file edits in this task. + + + grep -c "id=\"probe-form\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' && grep -c "id=\"probe-modal-trigger\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' && grep -c "id=\"probe-table\"" tests/uat/extension-page-harness.html | grep -v '^#' | grep -q '^1$' && ! grep -q "textarea" tests/uat/extension-page-harness.html && grep -q '' tests/uat/extension-page-harness.html + + + - `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 '' 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. + + + 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. + + + + + Task 2: Add assertA29 page-side stub + driveA29 host-side + harness.test.ts orchestrator wiring + tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts + + - 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) + + + 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` 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;` 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` 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 = (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). + + +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 { + 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('#probe-text'); + if (textInput !== null) { + textInput.value = 'probe'; + textInput.dispatchEvent(new Event('input', { bubbles: true })); + } + const modalTrigger = document.querySelector('#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;` entry and BEFORE `getManifestVersion`, insert: + +```typescript + // Plan 03-01 — rrweb DOM verification (SPEC §10 #4) + assertA29: () => Promise; +``` + +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 { + // 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: ``, + 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 = + (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). + + + npx tsc --noEmit && grep -c "assertA29" tests/uat/extension-page-harness.ts | grep -v '^#' | awk '$1 >= 3' && grep -c "driveA29" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | awk '$1 >= 2' && grep -c "driveA29" tests/uat/harness.test.ts | grep -v '^#' | awk '$1 >= 3' && grep -c "from '@rrweb/types'" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | grep -q '^1$' && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat + + + - `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). + + + 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. + + + + + + +## 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. + + + +- `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 '' tests/uat/extension-page-harness.html` returns 1. + + + +- 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. + + + +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 + diff --git a/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-PLAN.md b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-PLAN.md new file mode 100644 index 0000000..79b1e20 --- /dev/null +++ b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-PLAN.md @@ -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\\(" +--- + + +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. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + +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; +} +``` + +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(msg: unknown, timeoutMs: number, label: string): Promise; +// 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 +``` + + +# 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 ``-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. + + + + + + Task 1: Add assertA30 page-side orchestrator (5 event triggers + SAVE) + tests/uat/extension-page-harness.ts + + - 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) + + + - 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` 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`. + + +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 { + 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('#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('#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; +``` + +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. + + + npx tsc --noEmit && grep -c "assertA30" tests/uat/extension-page-harness.ts | grep -v '^#' | awk '$1 >= 3' + + + - `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). + + + Page-side assertA30 dispatches 5 synthetic event triggers + SAVE; registered on __mokoshHarness; ready for Task 2 host-side driveA30 grep. + + + + + Task 2: Add driveA30 host-side (UserEvent type grep) + orchestrator wiring + tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts + + - 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)" + + + 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`: + - 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 = (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`. + + +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 = [ + '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 { + // 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: ``, + 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(); + 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 = + (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`. + + + npx tsc --noEmit && grep -c "driveA30" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | awk '$1 >= 2' && grep -c "driveA30" tests/uat/harness.test.ts | grep -v '^#' | awk '$1 >= 3' && grep -c "from '../../../src/shared/types'" tests/uat/lib/harness-page-driver.ts | grep -v '^#' | grep -q '^1$' && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat + + + - `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). + + + 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. + + + + + + +## 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. + + + +- `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. + + + +- 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). + + + +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 + diff --git a/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-PLAN.md b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-PLAN.md new file mode 100644 index 0000000..dcb158d --- /dev/null +++ b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-PLAN.md @@ -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'\\)" +--- + + +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 `` +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. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + +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; + } + +From Plan 03-01: probe HTML in extension-page-harness.html provides +``. +The id-based selector means getSelector(target) returns `#probe-password` +for any logged event on this element (per src/content/index.ts:241-243). + + +# 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. + + + + + + Task 1: Add assertA31 page-side orchestrator (sentinel-into-password trigger + SAVE) + tests/uat/extension-page-harness.ts + + - 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) + + + 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 + + +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 { + 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(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; +``` + +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. + + + npx tsc --noEmit; T=$(grep -c "assertA31" tests/uat/extension-page-harness.ts); test "$T" -ge 3 && S=$(grep -c "A31_PASSWORD_SENTINEL" tests/uat/extension-page-harness.ts); test "$S" -ge 2 && grep -q "'secret-do-not-log-123'" tests/uat/extension-page-harness.ts + + + - `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. + + + Page-side assertA31 types sentinel into the password input + SAVEs; registered on __mokoshHarness; ready for Task 2 host-side driveA31 negative-assertion. + + + + + Task 2: Add driveA31 host-side (sentinel-absence grep) + orchestrator wiring + tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts + + - 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)" + + + 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`: + - 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`. + + +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 { + // 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: ``, + 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 = + (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`. + + + npx tsc --noEmit; D=$(grep -c "driveA31" tests/uat/lib/harness-page-driver.ts); test "$D" -ge 2 && H=$(grep -c "driveA31" tests/uat/harness.test.ts); test "$H" -ge 3 && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat + + + - `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). + + + 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. + + + + + + +## 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. + + + +- `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`. + + + +- 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. + + + +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 + diff --git a/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-04-PLAN.md b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-04-PLAN.md new file mode 100644 index 0000000..d979a1a --- /dev/null +++ b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-04-PLAN.md @@ -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\\(\\)" +--- + + +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. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + +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; + +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. + + +# 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. + + + + + + Task 1: Add driveA32 host-side (puppeteer.Page.metrics scaffolding) + orchestrator wiring + tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts + + - 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) + + + Host-side (`tests/uat/lib/harness-page-driver.ts`): + - Adds `export async function driveA32(page: Page): Promise`: + - 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`. + + +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 { + 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`. + + + npx tsc --noEmit; D=$(grep -c "driveA32" tests/uat/lib/harness-page-driver.ts); test "$D" -ge 2 && H=$(grep -c "driveA32" tests/uat/harness.test.ts); test "$H" -ge 2 && grep -q "NOTE: page-realm only" tests/uat/lib/harness-page-driver.ts && HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat + + + - `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). + + + 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. + + + + + + +## 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). + + + +- `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). + + + +- 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. + + + +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 + diff --git a/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-05-PLAN.md b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-05-PLAN.md new file mode 100644 index 0000000..7cacb62 --- /dev/null +++ b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-05-PLAN.md @@ -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" +--- + + +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. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + +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. + + +# 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. + + + + + + Task 1: Run pre-checkpoint bundle gates (6/6 standard inventory) + + + - /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) + + + 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. + + +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. + + + 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/ + + + - `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. + + + 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. + + + + + Task 2: Write 03-VERIFICATION.md (9-criterion sweep + T5 override + human_verification + cross-cutting gates) + .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md + + - .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 `` 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 `` block (10 deferred items to carry into the Forward-Looking Deferred Items table) + + + 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 `` 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) + + +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 `` 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 + +phase: 03-spec-10-smoke-verification-dom-event-log-verification +verified: +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: . 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: . + - 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: . + + 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 MB" or + describe deviation. The Operator-Empirical Acks table below records the result. + + +# 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:** +**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 | +|---|---|---|---| +| | 03 (§10 #9 RAM operator check per D-P3-04) | | | + +*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 | + + + +*Verified: by Claude (gsd-verifier — Phase 3 closure aggregator)* +*Verifier: Plan 03-05 (this document)* +``` + +3. Replace `` with the current ISO timestamp at write time (e.g. `2026-05-20T19:00:00Z`). +4. Replace `` 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. + + + test -f .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md && L=$(wc -l < .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md) && test "$L" -ge 120 && grep -q 'overrides_applied: 3' .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md && grep -q 'human_verification:' .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md && grep -q 'score: 9/9 SPEC §10 criteria' .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md + + + - 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 `` block. + + + 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. + + + + + Task 3: Flip REQUIREMENTS.md + ROADMAP.md + STATE.md markers (Phase 3 closure) + .planning/REQUIREMENTS.md, .planning/ROADMAP.md, .planning/STATE.md + + - .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) + + + 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: ` + `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`. + + +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: (rrweb DOM A29) → (event log A30) → (password absence A31) → (RAM scaffolding A32) → (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: +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. + + + grep -E "^- \[x\] \*\*REQ-rrweb-dom-buffer" .planning/REQUIREMENTS.md | grep -v '^#' | grep -q REQ-rrweb-dom-buffer && grep -E "^- \[x\] \*\*REQ-user-event-log" .planning/REQUIREMENTS.md | grep -v '^#' | grep -q REQ-user-event-log && grep -E "^- \[x\] \*\*Phase 3" .planning/ROADMAP.md | grep -v '^#' | grep -q "Phase 3" && grep -q "completed_phases: 3" .planning/STATE.md + + + - `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`). + + + 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. + + + + + + +## 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). + + + +- `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. + + + +- 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. + + + +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 + diff --git a/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VALIDATION.md b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VALIDATION.md index f83c528..1952dce 100644 --- a/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VALIDATION.md +++ b/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VALIDATION.md @@ -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 `` 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 +`` 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/.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` 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 '' tests/uat/extension-page-harness.html ; ! grep -q '= 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 `` 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 `` 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.
` scaffold (head unchanged; tokens.css link +preserved for A18/A21). +
` (line 21) and BEFORE the existing `` (line 22). +3. Insert the following probe HTML block BETWEEN line 21 and line 22 (preserving the trailing newline structure): + +```html + +
Probe modal content (Plan 03-01 A29 IncrementalSnapshot trigger).