Files
mokosh/.planning/phases/01-stabilize-video-pipeline/01-12-PLAN.md
Mark 3fe018beb9 fix(01-12): revise plan baselines per Plan 01-14 landing (vitest 98→100, UAT 15→16, FORBIDDEN_HOOK_STRINGS 10→12)
Surgical amendment — Plan 01-12 is unexecuted; safe for in-place revision.
Plan 01-14 landed (b46712352541459792c0f) since 01-12 was authored,
adding monitorTypeSurfaces:'include' + UAT A23 + 2 FORBIDDEN_HOOK_STRINGS
entries. Plan 01-12 baseline arithmetic updated to reflect post-01-14
floors and depends_on chain extended.

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:45:58 +02:00

1251 lines
109 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 01-stabilize-video-pipeline
plan: 12
type: tdd
wave: 6
depends_on:
- 01-09
- 01-13
- 01-14
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('<key>') 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('<key>') 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 12 strings post-Plan-01-14 (no new test-mode symbols introduced by Plan 01-12; the i18n + tokens + font work uses production chrome.* APIs end-to-end). Plan 01-14 established the 12-string baseline by adding `lastGetDisplayMediaConstraints` + `get-last-getDisplayMedia-constraints` for its monitorTypeSurfaces harness. 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), Plan 01-14 assertion (A23), 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 100 GREEN (post-01-14) to 106 GREEN; npm run test:uat grows from 16/16 (post-01-14, or 19/19 with 01-10) to 21/21 (or 24/24 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: <title>Mokosh — Session Capture</title> 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 12 entries post-Plan-01-14 (no new test-mode symbols introduced by Plan 01-12). Assertion count grows from 16/16 (post-01-13+01-14) or 19/19 (post-01-10+01-13+01-14) to 21/21 or 24/24 (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 21/21 GREEN (or 24/24 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/<face>.woff2') per RESEARCH §2 Pattern A; Vite asset pipeline rebases at build to dist/assets/<hash>.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 <svg> -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 `<scope_estimation>`, 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.
<objective>
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 100+ 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 21/21 (or 24/24 with 01-10) GREEN.
- Wave 7: operator checkpoint; STATE.md + ROADMAP.md sync; SUMMARY produced.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/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
<interfaces>
<!-- Key types, paths, and surfaces the executor needs. Embedded so no codebase exploration required. -->
### 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<HTMLElement>('[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<AssertionRecord> {
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<AssertionRecord> {
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<AssertionRecord> {
// 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 12 post-Plan-01-14)
```ts
const FORBIDDEN_HOOK_STRINGS: ReadonlyArray<string> = [
'__mokoshTest',
'setCurrentStream',
'setSegmentCountGetter',
'installFakeDisplayMedia',
'uninstallFakeDisplayMedia',
'dispatchEndedOnTrack',
'getSegmentCount',
'__mokoshOffscreenQuery',
'get-display-surface',
'get-segment-count',
'lastGetDisplayMediaConstraints', // added by Plan 01-14
'get-last-getDisplayMedia-constraints', // added by Plan 01-14
];
// Plan 01-12 introduces NO new test-mode symbols (A18-A22 use production chrome.* APIs
// + fetch + getComputedStyle exclusively). Do NOT extend this list beyond the 12 entries
// established by Plan 01-14.
```
### 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);
});
});
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Wave 0 Task 1: Scaffold RED unit tests (TDD baseline)</name>
<files>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</files>
<behavior>
- 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.
</behavior>
<action>
Create the six test files per the `<interfaces>` 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 `<interfaces>` 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.
</action>
<verify>
<automated>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)"</automated>
</verify>
<done>
- 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 100 vitest GREEN baseline (post-01-14) 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`
</done>
</task>
<task type="auto" tdd="true">
<name>Wave 1 Task 1: Subset OFL fonts to src/shared/fonts/</name>
<files>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</files>
<behavior>
Per RESEARCH §1 + §6 + the `<interfaces>` 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.
</behavior>
<action>
1. Create scripts/subset-fonts.sh per the `<interfaces>` 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 `<interfaces>`, 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.
</action>
<verify>
<automated>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</automated>
</verify>
<done>
- 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)`
</done>
</task>
<task type="auto" tdd="true">
<name>Wave 1 Task 2: Land canonical src/shared/tokens.css (R2 Lora substitution + .mks-word + CSP-safe @font-face)</name>
<files>src/shared/tokens.css, src/shared/brand/mokosh-mark.svg, src/shared/brand/mokosh-lockup.svg</files>
<behavior>
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 `<interfaces>`
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 `<interfaces>` 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).
</behavior>
<action>
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 `<interfaces>` — 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 `<interfaces>` 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.
</action>
<verify>
<automated>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</automated>
</verify>
<done>
- 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)`
</done>
</task>
<task type="auto" tdd="true">
<name>Wave 2 Task 1: Rasterize Loom brand mark to icons/icon{16,48,128}.png</name>
<files>icons/icon16.png, icons/icon48.png, icons/icon128.png, scripts/rasterize-icons.sh</files>
<behavior>
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).
</behavior>
<action>
1. Create scripts/rasterize-icons.sh per the `<interfaces>` 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.
<verify>
<automated>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</automated>
</verify>
<done>
- 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)`
</done>
</task>
<task type="auto" tdd="true">
<name>Wave 3 Task 1: Land _locales/{en,ru}/messages.json + migrate manifest.json to __MSG_*__ + default_locale</name>
<files>_locales/en/messages.json, _locales/ru/messages.json, manifest.json</files>
<behavior>
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.
</behavior>
<action>
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": "<text>", "description": "<context for translators>" }` 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.
</action>
<verify>
<automated>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</automated>
</verify>
<done>
- _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 100 vitest baseline (post-01-14) + 6 new tests = 106 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)`
</done>
</task>
<task type="auto" tdd="true">
<name>Wave 4 Task 1: Migrate src/popup/ + src/background/ to tokens.css palette + chrome.i18n.getMessage</name>
<files>src/popup/index.html, src/popup/index.ts, src/popup/style.css, src/background/index.ts</files>
<behavior>
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).
</behavior>
<action>
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:
- <title>Mokosh — Session Capture</title> (literal English title — chrome doesn't substitute __MSG__ in HTML body text or <title>; popup's title is a sensible default for non-RU locales). Document the limitation in a comment.
- <span class="button-text"></span> — empty (populated by JS); remove the hardcoded 'Сохранить отчёт об ошибке'
- <p class="info-text" data-mks-key="popupInfoText"></p> — 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 `<interfaces>` 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(<key>) || <fallback_const>` 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 === <key-name> 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 `|| <const>` 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 106+ GREEN (100 existing post-01-14 + 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`).
</action>
<verify>
<automated>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</automated>
</verify>
<done>
- 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 106+ GREEN (existing 100 post-01-14 + 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)`
</done>
</task>
<task type="auto" tdd="true">
<name>Wave 5 Task 1: Welcome page conditional migration + Vite __VITE_DEV__ define-token + scripts/README.md</name>
<files>src/welcome/welcome.css, src/welcome/welcome-tokens.css, src/welcome/welcome.ts, src/welcome/copy.ts, vite.config.ts, scripts/README.md</files>
<behavior>
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.
</behavior>
<action>
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 (100 post-01-14) + 6 new tests = 106 GREEN.
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.
</action>
<verify>
<automated>(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</automated>
</verify>
<done>
- 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 106+ 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`
</done>
</task>
<task type="auto" tdd="true">
<name>Wave 6 Task 1: Extend UAT harness with A18-A22 (font / icon / manifest-i18n / Lora-resolved / welcome-tokens)</name>
<files>tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts</files>
<behavior>
The 01-13 + 01-14 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 21/21 GREEN (or 24/24 with 01-10's A15-A17 already in place). A0-A14 (Plan 01-13) and A23 (Plan 01-14) remain GREEN as no-regression guarantees.
Tier-1 FORBIDDEN_HOOK_STRINGS list stays at 12 entries post-Plan-01-14 (no new test-mode surface introduced by Plan 01-12).
</behavior>
<action>
1. Extend tests/uat/extension-page-harness.ts: add 5 page-side methods to window.__mokoshHarness per the `<interfaces>` 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 <link rel='stylesheet' href> 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, with A23 from 01-14 elsewhere in the list). Keep FORBIDDEN_HOOK_STRINGS UNCHANGED at 12 entries (the 01-14-established baseline) — 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: 21/21 (or 24/24) 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/).
</action>
<verify>
<automated>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 15 | grep -v "^#" | 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 21/21 or 24/24 GREEN")</automated>
</verify>
<done>
- 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 21/21 (or 24/24) GREEN
- Tier-1 forbidden-strings inventory in no-test-hooks-in-prod-bundle.test.ts UNCHANGED at 12 entries post-Plan-01-14 (no new test-mode symbols added by Plan 01-12)
- Atomic commit `feat(01-12): wave-6 task-1 — harness A18-A22 (font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens)`
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Wave 7 Checkpoint: Operator empirical brand-fit ack</name>
<files>(operator-driven; no source files modified by this checkpoint — Wave 1-6 already landed the artifacts)</files>
<action>See <how-to-verify> 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.</action>
<verify>
<automated>echo "checkpoint:human-verify — see how-to-verify section; resume signal is the gate"</automated>
</verify>
<done>Operator types 'approved' after running the how-to-verify steps. See <resume-signal>.</done>
<what-built>
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 21/21 (or 24/24) GREEN with A18-A22 covering font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens (A0-A14 from 01-13 and A23 from 01-14 remain GREEN as no-regression guarantees).
</what-built>
<how-to-verify>
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://<id>/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://<id>/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 21/21 GREEN (or 24/24 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.
</how-to-verify>
<resume-signal>Type "approved" or describe issues for follow-up</resume-signal>
</task>
<task type="auto">
<name>Wave 7 Task 1: Plan closure (SUMMARY + STATE.md + ROADMAP.md sync)</name>
<files>.planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md, .planning/STATE.md, .planning/ROADMAP.md</files>
<action>
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 21/21 (or 24/24) 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 (|| <const>); .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)`.
</action>
<verify>
<automated>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</automated>
</verify>
<done>
- .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)
</done>
</task>
</tasks>
<threat_model>
## 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 `|| <const>` 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-* + T-1-14-* 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 12 entries post-Plan-01-14; 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. |
</threat_model>
<verification>
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:** 100 (post-01-14) → 106+ GREEN (+6 new unit tests; +N if welcome migration adds keys).
4. **UAT harness 21/21 (or 24/24 with 01-10) GREEN:** `npm run test:uat` exits 0; A0 Tier-1 grep gate GREEN; A1-A14 (or A1-A17 with 01-10) + A23 (Plan 01-14) 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 12 entries post-Plan-01-14.**
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".
</verification>
<success_criteria>
- [ ] 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 `|| <const>` 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 21/21 (or 24/24 with 01-10) GREEN
- [ ] Tier-1 forbidden-strings inventory UNCHANGED at 12 entries post-Plan-01-14
- [ ] 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 — <one-liner>` convention
</success_criteria>
<output>
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).
</output>