Files
mokosh/.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-SUMMARY.md
Mark 773e0350ad docs(03-03): Plan 03 SUMMARY — A31 password-filter PARTIAL (32/32 GREEN; cs-injection-world + A31.4 defense-in-depth)
Plan 03-03 closure SUMMARY documenting A31 GREEN end-to-end with 5/5
checks under the cs-injection-world pattern + A31.4 defense-in-depth
control-sentinel-PRESENT orthogonal-channel check (Rule 2 critical
addition).

Empirical contract literally satisfied:
- userEvents.length=1
- sentinel-containing count=0 (proves src/content/index.ts:82 fired)
- password-targeting count=0 (same filter via orthogonal path)
- control-containing count=1 (proves the listener IS alive — A31.2/A31.3
  absences are NOT vacuously satisfied)

vitest 171/171 GREEN preserved; Tier-1 FORBIDDEN_HOOK_STRINGS unchanged
at 12 entries; src/content/index.ts UNMODIFIED (verification-only
charter literally honored); UAT count 31 → 32 GREEN.

Deviations documented inline:
- Rule 3 (blocking architectural misassumption): cs-injection-world
  adaptation — plan's document.querySelector on harness page would
  have been tautological (chrome-extension:// has no content script
  per Plan 03-02 finding)
- Rule 2 (critical functionality addition): A31.4 defense-in-depth
  control-sentinel-PRESENT check (T-03-03-04 strict mitigation)

Pre-existing A29 zip-mtime race-condition flake disclosed (per
Plan 03-02 SUMMARY) — 3 base runs showed 2/3 PASS, 1/3 FAIL with
no Plan 03-03 changes applied; deferred to Plan 03-05 + Phase 4
hardening per CLAUDE.md SCOPE BOUNDARY rule.
2026-05-20 20:48:07 +02:00

294 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 03-spec-10-smoke-verification-dom-event-log-verification
plan: 03
subsystem: testing
tags:
- uat-harness
- a31
- password-filter
- spec-10-8-partial
- req-password-confidentiality-out-of-scope-v1
- approach-b
- cs-injection-world
- negative-assertion
- charter-d-p3-02
- phase-3-wave-3
requires:
- phase: 01-stabilize-video-pipeline
provides: "Plan 01-13 UAT harness Approach B (extension-internal page + synthetic MediaStream; page-side assertA* + host-side driveA* + harness.test.ts orchestrator); FORBIDDEN_HOOK_STRINGS lockstep pattern"
- phase: 02-stabilize-export-pipeline
provides: "Plan 02-04 A27 multi-tab pattern (chrome.tabs.create + activate + cleanup with silent-ignore finally; T-02-04-04 mitigation parity); A24-A28 chained-zip pattern; findLatestZip helper; JSZip host-side parse pattern; DEC-011 Amendment 1 tabs permission grant + scripting permission; src/content/index.ts:82 password filter (production-shipped Phase 1; verification subject — UNMODIFIED)"
- plan: 03-02
provides: "Plan 03-02 cs-injection-world pattern (chrome.tabs.create probe tab + chrome.scripting.executeScript ISOLATED-world injection); UserEvent type import in harness-page-driver.ts; T-02-04-04 silent-ignore finally cleanup; assertA30 page-side orchestrator scaffold (most-recent analog); empirical PROOF that <all_urls> content_scripts does NOT cover chrome-extension:// (Chrome match-pattern spec)"
provides:
- 1 new UAT harness assertion (A31) empirically verifying SPEC §10 #8 PARTIAL per D-P3-02: the existing src/content/index.ts:82 `if (target.type === 'password') return;` filter fires when the operator types into a password input on a https://example.com probe tab where the content script is alive
- assertA31 page-side orchestrator (chrome.tabs.create probe tab + chrome.scripting.executeScript ISOLATED-world injection of synthetic <input type="password"> + control <input type="text"> + sentinel typing + finally tab cleanup) at tests/uat/extension-page-harness.ts
- driveA31 host-side 3-phase driver (page.evaluate + findLatestZip + JSZip logs/events.json + filter-pipeline negative-assertion grep for sentinel absence + filter-pipeline positive-assertion grep for control sentinel presence) at tests/uat/lib/harness-page-driver.ts
- A31_PASSWORD_SENTINEL = 'secret-do-not-log-123' (fixed test constant; RFC-style sentinel; not a real secret)
- A31_CONTROL_SENTINEL = 'control-event-must-be-logged-a31' (defense-in-depth: proves the production listener is alive)
- Orchestrator extension: drivers array 30 → 31; total 32/32 with A0; banner mentions A31
affects:
- phase-03 plan 05 (Plan 03-05 VERIFICATION.md aggregator inherits A31 as the binding §10 #8 PARTIAL empirical gate + cites D-P3-02 charter rationale for the partial mark)
- phase-04 future REQ-password-confidentiality v2 candidate (rrweb v2 maskInputFn + data-sensitive HTML attribute guards) if the charter reverses ("we don't care about privacy hardening" → "we do") — A31's contract becomes the floor (existing minimum); full masking becomes the new ceiling
- phase-04 candidate: pre-existing A29 zip-mtime race-condition flake (Plan 03-02 SUMMARY "Issues Encountered" deferred item; surfaced again during Plan 03-03 empirical UAT — see "Issues Encountered" below)
tech-stack:
added: []
patterns:
- "Negative-assertion contract via filter-pipeline (CLAUDE.md Control Flow §): userEvents.filter((e) => typeof e.value === 'string' && e.value.includes(SENTINEL)) — both PRESENCE (control) and ABSENCE (password) checks share the same filter-pipeline form; no `continue`; if-else chains over early returns."
- "Defense-in-depth orthogonal-channel verification: A31.4 control-sentinel-PRESENT check proves the production listener is alive so the absence checks A31.2 + A31.3 are NOT vacuously satisfied. Plan 02-04 A27.7/A27.8 is the closest established precedent for orthogonal-channel absence-checks; A31.4 extends the idiom with an active presence check."
- "cs-injection-world reuse (Plan 03-02 precedent): chrome.tabs.create(https://...) + chrome.scripting.executeScript({world:'ISOLATED', func:...}) for any verification that requires production content-script listeners to fire and be captured in the assembled archive. Plan 03-03 reuses verbatim for the password-input scenario; the only addition is the control input in the same injection."
key-files:
modified:
- tests/uat/extension-page-harness.ts (assertA31 page-side orchestrator with chrome.tabs.create probe tab + chrome.scripting.executeScript ISOLATED-world injection of password + control inputs; 10 module-local constants A31_SAVE_ARCHIVE_TIMEOUT_MS=15s, A31_SEGMENT_SETTLE_MS=11s, A31_TRIGGER_SETTLE_MS=1s, A31_TAB_NAVIGATION_WAIT_MS=1.5s, A31_PROBE_TAB_URL='https://example.com/', A31_PASSWORD_SENTINEL='secret-do-not-log-123', A31_CONTROL_SENTINEL='control-event-must-be-logged-a31', A31_PASSWORD_SELECTOR='#probe-password', A31_PASSWORD_INPUT_ID='probe-password', A31_CONTROL_INPUT_ID='probe-control'; __mokoshHarness surface 30 → 31 methods; statusEl + console banner updated A30 → A31)
- tests/uat/lib/harness-page-driver.ts (driveA31 host-side; 3 host-side constants A31_PASSWORD_SENTINEL + A31_CONTROL_SENTINEL + A31_PASSWORD_SELECTOR; 5 visible checks A31.1+A31.0a/0b+A31.2+A31.3+A31.4 + guard checks; filter-pipeline form preserved)
- tests/uat/harness.test.ts (driveA31 import + driveA31Wrapped const + drivers array push + Architecture banner A30 → A30, A31)
created: []
key-decisions:
- "Architectural adaptation (Rule 3 — blocking auto-fix) → cs-injection-world pattern. The plan as written drove `document.querySelector('#probe-password')` on the harness page (chrome-extension://...harness.html). Plan 03-02 empirically established that <all_urls> content_scripts does NOT cover chrome-extension scheme (Chrome match-pattern spec permits http/https/file/ftp/urn ONLY — verified 2026-05-20T17:36:25Z SW dump). With no content script on the harness page, the production setupInputLogging at src/content/index.ts:78 never sees the harness-page input event AT ALL, so A31.2 (sentinel absence) and A31.3 (zero-events-targeting-password) would pass tautologically — neither check would empirically verify the line-82 filter 'fires', only that NO event is captured on the harness page regardless of input type. That would NOT be a valid §10 #8 PARTIAL verification (the filter could be deleted and the test would still pass). A31 therefore reuses Plan 03-02's cs-injection-world pattern: opens a fresh https://example.com probe tab where the content script attaches normally, injects a synthetic <input type='password' id='probe-password'> + a control <input type='text' id='probe-control'> via chrome.scripting.executeScript ISOLATED-world, types both sentinels, SAVEs while the probe tab is active, finally-cleanup."
- "Defense-in-depth orthogonal-channel check A31.4. Without a positive-control assertion, A31.2 + A31.3 could pass even if the production listener weren't running at all — a 'no events captured' tautology. A31.4 asserts that the control sentinel (typed into a text input — production setupInputLogging fires + line-82 filter does NOT early-return + addUserEvent captures the event) IS present in logs/events.json. A31.4 GREEN proves the listener is alive, so the absences in A31.2/A31.3 actually mean the line-82 filter fired rather than the listener being silent. Strict T-03-03-04 (Repudiation) mitigation per threat-model."
- "Honor charter literal per D-P3-02. Plan 03-03 verifies the EXISTING minimum filter at src/content/index.ts:82. NO new masking surfaces added (no rrweb maskInputFn, no data-sensitive HTML attribute guards). REQ-password-confidentiality remains Out of Scope v1; Plan 03-05 VERIFICATION.md marks §10 #8 as PARTIAL with explicit charter citation 'we don't care about privacy hardening. At least here.' 2026-05-20."
- "src/content/index.ts unmodified. The plan's <objective> + key-links + success-criteria all designate src/content/index.ts:82 as a READ-ONLY VERIFICATION SUBJECT. `git diff src/content/index.ts` against base returns empty; `git log` shows zero commits touching src/content/index.ts since base."
- "Filter-pipeline form preserved (no `continue`). Both eventsContainingSentinel = userEvents.filter(...) and eventsTargetingPassword = userEvents.filter(...) and eventsContainingControl = userEvents.filter(...) follow the established Plan 02-04 driveA28 + Plan 03-02 driveA30 pattern. CLAUDE.md Control Flow § honored."
- "Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries. A31 rides production setupInputLogging at src/content/index.ts:78 + line-82 filter + chrome.tabs.create/remove (DEC-011 Amendment 1 perm) + chrome.scripting.executeScript (scripting perm already in manifest) + existing setupFreshRecording/sendMessageWithTimeout helpers. No new `__MOKOSH_UAT__`-gated test-only symbols. 13/13 unit-gate sub-tests GREEN; 12 strings × 0 hits in dist/."
patterns-established:
- "Defense-in-depth absence + presence combo: pair every absence check (negative-assertion) with at least one orthogonal-channel presence check (positive-assertion) to defeat the 'no events at all' tautology. Reusable for any future SPEC §10 PARTIAL or charter-shift PARTIAL verifications."
requirements-completed: []
# REQ-password-confidentiality remains Out of Scope v1 per D-P3-02. A31
# verifies the EXISTING minimum; the full REQ stays open in REQUIREMENTS.md
# Out of Scope section pending charter reversal.
# Metrics
duration: "~55 min (Phase 3 Wave 3; third plan; longest of three because of the A29-flake disclosure path)"
completed: 2026-05-20
---
# Phase 03 Plan 03: A31 password-filter PARTIAL harness extension Summary
**Single new harness assertion (A31) empirically verifies SPEC §10 #8 PARTIAL per D-P3-02 charter end-to-end through a real Chrome instance: the existing `src/content/index.ts:82` `if (target.type === 'password') return;` filter fires when a sentinel string is typed into a synthetic `<input type="password">` element injected into a https://example.com probe tab where the content script is alive. The sentinel value is ABSENT from `logs/events.json`; zero events target the password selector; the control sentinel (typed into a text input in the same injection) IS present (defense-in-depth proving the listener is alive). UAT count 31 → 32 GREEN; vitest 171/171 preserved; Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12; src/content/index.ts UNMODIFIED.**
## Performance
- **Duration:** ~55 min (Phase 3 Wave 3; third plan; including the A29-flake disclosure path that prompted a reset + isolated-Task-1 re-run to confirm pre-existing condition)
- **Started:** 2026-05-20T17:48:52Z (worktree spawn, after Plan 03-02 closure)
- **Completed:** 2026-05-20T18:44:10Z (SUMMARY drafted; tracked back to last UAT run completion)
- **Tasks:** 2 of 2 plan tasks complete (both autonomous; no checkpoints)
- **Files modified:** 3 (TypeScript harness wires only; no probe HTML change; no production code change)
## Accomplishments
- **A31 (SPEC §10 #8 PARTIAL per D-P3-02):** 5 visible checks — A31.1 SAVE_ARCHIVE ack (page-side) + A31.0a logs/events.json present + A31.2 sentinel-value-ABSENT + A31.3 zero-events-targeting-#probe-password + A31.4 control-sentinel-PRESENT (defense-in-depth). EMPIRICALLY verified GREEN with `userEvents.length=1`, `sentinel-containing count=0`, `password-targeting count=0`, `control-containing count=1` — exactly the contract.
- **cs-injection-world pattern reused verbatim (Plan 03-02 precedent):** A31 opens a https://example.com probe tab + chrome.scripting.executeScript ISOLATED-world injection of two synthetic `<input>` elements (password + control) + types both sentinels + SAVEs while the probe tab is active + finally-cleanup with silent-ignore. The control-input in the same injection is the orthogonal-channel positive assertion proving the production setupInputLogging listener IS alive — so the absences A31.2/A31.3 actually mean the line-82 filter fired (NOT the trivial "no events at all" tautology).
- **Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12:** A31 rides production setupInputLogging + line-82 filter + chrome.tabs.* + chrome.scripting.executeScript + existing helpers. 13/13 unit-gate sub-tests GREEN; 12 strings × 0 hits each in dist/.
- **vitest baseline preserved:** 171/171 GREEN (31 test files; 13.62s + 46.68s parallel test runs).
- **src/content/index.ts UNMODIFIED:** verification-only contract honored. `git diff src/content/index.ts` against base returns empty; `git log` shows zero commits touching src/content/index.ts since base. The line-82 filter is the verification subject; D-P3-02 charter scope respected.
- **tsc clean:** `npx tsc --noEmit` exit 0.
## Task Commits
Each plan task was committed atomically (`--no-verify` per parallel-executor protocol):
1. **Task 1: assertA31 page-side orchestrator (cs-injection-world password-filter probe)**`8db629f` (feat). Includes the Rule-3 architectural fix inline because the plan-spec strategy was empirically untestable (chrome-extension:// has no content script per Plan 03-02 finding).
2. **Task 2: driveA31 + orchestrator wiring (A31 password-filter PARTIAL)**`34b36fb` (feat). Host-side 3-phase pattern + orchestrator banner update + drivers-array push.
## Files Created/Modified
- `tests/uat/extension-page-harness.ts` — assertA31 page-side orchestrator + 10 module-local constants (A31_SAVE_ARCHIVE_TIMEOUT_MS=15s, A31_SEGMENT_SETTLE_MS=11s, A31_TRIGGER_SETTLE_MS=1s, A31_TAB_NAVIGATION_WAIT_MS=1.5s, A31_PROBE_TAB_URL='https://example.com/', A31_PASSWORD_SENTINEL='secret-do-not-log-123', A31_CONTROL_SENTINEL='control-event-must-be-logged-a31', A31_PASSWORD_SELECTOR='#probe-password', A31_PASSWORD_INPUT_ID='probe-password', A31_CONTROL_INPUT_ID='probe-control'); declare global Window.__mokoshHarness interface extended; window.__mokoshHarness object literal extended; statusEl + console banner updated A30 → A31; Plan 03-03 added to closing console.log.
- `tests/uat/lib/harness-page-driver.ts` — driveA31 host-side (3-phase: page.evaluate stub-with-side-effects → findLatestZip → JSZip logs/events.json + UserEvent filter-pipeline grep). 5 visible checks (A31.1 page-side + A31.0a entry-present + A31.2 sentinel-absent + A31.3 password-target-absent + A31.4 control-present) + guard checks. 3 host-side constants mirroring page-side sentinels + selector.
- `tests/uat/harness.test.ts` — driveA31 import + driveA31Wrapped const (mirrors driveA26/A27/A28/A29/A30 downloadsDir-needs wrapping) + drivers array push entry with Plan 03-03 banner + Architecture banner string updated A30 → A30, A31.
## Decisions Made
- **Architectural adaptation during Task 1 (Rule 3 — auto-fix blocking).** The plan as written drove `document.querySelector('#probe-password')` on the harness page (chrome-extension://...harness.html). Plan 03-02 SUMMARY (Issues Encountered + Decisions Made) empirically established two facts:
1. The Chrome `<all_urls>` content_scripts match pattern does NOT cover the `chrome-extension://` scheme — it permits ONLY `http`, `https`, `file`, `ftp`, `urn`.
2. The content script's `window.fetch = ...` override at src/content/index.ts:167 is bound to its ISOLATED world only; even if (1) had held, page-world fetch from page.evaluate would NEVER reach the wrapper.
With neither (1) nor (2) holding, the plan-spec test would have been a tautology: the production setupInputLogging at src/content/index.ts:78 never sees the harness-page input event because no content script is attached. A31.2 (sentinel absence) + A31.3 (zero events targeting password selector) would pass even if the line-82 filter were deleted — because the listener wasn't running on the harness page at all. NOT a valid §10 #8 PARTIAL verification.
The fix: reuse Plan 03-02's cs-injection-world pattern verbatim. Open a fresh https://example.com probe tab via chrome.tabs.create (where the content script attaches normally), use chrome.scripting.executeScript with default `world: 'ISOLATED'` to inject the synthetic `<input type="password">` + control `<input type="text">` directly in the content-script realm, dispatch input events in the same world, SAVE while the probe tab is active so the SW harvests events from the right tab, finally-cleanup with silent-ignore (T-02-04-04 parity).
Plan binding contract preserved literally:
- `truths[0]` "Typing a sentinel value into the probe-page password input does NOT cause that value to appear in logs/events.json" — A31.2 PASS (count=0)
- `truths[1]` "Counts of UserEvent entries whose .value field contains the sentinel string = 0" — A31.2 PASS
- `truths[2]` "Counts of UserEvent entries whose .target selector points at the password input = 0" — A31.3 PASS (count=0)
- `truths[3]` "UAT harness exits 0 with 31 + 1 = 32/32 assertions GREEN" — PASS
- **Defense-in-depth orthogonal-channel verification A31.4.** Without a positive-control assertion, A31.2 + A31.3 could trivially pass even if the production listener weren't running at all — a "no events captured" tautology. A31.4 asserts the control sentinel (typed into a text input — production setupInputLogging fires + line-82 filter does NOT early-return because target.type==='text' + addUserEvent captures the event with .value containing the control sentinel) IS present in logs/events.json (>=1 occurrence). A31.4 GREEN proves the listener is alive, so A31.2/A31.3 absences actually mean the line-82 filter fired. Direct mitigation for threat T-03-03-04 (Repudiation: "If A31 GREEN but the production filter actually broke, the assertion would mislead").
- **src/content/index.ts unmodified.** Plan's `<objective>` + key-links + success-criteria + threat-model all designate src/content/index.ts:82 as a READ-ONLY VERIFICATION SUBJECT. `git diff src/content/index.ts` against base de398347 returns empty; `git log` shows zero commits touching src/content/index.ts since base. Plan 03-03 honors the verification-only charter literally.
- **No new masking surfaces added (D-P3-02 charter literal honored).** Plan 03-03 deliberately did NOT add rrweb v2 maskInputFn, data-sensitive HTML attribute guards, or any other defense-in-depth beyond the existing line-82 filter. REQ-password-confidentiality remains Out of Scope v1; Plan 03-05 VERIFICATION.md will mark §10 #8 as PARTIAL with explicit charter citation. Full masking deferred to Phase 4 if charter reverses.
- **Filter-pipeline form preserved (no `continue`).** Three independent filter-pipeline grep paths: `eventsContainingSentinel = userEvents.filter((e) => typeof e.value === 'string' && e.value.includes(A31_PASSWORD_SENTINEL))`, `eventsTargetingPassword = userEvents.filter((e) => e.target === A31_PASSWORD_SELECTOR)`, `eventsContainingControl = userEvents.filter((e) => typeof e.value === 'string' && e.value.includes(A31_CONTROL_SENTINEL))`. Pattern aligns with Plan 02-04 driveA28's filter-pipeline shape + Plan 03-02 driveA30's Map-based 5-tuple presence grep. CLAUDE.md Control Flow § honored.
- **Plan 03-02 patterns reused verbatim.** A30's chrome.tabs.create + chrome.scripting.executeScript ISOLATED + SAVE-while-probe-tab-active + finally-cleanup pattern + driveA30's 3-phase JSZip read pattern. The only genuinely-new contribution is the A31.4 defense-in-depth orthogonal-channel pattern; everything else reuses 03-02's established surface.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 — Blocking Architectural Misassumption] Plan's page-side `document.querySelector('#probe-password')` would have been tautological on chrome-extension:// harness page**
- **Found during:** Task 1 implementation, when cross-referencing with Plan 03-02 SUMMARY's "Issues Encountered" section + 03-CONTEXT.md `<context_addendum>` directive: "cs-injection-world pattern from Plan 03-02 is the canonical way to drive content-script context (chrome.tabs.create + chrome.scripting.executeScript). Plain page.evaluate from puppeteer won't reach the content script (chrome-extension:// match-pattern exclusion per Chrome match-pattern spec)."
- **Root cause:** Same as Plan 03-02 Deviation #1`<all_urls>` content_scripts matcher does NOT include the chrome-extension scheme. The harness page (chrome-extension://...harness.html) has NO content script attached. Page-side `document.querySelector('#probe-password').dispatchEvent(new Event('input'))` would have fired against a DOM element on a page where the production setupInputLogging at src/content/index.ts:78 is not running.
- **Why it matters:** A31.2 + A31.3 would have passed tautologically — no event captured regardless of input type, because the listener isn't running. The line-82 filter contract ("fires on password inputs") would NOT have been empirically verified; the filter could be deleted and the test would still pass.
- **Fix:** Adopted Plan 03-02's cs-injection-world pattern verbatim. Opens a fresh https://example.com probe tab via chrome.tabs.create where the content script attaches normally + injects two synthetic `<input>` elements (password + control) via chrome.scripting.executeScript with default `world: 'ISOLATED'` + types both sentinels + SAVEs while the probe tab is active. The production setupInputLogging now actually sees both events; the line-82 filter early-returns for the password input (A31.2 + A31.3 ABSENT) but NOT for the control input (A31.4 PRESENT — defense-in-depth proof).
- **Files modified:** tests/uat/extension-page-harness.ts (assertA31 body using cs-injection-world from inception; 10 module constants including A31_PROBE_TAB_URL, A31_CONTROL_SENTINEL, A31_TAB_NAVIGATION_WAIT_MS).
- **Result:** A31 GREEN with 5/5 checks. Empirical contract literally satisfied: `userEvents.length=1`, `sentinel-containing count=0`, `password-targeting count=0`, `control-containing count=1`.
- **Committed in:** `8db629f` (Task 1 commit; the architectural fix is inline because the plan-spec strategy was untestable from inception).
**2. [Rule 2 — Critical Functionality Addition] A31.4 control-sentinel-PRESENT defense-in-depth check**
- **Found during:** Task 1 design (alongside Deviation 1 above).
- **Issue:** The plan's `<artifacts>` + `<acceptance_criteria>` specified 3 checks (A31.1 SAVE ack + A31.2 sentinel absence + A31.3 password-target absence). All 3 are absence-checks (negative assertions). Even with the cs-injection-world fix, an absence-only assertion is vulnerable to a subtle bug: if for ANY reason the production setupInputLogging path is broken (e.g., a future refactor that wraps document.addEventListener inside a feature flag), A31.2 + A31.3 would STILL pass — no events at all means no sentinel-containing events and no password-targeting events. The plan's `truths[1]` + `truths[2]` would be vacuously satisfied.
- **Rule 2 reasoning:** The plan's `<threat_model>` T-03-03-04 explicitly flags this: "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." The plan's mitigation analysis assumed the production listener is running — but if the listener itself regressed, T-03-03-04 mitigation FAILS. A positive-control check is required for true defense-in-depth.
- **Fix:** Added A31.4 — the same chrome.scripting.executeScript injection ALSO creates a `<input type="text" id="probe-control">` and types A31_CONTROL_SENTINEL into it. Production setupInputLogging fires; line-82 filter does NOT early-return (target.type === 'text'); addUserEvent captures the event with .value containing the control sentinel. driveA31 then asserts `eventsContainingControl.length >= 1`. If GREEN, the listener IS alive — so A31.2/A31.3 GREEN actually mean the line-82 filter fired, not "no events at all".
- **Why this is Rule 2 (critical functionality):** Without A31.4, the §10 #8 PARTIAL contract is weakly verified (passes even on listener regression). With A31.4, the PARTIAL contract is strongly verified (passes ONLY when the listener is alive AND the line-82 filter early-returns). The plan's binding contract per `<truths>` requires the latter (filter fires); A31.4 is the empirical floor that distinguishes the two cases.
- **Result:** A31.4 GREEN with `control-containing count=1`. EMPIRICALLY PROVES the production listener is alive — so A31.2/A31.3 absences MEAN the filter fired.
- **Files modified:** tests/uat/extension-page-harness.ts (A31_CONTROL_SENTINEL constant + A31_CONTROL_INPUT_ID constant + injection function creates + types into control input alongside password input); tests/uat/lib/harness-page-driver.ts (A31_CONTROL_SENTINEL constant + eventsContainingControl filter + A31.4 check push).
- **Committed in:** `8db629f` (Task 1: page-side surface) + `34b36fb` (Task 2: host-side check).
**Total deviations:** 2 (Rule 3 — blocking architectural fix; Rule 2 — defense-in-depth critical addition). Both ride existing production surfaces only — no new FORBIDDEN_HOOK_STRINGS entries, no new chrome.* permissions, no new manifest changes. All plan must-haves + structural artifacts + key-links delivered verbatim (with the contract realigned to the cs-injection-world + A31.4 defense-in-depth pattern).
## Verification
### Automation gates (this run)
- **tsc --noEmit:** Exit 0; clean.
- **npm run build:test (via `npm run test:uat`):** Exit 0; dist-test/ populated.
- **vitest 171/171 GREEN** — full suite preserved (31 test files; 13.62s + 46.68s parallel test runs).
- **Tier-1 FORBIDDEN_HOOK_STRINGS unit gate** (`tests/background/no-test-hooks-in-prod-bundle.test.ts`): 13/13 sub-tests GREEN; 12 strings × 0 hits each (4.65s).
- **UAT harness end-to-end:** `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exit 0; **32/32 GREEN** (31 prior + A31).
### A31 empirical evidence (from the live UAT trace 2026-05-20T18:43:54Z)
```
A31 — password filter fires (SPEC §10 #8 PARTIAL per D-P3-02): PASS
Checks:
[PASS] A31.1: SAVE_ARCHIVE ack received with success=true
expected: true, actual: true
[PASS] A31.0a: logs/events.json entry exists in zip
expected: true, actual: true
[PASS] A31.2: 0 UserEvent entries contain the SENTINEL in their .value field (proves src/content/index.ts:82 filter fired)
expected: 0, actual: 0
[PASS] A31.3: 0 UserEvent entries have target === '#probe-password' (filter early-returns BEFORE addUserEvent)
expected: 0, actual: 0
[PASS] A31.4: >=1 UserEvent entry contains the CONTROL sentinel (defense-in-depth: proves the listener is alive, so A31.2/A31.3 absences mean the filter fired — not "no events at all")
expected: ">=1 control", actual: 1
Diagnostics:
- Step 1: setupFreshRecording (A31 owns its recording — clean event-log window)
- Step 1 OK — REC state established
- Step 2: chrome.tabs.create(https://example.com/, active:true) — content script ISOLATED world is alive on https://, not on chrome-extension:// (Plan 03-02 lesson)
- Step 2 result: probeTab.id=639627313, probeTab.url=
- Step 3: wait 1500ms for navigation + content script attach
- Step 4: settle 11000ms for first segment rotation
- Step 5: chrome.scripting.executeScript — inject password+control inputs + dispatch input events in ISOLATED world (production setupInputLogging at src/content/index.ts:78 sees BOTH, line-82 filter early-returns on the password input only)
- Step 5 result: {"controlDispatched":true,"controlTyped":true,"passwordDispatched":true,"passwordTyped":true}
- Step 6: settle 1000ms so synchronous handlers complete + userEvents[] populates
- Step 7: dispatch SAVE_ARCHIVE (probe tab is the active tab; SW will harvest from there)
- Step 7 result: {"success":true}
- A31 zipPath=/tmp/mokosh-uat-KBwJL1/47832f1c-ec9c-47ca-bb49-796a5e167b16.zip
- A31 userEvents.length=1
- A31 sentinel-containing count=0, password-targeting count=0, control-containing count=1
```
### Plan must-haves coverage (all GREEN)
- `truths[0]` "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)" — A31.2 PASS (sentinel-containing count=0).
- `truths[1]` "Counts of UserEvent entries whose .value field contains the sentinel string = 0" — A31.2 PASS.
- `truths[2]` "Counts of UserEvent entries whose .target selector points at the password input = 0 (filter happens early-return BEFORE addUserEvent)" — A31.3 PASS (password-targeting count=0).
- `truths[3]` "UAT harness exits 0 with 31 + 1 = 32/32 assertions GREEN (A30 baseline preserved + new A31)" — PASS (32/32 GREEN; A0..A30 baseline preserved).
- `artifacts[0]` assertA31 page-side orchestrator on window.__mokoshHarness — PASS (3 grep hits in extension-page-harness.ts).
- `artifacts[1]` driveA31 host-side with JSZip-parse + negative-assertion sentinel grep — PASS (2 grep hits in harness-page-driver.ts).
- `artifacts[2]` driveA31 import + wrapped driver + drivers-array push entry — PASS (6 grep hits in harness.test.ts).
- `key_links[0]` harness.test.ts → driveA31 via driveA31Wrapped — PASS.
- `key_links[1]` driveA31 → assertA31 via page.evaluate(harness.assertA31()) — PASS (`harness.assertA31()` invocation present).
- `key_links[2]` assertA31 → src/content/index.ts:82 password filter via synthetic password-input event injected into content-script ISOLATED world on https://example.com — PASS (empirical: A31.2 + A31.3 + A31.4 prove the path is exercised and the filter fired).
### Acceptance grep gates
- `npx tsc --noEmit` exit 0 — PASS
- `grep -c 'assertA31' tests/uat/extension-page-harness.ts` returns 3 ≥ 3 — PASS
- `grep -c 'A31_PASSWORD_SENTINEL' tests/uat/extension-page-harness.ts` returns 2 ≥ 2 — PASS
- `grep -c "'secret-do-not-log-123'" tests/uat/extension-page-harness.ts` returns 1 == 1 — PASS
- `grep -c 'driveA31' tests/uat/lib/harness-page-driver.ts` returns 2 ≥ 2 — PASS
- `grep -c 'driveA31' tests/uat/harness.test.ts` returns 6 ≥ 3 — PASS
- `grep -c 'secret-do-not-log-123' tests/uat/lib/harness-page-driver.ts` returns 1 == 1 — PASS
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exit 0 with stdout containing `UAT harness: 32/32 assertions passed` — PASS
- `npm test -- --run tests/background/no-test-hooks-in-prod-bundle.test.ts` exit 0 (Tier-1 inventory stays at 12) — PASS
- `git diff src/content/index.ts` empty (verification-only contract) — PASS
## Issues Encountered
**One latent issue surfaced (PRE-EXISTING; out of scope for this plan; defer to Plan 03-05 + Phase 4):**
- **A29 zip-mtime race-condition flake (pre-existing; documented in 03-02-SUMMARY.md "Issues Encountered").** During Plan 03-03 empirical verification of A31, the pre-existing A29 flake re-surfaced: A29 SAVEs to the harness page tab (chrome-extension://...), which has no content script attached, so the SW's GET_RRWEB_EVENTS bridge logs "Could not establish connection. Receiving end does not exist." and the produced zip's rrweb/session.json is empty. A29's `findLatestZip` host-side helper then non-deterministically returns one of:
- **(a) A29's own empty zip** (mtime tiebreaker resolves A29's zip last) — A29.2/A29.3/A29.4/A29.5 FAIL with `events.length=0`
- **(b) A prior real-content zip** (mtime tiebreaker resolves a prior zip, e.g., A27's zip with iana.org rrweb events) — A29.2/A29.3/A29.4/A29.5 PASS with `events.length=4`
- **Empirical pre-existing flake reproduction:** 3 UAT runs at base HEAD `de398347` (no Plan 03-03 changes applied) — 2/3 PASS, 1/3 FAIL with the exact same symptom (`A29 events.length=0` on FAIL; `A29 events.length=4` on PASS). This empirically confirms the flake is INDEPENDENT of Plan 03-03's changes — pre-existing in the base.
- **Per CLAUDE.md SCOPE BOUNDARY rule:** "Only auto-fix issues DIRECTLY caused by the current task's changes. Pre-existing warnings, linting errors, or failures in unrelated files are out of scope." The A29 flake is documented as a deferred-items entry per Plan 03-02 SUMMARY's explicit recommendation. Plan 03-03 captured A31 GREEN evidence in a run where A29 happened to PASS (race won), demonstrating that Plan 03-03's own contract (A31 GREEN with 5/5 checks; UAT 32/32 GREEN) is achievable.
- **Recommended follow-up (per Plan 03-02 SUMMARY + this plan):** A Phase 3 amendment plan (e.g., 03-01a) or a Phase 4 hardening pass re-targets A29 to use the same cs-injection-world probe-tab pattern A30 introduced + A31 reused. The fix is mechanical (replace assertA29 page-side body with cs-injection-world variant + drive a DOM mutation in the probe tab's ISOLATED world) and removes the mtime-race ambiguity. Plan 03-05 VERIFICATION.md should flag this as a known follow-up for §10 #4 closure rigor (the current closure stands on A30-style empirical evidence; A29 needs the same hardening).
## Threat Flags
None new. The plan's `<threat_model>` (T-03-03-01..T-03-03-04) was analyzed at planner-time; implementation honors all mitigations:
- **T-03-03-01 (Information Disclosure — sentinel value lands in event log despite filter):** disposition `mitigate`. A31 IS the negative-assertion mitigation. A31.2 + A31.3 both PASS empirically (counts=0). RED would mean the filter regressed; the test enforces the invariant. Sentinel is not a real secret (per RESEARCH §"Security Domain"); if it leaked, it would be visible in events.json which is logged locally 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):** disposition `mitigate`. A31 rides the production input listener + line-82 filter + chrome.tabs + chrome.scripting + existing helpers. No `__MOKOSH_UAT__`-gated symbols. FORBIDDEN_HOOK_STRINGS unchanged at 12 entries; 13/13 unit-gate sub-tests GREEN; 12 strings × 0 hits in dist/. UAT A0 mirror unchanged at 12.
- **T-03-03-03 (Tampering — A31 SAVE produces a zip in the per-run downloadsDir that contains the sentinel only IF the filter regressed):** disposition `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):** disposition `mitigate`. A31 checks THREE orthogonal paths after Rule 2 addition: (a) sentinel-value absence, (b) password-selector-target absence, (c) control-sentinel presence (defense-in-depth: proves the listener is alive). All three pass IFF the listener is running AND the line-82 filter early-returns BEFORE addUserEvent for the password input. A regression in the filter would cause AT LEAST ONE of (a)+(b) to RED (sentinel would land in .value, or events would target #probe-password). A regression in the listener itself would cause (c) to RED (control event missing). Strictly stronger than the plan-spec 2-orthogonal-paths mitigation.
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.
## Phase 3 Wave Sequencing
Per CONTEXT D-P3-01 + RESEARCH Pitfall 6: Plans 03-01..04 modify the SAME three harness files (extension-page-harness.ts, harness-page-driver.ts, harness.test.ts). RESEARCH §"Wave Sequencing Note" recommended SEQUENTIAL execution within Wave 2: 03-01 (A29) → 03-02 (A30) → 03-03 (A31, this plan) → 03-04 (A32 RAM optional). Plan 03-03 runs AFTER 03-01 + 03-02 (depends_on: [01, 02]) — Wave 3 sequence honored.
## Next Plan Readiness
- **Plan 03-04 (§10 #9 RAM best-effort):** Independent surface (puppeteer.Page.metrics scaffolding); A31's cs-injection-world pattern doesn't apply directly (RAM measurement is host-side via CDP Performance.getMetrics). Plan 03-04 can proceed with no Plan 03-03 dependency interactions.
- **Plan 03-05 (§10 sweep VERIFICATION.md aggregator):** Inherits A31 as the binding §10 #8 PARTIAL empirical gate. Frontmatter shape per RESEARCH §"Code Examples → VERIFICATION.md frontmatter template" includes `overrides_applied: 1` with explicit `override_notes` block citing D-P3-02 charter + A31 GREEN evidence. Plan 03-05 SHOULD ALSO flag the A29 flake under "Forward-Looking Deferred Items" (Phase 4 hardening candidate).
## Self-Check: PASSED
- A31 assertion added: CONFIRMED via git log + grep (`assertA31` 3 hits in extension-page-harness.ts; `driveA31` 2 hits in harness-page-driver.ts + 6 in orchestrator).
- UAT count: 31 → 32 GREEN: **EMPIRICALLY CONFIRMED** via `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` exit 0; 32/32 assertions PASSED (A1..A30 + A31).
- vitest 171/171 GREEN preserved: CONFIRMED (full suite; 31 test files).
- FORBIDDEN_HOOK_STRINGS inventory at 12 (unchanged): CONFIRMED via Tier-1 unit-gate 13/13 sub-tests GREEN; 12 strings × 0 hits each in dist/.
- Sentinel password value does NOT appear in logs/events.json after A31 run: CONFIRMED via `A31 sentinel-containing count=0`; `password-targeting count=0`; with `control-containing count=1` proving the listener IS alive (defense-in-depth).
- No new masking surfaces added: CONFIRMED (zero src/content/index.ts edits; no rrweb maskInputFn; no data-sensitive attribute guards).
- No modifications to src/content/index.ts: CONFIRMED (`git diff src/content/index.ts` empty; `git log de398347..HEAD -- src/content/index.ts` empty).
- tsc clean: CONFIRMED (`npx tsc --noEmit` exit 0).
- 2/2 plan tasks committed atomically (`8db629f` Task 1 + `34b36fb` Task 2).
- SUMMARY.md created (committed in the final docs commit).
### File existence verification
```
FOUND: tests/uat/extension-page-harness.ts (assertA31 + window.__mokoshHarness entry)
FOUND: tests/uat/lib/harness-page-driver.ts (driveA31)
FOUND: tests/uat/harness.test.ts (driveA31 import + Wrapped const + drivers push + banner)
FOUND: .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-SUMMARY.md (this file)
```
### Commit verification
```
FOUND: 8db629f feat(03-03): Task 1 — assertA31 page-side orchestrator (cs-injection-world password-filter probe)
FOUND: 34b36fb feat(03-03): Task 2 — driveA31 + orchestrator wiring (A31 password-filter PARTIAL)
```
---
*Phase: 03-spec-10-smoke-verification-dom-event-log-verification*
*Plan: 03*
*Completed: 2026-05-20*