Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -262,7 +262,14 @@ finalized at plan time):
|
||||
(`permissions.request`, the duplicate offscreen inline string) returns no
|
||||
live references.
|
||||
|
||||
**Plans**: TBD
|
||||
**Plans**: 7 plans (04-01 through 04-07). Wave 1 parallel (04-01 + 04-02) -> Wave 2 sequential (04-03 A29 rewrite -> 04-04 A33 SW persistence -> 04-05 A34 fetch+XHR) -> Wave 5 visual polish (04-06; operator empirical) -> Wave 6 closure (04-07).
|
||||
- [ ] 04-01-PLAN.md — Audit P1 polish #11 + #14 + #15 (TDD; 3 unit tests + 3 src/content/index.ts edits)
|
||||
- [ ] 04-02-PLAN.md — Build/CSP hygiene (setimmediate polyfill replacement + dead-code grep + generate-icons.cjs rename)
|
||||
- [ ] 04-03-PLAN.md — A29 cs-injection-world rewrite (strict-sentinel filter; closes ~1/3 flake)
|
||||
- [ ] 04-04-PLAN.md — A33 SW state persistence (spike-first; 5-min idle + worker.close() CDP; ROADMAP SC #1)
|
||||
- [ ] 04-05-PLAN.md — A34 fetch + XHR network_error empirical (ROADMAP SC #2; validates Plan 04-01 P1 #11 end-to-end)
|
||||
- [ ] 04-06-PLAN.md — Dark-logo currentColor + cursor visibility verification + 01-07-SUMMARY back-patch (UI-SPEC; operator empirical ack)
|
||||
- [ ] 04-07-PLAN.md — Phase 4 closure aggregator + ROADMAP backfill (D-P4-05) + v1 milestone close prep
|
||||
|
||||
## Progress
|
||||
|
||||
@@ -274,4 +281,4 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5.
|
||||
| 1. Stabilize video pipeline | 14/14 | **CLOSED 2026-05-20** via gsd-verifier audit GREEN (17/17 must-haves; commit 586836f); all markers flipped | Functional contract closed 2026-05-19 via Plan 01-13 harness PASS; design/brand contract closed 2026-05-20 via Plan 01-12 brand-fit ack; welcome-tab contract closed 2026-05-20 via Plan 01-10 cycle-2 operator ack "All good" + 5 inter-cycle debug fixes |
|
||||
| 2. Stabilize export pipeline | 0/4 | Plans landed 2026-05-20 (4 plans: Wave 0 RED → Wave 1 Blob URL + meta.urls parallel → Wave 2 harness + operator checkpoint); execution pending | - |
|
||||
| 3. SPEC §10 smoke + DOM/event-log verification | 0/TBD | Not started (absorbed Phase-2 DOM verification per 2026-05-20 re-phasing; ~2-3 plans) | - |
|
||||
| 4. Harden + clean up (optional) | 0/TBD | Not started (deferred backlog: cursor visibility, dark-surface logo, tabs perm gap, ffprobe flakes, ROADMAP backfill, rrweb-version upgrade research, REQ-password-confidentiality v2 candidate) | - |
|
||||
| 4. Harden + clean up (optional) | 0/7 | Plans landed 2026-05-21 (7 plans; 6 waves: W1 parallel pair + W2/3/4 sequential harness chain + W5 visual polish + W6 closure); execution pending | - |
|
||||
|
||||
339
.planning/phases/04-harden-clean-up-optional/04-01-PLAN.md
Normal file
339
.planning/phases/04-harden-clean-up-optional/04-01-PLAN.md
Normal file
@@ -0,0 +1,339 @@
|
||||
---
|
||||
phase: 04
|
||||
slug: harden-clean-up-optional
|
||||
plan: 01
|
||||
type: tdd
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/content/index.ts
|
||||
- tests/content/fetch-interception.test.ts
|
||||
- tests/content/navigation-tracking.test.ts
|
||||
- tests/content/rrweb-timestamps.test.ts
|
||||
autonomous: true
|
||||
requirements: []
|
||||
tags:
|
||||
- audit-p1-polish
|
||||
- p1-11-fetch-url
|
||||
- p1-14-nav-url
|
||||
- p1-15-rrweb-timestamps
|
||||
- tdd
|
||||
- content-script
|
||||
- charter-d-p4-02
|
||||
user_setup: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "fetch(new Request(url)) captures Request.url in the network_error UserEvent.target (NOT the literal string '[object Request]')"
|
||||
- "fetch(stringUrl) continues to capture the string URL in the network_error UserEvent.target (regression preserved)"
|
||||
- "popstate / hashchange navigation events carry the URL the operator just left in UserEvent.meta.previousUrl (NOT the literal string 'unknown')"
|
||||
- "rrweb emit events carry Unix-epoch timestamps (Date.now()-class, > 1e12 in 2026) so cleanupOldEvents' (now - event.timestamp) arithmetic is meaningful"
|
||||
- "vitest suite GREEN with +6 new tests across 3 new test files at tests/content/"
|
||||
artifacts:
|
||||
- path: "tests/content/fetch-interception.test.ts"
|
||||
provides: "Audit P1 #11 RED→GREEN — 3-test contract pinning Request-arg + string-arg URL extraction + network_error regression"
|
||||
contains: "instanceof Request"
|
||||
min_lines: 60
|
||||
- path: "tests/content/navigation-tracking.test.ts"
|
||||
provides: "Audit P1 #14 RED→GREEN — 3-test contract pinning module-level previousUrl tracking"
|
||||
contains: "previousUrl"
|
||||
min_lines: 60
|
||||
- path: "tests/content/rrweb-timestamps.test.ts"
|
||||
provides: "Audit P1 #15 RED→GREEN — 2-test contract pinning Unix-epoch timestamp semantics in rrweb emit + cleanupOldEvents arithmetic"
|
||||
contains: "Date.now()"
|
||||
min_lines: 50
|
||||
- path: "src/content/index.ts"
|
||||
provides: "3 surgical edits — line 174 + 190 fetch URL extraction (Request-narrow); lines 99-109 + new module-level previousUrl; lines 286-289 rrweb emit timestamp normalization"
|
||||
contains: "instanceof Request"
|
||||
key_links:
|
||||
- from: "src/content/index.ts setupNetworkLogging fetch wrapper"
|
||||
to: "addUserEvent({type:'network_error', target: <Request-narrow result>})"
|
||||
via: "args[0] instanceof Request ? args[0].url : String(args[0])"
|
||||
pattern: "instanceof Request"
|
||||
- from: "src/content/index.ts setupNavigationLogging handleNavigation closure"
|
||||
to: "module-level let previousUrl"
|
||||
via: "fromUrl = previousUrl; previousUrl = toUrl; emit with meta.previousUrl: fromUrl"
|
||||
pattern: "previousUrl = (window\\.location\\.href|toUrl)"
|
||||
- from: "src/content/index.ts initRrweb emit callback"
|
||||
to: "rrwebEvents.push(event) with event.timestamp = Date.now()"
|
||||
via: "event.timestamp = Date.now() before push"
|
||||
pattern: "event\\.timestamp = Date\\.now"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Close the three Audit P1 correctness gaps in `src/content/index.ts` per D-P4-02 ("All three — Recommended"):
|
||||
|
||||
- **P1 #11:** `args[0]?.toString()` becomes `'[object Request]'` when `fetch(new Request(url))` is called, masking the real failing URL in `events.json`. Replace with `args[0] instanceof Request ? args[0].url : String(args[0])` at both sites (line 174 ok-branch + line 190 catch-branch).
|
||||
- **P1 #14:** `history.state?.url || 'unknown'` always emits `'unknown'` in apps that don't populate `history.state` (which is almost all of them). Replace with module-level `let previousUrl` tracking — mutate on every navigation, emit the prior value in `meta.previousUrl`.
|
||||
- **P1 #15:** rrweb 2.0.0-alpha.4 emits page-load-relative timestamps (small integers); `cleanupOldEvents` at lines 27-50 does `(now - event.timestamp) < RRWEB_RETENTION_MS` arithmetic that's a category error. Normalize at emit time: `event.timestamp = Date.now()` before pushing to `rrwebEvents`.
|
||||
|
||||
TDD-strict: Wave 0 creates 3 RED test files; the source edits in Wave 1 flip them GREEN. RED phase is mandatory per workflow.tdd_mode: true.
|
||||
|
||||
Purpose: Each fix closes a real correctness bug (not stylistic). #11 unmasks the failing URL in saved archives, #14 unmasks the prior navigation URL operator left, #15 makes the rrweb cleanup arithmetic meaningful. These are operator-perceptible quality bugs in the archive contents — fixing them improves bug-reproduction value of saved sessions.
|
||||
|
||||
Output: 3 new test files at `tests/content/` (NEW directory mirroring the source-tree convention `src/content/` → `tests/content/`); +6 GREEN tests on the vitest baseline; 3 surgical edits in `src/content/index.ts` (3-5 lines each).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-CONTEXT.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-RESEARCH.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
|
||||
|
||||
# Source files — locus of P1 fixes (read once; extract patterns)
|
||||
@src/content/index.ts
|
||||
@src/shared/types.ts
|
||||
|
||||
# Analog test scaffold to mirror — pure-function-with-vi.fn-stub pattern
|
||||
@tests/background/start-video-capture-no-tab.test.ts
|
||||
@tests/background/onboarding.test.ts
|
||||
|
||||
<interfaces>
|
||||
<!-- Key shapes the executor consumes directly. Extracted from codebase 2026-05-21. -->
|
||||
|
||||
From src/content/index.ts (existing module-level state — additive pattern at lines 20-24):
|
||||
```typescript
|
||||
let rrwebEvents: EventWithTime[] = []; // line 21
|
||||
let userEvents: UserEvent[] = []; // line 24
|
||||
// Phase 4 Plan 04-01 P1 #14 — ADD at line 25 (or adjacent to existing module-level state):
|
||||
let previousUrl = ''; // initialized in initOrSetUpInitialUrl() at content script init
|
||||
```
|
||||
|
||||
From src/shared/types.ts (UserEvent shape — DO NOT MODIFY):
|
||||
```typescript
|
||||
export interface UserEvent {
|
||||
timestamp: number;
|
||||
type: 'click' | 'input' | 'navigation' | 'js_error' | 'network_error';
|
||||
target: string;
|
||||
value: string;
|
||||
url: string;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
From src/content/index.ts:174 + :190 (existing P1 #11 bug sites — TWO instances, both must be fixed):
|
||||
```typescript
|
||||
// Line ~174 (ok-branch):
|
||||
target: args[0]?.toString() || 'unknown',
|
||||
// Line ~190 (catch-branch):
|
||||
target: args[0]?.toString() || 'unknown',
|
||||
```
|
||||
|
||||
After Phase 4 Plan 04-01:
|
||||
```typescript
|
||||
// Helper at module scope OR inline in both branches:
|
||||
target: (args[0] instanceof Request ? args[0].url : String(args[0])) || 'unknown',
|
||||
```
|
||||
|
||||
From src/content/index.ts:107 (existing P1 #14 bug site):
|
||||
```typescript
|
||||
meta: { previousUrl: history.state?.url || 'unknown' }
|
||||
```
|
||||
|
||||
After Phase 4 Plan 04-01:
|
||||
```typescript
|
||||
// Module-level state add at ~line 25:
|
||||
let previousUrl = (typeof window !== 'undefined') ? window.location.href : '';
|
||||
|
||||
// In handleNavigation closure at line 100-109:
|
||||
const fromUrl = previousUrl;
|
||||
const toUrl = window.location.href;
|
||||
previousUrl = toUrl;
|
||||
addUserEvent({
|
||||
timestamp: Date.now(),
|
||||
type: 'navigation',
|
||||
target: 'window',
|
||||
value: toUrl,
|
||||
url: toUrl,
|
||||
meta: { previousUrl: fromUrl },
|
||||
});
|
||||
```
|
||||
|
||||
From src/content/index.ts:286-289 (existing P1 #15 bug site):
|
||||
```typescript
|
||||
function initRrweb() {
|
||||
record({
|
||||
emit(event) {
|
||||
rrwebEvents.push(event);
|
||||
},
|
||||
```
|
||||
|
||||
After Phase 4 Plan 04-01:
|
||||
```typescript
|
||||
function initRrweb() {
|
||||
record({
|
||||
emit(event) {
|
||||
event.timestamp = Date.now(); // Plan 04-01 P1 #15: normalize to Unix epoch for cleanupOldEvents arithmetic
|
||||
rrwebEvents.push(event);
|
||||
},
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Wave 0 RED — three content-script unit-test scaffolds (one per audit P1 item)</name>
|
||||
<files>tests/content/fetch-interception.test.ts, tests/content/navigation-tracking.test.ts, tests/content/rrweb-timestamps.test.ts</files>
|
||||
<read_first>tests/background/start-video-capture-no-tab.test.ts, tests/background/onboarding.test.ts, src/content/index.ts, .planning/phases/04-harden-clean-up-optional/04-PATTERNS.md (sections "tests/content/fetch-interception.test.ts" through "tests/content/rrweb-timestamps.test.ts")</read_first>
|
||||
<behavior>
|
||||
fetch-interception.test.ts (3 tests):
|
||||
- Test A: `fetch(stringUrl)` where response.ok=false → `addUserEvent` captures `target === stringUrl` (NOT `'[object Object]'` or `'unknown'`). RED today (works for strings; PASS-LOOKING because String(string)===string is identity).
|
||||
- Test B: `fetch(new Request(url))` where response.ok=false → captures `target === request.url` (NOT `'[object Request]'`). RED today (bug — current code emits `'[object Request]'`).
|
||||
- Test C: regression — `fetch(stringUrl)` that throws (.catch path) → captures `target === stringUrl` AND `type === 'network_error'`. Pins the catch-branch fix too.
|
||||
|
||||
navigation-tracking.test.ts (3 tests):
|
||||
- Test A: load content/index.ts; window.location starts at https://example.com/start; mutate to /next; dispatch popstate → captured UserEvent.meta.previousUrl === 'https://example.com/start' (NOT 'unknown'). RED today.
|
||||
- Test B: same shape via hashchange (location.hash mutation). RED today.
|
||||
- Test C: regression — history.pushState invocation still emits navigation with previousUrl honoring module state (not the just-pushed URL).
|
||||
|
||||
rrweb-timestamps.test.ts (2 tests):
|
||||
- Test A: stub rrweb.record so its internal callback can be triggered with a synthetic event carrying timestamp=42 (page-load-relative class); after content script initRrweb runs, the captured rrwebEvents[0].timestamp > 1e12 (Date.now()-class). RED today.
|
||||
- Test B: regression — cleanupOldEvents at lines 27-50 correctly filters; the (now - event.timestamp) arithmetic is meaningful because both sides are Unix-epoch. Build a synthetic rrwebEvents containing one event with timestamp=Date.now()-RRWEB_RETENTION_MS-1000 (older than retention) + one event with timestamp=Date.now() (fresh); after cleanup, only the fresh event remains.
|
||||
|
||||
Scaffold mirrors tests/background/start-video-capture-no-tab.test.ts:
|
||||
- `beforeEach(() => { vi.resetModules(); });`
|
||||
- Minimal `buildChromeStub()` providing `chrome.runtime.sendMessage` no-op + `chrome.runtime.onMessage.addListener` no-op (content script does not import chrome.tabs / chrome.storage directly).
|
||||
- Dynamic `await import('../../src/content/index.ts')` after stubbing window.fetch + history + rrweb (use vi.mock or stub-via-global).
|
||||
- For rrweb, mock the module: `vi.mock('rrweb', () => ({ record: vi.fn((opts) => { capturedEmit = opts.emit; }) }))`.
|
||||
</behavior>
|
||||
<action>
|
||||
1. Create `tests/content/` directory (new — no existing tests at this path).
|
||||
2. Create `tests/content/fetch-interception.test.ts` per the scaffold in 04-PATTERNS.md section "tests/content/fetch-interception.test.ts" (3-test contract). Use `vi.resetModules()` in `beforeEach`, `vi.mock('rrweb', ...)` to isolate from real rrweb (the rrweb module is irrelevant for fetch tests but the content script imports it at module top). The fetch wrapper is invoked by directly calling `window.fetch(...)` after the content script has run.
|
||||
3. Create `tests/content/navigation-tracking.test.ts` per the same scaffold (3-test contract). Set up JSDOM-style window.location via vitest's environment: 'jsdom' in vitest.config.ts (already configured per the analog test). Dispatch popstate via `window.dispatchEvent(new PopStateEvent('popstate'))`. Read captured UserEvents via a global hook installed by the test (e.g., spy on `addUserEvent` via vi.spyOn after module import, OR capture via `chrome.runtime.onMessage` listener).
|
||||
4. Create `tests/content/rrweb-timestamps.test.ts` per the same scaffold (2-test contract). Mock rrweb.record so the test can capture the `emit` callback handle, then synthesize an event with `timestamp: 42` (page-load-relative class) and assert that after `emit(synthetic)`, `rrwebEvents[0].timestamp > 1e12`.
|
||||
|
||||
All 3 files use TypeScript strict mode (project default); imports use absolute paths from project root per CLAUDE.md ("Always use absolute imports"); filter-pipeline form, no `continue` per CLAUDE.md Control Flow rules.
|
||||
|
||||
RED gate: run `npm test -- tests/content/ --run` — all 8 tests FAIL (Test A/C of fetch may pass on string args; Tests B + nav A/B + rrweb A guaranteed RED).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm test -- tests/content/ --run 2>&1 | tee /tmp/04-01-task-1-red.log; grep -c 'FAIL' /tmp/04-01-task-1-red.log</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- File `tests/content/fetch-interception.test.ts` exists with ≥ 3 `it(...)` blocks in a `describe('Audit P1 #11 ...')` block.
|
||||
- File `tests/content/navigation-tracking.test.ts` exists with ≥ 3 `it(...)` blocks in a `describe('Audit P1 #14 ...')` block.
|
||||
- File `tests/content/rrweb-timestamps.test.ts` exists with ≥ 2 `it(...)` blocks in a `describe('Audit P1 #15 ...')` block.
|
||||
- At least 4 tests FAIL when run (the RED tests for B/nav A/B/rrweb A); the regression tests may PASS pre-fix.
|
||||
- `grep -c "instanceof Request" tests/content/fetch-interception.test.ts | grep -v '^#'` returns ≥ 2 (Test B assertion + post-fix expectation).
|
||||
- `grep -c "previousUrl" tests/content/navigation-tracking.test.ts | grep -v '^#'` returns ≥ 3 (Tests A/B/C assertions).
|
||||
- `grep -cE "Date\\.now|timestamp > 1e12" tests/content/rrweb-timestamps.test.ts | grep -v '^#'` returns ≥ 2.
|
||||
</acceptance_criteria>
|
||||
<done>3 RED test files committed (8 tests; ≥ 4 RED, ≤ 4 passing-as-regression-baseline). Atomic commit: `test(04-01): Wave 0 RED — audit P1 #11/#14/#15 content-script test scaffolds`.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Wave 1 GREEN — 3 surgical edits in src/content/index.ts (P1 #11 + #14 + #15)</name>
|
||||
<files>src/content/index.ts</files>
|
||||
<read_first>src/content/index.ts (full; ~310 lines), tests/content/fetch-interception.test.ts, tests/content/navigation-tracking.test.ts, tests/content/rrweb-timestamps.test.ts</read_first>
|
||||
<behavior>
|
||||
All 8 unit tests from Task 1 flip GREEN after these 3 surgical edits.
|
||||
- Edit 1 (P1 #11): both fetch interception sites (ok-branch line ~174 + catch-branch line ~190) use `args[0] instanceof Request ? args[0].url : String(args[0])` to extract URL.
|
||||
- Edit 2 (P1 #14): add `let previousUrl = (typeof window !== 'undefined') ? window.location.href : '';` at module scope (~line 25); rewrite `handleNavigation` closure to swap `previousUrl` and emit prior value.
|
||||
- Edit 3 (P1 #15): in `initRrweb` (~line 286), prepend `event.timestamp = Date.now();` before `rrwebEvents.push(event);`.
|
||||
</behavior>
|
||||
<action>
|
||||
Read `src/content/index.ts` in full once (~310 lines, well under context limit). Extract exact line numbers for the 3 edit sites (they may have drifted since the PATTERNS.md mapping if any prior plan touched them — confirm via grep). All edits use the Edit tool with exact strings (no heredoc; no sed):
|
||||
|
||||
Edit 1 — P1 #11 fetch URL extraction (TWO sites, identical pattern):
|
||||
- At the ok-branch (currently ~line 174): replace `target: args[0]?.toString() || 'unknown',` with `target: (args[0] instanceof Request ? args[0].url : String(args[0])) || 'unknown',`
|
||||
- At the catch-branch (currently ~line 190): same replacement (the two literal strings are identical so the Edit tool needs both replacements; use the `replace_all: false` discipline by using a slightly larger surrounding string per the Edit-tool spec, OR target each separately by including unique context).
|
||||
- Optionally factor out to a helper `function extractFetchUrl(arg: unknown): string` at module scope; either is acceptable.
|
||||
|
||||
Edit 2 — P1 #14 navigation URL tracking:
|
||||
- Add at module scope (insert after the existing `let userEvents: UserEvent[] = [];` line ~24): `let previousUrl: string = (typeof window !== 'undefined') ? window.location.href : ''; // Plan 04-01 P1 #14 — module-level previous-URL tracker`
|
||||
- Rewrite the body of `handleNavigation` closure (currently lines 100-108):
|
||||
```typescript
|
||||
const handleNavigation = () => {
|
||||
const fromUrl = previousUrl;
|
||||
const toUrl = window.location.href;
|
||||
previousUrl = toUrl;
|
||||
addUserEvent({
|
||||
timestamp: Date.now(),
|
||||
type: 'navigation',
|
||||
target: 'window',
|
||||
value: toUrl,
|
||||
url: toUrl,
|
||||
meta: { previousUrl: fromUrl },
|
||||
});
|
||||
};
|
||||
```
|
||||
- Preserve the `addEventListener('popstate', handleNavigation)` + `addEventListener('hashchange', handleNavigation)` + `history.pushState/replaceState` wrap exactly as-is (no behavior change to the dispatch mechanism).
|
||||
|
||||
Edit 3 — P1 #15 rrweb emit timestamp normalization:
|
||||
- In the `record({ emit(event) { ... } })` block at ~line 286, prepend ONE line: `event.timestamp = Date.now(); // Plan 04-01 P1 #15 — normalize to Unix epoch for events.json downstream + cleanupOldEvents arithmetic correctness`. The existing `rrwebEvents.push(event);` stays unchanged.
|
||||
|
||||
TypeScript-strict expectations: tsc-clean (`npx tsc --noEmit`). The `EventWithTime = any;` alias at line 10 already opens `event.timestamp` for write; if strict-mode complains, use a typed widening cast (NEVER `as any`) per CLAUDE.md rule.
|
||||
|
||||
Run focused tests during edits: `npm test -- tests/content/ --run` should flip from 4-RED to 0-RED after all 3 edits.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm test -- tests/content/ --run 2>&1 | tee /tmp/04-01-task-2-green.log; grep -c 'PASS\\|✓' /tmp/04-01-task-2-green.log; npx tsc --noEmit 2>&1 | tee /tmp/04-01-task-2-tsc.log; grep -c 'error TS' /tmp/04-01-task-2-tsc.log</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `npm test -- tests/content/ --run` exits 0 with 8/8 GREEN (was 4-RED in Task 1).
|
||||
- `npx tsc --noEmit` exits 0 (no new type errors).
|
||||
- `grep -c "instanceof Request" src/content/index.ts` returns ≥ 2 (both sites) AND `grep -cE "args\\[0\\]\\?\\.toString" src/content/index.ts` returns 0 (old pattern removed).
|
||||
- `grep -cE "^let previousUrl" src/content/index.ts` returns 1 (the module-level state declaration).
|
||||
- `grep -cE "meta: \\{ previousUrl: fromUrl \\}" src/content/index.ts` returns 1 (the new closure emit).
|
||||
- `grep -cE "event\\.timestamp = Date\\.now\\(\\)" src/content/index.ts | grep -v '^#'` returns ≥ 1 (the rrweb emit normalization; addUserEvent at line 54 already has its own `event.timestamp = Date.now()` so total may be 2).
|
||||
- Full vitest suite passes: `npm test -- --run` exits 0 with previous baseline + 8 new tests = ≥ 179 GREEN (171 baseline + 8 new).
|
||||
</acceptance_criteria>
|
||||
<done>P1 #11 + #14 + #15 GREEN; tsc-clean; full vitest GREEN with +8. Atomic commit: `feat(04-01): Wave 1 GREEN — fix audit P1 #11 fetch URL + #14 nav URL + #15 rrweb timestamps`.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| page realm → content-script realm | `window.fetch` / `history.pushState` wrappers run in the content-script ISOLATED world after page realm dispatches; payloads (URLs, request objects) are page-controlled |
|
||||
| content script → SW (events.json downstream) | UserEvents accumulate in module-level arrays; flushed via `chrome.runtime.sendMessage` at archive-assembly time; the `meta` object is JSON-serialized verbatim |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-04-01-01 | Information Disclosure | fetch wrapper line 174/190 — `args[0]?.toString()` masks the real URL with `'[object Request]'` when a page calls `fetch(new Request(url))` | mitigate | Replace with `args[0] instanceof Request ? args[0].url : String(args[0])` — explicit type-narrow before string conversion. This IS the V5 ASVS input-validation pattern. |
|
||||
| T-04-01-02 | Repudiation | navigation tracker always emits `previousUrl: 'unknown'` so support cannot determine where the operator was before the failing navigation; degrades the diagnostic value of saved archives | mitigate | Module-level `previousUrl` state; mutate after every emit; emit prior value. No third-party data introduced; same write surface as existing `rrwebEvents` / `userEvents` arrays. |
|
||||
| T-04-01-03 | Tampering | rrweb timestamps are page-load-relative; cleanupOldEvents' Unix-epoch-style arithmetic silently drops events incorrectly (`now - smallInt` is always >> retention) | mitigate | Normalize at emit: `event.timestamp = Date.now()`. rrweb's internal timestamps remain rrweb-internal (used by rrweb-player for replay timing); the wrapper writes Unix epoch only for events.json downstream. No external data introduced. |
|
||||
| T-04-01-04 | Information Disclosure | new previousUrl module state could leak across content-script reloads if the variable persisted across navigations | accept | content script is re-injected per Chrome's `<all_urls>` match-pattern on each top-level navigation; module state resets per content-script lifecycle. Same risk profile as existing `userEvents` / `rrwebEvents` arrays. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `npm test -- --run` exits 0 (vitest baseline ≥ 171 → ≥ 179 with new tests).
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `grep -c "instanceof Request" src/content/index.ts` returns ≥ 2 (audit P1 #11 invariant pin).
|
||||
- `grep -c "previousUrl" src/content/index.ts` returns ≥ 4 (module-state declaration + closure body uses + emit).
|
||||
- `grep -cE "event\\.timestamp = Date\\.now\\(\\)" src/content/index.ts` returns ≥ 1 in the rrweb-emit region (lines 280-300 — verify with `grep -n` + line range check).
|
||||
- UAT harness 33/33 GREEN preserved (rrweb-emit timestamp change does NOT affect harness assertions which read EventType enum values, not timestamp ranges).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 3 RED test files committed (Task 1; 8 tests; ≥ 4 RED).
|
||||
- 3 surgical edits in src/content/index.ts committed (Task 2; flips 4 RED → 0 RED).
|
||||
- vitest baseline 171 → ≥ 179 GREEN (+8).
|
||||
- tsc-clean preserved.
|
||||
- UAT harness 33/33 GREEN preserved.
|
||||
- ROADMAP success criterion #2 (fetch + XHR network_error) PARTIAL — fetch URL extraction fixed; XHR variant is correct-by-construction (XHR.send uses `_url` captured at open-time, not args[0]); empirical XHR-404 verification deferred to Plan 04-04.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-harden-clean-up-optional/04-01-SUMMARY.md` capturing:
|
||||
- 3 test files added with test counts (3 + 3 + 2 = 8)
|
||||
- 3 edits with before/after snippets (1-liner each)
|
||||
- vitest baseline before/after counts
|
||||
- tsc-clean confirmation
|
||||
- RED→GREEN cycle commit refs (Task 1 + Task 2)
|
||||
- Open items for downstream plans: Plan 04-04 (A34 fetch+XHR empirical harness extension exercises the fix end-to-end)
|
||||
</output>
|
||||
</content>
|
||||
</invoke>
|
||||
347
.planning/phases/04-harden-clean-up-optional/04-02-PLAN.md
Normal file
347
.planning/phases/04-harden-clean-up-optional/04-02-PLAN.md
Normal file
@@ -0,0 +1,347 @@
|
||||
---
|
||||
phase: 04
|
||||
slug: harden-clean-up-optional
|
||||
plan: 02
|
||||
type: tdd
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- vite.config.ts
|
||||
- src/background/index.ts
|
||||
- generate-icons.js
|
||||
- generate-icons.cjs
|
||||
- tests/build/no-new-function-in-sw-chunk.test.ts
|
||||
- tests/build/dead-code-grep.test.ts
|
||||
- .planning/phases/01-stabilize-video-pipeline/deferred-items.md
|
||||
autonomous: true
|
||||
requirements: []
|
||||
tags:
|
||||
- build-hygiene
|
||||
- csp-hardening
|
||||
- setimmediate-polyfill
|
||||
- dead-code-grep
|
||||
- generate-icons-cjs
|
||||
- roadmap-sc-3
|
||||
- roadmap-sc-4
|
||||
- tdd
|
||||
- charter-d-p4-01
|
||||
user_setup: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "grep -c 'new Function' dist/assets/index.ts-*.js returns 0 (was 1 — the setimmediate polyfill literal)"
|
||||
- "JSZip's inline polyfill chain (MessageChannel/postMessage/setTimeout) handles setImmediate fallback cleanly in the SW chunk after the dependency-side polyfill is excluded"
|
||||
- "ripgrep 'permissions\\.request' src/ returns 0 hits (was removed in Phase 1 Plan 01-05; regression-pinned)"
|
||||
- "node generate-icons.cjs exits 0 under package.json type: module (CJS-explicit via .cjs extension)"
|
||||
- "vitest baseline +2 (no-new-function-in-sw-chunk + dead-code-grep tests) GREEN"
|
||||
- "Pre-checkpoint bundle Gate 2 polarity flipped (1 hit → 0 hits for 'new Function' in dist/assets/index.ts-*.js)"
|
||||
artifacts:
|
||||
- path: "tests/build/no-new-function-in-sw-chunk.test.ts"
|
||||
provides: "Wave 0 RED — grep gate pinning 0 hits of 'new Function' in SW chunk after Plan 04-02 polyfill replacement"
|
||||
contains: "new Function"
|
||||
min_lines: 50
|
||||
- path: "tests/build/dead-code-grep.test.ts"
|
||||
provides: "Wave 0 GREEN-on-arrival — regression pin for ROADMAP SC #4 (permissions.request removed in Plan 01-05)"
|
||||
contains: "permissions.request"
|
||||
min_lines: 40
|
||||
- path: "vite.config.ts"
|
||||
provides: "nodePolyfills config with exclude: ['setimmediate'] added"
|
||||
contains: "exclude: ['setimmediate']"
|
||||
- path: "src/background/index.ts"
|
||||
provides: "Top-of-module queueMicrotask-based setImmediate polyfill prelude (4 LOC; inserted BEFORE first import)"
|
||||
contains: "queueMicrotask"
|
||||
- path: "generate-icons.cjs"
|
||||
provides: "Renamed from generate-icons.js to disambiguate CJS under package.json type: module"
|
||||
contains: "require"
|
||||
- path: ".planning/phases/01-stabilize-video-pipeline/deferred-items.md"
|
||||
provides: "Appended closure-flip block — 'Resolved in Phase 4 Plan 04-02' (referenced from existing Plan 01-12 Wave 7 disclosure block)"
|
||||
contains: "Resolved in Phase 4 Plan 04-02"
|
||||
key_links:
|
||||
- from: "vite.config.ts nodePolyfills exclude: ['setimmediate']"
|
||||
to: "dist/assets/index.ts-*.js (SW chunk grep)"
|
||||
via: "vite build pipeline excludes setimmediate transitive dep"
|
||||
pattern: "exclude: \\['setimmediate'\\]"
|
||||
- from: "src/background/index.ts top-of-module setImmediate prelude"
|
||||
to: "JSZip + transitive deps that call setImmediate(fn)"
|
||||
via: "globalThis.setImmediate = (fn, ...args) => queueMicrotask(() => fn(...args))"
|
||||
pattern: "queueMicrotask\\(\\(\\) => fn\\(\\.\\.\\.args\\)\\)"
|
||||
- from: "tests/build/no-new-function-in-sw-chunk.test.ts"
|
||||
to: "dist/assets/index.ts-<hash>.js (post-build grep)"
|
||||
via: "execFile npm run build + readFileSync + countOccurrences"
|
||||
pattern: "FORBIDDEN_SW_CSP_PATTERNS"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Three independent build-hygiene fixes consolidated into one plan because they share the test-scaffold pattern (build-gate grep tests) AND none touch harness or content-script files (zero conflict with Plan 04-01):
|
||||
|
||||
1. **setimmediate polyfill replacement (RESEARCH Q1, Option a):** Drop `new Function` from the SW chunk by adding `exclude: ['setimmediate']` to `vite-plugin-node-polyfills` config + a 4-LOC `queueMicrotask`-based inline polyfill prelude at the top of `src/background/index.ts`. The plugin's transitive `setimmediate` package ships a CSP-unsafe `new Function(string)` fallback for the never-called `setImmediate(string)` form; JSZip (the only legitimate consumer in our bundle) falls back to its own inline MessageChannel/postMessage/setTimeout polyfill chain when `globalThis.setImmediate` is unset, so the explicit fast-path is a strict superset of behavior.
|
||||
|
||||
2. **Dead-code grep (ROADMAP SC #4):** `permissions.request` was removed in Phase 1 Plan 01-05; pin the absence via a new vitest grep test so a future regression breaks the build.
|
||||
|
||||
3. **generate-icons ESM/CJS (ROADMAP SC #3):** `package.json` declares `"type": "module"`, so `.js` files are parsed as ESM, but `generate-icons.js` uses `require('fs')` (CJS). Rename `generate-icons.js` → `generate-icons.cjs` (Node treats `.cjs` as CJS regardless of the enclosing type per Node packages docs). Single-file rename; no code change.
|
||||
|
||||
TDD-strict: Tests 1 + 2 are Wave 0 RED-by-build-state (test 1 is RED until the polyfill landed; test 2 is GREEN-on-arrival since `permissions.request` was already removed in Phase 1). Test 1 flips RED→GREEN after the polyfill replacement; test 2 acts as regression pin. The setimmediate fix has both `vite.config.ts` config change AND `src/background/index.ts` prelude change — they must land coherently in one commit.
|
||||
|
||||
Purpose: Each item closes an audit residual without touching app behavior. (1) tightens MV3 CSP posture by removing the static-analysis red flag in production code. (2) prevents accidental re-introduction of the dead `permissions.request` API call. (3) makes `node generate-icons.cjs` work under the project's ESM-by-default module setting.
|
||||
|
||||
Output: 2 new test files at `tests/build/`; 1-line addition to `vite.config.ts`; 8-line prelude at top of `src/background/index.ts`; 1 file rename `generate-icons.js` → `generate-icons.cjs`; 1-block append to `deferred-items.md` flipping the Plan 01-12 Wave 7 setimmediate disclosure to "Resolved in Phase 4 Plan 04-02".
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-CONTEXT.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-RESEARCH.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
|
||||
@.planning/phases/01-stabilize-video-pipeline/deferred-items.md
|
||||
|
||||
# Source files — locus of the build-hygiene edits
|
||||
@vite.config.ts
|
||||
@src/background/index.ts
|
||||
@generate-icons.js
|
||||
@package.json
|
||||
|
||||
# Analog test scaffold to mirror — build-gate grep test pattern
|
||||
@tests/build/no-remote-fonts.test.ts
|
||||
|
||||
<interfaces>
|
||||
<!-- Key shapes the executor consumes directly. Extracted from codebase 2026-05-21. -->
|
||||
|
||||
From vite.config.ts (current nodePolyfills config — lines 14-22):
|
||||
```typescript
|
||||
nodePolyfills({
|
||||
include: ['buffer'],
|
||||
globals: {
|
||||
Buffer: true,
|
||||
global: false,
|
||||
process: false,
|
||||
},
|
||||
protocolImports: false,
|
||||
}),
|
||||
```
|
||||
|
||||
After Phase 4 Plan 04-02 (1-line addition):
|
||||
```typescript
|
||||
nodePolyfills({
|
||||
include: ['buffer'],
|
||||
exclude: ['setimmediate'], // Plan 04-02 CSP hardening — drops `new Function` from SW chunk
|
||||
globals: {
|
||||
Buffer: true,
|
||||
global: false,
|
||||
process: false,
|
||||
},
|
||||
protocolImports: false,
|
||||
}),
|
||||
```
|
||||
|
||||
From src/background/index.ts:1 (current top — first line is the Logger import):
|
||||
```typescript
|
||||
import { Logger } from '../shared/logger';
|
||||
import { base64ToBlob, blobToBase64 } from '../shared/binary';
|
||||
// ... long import block
|
||||
```
|
||||
|
||||
After Phase 4 Plan 04-02 (8-line prelude INSERTED BEFORE first import):
|
||||
```typescript
|
||||
// Plan 04-02 CSP hardening — replace vite-plugin-node-polyfills' setimmediate
|
||||
// polyfill (which includes a CSP-unsafe `new Function(string)` fallback for
|
||||
// string-form setImmediate calls that this codebase never uses). JSZip falls
|
||||
// back to its inline polyfill chain (MessageChannel / postMessage / setTimeout)
|
||||
// when globalThis.setImmediate is unset; we provide the safest fast-path
|
||||
// explicitly. Reversible by `git revert`.
|
||||
//
|
||||
// Reference: RESEARCH §"Code Examples" Pattern 2; Q1 finding.
|
||||
if (typeof globalThis.setImmediate === 'undefined') {
|
||||
(globalThis as { setImmediate?: (fn: (...args: unknown[]) => void, ...args: unknown[]) => void }).setImmediate =
|
||||
(fn, ...args) => queueMicrotask(() => fn(...args));
|
||||
}
|
||||
|
||||
import { Logger } from '../shared/logger';
|
||||
// ... rest of existing imports unchanged
|
||||
```
|
||||
|
||||
From generate-icons.js:1 (current — CJS smoking gun):
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
```
|
||||
|
||||
After Phase 4 Plan 04-02:
|
||||
- File renamed via `git mv generate-icons.js generate-icons.cjs` (no code change).
|
||||
- Node 14+ treats `.cjs` as CJS regardless of enclosing `"type": "module"` per https://nodejs.org/api/packages.html#determining-module-system
|
||||
- Verify no scripts in package.json reference the old `.js` name; verify no docs reference the old `.js` name (`rg 'generate-icons\\.js' .` before the rename).
|
||||
|
||||
From tests/build/no-remote-fonts.test.ts (existing test scaffold — full 145 lines):
|
||||
- Imports: `execFile` + `existsSync` + `readFileSync` + `readdirSync` + `statSync` + `resolve` + `promisify` + vitest
|
||||
- Pattern: build → recursive walk → countOccurrences → describe block with one it() per forbidden string
|
||||
- `runProductionBuild()`: `execFileAsync('npm', ['run', 'build'], { timeout: 90_000 })`
|
||||
- Skip gate: `if (process.env.SKIP_BUILD !== '1') { await runProductionBuild(); }` — re-uses existing built dist/ when SKIP_BUILD=1 (developer-velocity escape hatch)
|
||||
- The Plan 04-02 RED test inherits the same scaffold; SCOPES the walk to `dist/assets/` filtered by `^index\\.ts-.*\\.js$` regex (the SW chunk only).
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Wave 0 RED — build-gate grep tests (no-new-function-in-sw-chunk + dead-code-grep)</name>
|
||||
<files>tests/build/no-new-function-in-sw-chunk.test.ts, tests/build/dead-code-grep.test.ts</files>
|
||||
<read_first>tests/build/no-remote-fonts.test.ts, .planning/phases/04-harden-clean-up-optional/04-PATTERNS.md (sections "tests/build/no-new-function-in-sw-chunk.test.ts" + "tests/build/dead-code-grep.test.ts")</read_first>
|
||||
<behavior>
|
||||
no-new-function-in-sw-chunk.test.ts (1 build-prep test + 1 grep test):
|
||||
- it: 'npm run build completes and dist/assets/ exists' → SKIP_BUILD=1 escape hatch + production-build invocation
|
||||
- it: 'dist/assets/index.ts-*.js does not contain "new Function" (Plan 04-02 CSP hardening — Q1 finding)' → RED today (existing setimmediate polyfill literal); GREEN after Task 2 lands the polyfill replacement.
|
||||
|
||||
dead-code-grep.test.ts (1 multi-pattern grep test):
|
||||
- it: 'src/ does not contain "permissions.request" (removed Phase 1 Plan 01-05)' → GREEN-on-arrival; acts as regression pin.
|
||||
- it: 'vite.config.ts does not contain the offscreen inline-string sentinel (removed Phase 1 Plan 01-06)' → if the planner can pin a specific sentinel from pre-01-06 vite.config.ts (e.g., a unique HTML literal the inline plugin emitted), include this; if not pinnable to a single string, drop this sub-test and document in the plan SUMMARY that ROADMAP SC #4's vite.config.ts-side audit is regression-pinned at the offscreen-related Vite plugin layer indirectly via `tests/build/no-remote-fonts.test.ts` (which audits the full dist/ for `googleapis` etc. — empirically covers the inline-string removal).
|
||||
</behavior>
|
||||
<action>
|
||||
1. Read `tests/build/no-remote-fonts.test.ts` (~145 lines) once. Extract the imports + helper signatures + describe-block scaffold.
|
||||
2. Create `tests/build/no-new-function-in-sw-chunk.test.ts`:
|
||||
- Mirror the imports verbatim (execFile + node:fs + node:path + node:util + vitest).
|
||||
- Define `const FORBIDDEN_SW_CSP_PATTERNS: ReadonlyArray<string> = ['new Function'];`
|
||||
- Define `const DIST_ASSETS_DIR = resolvePath(process.cwd(), 'dist', 'assets');`
|
||||
- Define a helper `listSwChunkFiles(): ReadonlyArray<string>` that returns `readdirSync(DIST_ASSETS_DIR)` filtered by `/^index\.ts-.*\.js$/`. The SW entry is bundled to a hashed file matching this regex (verify post-build: `ls dist/assets/index.ts-*.js`).
|
||||
- Define `countOccurrencesInFile` helper identical to the analog.
|
||||
- `describe('production SW chunk has no `new Function` literal (MV3 CSP hardening — Plan 04-02 / RESEARCH Q1)', ...)` with one it() for the build-prep gate + one it() per needle in FORBIDDEN_SW_CSP_PATTERNS.
|
||||
- Set SKIP_BUILD env-gate per the analog.
|
||||
|
||||
3. Create `tests/build/dead-code-grep.test.ts`:
|
||||
- Mirror imports + helpers (NO build invocation — this test reads `src/` and `vite.config.ts` directly).
|
||||
- Define `const FORBIDDEN_DEAD_CODE: ReadonlyArray<{ readonly needle: string; readonly searchPaths: ReadonlyArray<string>; readonly rationale: string }> = [...]` with the entries from 04-PATTERNS.md section.
|
||||
- One it() per entry; each runs `rg <needle> <paths>` (via execFile spawn of `rg` OR a recursive readdir+filter in pure Node — analog uses pure Node which is preferred for portability).
|
||||
- The `permissions.request` test is GREEN today (regression pin). If the planner empirically determines a vite.config.ts offscreen-inline-string sentinel CAN be pinned, add the second sub-test; otherwise document the rationale in the SUMMARY and skip.
|
||||
|
||||
Filter-pipeline form (no for-of with continue) per CLAUDE.md. Absolute imports.
|
||||
|
||||
RED gate: `npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run` — the dead-code-grep test is GREEN; the no-new-function test is RED ('new Function' count = 1 in current dist/).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm run build && npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run 2>&1 | tee /tmp/04-02-task-1.log; grep -cE 'FAIL|✗' /tmp/04-02-task-1.log; grep -cE 'PASS|✓' /tmp/04-02-task-1.log</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- File `tests/build/no-new-function-in-sw-chunk.test.ts` exists with ≥ 2 it() blocks (1 build-prep + 1+ grep).
|
||||
- File `tests/build/dead-code-grep.test.ts` exists with ≥ 1 it() block.
|
||||
- Running both files: `no-new-function-in-sw-chunk.test.ts` has at least 1 RED (the 'new Function' grep); `dead-code-grep.test.ts` is fully GREEN.
|
||||
- `grep -v '^#' tests/build/no-new-function-in-sw-chunk.test.ts | grep -c 'new Function'` returns ≥ 2 (1 in FORBIDDEN array + 1 in the it() description).
|
||||
- `grep -v '^#' tests/build/dead-code-grep.test.ts | grep -c 'permissions.request'` returns ≥ 2 (1 in FORBIDDEN_DEAD_CODE array + 1 in the it() description).
|
||||
</acceptance_criteria>
|
||||
<done>2 build-gate test files committed; 1 RED + 1 GREEN-on-arrival. Atomic commit: `test(04-02): Wave 0 — no-new-function-in-sw-chunk RED + dead-code-grep regression pin`.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Wave 1 GREEN — setimmediate polyfill replacement (vite.config.ts + src/background/index.ts prelude) + generate-icons.js → .cjs rename + deferred-items.md flip</name>
|
||||
<files>vite.config.ts, src/background/index.ts, generate-icons.js, generate-icons.cjs, .planning/phases/01-stabilize-video-pipeline/deferred-items.md</files>
|
||||
<read_first>vite.config.ts, src/background/index.ts (top-of-module — lines 1-30), generate-icons.js, .planning/phases/01-stabilize-video-pipeline/deferred-items.md, package.json (scripts section + type field)</read_first>
|
||||
<behavior>
|
||||
All 3 changes land in one commit per "8-line config change must land coherently with the SW-entry prelude" rule (RESEARCH Q1 acceptance gate). Post-fix:
|
||||
- `npm run build` exit 0
|
||||
- `grep -c 'new Function' dist/assets/index.ts-*.js` returns 0 (was 1)
|
||||
- `npm test -- tests/build/no-new-function-in-sw-chunk.test.ts --run` GREEN (the RED from Task 1 flips)
|
||||
- `node generate-icons.cjs` exit 0
|
||||
- `node generate-icons.js` exit 1 (the file no longer exists; this is the desired state — verifies the rename worked)
|
||||
- UAT harness 33/33 GREEN preserved (JSZip falls back to inline polyfill chain cleanly — RESEARCH Q1 Assumption A2)
|
||||
- vitest 171→179 (per Plan 04-01) → 181 GREEN (+2 from this plan's Task 1)
|
||||
</behavior>
|
||||
<action>
|
||||
Edit 1 — vite.config.ts (1-line addition):
|
||||
- Read full file (~75 lines).
|
||||
- Use Edit tool with surrounding context to insert ` exclude: ['setimmediate'], // Plan 04-02 CSP hardening — drops `new Function` from SW chunk` between the existing `include: ['buffer'],` line and the `globals: {` block. Preserve indentation (4 spaces for the array entry).
|
||||
|
||||
Edit 2 — src/background/index.ts top-of-module prelude:
|
||||
- Read lines 1-30 (the import block top).
|
||||
- Use Edit tool to insert the 11-line prelude (comment block + if-guard + assignment) BEFORE the existing `import { Logger } from '../shared/logger';` line. Match the exact prose from the `<interfaces>` block above (the executor can copy-paste verbatim).
|
||||
- Verify the typed widening cast uses inline interface intersection (no `as any`) per CLAUDE.md.
|
||||
- Run `npx tsc --noEmit` to confirm tsc-clean.
|
||||
|
||||
Edit 3 — generate-icons.js → generate-icons.cjs:
|
||||
- First grep for references: `rg 'generate-icons\\.js' . --files-with-matches` to enumerate any package.json scripts, README mentions, CLAUDE.md mentions, etc.
|
||||
- `git mv generate-icons.js generate-icons.cjs` (preserves git history).
|
||||
- For each reference found by the grep, use Edit tool to replace `generate-icons.js` → `generate-icons.cjs` in that file (likely package.json scripts; possibly README).
|
||||
- Verify: `node generate-icons.cjs` exits 0 (no Node ERR_REQUIRE_ESM error — that error was the smoking gun this rename fixes).
|
||||
|
||||
Edit 4 — deferred-items.md flip:
|
||||
- Read full file (~42 lines).
|
||||
- Append a new bullet at the end of the existing Plan 01-12 Wave 7 entry (after line 42), citing the Plan 04-02 commit hash (placeholder `<HASH>` — replace post-commit; the SUMMARY task will substitute the real hash). Use the exact prose from 04-PATTERNS.md section "deferred-items.md flip".
|
||||
|
||||
Coherence check: all 4 edits + the rename land in ONE commit per RESEARCH Q1 acceptance ("must land coherently in the same plan task"). If TypeScript errors surface during Edit 2, fix the typed cast iteratively, but do NOT split into a separate commit. Pre-checkpoint bundle Gate 2 polarity flip (from 1 → 0 hits of `new Function` in `dist/assets/index.ts-*.js`) is the post-Edit-1+2 invariant; the bundle-gate test from Task 1 verifies it.
|
||||
|
||||
Run the focused test: `npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run` — expect 100% GREEN. Run the full vitest: `npm test -- --run` — expect 181 GREEN (or post-Plan-04-01 baseline + 2). Run the UAT harness: `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` — expect 33/33 GREEN (the SKIP_PROD_REBUILD=0 forces a rebuild against the new vite config + prelude so the UAT runs against the post-fix bundle).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm run build && grep -c 'new Function' dist/assets/index.ts-*.js | head -1; node generate-icons.cjs; npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run; npx tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `npm run build` exits 0; `grep -c 'new Function' dist/assets/index.ts-*.js` returns 0 (was 1).
|
||||
- `node generate-icons.cjs` exits 0; `test ! -e generate-icons.js` (the old file no longer exists; rename via git mv preserves history).
|
||||
- `npm test -- tests/build/ --run` exits 0; all Plan 04-02 Task 1 tests GREEN (the no-new-function RED from Task 1 flipped GREEN).
|
||||
- `npx tsc --noEmit` exits 0 (the typed widening cast at the prelude is tsc-clean).
|
||||
- `grep -c "exclude: \\['setimmediate'\\]" vite.config.ts` returns 1.
|
||||
- `grep -c "queueMicrotask" src/background/index.ts | head -1` returns ≥ 1 (the polyfill assignment).
|
||||
- `grep -c "Resolved in Phase 4 Plan 04-02" .planning/phases/01-stabilize-video-pipeline/deferred-items.md` returns ≥ 1.
|
||||
- Full vitest passes: `npm test -- --run` exits 0 (Plan 04-01 baseline +2 from this plan = ≥ 181 GREEN).
|
||||
- UAT harness 33/33 GREEN preserved: `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 (verifies JSZip fallback works correctly post-polyfill-removal).
|
||||
</acceptance_criteria>
|
||||
<done>Polyfill replacement landed; SW chunk grep flipped 1 → 0; generate-icons CJS-renamed; deferred-items.md closure-flipped. Atomic commit: `feat(04-02): Wave 1 — setimmediate polyfill replaced + generate-icons.cjs + deferred-items closure`.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Vite build pipeline → SW chunk → MV3 CSP | The SW chunk is loaded under MV3's `script-src 'self'` CSP; `new Function(string)` literals are a static-analysis red flag for future tighter CSP (MV3 default does allow it but auditors will flag) |
|
||||
| JSZip dep → setImmediate global → SW realm | JSZip calls `setImmediate(fn)` (function form only — never string form); the polyfill chain has to provide a callable setImmediate; if the global is undefined, JSZip falls back to its own MessageChannel/postMessage/setTimeout polyfill (verified RESEARCH Q1) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-04-02-01 | Elevation of Privilege | SW chunk `new Function(string)` literal — a static-analysis red flag for tighter future CSP and a security-audit eyebrow-raiser even under current MV3 CSP defaults | mitigate | Replace transitive `setimmediate` polyfill with explicit `queueMicrotask`-based polyfill in SW entry; verifiable by grep against built dist/. Reversible by git revert. |
|
||||
| T-04-02-02 | DoS (functional) | JSZip relies on setImmediate to yield between zip-entry writes; if our explicit polyfill `(fn, ...args) => queueMicrotask(() => fn(...args))` is incompatible with JSZip's internal use, the zip-assembly could starve or deadlock | accept (verified by UAT) | The polyfill matches JSZip's signature expectation (function + variadic args); UAT harness 33/33 GREEN under the new bundle confirms behavior preserved (A24 specifically tests SAVE→zip; A28/A29/A30/A31 all exercise the zip-assembly path empirically) |
|
||||
| T-04-02-03 | Information Disclosure | leftover `permissions.request` literal in src/ could give a future audit the impression the codebase still relies on the deleted permission flow, masking the post-01-05 architecture | mitigate (regression pin) | dead-code-grep.test.ts pins absence — re-introduction breaks CI |
|
||||
| T-04-02-04 | Tampering | `.cjs` extension override applies only to the renamed file; future contributors writing `.js` files under the same project type:module would re-introduce the ESM/CJS error | accept | Convention is documented in the SUMMARY; CI catches future `require(` in `.js` files via `npx tsc --noEmit` at build time + the explicit `node generate-icons.cjs` invocation in any future ROADMAP backfill |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `npm run build` exits 0 with no new warnings.
|
||||
- `grep -c 'new Function' dist/assets/index.ts-*.js` returns 0 (was 1; Gate 2 polarity flipped).
|
||||
- `grep -c 'permissions.request' src/` returns 0 (regression pin).
|
||||
- `node generate-icons.cjs` exits 0.
|
||||
- vitest baseline +2 to ≥ 181 (Plan 04-01 baseline + 2 new build-gate tests).
|
||||
- tsc-clean.
|
||||
- UAT harness 33/33 GREEN preserved (validates JSZip fallback under the new polyfill regime).
|
||||
- Pre-checkpoint bundle Gate 2 (SW CSP-safety: `grep -rn "new Function\\|eval(" dist/assets/`) returns 0 hits (was 1 documented exception).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 2 new Wave 0 test files committed (Task 1).
|
||||
- vite.config.ts + src/background/index.ts + generate-icons rename + deferred-items.md flipped in one Wave 1 commit (Task 2).
|
||||
- ROADMAP SC #3 (generate-icons ESM/CJS) GREEN — `node generate-icons.cjs` exits 0.
|
||||
- ROADMAP SC #4 (dead-code grep) GREEN — regression pinned in vitest.
|
||||
- setimmediate polyfill replacement GREEN — `new Function` count in SW chunk = 0.
|
||||
- UAT harness 33/33 GREEN preserved.
|
||||
- Pre-checkpoint bundle Gate 2 polarity flipped (1 → 0).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-harden-clean-up-optional/04-02-SUMMARY.md` capturing:
|
||||
- 2 new build-gate test files added (Task 1)
|
||||
- vite.config.ts diff (1-line exclude addition)
|
||||
- src/background/index.ts diff (11-line prelude)
|
||||
- generate-icons.js → .cjs rename + any package.json/README updates
|
||||
- deferred-items.md closure-flip block
|
||||
- Pre-fix vs post-fix `grep -c 'new Function' dist/assets/index.ts-*.js` (1 → 0)
|
||||
- vitest baseline before/after
|
||||
- UAT harness GREEN preservation evidence
|
||||
- RED→GREEN flip for the no-new-function test (Task 1 RED → Task 2 GREEN)
|
||||
- Commit refs (Task 1 + Task 2)
|
||||
</output>
|
||||
</content>
|
||||
</invoke>
|
||||
299
.planning/phases/04-harden-clean-up-optional/04-03-PLAN.md
Normal file
299
.planning/phases/04-harden-clean-up-optional/04-03-PLAN.md
Normal file
@@ -0,0 +1,299 @@
|
||||
---
|
||||
phase: 04
|
||||
slug: harden-clean-up-optional
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 01
|
||||
- 02
|
||||
files_modified:
|
||||
- tests/uat/extension-page-harness.ts
|
||||
- tests/uat/lib/harness-page-driver.ts
|
||||
autonomous: true
|
||||
requirements: []
|
||||
tags:
|
||||
- uat-harness
|
||||
- a29-rewrite
|
||||
- cs-injection-world
|
||||
- flake-stabilization
|
||||
- strict-sentinel
|
||||
- rrweb
|
||||
- charter-d-p4-01
|
||||
user_setup: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "assertA29 follows the cs-injection-world pattern verbatim ported from assertA30/A31 (chrome.tabs.create + chrome.scripting.executeScript ISOLATED + chrome.tabs.remove cleanup in finally)"
|
||||
- "Injected DOM mutation carries a unique sentinel string ('a29-mutation-sentinel') that rrweb's MutationObserver captures inside the IncrementalSnapshot payload"
|
||||
- "Host-side driveA29 filters rrweb events for `data.source === IncrementalSource.Mutation` AND descends into `data.adds[*].node.textContent` looking for the sentinel string"
|
||||
- "A29 PASS rate flips from ~2/3 to 5/5 across consecutive `npm run test:uat` runs (closes iana.org-leftover-flake gap documented in Plan 03-02 SUMMARY)"
|
||||
- "UAT harness count remains 33/33 GREEN; no new assertions added; A29 rewrite is in-place"
|
||||
artifacts:
|
||||
- path: "tests/uat/extension-page-harness.ts"
|
||||
provides: "assertA29 rewritten to use cs-injection-world pattern with sentinel-bearing mutation injection"
|
||||
contains: "a29-mutation-sentinel"
|
||||
- path: "tests/uat/lib/harness-page-driver.ts"
|
||||
provides: "driveA29 rewritten with strict-sentinel rrweb event filter (IncrementalSource.Mutation + textContent sentinel grep)"
|
||||
contains: "IncrementalSource.Mutation"
|
||||
key_links:
|
||||
- from: "tests/uat/extension-page-harness.ts assertA29"
|
||||
to: "chrome.tabs.create({url:'https://example.com/', active:true}) + chrome.scripting.executeScript ISOLATED"
|
||||
via: "verbatim cs-injection-world skeleton from assertA30 lines 3517-3636"
|
||||
pattern: "world: 'ISOLATED'"
|
||||
- from: "tests/uat/lib/harness-page-driver.ts driveA29"
|
||||
to: "@rrweb/types EventType + IncrementalSource enums"
|
||||
via: "import + filter pipeline"
|
||||
pattern: "IncrementalSource\\.Mutation"
|
||||
- from: "tests/uat/lib/harness-page-driver.ts driveA29 strict-sentinel filter"
|
||||
to: "rrweb/session.json events array adds[*].node.textContent"
|
||||
via: "events.filter(e => e.type===EventType.IncrementalSnapshot && e.data?.source===IncrementalSource.Mutation).filter(e => e.data?.adds?.some(a => a?.node?.textContent?.includes(sentinel)))"
|
||||
pattern: "a29-mutation-sentinel"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Rewrite the A29 harness assertion (rrweb DOM verification) to use the canonical cs-injection-world pattern from Plan 03-02 (assertA30) instead of the harness-page approach Plan 03-01 originally used. The current A29 "PASSES" by reading leftover iana.org DOM mutations that A27/A28's probe tabs leave open at the moment SAVE_ARCHIVE fires (~2/3 success rate; documented as a pre-existing flake in Plan 03-02 + 03-03 SUMMARYs).
|
||||
|
||||
The fix per RESEARCH Q3:
|
||||
1. Open a fresh `https://example.com/` probe tab via `chrome.tabs.create` (canonical RFC 2606 reserved domain; matches A30/A31).
|
||||
2. Wait the canonical 1.5s for content-script attach (mirrors A27/A30/A31's `A2X_TAB_NAVIGATION_WAIT_MS`).
|
||||
3. Wait the canonical 11s for the first MediaRecorder segment to rotate (mirrors A30's `SEGMENT_SETTLE_MS`).
|
||||
4. Use `chrome.scripting.executeScript({world: 'ISOLATED', func: ...})` to inject a synthetic DOM mutation that carries a UNIQUE SENTINEL STRING (`'a29-mutation-sentinel'`) into a fresh `<div>` appendChild'd to `document.body`.
|
||||
5. Wait 500ms for rrweb's MutationObserver to enqueue the IncrementalSnapshot.
|
||||
6. Dispatch SAVE_ARCHIVE while the probe tab is active (its content script is the source of `rrweb/session.json`).
|
||||
7. Host-side driveA29 JSZip-parses the resulting archive, filters rrweb events for `EventType.IncrementalSnapshot` + `IncrementalSource.Mutation`, descends into the mutation payload's `adds[*].node.textContent` field, and asserts ≥ 1 event contains the sentinel — PROVING the captured mutation came from OUR injection, not from leftover iana.org tabs.
|
||||
|
||||
Purpose: Flake stabilization. Plan 03-02 + 03-03 SUMMARYs both document A29 as the canonical "tests-pass-for-wrong-reason" smell — the EventType-loose-grep currently in place trivially passes because iana.org's home page has plenty of mutations in its own normal rendering, and the previous A27/A28 probe tab leaks rrweb events into the buffer that A29's findLatestZip picks up. The fix is mechanical (verbatim port of an established pattern) but the diagnostic value is large (the test now genuinely verifies SPEC §10 #4 / REQ-rrweb-dom-buffer at the rrweb wiring at src/content/index.ts:284-309).
|
||||
|
||||
Output: 1 file rewrite in tests/uat/extension-page-harness.ts (the existing assertA29 body, lines 3363-3419) + 1 file rewrite in tests/uat/lib/harness-page-driver.ts (the existing driveA29 body, lines 1884-2001). No new harness assertion number is added; A29 count stays 33; the rewrite is in-place.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-CONTEXT.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-RESEARCH.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
|
||||
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-SUMMARY.md
|
||||
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-03-SUMMARY.md
|
||||
|
||||
# Source files — the canonical assertA30 + driveA30 to copy from + the current A29 to rewrite
|
||||
@tests/uat/extension-page-harness.ts
|
||||
@tests/uat/lib/harness-page-driver.ts
|
||||
|
||||
# Prior plan SUMMARYs documenting the A29 flake
|
||||
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key shapes the executor consumes directly. Extracted from codebase + RESEARCH 2026-05-21. -->
|
||||
|
||||
From tests/uat/extension-page-harness.ts:3363-3419 (current assertA29 — TO BE REWRITTEN):
|
||||
- Function body uses a harness-page approach (no chrome.tabs.create; relies on the existing harness page's probe HTML structure).
|
||||
- The harness-page approach is rejected per Plan 03-02 SUMMARY's "Pitfall 2" — `<all_urls>` content-script match-pattern EXCLUDES chrome-extension:// scheme; the harness page never gets a content script attached, so rrweb never wires up, so any mutations captured come from elsewhere.
|
||||
|
||||
From tests/uat/extension-page-harness.ts:3517-3636 (assertA30 — the CANONICAL cs-injection-world skeleton to copy):
|
||||
- 7-step pattern: setupFreshRecording → chrome.tabs.create probe → wait nav → wait segment → chrome.scripting.executeScript ISOLATED → wait settle → SAVE_ARCHIVE
|
||||
- Uses constants block: `const A30_PROBE_TAB_URL = 'https://example.com/';` + similar.
|
||||
- Uses `try/finally` to ensure probe-tab cleanup via `chrome.tabs.remove` (silent-ignore via T-02-04-04).
|
||||
|
||||
From tests/uat/lib/harness-page-driver.ts:1884-2001 (current driveA29 — TO BE REWRITTEN):
|
||||
- Currently performs a JSZip parse + EventType-loose-grep that counts `e.type === EventType.Meta` + `EventType.FullSnapshot` + `EventType.IncrementalSnapshot` occurrences ≥ 1.
|
||||
- The loose-grep is RESEARCH Pitfall 1 ("Trusting Plan 03-02's SUMMARY 'A29 events.length=4' diagnostic as proof of correctness").
|
||||
- After Plan 04-03 rewrite: still does JSZip parse but adds a strict-sentinel filter step PER RESEARCH Code Example Pattern 3.
|
||||
|
||||
From tests/uat/lib/harness-page-driver.ts:2039-2148 (driveA30 — the CANONICAL host-side filter pattern):
|
||||
- Imports `EventType` (and now `IncrementalSource`) from `@rrweb/types`.
|
||||
- Calls `JSZip.loadAsync(readFileSync(zipPath))` + `zip.file('rrweb/session.json')?.async('text')`.
|
||||
- Filter-pipeline form for events extraction (no for/continue per CLAUDE.md).
|
||||
|
||||
From RESEARCH Q3 Code Example Pattern 3 (host-side strict-sentinel filter — the NEW code path for driveA29):
|
||||
```typescript
|
||||
import { EventType, IncrementalSource } from '@rrweb/types';
|
||||
const A29_MUTATION_SENTINEL = 'a29-mutation-sentinel';
|
||||
|
||||
const mutationEvents = events.filter((e) =>
|
||||
e.type === EventType.IncrementalSnapshot &&
|
||||
e.data?.source === IncrementalSource.Mutation,
|
||||
);
|
||||
const sentinelEvents = mutationEvents.filter((e) => {
|
||||
const adds = e.data?.adds ?? [];
|
||||
return adds.some((a: any) =>
|
||||
typeof a?.node?.textContent === 'string' &&
|
||||
a.node.textContent.includes(A29_MUTATION_SENTINEL),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
Constants to define at the top of assertA29 (RESEARCH Q3 Code Example Pattern 3):
|
||||
```typescript
|
||||
const A29_PROBE_TAB_URL = 'https://example.com/'; // RFC 2606 reserved; matches A30/A31
|
||||
const A29_TAB_NAVIGATION_WAIT_MS = 1_500; // mirrors A27/A30/A31 canonical
|
||||
const A29_SEGMENT_SETTLE_MS = 11_000; // first segment rotation (matches A30)
|
||||
const A29_MUTATION_SETTLE_MS = 500; // rrweb MutationObserver enqueue
|
||||
const A29_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
|
||||
const A29_MUTATION_SENTINEL = 'a29-mutation-sentinel';
|
||||
const A29_PROBE_DIV_ID = 'a29-probe-mutation';
|
||||
```
|
||||
|
||||
Injection function body (inside chrome.scripting.executeScript func: arg):
|
||||
```typescript
|
||||
func: (sentinel: string, divId: string) => {
|
||||
const div = document.createElement('div');
|
||||
div.id = divId;
|
||||
div.textContent = sentinel;
|
||||
document.body.appendChild(div);
|
||||
},
|
||||
args: [A29_MUTATION_SENTINEL, A29_PROBE_DIV_ID],
|
||||
```
|
||||
|
||||
Note on `@rrweb/types` IncrementalSource enum (verified for rrweb 2.0.0-alpha.4 per RESEARCH Assumption A5):
|
||||
- `IncrementalSource.Mutation = 0` — the mutation-source enum value
|
||||
- Already imported at tests/uat/lib/harness-page-driver.ts:2039+ for driveA30 (or grep to confirm; if not imported, ADD the import alongside the existing `EventType` import).
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Rewrite assertA29 to use cs-injection-world pattern (page-side)</name>
|
||||
<files>tests/uat/extension-page-harness.ts</files>
|
||||
<read_first>tests/uat/extension-page-harness.ts:3363-3419 (current assertA29), tests/uat/extension-page-harness.ts:3517-3636 (assertA30 — canonical cs-injection-world skeleton), .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-SUMMARY.md, .planning/phases/04-harden-clean-up-optional/04-RESEARCH.md Q3 section</read_first>
|
||||
<action>
|
||||
1. Read tests/uat/extension-page-harness.ts in the line range 3300-3700 once (covers existing assertA29 + assertA30 + assertA31 + the `__mokoshHarness` global registration block at line ~3971). DO NOT re-read this range — extract everything needed in this single pass.
|
||||
2. Locate the existing `async function assertA29(): Promise<AssertionResult>` at line 3363. Replace its entire body (closing brace at ~line 3419) with the cs-injection-world skeleton, mirroring assertA30 (lines 3517-3636) verbatim with the A29-specific substitutions:
|
||||
- Constants block at the top of the function (use the A29_* names from `<interfaces>` above).
|
||||
- Step 1: `await setupFreshRecording()` (existing helper at extension-page-harness.ts ~line 600+).
|
||||
- Step 2: `const probeTab = await chrome.tabs.create({url: A29_PROBE_TAB_URL, active: true});`
|
||||
- Step 3: `await new Promise(r => setTimeout(r, A29_TAB_NAVIGATION_WAIT_MS));`
|
||||
- Step 4: `await new Promise(r => setTimeout(r, A29_SEGMENT_SETTLE_MS));`
|
||||
- Step 5: `await chrome.scripting.executeScript({target: {tabId: probeTab.id!}, world: 'ISOLATED', func: ..., args: [A29_MUTATION_SENTINEL, A29_PROBE_DIV_ID]});` per the injection function body in `<interfaces>`.
|
||||
- Step 6: `await new Promise(r => setTimeout(r, A29_MUTATION_SETTLE_MS));`
|
||||
- Step 7: `const ack = await sendMessageWithTimeout({type: 'SAVE_ARCHIVE'}, A29_SAVE_ARCHIVE_TIMEOUT_MS, 'SAVE_ARCHIVE (A29)');`
|
||||
- Push `result.checks` for `A29.1: SAVE_ARCHIVE ack success === true`.
|
||||
- try/finally: silent-ignore `chrome.tabs.remove(probeTab.id)` cleanup per T-02-04-04 precedent.
|
||||
- `result.passed = result.checks.every(c => c.passed);` final aggregate.
|
||||
- Preserve the existing `diag(result, ...)` helper calls for each step (mirror assertA30's diag pattern).
|
||||
- Function name and return type are UNCHANGED — only the body is replaced.
|
||||
3. Verify the `__mokoshHarness` global registration at line ~3971 still has the `assertA29` entry (it does — no change needed there).
|
||||
|
||||
Filter-pipeline form: no `continue`. TypeScript-strict. Inline cleanup comments cite RESEARCH Q3 + Plan 03-02 SUMMARY at top of function.
|
||||
|
||||
Verify: `npx tsc --noEmit` exits 0. Build the test bundle: `npm run build:test` exits 0 (the bundled `dist-test/` is what the harness loads; assertA29 lives in extension-page-harness.ts which is bundled by the test config).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit 2>&1 | grep -c 'error TS'; npm run build:test 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `npm run build:test` exits 0.
|
||||
- `grep -c 'A29_MUTATION_SENTINEL' tests/uat/extension-page-harness.ts` returns ≥ 2 (constant definition + injection args).
|
||||
- `grep -c 'a29-mutation-sentinel' tests/uat/extension-page-harness.ts` returns ≥ 1 (constant value literal).
|
||||
- `grep -nE "world: 'ISOLATED'" tests/uat/extension-page-harness.ts` returns ≥ 3 lines (assertA29 new entry + existing assertA30 + assertA31 — all 3 must continue to use ISOLATED world; ANY MAIN-world drift is a regression per RESEARCH Pitfall 5).
|
||||
- `grep -nE 'chrome\\.tabs\\.create.*A29_PROBE_TAB_URL' tests/uat/extension-page-harness.ts` returns ≥ 1.
|
||||
- Existing assertA30 + assertA31 untouched: `git diff tests/uat/extension-page-harness.ts | grep -c '^+.*assertA30\\|^+.*assertA31'` returns 0 (only assertA29 changed).
|
||||
</acceptance_criteria>
|
||||
<done>assertA29 rewritten in-place using cs-injection-world pattern + sentinel-bearing DOM mutation injector. Commit: `feat(04-03): A29 page-side rewrite — cs-injection-world + sentinel`.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Rewrite driveA29 with strict-sentinel rrweb event filter (host-side)</name>
|
||||
<files>tests/uat/lib/harness-page-driver.ts</files>
|
||||
<read_first>tests/uat/lib/harness-page-driver.ts:1884-2001 (current driveA29), tests/uat/lib/harness-page-driver.ts:2039-2148 (driveA30 — canonical host-side filter pattern), .planning/phases/04-harden-clean-up-optional/04-RESEARCH.md Q3 Code Example Pattern 3</read_first>
|
||||
<action>
|
||||
1. Read tests/uat/lib/harness-page-driver.ts in the line range 1850-2200 once (covers existing driveA29 + driveA30 import block + driveA31). DO NOT re-read.
|
||||
2. Verify the imports at the top of the file include both `EventType` and `IncrementalSource` from `@rrweb/types`:
|
||||
- If only `EventType` is imported, ADD `IncrementalSource` to the import binding list.
|
||||
- Mirror the existing `import { EventType } from '@rrweb/types';` shape.
|
||||
3. Locate the existing `export async function driveA29(...)` at line ~1884. Replace its body to:
|
||||
- Preserve the existing Phase 1 page-side stub call: `const pageResult = await page.evaluate(() => harness.assertA29());`
|
||||
- Preserve the existing zipPath = findLatestZip(downloadsDir); + null-check fallback.
|
||||
- Replace the existing loose-EventType-grep block with the strict-sentinel filter per RESEARCH Code Example Pattern 3 (use the exact code snippet from `<interfaces>` above).
|
||||
- Push a single check `A29.2: rrweb captured the injected mutation containing 'a29-mutation-sentinel' (closes iana.org-leftover-flake)` with `passed: sentinelEvents.length >= 1`.
|
||||
- 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.
|
||||
- Filter-pipeline form (no for/continue).
|
||||
- Aggregate: `mergedPassed = mergedChecks.every(c => c.passed);` and return.
|
||||
4. Verify: `npx tsc --noEmit` exits 0.
|
||||
|
||||
Run the full UAT harness as the empirical gate: `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` (uses the bundle from Plan 04-02 Task 2 dist build; harness has its own build pipeline via build:test). Expect:
|
||||
- A29 GREEN (specifically: A29.1 SAVE ack + A29.2 sentinel found).
|
||||
- All other assertions A0-A28, A30-A32 GREEN (no regression to A30's sentinel-based check; no regression to A31's password-filter check).
|
||||
- Total 33/33 GREEN.
|
||||
|
||||
Run the 5-consecutive-runs stress test to confirm flake fixed:
|
||||
```bash
|
||||
for i in {1..5}; do
|
||||
HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat 2>&1 | grep -E '^(A29|TOTAL)' | tail -2;
|
||||
done
|
||||
```
|
||||
Expect: A29 PASS in all 5 runs (was ~3/5 historical baseline per Plan 03-03 SUMMARY).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit && HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat 2>&1 | tail -10 | tee /tmp/04-03-uat-1.log; grep -c '33/33' /tmp/04-03-uat-1.log</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- UAT harness 33/33 GREEN in one run; A29 specifically PASS.
|
||||
- `grep -c 'IncrementalSource' tests/uat/lib/harness-page-driver.ts` returns ≥ 2 (import + filter usage).
|
||||
- `grep -c 'a29-mutation-sentinel' tests/uat/lib/harness-page-driver.ts` returns ≥ 1 (constant value or import from a shared symbol; verifiable post-edit).
|
||||
- `grep -cE "A29\\.2:.*sentinel" tests/uat/lib/harness-page-driver.ts` returns ≥ 1 (the new strict-sentinel check entry).
|
||||
- 5 consecutive UAT runs all GREEN for A29 (manual stress; record outcomes in SUMMARY).
|
||||
- No regression to A30 + A31 (they have their own sentinel-based checks; verify their PASS state unchanged).
|
||||
</acceptance_criteria>
|
||||
<done>driveA29 strict-sentinel filter landed; UAT harness 33/33 GREEN; flake stress 5/5 PASS. Commit: `feat(04-03): A29 host-side strict-sentinel filter + 5/5 PASS stress test`.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| harness host (Node tsx) → Puppeteer page (extension realm) | host-side driveA29 only reads the saved zip; the actual mutation injection happens in the page realm via chrome.scripting.executeScript (sender.id matches chrome.runtime.id automatically — ISOLATED world; no untrusted input crosses boundary) |
|
||||
| chrome.scripting.executeScript ISOLATED world → page DOM | injected function runs in the content-script ISOLATED world (per Chrome match-pattern docs); rrweb's MutationObserver lives in the same ISOLATED world, so the injected mutation IS captured |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-04-03-01 | Repudiation | A29 currently passes for the wrong reason (iana.org-leftover events from A27/A28 probe tabs); a real rrweb regression at src/content/index.ts:284 would be masked by the loose-EventType-grep | mitigate | Strict-sentinel filter (`IncrementalSource.Mutation` + `adds[*].node.textContent` includes injected sentinel string); test fails fast if the captured mutation doesn't carry OUR sentinel |
|
||||
| T-04-03-02 | Spoofing | a future plan could accidentally inject a mutation via MAIN world instead of ISOLATED world, silently masking the test (mutation visible in DOM but rrweb's observer in ISOLATED doesn't see it) | mitigate | Explicit `world: 'ISOLATED'` written in code + acceptance criterion grep gate requires ≥ 3 occurrences across the 3 cs-injection-world callers (A29 + A30 + A31) |
|
||||
| T-04-03-03 | DoS (test) | the 5-min idle path (Plan 04-04 A33) could leave a leftover probe tab from A29 if cleanup misfires, polluting the next run's iana.org-leftover baseline | accept | The harness uses `mkdtemp` downloadsDir per run + each assertA* uses try/finally chrome.tabs.remove; cross-assertion leftover risk is bounded by Puppeteer browser.close() between full runs |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `npm run build:test` exits 0 (test bundle compiles).
|
||||
- UAT harness 33/33 GREEN in one run (`HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat`).
|
||||
- A29 specifically: A29.1 SAVE ack PASS + A29.2 sentinel filter PASS.
|
||||
- 5 consecutive UAT runs: A29 PASS 5/5 (stress-confirmed flake fixed).
|
||||
- A30 + A31 unchanged (no regression to existing sentinel-based checks).
|
||||
- `grep -nE "world: 'ISOLATED'" tests/uat/extension-page-harness.ts | grep -v '^#'` returns ≥ 3 lines (A29 + A30 + A31 all explicit).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- assertA29 rewritten in-place using cs-injection-world skeleton + sentinel-bearing mutation injection (Task 1).
|
||||
- driveA29 rewritten with strict-sentinel rrweb event filter (Task 2).
|
||||
- UAT harness 33/33 GREEN preserved.
|
||||
- A29 flake stabilized — 5/5 PASS across consecutive runs (was ~2/3 historical).
|
||||
- Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12 (production chrome.tabs.create + chrome.scripting.executeScript exclusively — no new `__MOKOSH_UAT__`-gated symbols).
|
||||
- vitest baseline preserved (Plan 04-02 baseline; ≥ 181 GREEN).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-harden-clean-up-optional/04-03-SUMMARY.md` capturing:
|
||||
- assertA29 diff (full body rewrite cited verbatim)
|
||||
- driveA29 diff (filter pipeline cited verbatim)
|
||||
- 5/5 PASS stress test results (timestamped UAT runs)
|
||||
- A29.0a / A29.2 / A29.3+ check inventory (which preserved, which added, which removed)
|
||||
- UAT total before/after (33/33 → 33/33; assertion count unchanged; quality changed)
|
||||
- Pre-existing flake closure citation (Plan 03-02 SUMMARY + 03-03 SUMMARY)
|
||||
- Commit refs (Task 1 + Task 2)
|
||||
</output>
|
||||
</content>
|
||||
</invoke>
|
||||
414
.planning/phases/04-harden-clean-up-optional/04-04-PLAN.md
Normal file
414
.planning/phases/04-harden-clean-up-optional/04-04-PLAN.md
Normal file
@@ -0,0 +1,414 @@
|
||||
---
|
||||
phase: 04
|
||||
slug: harden-clean-up-optional
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on:
|
||||
- 01
|
||||
- 02
|
||||
- 03
|
||||
files_modified:
|
||||
- tests/uat/extension-page-harness.ts
|
||||
- tests/uat/lib/harness-page-driver.ts
|
||||
- tests/uat/harness.test.ts
|
||||
autonomous: true
|
||||
requirements: []
|
||||
tags:
|
||||
- uat-harness
|
||||
- a33
|
||||
- sw-state-persistence
|
||||
- sw-eviction
|
||||
- spike-first
|
||||
- cdp-worker-close
|
||||
- roadmap-sc-1
|
||||
- charter-d-p4-01
|
||||
user_setup: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "Wave 0 spike verifies empirically whether the offscreen document survives 5-min SW idle + worker.close() while MediaRecorder is actively recording — informs whether A33 is verification-only OR needs IndexedDB persistence work"
|
||||
- "stopServiceWorker(browser, extensionId) helper exists in tests/uat/lib/harness-page-driver.ts using Puppeteer CDP browser.waitForTarget + worker.close() per Chrome devrel canonical pattern"
|
||||
- "assertA33 / driveA33 land per the spike outcome: if PASS (offscreen survives) → verification-only A33 that does a real 5-min wall-clock idle + SW kill + SAVE → asserts archive's video/last_30sec.webm size > 100 KB"
|
||||
- "A33 is env-gated by SKIP_LONG_UAT (default: RUN for closure + alpha gate; SKIP_LONG_UAT=1 to skip for per-commit iteration)"
|
||||
- "UAT harness count flips from 33 → 34 (A33 added); 34/34 GREEN when SKIP_LONG_UAT unset"
|
||||
- "ROADMAP SC #1 (SW state persistence) GREEN — A33 empirical evidence that a real-world 5-min idle + SAVE produces a non-empty video buffer"
|
||||
artifacts:
|
||||
- path: "tests/uat/extension-page-harness.ts"
|
||||
provides: "assertA33 page-side stub (or thin driver) for SAVE_ARCHIVE dispatch after host-side SW kill"
|
||||
contains: "assertA33"
|
||||
- path: "tests/uat/lib/harness-page-driver.ts"
|
||||
provides: "stopServiceWorker(browser, extensionId) NEW helper + driveA33 host-side CDP-kill + JSZip video-size check"
|
||||
contains: "worker.close()"
|
||||
- path: "tests/uat/harness.test.ts"
|
||||
provides: "driveA33 import + wrapped driver const (passes handles.browser + handles.extensionId + handles.downloadsDir) + drivers-array push + SKIP_LONG_UAT env-gate wrapper"
|
||||
contains: "driveA33Wrapped"
|
||||
key_links:
|
||||
- from: "tests/uat/harness.test.ts driveA33Wrapped"
|
||||
to: "tests/uat/lib/harness-page-driver.ts driveA33(page, browser, extensionId, downloadsDir)"
|
||||
via: "(page) => driveA33(page, handles.browser, handles.extensionId, handles.downloadsDir)"
|
||||
pattern: "handles\\.browser.*handles\\.extensionId"
|
||||
- from: "tests/uat/lib/harness-page-driver.ts stopServiceWorker"
|
||||
to: "Chrome MV3 SW target via Puppeteer CDP"
|
||||
via: "browser.waitForTarget(t => t.type()==='service_worker' && t.url().startsWith(`chrome-extension://${extensionId}`)) + target.worker().close()"
|
||||
pattern: "service_worker"
|
||||
- from: "tests/uat/lib/harness-page-driver.ts driveA33 video-size check"
|
||||
to: "zip.file('video/last_30sec.webm') → byteLength > 100_000"
|
||||
via: "JSZip.loadAsync + entry.async('uint8array')"
|
||||
pattern: "video/last_30sec\\.webm"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Ship the A33 harness assertion that empirically verifies ROADMAP SC #1 (SW state persistence across the 30s idle unload edge cases). Per RESEARCH Q2: the current architecture stores segments only in offscreen-document RAM (src/offscreen/recorder.ts:91 `let segments: Blob[] = []`). The SW NEVER stores the buffer. So the actual question becomes: does the offscreen document survive 5 minutes of SW idle? Per Chrome docs, the offscreen has its own lifecycle independent of the SW, with active `MediaRecorder` being the canonical "compelling reason" to keep the offscreen alive.
|
||||
|
||||
This plan uses the SPIKE-FIRST approach to avoid over-engineering:
|
||||
|
||||
**Wave 0 (spike):** A30-min empirical investigation. Start recording; wait 5 min real wall-clock; force-kill the SW via Puppeteer CDP `worker.close()` (Chrome devrel canonical pattern; Puppeteer ≥22.1.0 supports it — our pin ^25 is comfortably above); dispatch SAVE_ARCHIVE from page.evaluate; check the resulting archive's `video/last_30sec.webm` size.
|
||||
|
||||
**Wave 1 (impl):** Based on spike outcome:
|
||||
- **If spike PASSES** (likely outcome per RESEARCH architecture analysis + Chrome docs): A33 is a VERIFICATION-ONLY harness assertion that wraps the spike methodology into a repeatable test. Ships the spike's exact pattern as `driveA33` + `assertA33` + orchestrator wiring + env-gate. ROADMAP SC #1 is satisfied by the CURRENT architecture; no persistence layer needed.
|
||||
- **If spike FAILS** (the offscreen dies along with the SW, contrary to Chrome docs): A33 implementation expands per RESEARCH Q2 sub-question (b) recommendation (Option C: IndexedDB persistence in offscreen — Blobs serialize cleanly to IDB; structured-clone supports them natively; per-segment write ~3 MB; ~3 writes per 30s window). This is a wider plan rewrite; the plan-checker should flag for re-planning if it materializes. RESEARCH confidence on offscreen-surviving-SW-kill is MEDIUM; the spike-first approach is the canonical risk hedge per Plan 01-07 precedent.
|
||||
|
||||
Purpose: Forms the empirical evidence for ROADMAP SC #1 ("After running the extension idle for >5 minutes, then exporting, the archive still contains a non-empty video buffer"). The spike-first approach hedges against the RESEARCH MEDIUM-confidence assumption (A3 — offscreen survives SW eviction). If the assumption holds, A33 is a verification gate; if not, persistence work is plain-needed.
|
||||
|
||||
Output: 1 NEW assertion (A33; harness count 33→34); 1 NEW helper (`stopServiceWorker` CDP wrapper); 3-file lockstep update per the Approach B pattern (extension-page-harness.ts + harness-page-driver.ts + harness.test.ts); env-gate via SKIP_LONG_UAT (default = RUN; set to '1' to skip for per-commit iteration).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-CONTEXT.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-RESEARCH.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
|
||||
|
||||
# Source files — locus of the harness extension
|
||||
@tests/uat/extension-page-harness.ts
|
||||
@tests/uat/lib/harness-page-driver.ts
|
||||
@tests/uat/harness.test.ts
|
||||
@tests/uat/lib/launch.ts
|
||||
@src/offscreen/recorder.ts
|
||||
@src/background/index.ts
|
||||
|
||||
# Prior plan SUMMARYs to mirror — Approach B harness extension precedent
|
||||
@.planning/phases/02-stabilize-export-pipeline/02-04-SUMMARY.md
|
||||
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-04-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key shapes the executor consumes directly. Extracted from codebase + RESEARCH 2026-05-21. -->
|
||||
|
||||
From tests/uat/lib/launch.ts:80-90 (HarnessHandles — already exposes browser + extensionId; no extension needed):
|
||||
```typescript
|
||||
export interface HarnessHandles {
|
||||
readonly browser: Browser; // ← already exposed; used by driveA33
|
||||
readonly extensionId: string; // ← already exposed; used by driveA33
|
||||
readonly harnessPage: Page;
|
||||
readonly victimPage: Page;
|
||||
readonly downloadsDir: string;
|
||||
readonly swConsole: string[];
|
||||
readonly offConsole: string[];
|
||||
}
|
||||
```
|
||||
|
||||
From RESEARCH Q2 Code Example Pattern 1 (stopServiceWorker — NEW helper; verbatim from Chrome devrel doc):
|
||||
```typescript
|
||||
import type { Browser } from 'puppeteer';
|
||||
|
||||
/**
|
||||
* Force-terminate the MV3 service worker via Puppeteer CDP. Required
|
||||
* because Puppeteer's persistent CDP attach keeps SWs alive indefinitely;
|
||||
* natural 30s idle eviction does NOT fire under test conditions per Chrome
|
||||
* docs (https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle).
|
||||
*
|
||||
* Reference: https://developer.chrome.com/docs/extensions/how-to/test/test-serviceworker-termination-with-puppeteer
|
||||
*/
|
||||
async function stopServiceWorker(browser: Browser, extensionId: string): Promise<void> {
|
||||
const host = `chrome-extension://${extensionId}`;
|
||||
const target = await browser.waitForTarget(
|
||||
(t) => t.type() === 'service_worker' && t.url().startsWith(host),
|
||||
);
|
||||
const worker = await target.worker();
|
||||
if (worker !== null) {
|
||||
await worker.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
From RESEARCH Q2 Code Example Pattern 4 (driveA33 — host-side body):
|
||||
```typescript
|
||||
const A33_IDLE_WAIT_MS = 5 * 60 * 1000; // 300_000 — real wall-clock
|
||||
const A33_NEW_SW_BOOT_MS = 500; // post-worker.close() settle
|
||||
const A33_OVERALL_TIMEOUT_MS = A33_IDLE_WAIT_MS + 60_000; // 360_000
|
||||
const A33_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
|
||||
|
||||
export async function driveA33(
|
||||
page: Page,
|
||||
browser: Browser,
|
||||
extensionId: string,
|
||||
downloadsDir: string,
|
||||
): Promise<AssertionRecord> {
|
||||
const r: AssertionRecord = { name: 'A33', passed: false, checks: [], diagnostics: [] };
|
||||
|
||||
// Step 1: prime recording on the probe tab
|
||||
await page.evaluate(() => (window as any).__mokoshHarness.setupFreshRecording());
|
||||
|
||||
// Step 2: 5-min wall-clock idle
|
||||
r.diagnostics.push(`waiting ${A33_IDLE_WAIT_MS}ms for SW idle window`);
|
||||
await new Promise((res) => setTimeout(res, A33_IDLE_WAIT_MS));
|
||||
|
||||
// Step 3: force SW termination via CDP
|
||||
await stopServiceWorker(browser, extensionId);
|
||||
r.diagnostics.push('SW terminated via worker.close()');
|
||||
|
||||
// Step 4: brief settle for SW teardown
|
||||
await new Promise((res) => setTimeout(res, A33_NEW_SW_BOOT_MS));
|
||||
|
||||
// Step 5: dispatch SAVE_ARCHIVE — wakes SW back up as an event
|
||||
// (event-driven respawn is the canonical MV3 wakeup path)
|
||||
const saveResult = await page.evaluate(() => (window as any).__mokoshHarness.dispatchSaveArchive());
|
||||
r.checks.push({
|
||||
name: 'A33.1: SAVE_ARCHIVE ack success after 5-min idle + SW kill',
|
||||
expected: true,
|
||||
actual: saveResult.success,
|
||||
passed: saveResult.success === true,
|
||||
});
|
||||
|
||||
// Step 6: verify zip contains non-empty video buffer
|
||||
const zipPath = findLatestZip(downloadsDir);
|
||||
if (zipPath === null) {
|
||||
r.checks.push({ name: 'A33.0: zip present', expected: '>=1 zip', actual: 'none', passed: false });
|
||||
r.passed = false;
|
||||
return r;
|
||||
}
|
||||
const zip = await JSZip.loadAsync(readFileSync(zipPath));
|
||||
const videoEntry = zip.file('video/last_30sec.webm');
|
||||
const videoSize = videoEntry !== null
|
||||
? (await videoEntry.async('uint8array')).byteLength
|
||||
: 0;
|
||||
r.checks.push({
|
||||
name: 'A33.2: video/last_30sec.webm size > 0 (buffer survived SW eviction)',
|
||||
expected: '>0',
|
||||
actual: String(videoSize),
|
||||
passed: videoSize > 0,
|
||||
});
|
||||
r.checks.push({
|
||||
name: 'A33.3: video size > 100 KB (sanity floor; real archives 1-3 MB)',
|
||||
expected: '>100000',
|
||||
actual: String(videoSize),
|
||||
passed: videoSize > 100_000,
|
||||
});
|
||||
|
||||
r.passed = r.checks.every((c) => c.passed);
|
||||
return r;
|
||||
}
|
||||
```
|
||||
|
||||
From RESEARCH Q2 sub-question (c) env-gate recommendation:
|
||||
```typescript
|
||||
// In tests/uat/harness.test.ts drivers-array entry:
|
||||
{
|
||||
name: 'A33',
|
||||
drive: process.env.SKIP_LONG_UAT === '1'
|
||||
? async (): Promise<AssertionRecord> => ({
|
||||
name: 'A33',
|
||||
passed: true,
|
||||
checks: [],
|
||||
diagnostics: ['A33 SKIPPED (SKIP_LONG_UAT=1; unset to run 5-min idle test)'],
|
||||
})
|
||||
: driveA33Wrapped,
|
||||
},
|
||||
```
|
||||
|
||||
Default polarity: SKIP_LONG_UAT unset → RUN A33 (this matches the closure + alpha-gate semantics; per-commit dev iteration uses SKIP_LONG_UAT=1).
|
||||
|
||||
From src/offscreen/recorder.ts:91 (architecture invariant — segments only in offscreen RAM):
|
||||
```typescript
|
||||
let segments: Blob[] = []; // module-level state; NO chrome.storage.local persistence; NO IndexedDB
|
||||
```
|
||||
|
||||
The spike's job: verify this RAM-only design survives a 5-min SW idle. If it does, ROADMAP SC #1 is satisfied with ZERO source-code changes — just a new harness assertion that exercises the path.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Wave 0 SPIKE — empirical verification that offscreen survives 5-min SW idle</name>
|
||||
<files>tests/uat/lib/harness-page-driver.ts</files>
|
||||
<read_first>tests/uat/lib/harness-page-driver.ts (full; ~2200 lines — read selectively: imports lines 1-40, findLatestZip ~1395, driveA30 host-side filter ~2039-2148), tests/uat/extension-page-harness.ts:600-700 (setupFreshRecording helper), src/offscreen/recorder.ts:80-100 (segments array context), .planning/phases/04-harden-clean-up-optional/04-RESEARCH.md Q2 sub-question (b), .planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md (Plan 07 spike precedent)</read_first>
|
||||
<action>
|
||||
1. Add the `stopServiceWorker(browser, extensionId)` helper to `tests/uat/lib/harness-page-driver.ts` per the Code Example in `<interfaces>` above. Place it near the top of the file (after existing imports + before existing driveA-* functions). Add the `import type { Browser } from 'puppeteer';` if not already present.
|
||||
|
||||
2. Create a one-shot spike script `tests/uat/spike-a33-sw-persistence.ts` (NEW; treat as scratch file for this spike — delete after spike concludes; record outcome in plan SUMMARY). The script:
|
||||
- Imports `launchHarnessBrowser` from `./lib/launch.ts`.
|
||||
- Imports `stopServiceWorker` + `findLatestZip` from `./lib/harness-page-driver.ts`.
|
||||
- Launches the harness browser, primes recording via the harness page's setupFreshRecording method.
|
||||
- `console.log('SPIKE: waiting 5 minutes for SW idle window...')`
|
||||
- `await new Promise(r => setTimeout(r, 5 * 60 * 1000));`
|
||||
- `await stopServiceWorker(handles.browser, handles.extensionId);`
|
||||
- `await new Promise(r => setTimeout(r, 500));` (settle)
|
||||
- Dispatch SAVE_ARCHIVE via `await handles.harnessPage.evaluate(() => (window as any).__mokoshHarness.dispatchSaveArchive());`
|
||||
- `await new Promise(r => setTimeout(r, 5000));` (let download complete)
|
||||
- `const zipPath = findLatestZip(handles.downloadsDir);`
|
||||
- `const zip = await JSZip.loadAsync(readFileSync(zipPath));`
|
||||
- `const videoEntry = zip.file('video/last_30sec.webm');`
|
||||
- `const videoSize = videoEntry ? (await videoEntry.async('uint8array')).byteLength : 0;`
|
||||
- `console.log(\`SPIKE RESULT: videoSize=${videoSize} bytes (>0 = OFFSCREEN SURVIVED; =0 = OFFSCREEN DIED)\`);`
|
||||
- `await handles.browser.close();`
|
||||
|
||||
3. Run the spike: `tsx tests/uat/spike-a33-sw-persistence.ts` with HEADLESS=1 (so it runs in CI mode; ~5 min wall-clock).
|
||||
|
||||
4. Record the result. If videoSize > 100_000 → SPIKE PASSED (offscreen survives) → proceed to Task 2 with verification-only A33. If videoSize ≤ 100_000 OR throw → SPIKE FAILED → SUMMARY documents the failure mode + flag to plan-checker for re-planning (IndexedDB persistence work would expand Plan 04-04 substantially; that's a planning event, not an execution event).
|
||||
|
||||
5. Commit the `stopServiceWorker` helper (Task 1's persisting artifact). The spike script is OK to delete OR keep committed as `tests/uat/spike-*.ts` for future SW-lifecycle investigations.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit && HEADLESS=1 tsx tests/uat/spike-a33-sw-persistence.ts 2>&1 | tee /tmp/04-04-spike.log; grep -c 'SPIKE RESULT' /tmp/04-04-spike.log</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `stopServiceWorker(browser, extensionId)` helper exists at `tests/uat/lib/harness-page-driver.ts` with the canonical Chrome devrel signature (`Browser` + extensionId args; `target.worker()?.close()` body).
|
||||
- Spike script ran to completion (no Puppeteer throw).
|
||||
- Spike result logged with explicit `videoSize=<N> bytes` line.
|
||||
- If videoSize > 100_000: spike PASSED; proceed to Task 2 with verification-only path.
|
||||
- If videoSize ≤ 100_000: spike FAILED; pause plan + flag to plan-checker for re-planning (out of scope for this task to escalate, but SUMMARY documents).
|
||||
- Total spike wall-clock: ~6-7 minutes (5 min idle + ~1-2 min orchestration).
|
||||
</acceptance_criteria>
|
||||
<done>Spike outcome recorded in plan SUMMARY; stopServiceWorker helper committed. Atomic commit: `feat(04-04): Wave 0 spike — stopServiceWorker helper + 5-min SW idle empirical result`.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wave 1 — A33 assertion + driveA33 + orchestrator wiring (assumes spike PASSED)</name>
|
||||
<files>tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
|
||||
<read_first>tests/uat/extension-page-harness.ts:3517-3636 (assertA30 — canonical setupFreshRecording + SAVE pattern), tests/uat/extension-page-harness.ts:3971-4000 (__mokoshHarness global registration block), tests/uat/lib/harness-page-driver.ts:2039-2148 (driveA30 — host-side filter pattern), tests/uat/harness.test.ts:100-110 (import block), tests/uat/harness.test.ts:340-360 (wrapped-driver block), tests/uat/harness.test.ts:459-486 (drivers-array push block), tests/uat/harness.test.ts:225-240 (SKIP_PROD_REBUILD env-gate pattern)</read_first>
|
||||
<action>
|
||||
**GATING CONDITION:** Task 1 spike produced videoSize > 100_000. (If FAILED, this task is BLOCKED and the plan must be re-planned to add IndexedDB persistence work.)
|
||||
|
||||
3-file lockstep update per the Approach B harness extension pattern:
|
||||
|
||||
**File 1: tests/uat/extension-page-harness.ts**
|
||||
- Locate the existing `__mokoshHarness` registration block (~line 3971) and the `__mokoshHarness` Window interface declaration (~line 3950).
|
||||
- Add a thin `dispatchSaveArchive()` helper to `__mokoshHarness` if not already present (it may exist as `dispatchSaveArchiveForA33` or similar; reuse the existing SAVE_ARCHIVE dispatch). If the existing `setupFreshRecording` already covers Step 1 (priming the recording), no new page-side helper is needed — driveA33 calls it directly via `page.evaluate`.
|
||||
- If a Step-1 page-side helper IS needed for driveA33: add a thin wrapper `setupFreshRecordingForA33` that's a 1-line forwarder to existing `setupFreshRecording`. Per RESEARCH note (FORBIDDEN_HOOK_STRINGS stays at 12): NO new test-only symbol needed — the new helper calls existing production-surface APIs.
|
||||
- Add `assertA33` ENTRY to the `__mokoshHarness` window interface declaration (`assertA33: () => Promise<AssertionResult>;`) IF a page-side assertA33 is needed. Per RESEARCH driveA33 pattern: the host-side driveA33 owns the 5-min wait + SW kill + SAVE dispatch via the existing harness methods — likely NO new `assertA33` page-side function is needed; the host-side drives everything via existing primitives.
|
||||
- If page-side function is NOT needed: just verify orchestrator uses host-only driveA33 (Step 1's setupFreshRecording is already there; Step 5's dispatchSaveArchive call uses existing SAVE_ARCHIVE messaging).
|
||||
- Decision recorded in plan SUMMARY.
|
||||
|
||||
**File 2: tests/uat/lib/harness-page-driver.ts**
|
||||
- Append `driveA33` function per RESEARCH Code Example Pattern 4 (full body in `<interfaces>` above).
|
||||
- Place it after the existing driveA32 (which is the most-recent Phase 3 addition).
|
||||
- Verify the `stopServiceWorker` helper from Task 1 is in scope (same file).
|
||||
- Filter-pipeline form; no `continue`; typed function signature `(page, browser, extensionId, downloadsDir) => Promise<AssertionRecord>` per the new 4-arg shape.
|
||||
- Add `import { readFileSync } from 'node:fs';` + `import JSZip from 'jszip';` if not already present (they should be — these are reused from driveA29/30/31).
|
||||
|
||||
**File 3: tests/uat/harness.test.ts**
|
||||
- Import: add `driveA33,` to the import block at ~line 101 (alongside `driveA29`-`driveA32`).
|
||||
- Wrapped-driver: add at ~line 357 (after `driveA31Wrapped`):
|
||||
```typescript
|
||||
// Plan 04-04 — driveA33 needs Browser + extensionId for CDP-based SW kill
|
||||
// AND downloadsDir for host-side JSZip parse of post-restart zip.
|
||||
const driveA33Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||
(page) => driveA33(page, handles.browser, handles.extensionId, handles.downloadsDir);
|
||||
```
|
||||
- Drivers-array push: add at ~line 486 (after the existing A32 entry):
|
||||
```typescript
|
||||
// Plan 04-04 A33: SW state persistence 5-min idle (ROADMAP SC #1; RESEARCH Q2).
|
||||
// Forces SW eviction via Puppeteer CDP worker.close() per the canonical
|
||||
// Chrome devrel pattern (RESEARCH Pattern 1). Verifies offscreen-RAM
|
||||
// segments survive SW restart. Env-gated by SKIP_LONG_UAT for fast
|
||||
// per-commit iteration; defaults to RUN for Phase 4 closure + alpha gate.
|
||||
{
|
||||
name: 'A33',
|
||||
drive: process.env.SKIP_LONG_UAT === '1'
|
||||
? async (): Promise<AssertionRecord> => ({
|
||||
name: 'A33',
|
||||
passed: true,
|
||||
checks: [],
|
||||
diagnostics: ['A33 SKIPPED (SKIP_LONG_UAT=1; unset to run 5-min idle test)'],
|
||||
})
|
||||
: driveA33Wrapped,
|
||||
},
|
||||
```
|
||||
|
||||
Verify:
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `npm run build:test` exits 0.
|
||||
- Quick UAT: `HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat` exits 0 with 34/34 GREEN (A33 SKIPPED message visible; preserves baseline + adds A33 skip placeholder).
|
||||
- Full UAT: `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` exits 0 with 34/34 GREEN (A33 actually runs ~6 min wall-clock; A33.1 SAVE ack + A33.2 size > 0 + A33.3 size > 100 KB all PASS).
|
||||
- Tier-1 FORBIDDEN_HOOK_STRINGS check: `grep -c 'FORBIDDEN_HOOK_STRINGS' tests/uat/harness.test.ts tests/background/no-test-hooks-in-prod-bundle.test.ts` — verify the inventory count in both files unchanged (preserves the 12-entry invariant per CONTEXT §"Claude's Discretion").
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit && npm run build:test && HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat 2>&1 | tail -5 | tee /tmp/04-04-task-2-skip.log; grep -c '34/34' /tmp/04-04-task-2-skip.log</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `npm run build:test` exits 0.
|
||||
- UAT harness count flips 33 → 34 (A33 added).
|
||||
- Skip-mode run: `HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat` GREEN 34/34 (A33 SKIPPED placeholder GREEN; total takes ~95s — unchanged).
|
||||
- Full-mode run: `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` GREEN 34/34 (~6.5 min; A33 actually runs and passes A33.1 + A33.2 + A33.3).
|
||||
- `grep -c 'A33' tests/uat/harness.test.ts` returns ≥ 4 (import + wrapped + push + comment banner).
|
||||
- `grep -c 'SKIP_LONG_UAT' tests/uat/harness.test.ts` returns ≥ 2 (env-gate + comment).
|
||||
- FORBIDDEN_HOOK_STRINGS count unchanged at 12 (no new test-only symbols introduced per CONTEXT §"Claude's Discretion"; verify by `wc -l` of the inventory arrays).
|
||||
</acceptance_criteria>
|
||||
<done>A33 lands; UAT 33→34 GREEN; SW persistence empirically verified at 5-min idle scale. Atomic commit: `feat(04-04): Wave 1 — A33 SW state persistence harness assertion (34/34 GREEN)`.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Puppeteer CDP → Chrome MV3 SW realm | `worker.close()` invokes the SW's `self.close()` via CDP `ServiceWorker.unregister` — this terminates the SW realm but does NOT touch the offscreen document's WebContents target. Native CDP surface; no untrusted input. |
|
||||
| Test idle interval (5 min wall-clock) → MediaRecorder active segment buffer | the MediaRecorder is in `state === 'recording'` during the idle; segments rotate every 10s; the offscreen-RAM array accumulates 30 segments (5 min × 60 sec / 10 sec per segment); trim-to-last-3 keeps memory bounded ≤ 30 MB (well under CON-ram-ceiling) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-04-04-01 | Tampering | a future architectural change might move the segments array to SW-side state, breaking the offscreen-survives-SW assumption A33 verifies | mitigate | A33 is a regression-catching gate; if a future PR moves segments off-offscreen, A33 fails fast (videoSize=0 after SW kill) |
|
||||
| T-04-04-02 | DoS (CI) | A33's 5-min idle adds ~5 min to harness wall-clock (95s → 395s); per-commit CI lanes would suffer | mitigate | Env-gated by SKIP_LONG_UAT (default RUN for closure + alpha; documented per-commit SKIP_LONG_UAT=1 for dev iteration) |
|
||||
| T-04-04-03 | Repudiation | natural 30s idle eviction does NOT fire under Puppeteer's CDP attach per Chrome docs; if a developer naively writes "wait 5 min and hope SW dies" the test silently passes via a SW that never died | mitigate | The CDP `worker.close()` call is explicit + cited in code comment; RESEARCH Pitfall 4 documents the misconception |
|
||||
| T-04-04-04 | Spoofing | Puppeteer 25.x patch versions could in theory change `Worker.close()` semantics; the canonical Chrome devrel pattern is pinned at Puppeteer ≥22.1.0 | accept | The project pin `puppeteer: ^25.0.2` is comfortably past the 22.1.0 floor; minor patch drift expected to be backward-compatible per Puppeteer's semver discipline. If A33 ever fails post-Puppeteer-upgrade, the SUMMARY's commit ref provides the exact Puppeteer version where it was validated. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `npm run build:test` exits 0.
|
||||
- UAT harness count 33 → 34.
|
||||
- Skip-mode: `HEADLESS=1 SKIP_LONG_UAT=1 npm run test:uat` GREEN 34/34 in ~95s.
|
||||
- Full-mode: `HEADLESS=1 npm run test:uat` GREEN 34/34 in ~6.5 min.
|
||||
- ROADMAP SC #1 GREEN — A33 empirical evidence: video buffer survived 5-min SW idle + worker.close().
|
||||
- FORBIDDEN_HOOK_STRINGS count unchanged at 12.
|
||||
- vitest baseline preserved (≥ 181 GREEN from Plans 04-01 + 04-02).
|
||||
- A29 + A30 + A31 + A32 unchanged (no regression to existing assertions).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wave 0 spike PASSED — empirical evidence that offscreen survives 5-min SW idle (Task 1).
|
||||
- assertA33 + driveA33 + stopServiceWorker helper + harness orchestrator wiring landed (Task 2).
|
||||
- UAT harness count 33 → 34 GREEN.
|
||||
- ROADMAP SC #1 (SW state persistence) GREEN.
|
||||
- Env-gated long-test pattern established (SKIP_LONG_UAT) — pattern reused by any future ≥5-min test.
|
||||
- Pre-checkpoint bundle gates 6/6 PASS unchanged (Plan 04-04 makes no source-code changes).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-harden-clean-up-optional/04-04-SUMMARY.md` capturing:
|
||||
- Spike outcome (videoSize value + interpretation; SPIKE PASSED/FAILED tag)
|
||||
- stopServiceWorker helper diff (full body)
|
||||
- driveA33 diff (full body)
|
||||
- Orchestrator wiring diff (3 sites in harness.test.ts)
|
||||
- SKIP_LONG_UAT env-gate decision (default RUN; rationale)
|
||||
- UAT before/after (33/33 → 34/34)
|
||||
- Full-mode wall-clock benchmark (e.g., ~6.5 min)
|
||||
- ROADMAP SC #1 closure evidence
|
||||
- Commit refs (Task 1 spike + Task 2 impl)
|
||||
- If spike FAILED: detailed failure mode + flag for re-planning (this branch is unlikely per RESEARCH MEDIUM-confidence; document as ALPHA-PATH-NOT-TAKEN)
|
||||
</output>
|
||||
</content>
|
||||
</invoke>
|
||||
328
.planning/phases/04-harden-clean-up-optional/04-05-PLAN.md
Normal file
328
.planning/phases/04-harden-clean-up-optional/04-05-PLAN.md
Normal file
@@ -0,0 +1,328 @@
|
||||
---
|
||||
phase: 04
|
||||
slug: harden-clean-up-optional
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on:
|
||||
- 01
|
||||
- 02
|
||||
- 03
|
||||
- 04
|
||||
files_modified:
|
||||
- tests/uat/extension-page-harness.ts
|
||||
- tests/uat/lib/harness-page-driver.ts
|
||||
- tests/uat/harness.test.ts
|
||||
autonomous: true
|
||||
requirements: []
|
||||
tags:
|
||||
- uat-harness
|
||||
- a34
|
||||
- fetch-network-error
|
||||
- xhr-network-error
|
||||
- roadmap-sc-2
|
||||
- cs-injection-world
|
||||
- charter-d-p4-01
|
||||
user_setup: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "A34 harness assertion fires synthetic fetch(404-url) + XMLHttpRequest(404-url) from a probe tab (via chrome.scripting.executeScript ISOLATED) — covering BOTH paths required by ROADMAP SC #2"
|
||||
- "Host-side driveA34 JSZip-parses the archive and asserts >=2 network_error entries in logs/events.json (one for fetch, one for XHR), each with `meta.status >= 400`"
|
||||
- "fetch network_error entry's `target` field carries the actual URL (validating Plan 04-01 P1 #11 fix: Request-narrow + String narrowing works empirically end-to-end at the SAVE -> archive layer)"
|
||||
- "UAT harness count 34 -> 35 GREEN with A34 added"
|
||||
artifacts:
|
||||
- path: "tests/uat/extension-page-harness.ts"
|
||||
provides: "assertA34 page-side stub (probe tab create + cs-injection-world fetch+XHR triggers + SAVE)"
|
||||
contains: "A34_404"
|
||||
- path: "tests/uat/lib/harness-page-driver.ts"
|
||||
provides: "driveA34 host-side: JSZip parse + filter logs/events.json for 2 network_error entries (fetch + XHR variants) + meta.status >= 400 assertion"
|
||||
contains: "network_error"
|
||||
- path: "tests/uat/harness.test.ts"
|
||||
provides: "driveA34 import + wrapped driver + drivers-array push with banner"
|
||||
contains: "driveA34Wrapped"
|
||||
key_links:
|
||||
- from: "tests/uat/extension-page-harness.ts assertA34"
|
||||
to: "cs-injection-world fetch + XHR triggers on probe tab"
|
||||
via: "chrome.scripting.executeScript ISOLATED with fetch('https://example.com/404-fetch-a34') + xhr.open('GET','/404-xhr-a34')"
|
||||
pattern: "A34_404"
|
||||
- from: "tests/uat/lib/harness-page-driver.ts driveA34"
|
||||
to: "JSZip parse logs/events.json + filter network_error entries"
|
||||
via: "userEvents.filter(e => e.type === 'network_error' && e.meta?.status >= 400)"
|
||||
pattern: "network_error"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Ship A34 as the empirical harness extension for ROADMAP SC #2 ("A page that issues a failing `fetch` (response code >= 400) produces a `network_error` entry in `events.json`; a failing `XMLHttpRequest` does too").
|
||||
|
||||
Phase 3's A30 already exercises the fetch path via a 404-fetch from a probe tab; A34 EXTENDS this with:
|
||||
1. An empirical end-to-end test that the Plan 04-01 P1 #11 fetch URL extraction fix (Request-narrow) works in a REAL Chrome page context (not just the unit-test JSDOM environment).
|
||||
2. A complementary XMLHttpRequest 404 path that the existing A30 does NOT cover — XHR uses a different code path in src/content/index.ts (lines ~208-237) and merits its own empirical gate.
|
||||
|
||||
The fix uses the canonical cs-injection-world pattern (Plan 03-02 / Plan 04-03 A29 precedent):
|
||||
- Open https://example.com/ probe tab.
|
||||
- Wait 1.5s for content-script attach.
|
||||
- Wait 11s for first segment rotation.
|
||||
- chrome.scripting.executeScript ISOLATED to inject TWO triggers:
|
||||
- `fetch('https://example.com/404-fetch-a34-' + Date.now())` (uniqueness guard against caching).
|
||||
- `new XMLHttpRequest(); xhr.open('GET', 'https://example.com/404-xhr-a34-' + Date.now()); xhr.send();`
|
||||
- Wait ~1s for both responses to land + content script's network-error wrapper to enqueue both UserEvents.
|
||||
- SAVE_ARCHIVE.
|
||||
- Host-side: JSZip parse logs/events.json; filter for `network_error` entries with `meta.status >= 400`; assert >=2 entries (one per protocol).
|
||||
|
||||
Purpose: Closes ROADMAP SC #2 empirically. Validates both:
|
||||
- The Plan 04-01 P1 #11 fetch URL extraction fix works end-to-end through the production bundle (the fetch network_error's `target` field carries the actual URL, not `'[object Request]'`).
|
||||
- The XHR path also produces network_error entries (was implicit before; now empirically pinned).
|
||||
|
||||
Output: 1 NEW assertion (A34; harness count 34->35); 3-file lockstep update.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-CONTEXT.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-RESEARCH.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
|
||||
|
||||
# Source files — locus of the harness extension
|
||||
@tests/uat/extension-page-harness.ts
|
||||
@tests/uat/lib/harness-page-driver.ts
|
||||
@tests/uat/harness.test.ts
|
||||
@src/content/index.ts
|
||||
|
||||
# Prior plan SUMMARYs — canonical cs-injection-world precedents
|
||||
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-02-SUMMARY.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key shapes the executor consumes directly. Extracted from codebase 2026-05-21. -->
|
||||
|
||||
From src/content/index.ts:163-237 (production fetch + XHR wrappers — the SUT for A34):
|
||||
```typescript
|
||||
function setupNetworkLogging() {
|
||||
// Перехват fetch (lines 165-199 post Plan 04-01)
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(...args) {
|
||||
return originalFetch.apply(this, args)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
addUserEvent({
|
||||
timestamp: Date.now(),
|
||||
type: 'network_error',
|
||||
target: (args[0] instanceof Request ? args[0].url : String(args[0])) || 'unknown',
|
||||
value: `HTTP ${response.status} ${response.statusText}`,
|
||||
url: window.location.href,
|
||||
meta: { status: response.status, statusText: response.statusText, url: response.url },
|
||||
});
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(error => { /* catch-branch — also Plan 04-01 fixed */ });
|
||||
};
|
||||
|
||||
// Перехват XMLHttpRequest (lines 201-237)
|
||||
// xhr.open captures _method + _url; xhr.send adds loadend listener that
|
||||
// emits network_error when xhr.status >= 400 OR xhr.status === 0.
|
||||
}
|
||||
```
|
||||
|
||||
Constants to define at top of assertA34:
|
||||
```typescript
|
||||
const A34_PROBE_TAB_URL = 'https://example.com/';
|
||||
const A34_TAB_NAVIGATION_WAIT_MS = 1_500;
|
||||
const A34_SEGMENT_SETTLE_MS = 11_000;
|
||||
const A34_NETWORK_SETTLE_MS = 1_000; // both fetch + xhr settle
|
||||
const A34_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
|
||||
const A34_404_FETCH_PATH = '/404-fetch-a34';
|
||||
const A34_404_XHR_PATH = '/404-xhr-a34';
|
||||
```
|
||||
|
||||
Injection function body (inside chrome.scripting.executeScript func: arg):
|
||||
```typescript
|
||||
func: (fetchPath: string, xhrPath: string) => {
|
||||
// Uniqueness guards against intermediate caching
|
||||
const stamp = Date.now();
|
||||
// Trigger 1: failing fetch (catch swallows the network rejection)
|
||||
fetch(`https://example.com${fetchPath}-${stamp}`).catch(() => { /* expected 404 */ });
|
||||
// Trigger 2: failing XHR
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `https://example.com${xhrPath}-${stamp}`);
|
||||
xhr.send();
|
||||
},
|
||||
args: [A34_404_FETCH_PATH, A34_404_XHR_PATH],
|
||||
```
|
||||
|
||||
Host-side filter (driveA34):
|
||||
```typescript
|
||||
const events = JSON.parse(eventsRaw) as Array<UserEvent>;
|
||||
const networkErrors = events.filter(e => e.type === 'network_error');
|
||||
const fetchEntries = networkErrors.filter(e => typeof e.target === 'string' && e.target.includes('404-fetch-a34'));
|
||||
const xhrEntries = networkErrors.filter(e => typeof e.target === 'string' && e.target.includes('404-xhr-a34'));
|
||||
// A34.1: SAVE ack (page-side); A34.2: >=1 fetch entry; A34.3: >=1 XHR entry;
|
||||
// A34.4: fetch entry's meta.status === 404 (Plan 04-01 P1 #11 end-to-end);
|
||||
// A34.5: XHR entry's meta.status === 404
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Append assertA34 (page-side) using cs-injection-world fetch + XHR injection</name>
|
||||
<files>tests/uat/extension-page-harness.ts</files>
|
||||
<read_first>tests/uat/extension-page-harness.ts:3517-3700 (assertA30 — canonical skeleton; the post-Plan-04-03 assertA29 also uses the same skeleton), tests/uat/extension-page-harness.ts:3950-4000 (__mokoshHarness global registration block), src/content/index.ts:163-237 (production SUT)</read_first>
|
||||
<action>
|
||||
1. Read tests/uat/extension-page-harness.ts in the line range 3700-4050 once (covers assertA30+A31+A32 patterns + __mokoshHarness global registration + Window interface declaration). Do not re-read.
|
||||
2. Append `async function assertA34(): Promise<AssertionResult>` AFTER the existing assertA32 (or near the end of the assertA* block per file convention; verify placement). Use the cs-injection-world skeleton from assertA30/A29 verbatim with A34-specific substitutions:
|
||||
- Constants block per the `<interfaces>` block above.
|
||||
- Step 1: `await setupFreshRecording()` (existing helper).
|
||||
- Step 2: `const probeTab = await chrome.tabs.create({url: A34_PROBE_TAB_URL, active: true});`
|
||||
- Step 3: `await new Promise(r => setTimeout(r, A34_TAB_NAVIGATION_WAIT_MS));`
|
||||
- Step 4: `await new Promise(r => setTimeout(r, A34_SEGMENT_SETTLE_MS));`
|
||||
- Step 5: `await chrome.scripting.executeScript({target: {tabId: probeTab.id!}, world: 'ISOLATED', func: ..., args: [A34_404_FETCH_PATH, A34_404_XHR_PATH]});` per the injection function body above. The `fetch(404).catch(() => {})` is required — without the catch, the unhandled rejection might surface as a separate js_error UserEvent which A34 doesn't care about.
|
||||
- Step 6: `await new Promise(r => setTimeout(r, A34_NETWORK_SETTLE_MS));` — both fetch + xhr need to settle their then/catch + loadend chains AND the content-script's setupNetworkLogging wrapper needs to push 2 UserEvents.
|
||||
- Step 7: `const ack = await sendMessageWithTimeout({type: 'SAVE_ARCHIVE'}, A34_SAVE_ARCHIVE_TIMEOUT_MS, 'SAVE_ARCHIVE (A34)');`
|
||||
- Push `result.checks` for A34.1 (SAVE ack success).
|
||||
- try/finally: silent-ignore `chrome.tabs.remove(probeTab.id)` cleanup per T-02-04-04 precedent.
|
||||
- `result.passed = result.checks.every(c => c.passed);` and return.
|
||||
3. Add `assertA34: () => Promise<AssertionResult>;` to the `__mokoshHarness` Window interface declaration block (~line 3971).
|
||||
4. Add `assertA34` entry to the `window.__mokoshHarness = { ... };` object literal (mirror existing entries).
|
||||
|
||||
Filter-pipeline form (no continue). TypeScript-strict. Inline comment cites Plan 04-01 P1 #11 (the fetch URL extraction fix that A34 empirically validates).
|
||||
|
||||
Verify: `npx tsc --noEmit` exits 0. `npm run build:test` exits 0.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit 2>&1 | grep -c 'error TS'; npm run build:test 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `npm run build:test` exits 0.
|
||||
- `grep -c 'assertA34' tests/uat/extension-page-harness.ts` returns >=3 (function def + Window interface entry + object literal entry).
|
||||
- `grep -c 'A34_404' tests/uat/extension-page-harness.ts` returns >=4 (constants + injection args).
|
||||
- `grep -nE "world: 'ISOLATED'" tests/uat/extension-page-harness.ts | grep -v '^#'` returns >=4 lines (A29 + A30 + A31 + A34 all explicit).
|
||||
- `grep -c 'new XMLHttpRequest()' tests/uat/extension-page-harness.ts` returns >=1 (the A34 XHR injection trigger).
|
||||
</acceptance_criteria>
|
||||
<done>assertA34 + Window interface + object literal entries appended. Commit: `feat(04-05): A34 page-side — cs-injection-world fetch + XHR 404 injection`.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Append driveA34 (host-side) + orchestrator wiring (3-file lockstep)</name>
|
||||
<files>tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
|
||||
<read_first>tests/uat/lib/harness-page-driver.ts:2039-2148 (driveA30 — canonical host-side filter pattern), tests/uat/harness.test.ts:100-110 (import block), tests/uat/harness.test.ts:340-360 (wrapped-driver block), tests/uat/harness.test.ts:459-490 (drivers-array push block)</read_first>
|
||||
<action>
|
||||
File 1: tests/uat/lib/harness-page-driver.ts
|
||||
- Append `export async function driveA34(page: Page, downloadsDir: string): Promise<AssertionRecord>` after driveA33 (Plan 04-04 placement).
|
||||
- Body shape:
|
||||
- Phase 1: page-side stub call: `const pageResult = await page.evaluate(() => (window as any).__mokoshHarness.assertA34() as AssertionRecord);`
|
||||
- Phase 2: `const zipPath = findLatestZip(downloadsDir);` + null-check guard pushing A34.0 fail check.
|
||||
- Phase 3: JSZip parse + read `logs/events.json` text, JSON.parse to Array<UserEvent>.
|
||||
- Filter pipeline:
|
||||
- networkErrors = events.filter(e => e.type === 'network_error')
|
||||
- fetchEntries = networkErrors.filter(e => typeof e.target === 'string' && e.target.includes('404-fetch-a34'))
|
||||
- xhrEntries = networkErrors.filter(e => typeof e.target === 'string' && e.target.includes('404-xhr-a34'))
|
||||
- Push A34.2: `passed: fetchEntries.length >= 1` (with descriptive name "fetch 404 produced network_error entry containing '404-fetch-a34' (Plan 04-01 P1 #11 end-to-end)")
|
||||
- Push A34.3: `passed: xhrEntries.length >= 1` (with descriptive name "XHR 404 produced network_error entry containing '404-xhr-a34'")
|
||||
- Push A34.4: `passed: fetchEntries[0]?.meta?.status === 404`
|
||||
- Push A34.5: `passed: xhrEntries[0]?.meta?.status === 404`
|
||||
- Aggregate: `mergedPassed = mergedChecks.every(c => c.passed);` and return.
|
||||
- Filter-pipeline form. TypeScript-strict (no any except the cast at page.evaluate result).
|
||||
|
||||
File 2: tests/uat/harness.test.ts
|
||||
- Import block (~line 101 after `driveA33,`): add `driveA34,` to the binding list.
|
||||
- Wrapped-driver block (~line 357 after `driveA33Wrapped`):
|
||||
```typescript
|
||||
// Plan 04-05 — driveA34 needs downloadsDir for host-side JSZip parse of logs/events.json
|
||||
const driveA34Wrapped: (page: import('puppeteer').Page) => Promise<AssertionRecord> =
|
||||
(page) => driveA34(page, handles.downloadsDir);
|
||||
```
|
||||
- Drivers-array push (~line 486 after the A33 entry):
|
||||
```typescript
|
||||
// Plan 04-05 A34: fetch + XHR network_error empirical (ROADMAP SC #2).
|
||||
// Verifies both protocol paths in src/content/index.ts setupNetworkLogging
|
||||
// produce events.json entries. Empirically validates Plan 04-01 P1 #11
|
||||
// fetch URL extraction fix at the SAVE->archive layer (A34.4 + A34.5).
|
||||
{ name: 'A34', drive: driveA34Wrapped },
|
||||
```
|
||||
|
||||
Verify gates:
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `npm run build:test` exits 0.
|
||||
- Quick UAT: `HEADLESS=1 SKIP_PROD_REBUILD=0 SKIP_LONG_UAT=1 npm run test:uat` exits 0 with 35/35 GREEN (A33 SKIPPED placeholder; A34 actually runs ~25s).
|
||||
- Full UAT: `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` exits 0 with 35/35 GREEN (~7 min total).
|
||||
- FORBIDDEN_HOOK_STRINGS count unchanged at 12.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit && npm run build:test && HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat 2>&1 | tail -10 | tee /tmp/04-05-task-2.log; grep -c '35/35' /tmp/04-05-task-2.log</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `npm run build:test` exits 0.
|
||||
- UAT harness count flips 34 -> 35.
|
||||
- Skip-mode (SKIP_LONG_UAT=1): 35/35 GREEN in ~95s (A33 skipped placeholder; A34 runs).
|
||||
- Full-mode: 35/35 GREEN in ~7 min (A33 + A34 both real).
|
||||
- `grep -c 'driveA34' tests/uat/harness.test.ts` returns >=3 (import + wrapped + push entry).
|
||||
- `grep -c '404-fetch-a34\\|404-xhr-a34' tests/uat/lib/harness-page-driver.ts` returns >=4 (2 sentinel literals + 2 includes() checks).
|
||||
- ROADMAP SC #2 GREEN — A34 produces 2 network_error entries with status === 404.
|
||||
- FORBIDDEN_HOOK_STRINGS count unchanged at 12.
|
||||
</acceptance_criteria>
|
||||
<done>driveA34 + orchestrator wiring landed; UAT 34 -> 35 GREEN. Atomic commit: `feat(04-05): A34 host-side + orchestrator — fetch+XHR network_error empirical (ROADMAP SC #2 GREEN)`.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| chrome.scripting.executeScript ISOLATED -> page realm | injected fetch + XHR run in the content-script ISOLATED world; the same realm as the content script's `window.fetch` + `XMLHttpRequest.prototype` wrappers, so the wrappers intercept the failing requests as designed |
|
||||
| network -> 404 response | example.com/404-* responds with HTTP 404 (the example.com domain is RFC 2606 reserved AND serves 404 for arbitrary unknown paths; both fetch and XHR see status=404 in their respective callbacks) |
|
||||
| Content script -> events.json (archive) | UserEvent buffer flushed at SAVE time via chrome.runtime.sendMessage; same path A30 uses; no new boundary |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-04-05-01 | Repudiation | a future regression to src/content/index.ts setupNetworkLogging would silently break ROADMAP SC #2 if A34 doesn't catch both paths | mitigate | A34's 4 checks (2 presence + 2 status-code) cover both protocols + the Plan 04-01 P1 #11 URL extraction in one assertion |
|
||||
| T-04-05-02 | Tampering | uniqueness stamps (`-${Date.now()}` suffix on probe URLs) defend against any future flake where iana.org or example.com caches a hit between consecutive runs | accept | The stamps are functionally unnecessary today (the paths don't exist; 404 is always fresh) but defend against future caching behavior changes |
|
||||
| T-04-05-03 | Information Disclosure | network_error UserEvent.target field carries the actual URL (post-Plan-04-01 fix); if the URL contains query-string secrets, those land in the archive | accept | Out of scope for v1 per CONTEXT charter; REQ-password-confidentiality deferred to v2; alpha distribution archives are operator-curated |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `npm run build:test` exits 0.
|
||||
- UAT harness count 34 -> 35.
|
||||
- Skip-mode `SKIP_LONG_UAT=1`: 35/35 GREEN ~95s.
|
||||
- Full-mode: 35/35 GREEN ~7 min.
|
||||
- ROADMAP SC #2 GREEN — A34.2 fetch presence + A34.3 XHR presence + A34.4/A34.5 status===404.
|
||||
- Plan 04-01 P1 #11 end-to-end validation: A34.4 fetch entry target contains the actual URL (NOT '[object Request]' string).
|
||||
- FORBIDDEN_HOOK_STRINGS unchanged at 12.
|
||||
- vitest baseline preserved (>=181 GREEN from Plans 04-01 + 04-02).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- assertA34 + driveA34 + orchestrator wiring landed (Tasks 1 + 2).
|
||||
- UAT harness 34 -> 35 GREEN.
|
||||
- ROADMAP SC #2 (fetch + XHR network_error) GREEN.
|
||||
- Plan 04-01 P1 #11 fetch URL extraction validated end-to-end (A34.4 pin).
|
||||
- Pre-checkpoint bundle gates 6/6 unchanged.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-harden-clean-up-optional/04-05-SUMMARY.md` capturing:
|
||||
- assertA34 diff (full body)
|
||||
- driveA34 diff (full body)
|
||||
- Orchestrator wiring diff (3 sites in harness.test.ts)
|
||||
- UAT before/after (34/34 -> 35/35)
|
||||
- ROADMAP SC #2 closure evidence (A34.4 fetch URL contains real path; A34.5 XHR status===404)
|
||||
- Plan 04-01 P1 #11 end-to-end empirical pin (no '[object Request]' in any network_error entry)
|
||||
- Commit refs (Task 1 + Task 2)
|
||||
</output>
|
||||
</content>
|
||||
</invoke>
|
||||
508
.planning/phases/04-harden-clean-up-optional/04-06-PLAN.md
Normal file
508
.planning/phases/04-harden-clean-up-optional/04-06-PLAN.md
Normal file
@@ -0,0 +1,508 @@
|
||||
---
|
||||
phase: 04
|
||||
slug: harden-clean-up-optional
|
||||
plan: 06
|
||||
type: execute
|
||||
wave: 5
|
||||
depends_on:
|
||||
- 01
|
||||
- 02
|
||||
- 03
|
||||
- 04
|
||||
- 05
|
||||
files_modified:
|
||||
- src/shared/brand/mokosh-mark.svg
|
||||
- src/welcome/welcome.ts
|
||||
- src/welcome/welcome.css
|
||||
- globals.d.ts
|
||||
- tests/uat/extension-page-harness.ts
|
||||
- tests/welcome/inline-svg.test.ts
|
||||
- tests/build/cursor-visibility.test.ts
|
||||
- .planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md
|
||||
autonomous: false
|
||||
requirements: []
|
||||
tags:
|
||||
- visual-polish
|
||||
- dark-logo-contrast
|
||||
- ui-spec
|
||||
- currentColor
|
||||
- inline-svg
|
||||
- cursor-visibility-verification
|
||||
- charter-d-p4-03
|
||||
- operator-empirical
|
||||
user_setup: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "src/shared/brand/mokosh-mark.svg root <svg> element carries `stroke=\"currentColor\"` (was `stroke=\"#181b2a\"`); the 12 <line> + 1 <rect> children are unchanged"
|
||||
- "src/welcome/welcome.ts imports the mark via `import markSvg from '../shared/brand/mokosh-mark.svg?raw';` (was `?url`); populateMark uses DOMParser to inject inline <svg> into the slot (NOT <img>)"
|
||||
- "Injected inline <svg> inherits color from the parent .welcome-hero__mark wrapper (color: var(--mks-fg-inverse)) so the stroke resolves via CSS currentColor cascade"
|
||||
- "globals.d.ts contains a `declare module '*.svg?raw'` ambient module declaration alongside the existing `*.svg?url`"
|
||||
- "tests/welcome/inline-svg.test.ts pins the contract: populateMark injects inline <svg>, stroke='currentColor', aria-hidden/role/aria-label preserved"
|
||||
- "A17.8 harness sub-check updated: welcome chunk JS bundles raw SVG source with viewBox='0 0 32 32' + stroke='currentColor' AND DOM-querySelector inline <svg> after populateMark"
|
||||
- "Cursor visibility verified ALREADY SHIPPED at src/offscreen/recorder.ts:285 (`cursor: 'always'`) per RESEARCH Finding 4; tests/build/cursor-visibility.test.ts regression-pins the literal"
|
||||
- ".planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md back-patched: stale 'deferred to Phase 5' lines flipped to 'shipped Plan 01-09; verified Phase 4 Plan 04-06'"
|
||||
- "Operator empirical checkpoint: dark-mode visual aesthetic judgment on welcome-hero (the ONE genuine operator-empirical case in Phase 4 per UI-SPEC §'Manual-Only Verifications')"
|
||||
- "PNG toolbar icons untouched (Chrome auto-inverts on dark toolbars; icons/icon{16,48,128}.png byte-identical)"
|
||||
artifacts:
|
||||
- path: "src/shared/brand/mokosh-mark.svg"
|
||||
provides: "Single-attribute change on root <svg>: stroke='#181b2a' -> stroke='currentColor'. 13 child elements unchanged."
|
||||
contains: "stroke=\"currentColor\""
|
||||
- path: "src/welcome/welcome.ts"
|
||||
provides: "?raw import + DOMParser-based inline SVG injection in populateMark (replaces <img> injection)"
|
||||
contains: "import markSvg from '../shared/brand/mokosh-mark.svg?raw'"
|
||||
- path: "src/welcome/welcome.css"
|
||||
provides: "(Optional) selector broadening for .welcome-hero__mark-img to match both img.* AND svg.* — bare class selector works for both; explicit selectors documented for transition clarity"
|
||||
contains: "welcome-hero__mark-img"
|
||||
- path: "globals.d.ts"
|
||||
provides: "Ambient module declaration for *.svg?raw imports (4-line block mirror of existing *.svg?url)"
|
||||
contains: "*.svg?raw"
|
||||
- path: "tests/welcome/inline-svg.test.ts"
|
||||
provides: "Wave 0 RED -> GREEN — 3-test contract pinning inline SVG injection + currentColor + aria preservation"
|
||||
contains: "currentColor"
|
||||
min_lines: 50
|
||||
- path: "tests/build/cursor-visibility.test.ts"
|
||||
provides: "Wave 0 GREEN-on-arrival — defensive pin for src/offscreen/recorder.ts:285 cursor: 'always' literal"
|
||||
contains: "cursor: 'always'"
|
||||
min_lines: 15
|
||||
- path: "tests/uat/extension-page-harness.ts"
|
||||
provides: "A17.8 sub-check updated to assert raw SVG source bundling + inline <svg> injection (NOT data URL)"
|
||||
contains: "A17.8"
|
||||
- path: ".planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md"
|
||||
provides: "Surgical back-patch — stale 'Phase 5' lines flipped (lines 47, 82, 109, 135, 205 per PATTERNS.md)"
|
||||
contains: "Phase 4 Plan 04-06"
|
||||
key_links:
|
||||
- from: "src/welcome/welcome.ts populateMark"
|
||||
to: "src/shared/brand/mokosh-mark.svg?raw"
|
||||
via: "Vite ?raw import returns SVG source as string; DOMParser parses; appendChild injects"
|
||||
pattern: "DOMParser"
|
||||
- from: "Injected inline <svg> stroke='currentColor'"
|
||||
to: ".welcome-hero__mark wrapper color: var(--mks-fg-inverse)"
|
||||
via: "CSS color cascade (W3C SVG2 §13.3)"
|
||||
pattern: "color: var\\(--mks-fg-inverse\\)"
|
||||
- from: "tests/uat/extension-page-harness.ts A17.8"
|
||||
to: "welcome chunk JS bundle string-grep"
|
||||
via: "post-build grep for raw SVG source + querySelector inline <svg>"
|
||||
pattern: "A17\\.8"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Land the UI-SPEC dark-surface logo contrast strategy (Option A — currentColor SVG + CSS color via existing `.dark` block in tokens.css). Per the UI-SPEC §"Implementation amendment", this requires a 2-part technique:
|
||||
|
||||
1. SVG attribute change: `stroke="#181b2a"` → `stroke="currentColor"` on the root `<svg>` of `src/shared/brand/mokosh-mark.svg`.
|
||||
2. Inline-SVG injection in welcome.ts: `<img>` rendering of SVG runs in an isolated SVG document context where `currentColor` resolves to the SVG's own root color (defaults to `canvastext` ≈ black). To get the desired CSS cascade, the SVG must be inlined into the welcome page's DOM. Switch `import markUrl from '.../*.svg?url'` to `import markSvg from '.../*.svg?raw'` + rewrite populateMark to use DOMParser + appendChild.
|
||||
|
||||
Two additional polish items co-land:
|
||||
|
||||
3. **Cursor visibility verification** (RESEARCH Finding 4): `cursor: 'always'` is ALREADY SHIPPED at `src/offscreen/recorder.ts:285` (Plan 01-09 opportunistically lifted the original Phase 5 deferral). Plan 04-06 confirms via a defensive grep test (`tests/build/cursor-visibility.test.ts`) + back-patches the stale "deferred to Phase 5" lines in `.planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md` (5 occurrences per PATTERNS.md mapping).
|
||||
|
||||
4. **Operator empirical checkpoint** (UI-SPEC Acceptance Criterion #6): the ONE Phase 4 genuine operator-empirical gate — dark-mode visual aesthetic judgment of the welcome hero. The harness covers `currentColor` resolution + inline-SVG injection automatically (Tasks 1-3); only the dark-OS aesthetic judgment is non-automatable. Per `feedback-trust-harness-over-manual-uat.md`, operator empirical is reserved for genuinely non-automatable cases; this is one.
|
||||
|
||||
Purpose: Closes UI-SPEC dark-logo contrast strategy + closes RESEARCH Finding 4 cursor verification + closes ROADMAP cursor visibility item + closes Plan 01-10 cycle-2 operator observation "also on dark surfaces probably either we need to place the logo on the light background or dunno".
|
||||
|
||||
Output: 5 source/test/spec file edits (per UI-SPEC §"Net file-change set") + 2 new test files at `tests/welcome/inline-svg.test.ts` + `tests/build/cursor-visibility.test.ts` + 1 docs back-patch + 1 operator empirical UAT cycle. The plan is `autonomous: false` because of the operator-empirical checkpoint (the ONLY autonomous: false plan in Phase 4).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-CONTEXT.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-RESEARCH.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-UI-SPEC.md
|
||||
@.planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md
|
||||
@.planning/phases/01-stabilize-video-pipeline/01-10-SUMMARY.md
|
||||
|
||||
# Source files — locus of the visual polish edits
|
||||
@src/shared/brand/mokosh-mark.svg
|
||||
@src/welcome/welcome.ts
|
||||
@src/welcome/welcome.css
|
||||
@globals.d.ts
|
||||
@src/offscreen/recorder.ts
|
||||
@tests/uat/extension-page-harness.ts
|
||||
|
||||
# Analog test scaffolds — welcome-page side-effect assertion + build-grep
|
||||
@tests/background/onboarding.test.ts
|
||||
@tests/i18n/manifest-i18n.test.ts
|
||||
@tests/build/no-remote-fonts.test.ts
|
||||
|
||||
<interfaces>
|
||||
<!-- Key shapes the executor consumes directly. Extracted from codebase 2026-05-21. -->
|
||||
|
||||
From src/shared/brand/mokosh-mark.svg (current — 13 elements; ~25 lines):
|
||||
```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="#181b2a" stroke-width="2.25" stroke-linecap="square" stroke-linejoin="miter">
|
||||
<rect x="2.5" y="2.5" width="27" height="27" rx="3.5" ry="3.5"></rect>
|
||||
<line x1="12" y1="2.5" x2="12" y2="11.5"></line>
|
||||
... (11 more line + rect children)
|
||||
</svg>
|
||||
```
|
||||
|
||||
After Plan 04-06 (1-character semantic change on root <svg>):
|
||||
```svg
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="square" stroke-linejoin="miter">
|
||||
... (13 child elements UNCHANGED)
|
||||
</svg>
|
||||
```
|
||||
|
||||
From src/welcome/welcome.ts:46 (current ?url import):
|
||||
```typescript
|
||||
import markUrl from '../shared/brand/mokosh-mark.svg?url';
|
||||
```
|
||||
|
||||
After Plan 04-06 (swap to ?raw):
|
||||
```typescript
|
||||
import markSvg from '../shared/brand/mokosh-mark.svg?raw';
|
||||
```
|
||||
|
||||
From src/welcome/welcome.ts:159-179 (current populateMark — <img> injection):
|
||||
```typescript
|
||||
function populateMark(): void {
|
||||
const slots = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('[data-mokosh-slot="mark"]'),
|
||||
);
|
||||
const altText = COPY['welcome.hero.mark.alt'] ?? 'Знак Mokosh';
|
||||
for (const slot of slots) {
|
||||
const img = document.createElement('img');
|
||||
img.src = markUrl;
|
||||
img.alt = altText;
|
||||
img.width = 64;
|
||||
img.height = 64;
|
||||
img.className = 'welcome-hero__mark-img';
|
||||
img.setAttribute('aria-hidden', 'true');
|
||||
slot.replaceChildren(img);
|
||||
}
|
||||
if (slots.length === 0) {
|
||||
logger.warn('populateMark: no [data-mokosh-slot="mark"] element found in DOM');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After Plan 04-06 (DOMParser inline-SVG injection):
|
||||
```typescript
|
||||
function populateMark(): void {
|
||||
const slots = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('[data-mokosh-slot="mark"]'),
|
||||
);
|
||||
const parser = new DOMParser();
|
||||
const altText = COPY['welcome.hero.mark.alt'] ?? 'Знак Mokosh';
|
||||
for (const slot of slots) {
|
||||
const doc = parser.parseFromString(markSvg, 'image/svg+xml');
|
||||
const svg = doc.documentElement;
|
||||
svg.classList.add('welcome-hero__mark-img');
|
||||
svg.setAttribute('aria-hidden', 'true');
|
||||
svg.setAttribute('role', 'img');
|
||||
svg.setAttribute('aria-label', altText);
|
||||
slot.replaceChildren(svg);
|
||||
}
|
||||
if (slots.length === 0) {
|
||||
logger.warn('populateMark: no [data-mokosh-slot="mark"] element found in DOM');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
NEVER use `innerHTML` — DOMParser + appendChild ONLY (MV3 CSP discipline). The DOMParser parse is safe because the input is a Vite-bundled compile-time literal (no runtime untrusted input).
|
||||
|
||||
From globals.d.ts (current ambient decl for ?url at lines 34-37):
|
||||
```typescript
|
||||
declare module '*.svg?url' {
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
```
|
||||
|
||||
After Plan 04-06 (append):
|
||||
```typescript
|
||||
declare module '*.svg?url' {
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
|
||||
// Plan 04-06 UI-SPEC dark-logo `currentColor` strategy: ambient module
|
||||
// declaration for Vite `?raw` asset imports.
|
||||
declare module '*.svg?raw' {
|
||||
const raw: string;
|
||||
export default raw;
|
||||
}
|
||||
```
|
||||
|
||||
From src/offscreen/recorder.ts:285 (verification target — ALREADY shipped):
|
||||
```typescript
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: { displaySurface: 'monitor', cursor: 'always' }, // ← Plan 01-09 opportunistic; Plan 04-06 verifies
|
||||
monitorTypeSurfaces: 'include',
|
||||
audio: false,
|
||||
} as ...);
|
||||
```
|
||||
|
||||
From src/welcome/welcome.css:91-95 (existing rule — bare class selector works for both img and svg):
|
||||
```css
|
||||
.welcome-hero__mark-img {
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
display: block;
|
||||
}
|
||||
```
|
||||
No change strictly required; the bare class selector matches both `<img class="welcome-hero__mark-img">` and `<svg class="welcome-hero__mark-img">`. Per UI-SPEC: optional explicit dual-selector for diff-clarity is allowed but not required.
|
||||
|
||||
From tests/uat/extension-page-harness.ts:2249-2294 (current A17.8 — Plan 01-10 mark-bundling invariant):
|
||||
- Current: asserts welcome chunk JS contains `data:image/svg+xml,...` data URL + canonical viewBox.
|
||||
- Post-Plan-04-06: assert welcome chunk JS contains raw SVG source as string literal with `currentColor` + canonical viewBox. May split into A17.8a (raw bundling) + A17.8b (runtime inline <svg> injection check via DOM query).
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Wave 0 RED — inline-SVG injection unit test + cursor-visibility defensive pin</name>
|
||||
<files>tests/welcome/inline-svg.test.ts, tests/build/cursor-visibility.test.ts</files>
|
||||
<read_first>tests/background/onboarding.test.ts (welcome-page side-effect assertion shape), tests/i18n/manifest-i18n.test.ts (file-read + string-assertion shape), tests/build/no-remote-fonts.test.ts (single-file grep scaffold), .planning/phases/04-harden-clean-up-optional/04-PATTERNS.md sections "tests/welcome/inline-svg.test.ts" + "tests/build/cursor-visibility.test.ts"</read_first>
|
||||
<action>
|
||||
1. Create `tests/welcome/` directory (NEW — mirrors src/welcome/ to source-tree convention).
|
||||
2. Create `tests/welcome/inline-svg.test.ts` per the 3-test contract (PATTERNS.md):
|
||||
- Setup: vitest jsdom environment (vitest.config.ts already configures this; verify by `grep environment.*jsdom vitest.config.ts`). vi.mock chrome.* + chrome.i18n + chrome.runtime + chrome.storage minimal stubs (welcome page calls chrome.i18n.getMessage in populateI18n; needs a stub).
|
||||
- Build a minimal welcome.html-like DOM with `<div data-mokosh-slot="mark"></div>`.
|
||||
- `await import('../../src/welcome/welcome.ts')` to trigger populateMark.
|
||||
- Test A: `document.querySelector('.welcome-hero__mark svg')` is non-null AND `.welcome-hero__mark img` is null (DOM is now inline SVG, not img).
|
||||
- Test B: `document.querySelector('svg.welcome-hero__mark-img')?.getAttribute('stroke') === 'currentColor'` AND `getAttribute('viewBox') === '0 0 32 32'`.
|
||||
- Test C: `svg.getAttribute('aria-hidden') === 'true'` AND `svg.classList.contains('welcome-hero__mark-img')` AND `svg.getAttribute('role') === 'img'`.
|
||||
|
||||
3. Create `tests/build/cursor-visibility.test.ts` per the single-it scaffold (PATTERNS.md, inverted polarity):
|
||||
- Imports node:fs + node:path + vitest.
|
||||
- `const RECORDER_PATH = resolvePath(process.cwd(), 'src/offscreen/recorder.ts');`
|
||||
- Single `describe('cursor visibility constraint shipped (Plan 01-09 -> verified Plan 04-06)', () => { ... })`.
|
||||
- Single `it("src/offscreen/recorder.ts contains \\`cursor: 'always'\\` in the getDisplayMedia constraints block", () => { ... })`:
|
||||
- `const text = readFileSync(RECORDER_PATH, 'utf8');`
|
||||
- `expect(text).toContain("cursor: 'always'");`
|
||||
|
||||
Filter-pipeline form. Absolute imports. TypeScript-strict.
|
||||
|
||||
RED gate: `npm test -- tests/welcome/ tests/build/cursor-visibility.test.ts --run`:
|
||||
- tests/welcome/inline-svg.test.ts: 3 RED tests (current populateMark uses `<img>` not inline `<svg>`).
|
||||
- tests/build/cursor-visibility.test.ts: 1 GREEN test (the literal exists; regression pin).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm test -- tests/welcome/ tests/build/cursor-visibility.test.ts --run 2>&1 | tee /tmp/04-06-task-1-red.log; grep -cE 'FAIL|✗' /tmp/04-06-task-1-red.log; grep -cE 'PASS|✓' /tmp/04-06-task-1-red.log</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- File `tests/welcome/inline-svg.test.ts` exists with 3 it() blocks in a `describe('UI-SPEC dark-logo currentColor strategy ...')` block.
|
||||
- File `tests/build/cursor-visibility.test.ts` exists with 1 it() block.
|
||||
- Inline-SVG tests: 3 RED today (current welcome.ts uses `<img>`).
|
||||
- Cursor-visibility test: 1 GREEN today (regression pin for already-shipped literal).
|
||||
- `grep -c 'currentColor' tests/welcome/inline-svg.test.ts` returns >=2.
|
||||
- `grep -c "cursor: 'always'" tests/build/cursor-visibility.test.ts` returns >=1.
|
||||
</acceptance_criteria>
|
||||
<done>2 test files committed; 3 RED + 1 GREEN-on-arrival. Atomic commit: `test(04-06): Wave 0 — inline-SVG RED + cursor-visibility regression pin`.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Wave 1 GREEN — SVG stroke recolor + welcome.ts ?raw import + populateMark inline injection + globals.d.ts ambient decl</name>
|
||||
<files>src/shared/brand/mokosh-mark.svg, src/welcome/welcome.ts, globals.d.ts</files>
|
||||
<read_first>src/shared/brand/mokosh-mark.svg, src/welcome/welcome.ts (full; ~199 lines), globals.d.ts, .planning/phases/04-harden-clean-up-optional/04-UI-SPEC.md §"Implementation amendment"</read_first>
|
||||
<action>
|
||||
Edit 1 — src/shared/brand/mokosh-mark.svg (1-attribute change on root):
|
||||
- Read the file (~25 lines, single read).
|
||||
- Use Edit tool to replace `stroke="#181b2a"` with `stroke="currentColor"` ONLY on the root `<svg>` element (the 12 `<line>` + 1 `<rect>` children inherit stroke from root; they are unchanged).
|
||||
- Verify: `grep -c 'stroke="#181b2a"' src/shared/brand/mokosh-mark.svg` returns 0 (was 1); `grep -c 'stroke="currentColor"' src/shared/brand/mokosh-mark.svg` returns 1.
|
||||
|
||||
Edit 2 — src/welcome/welcome.ts (?raw import + populateMark rewrite):
|
||||
- Read the file (~199 lines, single read).
|
||||
- Use Edit tool to replace the line 46 import `import markUrl from '../shared/brand/mokosh-mark.svg?url';` with `import markSvg from '../shared/brand/mokosh-mark.svg?raw';`.
|
||||
- Use Edit tool to replace the entire `populateMark` function body (lines 159-179) with the DOMParser-based inline injection per the `<interfaces>` block above. Preserve the function signature, the `slots` query, the `altText` resolution, the empty-slot warn fallback. The new body inlines the SVG via `parser.parseFromString(markSvg, 'image/svg+xml')` + `slot.replaceChildren(doc.documentElement)`.
|
||||
- Verify TypeScript-strict: the new code references `markSvg` (was `markUrl`); the IDE/tsc should accept this once globals.d.ts has the ambient decl (Edit 3).
|
||||
|
||||
Edit 3 — globals.d.ts (append ambient decl):
|
||||
- Read the file (~38 lines, single read).
|
||||
- Use Edit tool to APPEND the `declare module '*.svg?raw'` block after the existing `*.svg?url` block (lines 34-37). Include the explanatory comment block per the `<interfaces>` above.
|
||||
|
||||
Run gates:
|
||||
- `npx tsc --noEmit` exits 0 (the ?raw ambient decl makes welcome.ts tsc-clean).
|
||||
- `npm test -- tests/welcome/inline-svg.test.ts --run` — Wave 0 RED flips to 3/3 GREEN.
|
||||
- Run full vitest: `npm test -- --run` exits 0 (Plan 04-05 baseline + 4 new tests = >=185 GREEN).
|
||||
- Run `npm run build` and verify the welcome chunk JS contains the raw SVG source: `grep -c 'viewBox="0 0 32 32"' dist/assets/*.js | head -3` — at least one file should match (the welcome chunk has the raw SVG source as a string literal post-?raw-import).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit && npm test -- tests/welcome/inline-svg.test.ts tests/build/cursor-visibility.test.ts --run 2>&1 | tail -10 | tee /tmp/04-06-task-2.log; grep -c 'currentColor' src/shared/brand/mokosh-mark.svg</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `grep -c 'stroke="currentColor"' src/shared/brand/mokosh-mark.svg` returns 1.
|
||||
- `grep -c 'stroke="#181b2a"' src/shared/brand/mokosh-mark.svg` returns 0.
|
||||
- `grep -c 'svg?raw' src/welcome/welcome.ts` returns >=1.
|
||||
- `grep -c 'svg?url' src/welcome/welcome.ts` returns 0 (post-edit; the old import is gone).
|
||||
- `grep -c 'DOMParser' src/welcome/welcome.ts` returns >=1.
|
||||
- `grep -c "declare module '\\*\\.svg\\?raw'" globals.d.ts` returns >=1.
|
||||
- `npm test -- tests/welcome/inline-svg.test.ts --run` exits 0 with 3/3 GREEN (was 3-RED).
|
||||
- `npm test -- tests/build/cursor-visibility.test.ts --run` exits 0 with 1/1 GREEN (preserved).
|
||||
- Full vitest passes: `npm test -- --run` exits 0 with >= 185 GREEN.
|
||||
</acceptance_criteria>
|
||||
<done>SVG + welcome.ts + globals.d.ts edits landed; inline-SVG tests flip 3-RED -> 3-GREEN. Atomic commit: `feat(04-06): Wave 1 — dark-logo currentColor strategy + inline-SVG injection`.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: A17.8 harness sub-check update + 01-07-SUMMARY.md back-patch</name>
|
||||
<files>tests/uat/extension-page-harness.ts, .planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md</files>
|
||||
<read_first>tests/uat/extension-page-harness.ts:2249-2310 (existing A17.8 region), .planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md (full; ~250 lines; line ranges 47/82/109/135/205 per PATTERNS.md), .planning/phases/04-harden-clean-up-optional/04-PATTERNS.md §"A17.8 sub-check update" + §"01-07-SUMMARY.md back-patch"</read_first>
|
||||
<action>
|
||||
Edit 1 — A17.8 sub-check (tests/uat/extension-page-harness.ts):
|
||||
- Read the existing A17.8 block (~line 2294 region; ~20-30 lines around it).
|
||||
- Update the check description and the substring grep targets:
|
||||
- Current: checks for `data:image/svg+xml,...` data URL OR file URL + `viewBox='0 0 32 32'`.
|
||||
- New: checks for raw SVG source string literal (`stroke="currentColor"` + `viewBox="0 0 32 32"`) in the welcome chunk JS.
|
||||
- Either:
|
||||
(a) Update A17.8 in-place with a single new check description.
|
||||
(b) Split into A17.8a (raw SVG source bundled) + A17.8b (inline <svg> injected at populateMark runtime; checks via DOM querySelector that `.welcome-hero__mark svg` is non-null AND its stroke attribute is 'currentColor').
|
||||
- Either approach is acceptable per UI-SPEC §"Acceptance Criteria #3". Pick (a) for minimum-surface; pick (b) if defense-in-depth justifies the extra check.
|
||||
|
||||
Note line range: A17.8 is at ~line 2294 — disjoint from Plan 04-04/05 appends at ~line 3970+. This is what allows Plan 04-06 to land parallel-safe.
|
||||
|
||||
Edit 2 — 01-07-SUMMARY.md back-patch (5 stale "Phase 5" lines):
|
||||
- Read 01-07-SUMMARY.md once.
|
||||
- Locate the 5 stale lines per PATTERNS.md (lines 47, 82, 109, 135, 205 — line numbers approximate; grep for each):
|
||||
- `grep -nE 'Phase 5|deferred to Phase 5|cursor.*Phase 5' .planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md`
|
||||
- For each match, decide:
|
||||
- Flip to "Shipped opportunistically in Plan 01-09 (`recorder.ts:285 cursor: 'always'`); verified Phase 4 Plan 04-06" per the back-patch text in PATTERNS.md.
|
||||
- OR remove the stale framing entirely if the line was forward-pointing TODO.
|
||||
- Make each edit surgical with surrounding context; do NOT rewrite the whole SUMMARY.
|
||||
|
||||
Verify gates:
|
||||
- `npm run build:test` exits 0.
|
||||
- Run UAT harness: `HEADLESS=1 SKIP_PROD_REBUILD=0 SKIP_LONG_UAT=1 npm run test:uat` exits 0 with 35/35 GREEN (A17.8 updated; the dark-logo strategy land hasn't regressed any other assertion).
|
||||
- `grep -c 'Phase 5' .planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md` returns 0 (or only references that are not the stale-deferral; if 0 is too aggressive due to legitimate "Phase 5" mentions, accept >=1 but verify each remaining mention is contextually correct).
|
||||
- `grep -c 'Phase 4 Plan 04-06' .planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md` returns >=1 (the new back-patch citation).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm run build:test && HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat 2>&1 | tail -10 | tee /tmp/04-06-task-3.log; grep -c '35/35' /tmp/04-06-task-3.log</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `npm run build:test` exits 0.
|
||||
- UAT harness 35/35 GREEN with the updated A17.8.
|
||||
- `grep -c 'stroke=\"currentColor\"' tests/uat/extension-page-harness.ts | grep -v '^#'` returns >=1 (new A17.8 grep target).
|
||||
- `grep -c 'data:image/svg+xml' tests/uat/extension-page-harness.ts | grep -v '^#'` returns 0 (old A17.8 grep target removed; verify by counting lines not in comments).
|
||||
- 01-07-SUMMARY.md back-patched: stale "deferred to Phase 5" lines flipped.
|
||||
- `grep -c 'Phase 4 Plan 04-06' .planning/phases/01-stabilize-video-pipeline/01-07-SUMMARY.md` returns >=1.
|
||||
</acceptance_criteria>
|
||||
<done>A17.8 updated + 01-07-SUMMARY.md back-patched. Atomic commit: `feat(04-06): A17.8 inline-SVG check + back-patch 01-07-SUMMARY Phase-5 references`.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 4: Operator empirical UAT — dark-mode aesthetic judgment on welcome hero</name>
|
||||
<files>(no files modified — verification-only checkpoint; gates against the existing dist/ + a real Chrome instance)</files>
|
||||
<action>
|
||||
Step 1 — Pre-checkpoint bundle gates (orchestrator-driven; per saved memory `feedback-pre-checkpoint-bundle-gates.md`; MUST PASS before surfacing Step 2 to operator):
|
||||
1. `npm run build` -> exit 0; dist/ populated.
|
||||
2. SW CSP-safety grep: `grep -rE 'new Function\(|eval\(' dist/assets/index*-bg.js` -> 0 hits (Plan 04-02 effect preserved).
|
||||
3. SW Node-globals grep: `grep -rE 'Buffer\.from|Buffer\.alloc|process\.|require\(' dist/assets/index*-bg.js` -> 0 hits.
|
||||
4. DOM-globals grep: `grep -rE '(window\.|document\.)' dist/assets/index*-bg.js | grep -vE '^//|globalThis|^$'` -> 0 hits in SW chunk.
|
||||
5. Tier-1 SW-bundle-import gate: `npx vitest run tests/background/sw-bundle-import.test.ts` -> GREEN.
|
||||
6. Tier-1 FORBIDDEN_HOOK_STRINGS gate: `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts` -> 12 strings, 0 hits each -> GREEN.
|
||||
|
||||
Step 2 — Operator-driven empirical UAT cycle (manual; ~3-5 min): See `<how-to-verify>` block below for the operator-facing instructions.
|
||||
|
||||
Step 3 — Resume signal handling: orchestrator waits for operator response per `<resume-signal>` block below.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm run build && grep -c 'new Function' dist/assets/index*-bg.js | head -1; HEADLESS=1 SKIP_PROD_REBUILD=1 SKIP_LONG_UAT=1 npm run test:uat 2>&1 | tail -3</automated>
|
||||
</verify>
|
||||
<done>Operator returns "approved" verbatim OR describes a specific issue; if issue, route via `/gsd-debug` per `feedback-gsd-ceremony-for-fixes.md`.</done>
|
||||
<what-built>
|
||||
UI-SPEC dark-logo contrast strategy: SVG stroke recolor + inline-SVG injection + ambient module decl + harness update + cursor-visibility regression pin + docs back-patch. Pre-checkpoint bundle gates 6/6 PASS:
|
||||
- Gate 1: `npm run build` exit 0
|
||||
- Gate 2: `grep -c 'new Function' dist/assets/index.ts-*.js` returns 0 (Plan 04-02 effect preserved)
|
||||
- Gate 3: SW Node-globals grep clean
|
||||
- Gate 4: DOM-globals bundled-lib idiom (typeof-guarded)
|
||||
- Gate 5: Tier-1 SW-bundle-import unit gate GREEN
|
||||
- Gate 6: FORBIDDEN_HOOK_STRINGS at 12 (no change)
|
||||
- Plus pre-checkpoint vitest baseline >=185 GREEN; UAT harness 35/35 GREEN.
|
||||
|
||||
Auto-confirms:
|
||||
- mokosh-mark.svg now uses `stroke="currentColor"` (1-line semantic change)
|
||||
- welcome.ts inline-injects the SVG via DOMParser (no <img>, no innerHTML, no eval)
|
||||
- On the light surface (current welcome page rendering), the mark appears identical to Plan 01-10 cycle-2 ack — `color: var(--mks-fg-inverse)` at `.welcome-hero__mark` cascades to the inline SVG's `currentColor`, resolving to `--mks-linen-50` (off-white) stroke on the madder-orange BG.
|
||||
- PNG toolbar icons unchanged (Chrome auto-inverts on dark toolbars).
|
||||
- cursor: 'always' already at recorder.ts:285 (Plan 01-09 opportunistic; verified by tests/build/cursor-visibility.test.ts).
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Build the production bundle: `npm run build` exit 0.
|
||||
2. Load `dist/` as unpacked extension into Chrome (chrome://extensions/ -> Developer mode -> Load unpacked -> select dist/).
|
||||
3. Open the welcome page (it should auto-open on first install; if not, navigate to chrome-extension://<ID>/src/welcome/welcome.html).
|
||||
4. **Light-surface verification (regression baseline):** the welcome hero mark renders inside the madder-600 circle with linen-50 (off-white) stroke. Visually identical to Plan 01-10 cycle-2 ack 2026-05-20 "All good". Expected appearance unchanged.
|
||||
5. **Dark-surface aesthetic judgment (the operator-empirical decision):** enable your OS dark mode (macOS: System Settings -> Appearance -> Dark; Windows: Settings -> Personalization -> Colors -> Dark; Linux/GNOME: Settings -> Appearance -> Dark; OR Chrome's own setting at chrome://settings/appearance -> Theme: Dark).
|
||||
6. Reload the welcome page. The mark inherits the `.dark` class behavior from `src/shared/tokens.css` lines 234-251 (which overrides `--mks-fg-inverse` to `--mks-ink-900` deep indigo on dark surfaces). The wrapper background stays madder; the stroke darkens to deep indigo on dark surfaces.
|
||||
7. Judge the visual contrast: does the mark remain legible on the dark wrapper-on-dark-OS combination? The strategy aims to keep the mark crisp on EITHER light or dark surfaces by inverting the stroke when the surface flips.
|
||||
8. (Optional) Test a captured recording: start recording, click around, SAVE -> open `video/last_30sec.webm` -> verify the operator's cursor is visible in playback (the cursor: 'always' verification per ROADMAP cursor visibility item).
|
||||
|
||||
**Resume signals:**
|
||||
- "approved" — dark-mode aesthetic acceptable; Phase 4 dark-logo strategy closed.
|
||||
- Describe issues if any (e.g., "stroke too thin on dark; needs heavier stroke-width" or "wrapper madder clashes with dark BG; want a different accent"). Plan execution will route via `/gsd-debug` per `feedback-gsd-ceremony-for-fixes.md` (no hot-edits to the UI-SPEC or canonical design system).
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" or describe issues (e.g., "stroke too thin", "need heavier weight on dark", "want a different accent").</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Vite ?raw import -> compile-time string literal | SVG source is bundled as a compile-time string; NO runtime untrusted input flows into DOMParser; the parser's input is statically known at build time |
|
||||
| DOMParser parse -> live DOM | parseFromString('image/svg+xml') is a CSP-safe operation per MDN (does NOT execute scripts in the parsed SVG; runs in a sandboxed parser context); no innerHTML; no eval |
|
||||
| Inline <svg> CSS color inheritance | inline SVG (parsed + appended via DOMParser) inherits parent CSS color per W3C SVG2 §13.3; `currentColor` on stroke resolves to the wrapper's `color: var(--mks-fg-inverse)` cascade |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-04-06-01 | Tampering | a future developer might switch DOMParser to innerHTML for "simplicity" — innerHTML in a content script context is unsafe (CSP risk + script-execution surface) | mitigate | Inline code comment + executor-pattern in the action: "NEVER use innerHTML — DOMParser + appendChild only (MV3 CSP discipline)"; A17.8 harness check verifies the inline-SVG DOM shape (defense in depth) |
|
||||
| T-04-06-02 | Information Disclosure | the inline SVG has no PII or secret; it's the canonical Mokosh brand mark | accept | Static brand asset; out-of-tree threat surface |
|
||||
| T-04-06-03 | Spoofing | cursor visibility in captured frames could leak sensitive UI overlay state (e.g., 2FA OTP digit operator was about to type) — but this is a known and intended diagnostic feature per Plan 01-07 obs; out of scope for password masking per D-P3-02 charter | accept | Operator-side responsibility per CONTEXT charter; v2 candidate per CONTEXT Deferred Ideas |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `npx tsc --noEmit` exits 0.
|
||||
- `npm run build` exits 0.
|
||||
- vitest baseline +4 to >=185 GREEN (Task 1's 4 tests + Task 2 flips 3-RED -> 3-GREEN).
|
||||
- `grep -c 'stroke="currentColor"' src/shared/brand/mokosh-mark.svg` returns 1.
|
||||
- `grep -c "svg?raw" src/welcome/welcome.ts` returns 1.
|
||||
- `grep -c "declare module '\\*\\.svg\\?raw'" globals.d.ts` returns 1.
|
||||
- UAT harness 35/35 GREEN with updated A17.8 (no harness count change; assertion quality changes).
|
||||
- 01-07-SUMMARY.md back-patched.
|
||||
- Operator empirical ack received (Task 4 resume signal "approved").
|
||||
- Pre-checkpoint bundle gates 6/6 PASS.
|
||||
- PNG toolbar icons byte-identical (no change to scripts/rasterize-icons.sh or icons/*.png).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- UI-SPEC dark-logo strategy landed end-to-end (Tasks 1-3 automation + Task 4 operator ack).
|
||||
- Cursor visibility VERIFIED at recorder.ts:285 via regression-pin test + 01-07-SUMMARY back-patch (RESEARCH Finding 4 closure).
|
||||
- Inline-SVG injection contract pinned by 3 unit tests + 1 harness assertion update.
|
||||
- ROADMAP cursor visibility item GREEN.
|
||||
- ROADMAP dark-surface logo contrast item GREEN.
|
||||
- Operator empirical ack received on dark-mode visual aesthetic.
|
||||
- Pre-checkpoint bundle gates 6/6 PASS preserved.
|
||||
- UAT harness 35/35 GREEN preserved.
|
||||
- vitest baseline +4 (Plan 04-05 baseline 181 -> Plan 04-06 baseline >= 185).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-harden-clean-up-optional/04-06-SUMMARY.md` capturing:
|
||||
- mokosh-mark.svg diff (1-char change documented)
|
||||
- welcome.ts diff (?raw + DOMParser + populateMark rewrite)
|
||||
- globals.d.ts diff (ambient decl append)
|
||||
- A17.8 harness sub-check diff (old data-URL grep -> new raw-source grep)
|
||||
- 01-07-SUMMARY.md back-patch (5 stale Phase-5 lines flipped)
|
||||
- 2 new test files (inline-svg + cursor-visibility) with RED->GREEN cycle commits
|
||||
- Operator empirical UAT verbatim ack (e.g., "approved", "all good", or specific issues)
|
||||
- Pre-checkpoint bundle gates 6/6 PASS evidence (grep outputs)
|
||||
- vitest before/after (181 -> >=185)
|
||||
- UAT 35/35 GREEN preservation
|
||||
- Commit refs (Tasks 1 + 2 + 3 + 4 ack)
|
||||
</output>
|
||||
</content>
|
||||
</invoke>
|
||||
347
.planning/phases/04-harden-clean-up-optional/04-07-PLAN.md
Normal file
347
.planning/phases/04-harden-clean-up-optional/04-07-PLAN.md
Normal file
@@ -0,0 +1,347 @@
|
||||
---
|
||||
phase: 04
|
||||
slug: harden-clean-up-optional
|
||||
plan: 07
|
||||
type: execute
|
||||
wave: 6
|
||||
depends_on:
|
||||
- 01
|
||||
- 02
|
||||
- 03
|
||||
- 04
|
||||
- 05
|
||||
- 06
|
||||
files_modified:
|
||||
- .planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md
|
||||
- .planning/REQUIREMENTS.md
|
||||
- .planning/ROADMAP.md
|
||||
- .planning/STATE.md
|
||||
- .planning/PROJECT.md
|
||||
autonomous: true
|
||||
requirements: []
|
||||
tags:
|
||||
- verification
|
||||
- phase-4-closure
|
||||
- roadmap-backfill
|
||||
- v1-close-prep
|
||||
- aggregator
|
||||
- charter-d-p4-05
|
||||
- charter-d-p4-01
|
||||
user_setup: []
|
||||
must_haves:
|
||||
truths:
|
||||
- "04-VERIFICATION.md exists at .planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md with canonical frontmatter shape (phase + verified + status + score + overrides_applied + override_notes + human_verification)"
|
||||
- "Per-Requirement Scorecard cites all 4 ROADMAP success criteria with evidence rows (Plan + commit + UAT assertion + grep gate output)"
|
||||
- "All P1 polish items (#11 + #14 + #15) cited as GREEN with Plan 04-01 evidence"
|
||||
- "All Phase 4 hardening items (setimmediate + dead-code + generate-icons + A29 race fix + cursor verification + dark-logo + ROADMAP backfill) cited as GREEN with their plan + commit refs"
|
||||
- "Pre-checkpoint bundle gates 6/6 PASS evidence (per saved memory feedback-pre-checkpoint-bundle-gates.md)"
|
||||
- "Operator empirical ack from Plan 04-06 cited verbatim with date stamp"
|
||||
- "Deferred Items table carries forward the v1.1/v2 items: rrweb v2 upgrade + programmatic SW-RAM measurement + REQ-password-confidentiality v2 candidate"
|
||||
- "ROADMAP.md Phase 4 row flipped [x] with closure date; harness count updated from 33 to 35 (Plans 04-04 + 04-05 added A33 + A34)"
|
||||
- "ROADMAP.md Plans 01-08..01-13 rows verified per D-P4-05 backfill (per plan-checker flag #4)"
|
||||
- "REQUIREMENTS.md: no new REQs but verification status notes appended where ROADMAP SCs map to existing REQs"
|
||||
- "STATE.md progress.completed_phases incremented to 4 (was 3); milestone v1 status updated to CLOSED-PENDING-ALPHA OR CLOSED based on alpha redistribution status"
|
||||
- "PROJECT.md Validated section evolved to include Phase 4 hardening closure summary"
|
||||
artifacts:
|
||||
- path: ".planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md"
|
||||
provides: "Phase 4 closure aggregator: 4 ROADMAP SCs + audit P1 polish + flake stabilization + visual polish + build hygiene + cross-cutting gates + operator ack + deferred items"
|
||||
min_lines: 120
|
||||
- path: ".planning/REQUIREMENTS.md"
|
||||
provides: "Verification status notes appended for REQs touched by Phase 4 (no new REQs)"
|
||||
contains: "Phase 4 closure"
|
||||
- path: ".planning/ROADMAP.md"
|
||||
provides: "Phase 4 row flipped [x] + closure date + harness count update + Plan rows for 01-08..01-13 verified backfilled"
|
||||
contains: "Phase 4: Harden + clean up"
|
||||
- path: ".planning/STATE.md"
|
||||
provides: "progress.completed_phases: 4; current focus updated; session continuity logged"
|
||||
contains: "milestone v1"
|
||||
- path: ".planning/PROJECT.md"
|
||||
provides: "Validated section update + DEC-* table closure citations (where applicable)"
|
||||
contains: "Phase 4"
|
||||
key_links:
|
||||
- from: ".planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md"
|
||||
to: "Plan 04-01..04-06 SUMMARY files"
|
||||
via: "per-plan citation in Per-Requirement Scorecard"
|
||||
pattern: "Plan 04-0\\d"
|
||||
- from: ".planning/ROADMAP.md"
|
||||
to: ".planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md"
|
||||
via: "Phase 4 row flipped [x] with closure-doc citation"
|
||||
pattern: "04-VERIFICATION\\.md"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Phase 4 closure ceremony — the v1 milestone close. This plan does NO source-code or test changes; it does the documentation aggregation + marker flips that close out Phase 4 and signal v1 milestone closure.
|
||||
|
||||
Three workstreams:
|
||||
|
||||
1. **04-VERIFICATION.md (NEW; aggregator):** Create the canonical Phase 4 verification document per the precedents at Phase 3 03-VERIFICATION.md + Phase 2 02-VERIFICATION.md + Phase 1 01-VERIFICATION.md. The 4 ROADMAP SCs are cited row-by-row with evidence (Plan + commit ref + UAT assertion ID + grep gate output). The audit P1 polish items (#11 + #14 + #15) and the additional Phase 4 work (setimmediate polyfill + dead-code grep + generate-icons rename + A29 race fix + cursor verification + dark-logo strategy) are inventoried in a non-ROADMAP-SC section as "Audit residual closure" with the same evidence shape. Cross-cutting gates section: UAT harness 33 -> 35 GREEN (+A33 from Plan 04-04 + A34 from Plan 04-05), vitest baseline 171 -> >=185 GREEN, Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12, pre-checkpoint bundle gates 6/6 PASS (Gate 2 polarity flipped 1 -> 0 per Plan 04-02). Operator empirical ack (Plan 04-06 Task 4) cited verbatim. Deferred Items table carries forward the v1.1/v2 items from CONTEXT.
|
||||
|
||||
2. **ROADMAP backfill (D-P4-05):** Per the plan-checker flag #4 against Plan 01-13 — Plans 01-08..01-13 were inline-tracked but not row-added in ROADMAP.md Phase 1 row block. PATTERNS.md notes the rows ARE at lines 90-95 already, so this is mostly a verification + addition of any missing row text per re-audit. Add any Phase 2/3 plan rows similarly missing (per the re-audit; per saved feedback `feedback-no-unilateral-scope-reduction.md`, surface every potentially-missing row to the user via the VERIFICATION.md backfill table for sign-off rather than pre-filtering).
|
||||
|
||||
3. **Marker flips:** REQUIREMENTS.md (no new REQs but optional closure notes); ROADMAP.md (Phase 4 row [x] with date); STATE.md (`progress.completed_phases: 4`; `current_focus: v1 milestone CLOSED-PENDING-ALPHA OR CLOSED`); PROJECT.md (Validated section evolves; DEC-* table closure citations where applicable).
|
||||
|
||||
Purpose: Closes Phase 4 + closes v1 milestone (or stages CLOSED-PENDING-ALPHA gate depending on alpha redistribution state per D-P4-04). Phase 4 is THE LAST phase per the 4-phase ROADMAP; no Phase 5 work is planned.
|
||||
|
||||
Output: 1 NEW VERIFICATION.md + 4 docs marker flips. No code, no tests, no harness changes. Single closure commit per file (or 1 mega-commit per the closure convention).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-CONTEXT.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-RESEARCH.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-UI-SPEC.md
|
||||
@.planning/phases/04-harden-clean-up-optional/04-VALIDATION.md
|
||||
|
||||
# Prior verification documents — pattern precedents
|
||||
@.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md
|
||||
@.planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md
|
||||
@.planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md
|
||||
|
||||
# Plan SUMMARYs for evidence rows (populated when Plans 04-01..04-06 complete; this plan executes last)
|
||||
# @.planning/phases/04-harden-clean-up-optional/04-01-SUMMARY.md (Plan 04-01 closure)
|
||||
# @.planning/phases/04-harden-clean-up-optional/04-02-SUMMARY.md (Plan 04-02 closure)
|
||||
# @.planning/phases/04-harden-clean-up-optional/04-03-SUMMARY.md (Plan 04-03 closure)
|
||||
# @.planning/phases/04-harden-clean-up-optional/04-04-SUMMARY.md (Plan 04-04 closure)
|
||||
# @.planning/phases/04-harden-clean-up-optional/04-05-SUMMARY.md (Plan 04-05 closure)
|
||||
# @.planning/phases/04-harden-clean-up-optional/04-06-SUMMARY.md (Plan 04-06 closure)
|
||||
|
||||
<interfaces>
|
||||
<!-- Key shapes the executor consumes directly. Extracted from precedent VERIFICATION.md files 2026-05-21. -->
|
||||
|
||||
From .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md (immediate prior precedent; 4-override template):
|
||||
- Frontmatter shape: `phase` + `verified` ISO timestamp + `status: passed` + `score: <N>/<N> ROADMAP success criteria + <M>/<M> SPEC §10 criteria` + `overrides_applied: <count>` + `re_verification` block + `override_notes` block + `human_verification` block + `deferred` block.
|
||||
- Body sections: Per-Requirement Scorecard + Cross-Cutting Gates + Operator-Empirical Acks (verbatim + commit refs) + Deferred Items.
|
||||
|
||||
From .planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md (per-requirement scorecard + cross-cutting gates + operator acks shape):
|
||||
- Per-Requirement Scorecard: one row per REQ, columns include {ID, Acceptance, Status, Evidence, Plan + commit}.
|
||||
- Cross-Cutting Gates: vitest count, UAT count, Tier-1 grep count, bundle gates 6/6.
|
||||
- Operator-Empirical Acks: verbatim quote + date + commit + context (the operator surface that was empirically validated).
|
||||
- Deferred Items: forward-looking table with category + item + status + deferred_at.
|
||||
|
||||
Per CONTEXT §"Deferred Ideas" — v1.1/v2 items to carry forward in 04-VERIFICATION.md:
|
||||
- rrweb 2.0.0-alpha.4 -> stable v2 upgrade (D-P3-03 + D-P4-01)
|
||||
- Programmatic SW-realm RAM measurement (D-P3-04 + D-P4-01)
|
||||
- REQ-password-confidentiality v2 candidate (D-P3-02; only if charter reverses)
|
||||
- Alpha-tester findings integration (D-P4-04; routed via separate maintenance window)
|
||||
|
||||
Frontmatter sketch for 04-VERIFICATION.md:
|
||||
```yaml
|
||||
# (yaml document marker omitted for parser-compat)
|
||||
phase: 04-harden-clean-up-optional
|
||||
verified: 2026-05-21T<HH-MM-SS>Z
|
||||
status: passed
|
||||
score: 4/4 ROADMAP success criteria + 3/3 audit P1 polish items + 5/5 hardening items (setimmediate + dead-code + generate-icons + A29 race + cursor verify + dark-logo)
|
||||
overrides_applied: <count>
|
||||
re_verification:
|
||||
milestone: v1.0.0
|
||||
previous_phase_status: 3/3 phases complete
|
||||
previous_phase_score: 5/5 SPEC §10 + 9/9 acceptance criteria with 4 overrides
|
||||
gaps_closed:
|
||||
- "ROADMAP SC #1 — SW state persistence via A33 harness assertion (Plan 04-04)"
|
||||
- "ROADMAP SC #2 — fetch + XHR network_error via A34 harness assertion (Plan 04-05) + P1 #11 unit tests (Plan 04-01)"
|
||||
- "ROADMAP SC #3 — generate-icons.cjs rename verified (Plan 04-02)"
|
||||
- "ROADMAP SC #4 — dead-code grep regression-pinned (Plan 04-02)"
|
||||
- "Audit P1 #11/#14/#15 — content-script polish tests GREEN (Plan 04-01)"
|
||||
- "setimmediate polyfill — dist/ SW chunk grep 0 hits 'new Function' (Plan 04-02)"
|
||||
- "A29 flake — strict-sentinel rewrite + 5/5 PASS stress (Plan 04-03)"
|
||||
- "Cursor visibility — verified shipped at recorder.ts:285 + 01-07-SUMMARY back-patched (Plan 04-06; RESEARCH Finding 4)"
|
||||
- "Dark-logo currentColor strategy — inline-SVG injection landed (Plan 04-06; operator ack received)"
|
||||
- "ROADMAP backfill Plans 01-08..01-13 — verified per D-P4-05 (this plan)"
|
||||
override_notes:
|
||||
- dimension: "<per-criterion override per Phase 4 needs>"
|
||||
initial_status: "<...>"
|
||||
override_to: "<...>"
|
||||
rationale: |
|
||||
<verbatim memory-citation block>
|
||||
human_verification:
|
||||
- test: "dark-mode operator visual aesthetic on welcome hero"
|
||||
expected: "mark legible on OS dark-mode rendering surface"
|
||||
why_human: "Aesthetic contrast judgment — UI-SPEC §'Manual-Only Verifications' acceptance criterion #6; canonical operator-empirical case per feedback-trust-harness-over-manual-uat.md"
|
||||
evidence: "Plan 04-06 Task 4 operator ack <verbatim>"
|
||||
deferred:
|
||||
- truth: "rrweb 2.0.0-alpha.4 -> stable v2 upgrade"
|
||||
addressed_in: "v1.1 / v2 maintenance milestone"
|
||||
evidence: "D-P3-03 + D-P4-01 charter exclusion + alpha-pin stable across 13 plans + 35/35 UAT GREEN"
|
||||
- truth: "Programmatic SW-realm RAM measurement via chrome.devtools Memory API"
|
||||
addressed_in: "v1.1 / v2 maintenance milestone"
|
||||
evidence: "D-P3-04 + D-P4-01 charter exclusion; A32 best-effort + chrome://memory-internals + alpha-distribution coverage accepted"
|
||||
- truth: "REQ-password-confidentiality v2 candidate"
|
||||
addressed_in: "v1.1 / v2 maintenance milestone IF charter reverses"
|
||||
evidence: "D-P3-02 + D-P4-01 charter exclusion 2026-05-20 — 'we don't care about privacy hardening'"
|
||||
- truth: "Alpha-tester findings integration"
|
||||
addressed_in: "post-v1 maintenance window"
|
||||
evidence: "D-P4-04 charter — operator handles alpha signal out-of-band"
|
||||
# (yaml document marker omitted for parser-compat)
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Write 04-VERIFICATION.md aggregator (NEW)</name>
|
||||
<files>.planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md</files>
|
||||
<read_first>.planning/phases/01-stabilize-video-pipeline/01-VERIFICATION.md, .planning/phases/02-stabilize-export-pipeline/02-VERIFICATION.md, .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md, all Plan 04-01..04-06 SUMMARY files (.planning/phases/04-harden-clean-up-optional/04-0?-SUMMARY.md — produced by the prior plans)</read_first>
|
||||
<action>
|
||||
1. Read the 3 precedent VERIFICATION.md files (Phase 1 + 2 + 3) ONCE each to extract the canonical frontmatter shape + body section structure. Read each Plan 04-01..04-06 SUMMARY.md once to extract evidence (commit refs, test counts, harness assertion numbers, pre/post measurements).
|
||||
|
||||
2. Create `.planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md` with frontmatter per the sketch in `<interfaces>` above. Replace placeholder fields with actual values from the Plan 04-01..04-06 SUMMARYs:
|
||||
- `verified:` — current ISO timestamp (`date -u +%Y-%m-%dT%H:%M:%SZ`)
|
||||
- `score:` — fill in actual counts from SUMMARYs
|
||||
- `re_verification.gaps_closed[*]` — bullet per closed gap with Plan + commit citation
|
||||
- `override_notes` — populate if any T5 override applied to a Phase 4 criterion (e.g., the operator-empirical UAT for dark-logo can be cited as harness-coverage-augmented rather than override; depends on Plan 04-06 outcome).
|
||||
- `human_verification[0].evidence` — verbatim operator ack quote from Plan 04-06 Task 4 SUMMARY.
|
||||
- `deferred[]` — 4 v1.1/v2 items per CONTEXT Deferred Ideas + the alpha-distribution item per D-P4-04.
|
||||
|
||||
3. Body sections (after frontmatter):
|
||||
- `## Per-Requirement Scorecard` — Phase 4 has no new REQs but rows for the 4 ROADMAP SCs + the 5 hardening items (per `score:` line). Each row: {Item, Acceptance, Status, Evidence, Plan + commit}.
|
||||
- `## Cross-Cutting Gates`:
|
||||
- vitest count: 171 (Phase 3 baseline) -> 171 + new Wave 0 tests from Plans 04-01 + 04-02 + 04-06 = approximately 171 + 8 + 2 + 4 = 185 GREEN (verify exact count via final `npm test -- --run` invocation).
|
||||
- UAT harness count: 33 -> 35 GREEN (Plans 04-04 A33 + 04-05 A34 added; Plan 04-03 rewrote A29 in-place, count unchanged).
|
||||
- Tier-1 FORBIDDEN_HOOK_STRINGS: 12 unchanged (Phase 4 introduced no new __MOKOSH_UAT__-gated symbols per CONTEXT Claude's Discretion).
|
||||
- Pre-checkpoint bundle gates: 6/6 PASS (Plan 04-02 flipped Gate 2 polarity 1 -> 0 hits `new Function`).
|
||||
- `## Operator-Empirical Acks (verbatim + commit refs)`:
|
||||
- Append the Plan 04-06 Task 4 ack from the SUMMARY (verbatim quote + date + commit hash).
|
||||
- `## Deferred Items` — table mirroring frontmatter `deferred:`.
|
||||
- `## ROADMAP backfill verification (D-P4-05)` — list of Plans 01-08..01-13 row presence + any newly-added rows. If all rows are already present (per PATTERNS.md noting lines 90-95 exist), this section's content is "Verified — no row additions needed; plan-checker flag #4 closed".
|
||||
|
||||
4. Commit: `docs(04-07): Phase 4 closure — 04-VERIFICATION.md aggregator (4/4 ROADMAP SCs + N/N hardening items)`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f .planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md && grep -cE '^## ' .planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md; grep -c 'Plan 04-0' .planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- File exists at `.planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md`.
|
||||
- Frontmatter valid (parseable YAML; required fields present per precedent).
|
||||
- File has >= 5 top-level `## ` sections (Per-Requirement Scorecard + Cross-Cutting Gates + Operator-Empirical Acks + Deferred Items + ROADMAP backfill verification).
|
||||
- File >= 120 lines.
|
||||
- `grep -c 'Plan 04-0' .planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md` returns >= 6 (one per Plan 04-01..04-06 evidence citation).
|
||||
- `grep -c 'commit' .planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md` returns >= 6.
|
||||
</acceptance_criteria>
|
||||
<done>04-VERIFICATION.md created with frontmatter + 5+ body sections + 6+ plan citations + deferred items. Commit: `docs(04-07): Phase 4 closure — 04-VERIFICATION.md aggregator`.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Marker flips — REQUIREMENTS.md + ROADMAP.md + STATE.md + PROJECT.md</name>
|
||||
<files>.planning/REQUIREMENTS.md, .planning/ROADMAP.md, .planning/STATE.md, .planning/PROJECT.md</files>
|
||||
<read_first>.planning/REQUIREMENTS.md, .planning/ROADMAP.md, .planning/STATE.md (frontmatter only), .planning/PROJECT.md (Validated section), .planning/phases/03-spec-10-smoke-verification-dom-event-log-verification/03-VERIFICATION.md (precedent for closure marker style)</read_first>
|
||||
<action>
|
||||
Edit 1 — REQUIREMENTS.md (verification status notes):
|
||||
- Phase 4 has NO new REQ entries. The 4 ROADMAP success criteria verify against existing REQs (Phase 1-3). Optional: append a "Phase 4 verification" note to each REQ touched by a Phase 4 SC (per Phase 1/2/3 precedent of inline closure citations).
|
||||
- Surgical edits per REQ:
|
||||
- REQ-video-ring-buffer: append "Phase 4 closure 2026-05-21: ROADMAP SC #1 SW state persistence verified via Plan 04-04 A33 (UAT 5-min idle + worker.close() empirical PASS; archive video size > 100 KB after SW eviction)."
|
||||
- REQ-user-event-log: append "Phase 4 closure: Audit P1 #11/#14/#15 polish + ROADMAP SC #2 fetch+XHR empirical (Plan 04-01 + Plan 04-05 A34) GREEN."
|
||||
- REQ-rrweb-dom-buffer: append "Phase 4 closure: A29 cs-injection-world rewrite + strict-sentinel filter (Plan 04-03); flake closed 5/5 PASS."
|
||||
- REQ-install-clean: append "Phase 4 closure: ROADMAP SC #3 generate-icons.cjs rename + SC #4 dead-code grep verified (Plan 04-02)."
|
||||
|
||||
Edit 2 — ROADMAP.md (Phase 4 row [x] + backfill verification):
|
||||
- Locate the Phase 4 entry under `## Phases` (top-level list). Flip `- [ ] **Phase 4: Harden + clean up** _(optional)_ ...` to `- [x] **Phase 4: Harden + clean up** _(optional)_ — CLOSED 2026-05-21 via Plan 04-07 closure (4/4 ROADMAP SCs + N/N hardening items GREEN; 04-VERIFICATION.md at .planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md). UAT 33 -> 35 GREEN; vitest 171 -> >=185 GREEN; Pre-checkpoint bundle gates 6/6 PASS (Gate 2 polarity flipped per Plan 04-02). Operator empirical ack 2026-05-21 (Plan 04-06) for dark-mode visual aesthetic.`
|
||||
- Locate the Phase 4 details section. Add `**Plans**:` list with rows for 04-01..04-07 (mirror Phase 3 plan-row block):
|
||||
- [x] 04-01-PLAN.md — Audit P1 polish #11 + #14 + #15 (TDD; 3 unit tests + 3 surgical edits in src/content/index.ts)
|
||||
- [x] 04-02-PLAN.md — Build/CSP hygiene (setimmediate polyfill + dead-code grep + generate-icons.cjs rename)
|
||||
- [x] 04-03-PLAN.md — A29 cs-injection-world rewrite (strict-sentinel; 5/5 PASS stress)
|
||||
- [x] 04-04-PLAN.md — A33 SW state persistence harness assertion (5-min idle + worker.close() CDP; ROADMAP SC #1)
|
||||
- [x] 04-05-PLAN.md — A34 fetch + XHR network_error empirical (ROADMAP SC #2; validates Plan 04-01 P1 #11 end-to-end)
|
||||
- [x] 04-06-PLAN.md — Dark-logo currentColor + cursor visibility verification + 01-07-SUMMARY back-patch (UI-SPEC; operator empirical ack)
|
||||
- [x] 04-07-PLAN.md — Phase 4 closure aggregator (this plan)
|
||||
- Update the `## Progress` table row for Phase 4: change `0/TBD` to `7/7` and set Status to CLOSED 2026-05-21.
|
||||
- Verify Plans 01-08..01-13 rows exist in the Phase 1 plan-row block (per D-P4-05). Per PATTERNS.md they ARE at lines 90-95; just verify visually + document in 04-VERIFICATION.md.
|
||||
|
||||
Edit 3 — STATE.md (frontmatter + Current Position + Session Continuity):
|
||||
- Frontmatter:
|
||||
- `progress.completed_phases: 4` (was 3).
|
||||
- `progress.total_plans: 30` (was 23; +7 from Phase 4) — verify the exact count.
|
||||
- `progress.completed_plans: 30` (was 23; +7).
|
||||
- `progress.percent: 100`.
|
||||
- `status: ready_for_v1_close` OR `status: v1_closed` depending on whether the v1 close is a separate ceremony.
|
||||
- `stopped_at: 'Phase 4 closed 2026-05-21; v1 milestone ready for alpha re-distribution (per D-P4-04 ack out-of-band)'`.
|
||||
- `last_updated:` — current ISO timestamp.
|
||||
- Current Position block: update narrative to reflect Phase 4 closure + milestone v1 close gate (alpha redistribution).
|
||||
- Performance Metrics section: append Phase 04 row to the "By Phase" table with the plan count + cumulative time.
|
||||
- Session Continuity log: prepend a new entry for the current session "2026-05-21 Phase 4 closed end-to-end via 7 plans (04-01..04-07); v1 milestone ready for alpha redistribution".
|
||||
|
||||
Edit 4 — PROJECT.md (Validated section + DEC closure citations):
|
||||
- Locate the `## Validated` section. Append a Phase 4 entry summarizing the hardening + audit-residual closure + visual polish.
|
||||
- DEC-* table: if any DEC was touched by Phase 4 plans (likely none — Phase 4 is hardening, not new decisions), append closure citations. Otherwise no change.
|
||||
- Active section: update Phase 4 status to CLOSED; remove from active backlog.
|
||||
|
||||
Verify gates:
|
||||
- `git status` — only the 4 docs + 1 new VERIFICATION.md are dirty (no inadvertent code changes).
|
||||
- Each edit lands as a separate atomic commit OR one combined closure commit per the closure convention (Phase 1 + 2 + 3 precedent used 1 commit per marker file; Phase 4 should mirror).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -c 'Phase 4.*CLOSED\\|\\[x\\] \\*\\*Phase 4' .planning/ROADMAP.md; grep -c 'completed_phases: 4' .planning/STATE.md; grep -c 'Phase 4 closure' .planning/REQUIREMENTS.md</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c '\\[x\\] \\*\\*Phase 4' .planning/ROADMAP.md` returns >= 1 (Phase 4 row flipped).
|
||||
- `grep -c 'Phase 4 closure' .planning/REQUIREMENTS.md` returns >= 3 (4 REQ closure citations).
|
||||
- `grep -c 'completed_phases: 4' .planning/STATE.md` returns 1.
|
||||
- `grep -c 'Phase 4' .planning/PROJECT.md` returns >= 1 (Validated section entry).
|
||||
- ROADMAP.md `## Progress` table updated for Phase 4 row.
|
||||
- Plans 01-08..01-13 rows verified present per D-P4-05 (no addition needed if PATTERNS.md mapping holds; otherwise add per re-audit).
|
||||
</acceptance_criteria>
|
||||
<done>4 marker files updated; Phase 4 closure markers GREEN. Commits: `docs(04-07): closure markers — REQUIREMENTS + ROADMAP + STATE + PROJECT for Phase 4 + v1 close`.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Phase 4 plan SUMMARYs -> 04-VERIFICATION.md aggregator | aggregator reads each SUMMARY's reported counts + commit refs verbatim; no fabrication; if a SUMMARY is missing or incomplete, the aggregator MUST flag rather than fill defaults |
|
||||
| 04-VERIFICATION.md -> alpha redistribution -> tester confirmation | The 04-VERIFICATION.md scorecard is what the operator references when deciding whether the v1 build is ready for alpha tester redistribution per D-P4-04; misleading scorecard rows could cause premature alpha-build sign-off |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-04-07-01 | Repudiation | aggregator could over-claim closure (e.g., mark ROADMAP SC #2 GREEN when only fetch is empirically verified but XHR is not) | mitigate | Each scorecard row MUST cite specific UAT assertion ID (e.g., "A34.3 XHR 404 -> network_error" or "A34.5 XHR meta.status === 404") + commit hash; absence of an assertion = row marked PARTIAL or YELLOW with explanation |
|
||||
| T-04-07-02 | Tampering | a docs-only commit could in theory be conflated with a code commit; the closure ceremony must be docs-pure | accept | Verification gate `git diff --stat HEAD~1 HEAD` on closure commits should show only `.planning/` files dirty; pre-commit check |
|
||||
| T-04-07-03 | Repudiation | alpha redistribution decision is non-automated (per D-P4-04 user handles out-of-band); 04-VERIFICATION.md is advisory not gating | accept | Documented in scorecard + STATE.md status field clarifies v1 close vs CLOSED-PENDING-ALPHA distinction |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- 04-VERIFICATION.md exists with valid frontmatter + 5+ body sections + 6+ plan-evidence citations + 120+ lines.
|
||||
- ROADMAP.md Phase 4 row flipped [x] with closure date + plan-row block enumerating 04-01..04-07.
|
||||
- ROADMAP.md `## Progress` table updated for Phase 4 (count + status).
|
||||
- REQUIREMENTS.md: 3-4 REQ closure citation lines added (no new REQs).
|
||||
- STATE.md: `progress.completed_phases: 4` + `progress.percent: 100` + `stopped_at` updated + Session Continuity entry prepended.
|
||||
- PROJECT.md: Validated section updated for Phase 4 hardening closure.
|
||||
- No code or test changes in this plan; `git diff --stat HEAD~1 HEAD` shows only `.planning/` files modified.
|
||||
- All Phase 4 plan SUMMARY files exist (Plans 04-01..04-06; this plan creates its own SUMMARY at completion).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 04-VERIFICATION.md aggregator created (Task 1).
|
||||
- 4 marker files updated for Phase 4 closure (Task 2).
|
||||
- Plans 01-08..01-13 ROADMAP backfill verified (D-P4-05).
|
||||
- v1 milestone status updated: CLOSED-PENDING-ALPHA OR CLOSED.
|
||||
- All Phase 4 success criteria from the ROADMAP cited as GREEN with evidence.
|
||||
- Operator empirical ack from Plan 04-06 cited verbatim in 04-VERIFICATION.md.
|
||||
- Deferred items table carries forward v1.1/v2 items per CONTEXT.
|
||||
- No code/test changes (docs-only ceremony).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-harden-clean-up-optional/04-07-SUMMARY.md` capturing:
|
||||
- 04-VERIFICATION.md created (final line count + section structure)
|
||||
- 4 marker file diffs (REQUIREMENTS + ROADMAP + STATE + PROJECT)
|
||||
- D-P4-05 ROADMAP backfill outcome (rows already present per PATTERNS.md mapping; verified via grep)
|
||||
- v1 milestone status flip (CLOSED-PENDING-ALPHA OR CLOSED — depending on alpha redistribution sequencing)
|
||||
- Closure commit refs
|
||||
- Final tally: UAT 35/35 GREEN; vitest >=185 GREEN; pre-checkpoint bundle gates 6/6 PASS; FORBIDDEN_HOOK_STRINGS at 12.
|
||||
- Next steps for the operator: alpha redistribution + v1.0 tag + release notes (separate workstream per D-P4-04).
|
||||
</output>
|
||||
</content>
|
||||
</invoke>
|
||||
@@ -41,7 +41,22 @@ created: 2026-05-21
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| _Filled by planner during Plans 04-01..08 creation_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | _via planner_ | ⬜ pending |
|
||||
| 04-01 T1 RED 3 tests | 04-01 | 1 | Audit P1 #11/#14/#15 | T-04-01-01..03 | URL extraction + previousUrl + epoch normalization | unit (vitest jsdom) | `npm test -- tests/content/ --run` | ❌ NEW (Wave 0) | ⬜ pending |
|
||||
| 04-01 T2 GREEN edits | 04-01 | 1 | Audit P1 #11/#14/#15 | T-04-01-01..03 | Same; src/content/index.ts edits | unit (vitest) | `npm test -- tests/content/ --run` (+8 GREEN) + `npx tsc --noEmit` | ✗ EXISTS (modify) | ⬜ pending |
|
||||
| 04-02 T1 RED build gates | 04-02 | 1 | SC #4 dead-code + setimmediate hygiene | T-04-02-01/03 | grep gate | unit (vitest + execFile build) | `npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run` | ❌ NEW (Wave 0) | ⬜ pending |
|
||||
| 04-02 T2 GREEN polyfill + rename + flip | 04-02 | 1 | SC #3 generate-icons + setimmediate Q1 | T-04-02-01/02/04 | queueMicrotask polyfill; .cjs rename | build-gate + unit | `npm run build && grep -c 'new Function' dist/assets/index.ts-*.js` -> 0 + `node generate-icons.cjs` exit 0 | ✗ EXISTS (modify + rename) | ⬜ pending |
|
||||
| 04-03 T1 assertA29 rewrite | 04-03 | 2 | A29 flake stabilization | T-04-03-01/02 | cs-injection-world ISOLATED + sentinel | UAT (page-side) | `npx tsc --noEmit && npm run build:test` | ✗ EXISTS (modify) | ⬜ pending |
|
||||
| 04-03 T2 driveA29 strict-sentinel | 04-03 | 2 | A29 sentinel filter | T-04-03-01 | rrweb IncrementalSource.Mutation filter | UAT (host-side) | `HEADLESS=1 SKIP_PROD_REBUILD=1 npm run test:uat` 33/33 GREEN; 5/5 stress | ✗ EXISTS (modify) | ⬜ pending |
|
||||
| 04-04 T1 SPIKE | 04-04 | 3 | SC #1 SW state persistence empirical | T-04-04-01 | offscreen survives SW idle | spike script | `HEADLESS=1 tsx tests/uat/spike-a33-sw-persistence.ts` -> videoSize > 100_000 | ❌ NEW (Wave 0 spike) | ⬜ pending |
|
||||
| 04-04 T2 A33 + stopServiceWorker + orchestrator | 04-04 | 3 | SC #1 5-min idle harness | T-04-04-02/03/04 | CDP worker.close() + 5-min wait + SAVE | UAT | `HEADLESS=1 SKIP_LONG_UAT=1 npm run test:uat` 34/34 GREEN (skip-mode); full-mode 34/34 ~6.5 min | ✗ EXISTS (modify) | ⬜ pending |
|
||||
| 04-05 T1 assertA34 fetch+XHR | 04-05 | 4 | SC #2 fetch+XHR network_error | T-04-05-01 | cs-injection-world dual-trigger | UAT (page-side) | `npx tsc --noEmit && npm run build:test` | ✗ EXISTS (modify) | ⬜ pending |
|
||||
| 04-05 T2 driveA34 + orchestrator | 04-05 | 4 | SC #2 + P1 #11 end-to-end empirical | T-04-05-01 | 2 network_error entries with status===404 | UAT | `HEADLESS=1 SKIP_LONG_UAT=1 npm run test:uat` 35/35 GREEN; full-mode ~7 min | ✗ EXISTS (modify) | ⬜ pending |
|
||||
| 04-06 T1 RED inline-SVG + cursor-pin | 04-06 | 5 | UI-SPEC dark-logo + RESEARCH Finding 4 | T-04-06-01 | DOMParser inline injection (no innerHTML); cursor: 'always' literal | unit (vitest jsdom + build-grep) | `npm test -- tests/welcome/ tests/build/cursor-visibility.test.ts --run` | ❌ NEW (Wave 0) | ⬜ pending |
|
||||
| 04-06 T2 GREEN SVG + welcome.ts + globals | 04-06 | 5 | UI-SPEC stroke recolor + ?raw import | T-04-06-01 | currentColor + DOMParser inline | unit | `npm test -- tests/welcome/inline-svg.test.ts --run` 3/3 GREEN | ✗ EXISTS (modify) | ⬜ pending |
|
||||
| 04-06 T3 A17.8 + 01-07 back-patch | 04-06 | 5 | UI-SPEC harness invariant + docs hygiene | T-04-06-01 | A17.8 raw-source grep | UAT + docs | `HEADLESS=1 SKIP_LONG_UAT=1 npm run test:uat` 35/35 + grep verify | ✗ EXISTS (modify) | ⬜ pending |
|
||||
| 04-06 T4 Operator empirical | 04-06 | 5 | UI-SPEC AC #6 aesthetic judgment | T-04-06-01 | dark-mode visual contrast | manual | operator returns "approved" or describes issue | n/a | ⬜ pending |
|
||||
| 04-07 T1 04-VERIFICATION.md | 04-07 | 6 | Phase 4 closure aggregator | T-04-07-01 | scorecard + override notes + deferred items | docs aggregator | `test -f .planning/phases/04-harden-clean-up-optional/04-VERIFICATION.md && grep -cE '^## '` >= 5 | ❌ NEW | ⬜ pending |
|
||||
| 04-07 T2 Marker flips | 04-07 | 6 | D-P4-05 + ROADMAP/STATE flips | T-04-07-02/03 | Phase 4 [x] + completed_phases: 4 | docs | `grep -c '\[x\] \*\*Phase 4' .planning/ROADMAP.md` >= 1 | ✗ EXISTS (modify) | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
|
||||
Reference in New Issue
Block a user