feat(02): plans 01-04 — Phase 2 export pipeline closure (Blob URL + meta.urls + schema + harness)
Wave structure (4 plans, 3 waves): - 02-01 (Wave 1 RED): 15 RED tests pinning D-P2-01 (blob: URL contract), D-P2-02 (meta.urls schema + dedup + filter), D-P2-03 (strict 8-field validation + schemaVersion '2' cutover marker). - 02-02 (Wave 2): Offscreen-minted Blob URL pipeline — extends PortMessageType with CREATE/REVOKE messages; SW downloadArchive rewrite (data: → blob: via base64-on-wire to offscreen + URL.createObjectURL + chrome.downloads.onChanged revoke lifecycle). Closes audit P0-6; unblocks >2 MB archives. - 02-03 (Wave 2): meta.urls schema migration + tab-url-tracker module (chrome.tabs.onActivated + onUpdated → deduplicated, filtered, first-seen- ordered string[]); SessionMetadata 7→8 fields with schemaVersion + urls; REQUIREMENTS.md REQ-meta-json-schema amendment. Closes P1 #10. - 02-04 (Wave 3): UAT harness A24+A25+A26+A27 — blob: URL prefix, <5s SAVE→zip latency, meta.json 8-field shape, multi-tab dedup; pre-checkpoint bundle gates per saved memory + operator empirical UAT cycle 1. Tier-1 FORBIDDEN_HOOK_STRINGS inventory stays at 12 (no new hook symbols — chrome.* monkey-patches + JSZip + production APIs only). Locked decisions honored (per 02-CONTEXT.md): - D-P2-01: offscreen-minted Blob URL via existing keepalivePort + base64 wire format (reuses D-12 precedent at src/shared/binary.ts). - D-P2-02: meta.json url:string → urls:string[]; URL filter per CONTEXT.md <specifics> (include https://, chrome-extension://; exclude chrome://, about:, devtools://, file://); dedup + first-seen ordering. - D-P2-03: full scope; 8-field strict schema validation with schemaVersion='2' as the 8th field (planner-resolved tentative pick; revisable by plan-checker). Architectural constraints preserved: - Always-on charter (Plan 01-09 Amendment 3): no finally-block in saveArchive; no clearTabUrlsSeen on SAVE. - Tier-1 FORBIDDEN_HOOK_STRINGS = 12 (no new test-hook symbols). - Never await import(...) in src/background/index.ts (Plan 01-11 SUMMARY). - Pre-checkpoint bundle gates per feedback-pre-checkpoint-bundle-gates.md (run in 02-04 Task 4 before operator surface). Plan validation: gsd-sdk frontmatter.validate + verify.plan-structure GREEN for all 4 plans. ROADMAP updated: Phase 2 Plans list + Goal/Success Criteria block annotated with D-P2-02/D-P2-03 amendments + 5th success criterion (Blob URL + revoke lifecycle for >2 MB archives); Progress table 0/TBD → 0/4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
460
.planning/phases/02-stabilize-export-pipeline/02-03-PLAN.md
Normal file
460
.planning/phases/02-stabilize-export-pipeline/02-03-PLAN.md
Normal file
@@ -0,0 +1,460 @@
|
||||
---
|
||||
phase: 02-stabilize-export-pipeline
|
||||
plan: 03
|
||||
type: auto
|
||||
wave: 2
|
||||
depends_on: [01]
|
||||
files_modified:
|
||||
- src/background/tab-url-tracker.ts
|
||||
- src/shared/types.ts
|
||||
- src/background/index.ts
|
||||
- .planning/REQUIREMENTS.md
|
||||
autonomous: true
|
||||
requirements:
|
||||
- REQ-meta-json-schema
|
||||
tags:
|
||||
- meta-json-urls-array
|
||||
- tab-url-tracking
|
||||
- schema-amendment
|
||||
- p1-10-fix
|
||||
- d-p2-02
|
||||
- d-p2-03
|
||||
must_haves:
|
||||
truths:
|
||||
- "src/background/tab-url-tracker.ts exists exporting getTabUrlsSeen(): string[] returning a deduplicated, first-seen-ordered, filtered (no chrome://, no about:; include chrome-extension://) list of tab URLs observed during the rolling 30s window."
|
||||
- "SessionMetadata.url: string is replaced by SessionMetadata.urls: string[] in src/shared/types.ts."
|
||||
- "createArchive in src/background/index.ts assembles meta.json with urls (not url), plus a new schemaVersion: '2' field, totalling exactly 8 fields per D-P2-03."
|
||||
- "Tests tests/background/meta-json-urls-schema.test.ts (4 tests) and tests/build/strict-meta-json-validation.test.ts (8 tests) flip RED→GREEN."
|
||||
- ".planning/REQUIREMENTS.md REQ-meta-json-schema text amended to reflect the new 8-field shape, with the breaking-change cutover documented inline."
|
||||
- "chrome.tabs.onActivated and chrome.tabs.onUpdated listeners registered in src/background/index.ts maintain the tracker's internal Set; pruning happens alongside the video segment ring buffer lifecycle (no separate timer)."
|
||||
- "Always-on charter preserved (Plan 01-09 Amendment 3): SAVE creates a zip with the current tracker state; tracker continues accumulating after SAVE."
|
||||
- "manifest.json permissions: the existing `tabs` permission gap (Plan 01-13 SUMMARY Known Limitations item 3) is NOT closed by this plan — the tracker uses chrome.tabs.onActivated + chrome.tabs.onUpdated which work on `activeTab` permission alone for the active-tab URL events. Cross-window/inactive-tab URL events require `tabs` permission and are deferred to Phase 4 hardening (planner-decision documented inline)."
|
||||
artifacts:
|
||||
- path: "src/background/tab-url-tracker.ts"
|
||||
provides: "getTabUrlsSeen(): string[] + initTabUrlTracker(): void + clearTabUrlsSeen(): void"
|
||||
min_lines: 80
|
||||
contains: "getTabUrlsSeen|chrome\\.tabs\\.onActivated|chrome\\.tabs\\.onUpdated"
|
||||
- path: "src/shared/types.ts"
|
||||
provides: "SessionMetadata.urls: string[] (replaces url: string); new schemaVersion: string field"
|
||||
contains: "urls.*string\\[\\]|schemaVersion"
|
||||
- path: "src/background/index.ts"
|
||||
provides: "createArchive consumes getTabUrlsSeen for meta.urls + initTabUrlTracker at SW init"
|
||||
contains: "getTabUrlsSeen|schemaVersion"
|
||||
- path: ".planning/REQUIREMENTS.md"
|
||||
provides: "REQ-meta-json-schema amended for 8-field shape + breaking-change cutover note"
|
||||
contains: "schemaVersion|urls.*string\\[\\]"
|
||||
key_links:
|
||||
- from: "src/background/index.ts:initialize"
|
||||
to: "src/background/tab-url-tracker.ts:initTabUrlTracker"
|
||||
via: "initTabUrlTracker() called at SW init alongside chrome.runtime.onMessage listener"
|
||||
pattern: "initTabUrlTracker\\(\\)"
|
||||
- from: "src/background/index.ts:createArchive"
|
||||
to: "src/background/tab-url-tracker.ts:getTabUrlsSeen"
|
||||
via: "metadata.urls = getTabUrlsSeen()"
|
||||
pattern: "metadata\\.urls.*=.*getTabUrlsSeen"
|
||||
- from: "src/background/tab-url-tracker.ts"
|
||||
to: "chrome.tabs.onActivated + chrome.tabs.onUpdated"
|
||||
via: "addListener at module init; each event derives the URL via tab.url and inserts into the Set if it passes the filter"
|
||||
pattern: "chrome\\.tabs\\.on(Activated|Updated)\\.addListener"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Replace SessionMetadata.url: string with SessionMetadata.urls: string[] per D-P2-02. The urls array
|
||||
captures all tab URLs observed during the rolling recording window (operator's full multi-tab
|
||||
bug-reproduction context, not just the active-at-save tab). Add a new schemaVersion field per
|
||||
the planner-resolved 8th-field decision from Plan 02-01. Amend REQUIREMENTS.md REQ-meta-json-schema
|
||||
text to reflect the breaking-change cutover.
|
||||
|
||||
Purpose: closes audit P1 #10 + makes the meta.json multi-tab-aware for the operator's typical workflow
|
||||
(switching tabs across the 30s window when reproducing a bug). Maintains the always-on charter
|
||||
(tracker keeps running after SAVE; no reset).
|
||||
|
||||
Output:
|
||||
- New module src/background/tab-url-tracker.ts (~80-120 LOC).
|
||||
- src/shared/types.ts SessionMetadata interface updated.
|
||||
- src/background/index.ts createArchive + initialize updated.
|
||||
- .planning/REQUIREMENTS.md REQ-meta-json-schema text amended.
|
||||
- tests/background/meta-json-urls-schema.test.ts (4) + tests/build/strict-meta-json-validation.test.ts (8) flip RED→GREEN.
|
||||
</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/REQUIREMENTS.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/02-stabilize-export-pipeline/02-CONTEXT.md
|
||||
@.planning/phases/02-stabilize-export-pipeline/02-01-PLAN.md
|
||||
|
||||
# Source code under modification
|
||||
@src/background/index.ts
|
||||
@src/shared/types.ts
|
||||
|
||||
# Tests that flip GREEN under this plan
|
||||
@tests/background/meta-json-urls-schema.test.ts
|
||||
@tests/build/strict-meta-json-validation.test.ts
|
||||
|
||||
<interfaces>
|
||||
<!-- Target shape for the tab-url-tracker module. Embedded so executor needs no codebase exploration. -->
|
||||
|
||||
src/background/tab-url-tracker.ts public API:
|
||||
```typescript
|
||||
/**
|
||||
* Initialize the tab-URL tracker. Registers chrome.tabs.onActivated +
|
||||
* chrome.tabs.onUpdated listeners that maintain an internal Set of URLs
|
||||
* observed during the SW's lifetime. Must be called once at SW init.
|
||||
*
|
||||
* Idempotent — calling twice is safe (defensive try/catch around listener
|
||||
* registration matches the pattern in src/background/index.ts:bootstrap).
|
||||
*
|
||||
* D-P2-02 binding: captures the operator's multi-tab context, not just
|
||||
* the active-at-save tab.
|
||||
*/
|
||||
export function initTabUrlTracker(): void;
|
||||
|
||||
/**
|
||||
* Return the deduplicated, first-seen-ordered, filtered list of tab URLs
|
||||
* observed since init OR since the last clearTabUrlsSeen() call.
|
||||
*
|
||||
* Filter (per CONTEXT.md `<specifics>` block):
|
||||
* - INCLUDE: https://, http://, chrome-extension://
|
||||
* - EXCLUDE: chrome://, about:, devtools://, file:// (low diagnostic value
|
||||
* OR privacy-concerning local-fs paths)
|
||||
*
|
||||
* Dedup: each URL appears exactly once.
|
||||
* Order: first-seen-first.
|
||||
*
|
||||
* Always returns a NEW array (copy) — caller cannot mutate the internal Set.
|
||||
*/
|
||||
export function getTabUrlsSeen(): string[];
|
||||
|
||||
/**
|
||||
* Clear the internal Set. NOT called by saveArchive (always-on charter
|
||||
* preserved — SAVE creates a zip with the current state; tracker keeps
|
||||
* accumulating). Reserved for future use (e.g., manual session-reset).
|
||||
*/
|
||||
export function clearTabUrlsSeen(): void;
|
||||
```
|
||||
|
||||
Updated SessionMetadata (src/shared/types.ts):
|
||||
```typescript
|
||||
export interface SessionMetadata {
|
||||
schemaVersion: string; // NEW — '2' marks the D-P2-02 url→urls cutover
|
||||
timestamp: string; // ISO-8601 with Z
|
||||
urls: string[]; // NEW — replaces `url: string`; non-empty per D-P2-03
|
||||
userAgent: string;
|
||||
extensionVersion: string;
|
||||
videoBufferSeconds: number;
|
||||
logDurationMinutes: number;
|
||||
totalEvents: number;
|
||||
}
|
||||
```
|
||||
|
||||
Existing createArchive call site (src/background/index.ts:673-684):
|
||||
```typescript
|
||||
const metadata: SessionMetadata = {
|
||||
timestamp: new Date().toISOString(),
|
||||
url: new URL(chrome.runtime.getURL('')).origin, // ← REPLACE
|
||||
userAgent: navigator.userAgent,
|
||||
extensionVersion: manifest.version,
|
||||
videoBufferSeconds: 30,
|
||||
logDurationMinutes: 10,
|
||||
totalEvents: rrwebEvents.length + userEvents.length
|
||||
};
|
||||
```
|
||||
|
||||
REQ-meta-json-schema (REQUIREMENTS.md:106-119) — verbatim text to amend:
|
||||
```
|
||||
- [ ] **REQ-meta-json-schema**: `meta.json` inside the archive conforms to the
|
||||
verbatim schema:
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-05-15T14:32:10Z",
|
||||
"url": "https://...", ← REPLACE WITH urls: []
|
||||
"userAgent": "Chrome/...",
|
||||
"extensionVersion": "1.0.0",
|
||||
"videoBufferSeconds": 30,
|
||||
"logDurationMinutes": 10,
|
||||
"totalEvents": 143
|
||||
}
|
||||
```
|
||||
All fields required. Binding: CON-meta-json-schema.
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Create src/background/tab-url-tracker.ts module</name>
|
||||
<files>src/background/tab-url-tracker.ts</files>
|
||||
<behavior>
|
||||
- Module exports initTabUrlTracker, getTabUrlsSeen, clearTabUrlsSeen per the interface block.
|
||||
- Internal state: `let tabUrlsSeen: Set<string> = new Set()` AND `let firstSeenOrder: string[] = []` (Set for O(1) dedup; array for ordering).
|
||||
- On chrome.tabs.onActivated: query the activated tab via chrome.tabs.get(activeInfo.tabId); if tab.url passes filter, addUrl(tab.url).
|
||||
- On chrome.tabs.onUpdated: when changeInfo.url is present AND passes filter, addUrl(changeInfo.url). (Filters out the cascade of intermediate events that fire during page load — `changeInfo.status === 'complete'` is NOT required because we want to capture URL transitions even for SPA-style routing).
|
||||
- addUrl helper: if URL not in Set, push to firstSeenOrder array AND add to Set; idempotent.
|
||||
- Filter helper: accept https://, http://, chrome-extension://; reject chrome://, about:, devtools://, file://, blob:, data:.
|
||||
- Defense-in-depth: all chrome.* listener registrations wrapped in try/catch (matches src/background/index.ts:bootstrap pattern for chrome.notifications + chrome.action listeners).
|
||||
- getTabUrlsSeen returns a slice (copy) of firstSeenOrder — caller cannot mutate internal state.
|
||||
- clearTabUrlsSeen empties both the Set and the array.
|
||||
- module-init guard: `let initialized = false`; initTabUrlTracker sets to true; subsequent calls return early with a logger.warn for idempotency.
|
||||
</behavior>
|
||||
<action>
|
||||
Create `src/background/tab-url-tracker.ts` with the exact public API from the interfaces block.
|
||||
|
||||
Use the Logger pattern from src/shared/logger.ts (existing pattern at recorder.ts:78 / index.ts:49):
|
||||
```typescript
|
||||
import { Logger } from '../shared/logger';
|
||||
const logger = new Logger('TabUrlTracker');
|
||||
```
|
||||
|
||||
The filter implementation:
|
||||
```typescript
|
||||
function passesFilter(url: string): boolean {
|
||||
// Per .planning/phases/02-stabilize-export-pipeline/02-CONTEXT.md <specifics>:
|
||||
// INCLUDE: https://, http://, chrome-extension://
|
||||
// EXCLUDE: chrome://, about:, devtools://, file://, blob:, data:
|
||||
return /^(https?|chrome-extension):\/\//.test(url);
|
||||
}
|
||||
```
|
||||
|
||||
Module structure (top → bottom):
|
||||
1. Logger import + instantiation.
|
||||
2. State: `let tabUrlsSeen: Set<string>`, `let firstSeenOrder: string[]`, `let initialized: boolean`.
|
||||
3. passesFilter helper.
|
||||
4. addUrl helper (internal).
|
||||
5. initTabUrlTracker (public) — registers listeners with defensive try/catch.
|
||||
6. getTabUrlsSeen (public).
|
||||
7. clearTabUrlsSeen (public).
|
||||
|
||||
Per D-P2-02 + CONTEXT.md `<specifics>`: the operator's primary tab at SAVE time should always
|
||||
be in the array if it has a valid URL. The chrome.tabs.onActivated path covers this — the active
|
||||
tab fires onActivated whenever the operator switches to it (including the first activation when
|
||||
the SW starts up). Combined with chrome.tabs.onUpdated, this captures both tab-switch events
|
||||
and in-tab navigation events. No explicit "snapshot active tab at SAVE time" is needed (the
|
||||
tracker has already captured it via onActivated).
|
||||
|
||||
NOTE on `tabs` permission gap: chrome.tabs.onActivated provides tabId in the event payload;
|
||||
chrome.tabs.get(tabId) returns the tab WITHOUT the `.url` field unless `tabs` permission is
|
||||
granted (Plan 01-13 SUMMARY Known Limitations item 3). For the active tab specifically,
|
||||
`activeTab` permission grants temporary URL access. The tracker accepts the URL when present
|
||||
and skips silently when absent (logged at warn level for diagnostics). This means inactive-tab
|
||||
URL changes are NOT captured — acceptable per planner-decision deferred to Phase 4 hardening
|
||||
(CONTEXT.md `<deferred>` "tabs permission gap"). Document this limitation inline.
|
||||
|
||||
Per D-P2-03's "urls array non-empty" rule from Plan 02-01 Task 3: if getTabUrlsSeen returns
|
||||
an empty array at SAVE time, createArchive (Task 3 below) must fall back to a sentinel value
|
||||
(e.g., the extension-origin URL from `new URL(chrome.runtime.getURL('')).origin` — same as the
|
||||
current single-url path). This guarantees the strict 8-field schema validation never fails on
|
||||
empty urls[].
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<done>
|
||||
File exists. tsc clean. Three exports per public API. Defensive try/catch wraps chrome.tabs.*
|
||||
listener registrations. Atomic commit:
|
||||
`feat(02-03): tab-url-tracker — chrome.tabs.onActivated + onUpdated → urls[] with dedup + filter (D-P2-02)`.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Update SessionMetadata type + meta.json assembly + tracker wiring</name>
|
||||
<files>src/shared/types.ts, src/background/index.ts</files>
|
||||
<behavior>
|
||||
- src/shared/types.ts SessionMetadata: REPLACE `url: string` with `urls: string[]`; ADD `schemaVersion: string` as the first field. Total 8 fields.
|
||||
- src/background/index.ts:
|
||||
(a) import { initTabUrlTracker, getTabUrlsSeen } from './tab-url-tracker';
|
||||
(b) call initTabUrlTracker() inside the existing `initialize()` function (line 957) under defensive try/catch matching the chrome.notifications.onClicked pattern.
|
||||
(c) createArchive (line 673-684) updated:
|
||||
```typescript
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const tabUrls = getTabUrlsSeen();
|
||||
// D-P2-03 non-empty guarantee: if the tracker has no URLs (e.g., SW just spawned,
|
||||
// operator hasn't activated a real tab yet), fall back to the extension-origin URL
|
||||
// so the strict-validation schema (REQ-meta-json-schema) never fails on empty array.
|
||||
const urls = tabUrls.length > 0 ? tabUrls : [new URL(chrome.runtime.getURL('')).origin];
|
||||
const metadata: SessionMetadata = {
|
||||
schemaVersion: '2',
|
||||
timestamp: new Date().toISOString(),
|
||||
urls,
|
||||
userAgent: navigator.userAgent,
|
||||
extensionVersion: manifest.version,
|
||||
videoBufferSeconds: 30,
|
||||
logDurationMinutes: 10,
|
||||
totalEvents: rrwebEvents.length + userEvents.length
|
||||
};
|
||||
```
|
||||
- Object key ordering matches the SessionMetadata interface declaration order (TypeScript object
|
||||
literal preserves source order; meta.json output is JSON.stringify(metadata, null, 2) → keys
|
||||
in insertion order per ECMA-262).
|
||||
- Always-on charter preserved: createArchive does NOT call clearTabUrlsSeen() — the tracker
|
||||
continues accumulating across saves (next save captures any tabs activated after this one).
|
||||
</behavior>
|
||||
<action>
|
||||
1. Edit `src/shared/types.ts` SessionMetadata interface (lines 102-111):
|
||||
- DELETE `url: string;`.
|
||||
- INSERT `schemaVersion: string;` as the first field.
|
||||
- INSERT `urls: string[];` in the position previously occupied by `url`.
|
||||
- Final ordering: schemaVersion, timestamp, urls, userAgent, extensionVersion,
|
||||
videoBufferSeconds, logDurationMinutes, totalEvents (8 fields).
|
||||
- Add a docstring block above the interface citing D-P2-02 (the url→urls cutover) +
|
||||
D-P2-03 (schemaVersion + 8-field exact rule) + the planner-resolved Plan 02-01 Task 3
|
||||
decision to make schemaVersion the 8th field.
|
||||
|
||||
2. Edit `src/background/index.ts`:
|
||||
- Add import: `import { initTabUrlTracker, getTabUrlsSeen } from './tab-url-tracker';` near
|
||||
the existing import block (line 1-11).
|
||||
- In `initialize()` (line 957), add a defensive try/catch block calling initTabUrlTracker()
|
||||
right after the existing chrome.notifications.onClicked listener registration (search for
|
||||
that pattern; mirror the surrounding try/catch shape).
|
||||
- Replace the metadata-construction block in createArchive (line 673-684) with the new
|
||||
8-field shape per the behavior block above.
|
||||
|
||||
3. Per D-P2-02 + D-P2-03:
|
||||
- schemaVersion: '2' marks the breaking-change cutover. Future schema bumps increment.
|
||||
- urls is the operator's multi-tab context.
|
||||
- The non-empty fallback (extension-origin URL) guarantees REQ-meta-json-schema strict-
|
||||
validation passes even on a freshly-spawned SW with no tab activity.
|
||||
|
||||
Per the planner-resolved decision in Plan 02-01 Task 3: the 8th field name `schemaVersion`
|
||||
was a tentative planner pick. If plan-checker / implementer surfaces a stronger candidate
|
||||
name, BOTH this plan AND tests/build/strict-meta-json-validation.test.ts EXPECTED_KEYS must
|
||||
change in lockstep (matching the Plan 01-14 lockstep-change pattern for FORBIDDEN_HOOK_STRINGS).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx tsc --noEmit 2>&1 | head -20 ; npm run build 2>&1 | tail -10 ; npx vitest run tests/background/meta-json-urls-schema.test.ts tests/build/strict-meta-json-validation.test.ts 2>&1 | tail -30</automated>
|
||||
</verify>
|
||||
<done>
|
||||
tsc clean. npm run build clean. SessionMetadata has exactly 8 fields. createArchive emits
|
||||
8-field meta.json with schemaVersion='2' + urls (non-empty via fallback). All 4 tests in
|
||||
meta-json-urls-schema.test.ts GREEN. All 8 tests in strict-meta-json-validation.test.ts GREEN.
|
||||
Atomic commit:
|
||||
`feat(02-03): meta.json — urls[] + schemaVersion (D-P2-02 + D-P2-03; replaces url:string)`.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Amend REQUIREMENTS.md REQ-meta-json-schema for the new 8-field shape</name>
|
||||
<files>.planning/REQUIREMENTS.md</files>
|
||||
<behavior>
|
||||
- REQ-meta-json-schema entry (lines 106-119) updated to reflect the new 8-field schema verbatim.
|
||||
- Inline comment block citing D-P2-02 + D-P2-03 + Plan 02-03 as the cutover provenance.
|
||||
- Acceptance criteria preserved: all fields required; types correct; timestamp ISO-8601 with Z.
|
||||
- Add new acceptance criteria: urls is non-empty string[]; schemaVersion === '2'.
|
||||
- Traceability table entry for REQ-meta-json-schema status update: "Pending → Complete pending Plan 02-04 harness validation" (Plan 02-04 may flip this to Complete; this plan only ships the implementation + amended text).
|
||||
</behavior>
|
||||
<action>
|
||||
Edit `.planning/REQUIREMENTS.md`:
|
||||
|
||||
1. Replace lines 106-119 (REQ-meta-json-schema block) with the new 8-field schema:
|
||||
|
||||
```markdown
|
||||
- [ ] **REQ-meta-json-schema**: `meta.json` inside the archive conforms to the
|
||||
verbatim schema (D-P2-02 + D-P2-03 cutover; replaces the 7-field `url:string`
|
||||
shape per audit P1 #10 amendment 2026-05-20):
|
||||
```json
|
||||
{
|
||||
"schemaVersion": "2",
|
||||
"timestamp": "2025-05-15T14:32:10Z",
|
||||
"urls": ["https://example.com/", "https://app.example.com/dashboard"],
|
||||
"userAgent": "Chrome/...",
|
||||
"extensionVersion": "1.0.0",
|
||||
"videoBufferSeconds": 30,
|
||||
"logDurationMinutes": 10,
|
||||
"totalEvents": 143
|
||||
}
|
||||
```
|
||||
All 8 fields required. Acceptance:
|
||||
- `schemaVersion === '2'` (marks the D-P2-02 url→urls cutover; future schema bumps increment)
|
||||
- `timestamp` ISO-8601 with `Z` suffix
|
||||
- `urls` is a non-empty `string[]` of URLs matching `/^(https?|chrome-extension):\/\//` (per CONTEXT.md `<specifics>` filter rules — exclude chrome://, about:, devtools://, file://)
|
||||
- `urls` is deduplicated; ordering is first-seen-first across the rolling recording window
|
||||
- `extensionVersion` matches semver
|
||||
- `totalEvents` is a non-negative integer
|
||||
- exactly 8 keys; no extras
|
||||
Binding: CON-meta-json-schema (this REQ-text supersedes the original CON-meta-json-schema 7-field shape).
|
||||
```
|
||||
|
||||
2. Update the Traceability table entry (line 225) for REQ-meta-json-schema:
|
||||
FROM: `| REQ-meta-json-schema | Phase 3 (originally) → **Phase 2** (renumbered) | Pending |`
|
||||
TO: `| REQ-meta-json-schema | Phase 2 | Pending (implementation landed via Plan 02-03; harness validation deferred to Plan 02-04) |`
|
||||
|
||||
3. Append a `*Updated YYYY-MM-DD*` footer line at the bottom of the file matching the existing
|
||||
footer pattern (line 249 area). Date: 2026-05-20.
|
||||
|
||||
Per D-P2-02 + D-P2-03: this is the canonical schema-amendment Phase 2 ships. The 8-field shape
|
||||
becomes the new baseline for all downstream consumers (UAT harness, future v2 SRV-* uploads,
|
||||
operator post-mortem tooling).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -c "schemaVersion" /home/parf/projects/work/repremium/.planning/REQUIREMENTS.md ; grep -c "urls.*string\[\]" /home/parf/projects/work/repremium/.planning/REQUIREMENTS.md</automated>
|
||||
</verify>
|
||||
<done>
|
||||
REQUIREMENTS.md REQ-meta-json-schema block reflects 8-field shape. Traceability table updated.
|
||||
Footer line appended with 2026-05-20 date. Atomic commit:
|
||||
`docs(02-03): REQUIREMENTS — REQ-meta-json-schema amended for 8-field shape with urls[] + schemaVersion`.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| tab URL strings → meta.json | Tab URLs may contain sensitive operator state (per CONTEXT.md `<decisions>` D-P2-02 "Privacy note"); the "log is internal" charter accepts this. |
|
||||
| chrome.tabs.* event source | Chrome platform → SW; trustworthy per MV3 contract. |
|
||||
| tab-url-tracker internal Set → getTabUrlsSeen consumer | Module returns a COPY (slice); caller cannot mutate internal state. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-02-03-01 | Information Disclosure | URLs with embedded credentials (https://user:pass@host/) leak into meta.json | accept | Charter shift 2026-05-20 ("log is internal") explicitly accepts this. REQ-password-confidentiality moved to Out of Scope v1. No mitigation in v1; v2/Phase 4 candidate. |
|
||||
| T-02-03-02 | Information Disclosure | chrome-extension://<id>/ URLs reveal extension presence | accept | The extension is installed unpacked locally; presence is already self-evident from chrome://extensions. INCLUDE per CONTEXT.md `<specifics>` filter. |
|
||||
| T-02-03-03 | Tampering | Malicious extension overrides chrome.tabs.* events | mitigate | Extension permissions are user-granted at install time. Defensive try/catch wraps every chrome.* call so malformed events don't crash the SW. |
|
||||
| T-02-03-04 | Denial of Service | Unbounded tab-url Set growth in long-running SW | mitigate | URL set is bounded by O(unique tabs operator visits per SW lifetime). Operationally ≤500 even in heavy use. No pruning needed within v1 budget; SW idle teardown (~30s) clears state. Phase 4 hardening can add pruning if production telemetry surfaces unbounded growth. |
|
||||
| T-02-03-05 | Repudiation | Tracker silently drops URLs when `tabs` permission absent (chrome.tabs.get returns undefined .url) | mitigate | Diagnostic logger.warn at every dropped event so post-mortem investigation can identify the permission gap. Phase 4 hardening adds `tabs` permission per CONTEXT.md `<deferred>` item. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `npx tsc --noEmit` → clean.
|
||||
- `npm run build` → clean.
|
||||
- `npx vitest run tests/background/meta-json-urls-schema.test.ts` → 4/4 GREEN.
|
||||
- `npx vitest run tests/build/strict-meta-json-validation.test.ts` → 8/8 GREEN.
|
||||
- `npx vitest run` (full suite) → previously RED tests from Plan 02-01 all GREEN. Net: 153 + 15 = 168 GREEN (or whatever the exact count is post-Plan-02-01 + Plan-02-02).
|
||||
- `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts` → 12 FORBIDDEN_HOOK_STRINGS unchanged → GREEN.
|
||||
- `npm run test:uat` → 24/24 GREEN preserved (no harness changes in this plan; A13's meta.json shape check
|
||||
may detect the schemaVersion + urls fields — verify A13 still GREEN; if it depends on the old `url` field, Plan 02-04 addresses).
|
||||
- `grep -c "schemaVersion" .planning/REQUIREMENTS.md` ≥ 2 (in REQ block + acceptance bullet).
|
||||
- `grep -c "url: string" src/shared/types.ts` → 0 (the legacy field is gone).
|
||||
- Always-on charter check: `grep -n "clearTabUrlsSeen" src/background/index.ts` → 0 hits (createArchive does NOT clear; tracker keeps accumulating after SAVE).
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
1. tests/background/meta-json-urls-schema.test.ts 4/4 GREEN.
|
||||
2. tests/build/strict-meta-json-validation.test.ts 8/8 GREEN.
|
||||
3. UAT harness 24/24 GREEN preserved (or A13 amended in lockstep if its meta.json assertions depend on the old `url` field — verify before commit).
|
||||
4. .planning/REQUIREMENTS.md REQ-meta-json-schema reflects the 8-field shape with breaking-change cutover documented.
|
||||
5. Always-on charter preserved: tab-url-tracker continues accumulating after SAVE; no createArchive-time clearTabUrlsSeen call.
|
||||
6. Tier-1 FORBIDDEN_HOOK_STRINGS = 12 (unchanged).
|
||||
7. tabs permission gap NOT closed by this plan — explicit deferral comment in tab-url-tracker.ts citing CONTEXT.md `<deferred>`.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-stabilize-export-pipeline/02-03-SUMMARY.md`
|
||||
documenting:
|
||||
- New module src/background/tab-url-tracker.ts (LOC + public API summary)
|
||||
- SessionMetadata field-count delta (7 → 8) + ordering rationale
|
||||
- 8th field `schemaVersion` decision: planner-suggested in Plan 02-01 Task 3, ratified here
|
||||
- Filter rules verbatim from CONTEXT.md `<specifics>`
|
||||
- Tabs permission gap deferral citation (CONTEXT.md `<deferred>`)
|
||||
- Forward link: Plan 02-04 may amend UAT harness A13 if its meta.json assertions assumed the old `url` field
|
||||
</output>
|
||||
Reference in New Issue
Block a user