Drops the 2026-05-17 draft (zero commits, zero SUMMARY — virgin). Carries: - onInstalled flag-gated welcome tab (3 RED→GREEN unit tests; storage-key contract pinned per prior I-02 fix) - 5 new src/welcome/* files: welcome.html + welcome.ts + welcome.css + welcome-tokens.css + copy.ts - Design-swap-in-ready: every color via var(--mks-*); every string via COPY map; every font via var(--mks-font-*) with system fallback - vite.config.ts + vite.test.config.ts both gain welcome rollup input - manifest.json gains web_accessible_resources for welcome.html - Harness extended A15+A16+A17 (onboarding flag observability + no-re-open settle + design-swap invariant); Tier-1 forbidden-strings inventory unchanged at 10 - 4 autonomous tasks + 1 operator empirical checkpoint Deletes: REQUEST_PERMISSIONS flow (gone in 01-09); duplicate Start button (D-16-toolbar owns start path); start-path divergence Cites: D-17-onboarding (CONTEXT.md L537+), D-02 (welcome layout), D-08 (tagline), D-03 (voice register), D-16-toolbar (start ownership), brand-decisions-v1-followup-display-font.md (Plan 01-12 blocker; this plan ships TODAY with placeholders) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1276 lines
68 KiB
Markdown
1276 lines
68 KiB
Markdown
---
|
||
phase: 01-stabilize-video-pipeline
|
||
plan: 10
|
||
type: execute
|
||
wave: 4
|
||
depends_on:
|
||
- 01-09
|
||
- 01-13
|
||
files_modified:
|
||
- manifest.json
|
||
- vite.config.ts
|
||
- vite.test.config.ts
|
||
- src/welcome/welcome.html
|
||
- src/welcome/welcome.ts
|
||
- src/welcome/welcome.css
|
||
- src/welcome/copy.ts
|
||
- src/welcome/welcome-tokens.css
|
||
- src/background/index.ts
|
||
- tests/background/onboarding.test.ts
|
||
- tests/uat/extension-page-harness.ts
|
||
- tests/uat/extension-page-harness.html
|
||
- tests/uat/lib/harness-page-driver.ts
|
||
- tests/uat/harness.test.ts
|
||
autonomous: false
|
||
requirements:
|
||
- REQ-video-ring-buffer
|
||
tags:
|
||
- onboarding
|
||
- welcome
|
||
- chrome.runtime.onInstalled
|
||
- chrome.storage
|
||
- web_accessible_resources
|
||
- design-swap-in-ready
|
||
- css-variables
|
||
- i18n-keys
|
||
- D-17-onboarding
|
||
- D-02-welcome-layout
|
||
- D-08-tagline
|
||
- harness-A15-A17
|
||
|
||
must_haves:
|
||
truths:
|
||
- "On first install (chrome.runtime.onInstalled with reason 'install') AND chrome.storage.local.get('onboarding-completed') returns nothing, exactly ONE welcome tab opens automatically pointing at chrome.runtime.getURL('src/welcome/welcome.html'). After the tab opens, chrome.storage.local.set is called with {'onboarding-completed': true, 'installed-at': <Date.now()>}."
|
||
- "On subsequent installs (reason 'update' or 'chrome_update') OR when 'onboarding-completed' is already true, NO welcome tab opens (chrome.tabs.create is NOT called)."
|
||
- "The welcome page renders a hero (.welcome-hero slot) containing a placeholder mark, the D-08 tagline string (Russian primary + English subtitle), 2-3 explainer lines, a primary instruction pointing the operator at the toolbar icon (NOT a Start button — toolbar onClicked owns the start path per Plan 01-09 D-16-toolbar), and a privacy/safety footer."
|
||
- "The welcome page contains NO hardcoded hex color values in welcome.css — every color is sourced from a --mks-* CSS custom property defined in welcome-tokens.css (placeholder palette today; swapped for tokens.css import in Plan 01-12)."
|
||
- "All copy strings live in a single source-of-truth src/welcome/copy.ts exporting a const COPY map; welcome.ts reads keys via COPY['welcome.hero.title'] style access. When Plan 01-12 lands _locales/, the swap is a one-commit change of copy.ts reading chrome.i18n.getMessage(key)."
|
||
- "Typography uses font-family: var(--mks-font-display, <system fallback>) and var(--mks-font-ui, <system fallback>) — when Plan 01-12 self-hosts Newsreader (or designer-picked Cyrillic-capable alternative per brand-decisions-v1-followup-display-font.md) the bundled face resolves; until then the system fallback renders."
|
||
- "The welcome page references NO REQUEST_PERMISSIONS message type, NO chrome.runtime.sendMessage start path, NO duplicate getDisplayMedia trigger. Its CTA is informational — points operator at the toolbar icon (01-09 owns toolbar onClicked → picker)."
|
||
- "Onboarding unit test covers BOTH first-install AND subsequent-install paths AND the storage-key contract via three synthesized onInstalled events + chrome.storage.local.get mock setups."
|
||
- "UAT harness extends with three new assertions (A15, A16, A17). A15 = onboarding flag observability (chrome.storage.local 'onboarding-completed' is true AND 'installed-at' is a number AT LEAST ONCE during this session). A16 = subsequent-install does NOT re-open (post-A15 settle observation: no new welcome tabs spontaneously appear; chrome.tabs.query count delta is 0 over 2-second window). A17 = design-swap readiness invariant: welcome.html parses; .welcome-hero slot exists; welcome.css contains zero #hex literals (regex /#[0-9a-fA-F]{3,8}/ match count == 0); welcome.html contains ≥ 7 data-mokosh-key attributes; bundled welcome JS chunk contains substring 'COPY['."
|
||
- "npm run test:uat reports 18/18 GREEN at plan close (existing 15 + A15 + A16 + A17). npm test (vitest) reports baseline 98 GREEN + 3 new onboarding tests GREEN = 101 GREEN; Tier-1 forbidden-strings inventory unchanged at 10 (no new test-hook surface introduced)."
|
||
artifacts:
|
||
- path: "src/welcome/welcome.html"
|
||
provides: "Welcome page markup. Hero wrapped in <main class='welcome'><section class='welcome-hero'>...<section class='welcome-body'>...<footer class='welcome-footer'>... per D-02 Surface Kit §05 option A (Hero + Loom dial layout). Mark slot is <div class='welcome-hero__mark' role='presentation' data-mokosh-slot='mark'> — Plan 01-12 swaps to mokosh-lockup.svg."
|
||
min_lines: 30
|
||
- path: "src/welcome/welcome.ts"
|
||
provides: "Vanilla DOM entry point. Imports COPY from './copy'; on DOMContentLoaded populates each [data-mokosh-key='<key>'] element's textContent from COPY. No event handlers — welcome tab is informational + read-only (per D-16-toolbar charter). Uses centralized Logger from src/shared/logger.ts (new Logger('Welcome'))."
|
||
min_lines: 25
|
||
- path: "src/welcome/welcome.css"
|
||
provides: "Welcome page styling. Every color references var(--mks-*) from welcome-tokens.css; every font references var(--mks-font-*). Layout: max-width 720px (matches --mks-welcome-max-w in tokens.css), centered, hero+body+footer vertical stack."
|
||
min_lines: 30
|
||
- path: "src/welcome/welcome-tokens.css"
|
||
provides: "Placeholder token palette mirroring tokens.css semantic names — --mks-surface, --mks-surface-raised, --mks-fg-1, --mks-fg-2, --mks-rec, --mks-success, --mks-border, --mks-font-display, --mks-font-ui, --mks-welcome-max-w. Values are engineering-grade neutrals (warm-off-white / near-black / system serif / system sans). Plan 01-12 replaces this file with @import of canonical tokens.css."
|
||
min_lines: 25
|
||
- path: "src/welcome/copy.ts"
|
||
provides: "Single source-of-truth copy map. Exports const COPY: Readonly<Record<string, string>> keyed by stable identifiers (welcome.hero.title, welcome.hero.tagline.ru, welcome.hero.tagline.en, welcome.body.explainer.line1, welcome.body.explainer.line2, welcome.body.cta.toolbar, welcome.footer.privacy). Plan 01-12 either swaps to chrome.i18n.getMessage post _locales/ landing OR hot-swaps the literal strings to brand-team final 8 strings (Brief §02)."
|
||
min_lines: 20
|
||
- path: "src/background/index.ts"
|
||
provides: "openWelcomeIfFirstInstall(details) helper added (placed below ensureOffscreen). chrome.runtime.onInstalled handler extended to call helper AFTER existing IDB cleanup + initialize(). Helper: early-return on non-install reason; await chrome.storage.local.get; early-return if flag already true; chrome.tabs.create + chrome.storage.local.set. Fire-and-forget invocation wrapped in .catch(...)."
|
||
contains: "openWelcomeIfFirstInstall"
|
||
- path: "manifest.json"
|
||
provides: "web_accessible_resources array added (was absent). Single entry {resources:['src/welcome/welcome.html'], matches:['<all_urls>']}. storage permission already present at line 11; no change. No new permissions."
|
||
contains: "web_accessible_resources"
|
||
- path: "vite.config.ts"
|
||
provides: "rollupOptions.input gains welcome: 'src/welcome/welcome.html' alongside existing offscreen entry."
|
||
contains: "welcome:"
|
||
- path: "vite.test.config.ts"
|
||
provides: "Mirror of vite.config.ts: rollupOptions.input gains welcome entry so dist-test/ also carries the page (harness A15-A17 navigates fresh-install Chrome with test build loaded)."
|
||
contains: "welcome:"
|
||
- path: "tests/background/onboarding.test.ts"
|
||
provides: "Three RED→GREEN tests pinning the onInstalled contract. Test A: install reason + empty storage → exactly one tabs.create with welcome URL + storage.set with onboarding-completed:true + installed-at:<number>; additionally asserts chrome.storage.local.get called with EXACT key 'onboarding-completed' (storage-schema cross-version-compat pin, preserved from prior plan I-02 fix). Test B: update reason → zero tabs.create. Test C: install + flag already set → zero tabs.create."
|
||
min_lines: 120
|
||
- path: "tests/uat/extension-page-harness.ts"
|
||
provides: "Three new page-side assertion methods on window.__mokoshHarness: assertA15 (onboarding flag observability), assertA16 (subsequent-install does NOT re-open — settle-window check), assertA17 (design-swap-readiness — welcome.html parse + .welcome-hero slot + welcome.css fetch + zero-hex regex + data-mokosh-key count + COPY[ substring in bundled JS)."
|
||
min_lines: 150
|
||
- path: "tests/uat/lib/harness-page-driver.ts"
|
||
provides: "Three new host-side driver wrappers: driveA15, driveA16, driveA17. Standard page.evaluate pattern matching driveA14."
|
||
min_lines: 50
|
||
- path: "tests/uat/harness.test.ts"
|
||
provides: "Drivers list extended with A15, A16, A17 entries. FORBIDDEN_HOOK_STRINGS unchanged. Total assertions become 18 (A0 + A1..A14 + A15 + A16 + A17)."
|
||
contains: "A15"
|
||
key_links:
|
||
- from: "src/background/index.ts onInstalled handler (line 724+)"
|
||
to: "openWelcomeIfFirstInstall → chrome.tabs.create"
|
||
via: "Fire-and-forget invocation after initialize(); helper awaits chrome.storage.local.get + chrome.tabs.create + chrome.storage.local.set"
|
||
pattern: "openWelcomeIfFirstInstall"
|
||
- from: "manifest.json web_accessible_resources"
|
||
to: "src/welcome/welcome.html"
|
||
via: "MV3 web_accessible_resources array with matches:['<all_urls>']"
|
||
pattern: "src/welcome/welcome.html"
|
||
- from: "src/welcome/welcome.ts init"
|
||
to: "src/welcome/copy.ts COPY map"
|
||
via: "import { COPY } from './copy'; populates [data-mokosh-key='<key>'] text from COPY[key]"
|
||
pattern: "data-mokosh-key"
|
||
- from: "src/welcome/welcome.css"
|
||
to: "src/welcome/welcome-tokens.css"
|
||
via: "Implicit CSS cascade — welcome.html links both stylesheets; welcome.css references --mks-* vars declared in welcome-tokens.css"
|
||
pattern: "var(--mks-"
|
||
- from: "tests/uat/harness.test.ts drivers list"
|
||
to: "tests/uat/lib/harness-page-driver.ts driveA15/A16/A17"
|
||
via: "Driver tuple appended to drivers ReadonlyArray, same idiom as driveA14"
|
||
pattern: "driveA15"
|
||
- from: "tests/uat/extension-page-harness.ts window.__mokoshHarness"
|
||
to: "assertA15/A16/A17 methods"
|
||
via: "Added to existing __mokoshHarness surface alongside assertA1..A14"
|
||
pattern: "assertA15"
|
||
---
|
||
|
||
<objective>
|
||
First-install operator-friendly activation: when the extension is installed (Chrome Web Store one day, or Load Unpacked today), open a welcome tab automatically pointing at src/welcome/welcome.html. The page renders a hero + tagline + 2-3 explainer lines + a CTA instructing the operator to click the toolbar icon to begin recording. Subsequent installs/updates do NOT re-open.
|
||
|
||
This complements Plan 01-09's runtime activation paths (toolbar onClicked → picker; onStartup notification at browser launch) by adding the install-time activation path. The welcome tab does NOT trigger recording itself — start remains a SINGLE path through chrome.action.onClicked in idle mode (D-16-toolbar). The welcome tab is informational + read-only; CTA copy directs the operator at the toolbar.
|
||
|
||
**Design-swap-in-ready architecture (the key directive).** Plan 01-12 has not yet landed; the designer follow-up on --mks-font-display Cyrillic gap is still open (brand-decisions-v1-followup-display-font.md). Plan 01-10 ships TODAY with engineering placeholders, architected so the Plan 01-12 swap is a small commit: every color via --mks-* CSS custom property in a separate welcome-tokens.css file (Plan 01-12 replaces with @import of tokens.css); every copy string via src/welcome/copy.ts map (Plan 01-12 swaps to chrome.i18n.getMessage post _locales/); every typography choice via var(--mks-font-*) with system fallback (Plan 01-12 self-hosts WOFF2 + drops the fallback dependency).
|
||
|
||
**Harness integration (A15-A17).** Extend the Plan 01-13 harness with three new assertions covering: onboarding flag observability (A15), subsequent-install does NOT re-open (A16), design-swap-readiness invariant (A17). Drivers register alongside driveA1..driveA14 in tests/uat/harness.test.ts. No new forbidden-hook strings — the Tier-1 grep gate inventory stays at 10.
|
||
|
||
Output:
|
||
- src/welcome/welcome.{html,ts,css} + src/welcome/welcome-tokens.css + src/welcome/copy.ts — 5 new files for the design-swap-in-ready welcome page.
|
||
- src/background/index.ts — openWelcomeIfFirstInstall helper + onInstalled wiring.
|
||
- manifest.json — web_accessible_resources array entry for the welcome page.
|
||
- vite.config.ts + vite.test.config.ts — rollupOptions.input gains welcome entry in BOTH bundles.
|
||
- tests/background/onboarding.test.ts — 3 RED→GREEN unit tests.
|
||
- tests/uat/extension-page-harness.{html,ts} + tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts — A15+A16+A17 harness assertions.
|
||
|
||
Cites: D-17-onboarding (CONTEXT.md line 537+), D-02 (welcome layout — brand-decisions-v1.md), D-08 (tagline — brand-decisions-v1.md), D-03 (Sober voice register — brand-decisions-v1.md), D-16-toolbar (toolbar owns start — 01-09-SUMMARY.md).
|
||
</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-SUMMARY.md
|
||
@.planning/phases/01-stabilize-video-pipeline/01-11-SUMMARY.md
|
||
@.planning/phases/01-stabilize-video-pipeline/01-13-SUMMARY.md
|
||
@.planning/intel/brand-decisions-v1.md
|
||
@.planning/intel/brand-decisions-v1-followup-display-font.md
|
||
@.planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css
|
||
@.planning/debug/resolved/01-09-save-stops-recording.md
|
||
@src/background/index.ts
|
||
@src/popup/index.html
|
||
@src/popup/index.ts
|
||
@src/shared/logger.ts
|
||
@manifest.json
|
||
@vite.config.ts
|
||
@vite.test.config.ts
|
||
@tests/background/onstartup-notification.test.ts
|
||
@tests/uat/extension-page-harness.html
|
||
@tests/uat/extension-page-harness.ts
|
||
@tests/uat/lib/harness-page-driver.ts
|
||
@tests/uat/harness.test.ts
|
||
@tests/background/no-test-hooks-in-prod-bundle.test.ts
|
||
|
||
<interfaces>
|
||
Key Chrome API surfaces and project conventions the executor needs.
|
||
|
||
1. chrome.runtime.onInstalled
|
||
=============================
|
||
|
||
Signature (from @types/chrome):
|
||
chrome.runtime.onInstalled.addListener(
|
||
callback: (details: chrome.runtime.InstalledDetails) => void): void
|
||
interface InstalledDetails {
|
||
reason: 'install' | 'update' | 'chrome_update' | 'shared_module_update';
|
||
previousVersion?: string;
|
||
id?: string;
|
||
}
|
||
|
||
CURRENT handler in src/background/index.ts (line 724+, post-01-09):
|
||
chrome.runtime.onInstalled.addListener((details) => {
|
||
logger.log('Extension installed/updated:', details.reason);
|
||
try {
|
||
indexedDB.deleteDatabase('VideoRecorderDB');
|
||
logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
|
||
} catch (e) {
|
||
logger.warn('IDB cleanup failed:', e);
|
||
}
|
||
initialize();
|
||
});
|
||
|
||
This plan EXTENDS the handler by appending a fire-and-forget call to
|
||
openWelcomeIfFirstInstall(details) AFTER initialize(). The existing
|
||
synchronous handler stays synchronous; the helper is async-IIFE-style
|
||
with its own try/catch.
|
||
|
||
2. chrome.storage.local
|
||
=======================
|
||
|
||
Signature:
|
||
chrome.storage.local.get(key: string): Promise<{[key: string]: unknown}>
|
||
chrome.storage.local.set(items: {[key: string]: unknown}): Promise<void>
|
||
|
||
Used pattern (helper):
|
||
const stored = await chrome.storage.local.get(ONBOARDING_FLAG);
|
||
if (stored[ONBOARDING_FLAG] === true) { return; }
|
||
// open tab ...
|
||
await chrome.storage.local.set({
|
||
[ONBOARDING_FLAG]: true,
|
||
'installed-at': Date.now(),
|
||
});
|
||
|
||
storage permission is ALREADY in manifest.json:11. No change needed.
|
||
|
||
3. chrome.tabs.create
|
||
=====================
|
||
|
||
Signature:
|
||
chrome.tabs.create({url: string, active?: boolean}): Promise<chrome.tabs.Tab>
|
||
|
||
For an extension-internal URL: chrome.runtime.getURL('src/welcome/welcome.html')
|
||
returns chrome-extension://<id>/src/welcome/welcome.html. This URL is
|
||
ONLY navigable if the path is declared in web_accessible_resources
|
||
(MV3 requirement).
|
||
|
||
4. manifest.json web_accessible_resources (MV3)
|
||
================================================
|
||
|
||
Current manifest.json HAS NO web_accessible_resources block (verified
|
||
via Read at plan time). This plan adds one.
|
||
|
||
Shape (JSON):
|
||
"web_accessible_resources": [
|
||
{
|
||
"resources": ["src/welcome/welcome.html"],
|
||
"matches": ["<all_urls>"]
|
||
}
|
||
]
|
||
|
||
Insertion point: after "host_permissions" block (lines 15-17), before
|
||
"background" block. Verify storage permission still present at line 11.
|
||
|
||
5. vite.config.ts rollupOptions.input
|
||
======================================
|
||
|
||
Current state (vite.config.ts:52-55):
|
||
rollupOptions: {
|
||
input: { offscreen: 'src/offscreen/index.html' },
|
||
},
|
||
|
||
Add welcome entry:
|
||
rollupOptions: {
|
||
input: {
|
||
offscreen: 'src/offscreen/index.html',
|
||
welcome: 'src/welcome/welcome.html',
|
||
},
|
||
},
|
||
|
||
Mirror in vite.test.config.ts at the same key path (line 82-92) so
|
||
dist-test/ also emits the welcome page. The harness A15/A16/A17
|
||
navigates against the test bundle.
|
||
|
||
6. src/welcome/welcome.html structure
|
||
======================================
|
||
|
||
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title data-mokosh-key="welcome.page.title">Добро пожаловать в Mokosh</title>
|
||
<link rel="stylesheet" href="welcome-tokens.css">
|
||
<link rel="stylesheet" href="welcome.css">
|
||
</head>
|
||
<body>
|
||
<main class="welcome">
|
||
<section class="welcome-hero" aria-labelledby="welcome-title">
|
||
<div class="welcome-hero__mark" role="presentation" data-mokosh-slot="mark">
|
||
<span class="welcome-hero__mark-placeholder" aria-hidden="true">Mokosh</span>
|
||
</div>
|
||
<h1 id="welcome-title" class="welcome-hero__title" data-mokosh-key="welcome.hero.title"></h1>
|
||
<p class="welcome-hero__tagline-ru" data-mokosh-key="welcome.hero.tagline.ru"></p>
|
||
<p class="welcome-hero__tagline-en" data-mokosh-key="welcome.hero.tagline.en"></p>
|
||
</section>
|
||
<section class="welcome-body">
|
||
<p class="welcome-body__line" data-mokosh-key="welcome.body.explainer.line1"></p>
|
||
<p class="welcome-body__line" data-mokosh-key="welcome.body.explainer.line2"></p>
|
||
<p class="welcome-body__cta" data-mokosh-key="welcome.body.cta.toolbar"></p>
|
||
</section>
|
||
<footer class="welcome-footer">
|
||
<p class="welcome-footer__privacy" data-mokosh-key="welcome.footer.privacy"></p>
|
||
</footer>
|
||
</main>
|
||
<script type="module" src="welcome.ts"></script>
|
||
</body>
|
||
</html>
|
||
|
||
7. src/welcome/copy.ts shape
|
||
=============================
|
||
|
||
// src/welcome/copy.ts — single source-of-truth for welcome-page copy.
|
||
// Plan 01-10 D-17-onboarding ships with placeholder Russian strings
|
||
// per D-03 Sober voice register. Plan 01-12 swaps to chrome.i18n.getMessage
|
||
// (post _locales/) OR hot-swaps to brand-team final 8 strings (Brief §02).
|
||
// STABLE KEYS — do NOT rename; Plan 01-12 binds to these.
|
||
//
|
||
// D-08 tagline locked: "«Тридцать секунд назад, всегда под рукой.»" +
|
||
// "Thirty seconds ago, always at hand."
|
||
|
||
export const COPY: Readonly<Record<string, string>> = Object.freeze({
|
||
'welcome.page.title': 'Добро пожаловать в Mokosh',
|
||
'welcome.hero.title': 'Mokosh',
|
||
'welcome.hero.tagline.ru': '«Тридцать секунд назад, всегда под рукой.»',
|
||
'welcome.hero.tagline.en': 'Thirty seconds ago, always at hand.',
|
||
'welcome.body.explainer.line1':
|
||
'Mokosh непрерывно записывает последние 30 секунд экрана и 10 минут ' +
|
||
'логов вашего браузера.',
|
||
'welcome.body.explainer.line2':
|
||
'Когда возникает баг, вы одним кликом сохраняете архив для службы ' +
|
||
'поддержки. Данные не отправляются никуда — только локально.',
|
||
'welcome.body.cta.toolbar':
|
||
'Чтобы начать запись, нажмите иконку AI Call Recorder на панели ' +
|
||
'инструментов браузера (правый верхний угол).',
|
||
'welcome.footer.privacy':
|
||
'Mokosh не отправляет данные на серверы. Архив создаётся ' +
|
||
'локально по вашему запросу и остаётся на вашем компьютере.',
|
||
});
|
||
|
||
8. src/welcome/welcome-tokens.css shape (placeholder)
|
||
======================================================
|
||
|
||
/* welcome-tokens.css — Plan 01-10 D-17-onboarding placeholder palette.
|
||
* Mirrors tokens.css semantic names. Plan 01-12 replaces with
|
||
* @import url('./tokens.css'); OR direct deletion + canonical link in
|
||
* welcome.html. Variable NAMES match tokens.css exactly; only VALUES
|
||
* differ. */
|
||
|
||
:root {
|
||
--mks-surface: #fafafa;
|
||
--mks-surface-raised: #ffffff;
|
||
--mks-surface-sunken: #ececec;
|
||
--mks-border: #e0e0e0;
|
||
--mks-fg-1: #181b2a;
|
||
--mks-fg-2: #5b5f76;
|
||
--mks-fg-3: #7a7e94;
|
||
--mks-rec: #b2543d;
|
||
--mks-success: #5a7349;
|
||
--mks-font-display:
|
||
'Iowan Old Style', 'Apple Garamond', 'Baskerville',
|
||
'Times New Roman', 'Droid Serif', Times, serif;
|
||
--mks-font-ui:
|
||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||
'Helvetica Neue', Arial, sans-serif;
|
||
--mks-welcome-max-w: 720px;
|
||
--mks-space-4: 16px;
|
||
--mks-space-8: 32px;
|
||
--mks-space-12: 48px;
|
||
}
|
||
|
||
NOTE on welcome-tokens.css hex literals: this file IS allowed to contain
|
||
hex literals — it IS the engineering-grade placeholder palette source.
|
||
Plan 01-12 replaces this file entirely; until then the hex values
|
||
provide the actual color rendering. The A17 grep gate excludes
|
||
welcome-tokens.css from its scan; only welcome.css must be hex-literal-free.
|
||
|
||
9. src/welcome/welcome.ts shape (filter-pipeline form; no `continue`)
|
||
=====================================================================
|
||
|
||
import { Logger } from '../shared/logger';
|
||
import { COPY } from './copy';
|
||
|
||
const logger = new Logger('Welcome');
|
||
|
||
function populateCopy(): void {
|
||
const els = Array.from(document.querySelectorAll<HTMLElement>('[data-mokosh-key]'));
|
||
const pairs = els
|
||
.map((el) => ({ el, key: el.getAttribute('data-mokosh-key') }))
|
||
.filter((p): p is { el: HTMLElement; key: string } =>
|
||
typeof p.key === 'string')
|
||
.map((p) => ({ ...p, value: COPY[p.key] }))
|
||
.filter((p): p is { el: HTMLElement; key: string; value: string } =>
|
||
typeof p.value === 'string');
|
||
for (const { el, value } of pairs) {
|
||
if (el.tagName === 'TITLE') {
|
||
document.title = value;
|
||
} else {
|
||
el.textContent = value;
|
||
}
|
||
}
|
||
const missing = els.length - pairs.length;
|
||
if (missing > 0) {
|
||
logger.warn('populateCopy: ' + String(missing) +
|
||
' [data-mokosh-key] elements had missing COPY entries');
|
||
}
|
||
}
|
||
|
||
function init(): void {
|
||
populateCopy();
|
||
logger.log('welcome page ready');
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
|
||
10. src/welcome/welcome.css invariant
|
||
======================================
|
||
|
||
KEY INVARIANT (A17 contract): regex /#[0-9a-fA-F]{3,8}/ must match ZERO
|
||
times in welcome.css (NOT welcome-tokens.css; that one is the source).
|
||
Every color = var(--mks-*); every font = var(--mks-font-*). Layout: max-width
|
||
via var(--mks-welcome-max-w). BEM-ish class names match the HTML.
|
||
|
||
11. Harness A15-A17 architecture
|
||
=================================
|
||
|
||
Pattern mirrors A14 (most recent addition):
|
||
- Page-side: assertA15/A16/A17 in extension-page-harness.ts (added
|
||
to window.__mokoshHarness global surface).
|
||
- Host-side: driveA15/A16/A17 in lib/harness-page-driver.ts —
|
||
standard page.evaluate wrapper.
|
||
- Orchestrator: registered in drivers array in harness.test.ts.
|
||
|
||
A15 — onboarding flag observability:
|
||
Approach: read chrome.storage.local.get(['onboarding-completed',
|
||
'installed-at']) and assert both keys present + correct types.
|
||
Under puppeteer.launch({enableExtensions: [DIST_TEST_DIR]}) with a
|
||
fresh user-data-dir, the extension load IS the install event; the
|
||
helper fires; storage gets the flag set. If the harness's
|
||
launchHarnessBrowser uses a persistent user-data-dir between runs,
|
||
the flag may already be set from a previous run — still GREEN
|
||
(the contract is "first install set the flag", not "this run set
|
||
the flag").
|
||
|
||
A16 — subsequent-install does NOT re-open:
|
||
Approach: snapshot chrome.tabs.query({}) tab URLs BEFORE a 2-second
|
||
settle window. After the settle, snapshot again. Assert no new tab
|
||
URLs containing 'src/welcome/welcome.html' appeared. Combined with
|
||
A15's flag-is-set assertion: A15 proves install handler ran AT LEAST
|
||
ONCE; A16 proves the handler isn't spuriously re-firing.
|
||
|
||
A17 — design-swap-readiness invariant:
|
||
Fetch chrome-extension://<id>/src/welcome/welcome.html via
|
||
fetch(chrome.runtime.getURL('src/welcome/welcome.html')).
|
||
Same for src/welcome/welcome.css and src/welcome/copy.ts (the
|
||
bundled chunk path; locate via parsing the <script src> attribute
|
||
in welcome.html — Vite's hashed chunk filename).
|
||
Assertions:
|
||
1. welcome.html parses; document.querySelector('.welcome-hero') !== null.
|
||
2. welcome.css text content: NO match for /#[0-9a-fA-F]{3,8}/.
|
||
3. welcome.css text content: AT LEAST 5 occurrences of 'var(--mks-'.
|
||
4. welcome.html text: AT LEAST 7 occurrences of 'data-mokosh-key='.
|
||
5. The bundled welcome.js chunk text content: contains 'COPY['
|
||
substring (proves COPY-map indexing pattern survives the build).
|
||
|
||
No new forbidden-strings entries — A15-A17 use existing chrome.tabs.query
|
||
+ chrome.storage.local.get + fetch on public surfaces (web_accessible_resources).
|
||
The Tier-1 grep gate inventory stays at 10.
|
||
|
||
12. Centralized Logger pattern
|
||
===============================
|
||
|
||
src/shared/logger.ts exports Logger class. Welcome page uses
|
||
new Logger('Welcome'). If the executor prefers, they MAY add a
|
||
WelcomeLogger class mirroring OffscreenLogger / ContentLogger pattern.
|
||
|
||
13. Centralized chrome stub pattern for unit tests
|
||
===================================================
|
||
|
||
tests/background/onstartup-notification.test.ts exemplifies the chrome
|
||
stub used across Plan 01-09 unit tests. Copy the buildBgStub factory +
|
||
extend with:
|
||
- chrome.tabs.create: vi.fn().mockResolvedValue({id:99, url:'x'})
|
||
- chrome.storage.local.{get,set} mocks
|
||
- chrome.runtime.onInstalled rigged with _callbacks array (same idiom
|
||
as onStartup at line 24).
|
||
|
||
Per 01-09 SUMMARY deviation 4: every new listener registration in SW
|
||
MUST be in try/catch. The onInstalled handler is ALREADY registered
|
||
(line 724); this plan ONLY extends its body. The new openWelcomeIfFirstInstall
|
||
invocation gets its own try/catch via fire-and-forget .catch(...) chain.
|
||
</interfaces>
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 1: RED unit tests — onInstalled install creates welcome tab; update does NOT; already-completed flag suppresses; storage key contract.</name>
|
||
<read_first>
|
||
- tests/background/onstartup-notification.test.ts lines 1-235 (chrome stub scaffold + reset+inject pattern)
|
||
- src/background/index.ts lines 724-737 (existing onInstalled handler)
|
||
- manifest.json (confirms storage permission already at line 11)
|
||
</read_first>
|
||
<files>tests/background/onboarding.test.ts</files>
|
||
<behavior>
|
||
Three RED tests pinning truths 1 + 2 + 8 from must_haves:
|
||
|
||
Test A — first install + empty storage:
|
||
chrome.storage.local.get.mockResolvedValue({});
|
||
Fire onInstalled with {reason: 'install'}.
|
||
Assert:
|
||
1. chrome.tabs.create called exactly once with object whose
|
||
url contains 'src/welcome/welcome.html'.
|
||
2. chrome.storage.local.set called with object containing
|
||
{'onboarding-completed': true} AND {'installed-at': <number>}.
|
||
3. chrome.storage.local.get called with EXACT key
|
||
'onboarding-completed' (pins schema for cross-version compat;
|
||
preserves the I-02 lesson from prior plan draft).
|
||
|
||
Test B — subsequent install (reason='update'):
|
||
chrome.storage.local.get.mockResolvedValue({});
|
||
Fire onInstalled with {reason: 'update'}.
|
||
Assert chrome.tabs.create NOT called AND chrome.storage.local.set
|
||
NOT called.
|
||
|
||
Test C — install but flag already set:
|
||
chrome.storage.local.get.mockResolvedValue(
|
||
{'onboarding-completed': true});
|
||
Fire onInstalled with {reason: 'install'}.
|
||
Assert chrome.tabs.create NOT called.
|
||
</behavior>
|
||
<action>
|
||
1. Create tests/background/onboarding.test.ts.
|
||
2. Copy the buildBgStub factory + types from
|
||
tests/background/onstartup-notification.test.ts lines 59-102.
|
||
Extend the stub with:
|
||
- chrome.tabs.create: vi.fn().mockResolvedValue({id:99, url:'x'})
|
||
- chrome.storage = { local: { get: vi.fn(), set: vi.fn() } }
|
||
- chrome.runtime.onInstalled = { addListener: vi.fn(),
|
||
_callbacks: [] }, with addListener.mockImplementation
|
||
pushing into _callbacks
|
||
3. Three tests inside describe('Plan 01-10 onboarding contract', ...)
|
||
— beforeEach { vi.resetModules() }; pattern:
|
||
const stub = buildBgStub();
|
||
stub.storage.local.get.mockResolvedValue(<per test>);
|
||
(globalThis as ...).chrome = stub;
|
||
(globalThis as ...).indexedDB = { deleteDatabase: vi.fn() };
|
||
await import('../../src/background/index');
|
||
const cb = stub.runtime.onInstalled._callbacks[0];
|
||
expect(cb).toBeTypeOf('function');
|
||
cb({ reason: '<per test>' });
|
||
for (let i = 0; i < 16; i++) { await Promise.resolve(); }
|
||
<assertions per behavior>
|
||
4. Run npx vitest run tests/background/onboarding.test.ts — all 3
|
||
MUST be RED (openWelcomeIfFirstInstall helper does not exist;
|
||
chrome.tabs.create never called).
|
||
5. DO NOT modify src/background/index.ts. RED only.
|
||
|
||
Style/naming: full-word variable names; no `as any` (use the typed
|
||
extension); no `continue` in test loops (drain via for-i).
|
||
|
||
Commit (using Mark's em-dash + Co-Authored-By style):
|
||
test(01-10): wave-0 task-1 — RED onboarding tests (3 failing, pins install/update/flag + storage-key)
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
</action>
|
||
<verify>
|
||
<automated>npx vitest run tests/background/onboarding.test.ts 2>&1 | grep -E "3 failed" || (echo "expected 3 failed"; exit 1)</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- tests/background/onboarding.test.ts exists with exactly 3 tests.
|
||
- All 3 tests RED.
|
||
- npx tsc --noEmit exit 0.
|
||
- Baseline (98 passing after 01-13) preserved; new state 98 + 3 RED = 101 total.
|
||
- No changes to src/background/index.ts.
|
||
</acceptance_criteria>
|
||
<done>3 RED tests pin onInstalled welcome-tab + storage-flag contract; baseline preserved; commit landed.</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 2: Welcome page bundle (welcome.html + welcome.ts + welcome.css + welcome-tokens.css + copy.ts) + Vite entries in both bundles + manifest web_accessible_resources.</name>
|
||
<read_first>
|
||
- src/popup/index.html (style analog — lang=ru + module script pattern)
|
||
- src/popup/style.css (sizing reference — DO NOT inherit hex literals)
|
||
- vite.config.ts lines 52-55 (rollupOptions.input current shape)
|
||
- vite.test.config.ts lines 82-92 (mirrored input shape)
|
||
- manifest.json (insertion point for web_accessible_resources)
|
||
- .planning/intel/brand-decisions-v1.md §D-02 (welcome layout: Hero + Loom dial)
|
||
- .planning/intel/brand-decisions-v1.md §D-08 (tagline RU+EN)
|
||
- .planning/intel/brand-decisions-v1.md §D-03 (Sober voice register)
|
||
- .planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css (semantic-name reference)
|
||
- src/shared/logger.ts (Logger class shape)
|
||
</read_first>
|
||
<files>
|
||
src/welcome/welcome.html, src/welcome/welcome.ts, src/welcome/welcome.css,
|
||
src/welcome/welcome-tokens.css, src/welcome/copy.ts,
|
||
vite.config.ts, vite.test.config.ts, manifest.json
|
||
</files>
|
||
<action>
|
||
1. Create src/welcome/copy.ts per interfaces §7. Placeholder Russian
|
||
strings per D-03 Sober (short, declarative, period-terminated, no
|
||
superlatives). D-08 tagline strings LOCKED verbatim. Keys STABLE.
|
||
|
||
2. Create src/welcome/welcome-tokens.css per interfaces §8. Comment
|
||
header cites Plan 01-12 as the replacement moment + brand-decisions-v1.md
|
||
→ tokens.css as canonical source. Use EXACT --mks-* names from
|
||
tokens.css §Surface / §Foreground / §Accents / §Type / §Layout.
|
||
|
||
3. Create src/welcome/welcome.html per interfaces §6 — semantic
|
||
sections (welcome-hero / welcome-body / welcome-footer); every
|
||
textContent target carries data-mokosh-key=stable-key; mark slot
|
||
is data-mokosh-slot=mark (Plan 01-12 swap target); <title> also
|
||
has data-mokosh-key='welcome.page.title'. Two <link> stylesheet
|
||
tags: welcome-tokens.css BEFORE welcome.css (tokens cascade first).
|
||
|
||
4. Create src/welcome/welcome.ts per interfaces §9. Use the
|
||
filter-pipeline form (no `continue`). Import Logger from
|
||
'../shared/logger' + COPY from './copy'. document.readyState
|
||
guard: init immediately if already loaded.
|
||
|
||
5. Create src/welcome/welcome.css per interfaces §10. KEY INVARIANT:
|
||
grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css MUST exit 1
|
||
(no matches). Every color = var(--mks-*). BEM-ish class names
|
||
matching the HTML.
|
||
|
||
6. Update vite.config.ts lines 52-55 — add welcome entry alongside
|
||
offscreen:
|
||
rollupOptions: {
|
||
input: {
|
||
offscreen: 'src/offscreen/index.html',
|
||
welcome: 'src/welcome/welcome.html',
|
||
},
|
||
},
|
||
|
||
7. Update vite.test.config.ts lines 82-92 — mirror the welcome entry
|
||
alongside extension_page_harness:
|
||
rollupOptions: {
|
||
input: {
|
||
extension_page_harness: 'tests/uat/extension-page-harness.html',
|
||
welcome: 'src/welcome/welcome.html',
|
||
},
|
||
},
|
||
|
||
8. Update manifest.json — insert web_accessible_resources AFTER
|
||
host_permissions block (lines 15-17) BEFORE background block
|
||
(lines 18-21). Verify storage still in permissions array line 11.
|
||
The block to insert (proper JSON, comma after host_permissions,
|
||
comma after new block):
|
||
|
||
"web_accessible_resources": [
|
||
{
|
||
"resources": ["src/welcome/welcome.html"],
|
||
"matches": ["<all_urls>"]
|
||
}
|
||
],
|
||
|
||
9. Run npm run build — exit 0; verify:
|
||
- dist/src/welcome/welcome.html exists
|
||
- dist/manifest.json carries the web_accessible_resources block
|
||
- grep -E '#[0-9a-fA-F]{3,8}' dist/src/welcome/welcome.css exits 1
|
||
|
||
10. Run npm run build:test — exit 0; verify dist-test/src/welcome/welcome.html
|
||
exists.
|
||
|
||
11. Run npx tsc --noEmit — exit 0.
|
||
|
||
12. Run npx vitest run — baseline preserved (3 RED from Task 1 stay
|
||
RED; 98 others stay GREEN).
|
||
|
||
Style/naming: full-word names; no `as any`; no `continue` in welcome.ts.
|
||
welcome.html: lang="ru" matches popup precedent. Russian strings live
|
||
ONLY in copy.ts.
|
||
|
||
Commit:
|
||
feat(01-10): wave-1 task-2 — welcome page bundle + Vite entries + web_accessible_resources — design-swap-ready (D-02 + D-08 + D-03; placeholders for D-04 + D-05 per Plan 01-12)
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
</action>
|
||
<verify>
|
||
<automated>npm run build && npx tsc --noEmit && test -f dist/src/welcome/welcome.html && grep -q web_accessible_resources dist/manifest.json && ! grep -E '#[0-9a-fA-F]{3,8}' dist/src/welcome/welcome.css && npm run build:test && test -f dist-test/src/welcome/welcome.html</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- 5 new files exist under src/welcome/.
|
||
- welcome.css ZERO #hex literals (grep -E '#[0-9a-fA-F]{3,8}' exits 1).
|
||
- welcome.html ≥ 7 data-mokosh-key attributes (grep -o count).
|
||
- welcome.ts imports Logger + COPY; NO `as any`; NO `continue`.
|
||
- copy.ts exports frozen Record with stable keys; D-08 tagline verbatim.
|
||
- vite.config.ts + vite.test.config.ts both have welcome rollup entry.
|
||
- manifest.json has web_accessible_resources block; storage permission at line 11.
|
||
- npm run build + npm run build:test clean.
|
||
- npx tsc --noEmit exit 0.
|
||
- Vitest baseline: 3 RED from Task 1 still RED; 98 others GREEN.
|
||
</acceptance_criteria>
|
||
<done>Welcome page bundle staged + Vite picks both bundles + manifest declares page accessible + design-swap-readiness invariants hold in BUILT artifact.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 3: GREEN — openWelcomeIfFirstInstall helper + onInstalled wiring in src/background/index.ts; flips Task 1 tests GREEN.</name>
|
||
<read_first>
|
||
- tests/background/onboarding.test.ts (contracts from Task 1)
|
||
- src/background/index.ts lines 70-90 (existing top-level constants area)
|
||
- src/background/index.ts lines 159-186 (ensureOffscreen pattern)
|
||
- src/background/index.ts lines 724-737 (existing onInstalled handler)
|
||
- .planning/phases/01-stabilize-video-pipeline/01-09-SUMMARY.md "Defense-in-depth chrome.* try/catch" note
|
||
</read_first>
|
||
<files>src/background/index.ts</files>
|
||
<action>
|
||
1. Add top-level constants near badge/notification block at line 73-88:
|
||
|
||
// Plan 01-10 onboarding constants (D-17-onboarding).
|
||
const ONBOARDING_FLAG = 'onboarding-completed';
|
||
const ONBOARDING_INSTALLED_AT = 'installed-at';
|
||
const WELCOME_PATH = 'src/welcome/welcome.html';
|
||
|
||
SCREAMING_SNAKE per project naming rule for true constants.
|
||
|
||
2. Add openWelcomeIfFirstInstall helper just below ensureOffscreen
|
||
(line ~186). Full JSDoc citing Plan 01-10 D-17-onboarding (CONTEXT.md
|
||
line 537+; the SUFFIX disambiguates from D-17-port-lifecycle per
|
||
CONTEXT.md lines 540-545). Defense-in-depth try/catch wraps the body:
|
||
|
||
/**
|
||
* Open the welcome page on first install (Plan 01-10 D-17-onboarding).
|
||
*
|
||
* Trigger conditions (all must hold):
|
||
* - details.reason === 'install' (NOT 'update' / 'chrome_update' /
|
||
* 'shared_module_update');
|
||
* - chrome.storage.local key 'onboarding-completed' NOT === true.
|
||
*
|
||
* Side effects:
|
||
* - chrome.tabs.create({url: chrome.runtime.getURL('src/welcome/welcome.html')})
|
||
* - chrome.storage.local.set({'onboarding-completed': true,
|
||
* 'installed-at': Date.now()})
|
||
*
|
||
* Failure mode: any thrown chrome.* call caught and logged via
|
||
* logger.warn. Welcome tab failing is NOT fatal — toolbar onClicked
|
||
* (D-16-toolbar) remains the operator's start path.
|
||
*/
|
||
async function openWelcomeIfFirstInstall(
|
||
details: chrome.runtime.InstalledDetails,
|
||
): Promise<void> {
|
||
if (details.reason !== 'install') {
|
||
return;
|
||
}
|
||
try {
|
||
const stored = await chrome.storage.local.get(ONBOARDING_FLAG);
|
||
if (stored[ONBOARDING_FLAG] === true) {
|
||
logger.log('Onboarding already completed; skipping welcome tab.');
|
||
return;
|
||
}
|
||
const url = chrome.runtime.getURL(WELCOME_PATH);
|
||
await chrome.tabs.create({ url });
|
||
await chrome.storage.local.set({
|
||
[ONBOARDING_FLAG]: true,
|
||
[ONBOARDING_INSTALLED_AT]: Date.now(),
|
||
});
|
||
logger.log(
|
||
'Welcome tab opened (D-17-onboarding); onboarding flag set.',
|
||
);
|
||
} catch (err) {
|
||
logger.warn('openWelcomeIfFirstInstall failed:', err);
|
||
}
|
||
}
|
||
|
||
3. Extend the onInstalled handler at line 724. Fire-and-forget call
|
||
AFTER initialize(); .catch() so rejected Promise cannot escape
|
||
the synchronous listener boundary:
|
||
|
||
chrome.runtime.onInstalled.addListener((details) => {
|
||
logger.log('Extension installed/updated:', details.reason);
|
||
try {
|
||
indexedDB.deleteDatabase('VideoRecorderDB');
|
||
logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
|
||
} catch (e) {
|
||
logger.warn('IDB cleanup failed:', e);
|
||
}
|
||
initialize();
|
||
// Plan 01-10 D-17-onboarding: open welcome tab on first install.
|
||
// Fire-and-forget — helper logs its own errors.
|
||
openWelcomeIfFirstInstall(details).catch((err) => {
|
||
logger.warn('openWelcomeIfFirstInstall threw:', err);
|
||
});
|
||
});
|
||
|
||
4. Run npx vitest run tests/background/onboarding.test.ts — all 3 RED
|
||
MUST flip GREEN.
|
||
|
||
5. Run full vitest suite — baseline 98 + 3 new = 101 GREEN; zero RED.
|
||
|
||
6. Run npx tsc --noEmit — exit 0. If type complaint on
|
||
chrome.runtime.InstalledDetails, narrow parameter to { reason: string }.
|
||
|
||
7. Run npm run build + npm run build:test — both exit 0.
|
||
|
||
Style/naming pre-commit:
|
||
- No `as any` (chrome.runtime.InstalledDetails type or { reason: string } narrow)
|
||
- No `continue` (if-else early-return only)
|
||
- No new dependencies
|
||
- No edits outside src/background/index.ts
|
||
|
||
Commit:
|
||
feat(01-10): wave-2 task-3 — openWelcomeIfFirstInstall helper + onInstalled wiring (D-17-onboarding) — 3 RED → GREEN
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
</action>
|
||
<verify>
|
||
<automated>npx vitest run tests/background/onboarding.test.ts 2>&1 | grep -E "3 passed" && npx tsc --noEmit && npm run build && npm run build:test</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- openWelcomeIfFirstInstall helper exists with documented behavior.
|
||
- onInstalled handler invokes helper after initialize().
|
||
- All 3 onboarding tests GREEN.
|
||
- Full suite 98 + 3 = 101 GREEN.
|
||
- npx tsc --noEmit exit 0.
|
||
- npm run build + build:test exit 0.
|
||
- Tier-1 grep gate (tests/background/no-test-hooks-in-prod-bundle.test.ts)
|
||
stays GREEN — no new forbidden strings introduced.
|
||
</acceptance_criteria>
|
||
<done>onInstalled extended; SW handler tests GREEN; welcome flow wired end-to-end at the SW layer.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 4: Harness A15+A16+A17 extension — page-side asserts in extension-page-harness.ts, host-side drivers in harness-page-driver.ts, registration in harness.test.ts. npm run test:uat 18/18 GREEN.</name>
|
||
<read_first>
|
||
- tests/uat/extension-page-harness.ts lines 1820-1966 (assertA14 + window.__mokoshHarness install pattern)
|
||
- tests/uat/lib/harness-page-driver.ts lines 971-1000 (driveA14 standard wrapper)
|
||
- tests/uat/harness.test.ts lines 49-77 + 252-313 (driver imports + drivers array)
|
||
- tests/background/no-test-hooks-in-prod-bundle.test.ts lines 105-116 (FORBIDDEN_HOOK_STRINGS — DO NOT extend; A15-A17 use public APIs)
|
||
- src/welcome/welcome.html (the artifact A17 fetches + parses)
|
||
- dist-test/src/welcome/welcome.html (the BUILT artifact A15/A16/A17 actually navigate against)
|
||
</read_first>
|
||
<files>
|
||
tests/uat/extension-page-harness.ts,
|
||
tests/uat/lib/harness-page-driver.ts,
|
||
tests/uat/harness.test.ts
|
||
</files>
|
||
<action>
|
||
1. **Page-side (extension-page-harness.ts)**: append three new assertion
|
||
methods AFTER assertA14 (~line 1905, before the window.__mokoshHarness
|
||
installation block at line 1920).
|
||
|
||
assertA15 — onboarding flag observability:
|
||
async function assertA15(): Promise<AssertionResult> {
|
||
const result: AssertionResult = {
|
||
passed: false,
|
||
name: "A15 — onboarding flag set on first install (chrome.storage.local 'onboarding-completed' === true; 'installed-at' is number)",
|
||
checks: [],
|
||
diagnostics: [],
|
||
};
|
||
try {
|
||
const stored = await chrome.storage.local.get([
|
||
'onboarding-completed', 'installed-at',
|
||
]);
|
||
diag(result, 'storage.local read: ' + JSON.stringify(stored));
|
||
result.checks.push({
|
||
name: 'A15.1: onboarding-completed === true',
|
||
expected: true,
|
||
actual: stored['onboarding-completed'],
|
||
passed: stored['onboarding-completed'] === true,
|
||
});
|
||
result.checks.push({
|
||
name: 'A15.2: installed-at is a number',
|
||
expected: 'number',
|
||
actual: typeof stored['installed-at'],
|
||
passed: typeof stored['installed-at'] === 'number',
|
||
});
|
||
result.passed = result.checks.every((c) => c.passed);
|
||
} catch (err) {
|
||
result.error = err instanceof Error ? err.message : String(err);
|
||
diag(result, 'THREW: ' + result.error);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
assertA16 — subsequent-install does NOT re-open:
|
||
async function assertA16(): Promise<AssertionResult> {
|
||
const result: AssertionResult = {
|
||
passed: false,
|
||
name: 'A16 — subsequent install does NOT re-open welcome tab (2s settle: no new welcome.html tabs appear)',
|
||
checks: [],
|
||
diagnostics: [],
|
||
};
|
||
try {
|
||
const welcomeUrlSuffix = 'src/welcome/welcome.html';
|
||
const tabsBefore = await chrome.tabs.query({});
|
||
const beforeCount = tabsBefore.filter(
|
||
(t) => typeof t.url === 'string' &&
|
||
t.url.endsWith(welcomeUrlSuffix),
|
||
).length;
|
||
diag(result, 'A16: welcome tabs before settle: ' + String(beforeCount));
|
||
await new Promise((r) => setTimeout(r, 2000));
|
||
const tabsAfter = await chrome.tabs.query({});
|
||
const afterCount = tabsAfter.filter(
|
||
(t) => typeof t.url === 'string' &&
|
||
t.url.endsWith(welcomeUrlSuffix),
|
||
).length;
|
||
diag(result, 'A16: welcome tabs after settle: ' + String(afterCount));
|
||
const delta = afterCount - beforeCount;
|
||
result.checks.push({
|
||
name: 'A16.1: welcome-tab delta over 2s settle is 0 (onInstalled flag-gating works)',
|
||
expected: 0,
|
||
actual: delta,
|
||
passed: delta === 0,
|
||
});
|
||
result.passed = result.checks.every((c) => c.passed);
|
||
} catch (err) {
|
||
result.error = err instanceof Error ? err.message : String(err);
|
||
diag(result, 'THREW: ' + result.error);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
assertA17 — design-swap-readiness invariant:
|
||
async function assertA17(): Promise<AssertionResult> {
|
||
const result: AssertionResult = {
|
||
passed: false,
|
||
name: 'A17 — design-swap-readiness: welcome.html parses; .welcome-hero exists; welcome.css zero hex literals; ≥7 data-mokosh-key; bundled JS contains COPY[',
|
||
checks: [],
|
||
diagnostics: [],
|
||
};
|
||
try {
|
||
const welcomeUrl = chrome.runtime.getURL('src/welcome/welcome.html');
|
||
const htmlRes = await fetch(welcomeUrl);
|
||
const htmlText = await htmlRes.text();
|
||
const parsed = new DOMParser().parseFromString(htmlText, 'text/html');
|
||
const hero = parsed.querySelector('.welcome-hero');
|
||
result.checks.push({
|
||
name: 'A17.1: welcome.html parses + .welcome-hero exists',
|
||
expected: 'truthy',
|
||
actual: hero === null ? 'null' : 'element',
|
||
passed: hero !== null,
|
||
});
|
||
const keyMatches = htmlText.match(/data-mokosh-key=/g) ?? [];
|
||
result.checks.push({
|
||
name: 'A17.2: welcome.html has ≥ 7 data-mokosh-key attributes',
|
||
expected: '≥ 7',
|
||
actual: String(keyMatches.length),
|
||
passed: keyMatches.length >= 7,
|
||
});
|
||
const cssUrl = chrome.runtime.getURL('src/welcome/welcome.css');
|
||
const cssRes = await fetch(cssUrl);
|
||
const cssText = await cssRes.text();
|
||
const hexMatches = cssText.match(/#[0-9a-fA-F]{3,8}/g) ?? [];
|
||
result.checks.push({
|
||
name: 'A17.3: welcome.css contains ZERO hex literals (var(--mks-*) only)',
|
||
expected: 0,
|
||
actual: hexMatches.length,
|
||
passed: hexMatches.length === 0,
|
||
});
|
||
const varMatches = cssText.match(/var\(--mks-/g) ?? [];
|
||
result.checks.push({
|
||
name: 'A17.4: welcome.css contains ≥ 5 var(--mks- references',
|
||
expected: '≥ 5',
|
||
actual: String(varMatches.length),
|
||
passed: varMatches.length >= 5,
|
||
});
|
||
// Locate the bundled welcome JS chunk via <script src> in the
|
||
// parsed HTML. Vite emits a hashed chunk name; we fetch the
|
||
// referenced path relative to welcome.html.
|
||
const scriptEl = parsed.querySelector(
|
||
'script[type="module"][src]',
|
||
);
|
||
const scriptSrc = scriptEl?.getAttribute('src') ?? '';
|
||
const baseUrl = new URL(welcomeUrl);
|
||
const jsUrl = new URL(scriptSrc, baseUrl).href;
|
||
const jsRes = await fetch(jsUrl);
|
||
const jsText = await jsRes.text();
|
||
// The bundled chunk minifies but the COPY[ access pattern
|
||
// survives (subscript access is preserved by Vite's minifier
|
||
// because copy.ts uses a Readonly<Record> indexed by string
|
||
// keys).
|
||
result.checks.push({
|
||
name: 'A17.5: bundled welcome JS chunk contains COPY[ subscript pattern',
|
||
expected: 'contains "COPY["',
|
||
actual: jsText.includes('COPY[') ? 'present' : 'absent',
|
||
passed: jsText.includes('COPY['),
|
||
});
|
||
result.passed = result.checks.every((c) => c.passed);
|
||
diag(result, 'A17: ' + result.checks.filter((c) => c.passed).length + '/' + result.checks.length + ' subchecks passed');
|
||
} catch (err) {
|
||
result.error = err instanceof Error ? err.message : String(err);
|
||
diag(result, 'THREW: ' + result.error);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
NOTE A17.5 caveat: if the executor finds that Vite's minifier
|
||
transforms `COPY[` (e.g., inlines the Object.freeze entries),
|
||
relax to `var(--mks-` substring in welcome.css OR change A17.5
|
||
to assert that the bundled JS contains AT LEAST ONE of the
|
||
D-08 tagline strings verbatim — that string is uniquely identifiable
|
||
and survives minification. The check is "COPY data flows through
|
||
the bundle"; the literal-string-check fallback proves it equally well.
|
||
|
||
2. Update the global type declaration at line 1921+:
|
||
declare global {
|
||
interface Window {
|
||
__mokoshHarness: {
|
||
// ... existing assertA1..A14, getManifestVersion ...
|
||
assertA15: () => Promise<AssertionResult>;
|
||
assertA16: () => Promise<AssertionResult>;
|
||
assertA17: () => Promise<AssertionResult>;
|
||
};
|
||
}
|
||
}
|
||
window.__mokoshHarness = {
|
||
// ... existing ...
|
||
assertA15, assertA16, assertA17,
|
||
};
|
||
|
||
3. Update the page-ready log line at line 1966 to mention A17.
|
||
|
||
4. **Host-side (lib/harness-page-driver.ts)**: append three new
|
||
drivers AFTER driveA14 (~line 991):
|
||
|
||
export async function driveA15(page: Page): Promise<AssertionRecord> {
|
||
return await page.evaluate(async () => {
|
||
const harness = (window as any).__mokoshHarness;
|
||
const r: AssertionRecord = await harness.assertA15();
|
||
return r;
|
||
}) as AssertionRecord;
|
||
}
|
||
export async function driveA16(page: Page): Promise<AssertionRecord> {
|
||
return await page.evaluate(async () => {
|
||
const harness = (window as any).__mokoshHarness;
|
||
const r: AssertionRecord = await harness.assertA16();
|
||
return r;
|
||
}) as AssertionRecord;
|
||
}
|
||
export async function driveA17(page: Page): Promise<AssertionRecord> {
|
||
return await page.evaluate(async () => {
|
||
const harness = (window as any).__mokoshHarness;
|
||
const r: AssertionRecord = await harness.assertA17();
|
||
return r;
|
||
}) as AssertionRecord;
|
||
}
|
||
|
||
Re-add the same eslint-disable comment on the `as any` line that
|
||
driveA14 carries (line 985). Standard wrapper — no host-side fs
|
||
polling needed (these are read-only assertions on the SW state +
|
||
fetched static assets).
|
||
|
||
5. **Orchestrator (harness.test.ts)**: update the imports at line 55+:
|
||
import {
|
||
driveA1, ..., driveA14, driveA15, driveA16, driveA17, getManifestVersion,
|
||
} from './lib/harness-page-driver';
|
||
|
||
Update the drivers array at line 289+ to append A15+A16+A17:
|
||
{ name: 'A14', drive: driveA14 },
|
||
{ name: 'A15', drive: driveA15 },
|
||
{ name: 'A16', drive: driveA16 },
|
||
{ name: 'A17', drive: driveA17 },
|
||
|
||
6. Run npm run build:test — exit 0 (rebuilds dist-test with the new
|
||
page-side asserts).
|
||
|
||
7. Run npm run test:uat — bail-on-first-failure orchestrator now
|
||
executes 18 assertions. Expected output: "UAT harness: 18/18
|
||
assertions passed". If any A15/A16/A17 fails, surface the
|
||
diagnostic; iterate within this task.
|
||
|
||
8. Run full vitest suite — 101 GREEN preserved (no source under test
|
||
changed; the harness-page changes are dist-test/ only).
|
||
|
||
9. Run npx tsc --noEmit — exit 0.
|
||
|
||
10. Verify Tier-1 grep gate stays at 10 strings:
|
||
npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts
|
||
— all 11 sub-tests GREEN (1 build + 10 string assertions). No
|
||
new forbidden strings; the inventory is unchanged.
|
||
|
||
Style/naming pre-commit:
|
||
- No new `__mokoshTest`/`__MOKOSH_UAT__` test-hook surface
|
||
(A15-A17 use only chrome.tabs.query / chrome.storage.local.get /
|
||
fetch on public web_accessible_resources surfaces).
|
||
- No new entries in FORBIDDEN_HOOK_STRINGS.
|
||
- `as any` only inside the page.evaluate wrapper (eslint-disable
|
||
line comment matches driveA14 idiom).
|
||
|
||
Commit:
|
||
test(01-10): wave-3 task-4 — harness A15+A16+A17 (onboarding flag observability + no-re-open settle check + design-swap-readiness invariant); 18/18 GREEN
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
</action>
|
||
<verify>
|
||
<automated>npm run build:test && npm run test:uat 2>&1 | grep -E "UAT harness: 18/18" && npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts && npx tsc --noEmit</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- assertA15, assertA16, assertA17 exist in extension-page-harness.ts.
|
||
- window.__mokoshHarness exposes all three new methods.
|
||
- driveA15, driveA16, driveA17 exist in lib/harness-page-driver.ts.
|
||
- harness.test.ts drivers array includes A15, A16, A17 entries.
|
||
- npm run test:uat reports 18/18 assertions passed.
|
||
- Tier-1 forbidden-strings inventory unchanged at 10 (no new test-hook surface).
|
||
- npm test (vitest) baseline 101 GREEN preserved.
|
||
- npx tsc --noEmit clean.
|
||
</acceptance_criteria>
|
||
<done>Harness covers welcome-tab activation + design-swap-readiness; Plan 01-12 design swap is now testable as a one-commit delta.</done>
|
||
</task>
|
||
|
||
<task type="checkpoint:human-verify" gate="blocking">
|
||
<name>Task 5: Operator empirical UAT — fresh-profile install opens welcome tab; placeholders render coherently; reload does NOT re-open; harness 18/18 GREEN.</name>
|
||
<files>(operator-driven; no specific source file modified)</files>
|
||
<action>See <how-to-verify> below — operator-driven empirical check.</action>
|
||
<verify>
|
||
<automated>echo "checkpoint:human-verify — see how-to-verify; resume signal is the gate"</automated>
|
||
</verify>
|
||
<done>Operator types "approved" after running the how-to-verify steps. See <resume-signal> for the gate.</done>
|
||
<what-built>
|
||
Tasks 1-4 landed:
|
||
- src/welcome/* page bundle (5 files: welcome.html, welcome.ts, welcome.css, welcome-tokens.css, copy.ts).
|
||
- manifest.json web_accessible_resources for welcome.html.
|
||
- vite.config.ts + vite.test.config.ts rollupOptions.input welcome entry in BOTH bundles.
|
||
- src/background/index.ts openWelcomeIfFirstInstall helper + onInstalled wiring.
|
||
- tests/background/onboarding.test.ts 3 GREEN unit tests.
|
||
- tests/uat/* harness A15+A16+A17 extensions; 18/18 GREEN.
|
||
- Tier-1 forbidden-strings inventory unchanged at 10.
|
||
- vitest baseline 101 GREEN (98 from 01-13 + 3 new from this plan).
|
||
|
||
This checkpoint validates real Chrome behavior + brand placeholder
|
||
coherence (the design-swap-in-ready architecture should render
|
||
presentably even with engineering-grade placeholders; Plan 01-12
|
||
will swap to the canonical D-04 Loom palette + D-05 Newsreader +
|
||
IBM Plex Sans WOFF2 hosting).
|
||
</what-built>
|
||
<how-to-verify>
|
||
Pre-checkpoint bundle gates (operator OR executor verifies before
|
||
surfacing this checkpoint — per the orchestrator-loaded
|
||
feedback-pre-checkpoint-bundle-gates.md memory):
|
||
|
||
1. npm run build — exit 0; dist/src/welcome/welcome.html exists.
|
||
2. npm run build:test — exit 0.
|
||
3. grep -E '#[0-9a-fA-F]{3,8}' dist/src/welcome/welcome.css — exit 1
|
||
(no matches — design-swap-readiness invariant holds in BUILT artifact).
|
||
4. grep -q web_accessible_resources dist/manifest.json — exit 0.
|
||
5. npx vitest run — 101 passed.
|
||
6. npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts —
|
||
all GREEN; 10 forbidden strings absent from dist/.
|
||
7. npm run test:uat — 18/18 GREEN.
|
||
|
||
Operator empirical steps:
|
||
|
||
1. Verify dist/ is fresh: npm run build (exit 0).
|
||
2. Wipe smoke profile: rm -rf /tmp/mokosh-smoke-profile (or
|
||
KEEP_PROFILE=0 ./smoke.sh which does the wipe).
|
||
3. Launch fresh-profile Chrome: KEEP_PROFILE=0 ./smoke.sh.
|
||
4. chrome://extensions → Load Unpacked → select dist/. THE WELCOME
|
||
TAB SHOULD AUTOMATICALLY OPEN within ~1 second. URL should be
|
||
chrome-extension://<id>/src/welcome/welcome.html.
|
||
5. Verify the welcome page renders coherently:
|
||
- Centered card, max-width ~720px.
|
||
- Hero block at top: 'Mokosh' placeholder mark + 'Mokosh' H1 + RU tagline
|
||
'«Тридцать секунд назад, всегда под рукой.»' + EN tagline
|
||
'Thirty seconds ago, always at hand.' in italic serif.
|
||
- Body: 2 explainer lines about 30s screen + 10min logs +
|
||
local-only data + how to save archive.
|
||
- CTA box: 'Чтобы начать запись, нажмите иконку AI Call Recorder
|
||
на панели инструментов браузера (правый верхний угол).'
|
||
- Footer: privacy/safety note about no-server-uploads.
|
||
- Typography: serif for hero (system fallback today; Plan 01-12
|
||
swaps to Newsreader/PT-Serif WOFF2); sans for body.
|
||
- NO console errors in the welcome tab's DevTools console.
|
||
- NO inline "Start" button (D-16-toolbar charter — the welcome
|
||
tab is informational; toolbar owns start).
|
||
6. Close the welcome tab.
|
||
7. Click the AI Call Recorder toolbar icon (right-side toolbar).
|
||
Chrome's screen-share picker should appear (monitor-only per
|
||
01-09). Pick 'Entire screen' + accept. Badge transitions to
|
||
REC. (This validates that the welcome tab CTA accurately
|
||
describes the toolbar start path.)
|
||
8. Click the toolbar again (now in REC state). The popup opens
|
||
with the SAVE-only UI per 01-09 SAVE-only charter; a zip is
|
||
downloaded. Badge returns to OFF (per Plan 01-13 save-stops-
|
||
recording charter).
|
||
9. Reload the extension at chrome://extensions (toggle off + on).
|
||
Observe: the welcome tab does NOT re-open. This validates
|
||
A16's flag-gating contract.
|
||
10. Re-validate the install branch: Cmd+Q Chrome →
|
||
rm -rf /tmp/mokosh-smoke-profile → re-launch smoke.sh →
|
||
Load Unpacked again. Welcome tab opens again (chrome.storage.local
|
||
wiped with profile → onboarding-completed absent → first-install
|
||
path fires).
|
||
|
||
If step 4 (welcome tab opens), step 5 (page renders coherently),
|
||
step 7 (toolbar CTA validates), or step 9 (no re-open on reload)
|
||
fails: document the failure mode + Chrome version + SW console
|
||
errors. Iterate on Task 2 (assets) or Task 3 (SW handler) or
|
||
Task 4 (harness) accordingly.
|
||
|
||
Design-coherence note for operator:
|
||
The welcome page renders with engineering-grade placeholders TODAY
|
||
— system serif/sans, neutral grays. This is INTENDED. Plan 01-12
|
||
swaps welcome-tokens.css for an @import of tokens.css + bundles
|
||
Newsreader (or designer-picked Cyrillic-capable alternative per
|
||
brand-decisions-v1-followup-display-font.md) and the welcome page
|
||
transitions to the canonical D-04 Loom palette + D-05 type pairing
|
||
without any other code change. The harness A17 contract pins the
|
||
invariants that make this swap mechanical.
|
||
</how-to-verify>
|
||
<resume-signal>
|
||
Type 'approved' after pre-checkpoint gates 1-7 ALL pass AND operator
|
||
steps 4, 5, 7, 9 ALL pass. If any step fails, paste the failure
|
||
diagnostic. The harness 18/18 GREEN is the canonical functional gate;
|
||
operator empirical is the brand-coherence + Chrome-empirical gate.
|
||
</resume-signal>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<threat_model>
|
||
## Trust Boundaries
|
||
|
||
| Boundary | Description |
|
||
|----------|-------------|
|
||
| welcome page (extension-page context) ↔ SW | Welcome page is a same-origin extension page (chrome-extension://<id>/src/welcome/welcome.html). It does NOT call chrome.runtime.sendMessage in this plan — it is informational + read-only. No new IPC boundary introduced. |
|
||
| chrome.storage.local ↔ SW | Storage flag is non-secret (boolean + timestamp); the worst case if leaked or tampered is suppressing/re-firing the welcome tab. No PII; no sensitive content. |
|
||
| chrome.runtime.onInstalled ↔ SW | Existing Chrome MV3 boundary; this plan EXTENDS the handler body but does not introduce a new listener registration. |
|
||
| web_accessible_resources ↔ external pages | welcome.html is enumerable by any page that probes chrome-extension:// URLs in web_accessible_resources. Existing extension-fingerprinting concern; matches=['<all_urls>'] is the standard pattern for welcome flows. |
|
||
|
||
## STRIDE Threat Register
|
||
|
||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||
|-----------|----------|-----------|-------------|-----------------|
|
||
| T-1-10-01 | Tampering | Adversary clears chrome.storage.local 'onboarding-completed' flag to spam welcome tabs across SW respawns | accept | Welcome tab is non-destructive; worst case is an extra tab on each install (NOT each SW respawn — onInstalled only fires on install/update/chrome_update). chrome.storage.local clearing requires either operator action via Chrome DevTools OR an adversarial extension installed alongside — both out-of-band. No data exfiltration path. |
|
||
| T-1-10-02 | Information Disclosure | welcome.html fingerprinting via web_accessible_resources enumeration | accept | Extension identifier is already discoverable through chrome.runtime.getURL exposure on any extension page (popup + offscreen + harness page); matches=['<all_urls>'] is the standard MV3 welcome-flow pattern. Phase 5 hardening could narrow matches to a specific install-confirmation domain; out of scope. |
|
||
| T-1-10-03 | Denial of Service | Adversary controls chrome.storage.local quota | mitigate | Two values stored (boolean true + Date.now() number) ~80 bytes; storage.local quota is 10 MB; not exploitable. |
|
||
| T-1-10-04 | Elevation of Privilege | Welcome page tricks SW into bypassing checkpoints | accept | Welcome page sends NO messages to the SW in this plan (informational only). No new EoP path. Plan 01-12 may add an i18n probe via chrome.i18n.getMessage (synchronous, no IPC) — also no EoP path. |
|
||
| T-1-10-05 | Information Disclosure | Russian copy strings leak via web_accessible_resources | accept | Copy strings are operator-facing public marketing content (D-08 tagline; D-03 voice register); no PII. |
|
||
| T-1-10-06 | Tampering | Adversary modifies copy.ts contents via supply-chain attack on the source bundle | accept | Generic supply-chain risk applying to every TS file in the project; outside this plan's scope. The Tier-1 grep gate prevents test-hook leaks; brand-copy integrity is a Phase 5 / brand-team concern. |
|
||
| T-1-10-07 | Tampering | Adversary modifies welcome-tokens.css to inject malicious styles (CSP-bypass via background-image: url(data:...)) | mitigate | The token CSS contains ONLY color/font/spacing values — no url() expressions, no @import (except to tokens.css path post Plan 01-12, which is same-origin extension URL). MV3 CSP forbids inline scripts; CSS-only injection cannot escalate. Plan 01-12 swap is from canonical tokens.css delivered by the brand team — supply-chain risk shifts to brand-team handoff verification. |
|
||
</threat_model>
|
||
|
||
<verification>
|
||
- npx vitest run shows 101/101 GREEN (98 baseline from 01-13 + 3 new from onboarding.test.ts).
|
||
- npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts: all GREEN; FORBIDDEN_HOOK_STRINGS inventory unchanged at 10.
|
||
- npx tsc --noEmit: exit 0.
|
||
- npm run build: exit 0; dist/src/welcome/welcome.html exists; dist/manifest.json contains web_accessible_resources with welcome.html entry.
|
||
- npm run build:test: exit 0; dist-test/src/welcome/welcome.html exists.
|
||
- grep -E '#[0-9a-fA-F]{3,8}' dist/src/welcome/welcome.css: exit 1 (no matches — design-swap-readiness invariant survives the build pipeline).
|
||
- grep -o 'data-mokosh-key' dist/src/welcome/welcome.html | wc -l ≥ 7.
|
||
- grep -n 'chrome.tabs.create' src/background/index.ts ≥ 1 (the helper's call).
|
||
- grep -n 'welcome.html' manifest.json ≥ 1.
|
||
- grep -n 'openWelcomeIfFirstInstall' src/background/index.ts ≥ 2 (helper definition + call site).
|
||
- grep -n 'D-17-onboarding' src/background/index.ts ≥ 1 (cited in JSDoc).
|
||
- grep -nE 'REQUEST_PERMISSIONS' src/welcome/welcome.ts: exit 1 (no match — welcome tab does NOT reference REQUEST_PERMISSIONS per the deletion in 01-09 + per the D-16-toolbar single-start-path charter).
|
||
- npm run test:uat: "UAT harness: 18/18 assertions passed".
|
||
- Operator empirical steps 4, 5, 7, 9 from how-to-verify: ALL PASS.
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
Plan 01-10 is complete when:
|
||
1. The 3 onboarding unit tests are GREEN; full vitest suite 101/101 GREEN.
|
||
2. UAT harness reports 18/18 GREEN (existing A0..A14 + new A15+A16+A17).
|
||
3. Tier-1 forbidden-strings inventory stays at 10 (no new test-hook surface).
|
||
4. Welcome page renders coherently in fresh-profile Chrome with engineering-grade placeholders (Plan 01-12 swap target).
|
||
5. Design-swap-readiness invariants hold in the BUILT artifact (welcome.css zero hex literals; ≥ 7 data-mokosh-key attributes; bundled JS uses COPY[ subscript pattern).
|
||
6. Operator confirms via how-to-verify steps 4 / 5 / 7 / 9 + the harness 18/18 GREEN gate.
|
||
7. tsc + build + build:test all clean; manifest + vite + welcome assets consistent across both bundles.
|
||
8. No references to REQUEST_PERMISSIONS in src/welcome/ (start path remains toolbar-onClicked-owned per D-16-toolbar).
|
||
9. No `await import(...)` introduced into src/background/index.ts (MV3 SW dynamic-import blocker per 01-11-SUMMARY).
|
||
</success_criteria>
|
||
|
||
<output>
|
||
After completion, create .planning/phases/01-stabilize-video-pipeline/01-10-SUMMARY.md per the standard template. Cite:
|
||
- 3 new onboarding unit tests landed GREEN (Test A / B / C).
|
||
- 5 new src/welcome/* files (welcome.html / welcome.ts / welcome.css / welcome-tokens.css / copy.ts).
|
||
- manifest web_accessible_resources delta.
|
||
- vite.config.ts + vite.test.config.ts rollupOptions.input delta in BOTH bundles.
|
||
- openWelcomeIfFirstInstall helper + onInstalled wiring in src/background/index.ts.
|
||
- Harness A15 + A16 + A17 extensions; 15/15 → 18/18 GREEN delta.
|
||
- Tier-1 forbidden-strings inventory unchanged at 10 (no new test-hook surface).
|
||
- Design-swap-in-ready architecture summary: CSS variables, copy.ts source-of-truth, .welcome-hero slot, var(--mks-font-*) typography fallback chain.
|
||
- Cross-references: D-02 (welcome layout), D-08 (tagline), D-03 (voice register), D-17-onboarding (this plan's amendment marker), D-16-toolbar (start path ownership).
|
||
- Plan 01-12 swap path: replace welcome-tokens.css with @import 'tokens.css' + swap copy.ts to chrome.i18n.getMessage post _locales/ landing + bundle Newsreader (or PT-Serif/EB-Garamond/Lora per the designer follow-up reply, currently outstanding) WOFF2 self-hosted.
|
||
- Operator empirical UAT outcome (welcome tab opens; brand-coherence acceptable for placeholders; reload does NOT re-open; harness 18/18 GREEN).
|
||
</output>
|