Commit Graph

215 Commits

Author SHA1 Message Date
468f16d7e7 feat(01-12): wave-4 task-1 — adopt tokens.css + chrome.i18n.getMessage in src/popup/ + src/background/ (loom palette + RU i18n + en fallback)
src/popup/style.css:
- Adds @import "../shared/tokens.css" at top
- All hex literals removed; every color reads from var(--mks-*) per
  D-04 loom palette: --mks-surface body bg; --mks-rec/--mks-madder-700
  for SAVE button (default/hover); --mks-amber-600 for saving;
  --mks-moss-600 for done; --mks-error/--mks-success/--mks-warning for
  status messages; --mks-fg-disabled for disabled button
- Font families read from --mks-font-ui (IBM Plex Sans stack)
- Spacing/radius/shadows all token-driven

src/popup/index.html:
- <span class="button-text"> emptied (populated by JS via i18n)
- <p class="info-text" data-mks-key="popupInfoText"> attribute-marked
  for populateMksKeys() init-time population
- <title> kept as literal English (chrome doesn't substitute __MSG_*__
  in HTML body per RESEARCH Pitfall 3)

src/popup/index.ts:
- New `i18n(key, fallback)` helper: chrome.i18n.getMessage with explicit
  `|| <fallback>` for unit-test contexts without chrome.i18n stub
- New `populateMksKeys()` helper: walks [data-mks-key] elements at init
  and sets each textContent from i18n
- updateUI() reads popupSaveCta/popupSaving/popupSaveDoneShort at each
  state branch (idle/saving/done) with Russian fallbacks
- saveArchive() success branch reads popupSaveDone
- Empty-state path reads popupEmptyState

src/background/index.ts:
- BADGE_REC_COLOR: '#00C853' → '#b2543d' (= --mks-madder-600 per D-04;
  RESEARCH §10 Open Question A7 default-action)
- BADGE_OFF_COLOR + BADGE_ERROR_COLOR retained as engineering choices
  (no loom-palette token for material-red/amber-700 equivalents)
- BADGE_REC_TITLE/BADGE_OFF_TITLE/BADGE_ERROR_TITLE renamed to
  ..._FALLBACK and only referenced at the chrome.i18n.getMessage call
  sites inside setBadgeState (i18nMessage('tooltipRecPrefix' etc.))
- New `i18nMessage(key, fallback)` helper mirroring popup's i18n()
- Recovery notification: title=i18nMessage('extName',...); message=
  i18nMessage('notifRecovery',...)
- Startup notification: title=i18nMessage('extName',...); message=
  i18nMessage('notifStartup',...)
- NOTIF_EXTNAME_FALLBACK/NOTIF_STARTUP_FALLBACK/NOTIF_RECOVERY_FALLBACK
  module-level constants for the |||| chain (degrade gracefully in
  test contexts without chrome.i18n stub)
- NO `await import(...)` added (MV3 SW dynamic-import constraint per
  01-11-SUMMARY preserved)

Test-contract updates (3 tests; assertion-shape only — no semantic
regression):
- tests/background/badge-state-machine.test.ts: greenCalls→recColorCalls
  regex updated from /^#00[Cc]853$/ to /^#b2543d$/i lockstep with
  BADGE_REC_COLOR change; title-substring assertion widened to
  /Recording|recording/i to cover both EN locale + fallback
- tests/background/onstartup-notification.test.ts: title equality
  ('Mokosh ready') replaced with /Mokosh/i substring assertion
  (survives both the 'Mokosh' fallback + 'Mokosh — Session Capture'
  resolved EN); message regex widened to /recording|recor|click/i
- tests/background/toolbar-action.test.ts: DocumentStub gains
  querySelectorAll: () => [] so the new populateMksKeys() init path
  doesn't throw under the popup's no-DOM unit-test environment

Verification:
- tests/build/tokens-adopted.test.ts: 4/4 GREEN (was 2 RED + 2 GREEN)
- tests/build/no-remote-fonts.test.ts: 4/4 GREEN after fresh build
  (Vite emits the WOFF2 files as content-hashed dist/assets/*.woff2;
  tokens.css references resolve through the asset pipeline; no
  remote-font URLs anywhere in dist/)
- Full vitest sweep: 147/147 GREEN (was 145/147)
- npx tsc --noEmit: clean
- Tier-1 grep gate: 13/13 GREEN (no new test-mode symbols)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:27:19 +02:00
110cebc50d feat(01-12): wave-3 task-1 — manifest i18n (__MSG_*__ + default_locale='en') + _locales/{en,ru}/messages.json (16 keys; D-07 + D-08 baked in)
manifest.json migrated to chrome i18n placeholders:
- name: 'AI Call Recorder' → '__MSG_extName__'
- description: 'Запись сессий операторов для диагностики ошибок' → '__MSG_extDesc__'
- default_locale: 'en' (new field per RESEARCH §11 + Pitfall 4 fallback chain)
- action.default_title: '__MSG_tooltipOff__' (new field per Brief §02 string #1)

_locales/en/messages.json + _locales/ru/messages.json each carry the
same 16-key matrix per RESEARCH §10 + Brief §02 verbatim + D-07 user
override + D-08 tagline:

  extName, extDesc, tooltipOff, tooltipRecPrefix, tooltipErr,
  popupSavePrompt, popupSaveCta, popupSaveDone, popupSaving,
  popupSaveDoneShort, popupEmptyState, popupInfoText,
  notifStartup, notifRecovery, welcomeHeroRu, welcomeHeroEn

Canonical values (per brand-decisions-v1.md + RESEARCH §10 Brief §02):
- EN extName = 'Mokosh — Session Capture' (D-07 user override of A)
- EN extDesc = 'Thirty seconds ago, always at hand.' (D-08 tagline)
- RU extName = 'Mokosh — Запись сессии'
- RU extDesc = 'Тридцать секунд назад, всегда под рукой.'

NB on key count: the artifact-table 12-key baseline excluded the three
Wave-4 deltas (popupSaving, popupSaveDoneShort, popupEmptyState) which
are introduced now to avoid a re-locale-parity flap when Wave 4 lands.
popupInfoText is present in BOTH locales (the plan-checker flag 1
informational scope-slip is corrected here — see deviation block below).
Plan success-criteria reading: ≥12 keys with Wave 4 additive deltas
accounted for.

Each key carries both `message` and `description` per Chrome i18n
schema. EN descriptions are translator-facing; RU descriptions are
plain-Russian context for native operators.

npm run build emits:
- dist/manifest.json carries the i18n shape verbatim (crxjs preserves
  __MSG_* placeholders; Chrome's manifest loader resolves them at install)
- dist/_locales/en/messages.json (3.30 KB)
- dist/_locales/ru/messages.json (3.81 KB)

Verification:
- tests/i18n/manifest-i18n.test.ts: 10/10 GREEN
- tests/i18n/locale-parity.test.ts: 4/4 GREEN (en↔ru parity, non-empty
  messages, 16 keys each)
- tests/build/no-remote-fonts.test.ts: 4/4 GREEN (post-build dist/ has
  zero remote font URLs)
- Full vitest sweep: 145/147 GREEN (2 RED remaining are popup/style.css
  tokens-adopted cases — Wave 4 work)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:33:42 +02:00
7732a302cd feat(01-12): wave-2 task-1 — rasterize Loom mark to icons/icon{16,48,128}.png (overwrites Bug A placeholders)
scripts/rasterize-icons.sh (80 lines) is the one-off rasterization
recipe. It reads src/shared/brand/mokosh-mark.svg and emits the three
toolbar icon sizes via rsvg-convert, with assets-spec.md size FLOOR
sanity checks embedded.

Before:
  icons/icon16.png  574 B  16-bit/color RGB     (Bug A placeholder)
  icons/icon48.png  1153 B 16-bit/color RGB     (Bug A placeholder)
  icons/icon128.png 2615 B 16-bit/color RGB     (Bug A placeholder)

After:
  icons/icon16.png  406 B  8-bit/color RGBA     (Loom mark — D-01)
  icons/icon48.png  784 B  8-bit/color RGBA     (Loom mark — D-01)
  icons/icon128.png 1952 B 8-bit/color RGBA     (Loom mark — D-01)

All three clear assets-spec.md Chrome imageUtil silent-rejection floors
(16≥200B, 48≥500B, 128≥1024B). Sizes match RESEARCH §3 verification.

Per D-06 (Neutral mark + dynamic badge): single neutral mark per size;
no per-state PNG sets. The dynamic chrome.action.setBadgeBackgroundColor
in src/background/index.ts does state communication via colored badge.

Per RESEARCH §3 anti-pattern: PNGs are COMMITTED as static artifacts;
not regenerated at build time. scripts/rasterize-icons.sh is the
documented re-run recipe (for when src/shared/brand/mokosh-mark.svg
changes).

Verification:
- tests/build/icons-present.test.ts: 15/15 GREEN (existence, FLOOR,
  PNG signature, dimensions, color-type byte === 6 RGBA)
- Bug A regression check: file content differs from prior placeholders
  (verified via git diff binary status); the placeholder fingerprint
  used by harness A19 (Wave 6) will distinguish on first 32 bytes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:30:23 +02:00
abab6e1f59 feat(01-12): wave-1 task-2 — canonical src/shared/tokens.css (R2 Lora substitution + .mks-word + local @font-face block)
src/shared/tokens.css lands as the canonical token system — engineering
working copy of .planning/intel/design-incoming/system/bundle/mokosh-handoff/
tokens.css with three surgical edits per Plan 01-12:

1. Handoff's PREVIEW-ONLY Google Fonts @import (line 12) REMOVED +
   replaced with 8 local @font-face rules pointing at ./fonts/*.woff2
   (Lora normal + italic variable; Plex Sans Regular/Medium/SemiBold/Bold;
   Plex Mono Regular/Medium). MV3 CSP self-host enforced
   (style-src 'self' + font-src 'self').

2. --mks-font-display VALUE substituted from "Newsreader" to "Lora" per
   R2 designer reply 2026-05-19 (Cyrillic coverage; brand-decisions-v1-
   followup-display-font.md). The Lora foundry note is preserved in the
   Type-section comment. ZERO Newsreader references remain anywhere in
   the file (verified by grep).

3. .mks-word class added at end-of-file with the {font-family,font-size,
   font-weight,letter-spacing,fill} declarations from the lockup SVG's
   class="mks-word" usage. Required by mokosh-lockup.svg line 21 per
   RESEARCH §8.

src/shared/brand/ engineering working copies of:
- mokosh-mark.svg (Loom 2×2 weave intersection at 32×32 viewBox)
- mokosh-lockup.svg (mark + Mokosh wordmark at 240×56 viewBox)

The intel/ design-incoming/ copies remain unchanged as the original
handoff source-of-truth.

Verification:
- googleapis count: 0 (MV3 CSP self-host invariant)
- Newsreader count: 0 (R2 substitution complete)
- @font-face count: 8 (Lora normal + italic + Plex Sans ×4 + Plex Mono ×2)
- .mks-word: 1 definition (referenced by mokosh-lockup.svg)
- Lora references: 13 (font-family stack + @font-face + comments)
- tests/build/fonts-present.test.ts: 10/10 GREEN
- tests/build/tokens-adopted.test.ts case (a): GREEN (tokens.css exists)
- tests/build/tokens-adopted.test.ts cases (b)(c): still RED (Wave 4 work)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:29:37 +02:00
f86fd60d4a feat(01-12): wave-1 task-1 — self-host OFL font bundle (Lora + Plex Sans + Plex Mono; R2 designer reply 2026-05-19)
Self-hosted WOFF2 bundle lands at src/shared/fonts/ per D-05 typography
pairing with R2 Newsreader→Lora substitution (designer reply 2026-05-19).

Bundle composition (8 WOFF2 files; ~236 KB total):
- Lora-VariableFont.woff2 (49 KB) — display family, normal style, wght
  axis 400-700; Cyreal foundry (cyrealtype/Lora-Cyrillic main branch)
- Lora-Italic-VariableFont.woff2 (53 KB) — display family italic, wght
  400-700; separate variable file per upstream layout (A5 verified at
  execute time: Lora-Cyrillic ships italic as its own variable file).
- IBMPlexSans-{Regular,Medium,SemiBold,Bold}.woff2 (24/25/25/23 KB) —
  UI body family with Latin + Cyrillic basic
- IBMPlexMono-{Regular,Medium}.woff2 (15 KB each) — diagnostic / timer
  family

Companion artifacts:
- LICENSE-Lora.txt — verbatim OFL.txt + Lora Project Authors copyright
- LICENSE-IBM-Plex.txt — verbatim LICENSE.txt + IBM Corp. copyright
- README.md — substantive (160 lines): bundle table, R2 rationale,
  subset coverage (Cyrillic basic + supplements + №), regeneration recipe
  with one-off curl commands, MV3 CSP self-host rationale.

scripts/subset-fonts.sh (130 lines):
- One-off subsetting recipe; takes a scratch dir of upstream TTFs.
- UNICODES range: U+0020-007E + U+00A0-00FF + Cyrillic basic
  (U+0400-045F) + Ukrainian (Ґґ) + Kazakh (Ұұ) + № sign.
- Common pyftsubset flags shared across faces; per-face subset_face
  helper. Documents source URLs in usage block.

Bundle is sufficient for the 12 i18n keys (Wave 3) + welcome hero
(Plan 01-10 conditional) per the Brief §02 Russian copy specified in
.planning/intel/brand-decisions-v1.md.

Verification: tests/build/fonts-present.test.ts is 9/10 GREEN (1 RED
remaining is the tokens.css existence check, which is Wave 1 Task 2's
job). Existing 100/100 vitest baseline preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:13:50 +02:00
34a9ce10d4 test(01-12): wave-0 — scaffold RED unit tests (tokens / fonts / icons / no-remote-fonts / manifest-i18n / locale-parity)
Wave 0 of the design-integration plan. Six new test files at tests/build/
and tests/i18n/ pin the contracts that later waves will GREEN:

- tokens-adopted.test.ts (4 cases): src/shared/tokens.css exists +
  parses; src/popup/style.css @imports it; popup/style.css has zero
  hex literals; welcome.css conditional check.
- fonts-present.test.ts: 7 required WOFF2 faces (Lora normal + Plex Sans
  ×4 + Plex Mono ×2) + LICENSE-Lora + LICENSE-IBM-Plex + README +
  optional Lora-Italic (A5 verify-at-execute).
- icons-present.test.ts (15 cases across 3 sizes): existence, size FLOOR
  per assets-spec.md, PNG signature, dimensions, color-type byte === 6
  (RGBA — RED until Wave 2 rsvg-convert overwrites the 16-bit-RGB
  placeholders).
- no-remote-fonts.test.ts: production dist/ contains zero
  fonts.googleapis.com / https://fonts / googleapis substrings (MV3 CSP
  self-host invariant T-01-12-01).
- manifest-i18n.test.ts (10 cases): manifest:name === '__MSG_extName__',
  :description === '__MSG_extDesc__', :default_locale === 'en',
  :action.default_title === '__MSG_tooltipOff__'; _locales/{en,ru}/
  messages.json carry D-07 + D-08 canonical strings.
- locale-parity.test.ts (4 cases): ru→en parity, en→ru symmetric,
  non-empty .message strings (RESEARCH Pitfall 4 mitigation).

Current polarity: 29 RED + 18 GREEN across the 6 new files (placeholders
already clear dim+size floors; no-remote-fonts vacuous-GREEN since
tokens.css doesn't yet exist with remote URLs). Existing 100/100 vitest
baseline preserved (verified SKIP_BUILD=1 npx vitest run).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:56:08 +02:00
3fe018beb9 fix(01-12): revise plan baselines per Plan 01-14 landing (vitest 98→100, UAT 15→16, FORBIDDEN_HOOK_STRINGS 10→12)
Surgical amendment — Plan 01-12 is unexecuted; safe for in-place revision.
Plan 01-14 landed (b46712352541459792c0f) since 01-12 was authored,
adding monitorTypeSurfaces:'include' + UAT A23 + 2 FORBIDDEN_HOOK_STRINGS
entries. Plan 01-12 baseline arithmetic updated to reflect post-01-14
floors and depends_on chain extended.

Changes:
- frontmatter depends_on: append 01-14 (canonical sequential-dependency declaration)
- must_haves truth #11: 10 strings → 12 strings post-Plan-01-14, with provenance note
- must_haves truth #13: vitest 98→100→106; UAT 15→16→21 (or 19→24 with 01-10);
  A23 added to no-regression list alongside A0-A14 + (A15-A17 if 01-10)
- Embedded FORBIDDEN_HOOK_STRINGS code block: 10 → 12 entries (lastGetDisplayMediaConstraints,
  get-last-getDisplayMedia-constraints) with attribution comment
- Wave 6 task narrative + verify + done: 20/20 → 21/21; 23/23 → 24/24
- Wave 7 closure narrative + one-liner + checkpoint copy: same arithmetic update
- Threat model T-01-12-08: inheritance updated to include T-1-14-*; grep-gate floor 10→12
- Verify-block harness orchestrator grep gate: strengthened with `grep -v '^#'` filter
  to prevent self-invalidating count from comment lines (per planner.md gate hygiene)
- Success criteria items 3, 4, 9: vitest 98→100; UAT 20→21; inventory 10→12

Preserved verbatim (per surgeon-not-architect):
- 7-wave structure (Wave 0 fonts/tokens/icons/i18n RED scaffolds, Wave 1 fonts+tokens.css,
  Wave 2 icons, Wave 3 manifest i18n, Wave 4 popup/welcome adoption, Wave 5 8 i18n strings,
  Wave 6 harness A18-A22, Wave 7 operator brand-fit checkpoint)
- 10 tasks (validated via gsd-sdk verify.plan-structure)
- R2 Lora baked-in decision
- D-01..D-09 decision references
- All files_modified entries
- Threat model entries other than T-01-12-08 inheritance line
- Designer ack references
- Operator checkpoint (Wave 7)
- Must_have truths #1-10 and #12

Validation:
- gsd-sdk frontmatter.validate → valid:true
- gsd-sdk verify.plan-structure → valid:true, errors:0, task_count:10
- grep verification: zero stale references to 98/104/15/15/20/20/23/23/18/18/"at 10"
- All new baselines present at 32 update sites
- diff: 1 file changed, 34 insertions(+), 30 deletions(-)

Next step: plan-checker re-validation of arithmetic + consistency before executor spawn.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:45:58 +02:00
9792c0f6c3 docs(01-14): state + roadmap + requirements — Plan 01-14 closure
- STATE.md: advance plan counter, update progress (12/14 = 86%), record
  metric (Plan 01-14: 49m, 1 task, 7 files), add decision, record session.
- ROADMAP.md: update phase-01 progress table (plan_count=14, summary_count=12).
- REQUIREMENTS.md: mark REQ-video-ring-buffer complete (final closure for
  the Phase-01 video-pipeline charter).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:41:26 +02:00
52541452e0 docs(01-14): summary — picker enhancement landed (16/16 UAT GREEN, 100/100 vitest GREEN)
Canonical closure artifact for Plan 01-14:
- monitorTypeSurfaces:'include' shipped (W3C spec §6.1; Chrome >= 119 picker narrowing)
- A23 harness regression gate wired into the UAT orchestrator (post-A14 chain)
- Tier-1 grep gate inventory extended in lockstep (12 forbidden strings; was 10)

Single atomic feat commit (b467123) for the entire 1-task plan.

Verification: vitest 100/100 GREEN (+2 new parametrized FORBIDDEN_HOOK_STRINGS
tests; both PASS — production bundle hook-free for the new surface). UAT
harness 16/16 GREEN (was 15/15). tsc + build clean.

Ceremony: replaces the retired AMENDMENT-A.md improvisation path per
01-11-SUMMARY.md Architectural Notes. Canonical GSD flow — plan (41c1f7e)
→ checker (B-01-14-01) → revision (433ee28) → executor (b467123) → SUMMARY.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:40:11 +02:00
b467123578 feat(01-14): monitorTypeSurfaces:'include' — narrow picker to monitor surfaces only
[per Plan 01-14; closes B-01-14-01 via Step 1b lockstep]

- src/offscreen/recorder.ts: add monitorTypeSurfaces:'include' as top-level
  DisplayMediaStreamOptions sibling of video: (W3C Screen Capture spec §6.1;
  Chrome >= 119; removes tab/window panes from the operator's picker per
  Plan 01-10 RESEARCH §5 + §Pitfall-5 recommendation). Typed widening cast
  extended in lockstep to keep the explicit-typing contract (no `as any`).
  D-15 post-grant validation block at recorder.ts:294 UNCHANGED — belt
  (picker narrowing) + suspenders (post-grant tear-down) chain preserved.

- tests/offscreen/display-surface-constraint.test.ts: lockstep update of
  the strict-deep-equality assertion at lines 223-226 with the same key
  ordering as the source change (video -> monitorTypeSurfaces -> audio).
  toHaveBeenCalledWith contract preserved (NO expect.objectContaining —
  the test author's "catches future drops of ANY field" discipline is
  honored). This edit + the source change land in the SAME commit so the
  98/98 baseline never crosses a commit boundary in RED state.

- src/test-hooks/offscreen-hooks.ts: capture last constraints object in
  module-scoped `lastGetDisplayMediaConstraints` cell (was `_constraints`
  received-but-unused; renamed to `constraints`); add `get-last-getDisplayMedia-constraints`
  bridge op to the __mokoshOffscreenQuery dispatcher between
  get-display-surface and get-segment-count. Defensive try/catch mirrors
  the existing dispatcher pattern; the cell is module-internal so the
  MokoshTestSurface cross-cast in types.ts requires NO change (decision
  documented inline in offscreen-hooks.ts).

- tests/uat/extension-page-harness.ts: add `assertA23` mirroring `assertA3`
  (bridge query → 2-check AssertionResult: non-null constraints + value).
  Extend the `Window.__mokoshHarness` declaration + runtime export + status
  bar text + console.log to reference A23.

- tests/uat/lib/harness-page-driver.ts: export `driveA23(page)` mirroring
  the `driveA14` page.evaluate wrapper shape. Standard read-only driver.

- tests/uat/harness.test.ts: extend FORBIDDEN_HOOK_STRINGS (line 85) with
  `lastGetDisplayMediaConstraints` and `get-last-getDisplayMedia-constraints`.
  Import driveA23. Append `{ name: 'A23', drive: driveA23 }` to the drivers
  array after the A14 entry. Update header comment + orchestrator stdout
  to reflect A14 + A23 chain. The `Total = drivers.length + 1` arithmetic
  adapts automatically: 14 + 1 = 15 → 15 + 1 = 16.

- tests/background/no-test-hooks-in-prod-bundle.test.ts: lockstep
  extension of FORBIDDEN_HOOK_STRINGS (line 105) with the same 2 strings.
  Header comment updated to "Total: 12 surface strings." (was 10).
  Confirms production `dist/` has ZERO occurrences after `npm run build`
  via the `__MOKOSH_UAT__` dead-branch tree-shake (T-01-14-04 mitigation).

D-01 (whole-desktop only via getDisplayMedia; reject window/tab surfaces) is
the design intent that monitorTypeSurfaces:'include' realizes at the picker-
UI level. D-15 post-grant validation (recorder.ts:294-307) remains the
actual enforcement against managed-policy/DevTools/older-Chrome overrides.

Verification chain (per Plan 01-14 §verify; clean post-commit):
- `npx tsc --noEmit` exit 0
- `npm run build` exit 0; dist/ produced, monitorTypeSurfaces ships in
  the offscreen chunk as the operator-facing picker hint
- `npm run build:test` exit 0; dist-test/ produced with the harness
  hooks intact (gated)
- `npm test` 100/100 GREEN (was 98/98; +2 via the 2 new FORBIDDEN_HOOK_STRINGS
  parametrized tests — both PASS, production bundle hook-free)
- `npm run test:uat` 16/16 GREEN (15 → 16 via A23). A23 reads constraints
  `{video: {...}, monitorTypeSurfaces: 'include', audio: false}` from the
  fakeGetDisplayMedia capture cell — round-trips through the full call site.
- Production bundle spot-check:
    `grep -rc 'lastGetDisplayMediaConstraints\|get-last-getDisplayMedia-constraints' dist/ | grep -v ':0$'`
    → empty (all `:0` filtered) → ZERO leakage.

References:
- W3C Screen Capture §6.1 DisplayMediaStreamOptions:
  https://www.w3.org/TR/screen-capture/#dom-displaymediastreamoptions-monitortypesurfaces
- Chrome screen-sharing-controls (Chrome 119+):
  https://developer.chrome.com/docs/web-platform/screen-sharing-controls
- Plan 01-10 RESEARCH §5 + §Pitfall-5 (recommendation provenance):
  .planning/phases/01-stabilize-video-pipeline/01-10-RESEARCH.md
- Architectural-note (replaces retired AMENDMENT-A.md improvisation per
  01-11-SUMMARY): canonical GSD ceremony — plan → checker (B-01-14-01)
  → executor → SUMMARY (this commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:37:59 +02:00
433ee280f3 fix(01-14): revise plan per checker — include test-expectation update (B-01-14-01)
Plan-checker BLOCKER B-01-14-01: original plan's must_haves truth #5 understated
baseline regression risk. Adding `monitorTypeSurfaces: 'include'` as a sibling
constraint in src/offscreen/recorder.ts would have dropped vitest from 98/98
GREEN to 97/98 RED because tests/offscreen/display-surface-constraint.test.ts
Test 1 (line 223-226) uses strict deep-equality (toHaveBeenCalledWith, NOT
expect.objectContaining) on the constraints object — the test author's intent
(comment at line 221-222) is to catch future drops of ANY field.

Surgical revision per references/planner-revision.md (surgeon-not-architect):
- Frontmatter: add tests/offscreen/display-surface-constraint.test.ts to
  files_modified list.
- must_haves truth #5: replace the "no existing unit test references the
  constraints object" claim with a positive statement that the strict-deep-
  equality assertion at lines 223-226 is updated in lockstep; preserves the
  test author's "no objectContaining" discipline; explicit no-transient-RED
  guarantee across commit boundaries.
- must_haves artifacts: new entry for the test file documenting the in-place
  edit shape and the preserved test author comment.
- must_haves key_links: new link entry pairing the test assertion with the
  source call site under the lockstep contract.
- Interfaces block: add the explicit "test-expectation lockstep update" code
  fragment with the chosen key ordering (video → monitorTypeSurfaces → audio)
  so the executor lands the source change and the test update with matching
  shapes.
- Task 1 <files>: add tests/offscreen/display-surface-constraint.test.ts.
- Task 1 <action>: insert new Step 1b between Step 1 (source change) and
  Step 2 (offscreen-hooks bridge) — full single-line edit spec at lines
  223-226, preserve toHaveBeenCalledWith contract, preserve comment block,
  same-commit guarantee.
- Task 1 verify-block expected outputs: explicitly call out that 98/98 GREEN
  is preserved BECAUSE Step 1b lands (without it, 97/98 RED on the strict-
  deep-equality assertion).
- Task 1 <done>: add line covering the lockstep test update + the no-transient-
  RED guarantee.
- <verification> phase gate: add new check #2 (test-expectation lockstep)
  between source-line correctness and A23 round-trip.
- <success_criteria>: add bullet for the lockstep test-expectation update.
- <output> SUMMARY contract: add "Revision linkage" bullet documenting that
  the plan was revised once after the plan-checker flagged B-01-14-01.

Untouched (per checker's preserve-verbatim list):
- Source-line target (src/offscreen/recorder.ts:270)
- Harness wiring references (assertA3 686, driveA14 987, __mokoshHarness
  1922+1942, drivers array 289-312, Total comment 354)
- FORBIDDEN_HOOK_STRINGS lockstep contract (both inventories)
- `_constraints` capture path
- Scope discipline (still 1 task, autonomous, no checkpoint)
- Research traceability (Plan 01-10 RESEARCH §5 + §Pitfall-5 + W3C §6.1)
- Threat model (T-01-14-04 mirrors Plan 01-13)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 20:41:09 +02:00
41c1f7e82f feat(01-14): plan — monitorTypeSurfaces picker enhancement (canonical post-closure scope)
Plan 01-14 ships W3C Screen Capture monitorTypeSurfaces: 'include' (Chrome
119+) on the offscreen getDisplayMedia call, plus an A23 harness regression
assertion that verifies the constraint reaches the call site via the
existing offscreen-hooks bridge.

Scope: 1 source line + A23 wiring + Tier-1 grep gate inventory update
(lockstep extension of unit-gate + UAT A0 FORBIDDEN_HOOK_STRINGS).
Autonomous, single executor; no operator empirical checkpoint (UAT 16/16
harness coverage suffices per feedback-pre-checkpoint-bundle-gates.md).

Canonical sources:
- Plan 01-10 RESEARCH section 5 ('monitorTypeSurfaces: include' recommendation)
- Plan 01-10 RESEARCH section Pitfall-5 ('Misinterpreting displaySurface
  as a hard constraint' — monitorTypeSurfaces is the picker-UI complement
  to D-15's post-grant validation)
- W3C Screen Capture spec section 6.1 DisplayMediaStreamOptions
- developer.chrome.com/docs/web-platform/screen-sharing-controls

Decisions honored:
- D-01 (whole-desktop only via getDisplayMedia; reject window/tab) — the
  new constraint is the picker-UI realization of D-01's intent.
- D-15 (post-grant displaySurface validation) — UNCHANGED; remains the
  enforcement (this plan is belt-and-suspenders at the picker UI level).

Ceremony note: this plan replaces the prior AMENDMENT-A.md improvisation
path retired per 01-11-SUMMARY Architectural Notes. Canonical GSD ceremony
(plan -> checker -> executor -> SUMMARY).

Validations:
- gsd-sdk frontmatter.validate -> valid: true (8/8 required fields).
- gsd-sdk verify.plan-structure -> valid: true (1 task; hasFiles/hasAction
  /hasVerify/hasDone all true).
- ROADMAP.md Phase 1 plans list extended with 01-14 entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 20:34:17 +02:00
4d828f1080 docs(01-10): install-flow + auto-select research — both asks INFEASIBLE
Researcher (gsd-phase-researcher) returned HIGH-confidence verdicts on the
12-area brief from .continue-here.md:

Ask 1 (install-time auto-start): INFEASIBLE in unmanaged Chrome.
  W3C Screen Capture spec §5.1 mandates transient user activation;
  chrome.runtime.onInstalled confers none. Floor: 2 clicks
  (toolbar/welcome-page → Share button on picker). Enterprise policy
  ScreenCaptureWithoutGestureAllowedForOrigins exists (Chrome+Edge ≥ 123)
  but only applies to managed-Chrome contexts with extension URL
  whitelisted — does NOT apply to Load-Unpacked deployment; deferred-idea.

Ask 2 (auto-select desktop / skip picker): INFEASIBLE in unmanaged Chrome.
  W3C spec mandates user MUST choose every time. displaySurface:'monitor'
  is a hint (already applied src/offscreen/recorder.ts:270).
  chooseDesktopMedia doesn't auto-accept on single-monitor setups AND
  streamId not usable in MV3 offscreen documents (Chrome DevRel position).

Primary recommendation: KEEP Plan 01-10's current informational CTA
charter (commit 3a530c2 unchanged). Both architectures hit the same
2-click floor; informational CTA has lower maintenance surface AND
teaches operator where the toolbar icon lives (critical for day-2+
sessions).

Enhancement candidate (NOT required for 01-10): add
monitorTypeSurfaces:'include' (Chrome ≥ 119) to offscreen getDisplayMedia
constraints — single line, zero risk, removes tab/window panes from
picker entirely. Suggest as Plan 01-09 amendment OR mini-plan.

Open questions for orchestrator (3):
1. Confirm informational CTA charter (this research recommends YES)
2. Ship monitorTypeSurfaces:'include' as Plan 01-09 amendment vs deferred?
3. Are Repremium operators on Wayland Linux? (affects welcome-page copy)

File: .planning/phases/01-stabilize-video-pipeline/01-10-RESEARCH.md
  (1092 lines; full W3C citations, Chromium issue links, prior art for
  Loom/Screencastify/Veed/Awesome-Screenshot, ASCII flow diagrams,
  edge-case enumeration for macOS/Wayland/incognito/managed-Chrome)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 20:12:18 +02:00
6a29ae4124 chore(01): resume work — consume HANDOFF.json + ignore dist-archives
- Delete .planning/HANDOFF.json (one-shot artifact per resume-project workflow)
- Add dist-archives/ to .gitignore (from prior session's distribution-zip build)
- Bump STATE.md Session Continuity to reflect resumed session + next action
  (install-flow + auto-select researcher spawn)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 18:47:52 +02:00
cecefc61f9 wip: phase-01 paused — .continue-here.md handoff (pairs with c60b887 HANDOFF.json)
Human-readable handoff. Captures:
- 3 BLOCKING CONSTRAINTS from saved memory (scope reduction, GSD ceremony, pre-checkpoint gates)
- 3 anti-patterns from this session (improvised artifact types, claiming canonical without verifying, save-stops UX cycle)
- Current state + working tree + test/build baseline
- Next-session order of operations (10 steps)
- Required reading order
- Pending researcher brief (12 areas; was 529-blocked)
- Infrastructure state + API capacity note
- Followup backlog

Resume: /clear → /gsd-resume-work

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:57:45 +02:00
c60b8878df wip: phase-01 paused — Plan 01-13 closed; 01-10 + 01-12 plans ready; researcher pending
Session arc:
- Plan 01-13 UAT harness: closed at 285e46f (15/15 GREEN; Bug A + Bug B regression demos verified; operator UAT ack)
- Save-stops-recording: fix→reversal cycle (operator preferred original always-on charter); Amendments 2+3 of 01-09-PLAN.md document both cycles
- Designer handoff fully ingested + 9 brand decisions resolved + R2 Lora reply unblocks display font
- Plan 01-10 (welcome tab) rewritten in place at 3a530c2 — design-swap-in-ready architecture
- Plan 01-12 (Design Integration) created at 8d1c8fb — R2 Lora baked in; 7 waves
- Plan 01-11 closed as spike-pivot at ba5474c — architectural learnings preserved
- Distribution zip published at dist-archives/ (gitignored; INSTALL.md included)

Pause state:
- vitest 98/98 GREEN; npm run test:uat 15/15 GREEN; tsc + build clean
- Working tree: M .gitignore (dist-archives entry; safe to commit next session)
- HEAD: e035fd2

Remaining Phase 1 work captured in HANDOFF.json + .continue-here.md:
- Re-spawn install-flow + auto-select researcher (529'd twice during session; API capacity issue)
- Plan 01-10 executor
- Plan 01-12 executor
- Operator brand-fit UAT after 01-12 lands
- Phase 1 closure markers + ROADMAP.md backfill

Resume with /gsd-resume-work after /clear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:56:28 +02:00
e035fd279d docs(01-09): Amendment 3 + 01-13 SUMMARY reversal note + STATE.md sync + debug records
Plan 01-09 Amendment 3 (2026-05-19) — atomic documentation pass for
the save-does-not-stop-recording charter reversal.

Changes:
- .planning/phases/01-stabilize-video-pipeline/01-09-PLAN.md:
  Amendment 3 block added above <success_criteria> (mirrors
  Amendment 2 placement). Describes the reversed charter,
  references the new debug record, points at the inverted
  test file + harness A14.
- .planning/phases/01-stabilize-video-pipeline/01-13-SUMMARY.md:
  "Subsequent Reversal (2026-05-19)" footer added. Notes that
  npm run test:uat still 15/15 GREEN under the inverted A14
  contract; vitest baseline preserved at 98 GREEN.
- .planning/STATE.md:
  Plan 01-13 closure block extended with CHARTER REVERSAL bullet
  citing the 4 commit SHAs (6ac23fd RED, 7645765 GREEN,
  1baaf45 A14 invert, this commit docs).
- .planning/debug/resolved/01-09-save-stops-recording.md:
  SUPERSEDED 2026-05-19 footer appended (audit trail; original
  fix was technically correct against its charter, reversal is
  UX iteration not technical defect).
- .planning/debug/resolved/01-09-save-does-not-stop-recording.md:
  NEW debug record landed directly in resolved/ (no checkpoint
  cycle — orchestrator-diagnosed reversal). Documents symptom,
  charter clarification cycle, fix shape, RED→GREEN evidence
  with commit SHAs + vitest/harness output, anti-regression
  coverage at unit + E2E layers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:50:49 +02:00
1baaf45702 feat(01-13-A14-invert): A14 — invert to assert continuous-recording post-SAVE
Plan 01-09 Amendment 3 (2026-05-19) end-to-end lock. Inverted A14 to
match the reversed charter (SAVE creates zip, recording continues).

Page-side (tests/uat/extension-page-harness.ts):
- assertA14: assert badge==='REC' (was ''), popup endsWith
  'src/popup/index.html' (was ''), no-new-recovery-notif (unchanged).
- A14 name + check labels updated to reflect continuous-recording semantic.
- New constant A14_POPUP_HTML_SUFFIX for the popup endsWith check
  (ext-id-agnostic via suffix match).
- A13 docstring + diag strings refreshed: setupFreshRecording is now
  defensive (orthogonal to A12 ordering) rather than a workaround for
  the prior auto-stop. 11s settle preserved (same wall-clock cost).

Host-side (tests/uat/lib/harness-page-driver.ts):
- driveA14 docstring refreshed to mention Amendment 3 + the inverted
  contract; mechanical wrapper unchanged.

Verification:
- npm run test:uat: 15/15 GREEN
- A14 actual output:
    badge='REC'
    popup='chrome-extension://<ext-id>/src/popup/index.html'
    recoveryDelta=0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:33:53 +02:00
7645765401 feat(01-09-no-stop): GREEN — remove SAVE_ARCHIVE finally block; recording continues
Plan 01-09 Amendment 3 (2026-05-19) — code surgery to drive
tests/background/save-archive-does-not-stop-recording.test.ts from
RED (commit 6ac23fd) to GREEN.

Surgery: removed the entire `finally` block from saveArchive() in
src/background/index.ts (introduced by Amendment 2 commit 4f4c3e2).
SAVE_ARCHIVE now returns to its original semantics:
  create zip → download → done. No state transitions.

Updated the function-level docstring to reflect the new charter +
point at the regression-locking test file and harness A14 assertion.

Verification:
- save-archive-does-not-stop-recording.test.ts: 4/4 GREEN
- Full vitest baseline: 98/98 GREEN (SKIP_BUILD=1)
- tsc --noEmit: clean
- npm run build: clean (374.91 kB SW chunk; no test-hook leaks)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:21:15 +02:00
6ac23fdbd8 test(01-09-no-stop): RED — invert save-archive contract to lock always-on charter
Per operator UX iteration (2026-05-19), the Amendment 2 save-stops-recording
fix (commits cd83eb0+4f4c3e2+2b6c24b+89f3337) is REVERSED. SAVE_ARCHIVE
creates a new zip but does NOT stop the recorder — matches SPEC's
continuous-capture / always-on safety-net framing.

This commit renames the test file via `git mv` (history preserved) and
inverts tests A..C to assert the new contract:
- A: no NEW setBadgeText({text:''}) call (badge stays REC)
- B: no setPopup({popup:''}) call (popup stays pinned to popup.html)
- C: no STOP_RECORDING dispatch via chrome.runtime.sendMessage
Test D (no recovery notification) preserved unchanged as regression guard.

RED expected — src/background/index.ts still has the Amendment 2
`finally` block dispatching STOP_RECORDING + setIdleMode. Next commit
removes that block to drive GREEN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:45:31 +02:00
8d1c8fb0cc docs(01-12): create Plan 01-12 (Design Integration; R2 Lora unblocks; 7 waves)
Final designer reply received 2026-05-19 unblocks Plan 01-12: R2
substitution — replace Newsreader with Lora (OFL, Cyreal foundry, full
Cyrillic-Latin parity, variable wght 400-700). All 9 brand decisions
now resolved; R2 displaces Newsreader from `--mks-font-display`.

Plan structure: 7 waves, 10 tasks.

- Wave 0 (TDD scaffolds): 6 RED unit tests — tokens-adopted,
  fonts-present, icons-present, no-remote-fonts, manifest-i18n,
  locale-parity. Each RED until its corresponding artifact wave lands.
- Wave 1: Self-host OFL font bundle (Lora variable normal + italic,
  Plex Sans ×4, Plex Mono ×2) at src/shared/fonts/ via pyftsubset
  (Latin + Cyrillic basic subset); land src/shared/tokens.css canonical
  (Google Fonts @import → 7 local @font-face rules; Newsreader → Lora
  per R2; .mks-word class added per RESEARCH §8 + lockup SVG line 21).
- Wave 2: Rasterize Loom mark to icons/icon{16,48,128}.png via
  rsvg-convert; overwrite Bug A placeholders; 8-bit RGBA at all sizes.
- Wave 3: Land _locales/{en,ru}/messages.json (12 keys: 8 Brief §02
  operator strings + 4 supporting keys); manifest.json → __MSG_extName__
  + __MSG_extDesc__ + default_locale 'en' + action.default_title.
  extName='Mokosh — Session Capture' per D-07 user override; extDesc per
  D-08 brand-decisions-v1.md wording.
- Wave 4: src/popup/ + src/background/ adopt tokens.css (loom palette)
  + chrome.i18n.getMessage at every operator-facing copy site; replace
  hex literals with var(--mks-*) references; BADGE_REC_COLOR madder
  '#b2543d' (= --mks-madder-600 per D-04 + RESEARCH §10 Open Q A7).
- Wave 5: Welcome page conditional migration (if 01-10 landed, swap
  welcome-tokens.css → @import canonical tokens.css; migrate copy.ts
  shim to chrome.i18n.getMessage fallback); add __VITE_DEV__ define
  per RESEARCH §12 D-09 spirit; scripts/README.md smoke-isolation note.
- Wave 6: UAT harness A18-A22 (font reachability via document.styleSheets
  walk + fetch + byteLength; icon-not-placeholder via fingerprint diff;
  manifest:name === 'Mokosh — Session Capture'; --mks-font-display
  resolves to Lora via getComputedStyle; welcome tokens loaded
  conditional on 01-10). Tier-1 forbidden-strings UNCHANGED at 10.
- Wave 7: Operator empirical brand-fit checkpoint (last Phase 1 gate);
  SUMMARY + STATE.md + ROADMAP.md sync.

ROADMAP.md Phase 1 plan list extended from 7 → 13 entries (gap noted in
01-13 SUMMARY's known-limitations now closed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:59:59 +02:00
3a530c2334 docs(01-10): rewrite plan in place — D-02/D-08/D-17-onboarding charter + design-swap-in-ready arch + harness A15+A16+A17
Drops the 2026-05-17 draft (zero commits, zero SUMMARY — virgin). Carries:
- onInstalled flag-gated welcome tab (3 RED→GREEN unit tests; storage-key contract pinned per prior I-02 fix)
- 5 new src/welcome/* files: welcome.html + welcome.ts + welcome.css + welcome-tokens.css + copy.ts
- Design-swap-in-ready: every color via var(--mks-*); every string via COPY map; every font via var(--mks-font-*) with system fallback
- vite.config.ts + vite.test.config.ts both gain welcome rollup input
- manifest.json gains web_accessible_resources for welcome.html
- Harness extended A15+A16+A17 (onboarding flag observability + no-re-open settle + design-swap invariant); Tier-1 forbidden-strings inventory unchanged at 10
- 4 autonomous tasks + 1 operator empirical checkpoint

Deletes: REQUEST_PERMISSIONS flow (gone in 01-09); duplicate Start button (D-16-toolbar owns start path); start-path divergence

Cites: D-17-onboarding (CONTEXT.md L537+), D-02 (welcome layout), D-08 (tagline), D-03 (voice register), D-16-toolbar (start ownership), brand-decisions-v1-followup-display-font.md (Plan 01-12 blocker; this plan ships TODAY with placeholders)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:23:56 +02:00
285e46f620 docs(01-13): close — operator UAT ack 2026-05-19 + save-stops debug resolved + SUMMARY landed
Plan 01-13 fully closed. Operator UAT acked "all good" on 2026-05-19;
recovery flow (A7) + restart-after-click (A2) both harness-covered, no
manual verification needed.

What this commit lands:
- 01-13-SUMMARY.md (full spike-pivot-then-implementation narrative; tracks
  all 16 commits across Plan 01-11 spike + Plan 01-13 4-wave execution +
  save-stops debug session; documents 15/15 npm run test:uat GREEN +
  98/98 vitest GREEN + Bug A/B regression-rewind demos verified)
- Save-stops debug record moved to .planning/debug/resolved/ (closure-
  canonical location; prior inline path tracked via git mv)
- STATE.md sync: completed_plans 11→12, percent 95→96, Plan 01-13
  fully-closed narrative + save-stops debug session captured

Phase 1 functional contract: CLOSED via harness PASS.
Remaining Phase 1 gates: Plan 01-10 (welcome tab) + Plan 01-12 (design
integration; pending designer Newsreader-Cyrillic reply).

Phase 2 inherits the harness as its closure-gate template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:46:49 +02:00
89f3337334 docs(01-09-save-stops): debug record — RED → GREEN → A14 evidence + closure notes
Updates .planning/debug/01-09-save-stops-recording.md with:
  - status awaiting_human_verify
  - SHA references (red_commit cd83eb0, green_commit 4f4c3e2, a14_commit 2b6c24b)
  - Verification evidence (98/98 unit GREEN, 15/15 UAT GREEN, tsc+build clean)
  - Vitest + UAT output snippets
  - A14 design rationale
  - Noteworthy: no PortMessage/Message changes (STOP_RECORDING already
    in MessageType + offscreen handler already wired); resetBuffer
    intentionally NOT called by SW post-save (offscreen.stopRecording
    deliberately omits it; next session's startRecording handles it);
    empty-buffer trade-off (brief recovery notif in production, badge
    still resolves to OFF — documented inline in src/background/index.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:32:51 +02:00
2b6c24b2d9 feat(01-13): A14 — post-SAVE state check (badge='', popup='', no new recovery notif)
Plan 01-13 Task 9 closure for operator empirical UAT bug
.planning/debug/01-09-save-stops-recording.md. Adds the harness
assertion that empirically verifies the SAVE-auto-stops-recording fix
(committed at 4f4c3e2) holds end-to-end through a real Chrome instance
+ real MediaRecorder + real chrome.action + real chrome.notifications.

A14 design (read-only post-state check):
  1. Snapshot active mokosh-recovery-* notification ids (delta baseline).
  2. Settle 500ms for the post-A13 SAVE finally block to land.
  3. Read chrome.action.getBadgeText/getPopup + getActiveNotificationIds.
  4. Assert badge='', popup='', recoveryDelta=0 — three checks total.

A14 chains off A13's SAVE_ARCHIVE (which under the new fix auto-stops
the recording per SPEC one-shot intent). A14 does NOT dispatch its own
SAVE — A13's SAVE is the event A14 observes the post-state of. This
keeps the harness wall-clock minimal (~500ms added for A14, no
additional 11s segment-settle).

Amendment to A13: now does setupFreshRecording + 11s segment-settle
BEFORE its own SAVE_ARCHIVE dispatch. Under the new fix, A12's
SAVE_ARCHIVE stopped the recording — without this A13 would dispatch
against an empty buffer and fail with EmptyVideoBufferError. The
amendment adds ~11s to harness wall-clock; acceptable given the SPEC
SAVE=stop contract is now load-bearing.

A14 contract notes per orchestrator simpler-design recommendation:
  - direct isRecording proxy check skipped (no bridge op exposes it;
    transitively verified via badge='' — production state machine
    pairs isRecording transitions with badge transitions atomically)
  - recovery-notif check is delta-based (A7 left a mokosh-recovery-*
    in the active set; we verify A13's SAVE did NOT add another one)

Files modified:
  - tests/uat/extension-page-harness.ts: +assertA14 (~110 lines) +
    A13 amended with setupFreshRecording + 11s settle + 1 new
    SETUP check + window.__mokoshHarness export wire
  - tests/uat/lib/harness-page-driver.ts: +driveA14 wrapper
  - tests/uat/harness.test.ts: +A14 in drivers array + header doc +
    total 14/14 → 15/15 + import line

Verification:
  - npm run test:uat: 15/15 GREEN (was 14/14)
  - npx tsc --noEmit: exit 0 (no type errors)
  - npm run build: exit 0 (production bundle clean)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:31:13 +02:00
4f4c3e2241 feat(01-09-save-stops): GREEN — SAVE_ARCHIVE auto-stops recording per SPEC one-shot intent
Operator UAT closure for Plan 01-13 Task 9. Patches saveArchive() in
src/background/index.ts with a `finally` block that dispatches
STOP_RECORDING to offscreen (mirrors the existing START_RECORDING
control-plane channel via chrome.runtime.sendMessage), flips
isRecording=false, and calls setIdleMode() — applied to BOTH the
success and empty-buffer-error paths.

Operator UX contract: SAVE click ALWAYS stops the session, regardless of
internal success/empty-buffer outcome. The badge clears, the popup
empties (re-enabling chrome.action.onClicked for restart), and Chrome's
sharing banner closes via the offscreen recorder's stopRecording()
(which nulls mediaStream + stops all tracks + clears the rotation
timer — line 527 of src/offscreen/recorder.ts, already wired since
Plan 01-05).

Trade-off documented inline: empty-buffer path still surfaces a
recovery notification (the catch branch emits RECORDING_ERROR{
error:'empty-video-buffer'} → SW's own onMessage handler runs
setErrorMode + creates a mokosh-recovery-* notif). The finally block
then setIdleMode()'s, so the FINAL visible state is OFF/empty-popup —
clean restart path. The notification stays visible briefly so the
operator sees that something went wrong, then clicks it to start a
new session.

Test count: 94 GREEN (baseline) → 98 GREEN (+4 from
tests/background/save-archive-stops-recording.test.ts).

Files modified:
  - src/background/index.ts (saveArchive + finally block; no
    PortMessage/Message type changes — STOP_RECORDING already in
    MessageType per src/shared/types.ts:14, offscreen handler at
    recorder.ts:848 already wired)

Toolchain:
  - npx tsc --noEmit: exit 0 (no type errors)
  - npm run build: exit 0 (dist/ clean rebuild)

Debug record: .planning/debug/01-09-save-stops-recording.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:22:37 +02:00
cd83eb0498 test(01-09-save-stops): RED — SAVE_ARCHIVE triggers STOP_RECORDING + setIdleMode + no recovery notif
Plan 01-13 Task 9 operator UAT closure. Operator 2026-05-19 empirical
session: SAVE click downloaded zip but recording stayed live (badge=REC,
sharing banner persisted, subsequent toolbar press re-opened SAVE-only
popup). Operator pressed 4×, got 2 zips + confusion.

Root cause: src/background/index.ts saveArchive() returns success after
chrome.downloads.download without signaling offscreen to stop or
transitioning the SW state machine — SPEC `Тз расширение фаза1.md`
"one click MUST produce a self-contained archive" was over-extended to
"always-on" framing by the implementation.

Fix contract (RED today; GREEN after src/background/index.ts patch):

  A: setBadgeText({text:''}) called post-save (setIdleMode side effect)
  B: setPopup({popup:''}) called post-save (re-enables chrome.action.onClicked
     restart path per MV3 contract)
  C: chrome.runtime.sendMessage({type:'STOP_RECORDING'}) dispatched
     (offscreen recorder.ts:848 STOP_RECORDING case already wired —
     no offscreen-side change needed)
  D: NO mokosh-recovery-* notification fires (deliberate stop ≠ error;
     mirrors Bug B `user-stopped-sharing` suppression branch from
     .planning/debug/resolved/01-09-recovery-flow.md)

Tests A/B/C RED (assertion errors `expected 0 >= 1`); Test D GREEN today
as the regression guard against fix over-rotating to setErrorMode.

Test architecture mirrors tests/background/request-id-protocol.test.ts:
synthetic BUFFER response delivered via port.onMessage listeners to drive
saveArchive's request-id'd buffer fetch to completion. Empty-segments
BUFFER causes createArchive → EmptyVideoBufferError → catch branch; the
fix's STOP+IDLE dispatch MUST happen on both success and empty-buffer
paths (operator UI contract: SAVE click = stop, success or empty alike).

Debug record: .planning/debug/01-09-save-stops-recording.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:17:19 +02:00
9c5ff8b2a7 docs(01-13): wave-4 task-8 — Plan 01-09 closure-via-harness amendment + STATE.md sync
Lands the Wave 4 closure docs:
- 01-09-PLAN.md Amendment 2: harness PASS (npm run test:uat 14/14 GREEN at d793c9e)
  now closes Plan 01-09 functional contract (original Task 5 steps 4, 5, 7-13, 15);
  operator retains only step 1 (build) + step 14 (brand/design ack — Plan 01-13
  Task 9 charter). Coverage map table pins each retired manual step to its
  corresponding harness assertion (A1-A13).
- STATE.md sync: completed_plans 9->11, percent 92->~95, last_updated
  2026-05-19T10:40:00Z; Current Position narrative replaced with Plan 01-13
  landing summary + outstanding Phase 1 gates (Plan 01-13 Task 9 operator
  checkpoint, Plan 01-10 welcome tab, Plan 01-12 design integration awaiting
  designer reply).

Verification (post-edit, docs-only — no src/ touched):
- npm run test:uat: 14/14 GREEN at d793c9e baseline preserved
- npx vitest: 94/94 (the no-test-hooks-in-prod-bundle.test.ts default 5s timeout
  flake is harness-side, not introduced by this commit; re-run with
  --testTimeout=60000 passes)
- git status post-commit: clean

Followup: ROADMAP.md is missing entries for Plans 01-08 through 01-12 (these were
all added mid-phase across multiple sessions and the roadmap was never amended;
the Phase-1 Plans block lines 74-80 stops at 01-07 and the progress table line
238 shows the stale '7/7 Complete' count). Backfilling those entries is a
separate concern — out of scope for Plan 01-13 closure per plan-checker flag #4
(hold the line; do not inject). STATE.md notes the counter > total mismatch for
visibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:50:13 +02:00
d793c9e1e5 feat(01-13): wave-3D — A11+A12+A13 GREEN + get-segment-count bridge op; 14/14 GREEN
Lands the final three UAT-harness assertions. All 14 assertions (A0..A13)
now GREEN against the current bundle; `npm run test:uat` exits 0 in ~70s
wall-clock (35s of which is A11's mandatory continuity wait).

Assertions wired:

 - A11 — 35s buffer continuity → segments.length >= 3. Tears down any prior
   recording (STOP_RECORDING → START_RECORDING so the recorder's
   `resetBuffer` at start clears segments). Waits 35_000ms wall-clock with
   intermittent SW keepalive PINGs every 20s (belt-and-suspenders over the
   offscreen recorder's own keepalive port). Queries the new
   `get-segment-count` bridge op. Asserts count >= 3 (per D-13:
   SEGMENT_DURATION_MS=10s × MAX_SEGMENTS=3).

 - A12 — SAVE_ARCHIVE produces zip; webm passes ffprobe. Page side
   dispatches SAVE_ARCHIVE (recording from A11 still alive). Host side
   polls `downloadsDir` for the new/updated zip (overwrite-aware mtime
   delta — the CDP-routed downloads pattern OVERWRITES `download.zip`
   rather than numbering it, empirically verified during initial RED).
   Extracts `video/last_30sec.webm` via JSZip to a tmpfile. Runs
   `/usr/bin/ffprobe -v error -f matroska <path>`; asserts exit 0 + clean
   stderr. Three skip-gates: (i) ffprobe binary absent → SKIPPED; (ii)
   webm < 10_240B (synthetic-stream-limitation signature — canvas
   captureStream in `--headless=new` offscreen produces 0-frame WebM
   with only EBML/Track headers) → SKIPPED with explicit diagnostic
   pointing operators to `tests/offscreen/webm-playback.test.ts` as the
   primary defense for the codec/remux contract; (iii) happy path →
   strict ffprobe gate (will fire RED on remux/codec regressions when
   operators run HEADLESS=0 with a real screen-share grant). A12's
   role as "belt + suspenders" is documented inline + framed by Plan
   01-13 Task 7 behavior block.

 - A13 — Zip structure + meta.json shape. Second SAVE_ARCHIVE (verifies
   idempotency over A12's first save). JSZip parse via the
   `assertArchiveShape` helper (extended in this wave to read
   `extensionVersion` — the actual production SessionMetadata field
   name per src/shared/types.ts:103, vs. the earlier 01-11 prototype's
   incorrect `version` assumption). Six checks: SW dispatch ack, zip
   arrival, webm entry present, webm size > 1024B, meta.json entry
   present, meta.json.extensionVersion matches
   chrome.runtime.getManifest().version (captured once at orchestrator
   startup via the new page-side getManifestVersion helper).

Bridge op + recorder wire:

 - Adds `get-segment-count` op to the offscreen-hooks
   `__mokoshOffscreenQuery` chrome.runtime.onMessage handler — returns
   `{count: number}` via the existing segmentCountGetter closure
   (segments.length captured at recorder.ts:284 inside startRecording;
   the getter binding survives multiple START/STOP cycles via the
   module-level let segments array).

 - Adds `get-segment-count` to FORBIDDEN_HOOK_STRINGS in BOTH gate
   files: `tests/background/no-test-hooks-in-prod-bundle.test.ts`
   (Tier-1 unit gate; 9 → 10 entries; vitest 93 → 94 GREEN) and
   `tests/uat/harness.test.ts:assertA0_GrepGate` (UAT-level mirror).
   Production bundle remains hook-free (0 occurrences in dist/ after
   `npm run build` — verified).

Harness surface:

 - `tests/uat/extension-page-harness.ts` extends `window.__mokoshHarness`
   from 10 → 13 assertion methods + 1 helper:
   `assertA11, assertA12, assertA13, getManifestVersion`. Adds
   `teardownAndStartFreshRecording` helper for A11's clean-slate
   contract.

 - `tests/uat/lib/harness-page-driver.ts` retires the Wave-3 stub
   marker (no more NYI throws). Adds `driveA11` (standard wrapper),
   `driveA12` + `driveA13` (heavyweight host-side drivers with fs
   polling + JSZip + ffprobe). Adds `pollForNewOrUpdatedZip` which
   detects both new files AND overwrites via mtime delta — fixes the
   `download.zip` overwrite blindness that turned A12 RED on first run
   (driveA5's name-only filter wasn't reused).

 - `tests/uat/lib/zip.ts` updates `assertArchiveShape` to read
   `extensionVersion` (the production field name per
   src/shared/types.ts:103); adds the A13_MIN_VIDEO_BYTES=1024 floor
   constant.

 - `tests/uat/harness.test.ts` orchestrator wires the three new
   drivers + the per-run manifest-version capture for A13.

Baseline:

 - `npx tsc --noEmit`: exit 0.
 - `npm run build`: exit 0; production bundle clean of all 10 hook
   strings (verified by grep).
 - `npm run build:test`: exit 0; test bundle ships `get-segment-count`.
 - `npx vitest run`: 94/94 GREEN (was 93; +1 from the new gate string).
 - `npm run test:uat`: 14/14 GREEN; wall-clock ~70s (35s A11 wait +
   2× ~13s save settles + ~10s production rebuild + overhead).

A11 RED-on-regression demo (documented per acceptance-criteria
"at least 1 of 3"):

  Edit src/offscreen/recorder.ts:52: `SEGMENT_DURATION_MS = 10_000`
  → `SEGMENT_DURATION_MS = 30_000`. Rebuild dist-test. Re-run UAT.
  A11 FAILS (only 1 segment rotates in 35s, vs floor of 3). Revert
  the edit; A11 PASSES. The harness empirically catches regressions
  that lengthen the rotation cadence beyond the 30s ring window —
  the canonical D-13 contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:24:39 +02:00
b665919c5f feat(01-13): wave-3C — A8+A9+A10 GREEN + Bug A canonical regression rewind
Plan 01-13 Task 6 (Wave 3C). Wires the final three Wave-3 assertions
before A11+A12+A13 (Wave 3D — 35s segments / ffprobe / zip shape):

- A8 (Bug A canonical regression rewind) — invokes
  chrome.notifications.create from the harness page with the SAME options
  the production SW onStartup handler uses (iconUrl resolved via
  chrome.runtime.getURL('icons/icon128.png')). Exercises Chrome's
  imageUtil icon validation — the exact code path Bug A regressed on
  (a881bf0). 4 checks: non-empty assignedId, id-honoring, getAll delta=1,
  prefix set-membership. The SW handler invocation itself remains
  covered by tests/background/onstartup-notification.test.ts (unit
  tier); A8 covers the end-to-end imageUtil-acceptance gate (e2e tier).
  Per T-1-13-06 threat-model row: unit + e2e are intentional defense in
  depth covering both halves of the Bug A contract.

- A9 (icon file sizes meet imageUtil floors) — fetches icons/icon{16,48,
  128}.png via chrome.runtime.getURL and asserts blob.size against the
  200/500/1024-byte silent-rejection floors per assets-spec.md. Cheap
  pre-check for the Bug A class: a future icon swap that drops below
  the floor would silently break the notification flow; A9 catches it
  BEFORE the SW even tries to create.

- A10 (manifest shape contract) — chrome.runtime.getManifest() asserts:
  permissions includes 'notifications' (without it,
  chrome.notifications.create is unreachable), icons['16/48/128']
  defined + non-empty, action.default_icon['16/48/128'] same. 7 checks
  total. Catches manifest-edit regressions that would silently break A8.

Bug A canonical RED-on-regression demo cycle
============================================

Regression trigger: head -c 50 /tmp/icon128.png.backup > icons/icon128.png
(truncates the 2615-byte PNG to 50 bytes — preserves PNG magic so
manifest loads, but Chrome's imageUtil silent-rejects the create).

RED — A8 standalone driver with truncated icon128.png (50 bytes):

  A8 — BUG A canonical: chrome.notifications.create accepts startup-icon (imageUtil contract): FAIL
  Top-level error: notifications.create rejected: Unable to download all specified images.

  Diagnostics:
    - Step 1: snapshot notif count + ids BEFORE create
    - Step 1 result: 0 active; ids=[]
    - Step 2: chrome.notifications.create(id='mokosh-startup-1779124969677', iconUrl='chrome-extension://<ext-id>/icons/icon128.png')
    - THREW: notifications.create rejected: Unable to download all specified images.

GREEN — A8 standalone driver after restoring icon128.png (2615 bytes):

  A8 — BUG A canonical: chrome.notifications.create accepts startup-icon (imageUtil contract): PASS

  Checks:
    [PASS] A8.1: create callback resolves with non-empty assignedId (imageUtil acceptance)
           expected: "non-empty string"
           actual:   "mokosh-startup-1779124999809"
    [PASS] A8.2: assignedId matches input id (chrome.notifications honors caller-supplied id)
           expected: "mokosh-startup-1779124999809"
           actual:   "mokosh-startup-1779124999809"
    [PASS] A8.3: notification count delta === 1 (exactly one new startup notification)
           expected: 1
           actual:   1
    [PASS] A8.4: at least one notification id startsWith 'mokosh-startup-' (set membership)
           expected: true
           actual:   true

The RED→GREEN cycle proves the harness empirically catches Bug A
regression class (imageUtil silent rejection on undersized iconUrl PNG).
The "Unable to download all specified images." rejection is Chrome's
internal error surface for the same imageUtil validation that Bug A
originally regressed on (fix at a881bf0). Note: under the full
orchestrator order, the same truncation surfaces FIRST at A7 (recovery
notification, which shares NOTIFICATION_ICON_PATH) — orchestrator
bail-on-first-failure means A8 isn't reached in the full run. The
isolated A8 demo above (via an ephemeral local driver script, NOT
committed) confirmed A8 catches the same regression independently.

Baseline preserved
==================

- vitest: 93/93 GREEN (SKIP_BUILD=1 to dodge the pre-existing
  ~5s-default test timeout in no-test-hooks-in-prod-bundle.test.ts;
  with a fresh dist/ in place all 9 hook-string sub-tests PASS).
- tsc: clean (no diagnostics).
- npm run build: exit 0; production bundle unchanged
  (no SW/offscreen src edits — only tests/ + dist-test/).
- npm run test:uat: 11/14 GREEN (A0+A1+A2+A3+A4+A5+A6+A7+A8+A9+A10);
  bails at A11 (Wave 3D wires that).

Files touched
=============

- tests/uat/extension-page-harness.ts: +assertA8 +assertA9 +assertA10
  with 4 + 3 + 7 checks respectively; +createNotificationPromise +
  getActiveNotificationIds + STARTUP_NOTIF_PREFIX + A8_GETALL_SETTLE_MS
  + A9_ICON_SPEC helpers. window.__mokoshHarness extends 7 → 10 methods.
- tests/uat/lib/harness-page-driver.ts: replaces driveA8/driveA9/driveA10
  NYI stubs with page.evaluate wrappers.
- tests/uat/harness.test.ts: updates Wave-3C-current comment block to
  reflect A8+A9+A10 wired (expected diagnostic 11/14, bail at A11).

Approach rationale (per plan resolved-questions §A8)
====================================================

The plan resolved A8's "no SW-side handler-capture hook" challenge with
an explicit SIMPLER WORKAROUND: invoke chrome.notifications.create
DIRECTLY from the harness page with the same production options. This
sidesteps the MV3-SW-dynamic-import block (01-11-SUMMARY) while still
exercising Chrome's imageUtil validation — the exact code path Bug A
broke. Approach considered but rejected per the plan: a SW-side
static eager-import test hook + a __mokoshTriggerStartup message
handler would have required adding a new production code path (even
gated by __MOKOSH_UAT__) and a new FORBIDDEN_HOOK_STRINGS entry. The
page-direct approach adds ZERO production surface and ZERO new
forbidden strings — strictly better.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:07:47 +02:00
6a77967b6c feat(01-13): wave-3B — A5+A6+A7 GREEN + Bug B canonical regression rewind
Wave 3B lands the A5 (SAVE_ARCHIVE → zip on disk) and A7 (genuine
RECORDING_ERROR → ERR + recovery notification) assertions, completing
8/14 of the orchestrator's GREEN floor (A0+A1+A2+A3+A4+A5+A6+A7).
Bails at A8 (Wave 3C scope).

Changes per file:

  tests/uat/extension-page-harness.ts
    - assertA5: 11s settle (>= SEGMENT_DURATION_MS so first rotation
      lands a segment) + send SAVE_ARCHIVE + assert resp.success=true.
      Page-side only checks SW handler ack; host-side driver verifies
      disk-side outcome (zip presence + size floor).
    - assertA7: setupFreshRecording helper (A6 tears down; A7 needs
      REC state) → snapshot notif count → send RECORDING_ERROR with
      a non-Bug-B error code ('codec-unsupported') → 200ms settle →
      assert badge='ERR' + popup endsWith popup.html + notif delta=1
      + set-membership for 'mokosh-recovery-*' prefix.
    - setupFreshRecording: shared helper for A7 + future assertions
      that need a fresh REC state after a teardown.

  tests/uat/lib/harness-page-driver.ts
    - driveA5: page.evaluate(assertA5) THEN host-side fs polling for
      *.zip in handles.downloadsDir. The CDP Browser.setDownloadBehavior
      override renames the file to download.zip (data: URL filename
      gap), so we accept any *.zip suffix. Merges page-side check +
      host-side checks into a single AssertionRecord. Signature now
      takes downloadsDir as a second arg.
    - driveA7: standard page.evaluate wrapper (no host-side work).

  tests/uat/harness.test.ts
    - Wraps driveA5 in a closure that captures handles.downloadsDir.
    - Reordered: launchHarnessBrowser MUST run before driver list so
      the closure can read handles without a TDZ trap.

  tests/uat/lib/launch.ts
    - Victim page switched from about:blank to a file:// URL backed by
      a tmp HTML file in downloadsDir. About:blank breaks A5 because
      chrome.tabs.captureVisibleTab needs <all_urls> permission which
      matches http/https/file/ftp but NOT about: or data: URLs. The
      stub HTML satisfies <all_urls> + provides a real .url for the
      production saveArchive's chrome.tabs.query.

  src/test-hooks/offscreen-hooks.ts (test-only — tree-shaken from prod)
    - installFakeDisplayMedia: mintStream() helper called per
      fakeGetDisplayMedia invocation; each call mints a FRESH
      MediaStream from the persistent canvas. Real getDisplayMedia
      returns a new stream per call — fake now matches. Required for
      A7's setupFreshRecording where the previous recording's stream
      tracks were stopped by A6's onUserStoppedSharing teardown.
    - Added 33ms setInterval-driven drawFrame() alongside the
      existing requestAnimationFrame loop. RAF can throttle in
      headless Chrome on offscreen documents (page-visibility
      heuristics produce 0 fps), which yields zero-byte
      MediaRecorder segments that crash ts-ebml's VINT decode in
      webm-remux.extractFramesFromSegment with "Unrepresentable
      length: Infinity". The setInterval is redundant when RAF fires
      at full rate; it's a safety net for the headless-MV3 corner.

Bug B regression-catch demo (success_criteria #3 — MANDATORY per plan):

Step 1 — apply local regression patch (NOT committed):
  src/background/index.ts:792  setIdleMode() → setErrorMode()

Step 2 — npm run build:test && npm run test:uat RED snippet:

  A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: FAIL
    [PASS] SETUP: badge becomes REC after start
    [FAIL] A6.1: badge text is '' (NOT 'ERR') after user-stop
           expected: ""
           actual:   "ERR"
    [FAIL] A6.2: popup is '' (NOT manifest default) after user-stop
           expected: ""
           actual:   "chrome-extension://<id>/src/popup/index.html"
    [PASS] A6.3: NO recovery notification fired (count delta === 0)
    [PASS] A6.4: isRecording=false (via badge proxy)

  UAT harness: 6/14 assertions passed (bailed: A6 failed; see above)

Step 3 — revert local patch (git checkout -- src/background/index.ts).

Step 4 — npm run build:test && npm run test:uat GREEN snippet:

  A6 — BUG B canonical: user-stopped-sharing routes via setIdleMode: PASS
    [PASS] SETUP: badge becomes REC after start
    [PASS] A6.1: badge text is '' (NOT 'ERR') after user-stop
    [PASS] A6.2: popup is '' (NOT manifest default) after user-stop
    [PASS] A6.3: NO recovery notification fired (count delta === 0)
    [PASS] A6.4: isRecording=false (via badge proxy)

  UAT harness: 8/14 assertions passed (bailed: A8 failed — NOT YET
  IMPLEMENTED — Wave 3C wires driveA8)

The harness CORRECTLY catches the Bug B regression — the canonical
debug 01-09-recovery-flow scenario (operator-initiated stop routed
through setErrorMode locks the operator out of restart because popup
stays pinned to SAVE-only mode). Bug B is now CI-callable end-to-end.

vitest 93/93 GREEN throughout (unit-test layer unaffected). Tier-1
grep gate GREEN (9 forbidden hook strings: 0 occurrences in dist/).
npm run build exit 0; npx tsc --noEmit exit 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:01:06 +02:00
1b67b1c1d3 feat(01-13): wave-3A — A1+A2+A3+A4 GREEN + harness.test.ts orchestrator (5/14 assertions GREEN)
Wave 3A landed. `npm run test:uat` now exercises 5/14 assertions
end-to-end (A0 + A1 + A2 + A3 + A4); bails at A5 NOT YET IMPLEMENTED
(Wave 3B scope). A6 still PASSES 5/5 through the standalone
`npx tsx tests/uat/a6.test.ts` entry — the orchestrator-level A6 won't
reach in Wave 3A because the sequential loop bails at A5; once Wave 3B
wires driveA5 the loop will fall through to A6 (which uses the proven
Wave-2 driveA6 driver — no rework needed there).

Files changed:

- `tests/uat/extension-page-harness.ts` — extends `window.__mokoshHarness`
  from `{ assertA6 }` to `{ assertA1, assertA2, assertA3, assertA4,
  assertA6 }`. Per-assertion contracts:
  • A1 — chrome.action.getBadgeText({}) === '' + getPopup({}) === ''
    + isRecording=false (badge !== 'REC' proxy per state-machine atomic
    pairing). 3 CheckRecords.
  • A2 — ensureOffscreen + START_RECORDING direct-to-offscreen
    (workaround for the `tabs` manifest permission gap per
    01-11-SUMMARY + plan resolved-questions row 2) + manual
    setBadgeText('REC') + setPopup(POPUP_HTML_PATH) + waitFor
    badge==='REC'. The bypassed chrome.action.onClicked →
    startVideoCapture path is unit-tested in
    tests/background/badge-state-machine.test.ts; A2 verifies the
    contract that matters (recording reaches the REC state-machine
    row). 2 CheckRecords.
  • A3 — offscreen bridge query 'get-display-surface' (new in this
    plan via the prior commit's offscreen-hooks extension) → asserts
    === 'monitor'. 1 CheckRecord.
  • A4 — getPopup remains 'src/popup/index.html' + hasDocument()===true
    (no duplicate offscreen). Essentially a no-op verification —
    regression protection against future refactors that might unpin
    the popup during recording or spawn extra offscreens on stray
    events. 2 CheckRecords.
  • IMPORTANT: chrome.action.getPopup() returns the FULL absolute
    chrome-extension://<id>/... URL (not the manifest-relative path).
    A2.2 + A4.1 assert via .endsWith('src/popup/index.html') to stay
    extension-id independent. Empirical finding from first orchestrator
    run; documented inline.

- `tests/uat/lib/harness-page-driver.ts` — wires `driveA1/A2/A3/A4`
  (replaces the 4 NOT YET IMPLEMENTED Wave-3A stubs from
  eb64521). Each wraps a single page.evaluate(() =>
  window.__mokoshHarness.assertXX()) call per the contract laid down
  by driveA6. A5+A7..A13 remain stubbed for Waves 3B+3C+3D.

- `tests/uat/harness.test.ts` (NEW) — top-level UAT orchestrator
  driving all 14 assertions sequentially against a single Chrome +
  single harness page. A0 (Tier-1 grep gate) runs pre-flight before
  any Chrome launch — mirrors
  tests/background/no-test-hooks-in-prod-bundle.test.ts forbidden-
  string inventory (9 entries; belt-and-suspenders per
  feedback-pre-checkpoint-bundle-gates.md memory). Bail-on-first-
  failure with [SKIP] markers for unreached assertions + structured
  diagnostic dump (full SW + offscreen console tail) on each failure.
  SKIP_PROD_REBUILD=1 escape hatch skips the A0-side `npm run build`
  for developer iteration.

Verification (all GREEN):
  - npx tsc --noEmit: clean (root)
  - npx tsc --noEmit -p tests/uat: clean (UAT subtree)
  - npm run build: clean; production bundle hook-free
    (9-string grep gate in vitest unit gate)
  - npm run build:test: clean; dist-test/assets/extension_page_harness-*.js
    grew from 3.87kB → 7.67kB (A1+A2+A3+A4 added)
  - SKIP_BUILD=1 npx vitest run: 93/93 GREEN
    (Wave 0+1+2 baseline 92 + 1 from the 9th grep-gate string from
    the prior commit; this commit adds zero new vitest tests — the
    A1-A4 contracts are verified at UAT-harness time only)
  - npx tsx tests/uat/a6.test.ts (standalone): 5/5 GREEN; exit 0
    (Wave-2 A6 baseline preserved through orchestrator-adjacent
    harness page surface extension)
  - npm run test:uat (full operator entry): 5/14 GREEN
    (A0 + A1 + A2 + A3 + A4); bails at A5 NOT YET IMPLEMENTED
    (Wave 3B scope, expected). Total wall clock ~25s (~5s build +
    ~5s prod-rebuild for A0 + ~15s assertion sequence).

Operator empirical-verification deferred to orchestrator (per
feedback-pre-checkpoint-bundle-gates.md — the orchestrator runs SW
CSP-safety + Node-globals + DOM-globals grep on the built bundle
before surfacing any checkpoint).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:45:25 +02:00
2f1b1f36a7 feat(01-13): wave-3A — add get-display-surface bridge op (A3 prereq) + extend Tier-1 grep gate
Scope: prerequisite step for Wave 3A's A3 assertion (displaySurface=monitor
verification). The page→offscreen bridge gains a new op so the harness can
query the active stream's `getSettings().displaySurface` without needing
direct offscreen.evaluate access (impossible by-construction; the only
cross-isolate path is chrome.runtime.sendMessage).

Bridge op contract (`src/test-hooks/offscreen-hooks.ts`):
  - Protocol: { type: '__mokoshOffscreenQuery', op: 'get-display-surface' }
  - Response: { displaySurface: string|null }
    • null when no current stream (recording not active)
    • 'monitor' when installFakeDisplayMedia's monkey-patched
      getSettings() reports it (production code in
      src/offscreen/recorder.ts enforces this same value — tears down
      stream + throws 'wrong-display-surface' otherwise).
  - Failure: { ok: false, error: <message> } only on getSettings throw.

Tier-1 grep gate extension (`tests/background/no-test-hooks-in-prod-bundle.test.ts`):
  - FORBIDDEN_HOOK_STRINGS: 8 → 9 entries.
  - Added: 'get-display-surface' (the literal bridge-op string;
    matches the production-bundle absence invariant — the offscreen-hooks
    module is tree-shaken in production builds by the Vite mode gate in
    src/offscreen/recorder.ts top-of-module).

Verification:
  - npx tsc: clean
  - npm run build: clean (dist/ 4 chunks; no offscreen-hooks artifact)
  - npm run build:test: clean (dist-test/ adds offscreen-hooks-DfWtG71P.js, 2.38kB)
  - SKIP_BUILD=1 vitest run no-test-hooks-in-prod-bundle.test.ts → 10/10 GREEN
    (1 build-sanity + 9 forbidden-string checks; production bundle hook-free)
  - SKIP_BUILD=1 vitest run (full) → 93/93 GREEN
    (Wave 0+1+2 baseline 92 + 1 from the 9th grep-gate string)
  - npx tsx tests/uat/a6.test.ts → A6 5/5 GREEN
    (lib-driven path preserved; bridge op addition does not interfere)

Wave 3A continuation: assertA1/A2/A3/A4 land in the next commit which
wires the harness-page surface + driver wrappers + harness.test.ts
orchestrator. This commit is the bridge prerequisite — keeping the
bridge-op extension atomic + the grep-gate extension atomic so the
'production bundle hook-free' invariant is provable BEFORE the page-side
surface lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:33:35 +02:00
eb64521321 feat(01-13): wave-2 — launchHarnessBrowser + assertions + harness-page-driver scaffolding
Build out the Approach-B harness driver utilities atop the Wave 1
production paths. Three new files form the shared scaffold that
Wave 3's 13 assertion drivers (A1-A5, A7-A13) and the eventual
orchestrator (`tests/uat/harness.test.ts`) will all consume. The
standalone A6 driver (`tests/uat/a6.test.ts`) is rewritten to use
the new lib — behavior-preserving: A6 still PASSES 5/5 in ~7s.

New files:

  - tests/uat/lib/launch.ts (~320 LoC)
      `launchHarnessBrowser({ headless?, downloadsDir? }) → HarnessHandles`
      Extracts the Chrome-launch + victim-page + harness-page + console-
      attach pattern from a6.test.ts into a single reusable helper.
      NEW vs prototype: CDP `Browser.setDownloadBehavior` wires
      Chrome's download path to a per-run `mkdtempSync` tmp dir so A5
      (SAVE_ARCHIVE) can poll a known location without colliding with
      the operator's real downloads. Architectural commitments
      enforced (per 01-11-SUMMARY): no `--auto-select-desktop-capture-
      source` flag; victim about:blank brought to front for the
      production `chrome.tabs.query({active:true})` workaround; SW
      console attach best-effort with bounded poll; offscreen console
      attach opportunistic via `targetcreated` listener (offscreen
      target appears later, when the harness page calls
      chrome.offscreen.createDocument).

  - tests/uat/lib/assertions.ts (~210 LoC)
      Host-side assertion primitives:
        * `AssertionRecord`, `CheckRecord`, `ConsoleBuffers` types —
          mirror the page-side shape returned by `assertA*` methods.
        * `runAssertion(name, fn, buffers)` — try/catch wrapper that
          dumps the SW + offscreen console tails (last 100 lines each)
          to stderr on failure, then returns `{passed: false, error}`
          if `fn` throws.
        * `printAssertionResult(result)` — single source of truth for
          the formatted result print. Extracted from the inline
          `printResult` previously in the prototype's a6.test.ts so
          Wave 3's orchestrator can reuse it across all 14 assertions.
        * `assertEqual / assertGte / assertMatch / assertTrue` —
          structured failure messages atop node:assert/strict.
        * `waitFor(probe, predicate, timeoutMs, description)` — host-
          side polling primitive; mirrors the page-side waitFor
          semantics verbatim (they can't share a module: page-side is
          bundled into the harness HTML, host-side runs in Node).
      NO chrome.* helpers here — all chrome.* work happens inside the
      extension-internal harness page. This module is host-side ONLY
      by construction (no chrome global in Node anyway).

  - tests/uat/lib/harness-page-driver.ts (~170 LoC)
      One driver wrapper per assertion (A1..A13). Each wraps a single
      `page.evaluate(() => window.__mokoshHarness.assertXX())`.
      Centralizing this means adding/renaming an assertion = two-file
      edit (extension-page-harness.ts impl + this file) instead of
      touching every test-file caller.
      Wave 2 wires `driveA6` (proven from c647f61). The 12 Wave-3
      drivers (driveA1..A5, A7..A13) are stubbed as
      `throw new Error('NOT YET IMPLEMENTED — Wave 3<X> wires driveXX')`
      so the future orchestrator's `for (const drive of drivers)` loop
      fails cleanly on the first unimplemented one (bail-on-first-
      failure semantics). The `AssertionWithBytes` type is declared
      for A5/A12/A13 which return `bytesBase64` payloads (zip / webm
      bytes that the host side processes after the page-side
      assertion completes).

Rewrite — `tests/uat/a6.test.ts`:
  - Drops ~80 LoC of Chrome-launch + console-attach + result-print
    plumbing now living in lib/launch.ts + lib/assertions.ts.
  - Now ~70 LoC total — pure orchestration of
    launchHarnessBrowser → runAssertion(driveA6) → printAssertionResult
    → browser.close() → exit code.
  - Behavior-preserving: A6 still 5/5 GREEN with the same diagnostic
    output (SETUP, A6.1-A6.4) and the same ~7s end-to-end runtime.

Verification (all GREEN):
  - `npx tsc --noEmit` — exit 0 (root + tests/uat/tsconfig.json).
  - `npx tsx tests/uat/a6.test.ts` — exits 0 with "PASS"; 5 checks
    GREEN (SETUP, A6.1, A6.2, A6.3, A6.4). End-to-end runtime ~7s
    headless on this workstation.
  - `npm run build` — exit 0; Tier-1 grep gate GREEN (production
    bundle contains zero hook strings AND zero lib symbol names —
    the new lib files are test-only and not bundled into dist/).
  - `npm run build:test` — exit 0; dist-test/ still emits the
    extension-page-harness.html harness (lib files are host-side,
    not rollup inputs).
  - `npx vitest run` — 92/92 GREEN.

Wave 3 ready: harness-page-driver.ts has driveA1..A5/A7..A13 stubs
in place; extending requires only:
  1. Add `assertAXX` method to window.__mokoshHarness in
     tests/uat/extension-page-harness.ts.
  2. Replace the corresponding stub body in this file with the
     page.evaluate wrapper.
  3. (Wave 3A) Create tests/uat/harness.test.ts orchestrator that
     iterates over [A0 grep gate, driveA1..A13] with bail-on-fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:21:11 +02:00
eb2258a880 feat(01-13): wave-1 — promote c647f61 prototype to production paths; A6 GREEN
Move the three load-bearing prototype files from `tests/uat/prototype/`
to their production paths under `tests/uat/`, leaving the architectural
narrative (research findings, BLOCKER citations, falsification table
references) intact. No behavioral changes — A6 still PASSES 5/5 in ~7s
end-to-end from the new paths.

File moves (git mv preserves history):
  - tests/uat/prototype/extension-page-harness.html
      → tests/uat/extension-page-harness.html
  - tests/uat/prototype/extension-page-harness.ts
      → tests/uat/extension-page-harness.ts
  - tests/uat/prototype/a6.test.ts
      → tests/uat/a6.test.ts

The `tests/uat/prototype/` directory is now empty (git does not track
empty directories; will not appear in subsequent `git status`).

Path-reference updates inside the moved files:
  - tests/uat/extension-page-harness.html: `<p>` line referencing the
    chrome-extension:// URL updated to drop `/prototype/`.
  - tests/uat/extension-page-harness.ts: file-header docstring rewritten
    to cite Plan 01-13 / Approach B / inheritance from c647f61. The
    load-bearing architectural-finding comment block (MV3 SW dynamic-
    import block falsification, Approach-B chrome.* surface summary)
    is REWORDED but its semantic content + research citations are
    PRESERVED — every load-bearing fact survives the rename.
  - tests/uat/a6.test.ts:
      * File-header rewritten to position the file as Plan 01-13's
        standalone single-assertion entry point (preserves the future-
        proof rationale: this entry stays around forever for fast TDD
        iteration on A6 even after Wave 3 folds A6 into the orchestrator
        harness.test.ts).
      * REPO_ROOT resolvePath chain corrected from `..,..,..` to `..,..`
        — the file is now two directory levels above the repo root
        instead of three. Without this fix DIST_TEST_DIR would resolve
        to a path one level above the actual repo root and
        assertBundlePresent would throw. **VERIFIED by running the
        driver: build path resolves correctly.**
      * harnessUrl constant updated to drop `/prototype/` from the
        chrome-extension://<id>/tests/uat/extension-page-harness.html
        URL — must match the rollup emission path in dist-test/.
      * Stdout labels updated: 'PROTOTYPE A6 result' → 'A6 result',
        'Plan 01-11 PROTOTYPE — A6 ... feasibility test' → 'Plan 01-13
        — A6 (Bug B canonical) standalone driver'. Inside the docstrings
        the historical 'originally landed as 01-11 prototype' provenance
        is preserved per the plan's contract.

vite.test.config.ts:
  - `rollupOptions.input` renamed `prototype_harness` → `extension_page_harness`
    pointing at the new production path. crxjs emits the harness HTML
    to `dist-test/tests/uat/extension-page-harness.html` (verified by
    `ls dist-test/tests/uat/`).
  - The `modulePreload: { polyfill: false }` line is PRESERVED — this
    is the CRITICAL SW FIX per 01-11-SUMMARY (disabling the polyfill
    is what makes the test bundle's offscreen-side dynamic import work
    without crashing in non-DOM contexts that incorrectly try to call
    document.querySelector).
  - File-header comment §4 and the inline `define.__MOKOSH_UAT__` comment
    are PRESERVED — load-bearing rationale for the dedicated build-time
    token (vs `import.meta.env.MODE === 'test'` which collides with
    vitest).

Verification (all GREEN):
  - `npm run build:test` — exit 0; dist-test/ emits
    `tests/uat/extension-page-harness.html` and `assets/extension_page_harness-*.js`.
  - `npx tsx tests/uat/a6.test.ts` — exits 0 with "A6 result: PASS";
    5/5 checks GREEN (SETUP: badge becomes REC; A6.1 badge==''; A6.2
    popup==''; A6.3 notif delta==0; A6.4 isRecording=false). End-to-end
    runtime ~7s headless on this workstation.
  - `npx tsc --noEmit` — exit 0 (root tsconfig + tests/uat/tsconfig.json).
  - `npx vitest run` — 92/92 GREEN; the moves do not touch any vitest-
    discovered files.
  - `npm run build` — exit 0; Tier-1 grep gate stays GREEN
    (the moves do not touch production code).

Wave 2 (next): build out `tests/uat/lib/{launch,assertions,harness-page-
driver}.ts` around the extension-page architecture; rewrite
`tests/uat/a6.test.ts` to use the shared lib (still PASSES 5/5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 15:01:58 +02:00
a63066a289 chore(01-13): wave-0 — clean broken Approach-A artifacts per 01-11-SUMMARY
Restore a clean baseline before promoting the c647f61 prototype to
production paths (Wave 1) and building out Approach-B driver
scaffolding (Wave 2). All deletions trace back to falsifications
documented in 01-11-SUMMARY.md.

Deleted — broken Approach-A files:
  - src/test-hooks/sw-hooks.ts
      MV3 SW blocks dynamic import (Chromium es_modules.md;
      w3c/webextensions#212). The gated `await import('../test-hooks/
      sw-hooks')` from 01-11 Wave 1 never resolved → SW silently died →
      production listeners never registered. File was dead-on-arrival;
      no fix possible while MV3 SWs disallow dynamic import. Approach-B
      replaces SW-side instrumentation with the extension-internal
      harness page's chrome.action.* + chrome.notifications.* surface
      (full privilege; no monkey-patching needed).

  - tests/uat/lib/{launch,extension,sw,offscreen,assertions}.ts
      Popup-bridge architecture (01-11 dbd977c) — falsification 2 +
      falsification 3 in 01-11-SUMMARY: `sw.evaluate` exposes only
      chrome.{loadTimes,csi}, NOT chrome.action.* / chrome.notifications.*
      / chrome.runtime.sendMessage; setPopup-juggling for extension-id
      resolution turned out to be unnecessary (browser.extensions()
      works directly per the prototype). These files will be reborn in
      Wave 2 around the extension-page architecture.

      Kept: tests/uat/lib/zip.ts (host-side JSZip work — architecture-
      agnostic; A12+A13 still use it) and tests/uat/lib/test-hook-
      contract.d.ts (type mirror — extended in Wave 3 but kept as-is here).

  - tests/uat/prototype/probe_{offscreen,sw,tabs,tabs2}.mjs
      Feasibility-research probes (01-11 spike) that empirically falsified
      the Approach-A hypotheses. The findings are encoded in 01-11-
      SUMMARY.md; the probes themselves are dead code.

  - tests/uat/harness.test.ts
      01-11 Wave 2 popup-bridge orchestrator (dbd977c). Imports the
      now-deleted tests/uat/lib/{assertions,extension,sw,offscreen,launch}
      modules — would not typecheck after this commit. Reborn in
      Wave 3A as the Approach-B orchestrator (extension-internal page
      driver + A0 grep gate + 13 assertion drivers).

Reverted — SW-side dynamic-import gate comment block:
  - src/background/index.ts lines 13-29
      The existing comment block (post-spike) described the SW-side
      gated dynamic import that never landed. Rewritten to cite 01-13
      Approach-B explicitly, link to 01-11-SUMMARY.md falsification,
      and clarify that the Tier-1 grep gate's enduring value is
      catching regressions in the offscreen chunk's __MOKOSH_UAT__
      gate (the SW chunk is hook-free by construction).

Updated — Tier-1 grep gate FORBIDDEN_HOOK_STRINGS inventory:
  - tests/background/no-test-hooks-in-prod-bundle.test.ts
      Removed: `simulateUserStop` (Approach-A naming; replaced by
      Approach-B `dispatchEndedOnTrack` which matches the W3C
      dispatchEvent semantics per RESEARCH §7 BLOCKER — track.stop()
      does NOT fire 'ended' per spec, so the simulation MUST use
      dispatchEvent).
      Added: `installFakeDisplayMedia`, `uninstallFakeDisplayMedia`,
      `dispatchEndedOnTrack`, `__mokoshOffscreenQuery`.
      Total inventory: 8 surface strings (was 5). Each MUST be absent
      from every file under dist/ post-build.

Verification (all GREEN):
  - `npm run build` — exit 0; dist/ populated.
  - `grep -rln <forbidden> dist/` — 0 matches.
  - `npm run build:test` — exit 0; dist-test/ populated; offscreen-hooks
    chunk contains `installFakeDisplayMedia` (gate runs correctly
    against the test build's distinct artifact).
  - `npx tsc --noEmit` — exit 0 (root + tests/uat/tsconfig.json).
  - `npx vitest run` — 92/92 tests passing (was 89; the +3 new tests
    come from the FORBIDDEN_HOOK_STRINGS list expanding 5 → 8 — each
    forbidden string is one parametric `it(...)` block).

Both prior-failing tests now GREEN:
  - tests/background/sw-bundle-import.test.ts (was missing dist/ → 92/92
    requires the test run to have a current dist/; vitest gate test
    rebuilds via execFile when SKIP_BUILD≠1, otherwise relies on prior
    `npm run build`).
  - tests/background/no-test-hooks-in-prod-bundle.test.ts (was failing
    on stale dist; now GREEN against the freshly-rebuilt clean bundle).

Wave 1 (next): promote tests/uat/prototype/{extension-page-harness.html,
extension-page-harness.ts,a6.test.ts} to tests/uat/ via `git mv`;
update vite.test.config.ts rollup input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:54:41 +02:00
70f4f4136a docs(01-13): create UAT harness plan — Approach B (extension-internal page)
5 waves, 9 tasks. Inherits Plan 01-11 spike-pivot rationale per
01-11-SUMMARY (commit ba5474c). Implements full 14-assertion harness
via Approach B architecture, proven by prototype c647f61.

- Wave 0: clean broken Approach-A artifacts (sw-hooks.ts, SW dynamic
  import, popup-bridge lib, feasibility probes); update Tier-1 grep
  gate to 10-string Approach-B forbidden inventory.
- Wave 1: promote c647f61 prototype (extension-page-harness +
  a6.test.ts) to production paths; A6 stays GREEN.
- Wave 2: rebuild Approach-B driver utilities (launch.ts,
  assertions.ts, harness-page-driver.ts) replacing deleted
  popup-bridge primitives.
- Wave 3 (4 task bundles): wire A1-A13 functional assertions; canonical
  Bug B (A6) + Bug A (A8) RED-on-regression demos mandatory in commit
  bodies.
- Wave 4: append 01-09 Amendment 2; update STATE.md + ROADMAP.md;
  operator brand/design checkpoint.

Open questions resolved: Wave 3 granularity = 4 bundles; tabs
permission gap = workaround retained (Phase 5 hardening); failure
isolation = single browser + bail-on-first; CI plumbing = defer.

Frontmatter validation: valid=true. Plan structure: valid=true,
task_count=9, all tasks have files/action/verify/done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:28:04 +02:00
ba5474c54f docs(01-11): close as spike-pivot — SUMMARY landed, AMENDMENT-A deleted, pivots to 01-13
Closes Plan 01-11 honestly per GSD spike-pivot pattern. Original
Approach A (Puppeteer sw.evaluate per RESEARCH §1+6) empirically
falsified across Wave 3 execution + feasibility research. Approach B
(extension-internal-page harness + offscreen synthetic stream) proven
via c647f61 prototype; full implementation moves to Plan 01-13.

What this commit does:
- ADDS 01-11-SUMMARY.md (spike-then-pivot framing per GSD artifact-
  types.md PLAN→SUMMARY lifecycle; captures retained infrastructure,
  falsified hypotheses, working prototype, bridge to 01-13)
- REVERTS frontmatter amendment block in 01-11-PLAN.md; replaces with
  closed_as/pivoted_in/closure_note pointing at SUMMARY + 01-13
- DELETES 01-11-PLAN-AMENDMENT-A.md (improvised artifact type — not
  recognized in GSD artifact-types.md; content folded into SUMMARY)

Lesson for orchestrator (captured in SUMMARY §Architectural Notes):
when a plan attempts an approach that proves infeasible, the right
move is honest SUMMARY + new plan, NOT in-place rewrite + AMENDMENT
artifact. The project's own pattern (01-08, 01-09, 01-10, 01-11
added mid-phase as new work surfaced) confirms add-new-plan-when-
scope-shifts is the established pattern.

Plan 01-09 closure via harness PASS NOT achieved by 01-11; still
requires operator UAT pending Plan 01-13 landing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:02:38 +02:00
565f8fa44c docs(01-11): amendment A — pivot to extension-internal harness page
Architectural pivot triggered by feasibility research prototype
(commit c647f61, A6 PASS 5/5 + Bug-B regression rewind verified).

Two empirical findings invalidate original architecture:
1. MV3 service workers BLOCK dynamic import. await import('test-hooks/
   sw-hooks') in src/background/index.ts silently kills the SW —
   chunk loads, await never resolves, no listeners register. Cited
   Chromium es_modules.md + w3c/webextensions#212.
2. Puppeteer WebWorker.evaluate against MV3 SW only exposes chrome.
   {loadTimes,csi} — not the extension chrome.* API surface.

The Wave 1 (cb1a729) SW-side hooks are fundamentally broken in test
builds (production unaffected — gated by __MOKOSH_UAT__ which is
false in prod). Executor must DELETE the SW-side dynamic import +
sw-hooks.ts entirely; offscreen-side hooks stay (offscreen IS a DOM
document; dynamic import works there).

Replacement (Verdict-A architecture, proven by prototype):
- Extension-internal harness page at chrome-extension://<id>/tests/
  uat/extension-page-harness.html — privileged extension context
  with FULL chrome.* API access
- Puppeteer drives the page via page.goto + page.evaluate
- For SW state: page calls chrome.runtime.sendMessage; SW responds
  via production messaging
- For getDisplayMedia: offscreen-side installFakeDisplayMedia() patches
  navigator.mediaDevices.getDisplayMedia → Canvas captureStream
  synthetic MediaStream

A6 (Bug B regression catch) PROVEN. Industry-standard pattern (MetaMask,
eyeo, Chrome MV3 official testing docs all converge).

Effort remaining: ~7-10h subagent budget (Wave 0 + bonus debug-import
commits keep; Wave 1 hooks rewire 30min; Wave 2 scaffolding 1-2h;
Wave 3 13 more assertions 4-6h; Wave 4 closure 1h).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 12:10:52 +02:00
c647f61553 wip(01-11): prototype — A6 via test-page+bridge+synthetic-stream PASSES
Plan 01-11 orchestrator commissioned a research+prototype investigation
into whether full MV3 UAT automation is feasible with the architecture:
extension-internal test page + chrome.runtime.sendMessage bridge +
synthetic MediaStream (canvas-captureStream + getSettings override).

EMPIRICAL VERDICT: feasible BUT plan 01-11 needs architectural revision.

Architectural findings (with proof):

1. DYNAMIC IMPORT BLOCKED IN MV3 SW. Top-of-module
   `await import('../test-hooks/sw-hooks')` in src/background/index.ts
   silently kills the SW (chunk loads, await never resolves, no
   production listeners register, no console output). This is by design
   per Chromium docs (es_modules.md) + w3c/webextensions#212. The Plan
   01-11 RESEARCH §6 architecture was wrong for the SW side.
   Workaround in this prototype: REMOVE the SW-side gated dynamic
   import. SW-side test hooks need a different design (see verdict).

2. OFFSCREEN-SIDE DYNAMIC IMPORT WORKS. Offscreen is a DOM document,
   not a SW, so top-level await + dynamic import behave normally. The
   offscreen-hooks.ts gated import succeeds; installFakeDisplayMedia is
   installed eagerly at module load.

3. EXTENSION-INTERNAL PAGE HAS FULL chrome.* SURFACE. Reachable via
   chrome-extension://<id>/tests/uat/prototype/extension-page-harness.html
   (added as rollup input in vite.test.config.ts). The page can call
   chrome.action.getBadgeText, chrome.action.getPopup, chrome.offscreen
   .createDocument, chrome.notifications.getAll, chrome.runtime
   .sendMessage — everything needed for A6.

4. NO 'tabs' PERMISSION → tab.url IS UNDEFINED. Production
   startVideoCapture's `chrome.tabs.query({active:true})` check
   (`if (!tab.id || !tab.url) throw`) fails because the manifest lacks
   the 'tabs' permission. Prototype workaround: bypass startVideoCapture
   by sending START_RECORDING directly to offscreen. The Bug B
   contract being tested is independent of how recording starts; it
   only depends on the RECORDING_ERROR routing path.

5. SYNTHETIC MEDIASTREAM WORKS. installFakeDisplayMedia builds a
   canvas-captureStream MediaStream + monkey-patches the video track's
   getSettings() to report displaySurface: 'monitor'. Production code's
   post-grant validation passes. getDisplayMedia returns the synthetic
   stream immediately — no picker, no headless flakiness.

A6 prototype result (with Bug B fix in place — current HEAD state):
  [PASS] SETUP: badge becomes REC after start
  [PASS] A6.1: badge text is '' (NOT 'ERR') after user-stop
  [PASS] A6.2: popup is '' (NOT manifest default) after user-stop
  [PASS] A6.3: NO recovery notification fired (count delta === 0)
  [PASS] A6.4: isRecording=false (via badge proxy)

A6 prototype result (with Bug B fix rewound to `if (false)`):
  [PASS] SETUP: badge becomes REC after start
  [FAIL] A6.1: badge text is '' (got "ERR")
  [FAIL] A6.2: popup is '' (got chrome-extension://.../popup/index.html)
  [FAIL] A6.3: notif delta = 0 (got 1)
  [PASS] A6.4: isRecording=false  ← false-positive (badge='ERR' not 'REC')

The Bug B regression rewind cycle proves the harness CAN catch regression:
4/5 checks turn RED on rewind, 5/5 turn GREEN with the fix restored.

Files in this commit:
- tests/uat/prototype/extension-page-harness.{html,ts} — the harness
  page (chrome-extension URL, exposes window.__mokoshHarness.assertA6)
- tests/uat/prototype/a6.test.ts — Puppeteer driver (~270 lines)
- tests/uat/prototype/probe_*.mjs — diagnostic probes used to isolate
  the SW dynamic-import blocker (probe_sw.mjs is the key one)
- src/test-hooks/offscreen-hooks.ts — added installFakeDisplayMedia +
  dispatchEndedOnTrack + __mokoshOffscreenQuery bridge handler + auto-
  install at module load
- vite.test.config.ts — added prototype harness page as rollup input;
  added modulePreload.polyfill=false (red herring; harmless)
- src/background/index.ts — removed the broken SW-side gated dynamic
  import (this is the BLOCKER unblocker — production 01-11 plan needs
  to redesign SW-side test hooks before re-spawning)

Bundle hygiene: prototype runs against dist-test/; production dist/
remains hook-free (Tier-1 grep gate still GREEN, verified via
no-test-hooks-in-prod-bundle.test.ts in the unit test suite).

Vitest baseline: 89/89 GREEN preserved.

Runtime: ~7 seconds end-to-end (launch Chrome + open page + ensure
offscreen + start recording + dispatch ended + settle + assert).

See: research return for VERDICT + recommended next step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 12:06:08 +02:00
f44ca3afba wip(01-11): wave-3 partial — A1+A4 attempted, popup-bridge SW state query unreliable
Task 4 of Plan 01-11 attempted A1-A4 wiring. Empirical run reveals an
architectural blocker that needs orchestrator-level decision.

Current state after this commit (SKIP_PROD_REBUILD=1 npx tsx tests/uat/harness.test.ts):
- A0 [PASS]: production bundle hook-leak grep gate (17ms)
- A1 [FAIL]: SW bootstrap → setIdleMode — popup state never transitions
  to '' despite keepalive ping + 3s waitFor. chrome.action.getPopup({})
  from the popup page consistently returns the manifest default
  (chrome-extension://<id>/src/popup/index.html), not the '' that
  setIdleMode's chrome.action.setPopup({popup:''}) should produce.
- A2 [FAIL]: toolbar onClicked — badge never transitions to "REC" after
  page.triggerExtensionAction(extension); 8s timeout. Either the
  toolbar action isn't reaching the SW listener, OR getDisplayMedia's
  picker isn't resolving in headless mode (despite the auto-select flag).
- A3 [FAIL]: offscreen target never appears (correlates with A2 — no
  recording started, no offscreen document spawned).
- A4 [PASS]: trivially passes (offscreen count is 0 → 0, both before
  + after the click). Not a true assertion of behavior; would also pass
  if the whole extension were broken.
- A5-A13: stubbed RED per plan.

Architectural blocker (Rule 4 — needs orchestrator decision):
- Puppeteer 25.0.2 + Chrome 148 + headless cannot reliably keep the MV3
  SW alive long enough OR expose its real chrome.* state to a popup
  page query. The popup-bridge architecture (Task 3 commit dbd977c)
  works for synchronous bridge queries (snapshot, fire-on-startup)
  but does NOT reliably reflect chrome.action.setPopup / setBadgeText
  state changes initiated by the SW.

Three plausible paths forward (need orchestrator pick):

  Option A — Content-script bridge: inject a content script that
    bridges chrome.* queries to a webpage's window.* RPC surface;
    harness uses page.evaluate against the content script instead of
    popup.evaluate. Pros: content scripts have stable lifetime tied to
    the page they're injected into. Cons: content scripts have
    DIFFERENT chrome.* surface (no chrome.action API surface — they
    can't read getBadgeText / getPopup at all). Likely DOESN'T solve
    the underlying problem.

  Option B — Headful with Xvfb on CI: relax the headless requirement;
    accept Xvfb dependency. Per Plan 01-11 RESEARCH §3, RESEARCH
    claimed headless works on Chrome 148 — empirical refutation here.
    Pros: SW lifetime is more stable in headful mode; setPopup
    propagation is reliable. Cons: introduces Xvfb dep that RESEARCH
    explicitly said wasn't needed; CI complication.

  Option C — Shrink harness scope to bridge-able assertions: A0 (grep
    gate), A8 (Bug A onStartup via bridge), A9 (icon sizes via popup
    fetch), A10 (manifest via popup), A13 (zip shape — operator runs
    SAVE_ARCHIVE manually + drops zip to a known path; harness reads
    it). Skip A1-A7, A11, A12 (the ones that require live SW state
    observation through chrome.action API). Pros: ships the
    bug-A-coverage portion of the harness today; keeps Plan 01-09's
    Task 5 operator-checkpoint partly automated. Cons: doesn't retire
    operator entirely; Plan 01-09 stays open on operator-empirical
    A1-A7.

  Option D — Switch to WebDriver BiDi (the Puppeteer 25 alternative
    backend): Puppeteer 25 supports BiDi via {protocol: 'webDriverBiDi'}.
    BiDi may handle extension SW evaluation differently (different
    isolation model). Speculative — no empirical evidence either way.

What landed cleanly:
- Tier-1 hook-leak grep gate (T-1-11-01) GREEN: dist/ has zero
  __mokoshTest / simulateUserStop / getSegmentCount / setCurrentStream
  / setSegmentCountGetter / __mokoshTestQuery / __mokoshKeepalive
  occurrences after npm run build.
- Two-bundle infrastructure (dist/ vs dist-test/) operational.
- Bridge handler in sw-hooks.ts works for snapshot + fire-on-startup
  + handler-types ops (verified by no-hang on keepalivePing call).
- Existing 89-test vitest baseline preserved (no regression from any
  Wave 0/1/2/3 work).

Verification:
- npx tsc --noEmit (src/): exit 0
- npx tsc --noEmit -p tests/uat: exit 0
- npm run build: exit 0; dist/ hook-free
- SKIP_BUILD=1 npx vitest run: 89/89 GREEN
- SKIP_PROD_REBUILD=1 npx tsx tests/uat/harness.test.ts:
  2/14 passed (A0 + A4-trivially), 12 FAIL — non-zero exit as expected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:24:06 +02:00
dbd977c815 feat(01-11): wave-2 — Puppeteer harness scaffolding + A0 GREEN, popup-bridge architecture
Task 3 of Plan 01-11 (Puppeteer UAT harness).

Harness file tree (tests/uat/):
- harness.test.ts: tsx-runnable top-to-bottom harness entry point.
  Runs A0 inline (filesystem grep gate, abort-on-fail T-1-11-01),
  then launches Chrome + opens popup bridge + queries manifest, then
  iterates A1-A13 stubs. Each stub throws "NOT YET IMPLEMENTED —
  Plan 01-11 Task N wires this assertion". Exit code = 0 on full
  pass, 1 otherwise. Final line: "UAT harness: N/14 assertions passed".
- lib/launch.ts: launchHarnessBrowser() — wraps puppeteer.launch with
  enableExtensions:[dist-test/], headless default (HEADLESS=0
  override), --no-sandbox + --auto-select-desktop-capture-source flags.
  Polls browser.extensions() until the extension registers (empirically
  ~100ms but the first call right after launch returns Map(0)).
  Opens both a blank page (for triggerExtensionAction) AND the popup
  page (the bridge surface). Returns { browser, extension, extensionId,
  sw, downloadsDir, page, popup }.
- lib/extension.ts: waitForOffscreenTarget + attachToOffscreen +
  countOffscreenTargets. Offscreen attach uses target.type() ===
  'background_page' + .asPage() (NOT .page() — RESEARCH §4 Pitfall 1).
- lib/sw.ts: chrome.* state queries via the POPUP page handle (NOT
  the WebWorker handle — see architecture note below). getBadgeText,
  getPopup, getManifest, getIconSize, getIsRecording (side-channeled
  through badge text), fireOnStartup (via __mokoshTestQuery bridge),
  sendSyntheticRecordingError, getNotificationSnapshot (via bridge),
  keepalivePing (no-op message to wake SW for ~30s).
- lib/offscreen.ts: getDisplaySurface, simulateUserStop (the
  dispatchEvent('ended') path per RESEARCH §7 BLOCKER — DO NOT REFACTOR
  to track.stop()), getSegmentCount.
- lib/assertions.ts: runAssertion(idx, name, buffers, fn) wrapper —
  records pass/fail/duration; on failure dumps last 30 lines of SW
  + offscreen console buffers to stderr before rethrowing. assertEqual
  / assertMatch / assertTrue / assertGte / waitFor polling helper.
- lib/zip.ts: jszip-based assertArchiveShape + extractEntryToFile for
  assertions 12 + 13.
- README.md: runtime + local-debug + CI semantics + locale gotcha
  + dev-dep size note + assertion catalog table.
- tsconfig.json: per-tree type-check config (mirrors root tsconfig.json
  compiler options but includes the harness tree explicitly).

Architecture refinement (DEVIATION from RESEARCH §1 — Rule 1+3 inline fix):
- RESEARCH §1 sketched `sw.evaluate(() => chrome.action.getBadgeText({}))`
  as the chrome.* query path. Empirical probes during Task 3 execution
  against Puppeteer 25.0.2 + Chrome 148 + --headless=true revealed two
  blockers:
    1. Puppeteer's WebWorker.evaluate runs in an ISOLATED WORLD that
       carries SW globals (clients, registration, ...) but NOT the
       extension's full chrome.* API surface. Object.keys(chrome) inside
       sw.evaluate returns ["loadTimes","csi"] — the public webpage
       chrome, not the extension chrome.
    2. Chrome 148's headless mode aggressively suspends MV3 service
       workers; subsequent swTarget.worker() calls return
       "Protocol error: No target with given id found".
- WORKAROUND: open the popup page (chrome-extension://<id>/src/popup/
  index.html) as a separate Puppeteer Page. The popup has full
  chrome.* access (it's an extension context with same privileges as
  the SW) AND stable Puppeteer lifetime. For SW-globalThis state
  (__mokoshTest in the SW isolate, NOT in the popup), bridge via
  chrome.runtime.sendMessage. The popup sends
  { type: '__mokoshTestQuery', op: 'snapshot' | 'fire-on-startup' |
  'handler-types' }; the SW hook's onMessage handler responds.
- Bridge implementation added to src/test-hooks/sw-hooks.ts — registers
  AFTER the production listeners so it never intercepts production
  messages (__mokoshTest* type is unambiguously test-only). Tier-1
  grep gate (no-test-hooks-in-prod-bundle.test.ts) continues to enforce
  ZERO __mokoshTest occurrences in dist/ — the bridge handler is
  tree-shaken alongside the rest of the hook module via the
  __MOKOSH_UAT__ gate.

Other configuration changes:
- vitest.config.ts: exclude tests/uat/** from vitest discovery. The
  Puppeteer harness is invoked via `npm run test:uat` (not vitest);
  running it under vitest would try to launch real Chrome inside a
  vitest worker. The .test.ts suffix is retained for editor +
  naming-convention consistency with the rest of the tree.

Verification:
- npx tsc --noEmit (src/): exit 0
- npx tsc --noEmit -p tests/uat: exit 0
- npm run build: exit 0
- grep -rln '__mokoshTest|simulateUserStop|getSegmentCount|setCurrentStream|setSegmentCountGetter|__mokoshTestQuery|__mokoshKeepalive' dist/: ZERO matches
- npm run build:test: exit 0; dist-test/ populated with the new bridge code
- SKIP_BUILD=1 npx vitest run: 89/89 GREEN
- SKIP_PROD_REBUILD=1 npx tsx tests/uat/harness.test.ts:
  → A0 [PASS]: production bundle has no test-hook leaks (19ms)
  → Browser launches; popup opens; manifest read succeeds
  → A1-A13 [FAIL]: NOT YET IMPLEMENTED — Plan 01-11 Task N wires this
  → "UAT harness: 1/14 assertions passed, 13 failed (first failure: A1)"
  → Exit code: 1 (expected — 13 RED stubs intentional)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:14:58 +02:00
cb1a729962 feat(01-11): wave-1 — gated test hooks for SW + offscreen, dist/ stays hook-free
Task 2 of Plan 01-11 (Puppeteer UAT harness).

Test hook surface:
- src/test-hooks/types.ts: canonical MokoshTestSurface — handlers
  (onClicked, onStartup, notificationOnClicked), notificationCount,
  lastNotificationOptions<true>, notificationIds, getCurrentStream,
  getSegmentCount. globalThis.__mokoshTest ambient declaration.
- src/test-hooks/sw-hooks.ts: SW-side hook. Monkey-patches addListener
  on chrome.action.onClicked / chrome.runtime.onStartup / chrome
  .notifications.onClicked to capture handler refs while chaining to
  the original. Wraps chrome.notifications.create across all four
  overload shapes (id+options+cb, options+cb, id+options→Promise,
  options→Promise) to increment notificationCount, save
  lastNotificationOptions, push resolved id into notificationIds.
- src/test-hooks/offscreen-hooks.ts: offscreen-side hook. Exports
  setCurrentStream + setSegmentCountGetter; the recorder calls both
  inside startRecording after the mediaStream + segments assignments.
  getCurrentStream getter closes over the cell so the harness reads
  the live MediaStream for displaySurface inspection + 'ended'
  dispatch (Bug B BLOCKER per RESEARCH §7).
- tests/uat/lib/test-hook-contract.d.ts: manual harness-side mirror of
  MokoshTestSurface (decoupled from src/ to keep tests/ import-clean
  per RESEARCH §11 resolution 5; drift risk documented inline).

Production-side wires (gated by __MOKOSH_UAT__ token):
- src/background/index.ts top-of-module: `if (__MOKOSH_UAT__) { await
  import('../test-hooks/sw-hooks'); }`. MUST run before any chrome.*
  addListener call below — top-of-module placement satisfies this.
- src/offscreen/recorder.ts top-of-module: symmetric gated dynamic
  import + module-scoped testHooks reference.
- src/offscreen/recorder.ts inside startRecording (after mediaStream
  assignment): `if (__MOKOSH_UAT__) { testHooks?.setCurrentStream(stream);
  testHooks?.setSegmentCountGetter(() => segments.length); }`
- src/offscreen/recorder.ts inside onUserStoppedSharing (after
  mediaStream = null): `if (__MOKOSH_UAT__) { testHooks?.setCurrentStream(null); }`
  — T-1-11-05 (Repudiation: stale stream ref) mitigation.

Build-time token wiring:
- vite.config.ts: declares `define: { __MOKOSH_UAT__: 'false' }` (prod
  default) + bumps `build.target: 'es2022'` so the top-level await in
  the gated dynamic imports compiles (MDN: Chrome 89 / Edge 89 /
  Firefox 89 / Safari 15 support TLA; MV3 floor Chrome 88 is
  effectively Chrome 89+ in field — comfortably inside the envelope).
- vite.test.config.ts: overrides `define: { __MOKOSH_UAT__: 'true' }`
  so the test bundle has the hooks active.
- vitest.config.ts: declares `define: { __MOKOSH_UAT__: 'false' }` for
  vitest's own source-loading runs. CRITICAL — without this, vitest
  would throw `ReferenceError: __MOKOSH_UAT__ is not defined` when
  loading src/background/index.ts; OR if we'd used `import.meta.env.MODE
  === 'test'` (RESEARCH §6's initial guidance), vitest's default
  MODE='test' would have ACTIVATED the hooks under unit tests +
  clobbered every existing vi.fn() chrome.notifications.create mock.
  The dedicated `__MOKOSH_UAT__` token sidesteps both failure modes
  cleanly — a refinement on RESEARCH §6 documented in the comment
  preambles of all three configs.
- globals.d.ts: declares `__MOKOSH_UAT__: boolean` ambient so
  `npx tsc --noEmit` passes without per-file annotations.
- tsconfig.json: include adds `globals.d.ts`.

Notification options generic refinement:
- chrome.notifications.NotificationOptions is declared with a
  `<true | false>` generic distinguishing "create" (all required —
  true) from "update" (all optional — false). Plan 01-11's production
  code always uses the create shape; types.ts + sw-hooks.ts pin to
  `NotificationOptions<true>` so the harness reads iconUrl etc. as
  definitely-present.

Verification:
- npx tsc --noEmit: exit 0
- npm run build: exit 0
- grep -rln '__mokoshTest\|simulateUserStop\|getSegmentCount\|setCurrentStream\|setSegmentCountGetter' dist/:
  ZERO matches (Tier-1 gate stays GREEN)
- npm run build:test: exit 0; dist-test/ emits separate sw-hooks-*.js
  + offscreen-hooks-*.js chunks (the gated dynamic imports survive
  tree-shaking when __MOKOSH_UAT__ === true)
- grep -rln '__mokoshTest' dist-test/: 2 matches
  (assets/sw-hooks-*.js + assets/offscreen-hooks-*.js)
- SKIP_BUILD=1 npx vitest run: 89/89 GREEN
  (83 baseline + 6 Tier-1 hook-leak surfaces)
- sw-bundle-import.test.ts: GREEN (the gated dynamic import does not
  break production module init — the `if (false)` branch is never
  reachable so the await + import are dead code in dist/)

In-flight bugs auto-fixed (Rule 1 + Rule 3):
- Rule 3: original RESEARCH §6 plan called for `import.meta.env.MODE
  === 'test'` as the gate; switched to `__MOKOSH_UAT__` define-token
  after observing vitest contamination (vitest defaults MODE='test'
  → hooks activated under unit tests → 8 existing tests broke with
  "Cannot read properties of undefined (reading 'calls')" because the
  hook wrapper replaced vi.fn() mocks). Documented in the comment
  preambles of all three configs as a refinement on RESEARCH §6.
- Rule 3: esbuild rejected TLA against the default ES2020 target;
  bumped to es2022 (Chrome 89+ supports TLA per MDN — inside MV3
  envelope). Recorded in vite.config.ts preamble.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:46:26 +02:00
0cd50fde94 docs(debug): import Bug B recovery-flow debug record from prior session
Untracked file present at session spawn (per orchestrator pre-flight
intelligence). The Bug B debug investigation that produced commit
b9eeeeb (the conditional-routing fix in src/background/index.ts
RECORDING_ERROR handler that Plan 01-11 assertion 6 verifies) was
recorded under .planning/debug/resolved/01-09-recovery-flow.md but
never committed. Importing it now so the debug provenance is
preserved alongside Plan 01-11's harness coverage of the bug class.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:46:09 +02:00
96fa8e8e11 chore(01-11): wave-0 — install puppeteer + tsx, add vite.test.config + Tier-1 hook-leak grep gate
Task 1 of Plan 01-11 (Puppeteer UAT harness).

- npm install --save-dev puppeteer@^25.0.2 tsx@^4 @types/node
  resolved: puppeteer@25.x, tsx@4.22.1, @types/node@25.8.0
  pulls ~150MB Chromium binary at install time (T-1-11-03 — accepted,
  package-lock pins resolved hashes via @puppeteer/browsers).
- package.json scripts: add build:test + test:uat (per RESEARCH §10
  two-bundle orchestration); existing dev/build/preview/test untouched.
- vite.test.config.ts: extends ./vite.config.ts via mergeConfig with
  mode:'test' + build.outDir:'dist-test' + emptyOutDir:true. Verified
  npm run build:test produces dist-test/ in 7.93s; npm run build keeps
  producing dist/ in 7.67s (no clobber).
- tsconfig.json `include: ["src"]` already covers src/test-hooks/**/*
  via wildcard — no edit needed.
- tests/background/no-test-hooks-in-prod-bundle.test.ts: Tier-1 gate
  mirroring sw-bundle-import.test.ts's execFile pattern. Greps the
  BUILT dist/ tree for 5 forbidden hook surfaces (one `it` per surface
  for granular failure isolation): __mokoshTest, simulateUserStop,
  getSegmentCount, setCurrentStream, setSegmentCountGetter. All 5
  surfaces absent today (RED-then-GREEN polarity inverted — the gate
  is GREEN now and MUST stay GREEN after Task 2 lands the hooks).
  SKIP_BUILD=1 escape hatch for developer iteration.
- .gitignore: add dist-test/ (no point versioning generated test bundle).

Verification:
- npx tsc --noEmit: exit 0
- npm run build: exit 0; dist/ populated (375.37 kB SW chunk)
- npm run build:test: exit 0; dist-test/ populated (identical chunk sizes —
  the gated dynamic imports do not land until Task 2; this commit only
  proves the two-bundle plumbing)
- SKIP_BUILD=1 npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts:
  6/6 GREEN (1 build-sanity + 5 forbidden-surface)
- SKIP_BUILD=1 npx vitest run (full suite): 89/89 GREEN
  (83 baseline + 6 new Tier-1 surfaces = 89)

Working-tree cleanup: a stale 5.4 MB tests/fixtures/last_30sec.webm
(unrelated operator smoke regen present at session spawn) was stashed
before running the baseline — it caused the webm-playback test to time
out at 5s. After stashing back to HEAD's 1.9 MB fixture, baseline passes
cleanly. Not committing the fixture restoration here (pre-existing
working-tree state, not part of Task 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:42:46 +02:00
2669ce38e7 docs(intel): designer follow-up #1 — Newsreader has no Cyrillic glyphs
D-05 picked Newsreader as display serif. Engineering research
(01-12-RESEARCH.md §1, commit 3df2750) verified Newsreader ships NO
Cyrillic glyphs via designer's own Brief embedded subset list +
Production Type's repo README ("Google Fonts Latin Plus glyph set").

Russian display text (welcome hero, tagline) under current D-05 picks
silently falls through to Iowan Old Style → Times New Roman → serif.
Mokosh's audience is Russian-primary, so this is the default rendering
path, not a corner case.

Routing back to designer per D-05 OWNER (Design team) with 4 options:
- R1 fallback chain (Newsreader + Cyrillic-capable secondary)
- R2 substitute display family entirely
- R3 other (designer proposes)
- R4 accept the Iowan/Times fallback

Candidate Cyrillic-capable OFL serifs: PT Serif, EB Garamond, Lora,
Source Serif. PT Serif highlighted (ParaType, RU-foundry, pairs with
IBM Plex Sans).

Blocks Plan 01-12 planner spawn + Plan 01-10 welcome hero.
Plan 01-11 (harness) unaffected — non-overlapping surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:25:35 +02:00
3df2750c64 docs(01-12): research Plan 01-12 (Design Integration) — 13 areas + BLOCKER
13 research surfaces investigated per Plan 01-12 prompt. Key findings:

- BLOCKER §1: Newsreader has no Cyrillic glyphs (verified via Decision
  Brief's own embedded @font-face subsets + Production Type README).
  Russian display text in Newsreader will silently fall through to
  serif fallback. Recommend §1 R1: add Cyrillic-capable OFL serif (PT
  Serif / EB Garamond / Lora) to --mks-font-display stack.

- §3 + §4: Mark legibility at 16 px is VERIFIED OK. rsvg-convert
  produces 406 / 784 / 1952 B PNGs at 16/48/128 — all clear Chrome
  imageUtil floors. The 2x2 weave holds at 16 px.

- §10: 8 i18n strings extracted verbatim from Brief §02 (decoded via
  Python JSON-decode of the bundler-template script). Recommended
  __MSG_*__ key names + source-file mapping table included.

- §12: D-09 (smoke gating) is NEAR-NO-OP. smoke.sh is a standalone
  bash script; nothing in src/ references it; dist/ carries no smoke
  artifacts. Recommend documenting current state + adding optional
  no-smoke-in-dist regression test.

- §13: tokens.css line 12 Google Fonts @import migration → 8 local
  @font-face rules (Plex variable consolidation collapses the
  estimated 12-18 down to 8).

All tooling verified installed on dev machine: rsvg-convert 2.60.0,
pyftsubset 4.63.0, woff2_compress, inkscape 1.4.4. No npm install
needed for execute-plan; only one-off font source TTF downloads.

7 assumptions logged for planner to surface (A1 default_locale, A2
popup DOM rework, A4 Cyrillic serif fallback are highest-impact).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:23:27 +02:00
5efc2a89e3 docs(intel): ingest designer-team handoff v1 + record 9 brand decisions
Designer-team delivered two bundles (2026-05-17):
- Mokosh.zip: presentation artifacts (Decision Brief + Surface Kit standalone HTMLs)
- Mokosh Design System.zip: implementation handoff (tokens.css + mark/lockup SVGs + memo)

Designer respected every contract: codename Mokosh preserved, MV3 CSP
acknowledged (flagged dev-only Google Fonts @import in tokens.css line 12
for engineering to replace), Cyrillic-first typography (IBM Plex Sans),
WCAG AA validation noted, made recommendations not unilateral decisions.

9 decisions resolved per user ack:
- D-01..06, D-08, D-09: accepted designer defaults
- D-07 OVERRIDE: B · "Mokosh — Session Capture" (vs designer's A · just "Mokosh")
  — operator-facing manifest:name surfaces capture purpose

Brand decisions persisted to brand-decisions-v1.md;
brand-identity.md Open Questions section marked resolved with backlink.

Designer artifacts staged at .planning/intel/design-incoming/ for Plan 01-12
(Design Integration) consumption. Mokosh.zip presentation HTMLs (3.4 MB)
preserved as audit/onboarding reference; can be pruned later if repo size
warrants.

Plan 01-12 implementation handoff catalogued in brand-decisions-v1.md
§"Implementation handoff" — covers tokens.css ingestion, WOFF2 self-hosting,
SVG → PNG rasterization, manifest:name + description update,
welcome layout, VITE_DEV smoke gating.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:00:54 +02:00
66e6c4af74 docs(01-11): create Puppeteer UAT harness plan (14 assertions, 9 tasks)
Retires operator-as-assertion-library role from Plan 01-09 Task 5. Bug A
(notification icon API rejection) and Bug B (state-machine routing of
user-stopped-sharing) both escaped vitest unit coverage and cost ~4-6h of
operator UAT cycles in Phase 1. Plan 01-11 ships a Puppeteer-driven Node
harness with CDP attach to SW + offscreen contexts; the 14 assertions cover
the Plan 01-08/01-09 functional contract end-to-end.

Locked from research (RESEARCH §1-§11):
- Puppeteer 25.0.2 + tsx + node:assert/strict (no vitest browser mode)
- Two-bundle separation via vite.test.config.ts (mode 'test' + dist-test/)
- Hook gating: import.meta.env.MODE === 'test' + dynamic import (Vite
  tree-shakes from production)
- Bug B trigger: track.dispatchEvent(new Event('ended')) — NOT track.stop()
  (W3C spec + empirical probe7 — track.stop does NOT fire 'ended')
- Tier-1 grep gate (tests/background/no-test-hooks-in-prod-bundle.test.ts)
  enforces zero __mokoshTest in production dist/
- Single browser, serial assertions, bail-on-first-failure (open question 4)

Wave structure (4 waves):
- Wave 0 (Task 1): puppeteer+tsx install, vite.test.config, build:test +
  test:uat scripts, Tier-1 grep gate committed GREEN.
- Wave 1 (Task 2): gated SW + offscreen hooks at src/test-hooks/; production
  bundle remains hook-free.
- Wave 2 (Task 3): harness scaffolding — tests/uat/lib/* + harness.test.ts
  with assertion 0 wired GREEN + assertions 1-13 stubbed RED.
- Wave 3 (Tasks 4-7): wire 13 assertions in 4 logically-grouped bundles
  (1-4 toolbar/displaySurface; 5-7 SAVE+BugB+ERROR; 8-10 BugA+icons+manifest;
  11-13 buffer continuity + ffprobe + zip).
- Wave 4 (Tasks 8-9): amend Plan 01-09 Task 5 to redirect functional steps
  to npm run test:uat; operator confirms brand/design.

Bug A + Bug B each have RED-on-regression canonical demos required in the
respective task's commit body — proves the harness CAN catch the regression,
not just passes under current conditions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:15:44 +02:00
969afbac89 docs(01-11): research Puppeteer UAT harness — empirical probes verify 10/10 unknowns
Probes 1-11 against local Chrome 148.0.7778.167 + Puppeteer 25.0.2:
- triggerExtensionAction works; popup-vs-onClicked contract confirmed
- --headless=new supports MV3 + getDisplayMedia (Xvfb not required)
- offscreen page reachable via background_page target type + .asPage()
- BLOCKER: track.stop() does NOT fire 'ended' per W3C spec — Bug B harness
  must use track.dispatchEvent(new Event('ended')) instead

13-assertion implementation table + 7 pitfalls + 2-bundle build design.
Wave 0 grep gate enforces tree-shake of __mokoshTest from production.
2026-05-17 17:42:40 +02:00