Commit Graph

4 Commits

Author SHA1 Message Date
c4161431e7 feat(04-06): Wave 1 GREEN — dark-logo currentColor strategy + inline-SVG injection
UI-SPEC Option A landed end-to-end at the source layer:

- src/shared/brand/mokosh-mark.svg: single-attribute change on the root
  <svg> — stroke="#181b2a" → stroke="currentColor". The 12 <line> + 1
  <rect> children inherit stroke from the root and are UNCHANGED. This
  switches the mark from a hardcoded near-black ink to inheriting the
  parent CSS `color` cascade (W3C SVG2 §13.3).
- src/welcome/welcome.ts: `import markUrl from '..mokosh-mark.svg?url'`
  → `import markSvg from '..mokosh-mark.svg?raw'`. populateMark()
  rewritten to inline-inject the SVG via DOMParser + replaceChildren
  (NOT <img>, NEVER innerHTML — MV3 CSP discipline / T-04-06-01).
  The inline <svg> inherits `color: var(--mks-fg-inverse)` from the
  `.welcome-hero__mark` wrapper (welcome.css:67); on the dark surface
  the `.dark` token override (tokens.css 234-251) flips the resolved
  colour automatically — contrast-correct on both surfaces, no JS
  branching. The bare class selector `.welcome-hero__mark-img`
  (welcome.css:91-95) is tag-agnostic so width/height/display rules
  apply identically to the injected <svg>. role='img' + aria-label
  preserve the prior accessibility shape.
- globals.d.ts: append the `declare module '*.svg?raw'` ambient block
  alongside the existing `*.svg?url` + `*.webm?url` blocks so tsc
  accepts the new import.

Gates run:
- npx tsc --noEmit exit 0.
- npm test against tests/welcome/inline-svg.test.ts + tests/build/
  cursor-visibility.test.ts: 4/4 GREEN (the 3 Wave-0 RED inline-svg
  tests flipped to GREEN; cursor-visibility stays GREEN).
- Full vitest: 187 passed / 1 failed (188 total). The single RED is
  tests/background/webm-remux.test.ts > ffprobe -count_frames timeout
  — the documented 04-CONTEXT #9/#10 parallel-vitest / ffprobe flake
  family. Re-run in isolation: 5/5 GREEN. TOLERATED per the Task 2
  VITEST GATE LOGIC (isolation-passing flake is NOT a regression).
- npm run build exit 0; the welcome chunk JS bundles the raw SVG
  source (currentColor + viewBox="0 0 32 32" both present in
  dist/assets/welcome-Bkrf1_bZ.js).

References:
- 04-UI-SPEC.md §"Implementation amendment" (the 2-part technique).
- Vite ?raw query: https://vite.dev/guide/assets.html#importing-asset-as-string
- W3C SVG2 §13.3 (currentColor inheritance).
- DOMParser is CSP-safe per MDN (no script execution).
2026-05-26 07:58:06 +02:00
81d9935b65 feat(04-08): video-file MediaStream + sync-install/lazy-first-frame + explicit WAR — methodology reframe per debug session-2 + iter-2 BLOCKER fixes
Task 1 of Plan 04-08 (methodology reframe of ROADMAP SC #1):

- Bundle 1.9 MB VP9 WebM fixture at tests/uat/fixtures/synthetic-display-source.webm (copy of internal Plan 01-07 fixture; CC0-equivalent project-owned)
- Add globals.d.ts ambient `*.webm?url` module decl (mirrors Plan 01-10 `*.svg?url`)
- Add manifest.json web_accessible_resources entry for `assets/*.webm` (iter-2 BLOCKER 1 — pre-decided to avoid executor improvisation; inert in production where dist/ has zero *.webm)
- Rewrite installFakeDisplayMedia() at src/test-hooks/offscreen-hooks.ts:
  * Replace canvas.captureStream(30) with HTMLVideoElement.captureStream(30) — bypasses Chrome bug 653548 invisible-canvas throttling (debug session-2 root cause)
  * Function signature remains SYNCHRONOUS (`: void`; iter-2 BLOCKER 2 — eager-install contract preserved at lines 528-537)
  * Video element creation + DOM append + monkey-patch assignment execute synchronously
  * canplay wait + .play() deferred INTO fakeGetDisplayMedia closure (lazy first-frame pattern)
  * fakeVideoReadyPromise kicked off at install time so first call observes resolved Promise
  * WARNING 1 (autoplay reject): explicit error class identifier 'autoplay-blocked or codec-unsupported in headless context'
  * displaySurface monkey-patch preserved verbatim
  * A23 lastGetDisplayMediaConstraints capture preserved
  * uninstallFakeDisplayMedia teardown adapted for videoEl (pauses + removes + nulls)
  * All 6 bridge ops UNCHANGED in their sync return-false form
- Add Tier-2 production-bundle filename-leak gate at tests/background/no-test-hooks-in-prod-bundle.test.ts (iter-2 WARNING 5 — synthetic-display-source string must be 0 hits in dist/)

Verification:
- npx tsc --noEmit: exit 0
- npm run build: dist/ produced; 0 *.webm files; 0 synthetic-display-source hits
- npm run build:test: dist-test/assets/synthetic-display-source-mbtR1t3u.webm emitted (1.9 MB; Vite ?url asset)
- Code-only grep (comment-filtered) on offscreen-hooks.ts: 0 canvas refs; 15 video refs
- installFakeDisplayMedia signature unchanged: `: void` 2x; `: Promise` 0x; `await installFakeDisplayMedia` 0x
- Architectural invariant unchanged: `let segments: Blob[] = []` at recorder.ts:91 (1 hit; grep gate enforces)
- Tier-1 FORBIDDEN_HOOK_STRINGS unchanged at 12 entries
- Tier-2 vitest gate PASSES: 14/14 GREEN under SKIP_BUILD=1 (12 Tier-1 + 1 build verify + 1 Tier-2)

Per iter-3 checker advisory 1: the wrong-display-surface throw lives at recorder.ts:313-321 (not line 294 as plan text states; off by ~25 lines but unambiguous).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:33:04 +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
cb1a729962 feat(01-11): wave-1 — gated test hooks for SW + offscreen, dist/ stays hook-free
Task 2 of Plan 01-11 (Puppeteer UAT harness).

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:46:26 +02:00