Surgical amendment — Plan 01-12 is unexecuted; safe for in-place revision. Plan 01-14 landed (b467123→5254145→9792c0f) since 01-12 was authored, adding monitorTypeSurfaces:'include' + UAT A23 + 2 FORBIDDEN_HOOK_STRINGS entries. Plan 01-12 baseline arithmetic updated to reflect post-01-14 floors and depends_on chain extended. Changes: - frontmatter depends_on: append 01-14 (canonical sequential-dependency declaration) - must_haves truth #11: 10 strings → 12 strings post-Plan-01-14, with provenance note - must_haves truth #13: vitest 98→100→106; UAT 15→16→21 (or 19→24 with 01-10); A23 added to no-regression list alongside A0-A14 + (A15-A17 if 01-10) - Embedded FORBIDDEN_HOOK_STRINGS code block: 10 → 12 entries (lastGetDisplayMediaConstraints, get-last-getDisplayMedia-constraints) with attribution comment - Wave 6 task narrative + verify + done: 20/20 → 21/21; 23/23 → 24/24 - Wave 7 closure narrative + one-liner + checkpoint copy: same arithmetic update - Threat model T-01-12-08: inheritance updated to include T-1-14-*; grep-gate floor 10→12 - Verify-block harness orchestrator grep gate: strengthened with `grep -v '^#'` filter to prevent self-invalidating count from comment lines (per planner.md gate hygiene) - Success criteria items 3, 4, 9: vitest 98→100; UAT 20→21; inventory 10→12 Preserved verbatim (per surgeon-not-architect): - 7-wave structure (Wave 0 fonts/tokens/icons/i18n RED scaffolds, Wave 1 fonts+tokens.css, Wave 2 icons, Wave 3 manifest i18n, Wave 4 popup/welcome adoption, Wave 5 8 i18n strings, Wave 6 harness A18-A22, Wave 7 operator brand-fit checkpoint) - 10 tasks (validated via gsd-sdk verify.plan-structure) - R2 Lora baked-in decision - D-01..D-09 decision references - All files_modified entries - Threat model entries other than T-01-12-08 inheritance line - Designer ack references - Operator checkpoint (Wave 7) - Must_have truths #1-10 and #12 Validation: - gsd-sdk frontmatter.validate → valid:true - gsd-sdk verify.plan-structure → valid:true, errors:0, task_count:10 - grep verification: zero stale references to 98/104/15/15/20/20/23/23/18/18/"at 10" - All new baselines present at 32 update sites - diff: 1 file changed, 34 insertions(+), 30 deletions(-) Next step: plan-checker re-validation of arithmetic + consistency before executor spawn. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, must_haves, tags_repeat, user_setup
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | tags | must_haves | tags_repeat | user_setup | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-stabilize-video-pipeline | 12 | tdd | 6 |
|
|
false |
|
|
|
|
|
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:
- 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.
- 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.
- Cross-wave atomicity: every wave produces a coherent commit-set with vitest GREEN at wave-close (Tier-1 gate, locale-parity, no-remote-fonts, etc. all run after every wave).
If a future revision DOES force a split, natural cut line: Plan 01-12A = Waves 0-3 (test scaffolds + fonts + tokens + icons + manifest + locales); Plan 01-12B = Waves 4-7 (source-code adoption + welcome + harness + closure). Wave numbering reflects this potential split cleanly.
Land the full Mokosh design integration end-to-end across 7 waves. Three coordinated changes from the current Plan 01-13 baseline:-
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/. Landsrc/shared/tokens.cssas the canonical token system — copy of designer's handoff with two surgical edits: (a) Google Fonts @import → local @font-face rules; (b)--mks-font-displayfrom"Newsreader"to"Lora". Add.mks-wordclass for the lockup wordmark. Verifies viatests/build/no-remote-fonts.test.ts(CSP-safe production bundle). -
Brand-mark PNG icons + manifest i18n. Rasterize
src/shared/brand/mokosh-mark.svgtoicons/icon{16,48,128}.pngviarsvg-convert, replacing the Plan 01-09 Path A dark-square + green-dot placeholders. Migratemanifest.jsonto__MSG_extName__+__MSG_extDesc__+default_locale: "en"+action.default_title: "__MSG_tooltipOff__". Land_locales/{en,ru}/messages.jsonwith the 8 Brief §02 operator-facing strings + 4 supporting keys (extName per D-07 override, extDesc per D-08 tagline, popupInfoText, popupEmptyState). -
Source-code adoption of tokens + i18n. Restyle
src/popup/style.cssto importsrc/shared/tokens.cssand replace all hex literals withvar(--mks-*)references per D-04 loom palette. Migratesrc/popup/index.ts+src/background/index.tsfrom hardcoded RU/EN strings tochrome.i18n.getMessagereads at every call site (badge titles, notification copy, popup CTA + status). UpdateBADGE_REC_COLORfrom material-green#00C853to madder#b2543d(= --mks-madder-600). Wire the welcome page (Plan 01-10 conditional) to the canonical tokens.css.
Wave 6 extends the 01-13 UAT harness with 5 new assertions A18-A22 covering: WOFF2 reachability + size floor; icons-not-placeholders; manifest:name i18n resolution; --mks-font-display resolves to Lora; welcome tokens loaded (conditional on 01-10 landing). The harness pattern follows 01-13 Wave 3 exactly (page-side assertA18..A22 methods + host-side driveA18..A22 wrappers + orchestrator entries).
Wave 7 closure: operator empirical brand-fit checkpoint (autonomous: false) — fresh build, load unpacked, verify branded surfaces (toolbar icon, popup palette + Lora display heading, manifest name in chrome://extensions, Russian copy with Lora rendering, notification copy). Operator ack closes the plan + Phase 1 charter.
Operator role at closure: post-01-13, the operator's gates are brand/design only. Plan 01-12 satisfies the LAST remaining Phase 1 operator gate (brand-fit ack of branded surfaces).
Output:
- Wave 0: 6 RED unit tests scaffolded (tokens-adopted, fonts-present, icons-present, no-remote-fonts, manifest-i18n, locale-parity); each RED today, GREEN after later waves land their respective artifacts.
- Wave 1: src/shared/fonts/ + src/shared/tokens.css + scripts/subset-fonts.sh; fonts-present + no-remote-fonts flip GREEN.
- Wave 2: icons/icon{16,48,128}.png + scripts/rasterize-icons.sh + src/shared/brand/; icons-present flips GREEN.
- Wave 3: manifest.json + _locales/{en,ru}/messages.json; manifest-i18n + locale-parity flip GREEN; manifest:name resolves to "Mokosh — Session Capture" on extension reload.
- Wave 4: src/popup/style.css + src/popup/index.html + src/popup/index.ts + src/background/index.ts; tokens-adopted flips GREEN; existing 100+ vitest tests stay GREEN (i18n-call-site migration is behavior-equivalent for the chrome.* mock pattern used by existing tests).
- Wave 5: src/welcome/* conditional migration (if 01-10 landed) OR skip; vite.config.ts VITE_DEV define-token + scripts/README.md.
- Wave 6: tests/uat/extension-page-harness.ts + tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts extended with A18-A22; npm run test:uat reports 21/21 (or 24/24 with 01-10) GREEN.
- Wave 7: operator checkpoint; STATE.md + ROADMAP.md sync; SUMMARY produced.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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.tsDesigner 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)
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)
/* ── 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)
/* ── 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)
#!/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)
{
"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):
const cta = chrome.i18n.getMessage('popupSaveCta'); // returns '' on missing key
buttonText.textContent = cta;
Background SW migration shape (src/background/index.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)
// 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)
// tests/uat/extension-page-harness.ts (page-side)
async function assertA18(): Promise<AssertionRecord> {
const checks: string[] = [];
// Resolve the actual emitted asset URL from the loaded stylesheet (handles Vite hashing).
const sheets = Array.from(document.styleSheets);
let woff2Url: string | null = null;
for (const sheet of sheets) {
try {
for (const rule of Array.from(sheet.cssRules)) {
if (rule.constructor.name === 'CSSFontFaceRule') {
const src = (rule as CSSFontFaceRule).style.getPropertyValue('src');
const match = src.match(/url\(["']?([^"')]+Lora[^"')]+\.woff2)["']?\)/);
if (match) { woff2Url = match[1]; break; }
}
}
} catch { /* cross-origin sheets throw; skip */ }
if (woff2Url) break;
}
if (!woff2Url) {
return { passed: false, name: 'A18', checks, diagnostics: 'No Lora @font-face rule found in any stylesheet', error: '' };
}
checks.push(`A18.1: found Lora @font-face src=${woff2Url}`);
const resp = await fetch(new URL(woff2Url, location.href).href);
if (!resp.ok) return { passed: false, name: 'A18', checks, diagnostics: `fetch ${woff2Url} returned ${resp.status}`, error: '' };
checks.push(`A18.2: fetch returned ${resp.status}`);
const buf = await resp.arrayBuffer();
if (buf.byteLength < 50000) return { passed: false, name: 'A18', checks, diagnostics: `Lora woff2 only ${buf.byteLength} B (expected >50000)`, error: '' };
checks.push(`A18.3: byteLength=${buf.byteLength}`);
return { passed: true, name: 'A18', checks, diagnostics: '' };
}
async function assertA20(): Promise<AssertionRecord> {
const manifest = chrome.runtime.getManifest();
if (manifest.name === 'Mokosh — Session Capture') {
return { passed: true, name: 'A20', checks: [`A20.1: manifest.name='${manifest.name}'`], diagnostics: '' };
}
return { passed: false, name: 'A20', checks: [], diagnostics: `Expected 'Mokosh — Session Capture'; got '${manifest.name}'`, error: '' };
}
async function assertA21(): Promise<AssertionRecord> {
// Create transient probe div with .mks-display-1 class
const probe = document.createElement('div');
probe.className = 'mks-display-1';
probe.textContent = 'Probe';
document.body.appendChild(probe);
// Force layout so CSS resolves
void probe.offsetHeight;
const computed = window.getComputedStyle(probe).fontFamily;
document.body.removeChild(probe);
if (computed.startsWith('Lora') || computed.startsWith('"Lora"')) {
return { passed: true, name: 'A21', checks: [`A21.1: fontFamily='${computed}'`], diagnostics: '' };
}
return { passed: false, name: 'A21', checks: [], diagnostics: `--mks-font-display did not resolve to Lora; computed='${computed}'`, error: '' };
}
Tier-1 forbidden-strings inventory (UNCHANGED at 12 post-Plan-01-14)
const FORBIDDEN_HOOK_STRINGS: ReadonlyArray<string> = [
'__mokoshTest',
'setCurrentStream',
'setSegmentCountGetter',
'installFakeDisplayMedia',
'uninstallFakeDisplayMedia',
'dispatchEndedOnTrack',
'getSegmentCount',
'__mokoshOffscreenQuery',
'get-display-surface',
'get-segment-count',
'lastGetDisplayMediaConstraints', // added by Plan 01-14
'get-last-getDisplayMedia-constraints', // added by Plan 01-14
];
// Plan 01-12 introduces NO new test-mode symbols (A18-A22 use production chrome.* APIs
// + fetch + getComputedStyle exclusively). Do NOT extend this list beyond the 12 entries
// established by Plan 01-14.
Locale-parity test shape
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);
});
});
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.
npx vitest run tests/build tests/i18n 2>&1 | tail -20 && echo "Expected: 6 test files, all RED (or RED on at least one case each — fonts-present, tokens-adopted, no-remote-fonts, manifest-i18n, locale-parity all RED today; icons-present RED on color-type-byte case only)"
- 6 test files exist at the documented paths
- vitest discovers all 6 + reports them in `npx vitest run tests/build tests/i18n`
- At least one case per file is RED today (per behavior above)
- npm run build / build:test still pass (these tests do not change source code yet — the existing 100 vitest GREEN baseline (post-01-14) stays GREEN; only the 6 new tests are RED)
- Atomic commit `test(01-12): wave-0 — scaffold RED unit tests for tokens / fonts / icons / no-remote-fonts / manifest-i18n / locale-parity`
Wave 1 Task 1: Subset OFL fonts to src/shared/fonts/
src/shared/fonts/Lora-VariableFont.woff2, src/shared/fonts/Lora-Italic-VariableFont.woff2, src/shared/fonts/IBMPlexSans-Regular.woff2, src/shared/fonts/IBMPlexSans-Medium.woff2, src/shared/fonts/IBMPlexSans-SemiBold.woff2, src/shared/fonts/IBMPlexSans-Bold.woff2, src/shared/fonts/IBMPlexMono-Regular.woff2, src/shared/fonts/IBMPlexMono-Medium.woff2, src/shared/fonts/LICENSE-Lora.txt, src/shared/fonts/LICENSE-IBM-Plex.txt, src/shared/fonts/README.md, scripts/subset-fonts.sh
Per RESEARCH §1 + §6 + the `` block subsetting recipe. Each WOFF2 file:
- Lora-VariableFont.woff2: variable-axis weight 400-700, Latin + Cyrillic basic subset, expected 80-120KB
- Lora-Italic-VariableFont.woff2: if upstream ships italic separately (A5 verify); otherwise document the consolidation in README.md and omit this file (and the corresponding @font-face rule in tokens.css)
- IBMPlexSans-{Regular,Medium,SemiBold,Bold}.woff2: ~25KB each
- IBMPlexMono-{Regular,Medium}.woff2: ~28KB each
Expected total ~250-300KB per the R2 substitution adjustment to RESEARCH §1's ~216KB estimate.
1. Create scripts/subset-fonts.sh per the `` block recipe. Header comment documents the source URLs (github.com/IBM/plex/releases for Plex v1.1.0; for Lora: github.com/cyrealtype/Lora-Cyrillic OR the Google Fonts release page accessed via `curl`). Script downloads upstream TTFs into a scratch tmpdir (`mktemp -d`), runs pyftsubset per face with the UNICODES range from ``, then cleans up the tmpdir.
2. Execute scripts/subset-fonts.sh. Verify each produced file:
- exists at the documented path
- size > 0 (typical: Lora ~100KB; Plex faces ~25-28KB each)
- is WOFF2 (first 4 bytes === 'wOF2' = 0x77 0x4F 0x46 0x32)
Document any execute-time surprises (italic consolidation per A5, etc.) in a brief note in src/shared/fonts/README.md.
3. Create src/shared/fonts/LICENSE-Lora.txt by curling https://raw.githubusercontent.com/cyrealtype/Lora-Cyrillic/master/OFL.txt (or equivalent canonical Lora OFL source — verify upstream layout during execute). Include copyright lines per the actual upstream content.
4. Create src/shared/fonts/LICENSE-IBM-Plex.txt by curling https://raw.githubusercontent.com/IBM/plex/master/LICENSE.txt (verbatim copy).
5. Create src/shared/fonts/README.md per RESEARCH §7 template + the R2 substitution note. Document: what's bundled; upstream URLs; subsetting unicodes; license notes; regeneration recipe (`scripts/subset-fonts.sh`). Mention the R2 substitution (Newsreader → Lora) + the designer 2026-05-19 reply.
6. Verify tests/build/fonts-present.test.ts now passes (all 7 or 8 expected files exist).
NOTE on Lora italic: if upstream variable file includes italic via the `ital` axis (single multi-axis file), update scripts/subset-fonts.sh to subset that single file + adjust tokens.css (next task) to use `font-style: oblique 0deg 14deg` per CSS Fonts Module 4 variable-italic syntax. Document the decision in src/shared/fonts/README.md.
ls -la src/shared/fonts/ && stat -c '%n: %s bytes' src/shared/fonts/*.woff2 && head -3 src/shared/fonts/LICENSE-*.txt && head -10 src/shared/fonts/README.md && (npx vitest run tests/build/fonts-present.test.ts 2>&1 | tail -10) && echo "Wave 1.1 done — fonts present; total bundle:" && du -ch src/shared/fonts/*.woff2 | tail -1
- 7 or 8 WOFF2 files exist (8 if Lora italic separate; 7 if collapsed)
- Each WOFF2 file is > 0 bytes + WOFF2 signature verified
- LICENSE-Lora.txt + LICENSE-IBM-Plex.txt exist + non-empty
- src/shared/fonts/README.md exists + lists all bundled faces + upstream URLs + R2 note
- scripts/subset-fonts.sh exists + executable
- Total bundle size between 200KB and 350KB (relaxed envelope for R2 italic variance)
- tests/build/fonts-present.test.ts GREEN
- Atomic commit `feat(01-12): wave-1 task-1 — self-host OFL font bundle (Lora + Plex Sans + Plex Mono; R2 designer reply 2026-05-19)`
Wave 1 Task 2: Land canonical src/shared/tokens.css (R2 Lora substitution + .mks-word + CSP-safe @font-face)
src/shared/tokens.css, src/shared/brand/mokosh-mark.svg, src/shared/brand/mokosh-lockup.svg
src/shared/tokens.css becomes the canonical token system. Contents:
- Verbatim copy of .planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css (281 lines) with three surgical edits:
1. Line 12 Google Fonts @import REMOVED + replaced with the 7-rule (or 8-rule if Lora italic separate) @font-face block from ``
2. Line 76 `--mks-font-display: "Newsreader", "Iowan Old Style", "Times New Roman", serif;` REPLACED with `--mks-font-display: "Lora", "Iowan Old Style", "Times New Roman", serif;` per R2 designer reply 2026-05-19
3. .mks-word class added at end of file per the `` block definition
- ZERO references to fonts.googleapis.com or any https:// URL
- ZERO Newsreader font-family references (R2 substitution complete)
src/shared/brand/mokosh-mark.svg + mokosh-lockup.svg are verbatim copies of the design-incoming files (so the engineering source tree has its own copy decoupled from the intel/ ingestion location).
1. Copy .planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css to src/shared/tokens.css (use `cp` or Read + Write). Apply the three edits:
- Replace line 12 (the Google Fonts @import) with the local @font-face block from `` — wrap in a comment header noting MV3 CSP + R2 designer reply 2026-05-19
- Replace line 76 `--mks-font-display: "Newsreader"...` with `--mks-font-display: "Lora", "Iowan Old Style", "Times New Roman", serif;` — also update the comment at line 72 to note "Lora for display (Cyreal foundry; R2 substitute for Newsreader per designer reply 2026-05-19; full Cyrillic coverage)"
- Append the .mks-word class block from `` at end of file with a clear header `/* ── Lockup wordmark — referenced by mokosh-lockup.svg line 21 ── */`
2. Create src/shared/brand/ directory; copy mokosh-mark.svg + mokosh-lockup.svg from .planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/ to src/shared/brand/. The intel/ copies stay as the original handoff source-of-truth; src/shared/brand/ is the engineering working copy.
3. Verify src/shared/tokens.css:
- `grep -c 'googleapis\\|https://fonts' src/shared/tokens.css` returns 0
- `grep -c 'Newsreader' src/shared/tokens.css` returns 0
- `grep -c '@font-face' src/shared/tokens.css` returns 7 (or 8 with Lora italic)
- `grep '\\.mks-word' src/shared/tokens.css` finds the new class
4. Verify CSS-parses by spawning `npx vite build --mode test --config vite.test.config.ts` (dry-build) and confirming no warnings about tokens.css; OR by running a quick CSS-syntax check via `npx stylelint src/shared/tokens.css --config '{}'` if available; OR by inspection.
5. Confirm tests/build/no-remote-fonts.test.ts can now run (after a `npm run build` it will read dist/ and verify zero remote-font URLs). Run it with SKIP_BUILD=1 to defer the actual build to a later wave verification.
grep -c "googleapis\|https://fonts" src/shared/tokens.css && grep -c "Newsreader" src/shared/tokens.css && grep -c "@font-face" src/shared/tokens.css && grep -c "\.mks-word" src/shared/tokens.css && grep -c "Lora" src/shared/tokens.css && ls -la src/shared/brand/ && npx vitest run tests/build/tokens-adopted.test.ts 2>&1 | tail -10
- src/shared/tokens.css exists + has 0 occurrences of `googleapis` / `https://fonts` / `Newsreader`
- src/shared/tokens.css has 7 (or 8) `@font-face` rule occurrences
- src/shared/tokens.css contains `.mks-word` class definition
- src/shared/tokens.css contains `Lora` font-family reference (≥1)
- src/shared/brand/mokosh-mark.svg + mokosh-lockup.svg exist + are byte-identical copies of intel/ originals
- tests/build/tokens-adopted.test.ts case (a) GREEN (tokens.css exists); cases (b)+(c) still RED until Wave 4 (popup/style.css doesn't yet import tokens.css)
- Atomic commit `feat(01-12): wave-1 task-2 — canonical src/shared/tokens.css (R2 Lora substitution + .mks-word + local @font-face block)`
Wave 2 Task 1: Rasterize Loom brand mark to icons/icon{16,48,128}.png
icons/icon16.png, icons/icon48.png, icons/icon128.png, scripts/rasterize-icons.sh
Rasterize src/shared/brand/mokosh-mark.svg to icons/icon{16,48,128}.png via rsvg-convert (verified locally at RESEARCH §3: 16=406B, 48=784B, 128=1952B — all clear assets-spec.md FLOOR). The Plan 01-09 Path A placeholder icons (16-bit-RGB dark-square + green-dot) are OVERWRITTEN.
Per D-06 (Neutral mark + dynamic badge): single neutral mark per size; NO per-state PNG sets (the dynamic chrome.action.setBadgeBackgroundColor in src/background/index.ts does state communication).
1. Create scripts/rasterize-icons.sh per the `` block recipe (with the embedded size-floor sanity checks). Make it executable (`chmod +x`).
2. Execute scripts/rasterize-icons.sh. Verify each produced PNG:
- exists at icons/icon{16,48,128}.png
- file(1) reports "PNG image data, NxN, 8-bit/color RGBA, non-interlaced" (RESEARCH §3 verified)
- size meets the FLOOR: 16≥200B, 48≥500B, 128≥1024B
- byte 25 (color-type) === 6 (RGBA) — this is the case Wave 0 Task 1's icons-present test asserts
3. Verify the OVERWRITE happened: the prior placeholders were `16-bit/color RGB` (per `file icons/icon*.png` at plan-write time); the new files MUST be `8-bit/color RGBA`. This is a load-bearing behavioral change confirming the new rasterized mark is in place.
4. Re-verify all Plan 01-13 harness assertions still pass — specifically A9 (icon file sizes meet imageUtil floors per RESEARCH §3 verification) and A8 (Bug A regression: chrome.notifications.create succeeds with the new icon).
Run npx vitest run tests/build/icons-present.test.ts; expect GREEN.
file icons/icon*.png && stat -c '%n: %s bytes' icons/icon*.png && npx vitest run tests/build/icons-present.test.ts 2>&1 | tail -10 && head -c 8 icons/icon16.png | xxd
- icons/icon16.png ≥ 200 B + 16×16 + 8-bit RGBA
- icons/icon48.png ≥ 500 B + 48×48 + 8-bit RGBA
- icons/icon128.png ≥ 1024 B + 128×128 + 8-bit RGBA
- scripts/rasterize-icons.sh exists + executable
- tests/build/icons-present.test.ts GREEN (all cases including the color-type RGBA case)
- Plan 01-13 harness A8 + A9 still GREEN (re-run `npm run test:uat -- --only=A8 --only=A9` if standalone selector supported; otherwise run full suite and confirm 15+/15+ GREEN)
- Atomic commit `feat(01-12): wave-2 task-1 — rasterize Loom mark to icons/{16,48,128}.png (overwrites Bug A placeholders)`
Wave 3 Task 1: Land _locales/{en,ru}/messages.json + migrate manifest.json to __MSG_*__ + default_locale
_locales/en/messages.json, _locales/ru/messages.json, manifest.json
Two messages.json files at _locales/en/ + _locales/ru/ carry the 12-key matrix per RESEARCH §10 + Brief §02 verbatim + D-07 user override + D-08 tagline. manifest.json migrates to i18n with __MSG_extName__ + __MSG_extDesc__ + default_locale='en' + action.default_title='__MSG_tooltipOff__'. On extension reload after this wave, chrome://extensions displays "Mokosh — Session Capture" (D-07) — verifies the end-to-end i18n wiring.
Test gates: tests/i18n/manifest-i18n.test.ts + tests/i18n/locale-parity.test.ts flip GREEN.
1. Create _locales/ + _locales/en/ + _locales/ru/ directories (do not yet exist).
2. Create _locales/en/messages.json with the 12 keys per RESEARCH §10 + D-07 override + D-08 tagline (using brand-decisions-v1.md "always at hand." wording per RESEARCH Open Questions #4 default-action). Each key is a `{ "message": "<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.
cat manifest.json | python3 -c 'import sys,json;m=json.load(sys.stdin);assert m["name"]=="__MSG_extName__",m["name"];assert m["description"]=="__MSG_extDesc__",m["description"];assert m["default_locale"]=="en";assert m["action"]["default_title"]=="__MSG_tooltipOff__";print("manifest OK")' && cat _locales/en/messages.json | python3 -c 'import sys,json;m=json.load(sys.stdin);assert m["extName"]["message"]=="Mokosh — Session Capture";assert m["extDesc"]["message"]=="Thirty seconds ago, always at hand.";print("en/messages.json OK")' && cat _locales/ru/messages.json | python3 -c 'import sys,json;m=json.load(sys.stdin);assert m["extName"]["message"]=="Mokosh — Запись сессии";assert m["extDesc"]["message"]=="Тридцать секунд назад, всегда под рукой.";print("ru/messages.json OK")' && npx vitest run tests/i18n/manifest-i18n.test.ts tests/i18n/locale-parity.test.ts 2>&1 | tail -10
- _locales/en/messages.json + _locales/ru/messages.json exist + parse + each carry the 12 keys with the canonical values
- manifest.json:name === '__MSG_extName__'; :description === '__MSG_extDesc__'; :default_locale === 'en'; :action.default_title === '__MSG_tooltipOff__'
- npm run build succeeds + dist/_locales/{en,ru}/messages.json exist + dist/manifest.json carries the new i18n shape
- tests/i18n/manifest-i18n.test.ts + tests/i18n/locale-parity.test.ts GREEN
- Existing 100 vitest baseline (post-01-14) + 6 new tests = 106 vitest GREEN
- Atomic commit `feat(01-12): wave-3 task-1 — manifest i18n (__MSG_*__ + default_locale='en') + _locales/{en,ru}/messages.json (12 keys; D-07 + D-08 baked in)`
Wave 4 Task 1: Migrate src/popup/ + src/background/ to tokens.css palette + chrome.i18n.getMessage
src/popup/index.html, src/popup/index.ts, src/popup/style.css, src/background/index.ts
src/popup/style.css imports tokens.css + replaces every hex literal with var(--mks-*) reference. src/popup/index.html + src/popup/index.ts populate operator-facing strings via chrome.i18n.getMessage. src/background/index.ts migrates badge titles + notification copy to chrome.i18n.getMessage + updates BADGE_REC_COLOR to madder (--mks-madder-600 = #b2543d per D-04). All existing vitest tests stay GREEN (chrome.i18n is a synchronous API; existing chrome.* mocks need a getMessage stub returning the key as the value for round-trip identity).
1. Update src/popup/style.css per the artifact-table `provides` summary:
- Add `@import "../shared/tokens.css";` at top (above `* { margin: 0; ...}`)
- body { background: var(--mks-surface); color: var(--mks-fg-1); font-family: var(--mks-font-ui); }
- .save-button { background: var(--mks-rec); color: var(--mks-fg-inverse); border-radius: var(--mks-radius-md); box-shadow: var(--mks-shadow-1); transition: all var(--mks-dur-base) var(--mks-ease-out); }
- .save-button:hover:not(:disabled) { background: var(--mks-madder-700); box-shadow: var(--mks-shadow-2); }
- .save-button:disabled { background: var(--mks-ink-300); }
- .save-button.saving { background: var(--mks-amber-600); }
- .save-button.done { background: var(--mks-moss-600); }
- .info-text { color: var(--mks-fg-2); font-family: var(--mks-font-ui); }
- .status-message.error { color: var(--mks-error); }
- .status-message.success { color: var(--mks-success); }
- .status-message.info { color: var(--mks-warning); }
- ZERO hex literals (regex /#[0-9a-fA-F]{3,8}/ match count === 0; verified by tests/build/tokens-adopted.test.ts case (c))
2. Update src/popup/index.html per artifact-table:
- <title>Mokosh — Session Capture</title> (literal English title — chrome doesn't substitute __MSG__ in HTML body text or <title>; popup's title is a sensible default for non-RU locales). Document the limitation in a comment.
- <span class="button-text"></span> — empty (populated by JS); remove the hardcoded 'Сохранить отчёт об ошибке'
- <p class="info-text" data-mks-key="popupInfoText"></p> — empty + data-mks-key attribute; remove the hardcoded 'Последние 30 сек видео + 10 мин лога'
3. Update src/popup/index.ts per artifact-table:
- In init(): call populateMksKeys() helper that loops over [data-mks-key] elements and sets each textContent = chrome.i18n.getMessage(el.dataset.mksKey || '')
- In updateUI() idle case: buttonText.textContent = chrome.i18n.getMessage('popupSaveCta')
- In updateUI() saving case: buttonText.textContent = chrome.i18n.getMessage('popupSaving') (new key — add to messages.json with 'Сохраняю...' RU / 'Saving...' EN; OR fallback to literal if missing — document choice)
- In updateUI() done case: buttonText.textContent = chrome.i18n.getMessage('popupSaveDoneShort') (new key — 'Готово! ✓' RU / 'Done ✓' EN) OR retain literal with a TODO
- In updateUI() empty-state branch: statusMessage.textContent = chrome.i18n.getMessage('popupEmptyState') (new key — 'Откройте запись через иконку расширения' RU / 'Open recording via the extension icon' EN)
- In saveArchive() success branch: statusMessage.textContent = chrome.i18n.getMessage('popupSaveDone')
Add the new keys (popupSaving, popupSaveDoneShort, popupEmptyState) to BOTH _locales/en/messages.json AND _locales/ru/messages.json. Re-verify locale-parity.test.ts.
4. Update src/background/index.ts per the `<interfaces>` shape block:
- Line 76 BADGE_REC_COLOR: '#00C853' → '#b2543d' with comment '// --mks-madder-600 per D-04 loom palette (Plan 01-12)'
- BADGE_OFF_COLOR ('#D32F2F') stays (no loom-palette token for OFF) OR change to var(--mks-ink-700) hex literal '#2f3349'; document choice in commit
- BADGE_ERROR_COLOR ('#F9A825') stays (no loom-palette token for warning amber) OR change to '#c98b3a' (= --mks-amber-600); document choice in commit
- Lines 82-84 BADGE_*_TITLE constants: KEEP as fallback strings (for any chrome.* stub contexts) BUT replace the runtime read in setBadgeState with `chrome.i18n.getMessage(<key>) || <fallback_const>` so the existing vitest tests that don't mock chrome.i18n still pass via fallback
- Line 862-865 RECOVERY notification body: title=chrome.i18n.getMessage('extName') || 'Mokosh', message=chrome.i18n.getMessage('notifRecovery') || 'Recording resumed.'
- Line 961-965 STARTUP notification body: title=chrome.i18n.getMessage('extName') || 'Mokosh', message=chrome.i18n.getMessage('notifStartup') || 'Recording started.'
Use the `|| fallback` pattern uniformly so chrome.i18n unavailability (in unit-test mocks lacking i18n) degrades to literal English-ish text rather than the empty string Chrome returns for missing keys.
5. Update relevant existing vitest tests that may need a chrome.i18n.getMessage stub:
- tests/background/badge-state-machine.test.ts: if setBadgeState now reads chrome.i18n.getMessage, the chromeStub needs `i18n: { getMessage: vi.fn((k: string) => k) }` so the test asserts title === <key-name> as an identity-passthrough. Adjust assertions accordingly.
- tests/background/onstartup-notification.test.ts: same — add i18n.getMessage stub.
- tests/background/save-archive-stops-recording.test.ts: same if it asserts notification copy.
The fallback `|| <const>` pattern reduces the need for chrome.i18n stubs in tests that don't depend on the displayed text; tests that DO depend on text shape need the identity-passthrough stub.
6. Run `npm test` — expect 106+ GREEN (100 existing post-01-14 + 6 new tests + any new copy in popup tests adjusted via chrome.i18n stub).
7. Run `npm run build` then re-run `tests/build/no-remote-fonts.test.ts` — expect GREEN (dist/tokens.css contains zero `googleapis` / `https://fonts`).
grep -c "#[0-9a-fA-F]\{3,8\}" src/popup/style.css && grep -c "var(--mks-" src/popup/style.css && grep -c "chrome.i18n.getMessage" src/popup/index.ts && grep -c "chrome.i18n.getMessage" src/background/index.ts && grep -n "BADGE_REC_COLOR.*#b2543d\|--mks-madder-600" src/background/index.ts && npx vitest run 2>&1 | tail -10 && npm run build 2>&1 | tail -5 && npx vitest run tests/build/no-remote-fonts.test.ts 2>&1 | tail -10
- src/popup/style.css contains 0 hex literals + ≥ 8 var(--mks-*) references + @import of tokens.css
- src/popup/index.html + src/popup/index.ts use chrome.i18n.getMessage at every operator-facing copy site (verified by grep ≥ 4 occurrences in popup/index.ts)
- src/background/index.ts uses chrome.i18n.getMessage at every operator-facing copy site (badge titles + 2 notification call sites; verified by grep ≥ 4 occurrences); BADGE_REC_COLOR === '#b2543d'
- tests/build/tokens-adopted.test.ts ALL cases GREEN (including (b)+(c) which were RED in Wave 0)
- tests/build/no-remote-fonts.test.ts GREEN (after npm run build)
- vitest baseline 106+ GREEN (existing 100 post-01-14 + 6 new); no regressions in tests/background/*
- Atomic commit `feat(01-12): wave-4 task-1 — adopt tokens.css + chrome.i18n.getMessage in src/popup/ + src/background/ (loom palette + RU i18n + en fallback)`
Wave 5 Task 1: Welcome page conditional migration + Vite __VITE_DEV__ define-token + scripts/README.md
src/welcome/welcome.css, src/welcome/welcome-tokens.css, src/welcome/welcome.ts, src/welcome/copy.ts, vite.config.ts, scripts/README.md
If Plan 01-10 has landed (src/welcome/ exists at execute-plan time): migrate welcome-tokens.css → @import '../shared/tokens.css' (one-liner) AND migrate copy.ts to chrome.i18n.getMessage reads for welcomeHeroRu + welcomeHeroEn + any other welcome keys.
If Plan 01-10 has NOT landed: skip all src/welcome/* changes; Plan 01-10 executor uses src/shared/tokens.css directly when 01-10 lands later (no placeholder welcome-tokens.css needed in that path).
Unconditionally: add __VITE_DEV__ define-token to vite.config.ts (RESEARCH §12 D-09 spirit-satisfaction) + create scripts/README.md documenting the smoke-isolation invariant.
1. Detect Plan 01-10 state: `ls src/welcome/welcome.html 2>/dev/null && echo "01-10 landed" || echo "01-10 not yet"`. Branch on result.
2A. IF Plan 01-10 has landed:
- Read src/welcome/welcome-tokens.css. Replace contents with `@import '../shared/tokens.css';\n` (single line file). NB: per RESEARCH §9 anti-pattern, do NOT create per-surface mini-tokens — collapse into the canonical tokens.css.
- Read src/welcome/welcome.css. Verify it uses var(--mks-*) references throughout (per 01-10 design-swap-ready architecture; no hex literals expected). Add a comment header noting "tokens.css now canonical via welcome-tokens.css → @import."
- Read src/welcome/copy.ts. Reduce to a fallback shim: export `function getCopy(key: string): string { const i18n = chrome.i18n?.getMessage?.(key); return i18n && i18n.length > 0 ? i18n : COPY[key] || ''; }` where COPY is the existing static map (retained as the safety net). Update src/welcome/welcome.ts to call `getCopy()` instead of `COPY[key]` directly.
- Add to _locales/en/messages.json + _locales/ru/messages.json the welcome keys that copy.ts exposes (welcomeHeroTitle, welcomeBodyExplainerLine1, welcomeBodyExplainerLine2, welcomeBodyCtaToolbar, welcomeFooterPrivacy, etc. — per 01-10-PLAN.md src/welcome/copy.ts artifact list). Re-verify locale-parity.test.ts GREEN.
- Re-run 01-10's Plan 01-10 onboarding tests + harness A17 (welcome.css zero-hex regex + COPY[ substring check): assert A17's regex-based welcome.css check still GREEN (welcome.css continues to have zero hex literals).
2B. IF Plan 01-10 has NOT landed:
- Skip all src/welcome/* file modifications. Document this in the commit body: "Plan 01-10 not yet landed; welcome migration deferred. When 01-10 lands, its executor uses src/shared/tokens.css directly (skipping the placeholder welcome-tokens.css step entirely)." Add a paragraph to .planning/STATE.md noting the deferral so 01-10's planner can adjust.
3. Unconditionally update vite.config.ts: in the existing `define` block, add `__VITE_DEV__: JSON.stringify(process.env.VITE_DEV === '1')` alongside the existing `__MOKOSH_UAT__: 'false'`. Add an inline comment per RESEARCH §12 explaining the defensive intent ("reserved for any future inline smoke-mode check; currently smoke lives entirely in smoke.sh which is outside Vite's input set — verified by `grep -rn 'smoke\\|SMOKE\\|data:text/html' src/` returning empty").
4. Verify vite.test.config.ts mergeConfig propagates __VITE_DEV__ (no explicit override needed — mergeConfig spreads define fields).
5. Create scripts/README.md (single paragraph per RESEARCH §12): document that smoke.sh + future dev-only scripts live in this directory + are NOT bundled by npm run build + the production dist/ contains no smoke artifacts. Cross-reference RESEARCH.md §12 for the no-op-confirmation rationale.
6. Run npm test + npm run build + npm run build:test — expect all GREEN baseline (100 post-01-14) + 6 new tests = 106 GREEN.
7. (Optional but recommended per RESEARCH §12 + the existing Tier-1 gate pattern): add a `tests/build/no-smoke-in-dist.test.ts` test asserting `grep -rn smoke dist/` returns 0 matches post-build. RED today if any smoke string leaks; GREEN per current architecture. Keep this OPTIONAL — if context budget tight, defer to a follow-up plan.
(ls src/welcome/welcome.html 2>/dev/null && echo "Plan 01-10 landed — checking welcome migration:" && grep -c "tokens.css" src/welcome/welcome-tokens.css && grep -c "chrome.i18n" src/welcome/welcome.ts) || echo "Plan 01-10 not landed — skipped welcome migration" && grep -c "__VITE_DEV__" vite.config.ts && ls -la scripts/README.md && npx vitest run 2>&1 | tail -10
- IF 01-10 landed: src/welcome/welcome-tokens.css === single @import line; welcome.ts uses chrome.i18n.getMessage via getCopy() shim
- IF 01-10 NOT landed: src/welcome/* unchanged; STATE.md gains deferral note
- vite.config.ts:define block contains __VITE_DEV__ entry
- scripts/README.md exists + documents smoke-isolation invariant
- vitest baseline 106+ GREEN (+ N welcome keys added to messages.json if 01-10 landed; locale-parity stays GREEN)
- Atomic commit `feat(01-12): wave-5 task-1 — welcome i18n migration (conditional on 01-10) + __VITE_DEV__ define + scripts/README.md`
Wave 6 Task 1: Extend UAT harness with A18-A22 (font / icon / manifest-i18n / Lora-resolved / welcome-tokens)
tests/uat/extension-page-harness.ts, tests/uat/lib/harness-page-driver.ts, tests/uat/harness.test.ts
The 01-13 + 01-14 harness gains 5 new assertions following the established Approach-B pattern (page-side assertA* + host-side driveA* wrapper + harness.test.ts orchestrator entry). Each assertion uses production chrome.* APIs + fetch + getComputedStyle (no new test-mode symbols). After this wave: `npm run test:uat` reports 21/21 GREEN (or 24/24 with 01-10's A15-A17 already in place). A0-A14 (Plan 01-13) and A23 (Plan 01-14) remain GREEN as no-regression guarantees.
Tier-1 FORBIDDEN_HOOK_STRINGS list stays at 12 entries post-Plan-01-14 (no new test-mode surface introduced by Plan 01-12).
1. Extend tests/uat/extension-page-harness.ts: add 5 page-side methods to window.__mokoshHarness per the `` block shapes:
- assertA18(): walks document.styleSheets for the first @font-face rule whose src URL contains 'Lora'; resolves the URL relative to location.href; fetches it; asserts response.ok + arrayBuffer().byteLength >= 50000. Returns AssertionRecord with structured checks (A18.1 found rule; A18.2 fetch status; A18.3 byteLength).
- assertA19(): fetches chrome.runtime.getURL('icons/icon128.png'); reads arrayBuffer; computes a simple identity check (first 32 bytes byte-by-byte comparison against a hardcoded snapshot of the Bug A placeholder file's first 32 bytes); returns A19.1: "icon differs from Bug A placeholder fingerprint". Stores the placeholder fingerprint as a const at top of the file with a comment referencing this plan + the verification command to refresh it.
- assertA20(): chrome.runtime.getManifest().name === 'Mokosh — Session Capture'; structured AssertionRecord with A20.1 manifest.name === '...'.
- assertA21(): creates a transient div, applies class='mks-display-1', reads getComputedStyle.fontFamily, asserts startsWith 'Lora' or '"Lora"'. Returns A21.1: fontFamily resolved.
- assertA22(): fetches chrome.runtime.getURL('src/welcome/welcome.html'); if status === 404, returns PASS with diagnostic "Plan 01-10 not landed; A22 skipped"; if status === 200, also fetches the linked welcome.css (extract from the HTML) and asserts welcome.css contains either `tokens.css` import OR substantive var(--mks-*) usage (regex /var\\(--mks-[a-z-]+\\)/ match count >= 3).
2. Extend tests/uat/lib/harness-page-driver.ts: add driveA18 / driveA19 / driveA20 / driveA21 / driveA22 wrappers following the established driveA4 pattern (page.evaluate(() => window.__mokoshHarness.assertXX())). No new host-side fs / ffprobe / zip primitives.
3. Extend tests/uat/harness.test.ts: import driveA18..A22; add entries to the orchestrator's assertion list in sequence (after A14, or A17 if 01-10 landed, with A23 from 01-14 elsewhere in the list). Keep FORBIDDEN_HOOK_STRINGS UNCHANGED at 12 entries (the 01-14-established baseline) — verify by `wc -l` diff before/after this task.
4. Run npm run build:test (rebuilds dist-test/). Then run `npm run test:uat` (or just `tsx tests/uat/harness.test.ts`). Expected output: 21/21 (or 24/24) GREEN.
5. Run `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts` — confirm Tier-1 grep gate GREEN (no new hook strings leaked into dist/).
grep -c "assertA1[89]\|assertA2[012]" tests/uat/extension-page-harness.ts && grep -c "driveA1[89]\|driveA2[012]" tests/uat/lib/harness-page-driver.ts && grep -c "driveA1[89]\|driveA2[012]" tests/uat/harness.test.ts && (wc -l tests/uat/harness.test.ts && grep "FORBIDDEN_HOOK_STRINGS" tests/uat/harness.test.ts -A 15 | grep -v "^#" | grep -c "'") && npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts 2>&1 | tail -5 && (npm run test:uat 2>&1 | tail -25 || echo "test:uat invocation needs verification; check for 21/21 or 24/24 GREEN")
- 5 new assertA* methods on window.__mokoshHarness (verified by grep ≥ 5)
- 5 new driveA* wrappers in harness-page-driver.ts
- harness.test.ts orchestrator runs all 20 (or 23 with 01-10) assertions sequentially
- npm run test:uat exits 0 with 21/21 (or 24/24) GREEN
- Tier-1 forbidden-strings inventory in no-test-hooks-in-prod-bundle.test.ts UNCHANGED at 12 entries post-Plan-01-14 (no new test-mode symbols added by Plan 01-12)
- Atomic commit `feat(01-12): wave-6 task-1 — harness A18-A22 (font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens)`
Wave 7 Checkpoint: Operator empirical brand-fit ack
(operator-driven; no source files modified by this checkpoint — Wave 1-6 already landed the artifacts)
See below — operator-driven empirical check against fresh build loaded in Chrome. The executor agent must NOT bypass this checkpoint by stubbing; brand fit is the LAST Phase 1 operator gate and must be confirmed against the real branded extension.
echo "checkpoint:human-verify — see how-to-verify section; resume signal is the gate"
Operator types 'approved' after running the how-to-verify steps. See .
Plan 01-12 lands the full design integration: Lora self-hosted (R2 designer reply); IBM Plex Sans + Mono self-hosted (Latin + Cyrillic); src/shared/tokens.css canonical (Newsreader → Lora substitution + .mks-word class + zero remote @import); icons rasterized from Loom mark (replaces Bug A placeholders); manifest:name + :description + :default_title migrated to chrome i18n (__MSG_*__ + _locales/{en,ru}/messages.json); 12 i18n keys land with D-07 + D-08 + Brief §02 verbatim Russian copy; src/popup/ + src/background/ use chrome.i18n.getMessage at every operator-facing site + tokens.css palette; BADGE_REC_COLOR madder-rust per D-04; welcome page (if 01-10 landed) inherits canonical tokens; vite __VITE_DEV__ defensive define; UAT harness 21/21 (or 24/24) GREEN with A18-A22 covering font reachability + icon-distinct + manifest-i18n + Lora-resolved + welcome-tokens (A0-A14 from 01-13 and A23 from 01-14 remain GREEN as no-regression guarantees).
1. Fresh build + load:
- `rm -rf dist/ dist-test/`
- `npm run build`
- Open `chrome://extensions/`, toggle Developer mode, click "Load unpacked", select the `dist/` directory.
- Verify: extension name shows "Mokosh — Session Capture" (en locale) or "Mokosh — Запись сессии" (ru locale). The extension's icon in the toolbar is the rasterized Loom mark (4×4-grid silhouette inside a rounded square), NOT the prior dark-square + green-dot placeholder.
2. Popup brand fit:
- Click the Mokosh toolbar icon to open the popup.
- Verify: popup background is warm-linen (--mks-surface = #faf7f1), NOT cool-gray; SAVE button is madder-rust (--mks-rec = #b2543d), NOT material-blue; info text is warm gray (--mks-fg-2 = #5b5f76); copy reads "Сохранить отчёт" (RU) or "Save report" (EN) per chrome's resolved locale.
3. Russian copy rendering in Lora:
- Switch Chrome to Russian locale (chrome://settings/languages — add Russian + move to top + restart).
- Re-open the Mokosh popup. Verify Russian strings render correctly in IBM Plex Sans (UI body) — text reads "Сохранить отчёт", "Архив сохранён в Загрузки.", etc.
- The display-register text (if any visible in popup) renders in Lora — verify via DevTools → Inspect → Computed → font-family (should read 'Lora' as the matched family for any .mks-display-* class). For now, popup doesn't have visible display-register text; the load-bearing display-register check happens on the welcome page (if 01-10 landed) OR by spawning a temporary `chrome-extension://<id>/src/welcome/welcome.html` if available.
4. Manifest name in chrome://extensions:
- Switch back to English Chrome (or stay in RU for parallel verification).
- chrome://extensions/ shows "Mokosh — Session Capture" (en) or "Mokosh — Запись сессии" (ru) in the extension card.
5. Notification icon + copy:
- Trigger a recording start (click toolbar — toolbar:onClicked → startVideoCapture → eventually notification).
- Verify: startup notification icon is the rasterized Loom mark (not placeholder); copy reads "Запись запущена. Я слежу за последними 30 секундами." (ru) / "Recording started. I'm watching the last 30 seconds." (en).
6. Welcome page (if 01-10 landed):
- Navigate to chrome-extension://<id>/src/welcome/welcome.html in a tab.
- Verify: welcome hero renders D-08 tagline in Lora; warm-linen background; mark slot shows the Loom mark; Russian + English parallel-text copy renders correctly.
7. Harness gate:
- `npm run test:uat` exits 0 with 21/21 GREEN (or 24/24 with 01-10).
- Tier-1 grep gate GREEN: `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts`.
8. Type "approved" if brand fit accepted, OR describe specific issues for follow-up.
Type "approved" or describe issues for follow-up
Wave 7 Task 1: Plan closure (SUMMARY + STATE.md + ROADMAP.md sync)
.planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md, .planning/STATE.md, .planning/ROADMAP.md
1. Create .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md following the template at $HOME/.claude/get-shit-done/templates/summary.md AND mirroring the structural choices of 01-13-SUMMARY.md (frontmatter with phase/plan/subsystem/tags/requires/provides/affects/tech-stack/key-files/decisions/metrics; body sections One-Liner / What Landed by Wave / Test Counts / Deviations from Plan / Architectural Notes Worth Carrying Forward / Self-Check / Known Limitations / Bridge to Phase 1 Closure).
Key content:
- One-Liner: "Design integration landed end-to-end: Lora self-hosted (R2 designer reply 2026-05-19); src/shared/tokens.css canonical; 12 i18n keys via _locales/{en,ru}; branded Loom icons replace placeholders; src/popup + src/background use chrome.i18n.getMessage + loom palette; harness 21/21 (or 24/24) GREEN with A18-A22; operator brand-fit ack received."
- Wave-by-wave breakdown with commit refs.
- Test Counts table: vitest before/after; harness before/after.
- Architectural Notes: R2 substitution rationale; OFL attribution pattern; tokens.css as single source of truth; chrome.i18n.getMessage fallback pattern (|| <const>); .mks-word class as engineering working-defn (designer overridable); icons-as-static-artifacts (NOT regenerated in prebuild per RESEARCH §3 anti-pattern).
- Self-Check: FOUND list including the 6 new vitest tests, 5 new harness assertions, font bundle, icons, manifest i18n, _locales/, source-code adoption.
- Bridge: Phase 1 closure status (the LAST Phase 1 gate, brand-fit ack, is now closed); Phase 2 inherits tokens.css + chrome.i18n.getMessage as production conventions.
2. Update .planning/STATE.md:
- completed_plans + 1 (current 12 → 13)
- percent re-computed (likely 100% Phase 1 now, since this was the last gate)
- Add a "Plan 01-12 closure" section under "## Current Position" with the brand-fit ack date + harness count + R2 substitution note
- Update "stopped_at" + "last_updated" + "last_activity" fields
- Update "## Outstanding Phase 1 gates" section: remove Plan 01-12 entry (it's now closed); confirm Plan 01-10 status (closed if landed; still pending if not)
3. Update .planning/ROADMAP.md:
- In the Phase 1 Plans list (after `01-13-PLAN.md` entry per 01-13 SUMMARY ROADMAP.md known-gap), add `- [x] 01-12-PLAN.md — Design integration (R2 Lora, tokens.css, 8 i18n strings, branded icons, manifest i18n)`
- If Plan 01-10 entry is missing per the 01-13 SUMMARY note about the ROADMAP.md gap, add it too as `- [x]` or `- [ ]` depending on its current status. (Per 01-13 SUMMARY Self-Check: Plan 01-10 was still pending at 01-13 close; if it landed between 01-13 close and this 01-12 close, mark `[x]`.)
- Update the Phase 1 header line if it carries a count (e.g., "**Plans:** N plans" → updated to actual final count).
4. Atomic commit: `docs(01-12): wave-7 task-1 — Plan 01-12 SUMMARY + STATE.md + ROADMAP.md sync (design integration closed; Phase 1 charter complete)`.
ls -la .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md && grep -n "01-12-PLAN.md\|R2 Lora\|design integration" .planning/ROADMAP.md && grep -n "01-12\|completed_plans" .planning/STATE.md | head -10 && head -20 .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md
- .planning/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md exists + follows template + carries all 9 frontmatter sections
- STATE.md completed_plans incremented + percent recomputed + Plan 01-12 closure section added
- ROADMAP.md Phase 1 Plans list contains the 01-12 entry (and 01-10 entry if previously missing)
- Atomic commit lands all three docs in one
- Plan 01-12 functionally CLOSED; Phase 1 charter complete (subject to the Wave 7 checkpoint operator ack)
<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 ` |
| T-01-12-08 | Elevation of Privilege | Test-mode hook strings leaking into production dist/ (T-1-11-01 + T-1-13-* + T-1-14-* inheritance) | accept | Plan 01-12 introduces ZERO new test-mode symbols (A18-A22 use production chrome.* + fetch + getComputedStyle exclusively). Tier-1 grep gate at tests/background/no-test-hooks-in-prod-bundle.test.ts remains at 12 entries post-Plan-01-14; no extension needed. Verified by file-diff at task end. |
| T-01-12-09 | Tampering | OFL license/attribution non-compliance | mitigate | src/shared/fonts/LICENSE-Lora.txt + LICENSE-IBM-Plex.txt ship verbatim per OFL-1.1 attribution best practice (RESEARCH §7); src/shared/fonts/README.md cross-references upstream URLs + R2 substitution rationale. |
| </threat_model> |
- Build cleanliness:
npm run buildexits 0;npm run build:testexits 0;npx tsc --noEmitexits 0. - MV3 CSP compliance: zero
googleapis/https://fontsmatches in dist/ (tests/build/no-remote-fonts.test.ts). - Vitest baseline grown: 100 (post-01-14) → 106+ GREEN (+6 new unit tests; +N if welcome migration adds keys).
- UAT harness 21/21 (or 24/24 with 01-10) GREEN:
npm run test:uatexits 0; A0 Tier-1 grep gate GREEN; A1-A14 (or A1-A17 with 01-10) + A23 (Plan 01-14) GREEN; A18-A22 GREEN. - Font bundle present: all 7 or 8 WOFF2 files exist under src/shared/fonts/; total bundle ≤ 350KB; LICENSE files + README.md present.
- 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.
- 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.
- 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.
- Tier-1 forbidden-strings inventory UNCHANGED at 12 entries post-Plan-01-14.
- R2 substitution complete: zero
Newsreaderreferences anywhere in src/shared/tokens.css; --mks-font-display === '"Lora", "Iowan Old Style", "Times New Roman", serif'. - Operator brand-fit ack: Wave 7 checkpoint received "approved".
<success_criteria>
- All 6 vitest tests in tests/build/ + tests/i18n/ GREEN
- src/shared/fonts/ contains all required WOFF2 + LICENSE + README.md files
- src/shared/tokens.css canonical: zero remote-font URLs, R2 Lora substitution, .mks-word class present
- icons/icon{16,48,128}.png rasterized from Loom mark, clearing all FLOORs, 8-bit RGBA
- manifest.json carries
__MSG_extName__+__MSG_extDesc__+default_locale: "en"+action.default_title: "__MSG_tooltipOff__" - _locales/{en,ru}/messages.json each carry the 12-key matrix with D-07 + D-08 + Brief §02 values
- src/popup/ + src/background/ use chrome.i18n.getMessage at every operator-facing copy site (with
|| <const>fallback) - src/popup/style.css has zero hex literals + imports tokens.css
- BADGE_REC_COLOR === '#b2543d' (--mks-madder-600 per D-04)
- If Plan 01-10 landed: welcome-tokens.css =
@import '../shared/tokens.css';; welcome.ts uses chrome.i18n.getMessage - vite.config.ts has
__VITE_DEV__defensive define - scripts/{rasterize-icons.sh,subset-fonts.sh,README.md} all present
- UAT harness A18-A22 implemented + GREEN; npm run test:uat exits 0 with 21/21 (or 24/24 with 01-10) GREEN
- Tier-1 forbidden-strings inventory UNCHANGED at 12 entries post-Plan-01-14
- Operator empirical brand-fit ack ("approved") received at Wave 7 checkpoint
- 01-12-SUMMARY.md exists + STATE.md + ROADMAP.md synced
- All commits atomic + follow
feat(01-12)/test(01-12)/docs(01-12): wave-N task-M — <one-liner>convention </success_criteria>