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