Files
mokosh/.planning/phases/04-harden-clean-up-optional/04-03-SUMMARY.md
Mark 303644f8cc docs(04-03): complete harden-clean-up-optional plan 04-03 — A29 flake fix
A29 (rrweb DOM verification) rewritten in-place via the canonical cs-
injection-world pattern + strict-sentinel filter. Closes ~2/3 flake
documented in Plans 03-02 + 03-03 SUMMARYs (A29 was "passing" by
reading iana.org leftover DOM events from A27/A28's still-open probe
tabs; a real rrweb regression at src/content/index.ts:284 would have
been masked).

Plan 04-03 task commits (atomic; sequential foreground mode):
- 73eb9b6: Task 1 — A29 page-side cs-injection-world skeleton +
  sentinel-bearing <div> injection
- b341a71: Task 2 — A29 host-side strict-sentinel filter (RESEARCH Q3
  Code Example Pattern 3); IncrementalSource added to @rrweb/types
  import binding; A29.2 PASS × 5/5 consecutive UAT runs

Empirical evidence:
- vitest 183/183 GREEN preserved (Plan 04-02 baseline)
- UAT harness 33/33 GREEN × 5 consecutive runs
- A29 strict-sentinel: mutationEvents=1, sentinelEvents=1 in ALL 5 runs
- Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12
- SW chunk: 0 new Function, 0 eval (Plan 04-02 baseline held)
- Pre-checkpoint bundle gates 5/5 PASS

STATE.md + ROADMAP.md updated per sequential workflow:
- Plan counter advanced 3 → 4 of 7
- Progress 83% → 87% (26/30 plans complete)
- Decision log entry added for Plan 04-03
- ROADMAP Phase 4 04-03 row flipped to [x]
2026-05-21 17:01:58 +02:00

218 lines
26 KiB
Markdown
Raw Permalink 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: 04-harden-clean-up-optional
plan: 03
subsystem: testing
tags:
- uat-harness
- a29-rewrite
- cs-injection-world
- flake-stabilization
- strict-sentinel
- rrweb
- incremental-source-mutation
- charter-d-p4-01
- phase-4-wave-2
requires:
- phase: 03-spec-10-smoke-verification-dom-event-log-verification
provides: "Plan 03-02 assertA30 + driveA30 cs-injection-world pattern (chrome.tabs.create probe tab on https://example.com + chrome.scripting.executeScript world:'ISOLATED' + try/finally chrome.tabs.remove silent-ignore via T-02-04-04). Plan 03-03 assertA31 + driveA31 sentinel-based negative-assertion precedent. Both established at tests/uat/extension-page-harness.ts + tests/uat/lib/harness-page-driver.ts."
- phase: 03-spec-10-smoke-verification-dom-event-log-verification
provides: "Plan 03-01 assertA29 + driveA29 page-side-orchestrator scaffold + __mokoshHarness wiring slot (A29 was rewritten in-place; orchestrator wire stays at 33 assertions)."
- phase: 02-stabilize-export-pipeline
provides: "Plan 02-04 A27 multi-tab pattern (chrome.tabs.create + finally cleanup; T-02-04-04 mitigation parity); findLatestZip helper; JSZip host-side parse pattern; DEC-011 Amendment 1 tabs permission grant (already provisioned for A27/A30/A31; no manifest change in Plan 04-03)."
- plan: 04-01
provides: "Audit P1 polish closure at src/content/index.ts (handleNavigation + fetch Request narrowing + rrweb timestamp normalization). Plan 04-03 verifies the rrweb pipeline at src/content/index.ts:285 end-to-end."
- plan: 04-02
provides: "vitest 183/183 baseline + setimmediate polyfill replacement + 0 new Function in SW chunk + generate-icons.cjs ESM/CJS fix. Plan 04-03 preserves the 183 baseline and the SW CSP-safe invariant."
provides:
- "A29 strict-sentinel verification: rrweb captures the page-side-injected DOM mutation containing 'a29-mutation-sentinel' in the IncrementalSnapshot payload — proves the rrweb pipeline at src/content/index.ts:285 is genuinely live on the SAVE-active tab (closes ~2/3 flake documented in Plans 03-02 + 03-03 SUMMARYs)"
- "A29 cs-injection-world rewrite: chrome.tabs.create(https://example.com) + chrome.scripting.executeScript world:'ISOLATED' + sentinel-bearing <div> injection + try/finally chrome.tabs.remove silent-ignore (verbatim port of assertA30/assertA31 skeleton)"
- "Host-side strict-sentinel filter pipeline (RESEARCH Q3 Code Example Pattern 3): filter events by (EventType.IncrementalSnapshot && IncrementalSource.Mutation) → filter mutation events for adds[*].node.textContent containing the sentinel string"
- "`import { EventType, IncrementalSource } from '@rrweb/types'` — extended binding list adds IncrementalSource alongside the prior EventType import"
- "5 module-local constants in assertA29: A29_SAVE_ARCHIVE_TIMEOUT_MS=15s, A29_SEGMENT_SETTLE_MS=11s, A29_MUTATION_SETTLE_MS=500ms, A29_TAB_NAVIGATION_WAIT_MS=1.5s, A29_PROBE_TAB_URL='https://example.com/', A29_MUTATION_SENTINEL='a29-mutation-sentinel', A29_PROBE_DIV_ID='a29-probe-mutation'"
- "1 module-local constant in driveA29: A29_MUTATION_SENTINEL='a29-mutation-sentinel' (kept in lockstep with page-side constant via the named-string convention)"
affects:
- "Any future Phase 4+ harness work that targets the rrweb pipeline (now follows the strict-sentinel pattern — loose EventType greps are deprecated). The pattern is canonical across A29 (mutation source) + A30 (event-log type-presence) + A31 (negative-assertion + control sentinel)."
- "Phase 4 closure (Plan 04-08 or equivalent VERIFICATION aggregator) should cite this plan as 'closes the A29 iana.org-leftover-flake' under the 'Forward-Looking Deferred Items' table from 03-VERIFICATION.md (item: 'A29 iana.org leftover race')."
- "Future rrweb v2-stable upgrade plan (deferred to v1.1+; D-P3-03 + D-P4-01 charter): the IncrementalSource enum bindings used here are version-pinned to rrweb 2.0.0-alpha.4 (verified via node_modules/@rrweb/types/dist/index.d.ts:295). The upgrade plan should re-verify enum stability."
tech-stack:
added: []
patterns:
- "Strict-sentinel filter for rrweb pipeline verification (NEW for Plan 04-03 / Phase 4): instead of asserting events.length > 0 + presence of EventType.{Meta,FullSnapshot,IncrementalSnapshot}, descend into IncrementalSnapshot.data.source === IncrementalSource.Mutation AND data.adds[*].node.textContent for a page-side-injected sentinel string. PROVES the captured mutation came from our injection (not from leftover probe-tab DOM noise). Reusable for any future assertion verifying rrweb captures specific application-level DOM changes."
- "cs-injection-world pattern applied to rrweb verification (extends the Plan 03-02 / 03-03 pattern from event-log verification to rrweb DOM verification): chrome.tabs.create(https://example.com) + chrome.scripting.executeScript world:'ISOLATED' targeted at the content-script's realm where rrweb's MutationObserver lives. The MAIN-world alternative would have the mutation visible to the DOM but invisible to rrweb's observer (per RESEARCH Pitfall 5)."
key-files:
modified:
- "tests/uat/extension-page-harness.ts — assertA29 page-side rewrite (7-step cs-injection-world skeleton mirroring assertA30 lines 3517-3636); banner block rewritten to cite RESEARCH Q3 + Plan 03-02/03-03 flake provenance; 7 module-local constants added (A29_TAB_NAVIGATION_WAIT_MS, A29_PROBE_TAB_URL, A29_MUTATION_SENTINEL, A29_PROBE_DIV_ID); A29_SAVE_ARCHIVE_TIMEOUT_MS + A29_SEGMENT_SETTLE_MS + A29_MUTATION_SETTLE_MS retained from pre-rewrite. assertA30 + assertA31 untouched. __mokoshHarness window registration unchanged (still references assertA29). Net change: +157 lines / -53 lines."
- "tests/uat/lib/harness-page-driver.ts — driveA29 host-side rewrite with strict-sentinel filter pipeline (RESEARCH Q3 Code Example Pattern 3); import { EventType, IncrementalSource } from '@rrweb/types' (IncrementalSource added alongside the existing EventType); module-local A29_MUTATION_SENTINEL='a29-mutation-sentinel' constant kept lockstep with page-side; filter-pipeline form (no for/continue); A29.2 = strict sentinel check (THE primary check); A29.3 + A29.4 preserved as defense-in-depth Meta + FullSnapshot presence; pre-rewrite A29.5 (loose IncrementalSnapshot >=1) retired (subsumed by stricter A29.2). driveA30 + driveA31 untouched. Net change: +99 lines / -29 lines."
key-decisions:
- "Mechanical verbatim port of assertA30/assertA31 skeleton — no architectural deviation from established cs-injection-world pattern (RESEARCH Q3 explicit recommendation). The fix is intentionally surgical: same 7-step flow as A30/A31 but with a single sentinel-bearing DOM mutation injected via chrome.scripting.executeScript world:'ISOLATED' instead of A30's 5 synthetic event triggers or A31's password-vs-control input pair."
- "Strict-sentinel filter (not loose) — RESEARCH Q3 Pitfall 3 explicitly rejects the loose `events.length > 0` strategy because it doesn't close the iana.org-leftover gap. Empirical proof: pre-rewrite A29 'PASSED' at ~2/3 success rate even when the rrweb pipeline could have been regressing silently (Plans 03-02 + 03-03 SUMMARYs). The strict filter requires the EXACT string 'a29-mutation-sentinel' that ONLY our injection can produce — no iana.org / example.com / any-other-tab path generates it."
- "Defense-in-depth A29.3 (Meta) + A29.4 (FullSnapshot) preserved — they remain valuable as 'rrweb wired up at all' indicators even though A29.2 strict-sentinel is stronger. If A29.2 fails but A29.3 + A29.4 PASS, the diagnostic is 'rrweb is alive but MutationObserver is broken or unscheduled'. If A29.2/A29.3/A29.4 all fail simultaneously, the diagnostic is 'rrweb is not wired up at all'. The renumbered scheme makes A29.2 the canonical primary check at the front of the list."
- "Pre-rewrite A29.5 (loose IncrementalSnapshot >=1 EventType grep) retired in favor of A29.2 strict-sentinel — strictly stronger contract. The loose A29.5 would have continued to 'pass' from iana.org leftovers in degenerate flake cases (the exact behavior Plan 04-03 closes). Keeping both would have been redundant; keeping only the loose one would have left the flake; keeping only the strict one (A29.2) is the canonical solution."
- "IncrementalSource enum is rrweb 2.0.0-alpha.4 alpha-pinned; verified via node_modules/@rrweb/types/dist/index.d.ts:295 (Mutation=0 first declaration). If/when the v2-stable upgrade lands (deferred per D-P3-03 + D-P4-01 charter), the upgrade plan needs to re-verify the enum value stability — but the import-binding form (`{ EventType, IncrementalSource }`) is enum-source-agnostic so the runtime contract holds across versions if Mutation stays at 0."
- "Filter-pipeline form preserved (no `continue`, no for-of with mutation-counters; chained `.filter(...)` + `.some(...)` over fold-style loops). CLAUDE.md Control Flow § honored."
- "FORBIDDEN_HOOK_STRINGS impact = 0 entries. A29 rewrite rides production chrome.tabs.create (DEC-011 Amendment 1 grant) + chrome.scripting.executeScript (already in manifest permissions) + existing setupFreshRecording / sendMessageWithTimeout helpers. No new `__MOKOSH_UAT__`-gated test-only symbols. Tier-1 inventory stays at 12 entries (verified via tests/background/no-test-hooks-in-prod-bundle.test.ts 13/13 sub-tests PASS)."
- "Saved-memory feedback-trust-harness-over-manual-uat.md honored — no operator empirical UAT requested for this plan. A29 is fully harness-verified; the 5/5 PASS stress test across consecutive runs is empirical proof that the flake is closed. Operator time reserved for genuinely non-automatable verification (A32 RAM is the canonical exception)."
patterns-established:
- "Strict-sentinel rrweb verification pipeline (NEW pattern for any future plan touching rrweb DOM capture): `events.filter(e => e.type === EventType.IncrementalSnapshot && e.data?.source === IncrementalSource.Mutation).filter(e => (e.data?.adds ?? []).some(a => typeof a?.node?.textContent === 'string' && a.node.textContent.includes(SENTINEL)))`. Replaces all loose EventType-grep patterns. The sentinel string MUST be page-side-injected via chrome.scripting.executeScript world:'ISOLATED' so it ends up in the content-script's rrweb buffer."
- "Lockstep page-side / host-side sentinel constant: the literal string ('a29-mutation-sentinel') is repeated as a `const` in BOTH tests/uat/extension-page-harness.ts AND tests/uat/lib/harness-page-driver.ts. Future maintainers MUST keep both in sync (changing one without the other silently breaks the test). The 04-03 review comment in driveA29 explicitly cites this lockstep requirement."
requirements-completed: []
# Metrics
duration: "~46 min"
completed: 2026-05-21
---
# Phase 04 Plan 03: A29 cs-injection-world rewrite + strict-sentinel flake fix
**A29 (rrweb DOM verification) rewritten in-place using the canonical cs-injection-world pattern (verbatim port from Plan 03-02 assertA30 + Plan 03-03 assertA31). chrome.tabs.create opens a fresh https://example.com probe tab, chrome.scripting.executeScript world:'ISOLATED' injects a sentinel-bearing `<div>` into the content-script's realm where rrweb's MutationObserver lives, SAVE_ARCHIVE harvests rrweb/session.json from that tab. Host-side driveA29 applies a strict-sentinel filter (IncrementalSource.Mutation + adds[*].node.textContent.includes('a29-mutation-sentinel')) that PROVES the captured mutation came from OUR injection — closes the documented ~2/3 flake where A29 "passed" by reading iana.org leftover DOM events from A27/A28's still-open probe tabs. UAT harness 33/33 GREEN preserved; A29 flake rate: 5/5 PASS across consecutive runs (was ~2/3 historical baseline); vitest 183/183 GREEN preserved; Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12.**
## Performance
- **Duration:** ~46 min (Phase 4 Wave 2; third plan)
- **Started:** 2026-05-21T13:43:00Z (executor spawn; ~2 min after Plan 04-02 closure)
- **Completed:** 2026-05-21T14:27:10Z (this SUMMARY draft)
- **Tasks:** 2 of 2 plan tasks complete (both autonomous)
- **Files modified:** 2 (tests/uat/extension-page-harness.ts + tests/uat/lib/harness-page-driver.ts; no production src/* changes; no manifest changes; no new dependencies)
## Accomplishments
- **A29 cs-injection-world rewrite** (Task 1): assertA29 page-side body replaced in-place with the 7-step skeleton mirroring assertA30 (lines 3517-3636) + assertA31 (lines 3788-3917). Setup → chrome.tabs.create probe tab → 1.5s nav wait → 11s segment settle → chrome.scripting.executeScript world:'ISOLATED' sentinel-bearing `<div>` injection → 500ms MutationObserver enqueue settle → SAVE_ARCHIVE → try/finally chrome.tabs.remove with silent-ignore. The pre-rewrite "harness-page DOM mutation" approach (which empirically never had a content script attached because `<all_urls>` doesn't cover chrome-extension://) is retired.
- **A29 strict-sentinel filter** (Task 2): driveA29 host-side body replaced in-place with the filter-pipeline form per RESEARCH Q3 Code Example Pattern 3. Adds `IncrementalSource` to the `@rrweb/types` import binding list. Module-local `A29_MUTATION_SENTINEL='a29-mutation-sentinel'` constant kept lockstep with page-side. The check entry "A29.2: rrweb captured the injected mutation containing 'a29-mutation-sentinel' (closes iana.org-leftover-flake from Plan 03-02/03-03)" is THE primary verification — preserves A29.3/A29.4 (Meta + FullSnapshot presence) as defense-in-depth.
- **Flake closure empirically verified**: 5/5 PASS across 5 consecutive `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` runs (was ~2/3 historical baseline per Plan 03-03 SUMMARY). Every run: A29 mutationEvents=1, sentinelEvents=1 — the injected sentinel-bearing mutation is the SOLE mutation captured (clean signal; example.com probe tab has minimal DOM churn).
- **vitest baseline preserved**: 183/183 GREEN (full suite, 36 test files; 12.97s). Plan 04-02 baseline (183) maintained — no regression.
- **tsc clean**: `npx tsc --noEmit` exit 0 throughout both task commits.
- **Pre-checkpoint bundle gates** (per saved memory `feedback-pre-checkpoint-bundle-gates.md`): Tier-1 FORBIDDEN_HOOK_STRINGS 13/13 sub-tests PASS (12 strings × 0 hits); SW CSP-safety `new Function`=0 + `eval`=0 (Plan 04-02 baseline held); Buffer/window/document counts unchanged from Plan 04-02 (Plan 04-03 modifies tests/ only — no production bundle changes); manifest validates clean against locked DEC-011 Amendment 1 permission set.
## Task Commits
Each plan task was committed atomically with normal git commits + pre-commit hooks (sequential foreground mode):
1. **Task 1: A29 page-side rewrite (cs-injection-world + sentinel)**`73eb9b6` (feat). Replaces assertA29 body with the 7-step cs-injection-world skeleton mirroring assertA30 + assertA31. Adds 4 new module-local constants (A29_TAB_NAVIGATION_WAIT_MS, A29_PROBE_TAB_URL, A29_MUTATION_SENTINEL, A29_PROBE_DIV_ID); retains 3 from pre-rewrite (A29_SAVE_ARCHIVE_TIMEOUT_MS, A29_SEGMENT_SETTLE_MS, A29_MUTATION_SETTLE_MS). Banner block extended to cite RESEARCH Q3 + the documented Plan 03-02/03-03 flake. assertA30 + assertA31 untouched. tsc clean; build:test clean; grep gates PASS (A29_MUTATION_SENTINEL ≥2, sentinel literal ≥1, world:'ISOLATED' ≥3 across A29+A30+A31, chrome.tabs.create.*A29_PROBE_TAB_URL ≥1). Net change: +157 lines / -53 lines.
2. **Task 2: A29 host-side strict-sentinel filter + 5/5 PASS stress test**`b341a71` (feat). Replaces driveA29 body with strict-sentinel filter pipeline per RESEARCH Q3 Code Example Pattern 3. Adds IncrementalSource to the @rrweb/types import binding list. Module-local A29_MUTATION_SENTINEL constant kept lockstep with page-side. A29.2 strict-sentinel check is THE primary check; A29.3 + A29.4 (Meta + FullSnapshot) preserved as defense-in-depth. Filter-pipeline form (no for/continue). UAT empirical verification: 33/33 GREEN × 5 consecutive runs; vitest 183/183 GREEN; pre-checkpoint bundle gates 5/5 PASS. Net change: +99 lines / -29 lines.
## Files Created/Modified
- `tests/uat/extension-page-harness.ts` — assertA29 rewritten in-place (lines 3318-3506 — banner + 7 constants + new 7-step function body). assertA30 (lines 3508+) + assertA31 (lines ~3680+) untouched; __mokoshHarness window registration unchanged.
- `tests/uat/lib/harness-page-driver.ts` — driveA29 rewritten in-place (lines 1854-2057 — banner + 1 constant + new strict-sentinel filter pipeline). IncrementalSource added to @rrweb/types import (line 42). driveA30 + driveA31 untouched.
## Decisions Made
See `key-decisions` frontmatter list above for the full decision log. Headline decisions:
1. Mechanical verbatim port of assertA30/assertA31 skeleton — no architectural deviation from RESEARCH Q3's explicit recommendation.
2. Strict-sentinel filter (not loose) — only way to close the documented iana.org-leftover flake.
3. Defense-in-depth A29.3 + A29.4 preserved — they provide diagnostic differentiation if A29.2 fails.
4. Pre-rewrite A29.5 (loose IncrementalSnapshot >=1) retired — strictly subsumed by A29.2 strict-sentinel.
5. FORBIDDEN_HOOK_STRINGS impact = 0 (no new __MOKOSH_UAT__-gated symbols; Tier-1 stays at 12).
## Deviations from Plan
**None - plan executed exactly as written.**
The plan's Task 1 + Task 2 specifications matched the implementation 1-to-1. The RESEARCH Q3 Code Example Pattern 3 host-side filter was applied verbatim. The assertA30 cs-injection-world skeleton was ported verbatim with A29-specific substitutions (single-mutation injection vs. A30's 5-trigger injection vs. A31's password+control input pair).
### Minor implementation specifics not in plan
- **eslint-disable comments on `any` types** in driveA29: the rrweb v2 event shape is polymorphic (the `data` field's structural type depends on `data.source`); the driver only reaches a few fields (source, adds, node.textContent). Three `eslint-disable-next-line @typescript-eslint/no-explicit-any` comments + explanatory comments added to keep the loose-type access localized + documented rather than rippling a strict @rrweb/types polymorphic-discriminated-union type into the driver. Plan did not specify type-strictness handling; this is the minimal idiomatic TypeScript approach.
- **A29.5 retirement is implicit in the plan**: the plan said "PRESERVE any existing A29.0a or A29.3+ checks (Meta/FullSnapshot presence) if they're valuable as defense-in-depth; the strict-sentinel A29.2 is THE primary check that closes the flake." The pre-rewrite A29.5 (loose IncrementalSnapshot >=1) is strictly subsumed by A29.2 (which requires IncrementalSnapshot AND Mutation source AND injected sentinel — a stronger contract). Keeping both would have been redundant + would have left a degraded check that A29.2 makes irrelevant. The new check ordering: A29.1 SAVE ack → A29.0a session.json present → A29.2 strict-sentinel → A29.3 Meta → A29.4 FullSnapshot.
- **Acceptance criterion `grep -cE "A29\.2:.*sentinel"` minor regex-vs-source mismatch**: the criterion as written expected the literal lowercase `sentinel` substring inside the source file; the implementation uses `A29.2: rrweb captured the injected mutation containing '${A29_MUTATION_SENTINEL}' ...` where `A29_MUTATION_SENTINEL` is an uppercase template-variable that resolves at runtime to lowercase `a29-mutation-sentinel`. Case-insensitive `grep -ciE "A29\.2:.*sentinel"` returns 2 (docstring + check entry). The substantive criterion ("the new strict-sentinel check entry exists") is fully satisfied via the runtime-resolved check name AND the documented intent in the surrounding docstring on line 1916 (`A29.2: STRICT-SENTINEL — at least one IncrementalSnapshot ...`).
**Total deviations:** 0 (none — plan executed exactly as written; implementation specifics above are sub-task fidelity notes, not deviations from plan contract).
**Impact on plan:** Plan executed as specified. No scope changes; no architectural decisions deferred; no checkpoint reached; no auth gates encountered.
## Issues Encountered
**None.** The cs-injection-world pattern from Plan 03-02 / 03-03 was empirically proven and the port to A29 was mechanical. The 5/5 PASS stress test confirmed the flake closure on the first attempt — no debugging cycle needed.
The only minor observation worth recording: in every UAT run, A29 mutationEvents=1 + sentinelEvents=1 — i.e., the example.com probe tab produces EXACTLY one mutation event in the 12-second observation window, and it's the one we injected. This is the cleanest possible signal (no iana.org-style mutation noise from the probe domain itself). example.com's static HTML is genuinely static — RFC 2606 reserved domains were the right choice for the cs-injection-world pattern.
## Stress Test Results
```
=== Stress test: 5 consecutive runs ===
----- run 1 ----- UAT harness: 33/33 assertions passed (A29.2 PASS; mutationEvents=1, sentinelEvents=1)
----- run 2 ----- UAT harness: 33/33 assertions passed (A29.2 PASS; mutationEvents=1, sentinelEvents=1)
----- run 3 ----- UAT harness: 33/33 assertions passed (A29.2 PASS; mutationEvents=1, sentinelEvents=1)
----- run 4 ----- UAT harness: 33/33 assertions passed (A29.2 PASS; mutationEvents=1, sentinelEvents=1)
----- run 5 ----- UAT harness: 33/33 assertions passed (A29.2 PASS; mutationEvents=1, sentinelEvents=1)
```
Logs preserved at `/tmp/04-03/uat-run-{1..5}.log` for the duration of this session.
**Historical baseline:** ~2/3 PASS (documented in Plan 03-03 SUMMARY).
**Post-rewrite:** 5/5 PASS — flake closed empirically with 5x stress.
## A29 Check Inventory (before/after)
| Check | Pre-Plan-04-03 | Post-Plan-04-03 | Rationale |
|-------|----------------|-----------------|-----------|
| A29.0 | "at least one zip in downloadsDir" | "at least one zip in downloadsDir" (unchanged; pre-condition for any host-side check) | Preserved |
| A29.0a | "rrweb/session.json entry exists" | "rrweb/session.json entry exists" (unchanged) | Preserved |
| A29.0b | "rrweb/session.json parses as JSON" (was implicit; explicit on parseErr branch) | "rrweb/session.json parses as JSON" (kept explicit on parseErr branch) | Preserved |
| A29.1 | "SAVE_ARCHIVE ack success=true" (page-side) | "SAVE_ARCHIVE ack success=true" (page-side; unchanged) | Preserved |
| A29.2 | "events.length > 0" (loose) | **"rrweb captured the injected mutation containing 'a29-mutation-sentinel' (closes iana.org-leftover-flake from Plan 03-02/03-03)" (strict)** | **REPLACED — primary verification flipped from loose count to strict sentinel match** |
| A29.3 | "rrweb emitted at least one Meta event" | "rrweb emitted at least one Meta event (defense-in-depth)" | Preserved (note added) |
| A29.4 | "rrweb emitted at least one FullSnapshot" | "rrweb emitted at least one FullSnapshot (defense-in-depth)" | Preserved (note added) |
| A29.5 | "rrweb emitted at least one IncrementalSnapshot" | RETIRED | Subsumed by stricter A29.2 (which requires IncrementalSnapshot AND Mutation AND sentinel) |
**Total checks per run:** 6 → 5. The retired A29.5 was the canonical "loose-grep-that-passed-for-the-wrong-reason"; A29.2 is strictly stronger.
## Pre-Checkpoint Bundle Gates
Per saved memory `feedback-pre-checkpoint-bundle-gates.md` — gates run against fresh `npm run build` output (`dist/`):
| Gate | Description | Result |
|------|-------------|--------|
| 1 | Tier-1 FORBIDDEN_HOOK_STRINGS (12 strings × 0 hits in dist/) | PASS — `tests/background/no-test-hooks-in-prod-bundle.test.ts` 13/13 sub-tests GREEN; inventory unchanged at 12 |
| 2 | SW CSP-safety (`new Function` / `eval(` in SW chunk) | PASS — 0 / 0 in `dist/assets/index.ts-DfBxWCT9.js` (Plan 04-02 baseline held) |
| 3 | Node-globals (`Buffer.`) in SW chunk | PASS — 3 hits pre-existing from JSZip internals (Plan 04-01/04-02 baseline; Plan 04-03 modifies tests/ only) |
| 4 | DOM-globals (`window.` / `document.`) in SW chunk | PASS — 1 / 3 hits pre-existing from shimmed-DOM in JSZip internals (Plan 04-01/04-02 baseline) |
| 5 | manifest.json validation | PASS — manifest_version=3, locked DEC-011 Amendment 1 permission set, host_permissions=`["<all_urls>"]` |
All 5 gates green. Plan 04-03 introduces zero production bundle changes (tests/ only).
## Threat Flags
None. Plan 04-03 closes a Repudiation threat (T-04-03-01 "A29 passes for the wrong reason — masking real rrweb regressions") via the strict-sentinel filter; no new surface introduced. The cs-injection-world pattern itself was risk-cleared by Plans 03-02 + 03-03 (chrome.tabs.create + chrome.scripting.executeScript ISOLATED are already production-shipped via DEC-011 Amendment 1 + manifest `scripting` permission).
## Self-Check: PASSED
- [x] `tests/uat/extension-page-harness.ts` modified: confirmed via `git log --stat 73eb9b6` (157 insertions / 53 deletions)
- [x] `tests/uat/lib/harness-page-driver.ts` modified: confirmed via `git log --stat b341a71` (99 insertions / 29 deletions)
- [x] Commit `73eb9b6` exists: confirmed via `git log --oneline -3 | grep 73eb9b6` (Task 1)
- [x] Commit `b341a71` exists: confirmed via `git log --oneline -3 | grep b341a71` (Task 2)
- [x] vitest 183/183 GREEN: confirmed via `npm test` 2026-05-21T14:26:00Z (Plan 04-02 baseline preserved)
- [x] UAT 33/33 GREEN × 5/5 runs: confirmed via stress test logs at /tmp/04-03/uat-run-{1..5}.log
- [x] A29.2 strict-sentinel PASS in all 5 runs: confirmed via `grep -c "A29.2.*PASS"` per run log
- [x] Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12: confirmed via `tests/background/no-test-hooks-in-prod-bundle.test.ts` 13/13 sub-tests PASS
- [x] tsc clean: confirmed via `npx tsc --noEmit` exit 0 after each task commit
- [x] build:test clean: confirmed via `npm run build:test` exit 0
- [x] No accidental deletions: confirmed via `git diff --diff-filter=D --name-only HEAD~1 HEAD` (empty after each task commit)
## Next Plan Readiness
Plan 04-04 (next in Wave 2 — pending validation) is ready to execute. Plan 04-03's A29 strict-sentinel pattern is now established and can be cited by any future plan that verifies rrweb DOM capture (the `IncrementalSource.Mutation` + `adds[*].node.textContent` sentinel descent is reusable for any specific application-level DOM change verification).
**Outstanding Phase 4 work:**
- Plan 04-04 (SW state persistence; ROADMAP SC #1; 5-min idle test)
- Plan 04-05 (fetch + XHR network_error harness extension; ROADMAP SC #2)
- Plan 04-06 (visual polish — cursor visibility verification + dark-logo contrast; per RESEARCH Finding 4 cursor visibility already shipped Plan 01-09, so this collapses to verification + doc correction + dark-logo work)
- Plan 04-07 / 04-08 (VERIFICATION.md aggregator + ROADMAP backfill + alpha re-distribution prep)
**No blockers introduced by Plan 04-03.** The strict-sentinel pattern is purely additive — future plans inheriting the cs-injection-world or rrweb-verification surface can adopt or extend it without any retrofit cost.
---
*Phase: 04-harden-clean-up-optional*
*Completed: 2026-05-21*