Commit Graph

232 Commits

Author SHA1 Message Date
d1ef77a7d1 docs(01-10): state + roadmap + requirements — Plan 01-10 closure
State markers sync after Plan 01-10 closure:

STATE.md:
- progress.completed_plans: 13 → 14
- progress.percent: 93 → 100 (all 14 Phase 1 functional plans complete)
- status: executing (Phase 1 final-closure marker flip pending)
- stopped_at + Last session timestamps refreshed
- Current Position bumped to 14/14 plans complete
- Outstanding Phase 1 gates: Plan 01-10 row marked CLOSED;
  Phase 1 final-closure marker flip listed as remaining work
- Plan 01-10 closure section added mirroring existing 01-12/01-13/01-14
  patterns: 4 wave commits + 5 inter-cycle debug commits + cycle-2 ack
- Performance Metrics: Phase 01 P10 row added (5h, 5 tasks, 14 files)
- Decisions: 3 Plan 01-10 architectural decisions added
  (first-install activation; D-16-toolbar charter preservation;
  three-pipeline DOM population pattern; startVideoCapture D-01
  cleanup gap closure)

ROADMAP.md:
- Plan 01-10 row flipped to [x] with full closure annotations
  (commit chain + harness counts + operator ack date)
- Plan 01-09 row annotated with closure-cycle follow-up debug
  commits (a2dfc8c startVideoCapture no-tab + 4bba679 notifStartup
  text split — both landed during Plan 01-10 closure cycle)
- Phase 1 plans-count narrative updated: "all 14 functional plans
  complete; Phase 1 final-closure marker flip pending"
- Progress table: Phase 1 plans complete 13/14 → 14/14;
  status row updated to "Final-closure marker flip pending"

REQUIREMENTS.md:
- Footer timestamp + closure note updated: Plan 01-10 introduced
  no new functional REQs (it consumed REQ-video-ring-buffer
  already-Complete via Plan 01-07) by adding the first-install
  operator-facing activation surface that complements the always-on
  capture pipeline.
- Phase 1 final functional plan delivered; final-closure marker
  flip pending (REQUIREMENTS / ROADMAP / STATE markers + optional
  /gsd-verify-work 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:10:56 +02:00
52dc2e6a6e docs(01-10): summary — welcome tab + 5-cycle debug closure + brand polish (153/153 vitest, 24/24 UAT GREEN, ack 2026-05-20)
Plan 01-10 (welcome tab) full SUMMARY landing closure of Phase 1's final
functional plan. Welcome tab landed end-to-end across 4 waves + Wave 4
operator empirical UAT cycle 2 ack "All good" 2026-05-20.

Architecture:
- chrome.runtime.onInstalled('install') + chrome.storage.local
  flag-gating (onboarding-completed:true + installed-at:<Date.now()>)
  + chrome.tabs.create + fire-and-forget .catch defense-in-depth.
- Plan 01-12 must_have #9 path-B contract honored: welcome.css opens
  with `@import '../shared/tokens.css';` (canonical Lora display +
  IBM Plex Sans UI + D-04 Loom palette); NO placeholder
  welcome-tokens.css file.
- chrome.i18n.getMessage for welcomeHeroRu + welcomeHeroEn with
  `|| <en-const>` fallback (Plan 01-12 fallback pattern preserved).
- Vite `?url` import + auto-WAR idiom bundles canonical
  mokosh-mark.svg as inline data URL in welcome chunk
  (closure-cycle debug 01-10-welcome-page-missing-mark).
- Three-pipeline DOM population: populateMark walks
  [data-mokosh-slot='mark']; populateCopy walks [data-mokosh-key]
  from in-file COPY map; populateI18n walks [data-mokosh-i18n-key]
  from chrome.i18n.getMessage.
- D-16-toolbar charter preserved: welcome page is informational +
  read-only; NO REQUEST_PERMISSIONS / chrome.runtime.sendMessage
  start path. CTA copy directs operator at toolbar icon.

Wave structure (4 plan-wave commits):
- 89e1e09 Wave 0 RED onboarding tests (3 tests A/B/C)
- 49f087f Wave 1 welcome bundle + Vite entries + manifest
  web_accessible_resources
- 8f329d8 Wave 2 openWelcomeIfFirstInstall + onInstalled wiring
- b112cb7 Wave 3 harness A15+A16+A17

Cycle-1 operator UAT rejection 2026-05-20 ~08:56 surfaced two
concerns + 5 inter-cycle debug fixes (in commit order):
- 4bba679 fix(01-09): notifStartup text split (notifStartupCta for
  onStartup; notifRecordingStarted reserved for manual-start)
- d48a715 fix(01-10): welcome page mark — bundle canonical
  mokosh-mark.svg via Vite ?url + populateMark + A17.8 sub-check
  (Plan 01-12 must_have #9 path-A swap-in gap closed)
- 0854baf fix(01-10): vitest build-test it() timeout bump to 30s
  for slower welcome-page build
- a2dfc8c fix(01-09): startVideoCapture — remove stale active-tab
  dependency (D-01 cleanup gap; +3 RED→GREEN tests at
  start-video-capture-no-tab.test.ts)
- d21ed17 fix(01-12): brand polish — replace stale 'AI Call Recorder'
  refs with Mokosh (4 files; .planning/intel/* preserved as audit
  trail)

Harness deltas:
- A15-A17 added at b112cb7 (24/24 UAT GREEN; A17 with 7 sub-checks
  incl. A17.7 --mks-rec getComputedStyle probe resolving to
  rgb(178,84,61) canonical).
- A17.8 mark-bundling sub-check added at d48a715 (verifies welcome
  chunk JS contains inlined data:image/svg+xml URL with canonical
  viewBox='0 0 32 32' preserved).
- FORBIDDEN_HOOK_STRINGS unchanged at 12 (A15-A17 use chrome.tabs
  .query + chrome.storage.local.get + fetch + DOMParser +
  getComputedStyle production APIs exclusively).

Test deltas:
- vitest 147 → 153 GREEN (+6: 3 onboarding tests Wave 0 + 3
  start-video-capture-no-tab tests closure-cycle debug a2dfc8c).
- UAT harness 21/21 → 24/24 GREEN (A0-A14 + A15-A17 + A18-A22 + A23).

Pre-checkpoint bundle gates per saved memory
feedback-pre-checkpoint-bundle-gates.md: Tier-1 hook-string grep
+ SW CSP-safety + Node-globals + DOM-globals + manifest validation
+ en↔ru parity — all PASS. setimmediate polyfill new Function in
SW chunk confirmed pre-existing (logged at
.planning/phases/01-stabilize-video-pipeline/deferred-items.md
for Phase 5 hardening per Plan 01-12 Wave 7 disclosure).

Operator brand-fit cycle-2 ack received verbatim "All good" + cycle-2
follow-up brand-rename ack via d21ed17. Phase 1's final functional
plan delivered; Phase 1 final-closure unblocked pending REQUIREMENTS
/ ROADMAP / STATE marker flip + optional /gsd-verify-work 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:10:30 +02:00
d21ed17310 fix(01-12): brand polish — replace stale 'AI Call Recorder' refs with Mokosh (4 files)
Plan 01-12 D-07 (commit 5efc2a8) migrated manifest.json:name + tooltip
+ extName/extDesc keys to chrome.i18n placeholders + _locales/{en,ru}/
messages.json resolving to 'Mokosh — Session Capture' (EN) / 'Mokosh —
Запись сессии' (RU). However, 4 trailing references to the pre-D-07
literal 'AI Call Recorder' in non-manifest content surfaces were never
propagated. Operator noticed during Plan 01-10 cycle-2 UAT 2026-05-20.

This commit applies the user-approved 4-file surgical rename:

  - src/welcome/copy.ts: welcome.body.cta.toolbar RU CTA
    "иконку AI Call Recorder" → "иконку Mokosh" (matches toolbar tooltip
    i18n key tooltipOff = "Mokosh — щёлкните, чтобы начать запись").
    Inline rationale comment added cross-ref'ing the i18n key + this
    debug session.

  - README.md: H1 + first paragraph rewritten to
    "# Mokosh — Session Capture" + EN tagline line. Rest of README body
    preserved verbatim (technical-stack section historical mentions
    left as project history, not brand surface).

  - package.json: name "ai-call-extension" → "mokosh-session-capture",
    description rewritten to "Mokosh — Session Capture: Chrome MV3
    extension for operator session recording." version, scripts,
    dependencies, devDependencies untouched.

  - tests/i18n/manifest-i18n.test.ts: header comment block rewritten to
    label the "AI Call Recorder" string as Wave-0 historical state +
    describe the post-D-07 regression-pin role. Test bodies + assertions
    unchanged (already pin the post-D-07 canonical state).

Preservation rationale: .planning/intel/* (brand-decisions-v1.md D-07,
design-system.md, brand-identity.md, classifications/README-*.json,
design-incoming/system/bundle/mokosh-handoff/handoff.html) is audit
trail documenting the "why" of D-07 — kept verbatim. _locales/{en,ru}/
messages.json and manifest.json already post-D-07 canonical — untouched.

Acceptance gates (all PASS 2026-05-20):
  - Empirical grep src/ tests/ README.md package.json: ZERO non-historical
    "AI Call Recorder" matches (only the labeled audit anchor in
    tests/i18n/manifest-i18n.test.ts:8).
  - npx tsc --noEmit: clean.
  - npm run build: ✓ built in 5.29s.
  - npx vitest run tests/i18n/manifest-i18n.test.ts: 10/10 GREEN.
  - npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts
    (Tier-1 hook-string grep gate): 13/13 GREEN; FORBIDDEN_HOOK_STRINGS
    list intact.
  - npm test: 151/153 (2 pre-existing ffprobe/ffmpeg timeout flakes in
    webm-remux + webm-playback — verified identical to pristine HEAD
    a2dfc8c via git stash baseline; unrelated to rename).
  - npm run test:uat: 24/24 GREEN.
  - Production bundle grep dist/: ZERO "AI Call Recorder" + ZERO
    "ai-call-extension" matches.

Unblocks Plan 01-10 closure + Phase 1 final closure (REQUIREMENTS /
ROADMAP / STATE marker flip).

Debug record: .planning/debug/resolved/01-12-stale-ai-call-recorder-references.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:49:46 +02:00
a2dfc8cb9b fix(01-09): startVideoCapture — remove stale active-tab dependency (D-01 cleanup gap)
The legacy chrome.tabs.query({ active: true, currentWindow: true }) +
"No active tab found" validation inside startVideoCapture were load-
bearing in the pre-D-01 chrome.tabCapture era but became functionally
dead after Plan 01-09's D-01 conversion to getDisplayMedia-in-offscreen.
The only post-D-01 consumer was a log line at index.ts:521.

The dead validation caused an activeTab-permission-scope asymmetry
between callers: chrome.action.onClicked grants activeTab on the click
gesture (so tab.url was readable → toolbar path worked silently) but
chrome.notifications.onClicked does NOT grant activeTab and the extension
has no `tabs` permission, so notifications.onClicked → startVideoCapture
threw "No active tab found" before reaching ensureOffscreen. Operator
2026-05-20 UAT against the new notifStartupCta CTA copy ("Mokosh ready.
Click to start a recording.", commit 4bba679) surfaced the silent
notification failure.

Surgical fix: remove the dead tab query + validation + tab-dependent log
(src/background/index.ts:514-521); replace with a tab-independent log
that documents WHY (cites D-01 + this debug session). captureScreenshot
+ saveArchive retain their genuine tab dependencies (tab.windowId for
chrome.tabs.captureVisibleTab; tab.id for content-script sendMessage).

Tests: tests/background/start-video-capture-no-tab.test.ts (NEW) pins
the contract with 3 cases (tabs.query → []; → [{id}] url-less; →
[{id,url,windowId}] regression guard for toolbar path).

Gates: vitest 153/153 GREEN (was 150/150 baseline; +3); test:uat 24/24
GREEN; tsc clean; build clean. Pre-checkpoint bundle gates per
feedback-pre-checkpoint-bundle-gates.md: SW chunk hook-string Tier-1
grep 0 matches; eval/Node-global/DOM-global matches unchanged from
baseline (all vendor-library feature-detect, guarded; no new imports).

Debug record: .planning/debug/resolved/01-09-notification-start-no-active-tab.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:33:18 +02:00
0854baf66c fix(01-10): vitest build-test it() timeout — bump to 30s for slower welcome-page build
The build-completes Tier-1 gate at tests/background/no-test-hooks-in-prod-bundle.test.ts:247
was racing vitest's default 5000ms it() ceiling. Plan 01-10 closure shipped the welcome
page (commits d48a715 welcome mark + 49f087f welcome HTML/CSS/JS + 8 WOFF2 fonts) which
slowed standalone `npm run build` from ~2.88s to ~5.28s. The exec-level
BUILD_TIMEOUT_MS = 60_000 child-process bound was correctly declared at line 240, but
the surrounding it() block had no timeout option, so the 5s default fired first and the
60s exec bound was never reachable.

Surgical fix: add `, 30_000` 3rd arg to the it() call. 30s is ~6× the observed build
duration and well below the 60s exec ceiling, so both bounds remain meaningfully
active. SKIP_BUILD=1 env-var escape hatch untouched.

Acceptance gates:
- `npm test` (FULL, no SKIP_BUILD=1): 150/150 GREEN, exit 0
- `npx tsc --noEmit`: exit 0
- `npm run build`: exit 0
- Tier-1 grep gate: PASS (all 12 FORBIDDEN_HOOK_STRINGS asserted against dist/)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:52:39 +02:00
d48a715da5 fix(01-10): welcome page mark — bundle canonical mokosh-mark.svg + replace placeholder
Plan 01-10 must_have #9 path-A swap-in (landed 2026-05-20 per debug
session 01-10-welcome-page-missing-mark). Closes the planning-coverage
gap where Plan 01-12 path-B (canonical tokens import) ran ahead of
01-10, leaving the welcome hero with a text placeholder 'Mokosh'
inside the rec-bg circle instead of the canonical 2×2 woven-square
mark from src/shared/brand/mokosh-mark.svg.

Why Option B (Vite ?url import) over manual WAR (A) or inline SVG (C):
- @crxjs/vite-plugin ^2.0.0-beta.25 auto-WARs transitively-reachable
  resources from extension pages — no manifest.json edit needed.
- Vite default-inlines small SVGs (~600 bytes < 4096 byte default
  assetsInlineLimit) as data:image/svg+xml URLs in the welcome chunk
  — no extra HTTP request, no extra WAR entry.
- Hashed asset fallback works automatically if the SVG grows past
  the inline limit in future revisions.
- Existing font-bundling precedent (dist/assets/Lora-*.woff2 +
  IBMPlex*.woff2) proves the Vite + crxjs pipeline.

Files modified:
- src/welcome/welcome.ts — added markUrl import + populateMark() that
  walks [data-mokosh-slot='mark'] and injects an <img>.
- src/welcome/welcome.html — added explanatory comment block; preserved
  the data-mokosh-slot wrapper for forward-compat (the placeholder
  span remains as the JS-fail-gracefully fallback).
- src/welcome/welcome.css — added .welcome-hero__mark-img rule
  (60% sizing inside the existing styled circle wrapper).
- src/welcome/copy.ts — added 'welcome.hero.mark.alt' COPY key
  (Russian per D-03 Sober voice).
- globals.d.ts — added *.svg?url ambient module declaration
  (Vite recommended pattern; keeps tsconfig.json types: ['chrome']
  clean by not requiring vite/client triple-slash directives).
- tests/uat/extension-page-harness.ts — extended A17 with A17.8
  sub-check verifying the canonical mark SVG is bundled into the
  welcome chunk (data URL OR file URL form) AND that the canonical
  viewBox='0 0 32 32' is preserved through bundling.

Acceptance gates passed:
- npx tsc --noEmit exit 0
- npm run build exit 0
- SKIP_BUILD=1 npm test → 150/150 GREEN
- npm run test:uat → 24/24 GREEN including A17.8
- Tier-1 hook-string grep gate PASS (no FORBIDDEN_HOOK_STRINGS
  in production bundle).
- Manifest valid JSON; web_accessible_resources auto-bundled.
- Pre-checkpoint bundle gates 1/2/3: vendor pre-existing hits
  (JSZip + ts-ebml) confirmed identical pre-change via git stash
  baseline; not caused by this fix.

Forward-looking deferred (out of scope):
- Issue 2 dark-surface contrast (e.g. chrome.notifications icon128
  may need a light-stroke variant). The welcome hero's rec-orange
  BG already provides high contrast with the dark ink stroke — this
  is correct design. Per the orchestrator's explicit constraint,
  light-variant mark for dark notification panels is deferred to
  Phase 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:28:58 +02:00
4bba679e39 fix(01-09): notifStartup text split — notifStartupCta for onStartup; notifRecordingStarted for manual-start
Operator UAT 2026-05-20 rejected the build because the OS notification fired
on `chrome.runtime.onStartup` ("Recording started. I'm watching the last 30
seconds.") implied recording had auto-started when in fact recording was
not running. Per Phase 1 always-on charter recording does NOT auto-start;
the notification is the gesture surface that invites the operator to start
one (notifications.onClicked → startVideoCapture, src/background/index.ts:1038).

Root cause: a single i18n key `notifStartup` conflated the pre-recording
CTA-with-gesture path (the only path actually wired today) and a future
post-manual-start confirmation path. The key's own `.description` field
acknowledged the conflation. Operator-facing text leaned toward the
confirmation phrasing.

Fix (key split, no behavior change):
- `notifStartupCta` — EN: "Mokosh ready. Click to start a recording." /
  RU: "Mokosh готов. Нажмите, чтобы начать запись." — wired into the
  onStartup handler.
- `notifRecordingStarted` — preserves the original text ("Recording
  started. I'm watching the last 30 seconds." / "Запись запущена…") for
  a future post-manual-start confirmation flow.
- Fallback constant renamed `NOTIF_STARTUP_FALLBACK` →
  `NOTIF_STARTUP_CTA_FALLBACK`; value updated to match the new CTA text.
- Inline test comment in tests/background/onstartup-notification.test.ts
  refreshed to reference the new key + fallback. Assertion regex
  /recording|recor|click/i covers both fallback + resolved locale variants,
  no logic change.

Notification behavior preserved: same id prefix `mokosh-startup-`, same
priority, same icon, same onClicked → startVideoCapture wiring. No new
test-mode symbols (FORBIDDEN_HOOK_STRINGS inventory stays at 12).

Files modified:
- _locales/en/messages.json
- _locales/ru/messages.json
- src/background/index.ts
- tests/background/onstartup-notification.test.ts

Verification:
- npx vitest run --exclude tests/build/** --exclude tests/background/no-test-hooks-in-prod-bundle.test.ts: 104/104 GREEN
- npx vitest run tests/i18n/ tests/background/onstartup-notification.test.ts: 18/18 GREEN (locale-parity 4/4 + onstartup-notification 14/14)
- npx tsc --noEmit clean on src/background/index.ts

The 2 build-dependent vitest gates (tests/build/no-remote-fonts.test.ts +
tests/background/no-test-hooks-in-prod-bundle.test.ts) and npm run test:uat
are deferred to orchestrator-level re-verification after the parallel
Plan 01-10 mark-bundling fix also lands (operator-UAT re-spawn coordinated
by orchestrator).

Debug record: .planning/debug/resolved/01-09-startup-notification-misleading-text.md
Operator UAT rejection event: 2026-05-20

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:14:08 +02:00
b112cb7861 test(01-10): wave-3 task-4 — harness A15+A16+A17 (onboarding flag observability + no-re-open settle + design-swap-readiness with @import probe); 24/24 GREEN
Plan 01-10 Wave 3: extends the UAT harness with three new page-side
assertions covering the onboarding contract + the canonical-tokens
design-swap-readiness invariant. UAT baseline 21 → 24 GREEN.

tests/uat/extension-page-harness.ts (page-side):
  - assertA15 — chrome.storage.local 'onboarding-completed' === true +
    'installed-at' is number. Verifies SW's openWelcomeIfFirstInstall
    side-effects.
  - assertA16 — 2s settle window; chrome.tabs.query welcome-tab count
    delta === 0. Verifies flag-gating across SW respawns.
  - assertA17 — 7 sub-checks covering: welcome.html parse + .welcome-hero
    + >=7 mokosh-keyed attrs + welcome.css canonical @import literal OR
    inlined --mks-* evidence + (zero hex OR canonical resolved) + >=5
    var(--mks-*) refs + bundled JS preserves populate plumbing +
    getComputedStyle --mks-rec → rgb(178, 84, 61) (canonical D-04 Loom).
  - window.__mokoshHarness surface extended with the three new methods;
    type declaration + assignment + page-ready status text updated.

tests/uat/lib/harness-page-driver.ts (host-side):
  - driveA15, driveA16, driveA17 — standard page.evaluate wrappers
    matching driveA14 / driveA18..A22 idiom. driveA16 dominates the
    new wall-clock budget (~2.1s for the settle window).

tests/uat/harness.test.ts (orchestrator):
  - Drivers array interleaves A15/A16/A17 AFTER A14 + BEFORE A18.
    A22's skip-gate no longer triggers (Plan 01-10 lands welcome.html;
    A22 now exercises the substantive token-usage path).
  - FORBIDDEN_HOOK_STRINGS unchanged at 12 entries (A15-A17 use only
    chrome.tabs.query / chrome.storage.local.get / fetch / DOMParser /
    getComputedStyle — all production-API surfaces).

DEVIATION (Rule 1 — auto-fix bug in plan-supplied check):
  The plan's A17.6 spec used literal substring checks 'COPY[' and
  'chrome.i18n.getMessage(' which fail against minified production
  output. Vite/Rollup terser renames `COPY` → `f` (local variable
  mangling) and welcome.ts's source uses optional chaining
  `chrome?.i18n?.getMessage?.(` which doesn't match the verbatim
  literal. Replaced with two minification-survivable witnesses:
    1. 'welcome.page.title' — literal Object.freeze key (terser
       preserves object-literal keys verbatim).
    2. 'i18n' + 'getMessage' + 'welcomeHero' substring conjunction —
       chrome global + property access + fallback key literal; all
       three survive minification regardless of optional-chaining
       insertion or rename.
  Both witnesses prove the populate plumbing survives the build (the
  ground-truth contract A17.6 enforces). The relaxed contract is
  semantically equivalent — neither substring is load-bearing on its
  own; both witness the same underlying invariant.

Verify (all GREEN):
  - npm run test:uat: 24/24 assertions passed (A0 grep gate + A1..A14
    + A15..A17 + A18..A22 + A23).
  - npx tsc --noEmit: clean.
  - npm run build:test: clean; dist-test/assets/welcome-wB0e_R_n.js
    bundled; harness page bundle includes new asserts.
  - SKIP_BUILD=1 npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts:
    13/13 GREEN (Tier-1 grep gate; FORBIDDEN_HOOK_STRINGS at 12).
  - Full vitest baseline preserved: 137 ex-grep-gate + 13 grep-gate
    = 150 GREEN (Plan 01-10 target).

A17.7 canonical proof: getComputedStyle.color = 'rgb(178, 84, 61)' —
the @import '../shared/tokens.css' directive resolves through to the
canonical D-04 Loom palette --mks-madder-600 = #b2543d at runtime, as
the empirical proof Plan 01-12 must_have #9 path-B contract demands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:41:10 +02:00
8f329d8b74 feat(01-10): wave-2 task-3 — openWelcomeIfFirstInstall helper + onInstalled wiring (D-17-onboarding) — 3 RED → GREEN
Plan 01-10 Wave 2: SW handler extension flips Task 1's 3 RED onboarding
tests GREEN.

src/background/index.ts changes:

  1. Three top-level constants added near the badge/notification block:
     - ONBOARDING_FLAG = 'onboarding-completed'
     - ONBOARDING_INSTALLED_AT = 'installed-at'
     - WELCOME_PATH = 'src/welcome/welcome.html'
     SCREAMING_SNAKE per project naming standard for true constants.

  2. openWelcomeIfFirstInstall helper added below ensureOffscreen
     (interfaces §1 placement). JSDoc cites D-17-onboarding (CONTEXT.md
     line 537+; SUFFIX disambiguates from D-17-port-lifecycle per
     CONTEXT.md lines 540-545). Body:
       - Early return on details.reason !== 'install' (subsequent
         installs / updates / chrome_update / shared_module_update do
         NOT open a welcome tab — Test B's contract).
       - chrome.storage.local.get(ONBOARDING_FLAG) read with the EXACT
         single-key string (storage-schema cross-version-compat pin;
         Test A.3's contract).
       - Early return if stored[ONBOARDING_FLAG] === true — Test C's
         contract (already-onboarded suppression).
       - chrome.tabs.create + chrome.storage.local.set with both the
         flag and Date.now() installed-at — Test A.1 + A.2's contract.
       - Defense-in-depth try/catch wraps the whole body; any thrown
         chrome.* call is logged via logger.warn but does not propagate
         (D-16-toolbar start path remains independent).

  3. onInstalled listener extended: fire-and-forget call to
     openWelcomeIfFirstInstall(details) AFTER initialize(); .catch()
     boundary so rejected promises cannot escape the synchronous
     listener. The existing IDB cleanup + initialize() call sequence
     stays unchanged.

Architectural compliance:
  - NO `await import(...)` added (01-11-SUMMARY architectural constraint
    preserved; the three matches in lines 14-28 are documentation
    comments about Plan 01-11's falsification).
  - NO `as any` (chrome.runtime.InstalledDetails ambient typing covers
    the parameter).
  - NO `continue` (if-else early-return only).
  - No new dependencies.

Verify (all GREEN):
  - npx vitest run tests/background/onboarding.test.ts: 3 passed (Test
    A flipped RED → GREEN; B + C continue passing as load-bearing
    guards).
  - Full vitest baseline 147 → 150 (137 ex-build-gated + 13 in build-
    gated = 150 GREEN total).
  - npx tsc --noEmit: clean.
  - npm run build: clean; openWelcomeIfFirstInstall + D-17-onboarding
    references survive into dist/assets/index.ts-*.js.
  - Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12 entries; gate GREEN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:16:42 +02:00
49f087fe40 feat(01-10): wave-1 task-2 — welcome page bundle + Vite entries + web_accessible_resources
Plan 01-10 Wave 1: welcome page bundle staged with canonical Plan 01-12
tokens.css @import + chrome.i18n for D-08 tagline (Plan 01-12 path-B
contract).

Files created:
  - src/welcome/copy.ts (Russian non-tagline COPY map per D-03 Sober
    voice; WELCOME_HERO_RU_FALLBACK + WELCOME_HERO_EN_FALLBACK exported
    for the welcome.ts `|| <en-const>` fallback chain).
  - src/welcome/welcome.html (lang='ru'; data-mokosh-key + data-mokosh-
    i18n-key attribute conventions; SINGLE stylesheet link; D-02 Hero +
    body + footer structure; 10 keyed attrs total; <title> populated
    via populateCopy).
  - src/welcome/welcome.css (FIRST LINE `@import '../shared/tokens.css';`;
    ZERO hex literals in source; 65 var(--mks-*) refs; D-02 layout +
    --mks-welcome-max-w=720px; --mks-rec madder for recording-related
    accents per D-04 Loom palette).
  - src/welcome/welcome.ts (vanilla DOM; populateCopy + populateI18n
    filter-pipeline form per project rule "no `continue`"; no `as any`;
    Logger from src/shared; document.readyState guard; no event
    handlers per D-16-toolbar informational charter).

Files modified:
  - vite.config.ts: rollupOptions.input gains `welcome:
    'src/welcome/welcome.html'`; __VITE_DEV__ + __MOKOSH_UAT__ defines
    untouched (Plan 01-12 Wave 5 baseline preserved verbatim).
  - vite.test.config.ts: mirror entry in dist-test/; mergeConfig pattern
    untouched.
  - manifest.json: web_accessible_resources block added after
    host_permissions, before background; storage permission preserved
    in permissions array; default_locale='en' + __MSG_*__ placeholders
    from Plan 01-12 Wave 3 preserved verbatim.

NO src/welcome/welcome-tokens.css file is created — Plan 01-12 must_have
#9 path-B contract: Plan 01-12 landed FIRST (b909c37 → 865d394; SUMMARY
2026-05-20); canonical src/shared/tokens.css is import-ready
(Lora @font-face + IBM Plex Sans + D-04 Loom palette + --mks-rec=
var(--mks-madder-600) = #b2543d); welcome.css @imports it directly. No
placeholder transition needed.

Verify (all GREEN):
  - grep -F "@import '../shared/tokens.css'" src/welcome/welcome.css: exit 0
  - grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css: exit 1 (zero hex)
  - grep -c 'var(--mks-' src/welcome/welcome.css: 65 (>= 5 required)
  - grep -oE 'data-mokosh-(i18n-)?key=' welcome.html | wc -l: 10 (>= 7)
  - npm run build: clean; dist/src/welcome/welcome.html present;
    dist/assets/welcome-D9oNz95l.css carries inlined tokens.css content
    (--mks-rec: var(--mks-madder-600); --mks-madder-600: #b2543d).
  - npm run build:test: clean; dist-test/src/welcome/welcome.html present;
    dist-test/assets/welcome-wB0e_R_n.js bundled.
  - npx tsc --noEmit: clean.
  - dist/manifest.json preserves "default_locale": "en" + __MSG_extName__
    + web_accessible_resources block present (Vite/crxjs propagated).
  - Vitest baseline preserved: Task 1's 3-test file unchanged
    (1 RED + 2 vacuous-GREEN; Task 3 flips Test A to GREEN).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:09:22 +02:00
89e1e09d60 test(01-10): wave-0 task-1 — RED onboarding tests (3 tests pin install/update/flag + storage-key)
Three tests in tests/background/onboarding.test.ts pinning the
Plan 01-10 D-17-onboarding contract:

  Test A (RED): first install + empty storage opens exactly ONE welcome
    tab whose URL contains 'src/welcome/welcome.html', sets
    chrome.storage.local.set({'onboarding-completed':true,
    'installed-at':<number>}), AND calls chrome.storage.local.get with
    EXACT key 'onboarding-completed' (storage-schema cross-version-compat
    pin; preserves I-02 lesson from prior draft).

  Test B (vacuous-GREEN, becomes load-bearing post-Task-3): reason='update'
    → chrome.tabs.create NOT called.

  Test C (vacuous-GREEN, becomes load-bearing post-Task-3): flag already
    true → chrome.tabs.create NOT called.

Tests B and C pass vacuously until Task 3 lands openWelcomeIfFirstInstall;
they remain load-bearing AFTER Task 3 as no-tab-open guards for the
update/already-onboarded branches. Test A flips RED → GREEN at Task 3.

Stub scaffold inherits buildBgStub from onstartup-notification.test.ts;
extended with chrome.tabs.create + chrome.storage.local.{get,set} +
chrome.runtime.onInstalled._callbacks (addListener.mockImplementation
pattern to capture the SW's registered listener).

DEVIATION NOTE: plan's <verify> expected `3 failed` but only Test A
(positive contract) goes RED pre-Task-3; Tests B+C are negative-path
guards that pass trivially when the helper is absent. This is standard
TDD (positive test fails RED; negative tests stay GREEN through GREEN→
REFACTOR). No code change needed — Task 3's GREEN gate is "all 3 GREEN".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:00:58 +02:00
7f58e0ae31 fix(01-10): revise plan per 01-12 + 01-14 baselines (vitest 98→147, UAT 15→21, FORBIDDEN 10→12, welcome.css @imports canonical tokens, welcomeHero keys read from chrome.i18n)
Surgical amendment to unexecuted Plan 01-10 absorbing the post-draft
landing of Plan 01-12 (canonical src/shared/tokens.css + 16 i18n keys
including welcomeHeroRu/welcomeHeroEn; 2026-05-20 operator brand-fit
ack) and Plan 01-14 (vitest +2 + UAT +1 + FORBIDDEN_HOOK_STRINGS +2).

Baseline shifts:
- vitest 98 → 147 GREEN (post Plan 01-12 + 01-14); plan close target 150.
- UAT 15 → 21 GREEN (A0-A14 + A18-A22 + A23); plan close target 24
  (A0-A14 + A15-A17 + A18-A22 + A23).
- FORBIDDEN_HOOK_STRINGS 10 → 12 (Plan 01-14: lastGetDisplayMediaConstraints
  + get-last-getDisplayMedia-constraints); Plan 01-10 introduces no new
  test-mode symbols; inventory unchanged at 12.

Plan 01-12 must_have #9 path-B contract honored end-to-end (Plan 01-12
landed FIRST, so the welcome page adopts canonical assets directly):

- welcome.css opens with `@import '../shared/tokens.css';` (NO placeholder
  welcome-tokens.css; removed from files_modified).
- D-08 hero tagline elements use data-mokosh-i18n-key='welcomeHeroRu' +
  data-mokosh-i18n-key='welcomeHeroEn'; welcome.ts reads via
  chrome.i18n.getMessage with `|| <en-const>` fallback per Plan 01-12
  fallback pattern. WELCOME_HERO_RU_FALLBACK + WELCOME_HERO_EN_FALLBACK
  constants exported from copy.ts for the degradation path.
- copy.ts COPY map retains non-tagline keys only (page title + explainer
  lines + CTA + footer privacy; engineering placeholders per D-03).
- A17 design-swap-readiness invariant extended with:
  - A17.5: welcome.css contains canonical @import directive OR inlined
    `--mks-rec:` evidence;
  - A17.6: bundled JS contains COPY[ OR chrome.i18n.getMessage('welcomeHero;
  - A17.7 NEW: getComputedStyle probe on var(--mks-rec) returns non-default
    value (canonical rgb(178, 84, 61) = #b2543d = --mks-madder-600 per
    Plan 01-12 Wave 4 D-04 Loom palette adoption).
- depends_on extended to [01-09, 01-13, 01-14, 01-12].

Preserved verbatim: 5-task structure, A15/A16 contracts, D-02/D-08/D-09
references, threat model + STRIDE register, operator empirical checkpoint
shape, Plan 01-12 default_locale='en' + __MSG_*__ + __VITE_DEV__ +
__MOKOSH_UAT__ + src/shared/tokens.css.

Validated: gsd-sdk frontmatter.validate + verify.plan-structure both PASS;
task_count=5, all tasks complete with files/action/verify/done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:48:24 +02:00
66e6f503a4 docs(01-12): state + roadmap + requirements — Plan 01-12 closure
Plan 01-12 closure documentation sync per the plan's Wave 7 Task 1
spec. Three docs land together as one atomic closure commit per the
Plan 01-13 + 01-14 closure cadence convention.

STATE.md changes:
- status: verifying → executing (Plan 01-10 welcome tab still pending)
- stopped_at: Plan 01-14 → Plan 01-12 closed via Wave 7 brand-fit
  ack 2026-05-20 'all good'; Plan 01-10 remains as final Phase 1
  functional plan
- last_updated + last_activity: 2026-05-19 → 2026-05-20
- progress.completed_plans: 12 → 13 (13 plans: 01-01..01-09 + 01-11
  spike + 01-12 + 01-13 + 01-14; Plan 01-10 pending)
- progress.percent: 86 → 93
- "Plan 01-13 closure" header annotated: brand/design ack subsequently
  closed via Plan 01-12 Wave 7 2026-05-20
- New "Plan 01-12 closure (2026-05-20)" section: 7-wave execution arc
  with all 10 commit hashes (3fe018b plan-baseline-revision → 34a9ce1
  Wave 0 → f86fd60 + abab6e1 Wave 1 → 7732a30 Wave 2 → 110cebc Wave 3
  → 468f16d Wave 4 → e8d2881 Wave 5 → b909c37 Wave 6 + 865d394
  pre-checkpoint + f319c7d SUMMARY); R2 Lora substitution; 16 i18n
  keys; branded icons; BADGE_REC_COLOR #b2543d; chrome.i18n fallback;
  A18-A22 harness; pre-checkpoint bundle gates per
  feedback-pre-checkpoint-bundle-gates.md; setimmediate polyfill
  discovery logged to deferred-items.md (Phase 5 hardening); operator
  brand-fit ack; Plan 01-13 Task 9 functional closure
- "Outstanding Phase 1 gates" updated: Plan 01-13 Task 9 + Plan 01-12
  CLOSED; only Plan 01-10 remains
- Performance Metrics table: Plan 01-12 entry appended (~10h
  cumulative; 10 tasks; ~50+ files)
- Decisions section: 2 new entries for Plan 01-12 design integration
  + Plan 01-13 Task 9 closure linkage
- Session Continuity: last/prior session updated to 2026-05-20 /
  2026-05-19

ROADMAP.md changes:
- Phase 1 Plans list: 01-12-PLAN.md entry flipped from [ ] to [x]
  with full closure annotation (R2 Lora self-host, tokens.css
  canonical, 16 i18n keys across en+ru, branded Loom icons, manifest
  i18n, BADGE_REC_COLOR madder #b2543d, chrome.i18n fallback,
  harness A18-A22, operator brand-fit ack 2026-05-20 'all good')
- Phase 1 plan count: 13 → 14 plans (01-01 through 01-14)
- Progress table Phase 1 row: 7/7 Complete → 13/14 Executing with
  closure-status disambiguation (functional via Plan 01-13;
  design/brand via Plan 01-12; Plan 01-10 welcome tab remains)

REQUIREMENTS.md changes:
- REQ-install-clean: [ ] Pending → [x] Complete (2026-05-20)
  with annotation: fresh build clean; zero remote-font CSP errors;
  branded icons; en+ru manifest:name resolution; operator brand-fit
  ack
- REQ-manifest-permissions: [ ] Pending → [x] Complete (2026-05-20)
  with annotation: manifest:name + :description +
  :action.default_title migrated to __MSG_*__ + default_locale='en';
  manifest validation PASS; en↔ru parity; permissions DEC-011
  baseline UNCHANGED
- Traceability table: both requirements moved from Phase 3/4 Pending
  to "Phase 1 closure via Plan 01-12" Complete
- Footer: last-updated 2026-05-15 → 2026-05-20 with annotation
  noting the requirements flipped at Plan 01-12 closure

No code changes; pure documentation closure sync.

Closure commit hashes:
- SUMMARY: f319c7d (.planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md)
- State sync: this commit (.planning/STATE.md + .planning/ROADMAP.md
  + .planning/REQUIREMENTS.md)

Phase 1 status post-closure:
- Functional contract: CLOSED via Plan 01-13 harness PASS (2026-05-19)
- Design/brand contract: CLOSED via Plan 01-12 brand-fit ack
  (2026-05-20)
- Remaining: Plan 01-10 (welcome tab) — operator-facing onboarding
  surface; canonical src/shared/tokens.css from Plan 01-12 now
  available for swap-in

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:33:52 +02:00
f319c7dc6e docs(01-12): summary — design integration landed (147/147 vitest, 21/21 UAT, brand-fit ack 2026-05-20)
Plan 01-12 closure SUMMARY landed at .planning/phases/01-stabilize-
video-pipeline/01-12-SUMMARY.md per the plan's <output> block + 01-13/
01-14 closure cadence. Mirrors the 01-13-SUMMARY frontmatter shape +
body sections (One-Liner / What Landed by Wave / Test Counts /
Deviations / Architectural Notes / Self-Check / Known Limitations /
Bridge to Phase 1 Closure).

Plan 01-12 design integration in one sentence: Lora self-hosted via
R2 designer substitution (Newsreader → Lora for Cyrillic coverage,
2026-05-19); src/shared/tokens.css canonical with 8 local @font-face
rules and zero remote URLs; 16 i18n keys across en + ru with parity;
branded Loom-mark icons replace Bug A placeholders; src/popup +
src/background migrated to chrome.i18n.getMessage with || <const>
fallback; UAT harness extended with A18-A22; pre-checkpoint bundle
gates established per feedback-pre-checkpoint-bundle-gates.md;
operator brand-fit ack received 2026-05-20 verbatim "all good".

Gate evidence (per Wave 7 pre-checkpoint 865d394 + this closure):
- vitest: 147/147 GREEN (re-verified on closure day; 26 test files)
- npm run test:uat: 21/21 GREEN (A0-A14 + A18-A22 + A23)
- npx tsc --noEmit: clean
- npm run build + npm run build:test: both clean
- MV3 CSP self-host: 0 googleapis / 0 https://fonts in dist/
- Tier-1 forbidden-strings: 13/13 GREEN (no new test-mode symbols)
- Operator brand-fit empirical ack 2026-05-20: "all good"

Closure linkage:
- Plan 01-12 functionally CLOSED (10/10 tasks; 7/7 waves)
- Plan 01-13 Task 9 (operator brand/design ack on loaded extension)
  functionally CLOSED via this checkpoint (same operator + same
  empirical surface coverage)
- Phase 1 design/brand contract CLOSED; only Plan 01-10 (welcome tab)
  remains as the last Phase 1 functional plan
- Phase 2 inherits tokens.css + chrome.i18n patterns + OFL self-host
  recipe + pre-checkpoint bundle gates as production conventions

Out-of-scope discovery (Wave 7 pre-checkpoint, logged for Phase 5
hardening, NOT a Plan 01-12 regression): setimmediate polyfill
`new Function` in SW chunk via vite-plugin-node-polyfills. Pre-existing
across Phase 1 history; logged at deferred-items.md. Suggested
follow-up: switch to a minimal Buffer shim or inline Buffer primitives
to drop the polyfill entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:59:34 +02:00
865d394ae0 docs(01-12): wave-7 pre-checkpoint — log out-of-scope discovery (setimmediate polyfill new Function pre-existing)
Wave 7 pre-checkpoint bundle gates per feedback-pre-checkpoint-bundle-gates.md
revealed a pre-existing benign concern in the SW production bundle:

`vite-plugin-node-polyfills` (configured for Buffer in vite.config.ts)
bundles the upstream `setimmediate` package which contains a fallback
`new Function("" + I)` evaluated when setImmediate is called with a
non-function argument. Production source code does NOT call
setImmediate(string); the construct is dead at the runtime call-graph
level but Rollup conservatively preserves it (behind a runtime
typeof check, not a static dead branch).

Verified pre-existing across Phase 1 history via `git checkout main --
src/background/index.ts vite.config.ts && npm run build` — same
`new Function` count. Plan 01-12 made NO changes to the polyfill
configuration; this is logged for future tightening (Phase 5
hardening or a dedicated MV3 CSP audit plan), NOT for fix in this
plan per the deviation-rule SCOPE BOUNDARY.

All other pre-checkpoint bundle gates PASS:
- Tier-1 forbidden-strings: 13/13 GREEN (no new test-mode symbols)
- SW-bundle-import: 15/15 GREEN
- Node-globals (Buffer.*) in SW chunk: 0
- DOM-globals direct SW calls: none
- Manifest validation: PASS (__MSG_*__ + default_locale='en' +
  16 i18n keys per locale; en+ru parity verified)
- Tokens.css MV3 CSP self-host: 0 googleapis / 0 https://fonts in dist/
- Icons rasterized: 8-bit RGBA at 406/784/1952 B
- vitest: 147/147 GREEN
- npm run test:uat: 21/21 GREEN (A1..A14 regression-free + A18..A22
  new + A23 from 01-14)
- npx tsc --noEmit: clean
- npm run build + npm run build:test: clean

Surfacing Wave 7 operator brand-fit checkpoint to orchestrator next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:47:13 +02:00
b909c374cc feat(01-12): wave-6 task-1 — harness A18-A22 (font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens)
UAT harness extended with 5 new page-side assertions following the
01-13 Approach B pattern (page-side assertA* + host-side driveA*
wrapper + harness.test.ts orchestrator entry):

A18 — Lora WOFF2 reachable from harness page (font self-host MV3 CSP
  invariant). Walks document.styleSheets for the first @font-face rule
  referencing Lora, resolves the rebased asset URL (handles Vite's
  content-hashing), fetches, asserts byteLength >= 40_000 (subset Lora
  is ~49 KB) + WOFF2 signature 'wOF2'. 4 checks.

A19 — icons rasterized from Loom mark (not Bug A placeholders). Fetches
  icon128.png, parses IHDR bytes 24-25 (bit-depth + color-type),
  asserts (8, 6) RGBA vs the placeholder (16, 2) RGB. 2 checks.

A20 — manifest:name resolves via chrome i18n. Reads
  chrome.runtime.getManifest().name; asserts it matches EN extName
  'Mokosh — Session Capture' OR RU 'Mokosh — Запись сессии' (robust
  to whatever locale Chrome uses); explicitly checks no __MSG_
  placeholder leaks. 2 checks.

A21 — --mks-font-display resolves to Lora stack. Creates transient
  .mks-display-1 probe div, reads getComputedStyle.fontFamily,
  asserts the stack starts with 'Lora' or '"Lora"' (accommodates
  both quoted + unquoted forms across Chrome versions); explicitly
  checks no Newsreader leak (R2 substitution complete). 2 checks.

A22 — welcome page tokens.css adoption (CONDITIONAL on Plan 01-10).
  Skip-gate on missing welcome.html: catches both HTTP 404 AND
  network-layer fetch failure (Chrome extensions throw TypeError
  'Failed to fetch' for non-web_accessible_resources paths). On
  reachable: extracts <link rel=stylesheet> hrefs, fetches each,
  asserts >= 3 var(--mks-*) usages OR tokens.css reference. 1 check.

Companion changes:

- tests/uat/extension-page-harness.html gains `<link rel="stylesheet"
  href="../../src/shared/tokens.css">` so A18 + A21 have the @font-face
  rules + .mks-display-1 class + CSS custom properties resolvable via
  document.styleSheets + getComputedStyle. Vite's crxjs plugin handles
  the asset path rebasing at build:test time.

- tests/uat/lib/harness-page-driver.ts: driveA18..driveA22 wrappers
  following the established driveA8 pattern (page.evaluate → window.
  __mokoshHarness.assertXX). No new host-side fs/ffprobe primitives;
  all A18-A22 work is page-side.

- tests/uat/harness.test.ts: orchestrator drivers list extended with
  A18-A22 between A14 and A23. FORBIDDEN_HOOK_STRINGS UNCHANGED at 12
  entries post-Plan-01-14 (A18-A22 use production chrome.* + fetch +
  getComputedStyle exclusively; no new test-mode symbols).

Verification (this commit):
- npm run test:uat: 21/21 GREEN (was 16/16 post-01-14)
- SKIP_BUILD=1 npm test: 147/147 GREEN
- Tier-1 grep gate: 13/13 GREEN (no FORBIDDEN_HOOK_STRINGS growth)
- npx tsc --noEmit: clean
- npm run build + npm run build:test: both succeed

The chain of A1..A14 + A18..A22 + A23 runs in ~95 seconds end-to-end
under Puppeteer headless mode against the bundled Chrome at
~/.cache/puppeteer/chrome/linux-148.0.7778.167.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:41:51 +02:00
e8d2881874 feat(01-12): wave-5 task-1 — welcome i18n migration (conditional on 01-10) + __VITE_DEV__ define + scripts/README.md
Plan 01-10 (welcome tab) has NOT yet landed at execute-plan time
(verified: ls src/welcome/welcome.html returns absent). Per Wave 5
branch 2B, src/welcome/* file modifications are DEFERRED — when Plan
01-10 lands, its executor will use src/shared/tokens.css directly
(skipping the placeholder welcome-tokens.css step entirely; the
canonical tokens.css is already import-ready from src/shared/).

Unconditional changes in this wave:

1. vite.config.ts gains __VITE_DEV__ define-token (RESEARCH §12 +
   D-09 spirit-satisfaction). Defaults to false; activates iff env
   var VITE_DEV=1 is set. Reserved for any future inline smoke-mode
   check. Currently smoke.sh lives entirely outside Vite's input set
   so the gate is a defensive no-op:
     define: { __MOKOSH_UAT__: 'false', __VITE_DEV__: JSON.stringify(...) }

2. vite.test.config.ts inherits __VITE_DEV__ via mergeConfig (the
   test config only overrides __MOKOSH_UAT__: 'true'; __VITE_DEV__
   from base flows through untouched).

3. scripts/README.md (NEW, ~50 lines): documents the smoke-isolation
   invariant — dev-only scripts in scripts/ are NOT bundled by
   `npm run build`; the production dist/ contains zero smoke
   artifacts (verified by RESEARCH §12 grep gate). Provides usage
   example for VITE_DEV env override + cross-references RESEARCH §12
   and brand-decisions-v1.md D-09. Index lists subset-fonts.sh,
   rasterize-icons.sh, and smoke.sh (if present).

Note on Plan 01-10 deferral: when Plan 01-10 executes after this
plan closes, the welcome page src/welcome/welcome.css can either
@import '../shared/tokens.css' directly OR a thin welcome-tokens.css
re-export — both paths are supported by the canonical tokens.css
landed in Wave 1. Plan 01-10's executor must adopt chrome.i18n.getMessage
for any welcome copy strings using the 16-key matrix in _locales/
(welcomeHeroRu + welcomeHeroEn already defined; additional keys
added per Plan 01-10's own artifact list).

Verification:
- vitest baseline 147/147 GREEN (no change from Wave 4 close)
- npm run build clean (no warnings; __VITE_DEV__ propagates through
  define static replacement)
- scripts/README.md exists with the smoke-isolation paragraph

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:29:07 +02:00
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