Final designer reply received 2026-05-19 unblocks Plan 01-12: R2
substitution — replace Newsreader with Lora (OFL, Cyreal foundry, full
Cyrillic-Latin parity, variable wght 400-700). All 9 brand decisions
now resolved; R2 displaces Newsreader from `--mks-font-display`.
Plan structure: 7 waves, 10 tasks.
- Wave 0 (TDD scaffolds): 6 RED unit tests — tokens-adopted,
fonts-present, icons-present, no-remote-fonts, manifest-i18n,
locale-parity. Each RED until its corresponding artifact wave lands.
- Wave 1: Self-host OFL font bundle (Lora variable normal + italic,
Plex Sans ×4, Plex Mono ×2) at src/shared/fonts/ via pyftsubset
(Latin + Cyrillic basic subset); land src/shared/tokens.css canonical
(Google Fonts @import → 7 local @font-face rules; Newsreader → Lora
per R2; .mks-word class added per RESEARCH §8 + lockup SVG line 21).
- Wave 2: Rasterize Loom mark to icons/icon{16,48,128}.png via
rsvg-convert; overwrite Bug A placeholders; 8-bit RGBA at all sizes.
- Wave 3: Land _locales/{en,ru}/messages.json (12 keys: 8 Brief §02
operator strings + 4 supporting keys); manifest.json → __MSG_extName__
+ __MSG_extDesc__ + default_locale 'en' + action.default_title.
extName='Mokosh — Session Capture' per D-07 user override; extDesc per
D-08 brand-decisions-v1.md wording.
- Wave 4: src/popup/ + src/background/ adopt tokens.css (loom palette)
+ chrome.i18n.getMessage at every operator-facing copy site; replace
hex literals with var(--mks-*) references; BADGE_REC_COLOR madder
'#b2543d' (= --mks-madder-600 per D-04 + RESEARCH §10 Open Q A7).
- Wave 5: Welcome page conditional migration (if 01-10 landed, swap
welcome-tokens.css → @import canonical tokens.css; migrate copy.ts
shim to chrome.i18n.getMessage fallback); add __VITE_DEV__ define
per RESEARCH §12 D-09 spirit; scripts/README.md smoke-isolation note.
- Wave 6: UAT harness A18-A22 (font reachability via document.styleSheets
walk + fetch + byteLength; icon-not-placeholder via fingerprint diff;
manifest:name === 'Mokosh — Session Capture'; --mks-font-display
resolves to Lora via getComputedStyle; welcome tokens loaded
conditional on 01-10). Tier-1 forbidden-strings UNCHANGED at 10.
- Wave 7: Operator empirical brand-fit checkpoint (last Phase 1 gate);
SUMMARY + STATE.md + ROADMAP.md sync.
ROADMAP.md Phase 1 plan list extended from 7 → 13 entries (gap noted in
01-13 SUMMARY's known-limitations now closed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1247 lines
108 KiB
Markdown
1247 lines
108 KiB
Markdown
---
|
||
phase: 01-stabilize-video-pipeline
|
||
plan: 12
|
||
type: tdd
|
||
wave: 6
|
||
depends_on:
|
||
- 01-09
|
||
- 01-13
|
||
files_modified:
|
||
- src/shared/tokens.css
|
||
- src/shared/fonts/Lora-VariableFont.woff2
|
||
- src/shared/fonts/Lora-Italic-VariableFont.woff2
|
||
- src/shared/fonts/IBMPlexSans-Regular.woff2
|
||
- src/shared/fonts/IBMPlexSans-Medium.woff2
|
||
- src/shared/fonts/IBMPlexSans-SemiBold.woff2
|
||
- src/shared/fonts/IBMPlexSans-Bold.woff2
|
||
- src/shared/fonts/IBMPlexMono-Regular.woff2
|
||
- src/shared/fonts/IBMPlexMono-Medium.woff2
|
||
- src/shared/fonts/LICENSE-Lora.txt
|
||
- src/shared/fonts/LICENSE-IBM-Plex.txt
|
||
- src/shared/fonts/README.md
|
||
- src/shared/brand/mokosh-mark.svg
|
||
- src/shared/brand/mokosh-lockup.svg
|
||
- icons/icon16.png
|
||
- icons/icon48.png
|
||
- icons/icon128.png
|
||
- scripts/rasterize-icons.sh
|
||
- scripts/subset-fonts.sh
|
||
- scripts/README.md
|
||
- manifest.json
|
||
- _locales/en/messages.json
|
||
- _locales/ru/messages.json
|
||
- src/popup/index.html
|
||
- src/popup/index.ts
|
||
- src/popup/style.css
|
||
- src/welcome/welcome.css
|
||
- src/welcome/welcome-tokens.css
|
||
- src/welcome/welcome.ts
|
||
- src/welcome/copy.ts
|
||
- src/background/index.ts
|
||
- vite.config.ts
|
||
- vite.test.config.ts
|
||
- tests/build/tokens-adopted.test.ts
|
||
- tests/build/fonts-present.test.ts
|
||
- tests/build/icons-present.test.ts
|
||
- tests/build/no-remote-fonts.test.ts
|
||
- tests/i18n/manifest-i18n.test.ts
|
||
- tests/i18n/locale-parity.test.ts
|
||
- tests/uat/extension-page-harness.ts
|
||
- tests/uat/lib/harness-page-driver.ts
|
||
- tests/uat/harness.test.ts
|
||
- .planning/STATE.md
|
||
- .planning/ROADMAP.md
|
||
autonomous: false
|
||
requirements:
|
||
- REQ-video-ring-buffer
|
||
- REQ-install-clean
|
||
- REQ-manifest-permissions
|
||
tags:
|
||
- design-integration
|
||
- brand
|
||
- tokens-css
|
||
- woff2-selfhost
|
||
- svg-rasterization
|
||
- mv3-csp
|
||
- i18n
|
||
- locales
|
||
- chrome-i18n
|
||
- D-01-mark
|
||
- D-02-welcome
|
||
- D-03-voice-sober
|
||
- D-04-palette
|
||
- D-05-typography
|
||
- D-06-icon-strategy
|
||
- D-07-extname-override
|
||
- D-08-tagline
|
||
- D-09-smoke-dev-only
|
||
- R2-lora-substitute
|
||
- Approach-B-harness-extension
|
||
- harness-A18-A22
|
||
|
||
must_haves:
|
||
truths:
|
||
- "Self-hosted WOFF2 font bundle lands at src/shared/fonts/: Lora variable (Latin+Cyrillic via OFL Cyreal foundry release, replaces Newsreader per R2 designer reply 2026-05-19); IBM Plex Sans 400/500/600/700 latin+cyrillic; IBM Plex Mono 400/500 latin+cyrillic. Total bundle ~250-300KB. LICENSE-Lora.txt + LICENSE-IBM-Plex.txt + README.md ship alongside per OFL-1.1 attribution best practice."
|
||
- "src/shared/tokens.css is the canonical token system: copy of .planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css with two surgical edits — (a) line 12 Google Fonts @import REPLACED with 7 local @font-face rules (Lora variable normal + italic; Plex Sans 4 weights; Plex Mono 2 weights), and (b) --mks-font-display value substituted from 'Newsreader' to 'Lora' per R2. Adds .mks-word class definition (font-family: var(--mks-font-display); font-size: 32px; font-weight: var(--mks-weight-regular); fill: var(--mks-ink-900);) required by mokosh-lockup.svg line 21."
|
||
- "src/shared/tokens.css contains ZERO references to fonts.googleapis.com or any https:// URL — MV3 CSP `style-src 'self'` + `font-src 'self'` enforced. Verified by tests/build/no-remote-fonts.test.ts grepping the built dist/ for the substring `googleapis` (occurrences === 0) and `https://fonts` (occurrences === 0)."
|
||
- "icons/icon{16,48,128}.png are rasterized from src/shared/brand/mokosh-mark.svg via rsvg-convert (per RESEARCH §3 verified locally: 16=406B, 48=784B, 128=1952B). Each clears the assets-spec.md FLOOR: 16≥200B, 48≥500B, 128≥1024B. PNGs are committed as static artifacts (NOT regenerated at build time per RESEARCH §3 anti-pattern); scripts/rasterize-icons.sh is the documented re-run recipe. The Bug A placeholder icons (dark-square + green-dot per Plan 01-09 Path A; verified pre-existing as 574B/1153B/2615B 16-bit RGB at icons/icon{16,48,128}.png) are OVERWRITTEN."
|
||
- "manifest.json:name = '__MSG_extName__' (was 'AI Call Recorder'); manifest.json:description = '__MSG_extDesc__' (was 'Запись сессий операторов для диагностики ошибок'); manifest.json:default_locale = 'en'; manifest.json:action.default_title = '__MSG_tooltipOff__'. _locales/en/messages.json carries extName='Mokosh — Session Capture' per D-07 user override and extDesc='Thirty seconds ago, always at hand.' per D-08 (using brand-decisions-v1.md final wording, not Brief §02's '...within reach.' variant — A4 from RESEARCH Open Questions resolved to brand-decisions-v1.md as the more recent doc)."
|
||
- "8 i18n keys land in BOTH _locales/en/messages.json AND _locales/ru/messages.json (default_locale parity per RESEARCH Pitfall 4): extName, extDesc, tooltipOff, tooltipRecPrefix, tooltipErr, popupSavePrompt, popupSaveCta, popupSaveDone, notifStartup, notifRecovery, welcomeHeroRu, welcomeHeroEn (12 total keys; the 8 Brief §02 strings + 4 supporting keys including manifest fields and tooltipRec split). Every key in ru/messages.json is also present in en/messages.json (verified by tests/i18n/locale-parity.test.ts)."
|
||
- "src/background/index.ts is migrated from hardcoded brand strings (line 82 BADGE_REC_TITLE 'Recording — last 30 s buffered. Click to save.'; line 83 BADGE_OFF_TITLE 'Not recording. Click to start.'; line 84 BADGE_ERROR_TITLE 'Recording error. Click to try again.'; line 862 'Mokosh stopped'; line 863 'Recording stopped. Click here to start a new session.'; line 964 'Mokosh ready'; line 965 startup-message) to chrome.i18n.getMessage('<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 10 strings (no new test-mode symbols introduced by Plan 01-12; the i18n + tokens + font work uses production chrome.* APIs end-to-end). Verified by file-diff after plan close."
|
||
- "UAT harness extended with 5 new assertions A18-A22: A18 = Lora WOFF2 loads from chrome.runtime.getURL('shared/fonts/Lora-VariableFont.woff2') OR equivalent emitted asset path (response.ok && content-length > 50000 — Lora variable Latin+Cyrillic is ~80-120KB after subsetting). A19 = icons are NOT the Bug A placeholders (icon128.png file size differs from the known 2615-byte 16-bit-RGB placeholder; OR icon128.png file content hash differs from the placeholder content hash). A20 = chrome.runtime.getManifest().name === 'Mokosh — Session Capture' (i18n resolution test; the en-locale fallback always returns this exact string per default_locale='en'). A21 = getComputedStyle on a test element with CSS class .mks-display-1 inside the harness page resolves to a font-family stack starting with 'Lora' (not 'Newsreader', not system fallback). A22 = welcome page tokens.css is loaded if 01-10 has landed (fetch chrome.runtime.getURL('src/welcome/welcome.html') returns 200 + welcome.css resolves --mks-rec to '#b2543d'); skip with a documented diagnostic if 01-10 has not landed."
|
||
- "Every Plan 01-13 harness assertion (A0-A14) and Plan 01-10 assertion (A15-A17, IF landed) remains GREEN after Plan 01-12 changes (no regression). vitest baseline grows by 6 tests (icons-present + tokens-adopted + fonts-present + no-remote-fonts + manifest-i18n + locale-parity) from 98 GREEN to 104 GREEN; npm run test:uat grows from 15/15 (or 18/18 with 01-10) to 20/20 (or 23/23 with 01-10 + 01-12) — all GREEN."
|
||
- "MV3 architectural constraints from 01-11-SUMMARY enforced: NO `await import(...)` anywhere in src/background/index.ts (chrome.i18n.getMessage is a synchronous API; no dynamic imports required); test-mode symbols stay in dist-test/ only via __MOKOSH_UAT__ define-token (Plan 01-12 introduces no new test-mode symbols)."
|
||
artifacts:
|
||
- path: "src/shared/tokens.css"
|
||
provides: "Canonical token system (Mokosh design tokens; single source of truth). Copy of design-incoming tokens.css with: (1) line 12 Google Fonts @import REMOVED + replaced with 7 local @font-face rules pointing at ./fonts/*.woff2 (Lora variable normal + italic; Plex Sans 400/500/600/700; Plex Mono 400/500); (2) --mks-font-display value substituted from 'Newsreader' → 'Lora' per R2 designer reply 2026-05-19; (3) .mks-word class definition added (font-family: var(--mks-font-display); font-size: 32px; font-weight: var(--mks-weight-regular); fill: var(--mks-ink-900); letter-spacing: var(--mks-tracking-display);) at end-of-file per RESEARCH §8 + lockup SVG line 21 requirement. ZERO https:// URLs anywhere in this file."
|
||
contains: "@font-face"
|
||
- path: "src/shared/fonts/Lora-VariableFont.woff2"
|
||
provides: "Lora variable font, normal style, weight axis 400-700, subset to Latin (U+0020-007E, U+00A0-00FF) + Cyrillic basic (U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116) via pyftsubset --flavor=woff2 from upstream OFL .ttf at github.com/cyrealtype/Lora-Cyrillic or Google Fonts metadata source. Expected size 80-120KB."
|
||
- path: "src/shared/fonts/Lora-Italic-VariableFont.woff2"
|
||
provides: "Lora variable italic, same subset as above. Expected size 80-120KB. NB: if upstream Lora ships italic in same file as normal via opsz/wght/ital axes (variable font with multi-axis support), this file may be omitted — A5 from RESEARCH Assumptions Log: verify upstream variable build structure at execute-plan time and adjust if italic+normal collapse into one file."
|
||
- path: "src/shared/fonts/IBMPlexSans-Regular.woff2"
|
||
provides: "IBM Plex Sans 400, Latin + Cyrillic basic. ~25KB per RESEARCH §1 sizing table."
|
||
- path: "src/shared/fonts/IBMPlexSans-Medium.woff2"
|
||
provides: "IBM Plex Sans 500, Latin + Cyrillic basic. ~25KB."
|
||
- path: "src/shared/fonts/IBMPlexSans-SemiBold.woff2"
|
||
provides: "IBM Plex Sans 600, Latin + Cyrillic basic. ~25KB."
|
||
- path: "src/shared/fonts/IBMPlexSans-Bold.woff2"
|
||
provides: "IBM Plex Sans 700, Latin + Cyrillic basic. ~25KB."
|
||
- path: "src/shared/fonts/IBMPlexMono-Regular.woff2"
|
||
provides: "IBM Plex Mono 400, Latin + Cyrillic basic. ~28KB."
|
||
- path: "src/shared/fonts/IBMPlexMono-Medium.woff2"
|
||
provides: "IBM Plex Mono 500, Latin + Cyrillic basic. ~28KB."
|
||
- path: "src/shared/fonts/LICENSE-Lora.txt"
|
||
provides: "Verbatim copy of Lora OFL-1.1 license + Cyreal foundry copyright lines, per OFL-FAQ best practice."
|
||
- path: "src/shared/fonts/LICENSE-IBM-Plex.txt"
|
||
provides: "Verbatim copy of IBM Plex OFL-1.1 license from github.com/IBM/plex/blob/master/LICENSE.txt."
|
||
- path: "src/shared/fonts/README.md"
|
||
provides: "One-pager attribution + regeneration recipe per RESEARCH §7 template. Lists all bundled faces + upstream URLs + subsetting recipe + license notes."
|
||
min_lines: 15
|
||
- path: "src/shared/brand/mokosh-mark.svg"
|
||
provides: "Copy of .planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/mokosh-mark.svg (Loom 2×2 weave intersection; D-01 brand mark). Source-of-truth for icon rasterization. Stays at hardcoded stroke='#181b2a' for PNG output; future Plan 01-10 may copy + adapt to currentColor for inline-SVG welcome usage (see RESEARCH §5)."
|
||
- path: "src/shared/brand/mokosh-lockup.svg"
|
||
provides: "Copy of mokosh-lockup.svg (mark + 'Mokosh' wordmark at 240×56 viewBox). The .mks-word class referenced at line 21 is defined in src/shared/tokens.css per artifact above."
|
||
- path: "icons/icon16.png"
|
||
provides: "Rasterized Loom mark at 16×16 via `rsvg-convert -w 16 -h 16 src/shared/brand/mokosh-mark.svg -o icons/icon16.png`. ≥200 bytes (~406B per RESEARCH §3 verified). REPLACES the prior 574B 16-bit-RGB placeholder."
|
||
- path: "icons/icon48.png"
|
||
provides: "Rasterized Loom mark at 48×48. ≥500 bytes (~784B). REPLACES the prior 1153B 16-bit-RGB placeholder."
|
||
- path: "icons/icon128.png"
|
||
provides: "Rasterized Loom mark at 128×128. ≥1024 bytes (~1952B). REPLACES the prior 2615B 16-bit-RGB placeholder."
|
||
- path: "scripts/rasterize-icons.sh"
|
||
provides: "One-off rasterization recipe per RESEARCH §3. Bash script reading src/shared/brand/mokosh-mark.svg and emitting icons/icon{16,48,128}.png via rsvg-convert. Documented for re-run when the mark SVG changes; NOT wired into prebuild (anti-pattern per RESEARCH §3)."
|
||
min_lines: 15
|
||
- path: "scripts/subset-fonts.sh"
|
||
provides: "One-off font subsetting recipe per RESEARCH §1. Bash script for Lora (Latin+Cyrillic subset, variable normal + italic), Plex Sans 400/500/600/700 (Latin+Cyrillic), Plex Mono 400/500 (Latin+Cyrillic), via pyftsubset --flavor=woff2 from upstream .ttf sources downloaded one-off into a scratch dir. Documents the canonical --unicodes string."
|
||
min_lines: 30
|
||
- path: "scripts/README.md"
|
||
provides: "One-paragraph note per RESEARCH §12: smoke.sh and other dev-only scripts live in this directory and are NOT bundled by `npm run build`. The production dist/ contains no smoke artifacts (verified by `grep -rn smoke dist/` returning empty)."
|
||
min_lines: 8
|
||
- path: "manifest.json"
|
||
provides: "Updated manifest: `name` = '__MSG_extName__'; `description` = '__MSG_extDesc__'; `default_locale` = 'en' (NEW field); `action.default_title` = '__MSG_tooltipOff__' (NEW field per RESEARCH §11 + Brief §02 string #1). All other fields preserved unchanged (permissions, host_permissions, background, content_scripts, action.default_popup, action.default_icon, icons)."
|
||
contains: "default_locale"
|
||
- path: "_locales/en/messages.json"
|
||
provides: "English locale messages (default_locale fallback). 12 keys: extName, extDesc, tooltipOff, tooltipRecPrefix, tooltipErr, popupSavePrompt, popupSaveCta, popupSaveDone, popupInfoText, notifStartup, notifRecovery, welcomeHeroRu, welcomeHeroEn. extName='Mokosh — Session Capture' per D-07; extDesc='Thirty seconds ago, always at hand.' per D-08 (brand-decisions-v1.md wording). Strings inherit D-03 Sober register. Each key has both message + description fields per Chrome i18n schema."
|
||
min_lines: 70
|
||
- path: "_locales/ru/messages.json"
|
||
provides: "Russian locale messages (primary operator locale). Same 12 keys as en/messages.json with Russian text per RESEARCH §10 Brief §02 verbatim extraction: extName='Mokosh — Запись сессии'; extDesc='Тридцать секунд назад, всегда под рукой.'; tooltipOff='Mokosh — щёлкните, чтобы начать запись'; tooltipRecPrefix='Mokosh — идёт запись'; tooltipErr='Mokosh — ошибка записи, щёлкните для восстановления'; popupSavePrompt='Сохранить отчёт об ошибке?'; popupSaveCta='Сохранить отчёт'; popupSaveDone='Архив сохранён в Загрузки.'; popupInfoText='Последние 30 сек видео + 10 мин лога'; notifStartup='Запись запущена. Я слежу за последними 30 секундами.'; notifRecovery='Запись возобновлена. Буфер снова заполняется.'; welcomeHeroRu='Тридцать секунд назад, всегда под рукой.'; welcomeHeroEn='Thirty seconds ago, always at hand.'."
|
||
min_lines: 70
|
||
- path: "src/popup/index.html"
|
||
provides: "Popup HTML migrated to i18n: <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 10 entries (no new test-mode symbols introduced). Assertion count grows from 15/15 (post-01-13) or 18/18 (post-01-10+01-13) to 20/20 or 23/23 (post-01-12). Bail-on-first-failure orchestration unchanged."
|
||
contains: "A18"
|
||
- path: ".planning/STATE.md"
|
||
provides: "Wave 7 closure update: completed_plans increments by 1 (Plan 01-12 closes); decision-log appends '[Phase 01-12]: design integration landed — Lora self-hosted (R2 per designer 2026-05-19) + tokens.css canonical + 8 i18n strings + branded icons + manifest:name i18n; UAT harness 20/20 GREEN (or 23/23 if 01-10 also landed); operator brand-fit ack received'. percent recomputed."
|
||
- path: ".planning/ROADMAP.md"
|
||
provides: "Phase 1 Plans list gains `- [x] 01-12-PLAN.md — Design integration (R2 Lora, tokens.css, 8 i18n strings, branded icons, manifest i18n)` entry after 01-13. If Plan 01-10 not yet in list, also add its entry as `- [ ]` placeholder unless already present."
|
||
key_links:
|
||
- from: "src/shared/tokens.css @font-face src urls"
|
||
to: "src/shared/fonts/*.woff2"
|
||
via: "relative url('./fonts/<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 98+ vitest tests stay GREEN (i18n-call-site migration is behavior-equivalent for the chrome.* mock pattern used by existing tests).
|
||
- Wave 5: src/welcome/* conditional migration (if 01-10 landed) OR skip; vite.config.ts __VITE_DEV__ define-token + scripts/README.md.
|
||
- Wave 6: tests/uat/extension-page-harness.ts + tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts extended with A18-A22; npm run test:uat reports 20/20 (or 23/23 with 01-10) GREEN.
|
||
- Wave 7: operator checkpoint; STATE.md + ROADMAP.md sync; SUMMARY produced.
|
||
</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 10)
|
||
|
||
```ts
|
||
const FORBIDDEN_HOOK_STRINGS: ReadonlyArray<string> = [
|
||
'__mokoshTest',
|
||
'setCurrentStream',
|
||
'setSegmentCountGetter',
|
||
'installFakeDisplayMedia',
|
||
'uninstallFakeDisplayMedia',
|
||
'dispatchEndedOnTrack',
|
||
'getSegmentCount',
|
||
'__mokoshOffscreenQuery',
|
||
'get-display-surface',
|
||
'get-segment-count',
|
||
];
|
||
// Plan 01-12 introduces NO new test-mode symbols (A18-A22 use production chrome.* APIs
|
||
// + fetch + getComputedStyle exclusively). Do NOT extend this list.
|
||
```
|
||
|
||
### Locale-parity test shape
|
||
|
||
```ts
|
||
import { describe, it, expect } from 'vitest';
|
||
import { readFileSync } from 'node:fs';
|
||
|
||
describe('_locales/ key parity', () => {
|
||
const ru = JSON.parse(readFileSync('_locales/ru/messages.json', 'utf8'));
|
||
const en = JSON.parse(readFileSync('_locales/en/messages.json', 'utf8'));
|
||
it('every key in ru/messages.json exists in en/messages.json', () => {
|
||
for (const k of Object.keys(ru)) expect(en).toHaveProperty(k);
|
||
});
|
||
it('every key in en/messages.json exists in ru/messages.json (symmetric)', () => {
|
||
for (const k of Object.keys(en)) expect(ru).toHaveProperty(k);
|
||
});
|
||
it('every key has a non-empty .message string', () => {
|
||
for (const k of Object.keys(en)) expect(typeof en[k].message).toBe('string');
|
||
for (const k of Object.keys(en)) expect(en[k].message.length).toBeGreaterThan(0);
|
||
for (const k of Object.keys(ru)) expect(typeof ru[k].message).toBe('string');
|
||
for (const k of Object.keys(ru)) expect(ru[k].message.length).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
```
|
||
</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 98 vitest GREEN baseline stays GREEN; only the 6 new tests are RED)
|
||
- Atomic commit `test(01-12): wave-0 — scaffold RED unit tests for tokens / fonts / icons / no-remote-fonts / manifest-i18n / locale-parity`
|
||
</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 98 vitest baseline + 6 new tests = 104 vitest GREEN
|
||
- Atomic commit `feat(01-12): wave-3 task-1 — manifest i18n (__MSG_*__ + default_locale='en') + _locales/{en,ru}/messages.json (12 keys; D-07 + D-08 baked in)`
|
||
</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 104+ GREEN (98 existing + 6 new tests + any new copy in popup tests adjusted via chrome.i18n stub).
|
||
|
||
7. Run `npm run build` then re-run `tests/build/no-remote-fonts.test.ts` — expect GREEN (dist/tokens.css contains zero `googleapis` / `https://fonts`).
|
||
</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 104+ GREEN (existing 98 + 6 new); no regressions in tests/background/*
|
||
- Atomic commit `feat(01-12): wave-4 task-1 — adopt tokens.css + chrome.i18n.getMessage in src/popup/ + src/background/ (loom palette + RU i18n + en fallback)`
|
||
</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 + 6 new tests.
|
||
|
||
7. (Optional but recommended per RESEARCH §12 + the existing Tier-1 gate pattern): add a `tests/build/no-smoke-in-dist.test.ts` test asserting `grep -rn smoke dist/` returns 0 matches post-build. RED today if any smoke string leaks; GREEN per current architecture. Keep this OPTIONAL — if context budget tight, defer to a follow-up plan.
|
||
</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 104+ GREEN (+ N welcome keys added to messages.json if 01-10 landed; locale-parity stays GREEN)
|
||
- Atomic commit `feat(01-12): wave-5 task-1 — welcome i18n migration (conditional on 01-10) + __VITE_DEV__ define + scripts/README.md`
|
||
</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 harness gains 5 new assertions following the established Approach-B pattern (page-side assertA* + host-side driveA* wrapper + harness.test.ts orchestrator entry). Each assertion uses production chrome.* APIs + fetch + getComputedStyle (no new test-mode symbols). After this wave: `npm run test:uat` reports 20/20 GREEN (or 23/23 with 01-10's A15-A17 already in place).
|
||
|
||
Tier-1 FORBIDDEN_HOOK_STRINGS list stays at 10 entries (no new test-mode surface introduced).
|
||
</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). Keep FORBIDDEN_HOOK_STRINGS UNCHANGED at 10 entries — verify by `wc -l` diff before/after this task.
|
||
|
||
4. Run npm run build:test (rebuilds dist-test/). Then run `npm run test:uat` (or just `tsx tests/uat/harness.test.ts`). Expected output: 20/20 (or 23/23) GREEN.
|
||
|
||
5. Run `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts` — confirm Tier-1 grep gate GREEN (no new hook strings leaked into dist/).
|
||
</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 13 | grep -c "'") && npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts 2>&1 | tail -5 && (npm run test:uat 2>&1 | tail -25 || echo "test:uat invocation needs verification; check for 20/20 or 23/23 GREEN")</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 20/20 (or 23/23) GREEN
|
||
- Tier-1 forbidden-strings inventory in no-test-hooks-in-prod-bundle.test.ts UNCHANGED at 10 entries (no new test-mode symbols)
|
||
- Atomic commit `feat(01-12): wave-6 task-1 — harness A18-A22 (font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens)`
|
||
</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 20/20 (or 23/23) GREEN with A18-A22 covering font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens.
|
||
</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 20/20 GREEN (or 23/23 with 01-10).
|
||
- Tier-1 grep gate GREEN: `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts`.
|
||
|
||
8. Type "approved" if brand fit accepted, OR describe specific issues for follow-up.
|
||
</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 20/20 (or 23/23) GREEN with A18-A22; operator brand-fit ack received."
|
||
- Wave-by-wave breakdown with commit refs.
|
||
- Test Counts table: vitest before/after; harness before/after.
|
||
- Architectural Notes: R2 substitution rationale; OFL attribution pattern; tokens.css as single source of truth; chrome.i18n.getMessage fallback pattern (|| <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-* inheritance) | accept | Plan 01-12 introduces ZERO new test-mode symbols (A18-A22 use production chrome.* + fetch + getComputedStyle exclusively). Tier-1 grep gate at tests/background/no-test-hooks-in-prod-bundle.test.ts remains at 10 entries; no extension needed. Verified by file-diff at task end. |
|
||
| T-01-12-09 | Tampering | OFL license/attribution non-compliance | mitigate | src/shared/fonts/LICENSE-Lora.txt + LICENSE-IBM-Plex.txt ship verbatim per OFL-1.1 attribution best practice (RESEARCH §7); src/shared/fonts/README.md cross-references upstream URLs + R2 substitution rationale. |
|
||
</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:** 98 → 104+ GREEN (+6 new unit tests; +N if welcome migration adds keys).
|
||
4. **UAT harness 20/20 (or 23/23 with 01-10) GREEN:** `npm run test:uat` exits 0; A0 Tier-1 grep gate GREEN; A1-A14 (or A1-A17 with 01-10) GREEN; A18-A22 GREEN.
|
||
5. **Font bundle present:** all 7 or 8 WOFF2 files exist under src/shared/fonts/; total bundle ≤ 350KB; LICENSE files + README.md present.
|
||
6. **Icons rasterized:** icons/icon{16,48,128}.png are 8-bit RGBA PNGs at correct dims + clearing FLOOR; differ in content from the Bug A placeholders.
|
||
7. **Manifest i18n wired:** chrome://extensions name reads "Mokosh — Session Capture" (en) or "Mokosh — Запись сессии" (ru); operator-facing tooltips + notification copy + popup CTA all resolve via chrome.i18n.getMessage.
|
||
8. **Tokens adopted:** src/popup/style.css has 0 hex literals + imports tokens.css; src/welcome/welcome.css (if 01-10 landed) imports tokens.css; BADGE_REC_COLOR is madder-rust #b2543d.
|
||
9. **Tier-1 forbidden-strings inventory UNCHANGED at 10 entries.**
|
||
10. **R2 substitution complete:** zero `Newsreader` references anywhere in src/shared/tokens.css; --mks-font-display === '"Lora", "Iowan Old Style", "Times New Roman", serif'.
|
||
11. **Operator brand-fit ack:** Wave 7 checkpoint received "approved".
|
||
</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 20/20 (or 23/23 with 01-10) GREEN
|
||
- [ ] Tier-1 forbidden-strings inventory UNCHANGED at 10 entries
|
||
- [ ] Operator empirical brand-fit ack ("approved") received at Wave 7 checkpoint
|
||
- [ ] 01-12-SUMMARY.md exists + STATE.md + ROADMAP.md synced
|
||
- [ ] All commits atomic + follow `feat(01-12)/test(01-12)/docs(01-12): wave-N task-M — <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>
|