From 8d1c8fb0cc11d182c1d98e6bb7c7ce2d8add053e Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 19 May 2026 14:59:59 +0200 Subject: [PATCH] docs(01-12): create Plan 01-12 (Design Integration; R2 Lora unblocks; 7 waves) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .planning/ROADMAP.md | 8 +- .../01-stabilize-video-pipeline/01-12-PLAN.md | 1246 +++++++++++++++++ 2 files changed, 1253 insertions(+), 1 deletion(-) create mode 100644 .planning/phases/01-stabilize-video-pipeline/01-12-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 4720014..c1bb718 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -70,7 +70,7 @@ directory + `vite.config.ts` inline string + `src/background/`. decoder errors, only expected muxer DTS-monotonicity warnings at segment join boundaries). -**Plans**: 7 plans +**Plans**: 13 plans (01-01 through 01-13) - [x] 01-01-PLAN.md — Doc cascade: amend DEC-003 / DEC-010 / RETIRE constraints / swap manifest permissions (D-A1..D-A6) - [x] 01-02-PLAN.md — Wave-0 test infrastructure: Vitest install + 4 RED test files + fixtures placeholder - [x] 01-03-PLAN.md — Offscreen recorder TDD: ring buffer + codec strict-mode + getDisplayMedia + track-ended cleanup; D-13 fallback skeleton pre-staged @@ -78,6 +78,12 @@ directory + `vite.config.ts` inline string + `src/background/`. - [x] 01-05-PLAN.md — SW shrink: delete legacy buffer + alarms + IndexedDB + tabCapture paths; wire SW-side onConnect host - [x] 01-06-PLAN.md — Build pipeline collapse: delete vite.config.ts inline plugin + top-level offscreen/ dir; declare rollupOptions.input - [x] 01-07-PLAN.md — Manual smoke + ffprobe D-12 acceptance gate + A3 empirical-playback gate; D-12 + A3 debug sessions resolved mid-execution via pre-staged base64 wire format + D-13 restart-segments; regression fixture committed; SPEC §10 #2/#3/#7 functionally green (Closed 2026-05-15) +- [x] 01-08-PLAN.md — WebM remux via ts-ebml + webm-muxer (replaces D-13 file-concat; closes SPEC §10 #7 playability per debug d13-multi-ebml-concat-unplayable.md) +- [x] 01-09-PLAN.md — Toolbar onClicked direct flow + monitor-only picker + onStartup notification + 3-state badge state machine; closure-by-harness Amendment 2 (Plan 01-13 PASS substitutes for operator UAT) +- [ ] 01-10-PLAN.md — Welcome tab (Hero + Loom dial per D-02; first-install onboarding; harness A15-A17) +- [x] 01-11-PLAN.md — UAT harness Approach-A spike (PIVOTED to 01-13; carries forward Wave 0 infrastructure + Tier-1 grep gate; falsified hypotheses recorded) +- [ ] 01-12-PLAN.md — Design integration (R2 Lora self-host, src/shared/tokens.css canonical, 8 i18n strings + 4 supporting keys, branded Loom icons, manifest i18n; harness A18-A22) +- [x] 01-13-PLAN.md — UAT harness via Approach B (extension-internal-page driver + offscreen synthetic stream; 15/15 GREEN; Plan 01-09 functional closure) ### Phase 2: Stabilize DOM + event capture privacy **Goal**: rrweb captures DOM events on typical pages and the user-event log diff --git a/.planning/phases/01-stabilize-video-pipeline/01-12-PLAN.md b/.planning/phases/01-stabilize-video-pipeline/01-12-PLAN.md new file mode 100644 index 0000000..dbe8f68 --- /dev/null +++ b/.planning/phases/01-stabilize-video-pipeline/01-12-PLAN.md @@ -0,0 +1,1246 @@ +--- +phase: 01-stabilize-video-pipeline +plan: 12 +type: tdd +wave: 6 +depends_on: + - 01-09 + - 01-13 +files_modified: + - src/shared/tokens.css + - src/shared/fonts/Lora-VariableFont.woff2 + - src/shared/fonts/Lora-Italic-VariableFont.woff2 + - src/shared/fonts/IBMPlexSans-Regular.woff2 + - src/shared/fonts/IBMPlexSans-Medium.woff2 + - src/shared/fonts/IBMPlexSans-SemiBold.woff2 + - src/shared/fonts/IBMPlexSans-Bold.woff2 + - src/shared/fonts/IBMPlexMono-Regular.woff2 + - src/shared/fonts/IBMPlexMono-Medium.woff2 + - src/shared/fonts/LICENSE-Lora.txt + - src/shared/fonts/LICENSE-IBM-Plex.txt + - src/shared/fonts/README.md + - src/shared/brand/mokosh-mark.svg + - src/shared/brand/mokosh-lockup.svg + - icons/icon16.png + - icons/icon48.png + - icons/icon128.png + - scripts/rasterize-icons.sh + - scripts/subset-fonts.sh + - scripts/README.md + - manifest.json + - _locales/en/messages.json + - _locales/ru/messages.json + - src/popup/index.html + - src/popup/index.ts + - src/popup/style.css + - src/welcome/welcome.css + - src/welcome/welcome-tokens.css + - src/welcome/welcome.ts + - src/welcome/copy.ts + - src/background/index.ts + - vite.config.ts + - vite.test.config.ts + - tests/build/tokens-adopted.test.ts + - tests/build/fonts-present.test.ts + - tests/build/icons-present.test.ts + - tests/build/no-remote-fonts.test.ts + - tests/i18n/manifest-i18n.test.ts + - tests/i18n/locale-parity.test.ts + - tests/uat/extension-page-harness.ts + - tests/uat/lib/harness-page-driver.ts + - tests/uat/harness.test.ts + - .planning/STATE.md + - .planning/ROADMAP.md +autonomous: false +requirements: + - REQ-video-ring-buffer + - REQ-install-clean + - REQ-manifest-permissions +tags: + - design-integration + - brand + - tokens-css + - woff2-selfhost + - svg-rasterization + - mv3-csp + - i18n + - locales + - chrome-i18n + - D-01-mark + - D-02-welcome + - D-03-voice-sober + - D-04-palette + - D-05-typography + - D-06-icon-strategy + - D-07-extname-override + - D-08-tagline + - D-09-smoke-dev-only + - R2-lora-substitute + - Approach-B-harness-extension + - harness-A18-A22 + +must_haves: + truths: + - "Self-hosted WOFF2 font bundle lands at src/shared/fonts/: Lora variable (Latin+Cyrillic via OFL Cyreal foundry release, replaces Newsreader per R2 designer reply 2026-05-19); IBM Plex Sans 400/500/600/700 latin+cyrillic; IBM Plex Mono 400/500 latin+cyrillic. Total bundle ~250-300KB. LICENSE-Lora.txt + LICENSE-IBM-Plex.txt + README.md ship alongside per OFL-1.1 attribution best practice." + - "src/shared/tokens.css is the canonical token system: copy of .planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css with two surgical edits — (a) line 12 Google Fonts @import REPLACED with 7 local @font-face rules (Lora variable normal + italic; Plex Sans 4 weights; Plex Mono 2 weights), and (b) --mks-font-display value substituted from 'Newsreader' to 'Lora' per R2. Adds .mks-word class definition (font-family: var(--mks-font-display); font-size: 32px; font-weight: var(--mks-weight-regular); fill: var(--mks-ink-900);) required by mokosh-lockup.svg line 21." + - "src/shared/tokens.css contains ZERO references to fonts.googleapis.com or any https:// URL — MV3 CSP `style-src 'self'` + `font-src 'self'` enforced. Verified by tests/build/no-remote-fonts.test.ts grepping the built dist/ for the substring `googleapis` (occurrences === 0) and `https://fonts` (occurrences === 0)." + - "icons/icon{16,48,128}.png are rasterized from src/shared/brand/mokosh-mark.svg via rsvg-convert (per RESEARCH §3 verified locally: 16=406B, 48=784B, 128=1952B). Each clears the assets-spec.md FLOOR: 16≥200B, 48≥500B, 128≥1024B. PNGs are committed as static artifacts (NOT regenerated at build time per RESEARCH §3 anti-pattern); scripts/rasterize-icons.sh is the documented re-run recipe. The Bug A placeholder icons (dark-square + green-dot per Plan 01-09 Path A; verified pre-existing as 574B/1153B/2615B 16-bit RGB at icons/icon{16,48,128}.png) are OVERWRITTEN." + - "manifest.json:name = '__MSG_extName__' (was 'AI Call Recorder'); manifest.json:description = '__MSG_extDesc__' (was 'Запись сессий операторов для диагностики ошибок'); manifest.json:default_locale = 'en'; manifest.json:action.default_title = '__MSG_tooltipOff__'. _locales/en/messages.json carries extName='Mokosh — Session Capture' per D-07 user override and extDesc='Thirty seconds ago, always at hand.' per D-08 (using brand-decisions-v1.md final wording, not Brief §02's '...within reach.' variant — A4 from RESEARCH Open Questions resolved to brand-decisions-v1.md as the more recent doc)." + - "8 i18n keys land in BOTH _locales/en/messages.json AND _locales/ru/messages.json (default_locale parity per RESEARCH Pitfall 4): extName, extDesc, tooltipOff, tooltipRecPrefix, tooltipErr, popupSavePrompt, popupSaveCta, popupSaveDone, notifStartup, notifRecovery, welcomeHeroRu, welcomeHeroEn (12 total keys; the 8 Brief §02 strings + 4 supporting keys including manifest fields and tooltipRec split). Every key in ru/messages.json is also present in en/messages.json (verified by tests/i18n/locale-parity.test.ts)." + - "src/background/index.ts is migrated from hardcoded brand strings (line 82 BADGE_REC_TITLE 'Recording — last 30 s buffered. Click to save.'; line 83 BADGE_OFF_TITLE 'Not recording. Click to start.'; line 84 BADGE_ERROR_TITLE 'Recording error. Click to try again.'; line 862 'Mokosh stopped'; line 863 'Recording stopped. Click here to start a new session.'; line 964 'Mokosh ready'; line 965 startup-message) to chrome.i18n.getMessage('') reads. BADGE_REC_COLOR ('#00C853' material-green at line 76) is updated to '#b2543d' (= --mks-madder-600 per D-04 loom palette) with a comment cross-referencing the token, per RESEARCH §10 Open Questions A7 default-action. BADGE_OFF_COLOR + BADGE_ERROR_COLOR remain as plan-discretion engineering choices unless the palette dictates otherwise; document as such in the source comment." + - "src/popup/index.html title element becomes '__MSG_extName__' (or empty, populated by JS via chrome.i18n.getMessage('extName')); span.button-text remains empty (populated by JS per RESEARCH Pitfall 3); src/popup/index.ts updateUI() reads buttonText.textContent = chrome.i18n.getMessage('popupSaveCta') in idle case, 'popupSaveDone' in done case; the 'Архив успешно сохранён!' literal at saveArchive() success branch (line 91) becomes chrome.i18n.getMessage('popupSaveDone'); the info-text 'Последние 30 сек видео + 10 мин лога' at index.html line 15 becomes a data-mks-key='popupInfoText' attribute populated by JS. src/popup/style.css imports `../shared/tokens.css` and ALL hardcoded hex literals (#2563eb, #1d4ed8, #94a3b8, #f8f9fa, #0891b2, #059669, #dc2626, #64748b) are replaced with var(--mks-*) references (--mks-rec for save button, --mks-success for done state, --mks-error for error, --mks-fg-2 for info text, --mks-surface for body bg, --mks-fg-disabled for disabled, --mks-warning for warning info)." + - "If Plan 01-10 (welcome tab) has landed before Plan 01-12 executes: src/welcome/welcome-tokens.css placeholder is REPLACED with a one-liner `@import '../shared/tokens.css';`, and any per-key copy literals in src/welcome/copy.ts are migrated to chrome.i18n.getMessage('') reads (welcomeHeroRu, welcomeHeroEn) with the in-file COPY map removed or reduced to a fallback shim. If Plan 01-10 has NOT yet landed: src/welcome/* files are NOT created by Plan 01-12; the canonical tokens.css is import-ready from src/shared/tokens.css and Plan 01-10 executor can swap to it from day 1 (no placeholder welcome-tokens.css needed)." + - "vite.config.ts gains a `define` block setting __VITE_DEV__ = JSON.stringify(process.env.VITE_DEV === '1') per RESEARCH §12 D-09 spirit-satisfaction (defensive flag for any future inline smoke-mode check; smoke.sh continues to live outside Vite's input set entirely — verified by `grep -rn 'smoke\\|SMOKE\\|data:text/html' src/` returning empty). vite.test.config.ts inherits via mergeConfig. scripts/README.md (NEW) documents the smoke isolation invariant in one paragraph." + - "Tier-1 forbidden-strings inventory in tests/background/no-test-hooks-in-prod-bundle.test.ts AND tests/uat/harness.test.ts is UNCHANGED at 10 strings (no new test-mode symbols introduced by Plan 01-12; the i18n + tokens + font work uses production chrome.* APIs end-to-end). Verified by file-diff after plan close." + - "UAT harness extended with 5 new assertions A18-A22: A18 = Lora WOFF2 loads from chrome.runtime.getURL('shared/fonts/Lora-VariableFont.woff2') OR equivalent emitted asset path (response.ok && content-length > 50000 — Lora variable Latin+Cyrillic is ~80-120KB after subsetting). A19 = icons are NOT the Bug A placeholders (icon128.png file size differs from the known 2615-byte 16-bit-RGB placeholder; OR icon128.png file content hash differs from the placeholder content hash). A20 = chrome.runtime.getManifest().name === 'Mokosh — Session Capture' (i18n resolution test; the en-locale fallback always returns this exact string per default_locale='en'). A21 = getComputedStyle on a test element with CSS class .mks-display-1 inside the harness page resolves to a font-family stack starting with 'Lora' (not 'Newsreader', not system fallback). A22 = welcome page tokens.css is loaded if 01-10 has landed (fetch chrome.runtime.getURL('src/welcome/welcome.html') returns 200 + welcome.css resolves --mks-rec to '#b2543d'); skip with a documented diagnostic if 01-10 has not landed." + - "Every Plan 01-13 harness assertion (A0-A14) and Plan 01-10 assertion (A15-A17, IF landed) remains GREEN after Plan 01-12 changes (no regression). vitest baseline grows by 6 tests (icons-present + tokens-adopted + fonts-present + no-remote-fonts + manifest-i18n + locale-parity) from 98 GREEN to 104 GREEN; npm run test:uat grows from 15/15 (or 18/18 with 01-10) to 20/20 (or 23/23 with 01-10 + 01-12) — all GREEN." + - "MV3 architectural constraints from 01-11-SUMMARY enforced: NO `await import(...)` anywhere in src/background/index.ts (chrome.i18n.getMessage is a synchronous API; no dynamic imports required); test-mode symbols stay in dist-test/ only via __MOKOSH_UAT__ define-token (Plan 01-12 introduces no new test-mode symbols)." + artifacts: + - path: "src/shared/tokens.css" + provides: "Canonical token system (Mokosh design tokens; single source of truth). Copy of design-incoming tokens.css with: (1) line 12 Google Fonts @import REMOVED + replaced with 7 local @font-face rules pointing at ./fonts/*.woff2 (Lora variable normal + italic; Plex Sans 400/500/600/700; Plex Mono 400/500); (2) --mks-font-display value substituted from 'Newsreader' → 'Lora' per R2 designer reply 2026-05-19; (3) .mks-word class definition added (font-family: var(--mks-font-display); font-size: 32px; font-weight: var(--mks-weight-regular); fill: var(--mks-ink-900); letter-spacing: var(--mks-tracking-display);) at end-of-file per RESEARCH §8 + lockup SVG line 21 requirement. ZERO https:// URLs anywhere in this file." + contains: "@font-face" + - path: "src/shared/fonts/Lora-VariableFont.woff2" + provides: "Lora variable font, normal style, weight axis 400-700, subset to Latin (U+0020-007E, U+00A0-00FF) + Cyrillic basic (U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116) via pyftsubset --flavor=woff2 from upstream OFL .ttf at github.com/cyrealtype/Lora-Cyrillic or Google Fonts metadata source. Expected size 80-120KB." + - path: "src/shared/fonts/Lora-Italic-VariableFont.woff2" + provides: "Lora variable italic, same subset as above. Expected size 80-120KB. NB: if upstream Lora ships italic in same file as normal via opsz/wght/ital axes (variable font with multi-axis support), this file may be omitted — A5 from RESEARCH Assumptions Log: verify upstream variable build structure at execute-plan time and adjust if italic+normal collapse into one file." + - path: "src/shared/fonts/IBMPlexSans-Regular.woff2" + provides: "IBM Plex Sans 400, Latin + Cyrillic basic. ~25KB per RESEARCH §1 sizing table." + - path: "src/shared/fonts/IBMPlexSans-Medium.woff2" + provides: "IBM Plex Sans 500, Latin + Cyrillic basic. ~25KB." + - path: "src/shared/fonts/IBMPlexSans-SemiBold.woff2" + provides: "IBM Plex Sans 600, Latin + Cyrillic basic. ~25KB." + - path: "src/shared/fonts/IBMPlexSans-Bold.woff2" + provides: "IBM Plex Sans 700, Latin + Cyrillic basic. ~25KB." + - path: "src/shared/fonts/IBMPlexMono-Regular.woff2" + provides: "IBM Plex Mono 400, Latin + Cyrillic basic. ~28KB." + - path: "src/shared/fonts/IBMPlexMono-Medium.woff2" + provides: "IBM Plex Mono 500, Latin + Cyrillic basic. ~28KB." + - path: "src/shared/fonts/LICENSE-Lora.txt" + provides: "Verbatim copy of Lora OFL-1.1 license + Cyreal foundry copyright lines, per OFL-FAQ best practice." + - path: "src/shared/fonts/LICENSE-IBM-Plex.txt" + provides: "Verbatim copy of IBM Plex OFL-1.1 license from github.com/IBM/plex/blob/master/LICENSE.txt." + - path: "src/shared/fonts/README.md" + provides: "One-pager attribution + regeneration recipe per RESEARCH §7 template. Lists all bundled faces + upstream URLs + subsetting recipe + license notes." + min_lines: 15 + - path: "src/shared/brand/mokosh-mark.svg" + provides: "Copy of .planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/mokosh-mark.svg (Loom 2×2 weave intersection; D-01 brand mark). Source-of-truth for icon rasterization. Stays at hardcoded stroke='#181b2a' for PNG output; future Plan 01-10 may copy + adapt to currentColor for inline-SVG welcome usage (see RESEARCH §5)." + - path: "src/shared/brand/mokosh-lockup.svg" + provides: "Copy of mokosh-lockup.svg (mark + 'Mokosh' wordmark at 240×56 viewBox). The .mks-word class referenced at line 21 is defined in src/shared/tokens.css per artifact above." + - path: "icons/icon16.png" + provides: "Rasterized Loom mark at 16×16 via `rsvg-convert -w 16 -h 16 src/shared/brand/mokosh-mark.svg -o icons/icon16.png`. ≥200 bytes (~406B per RESEARCH §3 verified). REPLACES the prior 574B 16-bit-RGB placeholder." + - path: "icons/icon48.png" + provides: "Rasterized Loom mark at 48×48. ≥500 bytes (~784B). REPLACES the prior 1153B 16-bit-RGB placeholder." + - path: "icons/icon128.png" + provides: "Rasterized Loom mark at 128×128. ≥1024 bytes (~1952B). REPLACES the prior 2615B 16-bit-RGB placeholder." + - path: "scripts/rasterize-icons.sh" + provides: "One-off rasterization recipe per RESEARCH §3. Bash script reading src/shared/brand/mokosh-mark.svg and emitting icons/icon{16,48,128}.png via rsvg-convert. Documented for re-run when the mark SVG changes; NOT wired into prebuild (anti-pattern per RESEARCH §3)." + min_lines: 15 + - path: "scripts/subset-fonts.sh" + provides: "One-off font subsetting recipe per RESEARCH §1. Bash script for Lora (Latin+Cyrillic subset, variable normal + italic), Plex Sans 400/500/600/700 (Latin+Cyrillic), Plex Mono 400/500 (Latin+Cyrillic), via pyftsubset --flavor=woff2 from upstream .ttf sources downloaded one-off into a scratch dir. Documents the canonical --unicodes string." + min_lines: 30 + - path: "scripts/README.md" + provides: "One-paragraph note per RESEARCH §12: smoke.sh and other dev-only scripts live in this directory and are NOT bundled by `npm run build`. The production dist/ contains no smoke artifacts (verified by `grep -rn smoke dist/` returning empty)." + min_lines: 8 + - path: "manifest.json" + provides: "Updated manifest: `name` = '__MSG_extName__'; `description` = '__MSG_extDesc__'; `default_locale` = 'en' (NEW field); `action.default_title` = '__MSG_tooltipOff__' (NEW field per RESEARCH §11 + Brief §02 string #1). All other fields preserved unchanged (permissions, host_permissions, background, content_scripts, action.default_popup, action.default_icon, icons)." + contains: "default_locale" + - path: "_locales/en/messages.json" + provides: "English locale messages (default_locale fallback). 12 keys: extName, extDesc, tooltipOff, tooltipRecPrefix, tooltipErr, popupSavePrompt, popupSaveCta, popupSaveDone, popupInfoText, notifStartup, notifRecovery, welcomeHeroRu, welcomeHeroEn. extName='Mokosh — Session Capture' per D-07; extDesc='Thirty seconds ago, always at hand.' per D-08 (brand-decisions-v1.md wording). Strings inherit D-03 Sober register. Each key has both message + description fields per Chrome i18n schema." + min_lines: 70 + - path: "_locales/ru/messages.json" + provides: "Russian locale messages (primary operator locale). Same 12 keys as en/messages.json with Russian text per RESEARCH §10 Brief §02 verbatim extraction: extName='Mokosh — Запись сессии'; extDesc='Тридцать секунд назад, всегда под рукой.'; tooltipOff='Mokosh — щёлкните, чтобы начать запись'; tooltipRecPrefix='Mokosh — идёт запись'; tooltipErr='Mokosh — ошибка записи, щёлкните для восстановления'; popupSavePrompt='Сохранить отчёт об ошибке?'; popupSaveCta='Сохранить отчёт'; popupSaveDone='Архив сохранён в Загрузки.'; popupInfoText='Последние 30 сек видео + 10 мин лога'; notifStartup='Запись запущена. Я слежу за последними 30 секундами.'; notifRecovery='Запись возобновлена. Буфер снова заполняется.'; welcomeHeroRu='Тридцать секунд назад, всегда под рукой.'; welcomeHeroEn='Thirty seconds ago, always at hand.'." + min_lines: 70 + - path: "src/popup/index.html" + provides: "Popup HTML migrated to i18n: Mokosh — Session Capture retained (chrome doesn't substitute __MSG__ in HTML body per RESEARCH Pitfall 3); button.button-text starts empty (populated by JS); info-text gains data-mks-key='popupInfoText' attribute; statusMessage retained for runtime status. Stylesheet link to style.css unchanged." + contains: "data-mks-key" + - path: "src/popup/index.ts" + provides: "Popup TS migrated to chrome.i18n.getMessage: updateUI() idle case sets buttonText.textContent = chrome.i18n.getMessage('popupSaveCta'); done case sets 'Готово! ✓' replaced with chrome.i18n.getMessage('popupSaveDone') OR a dedicated 'popupSaveDoneShort' key; init() populates all [data-mks-key] elements via a small helper. Empty-state message line 62 'Откройте запись через иконку расширения' becomes chrome.i18n.getMessage('popupEmptyState') with a corresponding key added to messages.json. Logger import + log helper unchanged." + contains: "chrome.i18n.getMessage" + - path: "src/popup/style.css" + provides: "Restyled per D-04 loom palette: @import '../shared/tokens.css' at top; body { background: var(--mks-surface); color: var(--mks-fg-1); font-family: var(--mks-font-ui); }; .save-button { background: var(--mks-rec); color: var(--mks-fg-inverse); border-radius: var(--mks-radius-md); box-shadow: var(--mks-shadow-1); }; .save-button:hover, .save-button.saving, .save-button.done all use --mks-madder-700, --mks-amber-600, --mks-moss-600 respectively; .info-text uses var(--mks-fg-2); .status-message.* use --mks-error / --mks-success / --mks-warning. ZERO hex literals." + contains: "var(--mks-" + - path: "src/welcome/welcome.css" + provides: "(CONDITIONAL — only modified if Plan 01-10 has landed at execute-plan time): Replace placeholder color tokens with @import '../shared/tokens.css' at top; keep all var(--mks-*) references (welcome.css already uses var(--mks-*) per 01-10 design-swap-ready architecture). If 01-10 hasn't landed: this file is NOT created by Plan 01-12." + contains: "../shared/tokens.css" + - path: "src/welcome/welcome-tokens.css" + provides: "(CONDITIONAL — only modified if Plan 01-10 has landed): file becomes a one-liner `@import '../shared/tokens.css';` OR is deleted entirely with welcome.css importing tokens.css directly. If 01-10 hasn't landed: file is NOT created." + - path: "src/welcome/welcome.ts" + provides: "(CONDITIONAL — only modified if Plan 01-10 has landed): copy population uses chrome.i18n.getMessage('welcomeHeroRu'), chrome.i18n.getMessage('welcomeHeroEn'), and other welcome keys instead of the in-file COPY map. The COPY import from './copy' is removed OR copy.ts is reduced to a fallback-only shim. If 01-10 hasn't landed: file is NOT created." + contains: "chrome.i18n.getMessage" + - path: "src/welcome/copy.ts" + provides: "(CONDITIONAL — only modified if Plan 01-10 has landed): file becomes either deleted or a fallback shim (e.g., a function `getCopy(key)` that returns `chrome.i18n.getMessage(key) || COPY[key]` for grace under missing locales). If 01-10 hasn't landed: file is NOT created." + - path: "src/background/index.ts" + provides: "Migrated to i18n + token palette: lines 76 BADGE_REC_COLOR='#00C853' updated to '#b2543d' with comment `// --mks-madder-600 per D-04 loom palette`; lines 82-84 BADGE_*_TITLE constants are unused once setTitle reads chrome.i18n.getMessage at the call site (the 4 setTitle call sites in setBadgeState read chrome.i18n.getMessage('tooltipOff'|'tooltipRec'|'tooltipErr')); lines 862-865 notification title='Mokosh stopped' + message='Recording stopped...' migrate to chrome.i18n.getMessage('notifRecoveryTitle' or similar + 'notifRecovery'); line 964 notification 'Mokosh ready' + 'Click here to start recording your session.' migrate to chrome.i18n.getMessage('notifStartupTitle' + 'notifStartup'). For tooltipRec the runtime time-suffix `' (' + formatElapsed(seconds) + ')'` is concatenated after the chrome.i18n.getMessage('tooltipRecPrefix') call per RESEARCH §10 note. No dynamic imports added (MV3 SW constraint)." + contains: "chrome.i18n.getMessage" + - path: "vite.config.ts" + provides: "Adds `define` field property __VITE_DEV__ = JSON.stringify(process.env.VITE_DEV === '1') alongside existing __MOKOSH_UAT__: 'false', per RESEARCH §12 D-09 defensive flag. NO other changes (build target, resolve.alias, rollupOptions.input, plugins all preserved)." + contains: "__VITE_DEV__" + - path: "vite.test.config.ts" + provides: "Inherits __VITE_DEV__ via mergeConfig (base config gains the field; test config doesn't override). No structural change to existing __MOKOSH_UAT__: 'true' override or rollupOptions.input." + - path: "tests/build/tokens-adopted.test.ts" + provides: "Vitest unit test: src/shared/tokens.css exists + parses as CSS; popup/style.css contains `@import '../shared/tokens.css'`; src/popup/style.css contains ZERO `#[0-9a-fA-F]{3,8}` hex literals (regex match count === 0); src/welcome/welcome.css if exists contains ZERO hex literals." + min_lines: 30 + - path: "tests/build/fonts-present.test.ts" + provides: "Vitest unit test: every WOFF2 file declared in src/shared/tokens.css @font-face exists at the relative path; each is > 0 bytes; the file-name format ('Lora-VariableFont.woff2' or similar) matches the @font-face src URL. Validates the 7 expected files (Lora normal, Lora italic if separate, Plex Sans ×4, Plex Mono ×2) per Wave 0 inventory." + min_lines: 25 + - path: "tests/build/icons-present.test.ts" + provides: "Vitest unit test: icons/icon16.png ≥ 200 B; icons/icon48.png ≥ 500 B; icons/icon128.png ≥ 1024 B (Chrome imageUtil floors per assets-spec.md). Each is a PNG by signature (first 8 bytes match \\x89PNG\\r\\n\\x1a\\n). Each is at the expected dimensions (read via simple PNG header parse: width at bytes 16-19, height at bytes 20-23, network byte order)." + min_lines: 35 + - path: "tests/build/no-remote-fonts.test.ts" + provides: "Vitest unit test: `npm run build` runs (or SKIP_BUILD=1); reads dist/ recursively; asserts ZERO files contain 'googleapis' OR 'https://fonts'. Mirrors the existing no-test-hooks-in-prod-bundle.test.ts execFile pattern (RESEARCH §1 + §13 CSP migration verification)." + min_lines: 40 + - path: "tests/i18n/manifest-i18n.test.ts" + provides: "Vitest unit test: manifest.json parses; manifest.name === '__MSG_extName__'; manifest.description === '__MSG_extDesc__'; manifest.default_locale === 'en'; manifest.action.default_title === '__MSG_tooltipOff__'; _locales/en/messages.json parses with extName.message === 'Mokosh — Session Capture' + extDesc.message === 'Thirty seconds ago, always at hand.'; _locales/ru/messages.json parses with extName.message === 'Mokosh — Запись сессии' + extDesc.message === 'Тридцать секунд назад, всегда под рукой.'." + min_lines: 35 + - path: "tests/i18n/locale-parity.test.ts" + provides: "Vitest unit test: every key in _locales/ru/messages.json exists in _locales/en/messages.json (default_locale parity per RESEARCH Pitfall 4). Every key's .message field is a non-empty string." + min_lines: 20 + - path: "tests/uat/extension-page-harness.ts" + provides: "EXTENDED with 5 new page-side assertion methods on window.__mokoshHarness: assertA18 (font asset reachability + size floor), assertA19 (icon not Bug A placeholder — hash-distinct), assertA20 (manifest name resolved via i18n), assertA21 (--mks-font-display computed value === 'Lora'), assertA22 (welcome tokens loaded — gated on welcome.html being reachable). Each method returns the standard AssertionRecord { passed, name, checks, diagnostics } shape consistent with A1-A17. The harness page DOM gains a small test-element div with class='mks-display-1' for A21's getComputedStyle probe; this DOM element ships in the test bundle only (dist-test/) since it lives inside extension-page-harness.html." + min_lines: 200 + - path: "tests/uat/lib/harness-page-driver.ts" + provides: "Adds driveA18, driveA19, driveA20, driveA21, driveA22 host-side wrappers following the existing driveA4 pattern (page.evaluate → window.__mokoshHarness.assertXX). No new host-side filesystem or ffprobe primitives; all A18-A22 work runs page-side via fetch + getComputedStyle + chrome.runtime.getManifest." + min_lines: 60 + - path: "tests/uat/harness.test.ts" + provides: "Orchestrator extended with A18-A22 entries. FORBIDDEN_HOOK_STRINGS list UNCHANGED at 10 entries (no new test-mode symbols introduced). Assertion count grows from 15/15 (post-01-13) or 18/18 (post-01-10+01-13) to 20/20 or 23/23 (post-01-12). Bail-on-first-failure orchestration unchanged." + contains: "A18" + - path: ".planning/STATE.md" + provides: "Wave 7 closure update: completed_plans increments by 1 (Plan 01-12 closes); decision-log appends '[Phase 01-12]: design integration landed — Lora self-hosted (R2 per designer 2026-05-19) + tokens.css canonical + 8 i18n strings + branded icons + manifest:name i18n; UAT harness 20/20 GREEN (or 23/23 if 01-10 also landed); operator brand-fit ack received'. percent recomputed." + - path: ".planning/ROADMAP.md" + provides: "Phase 1 Plans list gains `- [x] 01-12-PLAN.md — Design integration (R2 Lora, tokens.css, 8 i18n strings, branded icons, manifest i18n)` entry after 01-13. If Plan 01-10 not yet in list, also add its entry as `- [ ]` placeholder unless already present." + key_links: + - from: "src/shared/tokens.css @font-face src urls" + to: "src/shared/fonts/*.woff2" + via: "relative url('./fonts/.woff2') per RESEARCH §2 Pattern A; Vite asset pipeline rebases at build to dist/assets/.woff2 with @crxjs auto-generated web_accessible_resources entry" + pattern: "url\\(\\\"\\./fonts/[A-Z][A-Za-z]+-[A-Za-z]+\\.woff2\\\"\\)" + - from: "src/popup/style.css" + to: "src/shared/tokens.css" + via: "@import '../shared/tokens.css'; at top of file" + pattern: "@import.*shared/tokens\\.css" + - from: "manifest.json:action.default_title" + to: "_locales/{en,ru}/messages.json:tooltipOff.message" + via: "Chrome i18n runtime substitution via __MSG_tooltipOff__ token + default_locale fallback chain" + pattern: "__MSG_tooltipOff__" + - from: "src/background/index.ts setBadgeState() setTitle call" + to: "_locales/{en,ru}/messages.json" + via: "chrome.i18n.getMessage('tooltipOff'|'tooltipRec'|'tooltipErr') synchronous read" + pattern: "chrome\\.i18n\\.getMessage" + - from: "src/popup/index.ts saveArchive success branch" + to: "_locales/{en,ru}/messages.json:popupSaveDone.message" + via: "chrome.i18n.getMessage('popupSaveDone') in updateUI/saveArchive" + pattern: "popupSaveDone" + - from: "icons/icon{16,48,128}.png" + to: "src/shared/brand/mokosh-mark.svg" + via: "scripts/rasterize-icons.sh runs rsvg-convert -w N -h N -o icons/iconN.png" + pattern: "rsvg-convert.*mokosh-mark" + - from: "tests/uat/extension-page-harness.ts assertA18" + to: "src/shared/fonts/Lora-VariableFont.woff2" + via: "fetch(chrome.runtime.getURL('shared/fonts/Lora-VariableFont.woff2')) OR fetch using emitted asset-rebased path (extracted via document.styleSheets[i].cssRules @font-face src.url at runtime — works regardless of Vite hashing)" + pattern: "chrome\\.runtime\\.getURL.*\\.woff2" + - from: "tests/uat/extension-page-harness.ts assertA21" + to: "src/shared/tokens.css --mks-font-display value" + via: "Creates a transient div, applies class='mks-display-1', reads getComputedStyle(div).fontFamily, asserts it starts with 'Lora'" + pattern: "getComputedStyle.*fontFamily" + - from: "tests/build/no-remote-fonts.test.ts" + to: "dist/ artifact tree" + via: "execFile npm run build; recursive readFileSync over dist/; grep for 'googleapis' + 'https://fonts'" + pattern: "googleapis|https://fonts" + +tags_repeat: + - design-integration + - locales + - mv3-csp + +user_setup: + - service: "Lora font (OFL-1.1)" + why: "R2 designer reply 2026-05-19 substitutes Newsreader → Lora for Cyrillic coverage; Plan 01-12 self-hosts the WOFF2 per MV3 CSP" + network_needed: true + one_off_download: + - url: "https://github.com/cyrealtype/Lora-Cyrillic OR Google Fonts Lora release page" + why: "upstream Lora variable TTF — subsetted to Latin+Cyrillic by scripts/subset-fonts.sh" + installs_to: "(transient scratch dir; only the subsetted woff2 committed to src/shared/fonts/)" + license_attribution_required: true + license_file: "src/shared/fonts/LICENSE-Lora.txt" + - service: "IBM Plex Sans + Mono (OFL-1.1)" + why: "D-05 type pairing; Plex Sans is the UI body face with full Cyrillic; Plex Mono is the diagnostic/timer face" + network_needed: true + one_off_download: + - url: "https://github.com/IBM/plex/releases" + why: "upstream Plex Sans + Mono v1.1.0 TTFs" + installs_to: "(transient scratch dir)" + license_attribution_required: true + license_file: "src/shared/fonts/LICENSE-IBM-Plex.txt" +--- + +## Scope Sanity Note + +**7 waves, 14 tasks total** (Wave 0 TDD scaffolds + Waves 1-7 implementation including 1 closing checkpoint). Above the "split signal" thresholds in ``, but the scope is dictated by the source artifact set (8 i18n strings + token system + 7 fonts + 3 icons + manifest + popup + welcome conditional + harness extension + UAT closure) and the planner has no authority to reduce coverage (see `planner_authority_limits`). Each wave is intended for a fresh-context executor spawn per the GSD execute-phase pattern. + +**Why not split further:** + +1. **Wave structure already maps to natural seams**. Wave 0 (TDD scaffolds — RED-by-design unit tests for tokens/fonts/icons/i18n/no-remote-fonts) gates every later wave's GREEN outcome. Wave 1 (fonts + tokens.css) is the foundation. Wave 2 (icons) is independent. Wave 3 (manifest + locales) depends only on tokens existing. Wave 4 (popup + background i18n + palette) depends on Waves 1-3. Wave 5 (welcome conditional + Vite define) is a single mostly-cosmetic wave. Wave 6 (harness A18-A22) extends the proven 01-13 architecture. Wave 7 closure + operator checkpoint. +2. **Per-executor context budget**: Wave 0 ~10%, Wave 1 ~35% (font subsetting + tokens.css migration; the heaviest wave), Wave 2 ~10%, Wave 3 ~15%, Wave 4 ~25%, Wave 5 ~10%, Wave 6 ~20%, Wave 7 ~5%. Each wave fits the ~50% target with margin. +3. **Cross-wave atomicity**: every wave produces a coherent commit-set with vitest GREEN at wave-close (Tier-1 gate, locale-parity, no-remote-fonts, etc. all run after every wave). + +**If a future revision DOES force a split,** natural cut line: Plan 01-12A = Waves 0-3 (test scaffolds + fonts + tokens + icons + manifest + locales); Plan 01-12B = Waves 4-7 (source-code adoption + welcome + harness + closure). Wave numbering reflects this potential split cleanly. + + +Land the full Mokosh design integration end-to-end across 7 waves. Three coordinated changes from the current Plan 01-13 baseline: + +1. **Self-host the OFL font bundle + the canonical token system.** Subset Lora variable (per R2 designer reply 2026-05-19: Newsreader substituted with Lora for Cyrillic coverage), IBM Plex Sans 400/500/600/700, and IBM Plex Mono 400/500 to Latin + Cyrillic basic via pyftsubset. Place WOFF2 + LICENSE + README files under `src/shared/fonts/`. Land `src/shared/tokens.css` as the canonical token system — copy of designer's handoff with two surgical edits: (a) Google Fonts @import → local @font-face rules; (b) `--mks-font-display` from `"Newsreader"` to `"Lora"`. Add `.mks-word` class for the lockup wordmark. Verifies via `tests/build/no-remote-fonts.test.ts` (CSP-safe production bundle). + +2. **Brand-mark PNG icons + manifest i18n.** Rasterize `src/shared/brand/mokosh-mark.svg` to `icons/icon{16,48,128}.png` via `rsvg-convert`, replacing the Plan 01-09 Path A dark-square + green-dot placeholders. Migrate `manifest.json` to `__MSG_extName__` + `__MSG_extDesc__` + `default_locale: "en"` + `action.default_title: "__MSG_tooltipOff__"`. Land `_locales/{en,ru}/messages.json` with the 8 Brief §02 operator-facing strings + 4 supporting keys (extName per D-07 override, extDesc per D-08 tagline, popupInfoText, popupEmptyState). + +3. **Source-code adoption of tokens + i18n.** Restyle `src/popup/style.css` to import `src/shared/tokens.css` and replace all hex literals with `var(--mks-*)` references per D-04 loom palette. Migrate `src/popup/index.ts` + `src/background/index.ts` from hardcoded RU/EN strings to `chrome.i18n.getMessage` reads at every call site (badge titles, notification copy, popup CTA + status). Update `BADGE_REC_COLOR` from material-green `#00C853` to madder `#b2543d` (= --mks-madder-600). Wire the welcome page (Plan 01-10 conditional) to the canonical tokens.css. + +**Wave 6 extends the 01-13 UAT harness** with 5 new assertions A18-A22 covering: WOFF2 reachability + size floor; icons-not-placeholders; manifest:name i18n resolution; `--mks-font-display` resolves to Lora; welcome tokens loaded (conditional on 01-10 landing). The harness pattern follows 01-13 Wave 3 exactly (page-side `assertA18..A22` methods + host-side `driveA18..A22` wrappers + orchestrator entries). + +**Wave 7 closure:** operator empirical brand-fit checkpoint (autonomous: false) — fresh build, load unpacked, verify branded surfaces (toolbar icon, popup palette + Lora display heading, manifest name in chrome://extensions, Russian copy with Lora rendering, notification copy). Operator ack closes the plan + Phase 1 charter. + +**Operator role at closure:** post-01-13, the operator's gates are brand/design only. Plan 01-12 satisfies the LAST remaining Phase 1 operator gate (brand-fit ack of branded surfaces). + +Output: +- Wave 0: 6 RED unit tests scaffolded (tokens-adopted, fonts-present, icons-present, no-remote-fonts, manifest-i18n, locale-parity); each RED today, GREEN after later waves land their respective artifacts. +- Wave 1: src/shared/fonts/ + src/shared/tokens.css + scripts/subset-fonts.sh; fonts-present + no-remote-fonts flip GREEN. +- Wave 2: icons/icon{16,48,128}.png + scripts/rasterize-icons.sh + src/shared/brand/; icons-present flips GREEN. +- Wave 3: manifest.json + _locales/{en,ru}/messages.json; manifest-i18n + locale-parity flip GREEN; manifest:name resolves to "Mokosh — Session Capture" on extension reload. +- Wave 4: src/popup/style.css + src/popup/index.html + src/popup/index.ts + src/background/index.ts; tokens-adopted flips GREEN; existing 98+ vitest tests stay GREEN (i18n-call-site migration is behavior-equivalent for the chrome.* mock pattern used by existing tests). +- Wave 5: src/welcome/* conditional migration (if 01-10 landed) OR skip; vite.config.ts __VITE_DEV__ define-token + scripts/README.md. +- Wave 6: tests/uat/extension-page-harness.ts + tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts extended with A18-A22; npm run test:uat reports 20/20 (or 23/23 with 01-10) GREEN. +- Wave 7: operator checkpoint; STATE.md + ROADMAP.md sync; SUMMARY produced. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/REQUIREMENTS.md +@.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md +@.planning/phases/01-stabilize-video-pipeline/01-09-PLAN.md +@.planning/phases/01-stabilize-video-pipeline/01-09-SUMMARY.md +@.planning/phases/01-stabilize-video-pipeline/01-10-PLAN.md +@.planning/phases/01-stabilize-video-pipeline/01-11-SUMMARY.md +@.planning/phases/01-stabilize-video-pipeline/01-13-PLAN.md +@.planning/phases/01-stabilize-video-pipeline/01-13-SUMMARY.md +@.planning/phases/01-stabilize-video-pipeline/01-12-RESEARCH.md +@.planning/intel/brand-decisions-v1.md +@.planning/intel/brand-decisions-v1-followup-display-font.md +@.planning/intel/assets-spec.md +@.planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css +@.planning/intel/design-incoming/system/bundle/mokosh-handoff/README.txt +@.planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/mokosh-mark.svg +@.planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/mokosh-lockup.svg +@src/background/index.ts +@src/popup/index.html +@src/popup/index.ts +@src/popup/style.css +@manifest.json +@vite.config.ts +@vite.test.config.ts +@tests/uat/extension-page-harness.ts +@tests/uat/harness.test.ts +@tests/uat/lib/harness-page-driver.ts +@tests/background/no-test-hooks-in-prod-bundle.test.ts + + + + +### Designer reply ratifying R2 (per follow-up doc + the 2026-05-19 reply quoted in this plan's spawn brief) + +``` +--mks-font-display: "Lora", "Iowan Old Style", "Times New Roman", serif; +``` + +Use this EXACT string as the canonical token value in src/shared/tokens.css. Do NOT derive a different chain. Lora is OFL-1.1, variable-font (400-700 weight in one file), with FULL Cyrillic coverage (Cyreal foundry, designed by Olga Karpushina with Cyrillic-Latin parity). + +### Per-face WOFF2 subsetting recipe (RESEARCH §1 + §6) + +```bash +UNICODES='U+0020-007E,U+00A0-00FF,U+0131,U+0152-0153,U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116' + +# Lora variable (Latin+Cyrillic; variable wght axis 400-700) +pyftsubset Lora-VariableFont_wght.ttf \ + --unicodes="$UNICODES" \ + --flavor=woff2 \ + --output-file=src/shared/fonts/Lora-VariableFont.woff2 \ + --layout-features='*' --no-hinting --desubroutinize + +# Repeat for Lora-Italic-VariableFont_wght.ttf if upstream ships italic separately +# (A5 in RESEARCH Assumptions Log — verify at execute-plan time) + +# Plex Sans 400/500/600/700 (each as a separate static-weight file) +for weight in Regular Medium SemiBold Bold; do + pyftsubset "IBMPlexSans-${weight}.ttf" \ + --unicodes="$UNICODES" \ + --flavor=woff2 \ + --output-file="src/shared/fonts/IBMPlexSans-${weight}.woff2" \ + --layout-features='*' --no-hinting --desubroutinize +done + +# Plex Mono 400/500 +for weight in Regular Medium; do + pyftsubset "IBMPlexMono-${weight}.ttf" \ + --unicodes="$UNICODES" \ + --flavor=woff2 \ + --output-file="src/shared/fonts/IBMPlexMono-${weight}.woff2" \ + --layout-features='*' --no-hinting --desubroutinize +done +``` + +### Local-faced tokens.css @font-face replacement block (replaces line 12 Google Fonts @import) + +```css +/* ── Self-hosted fonts (MV3 CSP) ── + Faces per D-05 with R2 Newsreader→Lora substitution (designer reply 2026-05-19). + Subsetting: all faces are Latin (U+0020-007E + U+00A0-00FF) + Cyrillic basic + (U+0400-045F + supplemental code points per pyftsubset recipe). All OFL-1.1 + (LICENSE-Lora.txt + LICENSE-IBM-Plex.txt next to this directory). */ + +/* Lora variable — display family with full Cyrillic (R2 substitute for Newsreader) */ +@font-face { + font-family: "Lora"; + src: url("./fonts/Lora-VariableFont.woff2") format("woff2-variations"), + url("./fonts/Lora-VariableFont.woff2") format("woff2"); + font-weight: 400 700; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Lora"; + src: url("./fonts/Lora-Italic-VariableFont.woff2") format("woff2-variations"), + url("./fonts/Lora-Italic-VariableFont.woff2") format("woff2"); + font-weight: 400 700; + font-style: italic; + font-display: swap; +} + +/* IBM Plex Sans — UI body family with full Cyrillic */ +@font-face { + font-family: "IBM Plex Sans"; + src: url("./fonts/IBMPlexSans-Regular.woff2") format("woff2"); + font-weight: 400; font-style: normal; font-display: swap; +} +@font-face { + font-family: "IBM Plex Sans"; + src: url("./fonts/IBMPlexSans-Medium.woff2") format("woff2"); + font-weight: 500; font-style: normal; font-display: swap; +} +@font-face { + font-family: "IBM Plex Sans"; + src: url("./fonts/IBMPlexSans-SemiBold.woff2") format("woff2"); + font-weight: 600; font-style: normal; font-display: swap; +} +@font-face { + font-family: "IBM Plex Sans"; + src: url("./fonts/IBMPlexSans-Bold.woff2") format("woff2"); + font-weight: 700; font-style: normal; font-display: swap; +} + +/* IBM Plex Mono — diagnostic / timer family with full Cyrillic */ +@font-face { + font-family: "IBM Plex Mono"; + src: url("./fonts/IBMPlexMono-Regular.woff2") format("woff2"); + font-weight: 400; font-style: normal; font-display: swap; +} +@font-face { + font-family: "IBM Plex Mono"; + src: url("./fonts/IBMPlexMono-Medium.woff2") format("woff2"); + font-weight: 500; font-style: normal; font-display: swap; +} +``` + +NOTE: if upstream Lora ships normal+italic in a single multi-axis variable file (italic via `ital` axis), the italic @font-face rule above collapses into the normal one with `font-style: oblique 0deg 14deg` per the variable-italic syntax. Verify upstream Lora release structure during execute-plan; adjust @font-face block accordingly. RESEARCH §A5 calls this out as a verify-at-execute checkpoint. + +### .mks-word class (RESEARCH §8) + +```css +/* ── Lockup wordmark — referenced by mokosh-lockup.svg line 21 ── */ +.mks-word { + font-family: var(--mks-font-display); + font-size: 32px; + font-weight: var(--mks-weight-regular); + letter-spacing: var(--mks-tracking-display); + fill: var(--mks-ink-900); +} +``` + +### Icon rasterization recipe (RESEARCH §3 verified locally) + +```bash +#!/usr/bin/env bash +set -euo pipefail +SVG="src/shared/brand/mokosh-mark.svg" +for size in 16 48 128; do + rsvg-convert -w "$size" -h "$size" "$SVG" -o "icons/icon${size}.png" + bytes=$(stat -c%s "icons/icon${size}.png") + echo "✓ icons/icon${size}.png (${bytes} bytes)" + case $size in + 16) [[ $bytes -ge 200 ]] || { echo "FAIL: icon16 < 200 B"; exit 1; } ;; + 48) [[ $bytes -ge 500 ]] || { echo "FAIL: icon48 < 500 B"; exit 1; } ;; + 128) [[ $bytes -ge 1024 ]] || { echo "FAIL: icon128 < 1024 B"; exit 1; } ;; + esac +done +``` + +### messages.json schema (Chrome i18n; RESEARCH §10) + +```json +{ + "extName": { + "message": "Mokosh — Session Capture", + "description": "manifest.json:name; surfaces in chrome://extensions, Web Store, and right-click menus" + }, + "tooltipRecPrefix": { + "message": "Mokosh — recording", + "description": "Prefix; the SW concatenates ' (MM:SS)' at runtime via formatElapsed(seconds)" + } +} +``` + +JS-side read (synchronous; chrome.i18n.getMessage does NOT return a promise): + +```ts +const cta = chrome.i18n.getMessage('popupSaveCta'); // returns '' on missing key +buttonText.textContent = cta; +``` + +### Background SW migration shape (src/background/index.ts) + +```ts +// REPLACES line 76: const BADGE_REC_COLOR = '#00C853'; // material green +const BADGE_REC_COLOR = '#b2543d'; // --mks-madder-600 per D-04 loom palette (RESEARCH §10 Open Question A7) + +// REPLACES the const BADGE_*_TITLE block (lines 82-84) — read at call site instead: +function setBadgeState(state: BadgeState): void { + let text: string; + let color: string; + let titleKey: 'tooltipOff' | 'tooltipRec' | 'tooltipErr'; + if (state === 'REC') { + text = BADGE_REC_TEXT; + color = BADGE_REC_COLOR; + titleKey = 'tooltipRec'; + } else if (state === 'OFF') { + text = BADGE_OFF_TEXT; + color = BADGE_OFF_COLOR; + titleKey = 'tooltipOff'; + } else { + text = BADGE_ERROR_TEXT; + color = BADGE_ERROR_COLOR; + titleKey = 'tooltipErr'; + } + const title = chrome.i18n.getMessage(titleKey); + try { chrome.action.setBadgeText({ text }); } catch (e) { logger.warn('setBadgeText failed:', e); } + try { chrome.action.setBadgeBackgroundColor({ color }); } catch (e) { logger.warn('setBadgeBackgroundColor failed:', e); } + try { chrome.action.setTitle({ title }); } catch (e) { logger.warn('setTitle failed:', e); } +} + +// REPLACES line 862-865 (RECORDING_ERROR recovery notification body): +chrome.notifications.create(recoveryId, { + type: 'basic', + iconUrl: chrome.runtime.getURL(NOTIFICATION_ICON_PATH), + title: chrome.i18n.getMessage('extName'), + message: chrome.i18n.getMessage('notifRecovery'), + priority: 1, +}); + +// REPLACES line 961-965 (onStartup notification body): +chrome.notifications.create(notificationId, { + type: 'basic', + iconUrl: chrome.runtime.getURL(NOTIFICATION_ICON_PATH), + title: chrome.i18n.getMessage('extName'), + message: chrome.i18n.getMessage('notifStartup'), + priority: 1, +}); +``` + +### Popup TS migration shape (src/popup/index.ts) + +```ts +// in updateUI() +case 'idle': + buttonText.textContent = chrome.i18n.getMessage('popupSaveCta'); // was 'Сохранить отчёт об ошибке' + saveButton.className = 'save-button'; + saveButton.disabled = !popupState.hasPermissions; + break; +case 'saving': + buttonText.textContent = chrome.i18n.getMessage('popupSaving'); // new key OR keep 'Сохраняю...' as fallback + break; +case 'done': + buttonText.textContent = chrome.i18n.getMessage('popupSaveDoneShort'); // new key 'Готово! ✓' + break; + +// in saveArchive() success branch (was line 91): +statusMessage.textContent = chrome.i18n.getMessage('popupSaveDone'); // was 'Архив успешно сохранён!' + +// in init() — populate data-mks-key elements (info-text + status): +function populateMksKeys(): void { + document.querySelectorAll('[data-mks-key]').forEach((el) => { + const key = el.dataset.mksKey; + if (key) el.textContent = chrome.i18n.getMessage(key); + }); +} +``` + +### Harness assertion A18-A22 shape (extends 01-13 pattern) + +```ts +// tests/uat/extension-page-harness.ts (page-side) +async function assertA18(): Promise { + const checks: string[] = []; + // Resolve the actual emitted asset URL from the loaded stylesheet (handles Vite hashing). + const sheets = Array.from(document.styleSheets); + let woff2Url: string | null = null; + for (const sheet of sheets) { + try { + for (const rule of Array.from(sheet.cssRules)) { + if (rule.constructor.name === 'CSSFontFaceRule') { + const src = (rule as CSSFontFaceRule).style.getPropertyValue('src'); + const match = src.match(/url\(["']?([^"')]+Lora[^"')]+\.woff2)["']?\)/); + if (match) { woff2Url = match[1]; break; } + } + } + } catch { /* cross-origin sheets throw; skip */ } + if (woff2Url) break; + } + if (!woff2Url) { + return { passed: false, name: 'A18', checks, diagnostics: 'No Lora @font-face rule found in any stylesheet', error: '' }; + } + checks.push(`A18.1: found Lora @font-face src=${woff2Url}`); + const resp = await fetch(new URL(woff2Url, location.href).href); + if (!resp.ok) return { passed: false, name: 'A18', checks, diagnostics: `fetch ${woff2Url} returned ${resp.status}`, error: '' }; + checks.push(`A18.2: fetch returned ${resp.status}`); + const buf = await resp.arrayBuffer(); + if (buf.byteLength < 50000) return { passed: false, name: 'A18', checks, diagnostics: `Lora woff2 only ${buf.byteLength} B (expected >50000)`, error: '' }; + checks.push(`A18.3: byteLength=${buf.byteLength}`); + return { passed: true, name: 'A18', checks, diagnostics: '' }; +} + +async function assertA20(): Promise { + const manifest = chrome.runtime.getManifest(); + if (manifest.name === 'Mokosh — Session Capture') { + return { passed: true, name: 'A20', checks: [`A20.1: manifest.name='${manifest.name}'`], diagnostics: '' }; + } + return { passed: false, name: 'A20', checks: [], diagnostics: `Expected 'Mokosh — Session Capture'; got '${manifest.name}'`, error: '' }; +} + +async function assertA21(): Promise { + // Create transient probe div with .mks-display-1 class + const probe = document.createElement('div'); + probe.className = 'mks-display-1'; + probe.textContent = 'Probe'; + document.body.appendChild(probe); + // Force layout so CSS resolves + void probe.offsetHeight; + const computed = window.getComputedStyle(probe).fontFamily; + document.body.removeChild(probe); + if (computed.startsWith('Lora') || computed.startsWith('"Lora"')) { + return { passed: true, name: 'A21', checks: [`A21.1: fontFamily='${computed}'`], diagnostics: '' }; + } + return { passed: false, name: 'A21', checks: [], diagnostics: `--mks-font-display did not resolve to Lora; computed='${computed}'`, error: '' }; +} +``` + +### Tier-1 forbidden-strings inventory (UNCHANGED at 10) + +```ts +const FORBIDDEN_HOOK_STRINGS: ReadonlyArray = [ + '__mokoshTest', + 'setCurrentStream', + 'setSegmentCountGetter', + 'installFakeDisplayMedia', + 'uninstallFakeDisplayMedia', + 'dispatchEndedOnTrack', + 'getSegmentCount', + '__mokoshOffscreenQuery', + 'get-display-surface', + 'get-segment-count', +]; +// Plan 01-12 introduces NO new test-mode symbols (A18-A22 use production chrome.* APIs +// + fetch + getComputedStyle exclusively). Do NOT extend this list. +``` + +### Locale-parity test shape + +```ts +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; + +describe('_locales/ key parity', () => { + const ru = JSON.parse(readFileSync('_locales/ru/messages.json', 'utf8')); + const en = JSON.parse(readFileSync('_locales/en/messages.json', 'utf8')); + it('every key in ru/messages.json exists in en/messages.json', () => { + for (const k of Object.keys(ru)) expect(en).toHaveProperty(k); + }); + it('every key in en/messages.json exists in ru/messages.json (symmetric)', () => { + for (const k of Object.keys(en)) expect(ru).toHaveProperty(k); + }); + it('every key has a non-empty .message string', () => { + for (const k of Object.keys(en)) expect(typeof en[k].message).toBe('string'); + for (const k of Object.keys(en)) expect(en[k].message.length).toBeGreaterThan(0); + for (const k of Object.keys(ru)) expect(typeof ru[k].message).toBe('string'); + for (const k of Object.keys(ru)) expect(ru[k].message.length).toBeGreaterThan(0); + }); +}); +``` + + + + + + + Wave 0 Task 1: Scaffold RED unit tests (TDD baseline) + tests/build/tokens-adopted.test.ts, tests/build/fonts-present.test.ts, tests/build/icons-present.test.ts, tests/build/no-remote-fonts.test.ts, tests/i18n/manifest-i18n.test.ts, tests/i18n/locale-parity.test.ts + + - tokens-adopted: RED because src/shared/tokens.css does not yet exist + src/popup/style.css contains hex literals (#2563eb, #1d4ed8, ...). GREEN after Wave 1 + Wave 4. + - fonts-present: RED because src/shared/fonts/*.woff2 do not exist. GREEN after Wave 1. + - icons-present: RED-ish (existing placeholders DO clear floors: 574/1153/2615 > 200/500/1024 per `ls -la icons/`); the test should assert dimensions + signature + size FLOOR strictly. RED-FOR-DIMS today only if Wave 0 includes the dimension parse (the 16-bit-RGB placeholders ARE the right pixel dims). Test designed to be GREEN after Wave 2 (Loom mark rasterized via rsvg-convert produces 8-bit-RGBA per RESEARCH §3 — file content hash will change, but dim + floor checks pass before AND after Wave 2). Add explicit RED case: assert PNG color-type byte (offset 25) === 6 (RGBA) — placeholder is 2 (RGB) so this assertion RED today, GREEN after Wave 2 (rsvg-convert defaults to RGBA per RESEARCH §3 "PNG output is `8-bit/color RGBA, non-interlaced` per `file(1)`"). + - no-remote-fonts: RED-vacuous today (no dist/ tokens.css exists). After Wave 1 + Wave 4 + npm run build, dist/ exists + tokens.css inside it must have ZERO `googleapis` matches. + - manifest-i18n: RED today (manifest.name === 'AI Call Recorder' + no default_locale). GREEN after Wave 3. + - locale-parity: RED today (no _locales/). GREEN after Wave 3. + + + Create the six test files per the `` block patterns and the artifact-table `provides` summaries. Each test file follows the existing tests/background/no-test-hooks-in-prod-bundle.test.ts and tests/offscreen/codec-check.test.ts patterns for module structure (vitest describe/it + readFileSync + JSON.parse + regex checks). + + Key implementation notes: + + 1. tests/build/tokens-adopted.test.ts — three vitest cases: (a) `src/shared/tokens.css` exists + parses (`readFileSync`); (b) `src/popup/style.css` contains the import line `@import '../shared/tokens.css'` OR `@import "../shared/tokens.css"`; (c) `src/popup/style.css` matches `/#[0-9a-fA-F]{3,8}/g` ZERO times. NO build dependency — runs on source files only. + + 2. tests/build/fonts-present.test.ts — single describe; iterate over the expected file list (Lora-VariableFont.woff2, Lora-Italic-VariableFont.woff2 [SKIP if upstream consolidates italic into normal — flag via a TODO comment per RESEARCH §A5], IBMPlexSans-Regular.woff2, IBMPlexSans-Medium.woff2, IBMPlexSans-SemiBold.woff2, IBMPlexSans-Bold.woff2, IBMPlexMono-Regular.woff2, IBMPlexMono-Medium.woff2). For each: assert existsSync + statSync.size > 0. Also assert src/shared/tokens.css contains each filename via substring match. + + 3. tests/build/icons-present.test.ts — for each of {16: 200, 48: 500, 128: 1024} → assert existsSync(`icons/icon${size}.png`) + statSync.size >= floor + first 8 bytes match `\\x89PNG\\r\\n\\x1a\\n`. Read bytes 16-19 (width) + 20-23 (height) as big-endian uint32 and assert each === size. Read byte 25 (color-type) and assert === 6 (RGBA per Wave 2 rsvg-convert output) — this is the RED-today-GREEN-after-Wave-2 case. + + 4. tests/build/no-remote-fonts.test.ts — mirrors tests/background/no-test-hooks-in-prod-bundle.test.ts execFile pattern: `execFile('npm', ['run', 'build'])` (gated by `process.env.SKIP_BUILD === '1'` escape hatch), then recursive readdirSync over `dist/`, then for each file read content + assert ZERO matches for `googleapis` AND `https://fonts`. Use a 60_000ms build timeout (PROD_BUILD_TIMEOUT_MS). + + 5. tests/i18n/manifest-i18n.test.ts — read manifest.json + parse. Assert: `manifest.name === '__MSG_extName__'`; `manifest.description === '__MSG_extDesc__'`; `manifest.default_locale === 'en'`; `manifest.action.default_title === '__MSG_tooltipOff__'`. Read _locales/en/messages.json + parse + assert extName.message === 'Mokosh — Session Capture' + extDesc.message === 'Thirty seconds ago, always at hand.'. Read _locales/ru/messages.json + parse + assert extName.message === 'Mokosh — Запись сессии' + extDesc.message === 'Тридцать секунд назад, всегда под рукой.'. + + 6. tests/i18n/locale-parity.test.ts — per the `` block; three describe blocks (ru→en parity; en→ru symmetric; every key has non-empty .message string). + + Create the directories `tests/build/` and `tests/i18n/` (they do not yet exist — verified via `ls tests/`). Commit all six files in a single atomic commit. + + + npx vitest run tests/build tests/i18n 2>&1 | tail -20 && echo "Expected: 6 test files, all RED (or RED on at least one case each — fonts-present, tokens-adopted, no-remote-fonts, manifest-i18n, locale-parity all RED today; icons-present RED on color-type-byte case only)" + + + - 6 test files exist at the documented paths + - vitest discovers all 6 + reports them in `npx vitest run tests/build tests/i18n` + - At least one case per file is RED today (per behavior above) + - npm run build / build:test still pass (these tests do not change source code yet — the existing 98 vitest GREEN baseline stays GREEN; only the 6 new tests are RED) + - Atomic commit `test(01-12): wave-0 — scaffold RED unit tests for tokens / fonts / icons / no-remote-fonts / manifest-i18n / locale-parity` + + + + + Wave 1 Task 1: Subset OFL fonts to src/shared/fonts/ + src/shared/fonts/Lora-VariableFont.woff2, src/shared/fonts/Lora-Italic-VariableFont.woff2, src/shared/fonts/IBMPlexSans-Regular.woff2, src/shared/fonts/IBMPlexSans-Medium.woff2, src/shared/fonts/IBMPlexSans-SemiBold.woff2, src/shared/fonts/IBMPlexSans-Bold.woff2, src/shared/fonts/IBMPlexMono-Regular.woff2, src/shared/fonts/IBMPlexMono-Medium.woff2, src/shared/fonts/LICENSE-Lora.txt, src/shared/fonts/LICENSE-IBM-Plex.txt, src/shared/fonts/README.md, scripts/subset-fonts.sh + + Per RESEARCH §1 + §6 + the `` block subsetting recipe. Each WOFF2 file: + - Lora-VariableFont.woff2: variable-axis weight 400-700, Latin + Cyrillic basic subset, expected 80-120KB + - Lora-Italic-VariableFont.woff2: if upstream ships italic separately (A5 verify); otherwise document the consolidation in README.md and omit this file (and the corresponding @font-face rule in tokens.css) + - IBMPlexSans-{Regular,Medium,SemiBold,Bold}.woff2: ~25KB each + - IBMPlexMono-{Regular,Medium}.woff2: ~28KB each + Expected total ~250-300KB per the R2 substitution adjustment to RESEARCH §1's ~216KB estimate. + + + 1. Create scripts/subset-fonts.sh per the `` block recipe. Header comment documents the source URLs (github.com/IBM/plex/releases for Plex v1.1.0; for Lora: github.com/cyrealtype/Lora-Cyrillic OR the Google Fonts release page accessed via `curl`). Script downloads upstream TTFs into a scratch tmpdir (`mktemp -d`), runs pyftsubset per face with the UNICODES range from ``, then cleans up the tmpdir. + + 2. Execute scripts/subset-fonts.sh. Verify each produced file: + - exists at the documented path + - size > 0 (typical: Lora ~100KB; Plex faces ~25-28KB each) + - is WOFF2 (first 4 bytes === 'wOF2' = 0x77 0x4F 0x46 0x32) + Document any execute-time surprises (italic consolidation per A5, etc.) in a brief note in src/shared/fonts/README.md. + + 3. Create src/shared/fonts/LICENSE-Lora.txt by curling https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/master/OFL.txt (or equivalent canonical Lora OFL source — verify upstream layout during execute). Include copyright lines per the actual upstream content. + + 4. Create src/shared/fonts/LICENSE-IBM-Plex.txt by curling https://raw.githubusercontent.com/IBM/plex/master/LICENSE.txt (verbatim copy). + + 5. Create src/shared/fonts/README.md per RESEARCH §7 template + the R2 substitution note. Document: what's bundled; upstream URLs; subsetting unicodes; license notes; regeneration recipe (`scripts/subset-fonts.sh`). Mention the R2 substitution (Newsreader → Lora) + the designer 2026-05-19 reply. + + 6. Verify tests/build/fonts-present.test.ts now passes (all 7 or 8 expected files exist). + + NOTE on Lora italic: if upstream variable file includes italic via the `ital` axis (single multi-axis file), update scripts/subset-fonts.sh to subset that single file + adjust tokens.css (next task) to use `font-style: oblique 0deg 14deg` per CSS Fonts Module 4 variable-italic syntax. Document the decision in src/shared/fonts/README.md. + + + ls -la src/shared/fonts/ && stat -c '%n: %s bytes' src/shared/fonts/*.woff2 && head -3 src/shared/fonts/LICENSE-*.txt && head -10 src/shared/fonts/README.md && (npx vitest run tests/build/fonts-present.test.ts 2>&1 | tail -10) && echo "Wave 1.1 done — fonts present; total bundle:" && du -ch src/shared/fonts/*.woff2 | tail -1 + + + - 7 or 8 WOFF2 files exist (8 if Lora italic separate; 7 if collapsed) + - Each WOFF2 file is > 0 bytes + WOFF2 signature verified + - LICENSE-Lora.txt + LICENSE-IBM-Plex.txt exist + non-empty + - src/shared/fonts/README.md exists + lists all bundled faces + upstream URLs + R2 note + - scripts/subset-fonts.sh exists + executable + - Total bundle size between 200KB and 350KB (relaxed envelope for R2 italic variance) + - tests/build/fonts-present.test.ts GREEN + - Atomic commit `feat(01-12): wave-1 task-1 — self-host OFL font bundle (Lora + Plex Sans + Plex Mono; R2 designer reply 2026-05-19)` + + + + + Wave 1 Task 2: Land canonical src/shared/tokens.css (R2 Lora substitution + .mks-word + CSP-safe @font-face) + src/shared/tokens.css, src/shared/brand/mokosh-mark.svg, src/shared/brand/mokosh-lockup.svg + + src/shared/tokens.css becomes the canonical token system. Contents: + - Verbatim copy of .planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css (281 lines) with three surgical edits: + 1. Line 12 Google Fonts @import REMOVED + replaced with the 7-rule (or 8-rule if Lora italic separate) @font-face block from `` + 2. Line 76 `--mks-font-display: "Newsreader", "Iowan Old Style", "Times New Roman", serif;` REPLACED with `--mks-font-display: "Lora", "Iowan Old Style", "Times New Roman", serif;` per R2 designer reply 2026-05-19 + 3. .mks-word class added at end of file per the `` block definition + - ZERO references to fonts.googleapis.com or any https:// URL + - ZERO Newsreader font-family references (R2 substitution complete) + + src/shared/brand/mokosh-mark.svg + mokosh-lockup.svg are verbatim copies of the design-incoming files (so the engineering source tree has its own copy decoupled from the intel/ ingestion location). + + + 1. Copy .planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css to src/shared/tokens.css (use `cp` or Read + Write). Apply the three edits: + - Replace line 12 (the Google Fonts @import) with the local @font-face block from `` — wrap in a comment header noting MV3 CSP + R2 designer reply 2026-05-19 + - Replace line 76 `--mks-font-display: "Newsreader"...` with `--mks-font-display: "Lora", "Iowan Old Style", "Times New Roman", serif;` — also update the comment at line 72 to note "Lora for display (Cyreal foundry; R2 substitute for Newsreader per designer reply 2026-05-19; full Cyrillic coverage)" + - Append the .mks-word class block from `` at end of file with a clear header `/* ── Lockup wordmark — referenced by mokosh-lockup.svg line 21 ── */` + + 2. Create src/shared/brand/ directory; copy mokosh-mark.svg + mokosh-lockup.svg from .planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/ to src/shared/brand/. The intel/ copies stay as the original handoff source-of-truth; src/shared/brand/ is the engineering working copy. + + 3. Verify src/shared/tokens.css: + - `grep -c 'googleapis\\|https://fonts' src/shared/tokens.css` returns 0 + - `grep -c 'Newsreader' src/shared/tokens.css` returns 0 + - `grep -c '@font-face' src/shared/tokens.css` returns 7 (or 8 with Lora italic) + - `grep '\\.mks-word' src/shared/tokens.css` finds the new class + + 4. Verify CSS-parses by spawning `npx vite build --mode test --config vite.test.config.ts` (dry-build) and confirming no warnings about tokens.css; OR by running a quick CSS-syntax check via `npx stylelint src/shared/tokens.css --config '{}'` if available; OR by inspection. + + 5. Confirm tests/build/no-remote-fonts.test.ts can now run (after a `npm run build` it will read dist/ and verify zero remote-font URLs). Run it with SKIP_BUILD=1 to defer the actual build to a later wave verification. + + + grep -c "googleapis\|https://fonts" src/shared/tokens.css && grep -c "Newsreader" src/shared/tokens.css && grep -c "@font-face" src/shared/tokens.css && grep -c "\.mks-word" src/shared/tokens.css && grep -c "Lora" src/shared/tokens.css && ls -la src/shared/brand/ && npx vitest run tests/build/tokens-adopted.test.ts 2>&1 | tail -10 + + + - src/shared/tokens.css exists + has 0 occurrences of `googleapis` / `https://fonts` / `Newsreader` + - src/shared/tokens.css has 7 (or 8) `@font-face` rule occurrences + - src/shared/tokens.css contains `.mks-word` class definition + - src/shared/tokens.css contains `Lora` font-family reference (≥1) + - src/shared/brand/mokosh-mark.svg + mokosh-lockup.svg exist + are byte-identical copies of intel/ originals + - tests/build/tokens-adopted.test.ts case (a) GREEN (tokens.css exists); cases (b)+(c) still RED until Wave 4 (popup/style.css doesn't yet import tokens.css) + - Atomic commit `feat(01-12): wave-1 task-2 — canonical src/shared/tokens.css (R2 Lora substitution + .mks-word + local @font-face block)` + + + + + Wave 2 Task 1: Rasterize Loom brand mark to icons/icon{16,48,128}.png + icons/icon16.png, icons/icon48.png, icons/icon128.png, scripts/rasterize-icons.sh + + Rasterize src/shared/brand/mokosh-mark.svg to icons/icon{16,48,128}.png via rsvg-convert (verified locally at RESEARCH §3: 16=406B, 48=784B, 128=1952B — all clear assets-spec.md FLOOR). The Plan 01-09 Path A placeholder icons (16-bit-RGB dark-square + green-dot) are OVERWRITTEN. + + 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). + + + 1. Create scripts/rasterize-icons.sh per the `` block recipe (with the embedded size-floor sanity checks). Make it executable (`chmod +x`). + + 2. Execute scripts/rasterize-icons.sh. Verify each produced PNG: + - exists at icons/icon{16,48,128}.png + - file(1) reports "PNG image data, NxN, 8-bit/color RGBA, non-interlaced" (RESEARCH §3 verified) + - size meets the FLOOR: 16≥200B, 48≥500B, 128≥1024B + - byte 25 (color-type) === 6 (RGBA) — this is the case Wave 0 Task 1's icons-present test asserts + + 3. Verify the OVERWRITE happened: the prior placeholders were `16-bit/color RGB` (per `file icons/icon*.png` at plan-write time); the new files MUST be `8-bit/color RGBA`. This is a load-bearing behavioral change confirming the new rasterized mark is in place. + + 4. Re-verify all Plan 01-13 harness assertions still pass — specifically A9 (icon file sizes meet imageUtil floors per RESEARCH §3 verification) and A8 (Bug A regression: chrome.notifications.create succeeds with the new icon). + + Run npx vitest run tests/build/icons-present.test.ts; expect GREEN. + + file icons/icon*.png && stat -c '%n: %s bytes' icons/icon*.png && npx vitest run tests/build/icons-present.test.ts 2>&1 | tail -10 && head -c 8 icons/icon16.png | xxd + + + - icons/icon16.png ≥ 200 B + 16×16 + 8-bit RGBA + - icons/icon48.png ≥ 500 B + 48×48 + 8-bit RGBA + - icons/icon128.png ≥ 1024 B + 128×128 + 8-bit RGBA + - scripts/rasterize-icons.sh exists + executable + - tests/build/icons-present.test.ts GREEN (all cases including the color-type RGBA case) + - Plan 01-13 harness A8 + A9 still GREEN (re-run `npm run test:uat -- --only=A8 --only=A9` if standalone selector supported; otherwise run full suite and confirm 15+/15+ GREEN) + - Atomic commit `feat(01-12): wave-2 task-1 — rasterize Loom mark to icons/{16,48,128}.png (overwrites Bug A placeholders)` + + + + + Wave 3 Task 1: Land _locales/{en,ru}/messages.json + migrate manifest.json to __MSG_*__ + default_locale + _locales/en/messages.json, _locales/ru/messages.json, manifest.json + + Two messages.json files at _locales/en/ + _locales/ru/ carry the 12-key matrix per RESEARCH §10 + Brief §02 verbatim + D-07 user override + D-08 tagline. manifest.json migrates to i18n with __MSG_extName__ + __MSG_extDesc__ + default_locale='en' + action.default_title='__MSG_tooltipOff__'. On extension reload after this wave, chrome://extensions displays "Mokosh — Session Capture" (D-07) — verifies the end-to-end i18n wiring. + + Test gates: tests/i18n/manifest-i18n.test.ts + tests/i18n/locale-parity.test.ts flip GREEN. + + + 1. Create _locales/ + _locales/en/ + _locales/ru/ directories (do not yet exist). + + 2. Create _locales/en/messages.json with the 12 keys per RESEARCH §10 + D-07 override + D-08 tagline (using brand-decisions-v1.md "always at hand." wording per RESEARCH Open Questions #4 default-action). Each key is a `{ "message": "", "description": "" }` shape per Chrome i18n schema: + + - extName: "Mokosh — Session Capture" + - extDesc: "Thirty seconds ago, always at hand." + - tooltipOff: "Mokosh — click to start recording" + - tooltipRecPrefix: "Mokosh — recording" + - tooltipErr: "Mokosh — recording error, click to recover" + - popupSavePrompt: "Save error report?" + - popupSaveCta: "Save report" + - popupSaveDone: "Archive saved to Downloads." + - popupInfoText: "Last 30 s video + 10 min log" + - notifStartup: "Recording started. I'm watching the last 30 seconds." + - notifRecovery: "Recording resumed. Buffer refilling." + - welcomeHeroRu: (RU primary; same as RU file's entry — used as the Russian-quoted hero string on welcome.html under EN locale) + - welcomeHeroEn: "Thirty seconds ago, always at hand." + + 3. Create _locales/ru/messages.json with the same 12 keys + Russian text per RESEARCH §10 Brief §02 verbatim extraction: + + - extName: "Mokosh — Запись сессии" + - extDesc: "Тридцать секунд назад, всегда под рукой." + - tooltipOff: "Mokosh — щёлкните, чтобы начать запись" + - tooltipRecPrefix: "Mokosh — идёт запись" + - tooltipErr: "Mokosh — ошибка записи, щёлкните для восстановления" + - popupSavePrompt: "Сохранить отчёт об ошибке?" + - popupSaveCta: "Сохранить отчёт" + - popupSaveDone: "Архив сохранён в Загрузки." + - popupInfoText: "Последние 30 сек видео + 10 мин лога" + - notifStartup: "Запись запущена. Я слежу за последними 30 секундами." + - notifRecovery: "Запись возобновлена. Буфер снова заполняется." + - welcomeHeroRu: "Тридцать секунд назад, всегда под рукой." + - welcomeHeroEn: "Thirty seconds ago, always at hand." + + Each key gets a `description` field carrying the surface + production call site (e.g. for tooltipRecPrefix: "Suffix '(MM:SS)' appended at runtime by the SW via formatElapsed(seconds)"). + + 4. Update manifest.json (Read → Edit): + - `name` 'AI Call Recorder' → `__MSG_extName__` + - `description` 'Запись сессий операторов для диагностики ошибок' → `__MSG_extDesc__` + - Add `default_locale` field with value `"en"` (between version and permissions, or wherever idiomatic per current shape) + - Add `default_title` field to `action` (alongside default_popup + default_icon) with value `"__MSG_tooltipOff__"` + - Verify the trailing JSON is valid (`JSON.parse(readFileSync('manifest.json', 'utf8'))` succeeds) + + 5. Run `npm run build`. Verify: + - `dist/manifest.json` carries the new shape (crxjs writes its own manifest copy) + - `dist/_locales/en/messages.json` + `dist/_locales/ru/messages.json` exist (crxjs propagates the _locales/ directory verbatim) + + NB: if crxjs's manifest-rewriting clobbers the i18n placeholders, document the issue + use the public/ directory fallback per RESEARCH §2 Pattern B (place _locales/ in public/_locales/ instead). + + 6. Verify tests/i18n/manifest-i18n.test.ts + tests/i18n/locale-parity.test.ts both GREEN. + + 7. Manual sanity: load the extension unpacked + observe chrome://extensions name field reads "Mokosh — Session Capture" (in English Chrome) or "Mokosh — Запись сессии" (in Russian Chrome). Document the observed display name in the commit body. + + + cat manifest.json | python3 -c 'import sys,json;m=json.load(sys.stdin);assert m["name"]=="__MSG_extName__",m["name"];assert m["description"]=="__MSG_extDesc__",m["description"];assert m["default_locale"]=="en";assert m["action"]["default_title"]=="__MSG_tooltipOff__";print("manifest OK")' && cat _locales/en/messages.json | python3 -c 'import sys,json;m=json.load(sys.stdin);assert m["extName"]["message"]=="Mokosh — Session Capture";assert m["extDesc"]["message"]=="Thirty seconds ago, always at hand.";print("en/messages.json OK")' && cat _locales/ru/messages.json | python3 -c 'import sys,json;m=json.load(sys.stdin);assert m["extName"]["message"]=="Mokosh — Запись сессии";assert m["extDesc"]["message"]=="Тридцать секунд назад, всегда под рукой.";print("ru/messages.json OK")' && npx vitest run tests/i18n/manifest-i18n.test.ts tests/i18n/locale-parity.test.ts 2>&1 | tail -10 + + + - _locales/en/messages.json + _locales/ru/messages.json exist + parse + each carry the 12 keys with the canonical values + - manifest.json:name === '__MSG_extName__'; :description === '__MSG_extDesc__'; :default_locale === 'en'; :action.default_title === '__MSG_tooltipOff__' + - npm run build succeeds + dist/_locales/{en,ru}/messages.json exist + dist/manifest.json carries the new i18n shape + - tests/i18n/manifest-i18n.test.ts + tests/i18n/locale-parity.test.ts GREEN + - Existing 98 vitest baseline + 6 new tests = 104 vitest GREEN + - Atomic commit `feat(01-12): wave-3 task-1 — manifest i18n (__MSG_*__ + default_locale='en') + _locales/{en,ru}/messages.json (12 keys; D-07 + D-08 baked in)` + + + + + Wave 4 Task 1: Migrate src/popup/ + src/background/ to tokens.css palette + chrome.i18n.getMessage + src/popup/index.html, src/popup/index.ts, src/popup/style.css, src/background/index.ts + + src/popup/style.css imports tokens.css + replaces every hex literal with var(--mks-*) reference. src/popup/index.html + src/popup/index.ts populate operator-facing strings via chrome.i18n.getMessage. src/background/index.ts migrates badge titles + notification copy to chrome.i18n.getMessage + updates BADGE_REC_COLOR to madder (--mks-madder-600 = #b2543d per D-04). All existing vitest tests stay GREEN (chrome.i18n is a synchronous API; existing chrome.* mocks need a getMessage stub returning the key as the value for round-trip identity). + + + 1. Update src/popup/style.css per the artifact-table `provides` summary: + - Add `@import "../shared/tokens.css";` at top (above `* { margin: 0; ...}`) + - body { background: var(--mks-surface); color: var(--mks-fg-1); font-family: var(--mks-font-ui); } + - .save-button { background: var(--mks-rec); color: var(--mks-fg-inverse); border-radius: var(--mks-radius-md); box-shadow: var(--mks-shadow-1); transition: all var(--mks-dur-base) var(--mks-ease-out); } + - .save-button:hover:not(:disabled) { background: var(--mks-madder-700); box-shadow: var(--mks-shadow-2); } + - .save-button:disabled { background: var(--mks-ink-300); } + - .save-button.saving { background: var(--mks-amber-600); } + - .save-button.done { background: var(--mks-moss-600); } + - .info-text { color: var(--mks-fg-2); font-family: var(--mks-font-ui); } + - .status-message.error { color: var(--mks-error); } + - .status-message.success { color: var(--mks-success); } + - .status-message.info { color: var(--mks-warning); } + - ZERO hex literals (regex /#[0-9a-fA-F]{3,8}/ match count === 0; verified by tests/build/tokens-adopted.test.ts case (c)) + + 2. Update src/popup/index.html per artifact-table: + - Mokosh — Session Capture (literal English title — chrome doesn't substitute __MSG__ in HTML body text or ; popup's title is a sensible default for non-RU locales). Document the limitation in a comment. + - — empty (populated by JS); remove the hardcoded 'Сохранить отчёт об ошибке' + -

— empty + data-mks-key attribute; remove the hardcoded 'Последние 30 сек видео + 10 мин лога' + + 3. Update src/popup/index.ts per artifact-table: + - In init(): call populateMksKeys() helper that loops over [data-mks-key] elements and sets each textContent = chrome.i18n.getMessage(el.dataset.mksKey || '') + - In updateUI() idle case: buttonText.textContent = chrome.i18n.getMessage('popupSaveCta') + - In updateUI() saving case: buttonText.textContent = chrome.i18n.getMessage('popupSaving') (new key — add to messages.json with 'Сохраняю...' RU / 'Saving...' EN; OR fallback to literal if missing — document choice) + - In updateUI() done case: buttonText.textContent = chrome.i18n.getMessage('popupSaveDoneShort') (new key — 'Готово! ✓' RU / 'Done ✓' EN) OR retain literal with a TODO + - In updateUI() empty-state branch: statusMessage.textContent = chrome.i18n.getMessage('popupEmptyState') (new key — 'Откройте запись через иконку расширения' RU / 'Open recording via the extension icon' EN) + - In saveArchive() success branch: statusMessage.textContent = chrome.i18n.getMessage('popupSaveDone') + + Add the new keys (popupSaving, popupSaveDoneShort, popupEmptyState) to BOTH _locales/en/messages.json AND _locales/ru/messages.json. Re-verify locale-parity.test.ts. + + 4. Update src/background/index.ts per the `` shape block: + - Line 76 BADGE_REC_COLOR: '#00C853' → '#b2543d' with comment '// --mks-madder-600 per D-04 loom palette (Plan 01-12)' + - BADGE_OFF_COLOR ('#D32F2F') stays (no loom-palette token for OFF) OR change to var(--mks-ink-700) hex literal '#2f3349'; document choice in commit + - BADGE_ERROR_COLOR ('#F9A825') stays (no loom-palette token for warning amber) OR change to '#c98b3a' (= --mks-amber-600); document choice in commit + - Lines 82-84 BADGE_*_TITLE constants: KEEP as fallback strings (for any chrome.* stub contexts) BUT replace the runtime read in setBadgeState with `chrome.i18n.getMessage() || ` so the existing vitest tests that don't mock chrome.i18n still pass via fallback + - Line 862-865 RECOVERY notification body: title=chrome.i18n.getMessage('extName') || 'Mokosh', message=chrome.i18n.getMessage('notifRecovery') || 'Recording resumed.' + - Line 961-965 STARTUP notification body: title=chrome.i18n.getMessage('extName') || 'Mokosh', message=chrome.i18n.getMessage('notifStartup') || 'Recording started.' + + Use the `|| fallback` pattern uniformly so chrome.i18n unavailability (in unit-test mocks lacking i18n) degrades to literal English-ish text rather than the empty string Chrome returns for missing keys. + + 5. Update relevant existing vitest tests that may need a chrome.i18n.getMessage stub: + - tests/background/badge-state-machine.test.ts: if setBadgeState now reads chrome.i18n.getMessage, the chromeStub needs `i18n: { getMessage: vi.fn((k: string) => k) }` so the test asserts title === as an identity-passthrough. Adjust assertions accordingly. + - tests/background/onstartup-notification.test.ts: same — add i18n.getMessage stub. + - tests/background/save-archive-stops-recording.test.ts: same if it asserts notification copy. + + The fallback `|| ` pattern reduces the need for chrome.i18n stubs in tests that don't depend on the displayed text; tests that DO depend on text shape need the identity-passthrough stub. + + 6. Run `npm test` — expect 104+ GREEN (98 existing + 6 new tests + any new copy in popup tests adjusted via chrome.i18n stub). + + 7. Run `npm run build` then re-run `tests/build/no-remote-fonts.test.ts` — expect GREEN (dist/tokens.css contains zero `googleapis` / `https://fonts`). + + + grep -c "#[0-9a-fA-F]\{3,8\}" src/popup/style.css && grep -c "var(--mks-" src/popup/style.css && grep -c "chrome.i18n.getMessage" src/popup/index.ts && grep -c "chrome.i18n.getMessage" src/background/index.ts && grep -n "BADGE_REC_COLOR.*#b2543d\|--mks-madder-600" src/background/index.ts && npx vitest run 2>&1 | tail -10 && npm run build 2>&1 | tail -5 && npx vitest run tests/build/no-remote-fonts.test.ts 2>&1 | tail -10 + + + - src/popup/style.css contains 0 hex literals + ≥ 8 var(--mks-*) references + @import of tokens.css + - src/popup/index.html + src/popup/index.ts use chrome.i18n.getMessage at every operator-facing copy site (verified by grep ≥ 4 occurrences in popup/index.ts) + - src/background/index.ts uses chrome.i18n.getMessage at every operator-facing copy site (badge titles + 2 notification call sites; verified by grep ≥ 4 occurrences); BADGE_REC_COLOR === '#b2543d' + - tests/build/tokens-adopted.test.ts ALL cases GREEN (including (b)+(c) which were RED in Wave 0) + - tests/build/no-remote-fonts.test.ts GREEN (after npm run build) + - vitest baseline 104+ GREEN (existing 98 + 6 new); no regressions in tests/background/* + - Atomic commit `feat(01-12): wave-4 task-1 — adopt tokens.css + chrome.i18n.getMessage in src/popup/ + src/background/ (loom palette + RU i18n + en fallback)` + + + + + Wave 5 Task 1: Welcome page conditional migration + Vite __VITE_DEV__ define-token + scripts/README.md + src/welcome/welcome.css, src/welcome/welcome-tokens.css, src/welcome/welcome.ts, src/welcome/copy.ts, vite.config.ts, scripts/README.md + + If Plan 01-10 has landed (src/welcome/ exists at execute-plan time): migrate welcome-tokens.css → @import '../shared/tokens.css' (one-liner) AND migrate copy.ts to chrome.i18n.getMessage reads for welcomeHeroRu + welcomeHeroEn + any other welcome keys. + + If Plan 01-10 has NOT landed: skip all src/welcome/* changes; Plan 01-10 executor uses src/shared/tokens.css directly when 01-10 lands later (no placeholder welcome-tokens.css needed in that path). + + Unconditionally: add __VITE_DEV__ define-token to vite.config.ts (RESEARCH §12 D-09 spirit-satisfaction) + create scripts/README.md documenting the smoke-isolation invariant. + + + 1. Detect Plan 01-10 state: `ls src/welcome/welcome.html 2>/dev/null && echo "01-10 landed" || echo "01-10 not yet"`. Branch on result. + + 2A. IF Plan 01-10 has landed: + - Read src/welcome/welcome-tokens.css. Replace contents with `@import '../shared/tokens.css';\n` (single line file). NB: per RESEARCH §9 anti-pattern, do NOT create per-surface mini-tokens — collapse into the canonical tokens.css. + - Read src/welcome/welcome.css. Verify it uses var(--mks-*) references throughout (per 01-10 design-swap-ready architecture; no hex literals expected). Add a comment header noting "tokens.css now canonical via welcome-tokens.css → @import." + - Read src/welcome/copy.ts. Reduce to a fallback shim: export `function getCopy(key: string): string { const i18n = chrome.i18n?.getMessage?.(key); return i18n && i18n.length > 0 ? i18n : COPY[key] || ''; }` where COPY is the existing static map (retained as the safety net). Update src/welcome/welcome.ts to call `getCopy()` instead of `COPY[key]` directly. + - Add to _locales/en/messages.json + _locales/ru/messages.json the welcome keys that copy.ts exposes (welcomeHeroTitle, welcomeBodyExplainerLine1, welcomeBodyExplainerLine2, welcomeBodyCtaToolbar, welcomeFooterPrivacy, etc. — per 01-10-PLAN.md src/welcome/copy.ts artifact list). Re-verify locale-parity.test.ts GREEN. + - Re-run 01-10's Plan 01-10 onboarding tests + harness A17 (welcome.css zero-hex regex + COPY[ substring check): assert A17's regex-based welcome.css check still GREEN (welcome.css continues to have zero hex literals). + + 2B. IF Plan 01-10 has NOT landed: + - Skip all src/welcome/* file modifications. Document this in the commit body: "Plan 01-10 not yet landed; welcome migration deferred. When 01-10 lands, its executor uses src/shared/tokens.css directly (skipping the placeholder welcome-tokens.css step entirely)." Add a paragraph to .planning/STATE.md noting the deferral so 01-10's planner can adjust. + + 3. Unconditionally update vite.config.ts: in the existing `define` block, add `__VITE_DEV__: JSON.stringify(process.env.VITE_DEV === '1')` alongside the existing `__MOKOSH_UAT__: 'false'`. Add an inline comment per RESEARCH §12 explaining the defensive intent ("reserved for any future inline smoke-mode check; currently smoke lives entirely in smoke.sh which is outside Vite's input set — verified by `grep -rn 'smoke\\|SMOKE\\|data:text/html' src/` returning empty"). + + 4. Verify vite.test.config.ts mergeConfig propagates __VITE_DEV__ (no explicit override needed — mergeConfig spreads define fields). + + 5. Create scripts/README.md (single paragraph per RESEARCH §12): document that smoke.sh + future dev-only scripts live in this directory + are NOT bundled by npm run build + the production dist/ contains no smoke artifacts. Cross-reference RESEARCH.md §12 for the no-op-confirmation rationale. + + 6. Run npm test + npm run build + npm run build:test — expect all GREEN baseline + 6 new tests. + + 7. (Optional but recommended per RESEARCH §12 + the existing Tier-1 gate pattern): add a `tests/build/no-smoke-in-dist.test.ts` test asserting `grep -rn smoke dist/` returns 0 matches post-build. RED today if any smoke string leaks; GREEN per current architecture. Keep this OPTIONAL — if context budget tight, defer to a follow-up plan. + + + (ls src/welcome/welcome.html 2>/dev/null && echo "Plan 01-10 landed — checking welcome migration:" && grep -c "tokens.css" src/welcome/welcome-tokens.css && grep -c "chrome.i18n" src/welcome/welcome.ts) || echo "Plan 01-10 not landed — skipped welcome migration" && grep -c "__VITE_DEV__" vite.config.ts && ls -la scripts/README.md && npx vitest run 2>&1 | tail -10 + + + - IF 01-10 landed: src/welcome/welcome-tokens.css === single @import line; welcome.ts uses chrome.i18n.getMessage via getCopy() shim + - IF 01-10 NOT landed: src/welcome/* unchanged; STATE.md gains deferral note + - vite.config.ts:define block contains __VITE_DEV__ entry + - scripts/README.md exists + documents smoke-isolation invariant + - vitest baseline 104+ GREEN (+ N welcome keys added to messages.json if 01-10 landed; locale-parity stays GREEN) + - Atomic commit `feat(01-12): wave-5 task-1 — welcome i18n migration (conditional on 01-10) + __VITE_DEV__ define + scripts/README.md` + + + + + Wave 6 Task 1: Extend UAT harness with A18-A22 (font / icon / manifest-i18n / Lora-resolved / welcome-tokens) + tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts + + The 01-13 harness gains 5 new assertions following the established Approach-B pattern (page-side assertA* + host-side driveA* wrapper + harness.test.ts orchestrator entry). Each assertion uses production chrome.* APIs + fetch + getComputedStyle (no new test-mode symbols). After this wave: `npm run test:uat` reports 20/20 GREEN (or 23/23 with 01-10's A15-A17 already in place). + + Tier-1 FORBIDDEN_HOOK_STRINGS list stays at 10 entries (no new test-mode surface introduced). + + + 1. Extend tests/uat/extension-page-harness.ts: add 5 page-side methods to window.__mokoshHarness per the `` block shapes: + - assertA18(): walks document.styleSheets for the first @font-face rule whose src URL contains 'Lora'; resolves the URL relative to location.href; fetches it; asserts response.ok + arrayBuffer().byteLength >= 50000. Returns AssertionRecord with structured checks (A18.1 found rule; A18.2 fetch status; A18.3 byteLength). + - assertA19(): fetches chrome.runtime.getURL('icons/icon128.png'); reads arrayBuffer; computes a simple identity check (first 32 bytes byte-by-byte comparison against a hardcoded snapshot of the Bug A placeholder file's first 32 bytes); returns A19.1: "icon differs from Bug A placeholder fingerprint". Stores the placeholder fingerprint as a const at top of the file with a comment referencing this plan + the verification command to refresh it. + - assertA20(): chrome.runtime.getManifest().name === 'Mokosh — Session Capture'; structured AssertionRecord with A20.1 manifest.name === '...'. + - assertA21(): creates a transient div, applies class='mks-display-1', reads getComputedStyle.fontFamily, asserts startsWith 'Lora' or '"Lora"'. Returns A21.1: fontFamily resolved. + - assertA22(): fetches chrome.runtime.getURL('src/welcome/welcome.html'); if status === 404, returns PASS with diagnostic "Plan 01-10 not landed; A22 skipped"; if status === 200, also fetches the linked welcome.css (extract from the HTML) and asserts welcome.css contains either `tokens.css` import OR substantive var(--mks-*) usage (regex /var\\(--mks-[a-z-]+\\)/ match count >= 3). + + 2. Extend tests/uat/lib/harness-page-driver.ts: add driveA18 / driveA19 / driveA20 / driveA21 / driveA22 wrappers following the established driveA4 pattern (page.evaluate(() => window.__mokoshHarness.assertXX())). No new host-side fs / ffprobe / zip primitives. + + 3. Extend tests/uat/harness.test.ts: import driveA18..A22; add entries to the orchestrator's assertion list in sequence (after A14, or A17 if 01-10 landed). Keep FORBIDDEN_HOOK_STRINGS UNCHANGED at 10 entries — verify by `wc -l` diff before/after this task. + + 4. Run npm run build:test (rebuilds dist-test/). Then run `npm run test:uat` (or just `tsx tests/uat/harness.test.ts`). Expected output: 20/20 (or 23/23) GREEN. + + 5. Run `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts` — confirm Tier-1 grep gate GREEN (no new hook strings leaked into dist/). + + + grep -c "assertA1[89]\|assertA2[012]" tests/uat/extension-page-harness.ts && grep -c "driveA1[89]\|driveA2[012]" tests/uat/lib/harness-page-driver.ts && grep -c "driveA1[89]\|driveA2[012]" tests/uat/harness.test.ts && (wc -l tests/uat/harness.test.ts && grep "FORBIDDEN_HOOK_STRINGS" tests/uat/harness.test.ts -A 13 | grep -c "'") && npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts 2>&1 | tail -5 && (npm run test:uat 2>&1 | tail -25 || echo "test:uat invocation needs verification; check for 20/20 or 23/23 GREEN") + + + - 5 new assertA* methods on window.__mokoshHarness (verified by grep ≥ 5) + - 5 new driveA* wrappers in harness-page-driver.ts + - harness.test.ts orchestrator runs all 20 (or 23 with 01-10) assertions sequentially + - npm run test:uat exits 0 with 20/20 (or 23/23) GREEN + - Tier-1 forbidden-strings inventory in no-test-hooks-in-prod-bundle.test.ts UNCHANGED at 10 entries (no new test-mode symbols) + - Atomic commit `feat(01-12): wave-6 task-1 — harness A18-A22 (font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens)` + + + + + Wave 7 Checkpoint: Operator empirical brand-fit ack + (operator-driven; no source files modified by this checkpoint — Wave 1-6 already landed the artifacts) + See below — operator-driven empirical check against fresh build loaded in Chrome. The executor agent must NOT bypass this checkpoint by stubbing; brand fit is the LAST Phase 1 operator gate and must be confirmed against the real branded extension. + + echo "checkpoint:human-verify — see how-to-verify section; resume signal is the gate" + + Operator types 'approved' after running the how-to-verify steps. See . + + Plan 01-12 lands the full design integration: Lora self-hosted (R2 designer reply); IBM Plex Sans + Mono self-hosted (Latin + Cyrillic); src/shared/tokens.css canonical (Newsreader → Lora substitution + .mks-word class + zero remote @import); icons rasterized from Loom mark (replaces Bug A placeholders); manifest:name + :description + :default_title migrated to chrome i18n (__MSG_*__ + _locales/{en,ru}/messages.json); 12 i18n keys land with D-07 + D-08 + Brief §02 verbatim Russian copy; src/popup/ + src/background/ use chrome.i18n.getMessage at every operator-facing site + tokens.css palette; BADGE_REC_COLOR madder-rust per D-04; welcome page (if 01-10 landed) inherits canonical tokens; vite __VITE_DEV__ defensive define; UAT harness 20/20 (or 23/23) GREEN with A18-A22 covering font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens. + + + 1. Fresh build + load: + - `rm -rf dist/ dist-test/` + - `npm run build` + - Open `chrome://extensions/`, toggle Developer mode, click "Load unpacked", select the `dist/` directory. + - Verify: extension name shows "Mokosh — Session Capture" (en locale) or "Mokosh — Запись сессии" (ru locale). The extension's icon in the toolbar is the rasterized Loom mark (4×4-grid silhouette inside a rounded square), NOT the prior dark-square + green-dot placeholder. + + 2. Popup brand fit: + - Click the Mokosh toolbar icon to open the popup. + - Verify: popup background is warm-linen (--mks-surface = #faf7f1), NOT cool-gray; SAVE button is madder-rust (--mks-rec = #b2543d), NOT material-blue; info text is warm gray (--mks-fg-2 = #5b5f76); copy reads "Сохранить отчёт" (RU) or "Save report" (EN) per chrome's resolved locale. + + 3. Russian copy rendering in Lora: + - Switch Chrome to Russian locale (chrome://settings/languages — add Russian + move to top + restart). + - Re-open the Mokosh popup. Verify Russian strings render correctly in IBM Plex Sans (UI body) — text reads "Сохранить отчёт", "Архив сохранён в Загрузки.", etc. + - The display-register text (if any visible in popup) renders in Lora — verify via DevTools → Inspect → Computed → font-family (should read 'Lora' as the matched family for any .mks-display-* class). For now, popup doesn't have visible display-register text; the load-bearing display-register check happens on the welcome page (if 01-10 landed) OR by spawning a temporary `chrome-extension:///src/welcome/welcome.html` if available. + + 4. Manifest name in chrome://extensions: + - Switch back to English Chrome (or stay in RU for parallel verification). + - chrome://extensions/ shows "Mokosh — Session Capture" (en) or "Mokosh — Запись сессии" (ru) in the extension card. + + 5. Notification icon + copy: + - Trigger a recording start (click toolbar — toolbar:onClicked → startVideoCapture → eventually notification). + - Verify: startup notification icon is the rasterized Loom mark (not placeholder); copy reads "Запись запущена. Я слежу за последними 30 секундами." (ru) / "Recording started. I'm watching the last 30 seconds." (en). + + 6. Welcome page (if 01-10 landed): + - Navigate to chrome-extension:///src/welcome/welcome.html in a tab. + - Verify: welcome hero renders D-08 tagline in Lora; warm-linen background; mark slot shows the Loom mark; Russian + English parallel-text copy renders correctly. + + 7. Harness gate: + - `npm run test:uat` exits 0 with 20/20 GREEN (or 23/23 with 01-10). + - Tier-1 grep gate GREEN: `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts`. + + 8. Type "approved" if brand fit accepted, OR describe specific issues for follow-up. + + Type "approved" or describe issues for follow-up + + + + Wave 7 Task 1: Plan closure (SUMMARY + STATE.md + ROADMAP.md sync) + .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md, .planning/STATE.md, .planning/ROADMAP.md + + 1. Create .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md following the template at $HOME/.claude/get-shit-done/templates/summary.md AND mirroring the structural choices of 01-13-SUMMARY.md (frontmatter with phase/plan/subsystem/tags/requires/provides/affects/tech-stack/key-files/decisions/metrics; body sections One-Liner / What Landed by Wave / Test Counts / Deviations from Plan / Architectural Notes Worth Carrying Forward / Self-Check / Known Limitations / Bridge to Phase 1 Closure). + + Key content: + - One-Liner: "Design integration landed end-to-end: Lora self-hosted (R2 designer reply 2026-05-19); src/shared/tokens.css canonical; 12 i18n keys via _locales/{en,ru}; branded Loom icons replace placeholders; src/popup + src/background use chrome.i18n.getMessage + loom palette; harness 20/20 (or 23/23) GREEN with A18-A22; operator brand-fit ack received." + - Wave-by-wave breakdown with commit refs. + - Test Counts table: vitest before/after; harness before/after. + - Architectural Notes: R2 substitution rationale; OFL attribution pattern; tokens.css as single source of truth; chrome.i18n.getMessage fallback pattern (|| ); .mks-word class as engineering working-defn (designer overridable); icons-as-static-artifacts (NOT regenerated in prebuild per RESEARCH §3 anti-pattern). + - Self-Check: FOUND list including the 6 new vitest tests, 5 new harness assertions, font bundle, icons, manifest i18n, _locales/, source-code adoption. + - Bridge: Phase 1 closure status (the LAST Phase 1 gate, brand-fit ack, is now closed); Phase 2 inherits tokens.css + chrome.i18n.getMessage as production conventions. + + 2. Update .planning/STATE.md: + - completed_plans + 1 (current 12 → 13) + - percent re-computed (likely 100% Phase 1 now, since this was the last gate) + - Add a "Plan 01-12 closure" section under "## Current Position" with the brand-fit ack date + harness count + R2 substitution note + - Update "stopped_at" + "last_updated" + "last_activity" fields + - Update "## Outstanding Phase 1 gates" section: remove Plan 01-12 entry (it's now closed); confirm Plan 01-10 status (closed if landed; still pending if not) + + 3. Update .planning/ROADMAP.md: + - In the Phase 1 Plans list (after `01-13-PLAN.md` entry per 01-13 SUMMARY ROADMAP.md known-gap), add `- [x] 01-12-PLAN.md — Design integration (R2 Lora, tokens.css, 8 i18n strings, branded icons, manifest i18n)` + - If Plan 01-10 entry is missing per the 01-13 SUMMARY note about the ROADMAP.md gap, add it too as `- [x]` or `- [ ]` depending on its current status. (Per 01-13 SUMMARY Self-Check: Plan 01-10 was still pending at 01-13 close; if it landed between 01-13 close and this 01-12 close, mark `[x]`.) + - Update the Phase 1 header line if it carries a count (e.g., "**Plans:** N plans" → updated to actual final count). + + 4. Atomic commit: `docs(01-12): wave-7 task-1 — Plan 01-12 SUMMARY + STATE.md + ROADMAP.md sync (design integration closed; Phase 1 charter complete)`. + + + ls -la .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md && grep -n "01-12-PLAN.md\|R2 Lora\|design integration" .planning/ROADMAP.md && grep -n "01-12\|completed_plans" .planning/STATE.md | head -10 && head -20 .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md + + + - .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md exists + follows template + carries all 9 frontmatter sections + - STATE.md completed_plans incremented + percent recomputed + Plan 01-12 closure section added + - ROADMAP.md Phase 1 Plans list contains the 01-12 entry (and 01-10 entry if previously missing) + - Atomic commit lands all three docs in one + - Plan 01-12 functionally CLOSED; Phase 1 charter complete (subject to the Wave 7 checkpoint operator ack) + + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Chrome extension boundary (MV3 CSP) | tokens.css @font-face MUST reference local URLs only; remote `@import` (Google Fonts) violates `style-src 'self'` + `font-src 'self'` per RESEARCH §13 | +| User-locale boundary (chrome.i18n) | messages.json values are operator-facing copy; manifest:name + :description surface in Chrome Web Store + chrome://extensions across all locales | +| Asset-pipeline boundary (Vite + @crxjs) | WOFF2 files are emitted to dist/assets/ with content-hashed names; web_accessible_resources auto-generated by @crxjs | +| Test-only-symbol boundary (__MOKOSH_UAT__) | Tier-1 grep gate enforces test-mode hook symbols are absent from production dist/ — Plan 01-12 does NOT extend the inventory (no new symbols) | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-01-12-01 | Information Disclosure | Remote font load via `@import url(https://fonts.googleapis.com/...)` in tokens.css | mitigate | Replace line 12 Google Fonts @import with 7-rule local @font-face block (Wave 1 Task 2). Verify zero `googleapis` matches in dist/ via tests/build/no-remote-fonts.test.ts (Wave 0). Fonts ship under src/shared/fonts/ with OFL LICENSE files (Wave 1 Task 1). | +| T-01-12-02 | Tampering | CDN MITM on remote font fetch | mitigate | Same as T-01-12-01: self-hosting eliminates the CDN trust dependency entirely. WOFF2 files ship as extension-internal assets; integrity is guaranteed by Chrome's extension-package signature. | +| T-01-12-03 | Denial of Service | PNG icon below Chrome imageUtil floor → chrome.notifications.create silent rejection | mitigate | tests/build/icons-present.test.ts asserts each PNG ≥ FLOOR (16≥200B, 48≥500B, 128≥1024B) per RESEARCH §3 + assets-spec.md (Wave 0 + Wave 2). Plan 01-13 harness A9 + A8 (regression rewind for Bug A) catch leaks at harness-time. | +| T-01-12-04 | Denial of Service | _locales/ key tampering causing manifest:name resolution failure → manifest validation error at install | mitigate | tests/i18n/manifest-i18n.test.ts asserts manifest fields + messages.json key shapes (Wave 0); tests/i18n/locale-parity.test.ts enforces default_locale parity per RESEARCH Pitfall 4. JSON validity guaranteed by JSON.parse in tests. | +| T-01-12-05 | Tampering | Vite asset hashing breaking @font-face url() resolution at runtime | mitigate | RESEARCH §2 Pattern A validated empirically (tokens.css → relative url(./fonts/x.woff2) → Vite rebases → @crxjs auto-WAR). UAT harness A18 walks document.styleSheets to resolve the actual emitted URL (handles hashing); fetch + size check verifies runtime reachability. Pattern B fallback (public/fonts/) documented in RESEARCH §2 if Pattern A misfires. | +| T-01-12-06 | Spoofing | `__MSG_@@extension_id__` substitution in manifest fields (not supported per Chrome docs) | accept | Plan 01-12 does NOT use @@extension_id; documented as RESEARCH §11 anti-pattern + per the explicit "Anti-pattern" subsection. Low blast radius (only affects if engineer ignores docs). | +| T-01-12-07 | Information Disclosure | chrome.i18n.getMessage returning empty string when default_locale lacks key (RESEARCH Pitfall 4) | mitigate | The `|| ` fallback pattern uniformly applied in src/popup/index.ts + src/background/index.ts (Wave 4) ensures missing-key cases degrade to literal English-ish text rather than blank UI. tests/i18n/locale-parity.test.ts asserts key-parity to catch missing keys at unit-test time. | +| T-01-12-08 | Elevation of Privilege | Test-mode hook strings leaking into production dist/ (T-1-11-01 + T-1-13-* inheritance) | accept | Plan 01-12 introduces ZERO new test-mode symbols (A18-A22 use production chrome.* + fetch + getComputedStyle exclusively). Tier-1 grep gate at tests/background/no-test-hooks-in-prod-bundle.test.ts remains at 10 entries; no extension needed. Verified by file-diff at task end. | +| T-01-12-09 | Tampering | OFL license/attribution non-compliance | mitigate | src/shared/fonts/LICENSE-Lora.txt + LICENSE-IBM-Plex.txt ship verbatim per OFL-1.1 attribution best practice (RESEARCH §7); src/shared/fonts/README.md cross-references upstream URLs + R2 substitution rationale. | + + + +Phase-level invariants to assert post-plan: + +1. **Build cleanliness:** `npm run build` exits 0; `npm run build:test` exits 0; `npx tsc --noEmit` exits 0. +2. **MV3 CSP compliance:** zero `googleapis` / `https://fonts` matches in dist/ (tests/build/no-remote-fonts.test.ts). +3. **Vitest baseline grown:** 98 → 104+ GREEN (+6 new unit tests; +N if welcome migration adds keys). +4. **UAT harness 20/20 (or 23/23 with 01-10) GREEN:** `npm run test:uat` exits 0; A0 Tier-1 grep gate GREEN; A1-A14 (or A1-A17 with 01-10) GREEN; A18-A22 GREEN. +5. **Font bundle present:** all 7 or 8 WOFF2 files exist under src/shared/fonts/; total bundle ≤ 350KB; LICENSE files + README.md present. +6. **Icons rasterized:** icons/icon{16,48,128}.png are 8-bit RGBA PNGs at correct dims + clearing FLOOR; differ in content from the Bug A placeholders. +7. **Manifest i18n wired:** chrome://extensions name reads "Mokosh — Session Capture" (en) or "Mokosh — Запись сессии" (ru); operator-facing tooltips + notification copy + popup CTA all resolve via chrome.i18n.getMessage. +8. **Tokens adopted:** src/popup/style.css has 0 hex literals + imports tokens.css; src/welcome/welcome.css (if 01-10 landed) imports tokens.css; BADGE_REC_COLOR is madder-rust #b2543d. +9. **Tier-1 forbidden-strings inventory UNCHANGED at 10 entries.** +10. **R2 substitution complete:** zero `Newsreader` references anywhere in src/shared/tokens.css; --mks-font-display === '"Lora", "Iowan Old Style", "Times New Roman", serif'. +11. **Operator brand-fit ack:** Wave 7 checkpoint received "approved". + + + +- [ ] All 6 vitest tests in tests/build/ + tests/i18n/ GREEN +- [ ] src/shared/fonts/ contains all required WOFF2 + LICENSE + README.md files +- [ ] src/shared/tokens.css canonical: zero remote-font URLs, R2 Lora substitution, .mks-word class present +- [ ] icons/icon{16,48,128}.png rasterized from Loom mark, clearing all FLOORs, 8-bit RGBA +- [ ] manifest.json carries `__MSG_extName__` + `__MSG_extDesc__` + `default_locale: "en"` + `action.default_title: "__MSG_tooltipOff__"` +- [ ] _locales/{en,ru}/messages.json each carry the 12-key matrix with D-07 + D-08 + Brief §02 values +- [ ] src/popup/ + src/background/ use chrome.i18n.getMessage at every operator-facing copy site (with `|| ` fallback) +- [ ] src/popup/style.css has zero hex literals + imports tokens.css +- [ ] BADGE_REC_COLOR === '#b2543d' (--mks-madder-600 per D-04) +- [ ] If Plan 01-10 landed: welcome-tokens.css = `@import '../shared/tokens.css';`; welcome.ts uses chrome.i18n.getMessage +- [ ] vite.config.ts has `__VITE_DEV__` defensive define +- [ ] scripts/{rasterize-icons.sh,subset-fonts.sh,README.md} all present +- [ ] UAT harness A18-A22 implemented + GREEN; npm run test:uat exits 0 with 20/20 (or 23/23 with 01-10) GREEN +- [ ] Tier-1 forbidden-strings inventory UNCHANGED at 10 entries +- [ ] Operator empirical brand-fit ack ("approved") received at Wave 7 checkpoint +- [ ] 01-12-SUMMARY.md exists + STATE.md + ROADMAP.md synced +- [ ] All commits atomic + follow `feat(01-12)/test(01-12)/docs(01-12): wave-N task-M — ` convention + + + +After completion, create `.planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md` per the Wave 7 Task 1 spec — mirroring the structure of 01-13-SUMMARY.md (frontmatter with phase/plan/subsystem/tags/requires/provides/affects/tech-stack/key-files/decisions/metrics + body sections One-Liner / What Landed / Test Counts / Deviations / Architectural Notes Worth Carrying Forward / Self-Check / Known Limitations / Bridge to Phase 1 Closure). +