Session-2 (continuation of d614462 INCONCLUSIVE) executed disambiguation
plan and converged on a definitive verdict. Three independent observations
ruled out ALL architectural-failure hypotheses:
Step A: race-tolerant offscreen target attach (committed separately;
enabled visibility into the offscreen recorder + remux pipeline).
Step B: pre-kill and post-kill segment-count probes via the existing
`__mokoshOffscreenQuery 'get-segment-count'` bridge op (no new
test-only symbols introduced; FORBIDDEN_HOOK_STRINGS inventory
unchanged at 12 entries). Observed segments.length transition:
POST-PRIME=0 → PRE-KILL=3 → POST-KILL=3
Segments structurally survive the SW kill (offscreen still responds
to bridge query post-kill). Hypothesis A (architectural RAM loss
across SW termination) REFUTED.
Step C: SPIKE_SKIP_SW_KILL=1 env-var mode skips worker.close(). The
resulting videoSize is IDENTICAL to the canonical run (8505 bytes).
Hypothesis C (CDP-induced offscreen collateral teardown) REFUTED.
Since SW was not killed, its console listener stayed connected,
exposing the full Remux pipeline output:
[SW:Remux] Segment ts=1: 0 frames, duration=0ms, trackInfo=320x180
[SW:Remux] Segment ts=2: 0 frames, duration=0ms, trackInfo=320x180
[SW:Remux] Segment ts=3: 0 frames, duration=0ms, trackInfo=320x180
[SW:Remux] Remux complete: 0 frames, total timeline=0ms, output=8505 bytes
Each segment Blob has a valid track header (PixelWidth/Height parsed
successfully) but ZERO VP9 frames. Hypothesis B (canvas-captureStream
throttling in headless idle) CONFIRMED.
VERDICT: REFUTED-architecture (canvas-captureStream issue).
The architecture (offscreen-RAM `segments: Blob[] = []`) works
correctly; the spike's test methodology is invalid. The
`installFakeDisplayMedia` synthetic stream (canvas.captureStream(30)
on a hidden -9999px-offset 320x180 canvas) cannot sustain frame
production during a 5-min headless idle window despite the
`setInterval(drawFrame, 33ms)` belt-and-suspenders mitigation. This
matches the documented Chromium throttling of MediaRecorder on
invisible-canvas sources (Chrome bug 653548; auto-throttled-screen-capture
design doc; sendrec.eu blog "Why Canvas Breaks Your Screen Recorder").
ROUTING RECOMMENDATION (out of scope for this debug session):
- Do NOT proceed with the IndexedDB persistence plan-fix proposed by
Plan 04-04 SUMMARY. The plan-fix would NOT close SC #1 because the
spike would STILL produce 8505 bytes after IDB lands — the failure
is in the test's fake stream, not in segment persistence.
- Open a new plan slot (likely Plan 04-08 or a Phase 5 plan) that
reframes SC #1 verification methodology. Options:
(a) real getDisplayMedia in non-headless Puppeteer with
--auto-select-desktop-capture-source;
(b) video-file-backed MediaStream source (HTMLVideoElement
playing a bundled WebM) — bypasses canvas-captureStream
throttling entirely;
(c) reduce SC #1 wall-clock idle threshold to a value short
enough that canvas-captureStream survives (e.g., 30s) AND
add a separate manual operator-empirical test for 5-min.
ROADMAP SC #1 status: REMAINS OPEN. The architecture is sound; the
empirical verification gate is broken. Plan 04-04 SUMMARY's
characterization ("spike FAILED → architectural plan-fix needed") is
TECHNICALLY CORRECT on the first clause but INCORRECT on the second —
the spike's failure mode is in test infrastructure, not in production
code.
Files in this commit:
- tests/uat/spike-a33-sw-persistence.ts: added probeSegmentCount
helper using existing __mokoshOffscreenQuery bridge op; 3
checkpoints (POST-PRIME / PRE-KILL / POST-KILL); SPIKE_SKIP_SW_KILL=1
env-var skips worker.close() for Step C disambiguation.
- .planning/debug/sw-offscreen-persistence-investigation-session-2.md:
NEW session-2 debug note documenting full evidence trail + verdict
derivation + routing recommendation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan-04-04 debug session-2 root cause: the offscreen-console capture
in tests/uat/lib/launch.ts:registerOffscreenConsoleAttach matched zero
offscreen targets across 4 spike runs, creating a critical observability
gap that prevented disambiguation of Plan 04-04 Wave 0 spike failure
mode.
Empirical investigation (tests/uat/spike-diagnose-offscreen-target.ts,
NEW): when chrome.offscreen.createDocument fires, Puppeteer's
`targetcreated` event fires with `type='other'` and `url=''` BEFORE the
CDP target metadata stabilizes. The previous filter (whether
`background_page` or `page`) never matched at event time. By the time
the metadata stabilizes (visible via `browser.targets()`), the
target's type is `'background_page'` (not `'page'` — MV2's
background_page type IS still used by Chrome's CDP for invisible
extension documents, despite MV3 abolishing classic background pages).
Fix:
- Match the offscreen target by URL pattern (load-bearing criterion;
type field is intentionally unchecked because it's unreliable at
targetcreated time).
- Bind to BOTH `targetcreated` AND `targetchanged` events (the latter
fires when the URL stabilizes after navigation).
- Add a `browser.targets()` enumeration race-free safety net for
cases where the offscreen target exists at registration time.
Verification: tests/uat/spike-diagnose-offscreen-target.ts now emits
`(launch: offscreen console attached — url=chrome-extension://.../src/offscreen/index.html)`
followed by `[off:log] [OS:Recorder] Recording started ...` (zero such
lines in any prior spike run).
Test-infra correctness fix; ZERO production source changes. FORBIDDEN_HOOK_STRINGS
inventory unchanged at 12 entries. No new test-only `__MOKOSH_UAT__` symbols.
References:
- .planning/debug/sw-offscreen-persistence-investigation-session-2.md
(session-2 debug note documenting empirical root cause)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-commit-ceremony verification of Plan 04-04 Wave 0 SPIKE finding
(videoSize=8505 bytes after 5-min SW idle + Puppeteer worker.close()).
Reproducibility: 4/4 runs (incl. prior 3726eee) produced identical
8505-byte WebM. Deterministic.
Chrome docs research: chrome.offscreen DISPLAY_MEDIA reason has NO
lifetime limit; offscreen "may outlive" its SW; Puppeteer #9995 +
crbug 1371432 document CDP attach distorting SW lifecycle; chromium
auto-throttled-screen-capture + Chrome Bug 653548 document canvas-
captureStream throttling on invisible/background tabs.
Verdict: INCONCLUSIVE — the spike's 8505-byte result is consistent
with THREE competing root causes (test-invalid headless throttling;
CDP-artifact collateral teardown; architectural offscreen-RAM-loss)
and the spike cannot disambiguate between them. Observability gaps:
launch.ts:225 filters offscreen console on background_page (MV2)
when MV3 offscreen is type 'page' → zero offscreen logs in all spike
runs.
Recommendation: PAUSE the ~2-4h IndexedDB plan-fix. Three cheap
disambiguation steps (~75 min total) can isolate the actual root
cause before committing. Detailed in the debug note's
routing_recommendation block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the loose-EventType grep with a strict-sentinel filter pipeline
per RESEARCH Q3 Code Example Pattern 3:
- Import IncrementalSource from @rrweb/types (new binding alongside the
existing EventType import)
- Filter events for (e.type === EventType.IncrementalSnapshot &&
e.data?.source === IncrementalSource.Mutation)
- Descend into each filtered event's data.adds[*].node.textContent and
search for the page-side-injected 'a29-mutation-sentinel' string
- A29.2: assert sentinelEvents.length >= 1 — proves the captured
mutation came from OUR injection, not from iana.org leftovers
Defense-in-depth preserved:
- A29.3: rrweb emitted at least one Meta event (renumbered)
- A29.4: rrweb emitted at least one FullSnapshot (renumbered)
The previous A29.5 (loose IncrementalSnapshot >=1) is subsumed by the
A29.2 strict-sentinel check (which requires IncrementalSnapshot AND
Mutation source AND injected sentinel — strictly stronger).
Empirical verification (all 33/33 GREEN preserved, A29 flake closed):
- npx tsc --noEmit → 0
- npm test → 183/183 GREEN preserved (Plan 04-02 baseline)
- npm run test:uat → 33/33 GREEN × 5 consecutive runs
- A29 mutationEvents=1 + sentinelEvents=1 in ALL 5 runs (no flake)
A29 historical flake rate of ~2/3 (documented Plan 03-02 + 03-03
SUMMARYs) is closed end-to-end: the iana.org leftover DOM mutations
no longer satisfy A29 because the strict-sentinel filter requires the
EXACT string 'a29-mutation-sentinel' that only the page-side
chrome.scripting.executeScript injection produces.
Pre-checkpoint bundle gates verified (per feedback-pre-checkpoint-
bundle-gates.md):
- Gate 1: Tier-1 FORBIDDEN_HOOK_STRINGS — 13/13 sub-tests PASS, count
unchanged at 12
- Gate 2: SW CSP-safety — new Function=0, eval=0 (Plan 04-02 baseline)
- Gate 3+4: Buffer / window / document counts unchanged from Plan 04-02
(Plan 04-03 modifies tests/ only)
- Gate 5: manifest validates clean against locked DEC-011 Amendment 1
Replace harness-page-mutation approach with verbatim port of the
canonical cs-injection-world pattern from Plan 03-02 (assertA30) +
Plan 03-03 (assertA31):
- chrome.tabs.create(https://example.com/, active:true) opens probe
tab where content script + rrweb's record() attach normally
(chrome-extension:// is NOT covered by <all_urls> per Chrome
match-pattern spec; was the root flake cause)
- 1.5s tab-attach + 11s segment-settle waits (canonical A27/A30/A31)
- chrome.scripting.executeScript world: 'ISOLATED' injects a sentinel-
bearing <div> (textContent='a29-mutation-sentinel') into document.body
— rrweb's MutationObserver lives in the same ISOLATED world so the
IncrementalSnapshot's data.adds[*].node.textContent will carry the
sentinel
- 500ms MutationObserver-enqueue settle
- SAVE_ARCHIVE while probe tab is active (SW harvests rrweb/session.json
from there)
- try/finally chrome.tabs.remove with silent-ignore (T-02-04-04 parity)
A29 constants block extended: A29_TAB_NAVIGATION_WAIT_MS,
A29_PROBE_TAB_URL, A29_MUTATION_SENTINEL, A29_PROBE_DIV_ID.
This closes the documented ~2/3 success-rate flake from Plans 03-02 +
03-03 where A29 "passed" by reading iana.org leftover DOM mutations
from A27/A28's probe tabs — a real rrweb regression at
src/content/index.ts:284 would have been masked because iana.org's
home page emits plenty of mutations during normal rendering.
Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12; assertA30 + assertA31
untouched; __mokoshHarness wiring unchanged. Host-side driveA29
strict-sentinel filter lands in Task 2.
Verify:
- npx tsc --noEmit → 0
- npm run build:test → 0
- grep -c 'A29_MUTATION_SENTINEL' tests/uat/extension-page-harness.ts → 3
- grep -nE "world: 'ISOLATED'" tests/uat/extension-page-harness.ts → 3
call sites (A29 + A30 + A31) — ISOLATED parity per RESEARCH Pitfall 5
Coherent 5-edit Wave 1 GREEN landing per Plan 04-02 Task 2; RED gate from
Task 1 (`tests/build/no-new-function-in-sw-chunk.test.ts` 1-hit assertion)
flips GREEN with 0 hits of `new Function` in any SW chunk
(`dist/assets/index.ts-*.js` glob).
## Threat T-04-02-01 mitigation (Elevation of Privilege — `new Function` literal)
Three layered mechanisms cooperate to drop the CSP-unsafe `new Function`
literal from the SW chunk while preserving JSZip's zip-assembly correctness
end-to-end (REVISION iter-2 WARNING 1 empirically pinned at UAT harness 33/33):
1. **Runtime polyfill prelude** at top-of-module of `src/background/index.ts`
(BEFORE the first `import`): an inline `queueMicrotask`-based polyfill
installs `globalThis.setImmediate` at SW boot. JSZip's pre-bundled
`dist/jszip.min.js` IIFE guards its internal setimmediate polyfill behind
`if(!s.setImmediate){...}`, so the upstream offending body never executes
at runtime once our prelude has installed the safe fast-path.
2. **`vite-plugin-node-polyfills` `exclude: ['setimmediate']`** in vite.config.ts:
prevents the plugin from injecting its node-stdlib-browser-aliased
setimmediate polyfill into the chunk. NOTE: this alone is insufficient
because JSZip's `dist/jszip.min.js` ships its OWN bundled-in setimmediate
(via the package.json `"browser"` field that maps `./lib/index` →
`./dist/jszip.min.js`); the plugin's `exclude` only filters the plugin's
own contributions.
3. **`resolve.alias.setimmediate`** redirects bare-specifier `setimmediate`
requires to `src/shared/setimmediate-stub.ts` (a 22-LOC TS module that
installs the same `queueMicrotask`-based polyfill via side-effect import).
This catches any future direct `import 'setimmediate'` consumer that
bypasses the prelude.
4. **`stripSetimmediateNewFunction()` Rollup post-transform plugin** in
vite.config.ts: surgically replaces the single occurrence of
`(I=new Function(""+I))` with `(I=function(){})` in any output chunk
that contains the JSZip-bundled setimmediate IIFE. The replacement is
observably equivalent in our codepath (the parent `typeof I!="function"&&`
guard means the body never runs when I is already a function — which is
the only form JSZip ever uses — AND the runtime prelude makes the entire
IIFE body unreachable regardless). Without this plugin, JSZip's
pre-bundled distribution embeds the upstream setimmediate package's
`setImmediate.js` verbatim inside its internal CJS module registry
(slot 54), unreachable by Vite's resolve.alias or the polyfill plugin's
exclude.
## Architecture decision log
**Option α (force JSZip unbundled `lib/index.js` via `resolve.alias.jszip`)
was attempted and reverted 2026-05-21** (between commits 630d40c and this).
Empirically broke UAT harness A30+ because the unbundled entry's transitive
readable-stream-browser browser-field mapping did not propagate correctly
through Vite's resolver — the async zip-write pipeline silently produced
an empty events.json. The post-transform plugin (Option β) is the
minimum-surface fix that preserves JSZip's runtime behavior verbatim while
satisfying the textual `new Function` count = 0 invariant.
## Verification
**Build / static gates:**
- `npm run build` exits 0; SW chunk `dist/assets/index.ts-DfBxWCT9.js`
(378.92 kB) contains 0 occurrences of `new Function` (was 1 in pre-fix
`index.ts-8LkXuqac.js`).
- `npx tsc --noEmit` exits 0.
- `grep -rn 'permissions.request' src/` returns 0 hits (Plan 04-02 ROADMAP
SC #4 regression pin GREEN).
- `node generate-icons.cjs` exits 0; old `generate-icons.js` no longer
exists (rename via `git mv` preserves history).
- `grep -c "exclude: \\['setimmediate'\\]" vite.config.ts` returns 1.
- `grep -c "queueMicrotask" src/background/index.ts` returns ≥1.
- `grep -c "Resolved in Phase 4 Plan 04-02" .planning/phases/01-stabilize-video-pipeline/deferred-items.md` returns ≥1.
**Test gates:**
- Focused: `npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run` → 3/3 GREEN (Task 1's RED gate flipped GREEN).
- Full vitest: 183/183 GREEN on the clean run (180 baseline + 3 net new
from Plan 04-02 Task 1's two new files). Pre-existing intermittent flakes
per 04-01-SUMMARY Issues Encountered (blob-url-download / webm-remux /
webm-playback ffmpeg dry-run) persist across SUMMARY runs and are owned
by Plan 04-03.
**Pre-checkpoint bundle gates (per saved memory feedback-pre-checkpoint-bundle-gates.md):**
1. Tier-1 FORBIDDEN_HOOK_STRINGS: 13/13 tests GREEN; inventory unchanged at
12 strings (Plan 04-02 added no harness hooks).
2. SW CSP-safety grep: `grep -rn 'new Function\\|eval(' dist/assets/` returns
0 hits — polarity flipped from the pre-existing 1 documented exception
(the setimmediate literal). T-04-02-01 mitigation pin lands.
3. Node-globals: `Buffer.copy / .isView / .length / .push / .shift / .slice
/ .write` in SW chunk (pre-existing JSZip internals; unchanged from
04-01-SUMMARY).
4. DOM-globals: `document.createElement / .createTextNode / .documentElement
/ .F` + `window.Math / .console / .localStorage / .process` (pre-existing
JSZip text encoder fallback paths; unchanged from 04-01-SUMMARY).
5. manifest.json: present, MV3, `name: __MSG_extName__` (chrome.i18n intact).
**Empirical UAT harness (REVISION iter-2 WARNING 1):**
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` → 33/33 assertions
passed (verbatim `UAT harness: 33/33 assertions passed` in stdout).
Confirms JSZip's full SAVE → zip pipeline (A24-A32 inclusive, exercising
the in-memory MediaRecorder segments + base64 port wire + remux + zip
assembly + chrome.downloads + events.json + meta.json + screenshot)
operates correctly under the new bundle. The setimmediate polyfill
replacement preserves zip-write behavior end-to-end at the empirical
layer.
## Files
- **vite.config.ts**: imports `node:url` (fileURLToPath/URL) + `Plugin`
type from vite; adds `nodePolyfills.exclude: ['setimmediate']`;
adds `resolve.alias.setimmediate` → `src/shared/setimmediate-stub.ts`;
adds `stripSetimmediateNewFunction()` Rollup post-transform plugin
with full rationale comment.
- **src/background/index.ts**: 17-line top-of-module prelude inserted
BEFORE the first `import { Logger } ...` line. Inline `queueMicrotask`-based
setimmediate polyfill with typed widening cast (no `as any` per
CLAUDE.md). Reversible by `git revert`.
- **src/shared/setimmediate-stub.ts** (NEW): 50-LOC TS module providing
the same `queueMicrotask`-based polyfill via side-effect import.
Documented as the resolve.alias target.
- **generate-icons.js → generate-icons.cjs**: `git mv` preserving history.
Node 14+ treats `.cjs` as CJS regardless of `package.json` "type":
"module" per https://nodejs.org/api/packages.html#determining-module-system.
No code change; `require('fs')` + `require('path')` resolve cleanly.
No other references to the old `.js` path elsewhere in the codebase
outside the `.planning/` audit trail.
- **.planning/phases/01-stabilize-video-pipeline/deferred-items.md**:
appended "Resolved in Phase 4 Plan 04-02" closure block citing this
commit; details the 4-mechanism layered mitigation; documents the
Option α attempt + reversion.
References:
- .planning/phases/04-harden-clean-up-optional/04-RESEARCH.md §Q1
- .planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
§vite.config.ts + §src/background/index.ts
- Plan 04-02 threat model T-04-02-01 (Elevation of Privilege) +
T-04-02-02 (DoS — JSZip fallback compatibility; verified by UAT 33/33)
- node_modules/jszip/lib/utils.js:7 (upstream `require("setimmediate")`)
- node_modules/setimmediate/setImmediate.js (upstream polyfill source)
- Plan 01-12 Wave 7 deferred-items.md disclosure (Phase 5 → Phase 4 target)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new build-gate vitest files at `tests/build/` per Plan 04-02 Wave 0
TDD-strict RED-first contract:
- `no-new-function-in-sw-chunk.test.ts`: SW-chunk CSP-hardening grep gate.
Narrows the file walk to `dist/assets/index.ts-*.js` (the SW + loader
chunks; cf. plan-checker iter-1 BLOCKER 1 fix). RED today: 1 occurrence
of `new Function` in the SW chunk (the pre-existing `setimmediate` npm
package fallback bundled transitively by vite-plugin-node-polyfills,
per .planning/phases/01-stabilize-video-pipeline/deferred-items.md).
Flips GREEN after Task 2's setimmediate replacement lands. Build-prep
gate (npm run build + dist/assets/ existence + ≥1 SW chunk match)
precedes the grep gate so the test is self-bootstrapping under
SKIP_BUILD=0 and self-asserting under SKIP_BUILD=1.
- `dead-code-grep.test.ts`: ROADMAP SC #4 regression pin against `src/`.
Asserts absence of `permissions.request` (removed in Phase 1 Plan
01-05 SW shrink). GREEN-on-arrival today; acts as regression guard so
re-introducing the deleted permission-request flow breaks CI. The
offscreen-inline-string sub-test is documented as delegated to the
vite.config.ts review + tests/build/no-remote-fonts.test.ts (no single
literal sentinel pinnable post-Plan-01-06 collapse).
Polarity confirmation:
- Acceptance grep: `grep -v '^//' tests/build/no-new-function-in-sw-chunk.test.ts | grep -c 'new Function'` returns 3 (≥2 required).
- Acceptance grep: `grep -v '^//' tests/build/dead-code-grep.test.ts | grep -c 'permissions.request'` returns 2 (≥2 required).
- SKIP_BUILD=1 npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run: 2 passed + 1 failed (the expected RED gate).
- Full vitest: 180 passed + 3 failed (1 = this task's expected RED + 2 = pre-existing ffmpeg/ffprobe flakes per 04-01-SUMMARY Issues Encountered — owned by Plan 04-03).
References:
- .planning/phases/04-harden-clean-up-optional/04-PATTERNS.md §"tests/build/no-new-function-in-sw-chunk.test.ts" + §"tests/build/dead-code-grep.test.ts"
- .planning/phases/04-harden-clean-up-optional/04-RESEARCH.md §Q1
- Plan 04-02 threat model T-04-02-01 (Elevation of Privilege) + T-04-02-03 (Information Disclosure regression pin)
- tests/build/no-remote-fonts.test.ts (Plan 01-12 analog scaffold)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new test files at tests/content/ (NEW directory mirroring src/content/)
pin the canonical Plan 04-01 contracts; 7 of 9 tests are RED today and flip
GREEN once src/content/index.ts gains the three surgical edits in Task 2.
* tests/content/fetch-interception.test.ts (4 tests; A+C pass today via the
identity String(string)===string coincidence, B+D RED — they fetch a
`new Request(url)` and assert target === request.url under the canonical
`args[0] instanceof Request ? args[0].url : String(args[0])` narrow).
* tests/content/navigation-tracking.test.ts (3 tests; all 3 RED — popstate
+ hashchange + history.pushState wrap all read meta.previousUrl which is
permanently 'unknown' under today's `history.state?.url || 'unknown'`
emit; GREEN after module-level `let previousUrl` lands).
* tests/content/rrweb-timestamps.test.ts (2 tests; both RED — Test A asserts
rrweb-emit normalizes timestamps to Date.now()-class >1e12 instead of the
rrweb-internal page-load-relative small int; Test B regresses
cleanupOldEvents arithmetic correctness when both sides are Unix-epoch).
Scaffold mirrors tests/background/start-video-capture-no-tab.test.ts (Plan
01-09): vi.resetModules() in beforeEach, minimal chrome.* + window/document/
history/Request stubs installed on globalThis before
`await import('../../src/content/index')`. rrweb is mocked via vi.mock so the
content-script's `import { record } from 'rrweb'` short-circuits to a no-op
factory (avoids the rrweb-lib ESM-in-CJS transform crash). userEvents and
rrwebEvents are read back through the canonical GET_RRWEB_EVENTS chrome.
runtime.onMessage path the production archive pipeline uses.
Also folds in the .planning/config.json `use_worktrees: false` flip the
orchestrator staged before respawning this executor in foreground mode.
Plan: 04-01 Wave 0
Files:
- tests/content/fetch-interception.test.ts
- tests/content/navigation-tracking.test.ts
- tests/content/rrweb-timestamps.test.ts
- .planning/config.json (worktree mode disabled)
Verification (RED gate):
- npm test -- tests/content/ --run → 7 failed | 2 passed (9)
- grep -c "instanceof Request" tests/content/fetch-interception.test.ts → 5
- grep -c "previousUrl" tests/content/navigation-tracking.test.ts → 24
- grep -cE "Date\.now\(\)" tests/content/rrweb-timestamps.test.ts → 9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 carries one genuine designer-side decision: dark-surface logo contrast
strategy. Recommends Option A — `currentColor` SVG + CSS color driven via the
existing `.dark, [data-theme="dark"]` block in tokens.css (lines 234-251). Post-
research amendment: welcome.ts must swap `?url` (data URL → <img>) for `?raw`
(inline <svg> via DOMParser) because <img>-rendered SVGs do not inherit parent
CSS color — `currentColor` only resolves on inline DOM SVG.
Cursor visibility constraint (Plan 01-07 obs 2026-05-15) is listed as
behavioral-only inheritance, not a design surface — 1-line change in
src/offscreen/recorder.ts per Chrome CursorCaptureConstraint enum.
Inherits Phase 1 design system as read-only (Lora display + IBM Plex Sans UI
+ Loom palette + Mokosh mark + canonical tokens.css + 17-key i18n matrix).
Zero new tokens, zero new copy, zero new colors. PNG icons unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User invoked /gsd-plan-phase 4 and answered both gate questions before the
workflow correctly exited at the UI Design Contract gate (per workflow rule
that manual invocations cannot nested-Skill-spawn /gsd-ui-phase due to
AskUserQuestion-in-subcontext issue #1009).
Preferences saved at .plan-phase-preferences.md for the next plan-phase
invocation (after /gsd-ui-phase 4 produces UI-SPEC.md):
- UI gate: generate UI-SPEC.md first — unlike Phase 3 (false positive),
Phase 4 has genuine dark-logo work; UI-SPEC should be thin-but-real
(dark-logo design only; cursor visibility listed as inherited behavioral
change, not a design surface)
- Research gate: research first (light, ~10-20 min) — scope-limited to:
setimmediate polyfill replacement strategy + SW state persistence 5min
idle test patterns + chrome.scripting.executeScript world:'ISOLATED'
best practices for A29 cs-injection-world fix. Researcher NOT to
investigate already-deferred items (rrweb v2, SW-RAM, masking).
File auto-deletes when /gsd-plan-phase 4 honors these preferences.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the single-task Plan 03-04 closure end-to-end:
- A32 ships ~90 lines of best-effort RAM scaffolding per D-P3-04 +
RESEARCH Open Question 3 (host-side puppeteer.Page.metrics; no page-
side counterpart; no SAVE; no archive parse)
- Pitfall 2 mandatory diagnostic leads diagnostics array (T-03-04-01
Repudiation mitigation; three layers of operator-visible signal so
automation GREEN ≠ §10 #9 closure)
- UAT 32/32 → 33/33 GREEN; vitest 171/171 preserved; Tier-1
FORBIDDEN_HOOK_STRINGS unchanged at 12 (host-side API has no
production-bundle impact)
- Phase 4 inheritance path documented (per-target enumeration via
browser.targets() + createCDPSession + Performance.getMetrics for
SW + offscreen + harness page aggregate)
- Pre-existing parallel-vitest Tier-1-build-step race recurred once
(1/171); verified pre-existing across 03-02 + 03-03; not caused by
A32 changes; isolated re-run 13/13 GREEN
- Plan 03-05 wave dependency: VERIFICATION.md aggregator; will record
§10 #9 as `human_verification` regardless of A32 status
- Zero deviations: plan-spec verbatim implementation; the cleanest of
the four Wave-2/3/4 plans in Phase 3 by deviation count
A32 ships ~90 lines of best-effort RAM scaffolding per D-P3-04 +
RESEARCH Open Question 3 (recommended SHIP). Calls puppeteer.Page.metrics()
against the harness page and asserts JSHeapUsedSize is below the SPEC §10 #9
50 MB ceiling.
Page-realm scope is the load-bearing caveat (RESEARCH Pitfall 2): the MV3
service worker is a separate Puppeteer target with its own V8 isolate, so
Page.metrics() under-reports the operator-facing "extension background
RAM" measurement that §10 #9 actually requires. The binding §10 #9 gate
stays operator-driven (chrome://memory-internals OR chrome://extensions
service-worker memory display) and is recorded in Plan 03-05 VERIFICATION.md
human_verification block.
Mandatory diagnostic line emitted on EVERY run regardless of pass/fail:
"NOTE: page-realm only; SW context measurement requires
chrome://memory-internals operator verification per D-P3-04."
printAssertionResult prints diagnostics to stdout so the operator sees
the caveat in the live UAT trace, never confusing automation GREEN with
full §10 #9 closure (T-03-04-01 Repudiation mitigation).
Host-side only — no page-side assertA32, no setupFreshRecording, no
SAVE, no archive parse. driveA32 takes only `page` (no downloadsDir),
so the orchestrator pushes it bare in the drivers array without a wrapped
const. Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries
(Page.metrics is host-side puppeteer; not bundled).
Empirical: UAT harness 32/32 → 33/33 GREEN; A32.1 PASS (JSHeapUsedSize=
1909924 bytes); A32.2 PASS (1.82 MB << 50 MB). Tier-1 unit-gate 13/13
sub-tests GREEN; 12 strings × 0 hits each in dist/. vitest 171/171 GREEN.
Closes:
- Plan 03-04 must_have 'puppeteer.Page.metrics() returns a JSHeapUsedSize
value (>= 0) for the harness page realm' (A32.1)
- Plan 03-04 must_have 'JSHeapUsedSize for the harness page realm is
below 50 MB' (A32.2)
- Plan 03-04 must_have 'Driver emits an explicit diagnostic line: NOTE:
page-realm only' (Pitfall 2 gate — leads diagnostics array)
- Plan 03-04 must_have 'UAT harness exits 0 with 32 + 1 = 33/33
assertions GREEN' (empirical 33/33)
- Append driveA31 to tests/uat/lib/harness-page-driver.ts after driveA30:
- Reuses UserEvent type (Plan 03-02 import already present).
- 3-phase pattern: page.evaluate → findLatestZip → JSZip
logs/events.json parse + filter-pipeline grep for sentinel absence
+ control-sentinel presence.
- 3 host-side checks: A31.2 (eventsContainingSentinel.length === 0),
A31.3 (eventsTargetingPassword.length === 0), A31.4
(eventsContainingControl.length >= 1; defense-in-depth proves
the listener is alive so A31.2/A31.3 absences mean the filter
fired rather than a tautological "no events at all" pass).
- Standard guard checks A31.0 (zip present) + A31.0a (events.json
entry exists) + A31.0b (JSON.parse success) gate before A31.2..A31.4
per Plan 02-04 / Plan 03-01 / Plan 03-02 driveA26/A29/A30 precedent.
- Filter-pipeline form preserved (no `continue`) per CLAUDE.md
Control Flow §.
- Wire orchestrator in tests/uat/harness.test.ts:
- Add `driveA31,` to import block after `driveA30,`.
- Add `driveA31Wrapped` const after `driveA30Wrapped`.
- Add `{ name: 'A31', drive: driveA31Wrapped }` entry to drivers
array after the A30 entry with explanatory banner comment
citing the cs-injection-world precedent + the defense-in-depth
A31.4 control check.
- Append `, A31` to the orchestrator banner string.
Acceptance grep gates (post-commit):
- grep -c 'driveA31' tests/uat/lib/harness-page-driver.ts returns 2
- grep -c 'driveA31' tests/uat/harness.test.ts returns 6
- grep -c 'secret-do-not-log-123' tests/uat/lib/harness-page-driver.ts returns 1
- tsc --noEmit exit 0
A29 flake disclosure (per Plan 03-02 SUMMARY "Issues Encountered"):
- During Plan 03-03 empirical verification of A31, the pre-existing
A29 flakiness documented in 03-02-SUMMARY.md surfaced: A29 chains
off incidental zip-mtime ordering against prior assertions' zips,
so when A29's own (empty chrome-extension:// SAVE) zip mtime ties
with a prior real-content zip, findLatestZip non-deterministically
returns the prior zip with rrweb events from iana.org/example.com.
- 3 base runs (HEAD=de398347, no Plan 03-03 changes): 2/3 PASS,
1/3 FAIL — confirms PRE-EXISTING flake, NOT a Plan 03-03 regression.
- Per CLAUDE.md SCOPE BOUNDARY ("Only auto-fix issues DIRECTLY caused
by the current task's changes") + Plan 03-02 SUMMARY's explicit
recommendation ("Plan 03-05's VERIFICATION.md aggregator + a
Phase 4 hardening pass can pick it up"): A29 flake is OUT OF SCOPE
for Plan 03-03. Documented in SUMMARY as deferred item.
- Add assertA31 page-side orchestrator after assertA30: opens fresh
https://example.com probe tab via chrome.tabs.create, injects a
synthetic <input type="password" id="probe-password"> + a control
<input type="text" id="probe-control"> into the probe tab DOM via
chrome.scripting.executeScript world:'ISOLATED', types
A31_PASSWORD_SENTINEL='secret-do-not-log-123' + A31_CONTROL_SENTINEL
into each, dispatches input events, settles, SAVEs while the probe
tab is active, finally-cleanup with silent-ignore (T-02-04-04
parity).
- Add 8 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,
A31_PASSWORD_SENTINEL, A31_CONTROL_SENTINEL,
A31_PASSWORD_SELECTOR='#probe-password',
A31_PASSWORD_INPUT_ID, A31_CONTROL_INPUT_ID.
- Extend declare global Window.__mokoshHarness interface with assertA31
+ add assertA31 to window.__mokoshHarness object literal + update
statusEl banner + closing console.log to A31.
- 1 page-side check: A31.1 (SAVE_ARCHIVE ack). Host-side driveA31
(Task 2) will append A31.2 (sentinel-value-absent) + A31.3
(zero-events-targeting-password-selector) + A31.4 (control event
present — defense-in-depth proof the listener is alive, so A31.2
and A31.3 GREEN actually mean the filter fired rather than a
tautological pass from no events at all).
Rule 3 — Auto-fix blocking (cs-injection-world adaptation):
- The plan's <action> 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). With no content script on
the harness page, A31.2/A31.3 would pass tautologically (no events
captured regardless of input type — would not empirically verify
the line-82 filter "fires").
- A31 reuses the Plan 03-02 cs-injection-world pattern: probe tab on
https://example.com (where the content script attaches normally)
+ executeScript ISOLATED-world injection so production
setupInputLogging at src/content/index.ts:78 actually sees the
password input event AND its line-82 filter early-returns.
- A31.4 control-event check is added as defense-in-depth per
T-03-03-04: proves the listener IS alive, so the absence assertions
A31.2/A31.3 are not vacuously satisfied.
- Plan's binding contract (sentinel absent from logs/events.json +
zero events targeting password selector) preserved verbatim; only
the trigger mechanism changes.
FORBIDDEN_HOOK_STRINGS impact: NONE. A31 rides production
setupInputLogging + line-82 filter + chrome.tabs + chrome.scripting
(scripting perm already in manifest) + existing
setupFreshRecording/sendMessageWithTimeout helpers. Tier-1 unchanged
at 12.
- driveA30 host-side (tests/uat/lib/harness-page-driver.ts):
- import type { UserEvent } from '../../../src/shared/types' (5-type tuple grep).
- A30_EXPECTED_TYPES = ['click','input','navigation','js_error','network_error']
(canonical CON-event-log-schema 5-tuple).
- 3-phase pattern (page.evaluate stub → findLatestZip → JSZip
logs/events.json) per Plan 02-04 driveA26 analog.
- 6 host-side checks: A30.0a (entry present) + A30.2..A30.6 (5 type
presence). Filter-pipeline form; no `continue`.
- Orchestrator wiring (tests/uat/harness.test.ts):
- driveA30 import + driveA30Wrapped const + drivers-array entry with
Plan 03-02 banner; Architecture banner updated A29 -> A29, A30.
- assertA30 architectural rewrite (deviation Rule 3 — blocking fix):
The plan's original strategy "dispatch synthetic events ON the harness
page (chrome-extension://) so the production listeners on that page
fire" was empirically wrong on two counts:
1. Chrome MV3 `<all_urls>` match-pattern (Chrome match-pattern docs)
permits schemes http/https/file/ftp/urn only — NOT
chrome-extension. The harness page has NO content script attached;
the SW SAVE_ARCHIVE handler reported "Could not establish
connection. Receiving end does not exist." when the active tab was
the harness page (verified empirically 2026-05-20T17:36:25Z trace).
2. Even if (1) had been satisfied, page.evaluate-side fetch() runs in
the MAIN world while the content-script's window.fetch wrapper at
src/content/index.ts:167 patches only the content-script's
ISOLATED-world window. Page-world fetches NEVER reach the
production network_error wrapper.
Fix: A30 now creates a fresh https://example.com probe tab via
chrome.tabs.create (mirrors A27's pattern; DEC-011 Amendment 1 `tabs`
perm; `scripting` perm already in manifest); uses
chrome.scripting.executeScript with default `world: 'ISOLATED'` to
inject all 5 triggers directly in the content-script's realm; SAVEs
while the probe tab is active (SW harvests events.json from a tab
whose content script IS attached); cleans up the probe tab in finally
(T-02-04-04 silent-ignore parity). All 5 UserEvent types now land
empirically: type counts: click=1,input=1,navigation=1,js_error=1,
network_error=1; userEvents.length=5.
- UAT 30 → 31 GREEN; vitest 171/171 preserved; Tier-1 FORBIDDEN_HOOK_STRINGS
unchanged at 12 (A30 rides production chrome.tabs + chrome.scripting +
GET_RRWEB_EVENTS round-trip — no new test-only symbols).