Surgical amendment to unexecuted Plan 01-10 absorbing the post-draft landing of Plan 01-12 (canonical src/shared/tokens.css + 16 i18n keys including welcomeHeroRu/welcomeHeroEn; 2026-05-20 operator brand-fit ack) and Plan 01-14 (vitest +2 + UAT +1 + FORBIDDEN_HOOK_STRINGS +2). Baseline shifts: - vitest 98 → 147 GREEN (post Plan 01-12 + 01-14); plan close target 150. - UAT 15 → 21 GREEN (A0-A14 + A18-A22 + A23); plan close target 24 (A0-A14 + A15-A17 + A18-A22 + A23). - FORBIDDEN_HOOK_STRINGS 10 → 12 (Plan 01-14: lastGetDisplayMediaConstraints + get-last-getDisplayMedia-constraints); Plan 01-10 introduces no new test-mode symbols; inventory unchanged at 12. Plan 01-12 must_have #9 path-B contract honored end-to-end (Plan 01-12 landed FIRST, so the welcome page adopts canonical assets directly): - welcome.css opens with `@import '../shared/tokens.css';` (NO placeholder welcome-tokens.css; removed from files_modified). - D-08 hero tagline elements use data-mokosh-i18n-key='welcomeHeroRu' + data-mokosh-i18n-key='welcomeHeroEn'; welcome.ts reads via chrome.i18n.getMessage with `|| <en-const>` fallback per Plan 01-12 fallback pattern. WELCOME_HERO_RU_FALLBACK + WELCOME_HERO_EN_FALLBACK constants exported from copy.ts for the degradation path. - copy.ts COPY map retains non-tagline keys only (page title + explainer lines + CTA + footer privacy; engineering placeholders per D-03). - A17 design-swap-readiness invariant extended with: - A17.5: welcome.css contains canonical @import directive OR inlined `--mks-rec:` evidence; - A17.6: bundled JS contains COPY[ OR chrome.i18n.getMessage('welcomeHero; - A17.7 NEW: getComputedStyle probe on var(--mks-rec) returns non-default value (canonical rgb(178, 84, 61) = #b2543d = --mks-madder-600 per Plan 01-12 Wave 4 D-04 Loom palette adoption). - depends_on extended to [01-09, 01-13, 01-14, 01-12]. Preserved verbatim: 5-task structure, A15/A16 contracts, D-02/D-08/D-09 references, threat model + STRIDE register, operator empirical checkpoint shape, Plan 01-12 default_locale='en' + __MSG_*__ + __VITE_DEV__ + __MOKOSH_UAT__ + src/shared/tokens.css. Validated: gsd-sdk frontmatter.validate + verify.plan-structure both PASS; task_count=5, all tasks complete with files/action/verify/done. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1500 lines
87 KiB
Markdown
1500 lines
87 KiB
Markdown
---
|
||
phase: 01-stabilize-video-pipeline
|
||
plan: 10
|
||
type: execute
|
||
wave: 4
|
||
depends_on:
|
||
- 01-09
|
||
- 01-13
|
||
- 01-14
|
||
- 01-12
|
||
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/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
|
||
- canonical-tokens-import
|
||
- css-variables
|
||
- chrome.i18n-welcomeHero
|
||
- D-17-onboarding
|
||
- D-02-welcome-layout
|
||
- D-08-tagline
|
||
- harness-A15-A17
|
||
- post-01-12-baseline
|
||
- post-01-14-baseline
|
||
|
||
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. welcome.css opens with a one-liner `@import '../shared/tokens.css';` (Plan 01-12 must_have #9 path-B contract honored: Plan 01-12 landed FIRST; canonical src/shared/tokens.css is import-ready; NO placeholder welcome-tokens.css is created)."
|
||
- "The D-08 hero tagline strings render via chrome.i18n.getMessage('welcomeHeroRu') and chrome.i18n.getMessage('welcomeHeroEn') reads at populate time — the canonical keys exist in _locales/{en,ru}/messages.json from Plan 01-12 Wave 3. A `|| <en-const>` fallback per Plan 01-12's chrome.i18n.getMessage fallback pattern is included verbatim at each call site. src/welcome/copy.ts retains the COPY map ONLY for non-tagline keys (page title + explainer lines + CTA + privacy footer) which Plan 01-12 did NOT migrate to _locales/."
|
||
- "Typography uses font-family: var(--mks-font-display, <system fallback>) and var(--mks-font-ui, <system fallback>) — the canonical tokens.css from Plan 01-12 supplies Lora (display) + IBM Plex Sans (UI) via self-hosted @font-face rules; system fallback chain is preserved as graceful degradation."
|
||
- "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 the `@import '../shared/tokens.css';` directive (or equivalent compiled output); 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 EITHER substring 'COPY[' (non-tagline keys) OR chrome.i18n.getMessage('welcomeHero references; getComputedStyle on the .welcome-hero element resolves --mks-rec to '#b2543d' OR equivalent rgb(178,84,61) (proves the @import resolves canonical token values, not just engineering placeholders)."
|
||
- "npm run test:uat reports 24/24 GREEN at plan close (current baseline 21 from A0-A14 + A18-A22 + A23 + new A15 + A16 + A17). npm test (vitest) reports current baseline 147 GREEN + 3 new onboarding tests GREEN = 150 GREEN; Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 entries (10 original Plan 01-13 + 2 Plan 01-14 additions; Plan 01-10 introduces no new test-mode symbols)."
|
||
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'> (future swap to mokosh-lockup.svg). Two hero tagline elements carry data-mokosh-i18n-key='welcomeHeroRu' and data-mokosh-i18n-key='welcomeHeroEn' for the chrome.i18n.getMessage populate path."
|
||
min_lines: 30
|
||
- path: "src/welcome/welcome.ts"
|
||
provides: "Vanilla DOM entry point. Imports COPY from './copy' AND uses chrome.i18n.getMessage for the welcomeHeroRu + welcomeHeroEn keys. On DOMContentLoaded: (a) populates each [data-mokosh-key='<key>'] element's textContent from COPY[key] (non-tagline keys), (b) populates each [data-mokosh-i18n-key='<key>'] element's textContent from chrome.i18n.getMessage(key) || <en-const-fallback>. 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: 30
|
||
- path: "src/welcome/welcome.css"
|
||
provides: "Welcome page styling. FIRST LINE is `@import '../shared/tokens.css';` (Plan 01-12 canonical tokens). Every color references var(--mks-*); every font references var(--mks-font-*). Layout: max-width 720px (matches --mks-welcome-max-w in tokens.css), centered, hero+body+footer vertical stack. ZERO hex literals. Single <link rel=stylesheet> in welcome.html (the @import inside this file resolves the canonical bundle at build time)."
|
||
min_lines: 30
|
||
- path: "src/welcome/copy.ts"
|
||
provides: "Single source-of-truth COPY map for non-tagline strings. Exports const COPY: Readonly<Record<string, string>> keyed by stable identifiers (welcome.page.title, welcome.hero.title, welcome.body.explainer.line1, welcome.body.explainer.line2, welcome.body.cta.toolbar, welcome.footer.privacy). The two D-08 tagline strings (welcomeHeroRu + welcomeHeroEn) are NOT in this map — they live in _locales/{en,ru}/messages.json per Plan 01-12 Wave 3 and are read via chrome.i18n.getMessage at populate time. COPY map can hold en-const fallbacks for the welcomeHero keys as commented constants (so welcome.ts has a graceful-degradation copy-paste path matching Plan 01-12's `|| <en-const>` fallback pattern)."
|
||
min_lines: 25
|
||
- 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. default_locale='en' + __MSG_*__ pattern from Plan 01-12 Wave 3 preserved verbatim."
|
||
contains: "web_accessible_resources"
|
||
- path: "vite.config.ts"
|
||
provides: "rollupOptions.input gains welcome: 'src/welcome/welcome.html' alongside existing offscreen entry. __VITE_DEV__ + __MOKOSH_UAT__ define-tokens from Plan 01-12 Wave 5 preserved verbatim."
|
||
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 @import directive + zero-hex regex + ≥7 data-mokosh-key + bundled JS contains COPY[ OR chrome.i18n.getMessage('welcomeHero substring + getComputedStyle var resolution check)."
|
||
min_lines: 170
|
||
- 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 (interleaved into existing A0-A14 + A18-A22 + A23 ordering). FORBIDDEN_HOOK_STRINGS unchanged at 12 entries. Total assertions become 24 (A0-A14 + A15-A17 + A18-A22 + A23 — A18-A22 from Plan 01-12, A23 from Plan 01-14)."
|
||
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 + chrome.i18n.getMessage"
|
||
via: "import { COPY } from './copy'; populates [data-mokosh-key='<key>'] textContent from COPY[key]; populates [data-mokosh-i18n-key='<key>'] textContent from chrome.i18n.getMessage(key) || <en-const-fallback>"
|
||
pattern: "data-mokosh-key|data-mokosh-i18n-key"
|
||
- from: "src/welcome/welcome.css"
|
||
to: "src/shared/tokens.css"
|
||
via: "Top-of-file @import '../shared/tokens.css'; — canonical Plan 01-12 tokens (Lora @font-face + D-04 Loom palette + --mks-* variable set) cascades into welcome.css scope"
|
||
pattern: "@import.*shared/tokens.css"
|
||
- from: "_locales/{en,ru}/messages.json welcomeHeroRu + welcomeHeroEn"
|
||
to: "src/welcome/welcome.ts populate path"
|
||
via: "chrome.i18n.getMessage('welcomeHeroRu') + chrome.i18n.getMessage('welcomeHeroEn') reads with `|| <en-const>` fallback per Plan 01-12 fallback pattern"
|
||
pattern: "chrome.i18n.getMessage\\('welcomeHero"
|
||
- 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 + assertA18..A22 + assertA23"
|
||
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.
|
||
|
||
**Plan 01-12 landed FIRST (commits 34a9ce1 → b909c37 → 865d394; SUMMARY 2026-05-20).** Per Plan 01-12 must_have #9 path-B contract: canonical src/shared/tokens.css is import-ready (Lora self-hosted, D-04 Loom palette, --mks-* full set); _locales/{en,ru}/messages.json carries the welcomeHeroRu + welcomeHeroEn keys; no placeholder welcome-tokens.css is needed. Plan 01-10 ships welcome.css with `@import '../shared/tokens.css';` from day 1 AND populates the two D-08 tagline elements via chrome.i18n.getMessage with `|| <en-const>` fallback per the Plan 01-12 fallback pattern. Non-tagline copy (page title + explainer lines + CTA + footer privacy) continues via the in-file COPY map (those keys are not in _locales/ — engineering placeholder until a future copy-iteration plan migrates them).
|
||
|
||
**Plan 01-14 also landed (commits b467123 → 9792c0f; SUMMARY 2026-05-19).** Per Plan 01-14: vitest baseline is 147 GREEN (not 98); UAT baseline is 21 GREEN (A0-A14 + A18-A22 + A23); FORBIDDEN_HOOK_STRINGS inventory is 12 entries (10 original + 2 Plan 01-14 additions: `lastGetDisplayMediaConstraints` + `get-last-getDisplayMedia-constraints`). Plan 01-10 introduces ZERO new test-mode symbols at the chrome.* + fetch + getComputedStyle production-API layer A15-A17 uses; the inventory stays at 12.
|
||
|
||
**Harness integration (A15-A17).** Extend the Plan 01-13 + 01-14 + 01-12 harness with three new assertions covering: onboarding flag observability (A15), subsequent-install does NOT re-open (A16), design-swap-readiness invariant (A17 — extended to verify the canonical @import resolves AND --mks-rec resolves to non-empty value via getComputedStyle). Drivers register alongside driveA1..driveA14 + driveA18..driveA22 + driveA23 in tests/uat/harness.test.ts.
|
||
|
||
Output:
|
||
- src/welcome/welcome.{html,ts,css} + src/welcome/copy.ts — 4 new files for the canonical-tokens + chrome.i18n welcome page (NO placeholder welcome-tokens.css; that file is NEVER created per Plan 01-12 path-B).
|
||
- 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), Plan 01-12 must_have #9 path-B (canonical tokens.css @import + chrome.i18n for welcomeHero), Plan 01-14 FORBIDDEN_HOOK_STRINGS baseline at 12.
|
||
</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-12-SUMMARY.md
|
||
@.planning/phases/01-stabilize-video-pipeline/01-13-SUMMARY.md
|
||
@.planning/phases/01-stabilize-video-pipeline/01-14-SUMMARY.md
|
||
@.planning/intel/brand-decisions-v1.md
|
||
@.planning/intel/brand-decisions-v1-followup-display-font.md
|
||
@src/shared/tokens.css
|
||
@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
|
||
@_locales/en/messages.json
|
||
@_locales/ru/messages.json
|
||
@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, before "background"
|
||
block. Verify storage permission still present at line 11. Verify
|
||
default_locale='en' + __MSG_*__ pattern from Plan 01-12 Wave 3
|
||
preserved verbatim.
|
||
|
||
5. vite.config.ts rollupOptions.input
|
||
======================================
|
||
|
||
Current state (post-01-12 Wave 5; vite.config.ts):
|
||
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 so dist-test/ also
|
||
emits the welcome page. The harness A15/A16/A17 navigates against the
|
||
test bundle. Plan 01-12's __VITE_DEV__ + __MOKOSH_UAT__ define-tokens
|
||
must be preserved verbatim — do NOT touch.
|
||
|
||
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.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-i18n-key="welcomeHeroRu"></p>
|
||
<p class="welcome-hero__tagline-en" data-mokosh-i18n-key="welcomeHeroEn"></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>
|
||
|
||
NOTE: ONLY ONE <link rel=stylesheet> tag — welcome.css. The `@import
|
||
'../shared/tokens.css';` at the top of welcome.css resolves the canonical
|
||
tokens during the Vite build (CSS @import is handled by Rollup's CSS
|
||
plugin which inlines or rewrites relative paths). NO placeholder
|
||
welcome-tokens.css link tag.
|
||
|
||
Two new attribute conventions:
|
||
- data-mokosh-key="<copy-map-key>" → populated from src/welcome/copy.ts
|
||
COPY map at populateCopy() time.
|
||
- data-mokosh-i18n-key="<chrome-i18n-key>" → populated from
|
||
chrome.i18n.getMessage(key) || <en-const-fallback> at
|
||
populateI18n() time.
|
||
|
||
7. src/welcome/copy.ts shape
|
||
=============================
|
||
|
||
// src/welcome/copy.ts — single source-of-truth for welcome-page
|
||
// NON-TAGLINE copy. Plan 01-12 migrated the two D-08 tagline strings
|
||
// (welcomeHeroRu + welcomeHeroEn) to _locales/{en,ru}/messages.json;
|
||
// those keys are read via chrome.i18n.getMessage in welcome.ts and
|
||
// intentionally NOT included in this map. Remaining keys are
|
||
// engineering-grade placeholders (Russian per D-03 Sober register);
|
||
// a future copy-iteration plan may migrate them to _locales/.
|
||
//
|
||
// D-08 tagline reference (lives in _locales/, not here):
|
||
// en/welcomeHeroEn → "Thirty seconds ago, always at hand."
|
||
// ru/welcomeHeroRu → "Тридцать секунд назад, всегда под рукой."
|
||
|
||
// Plan 01-12 fallback-pattern constants for the welcomeHero keys.
|
||
// welcome.ts uses these as the `|| <en-const>` fallback when
|
||
// chrome.i18n.getMessage returns empty string (RESEARCH Pitfall 4
|
||
// mitigation). Exported separately from COPY so the i18n populate
|
||
// path imports them by name.
|
||
export const WELCOME_HERO_RU_FALLBACK =
|
||
'Тридцать секунд назад, всегда под рукой.';
|
||
export const WELCOME_HERO_EN_FALLBACK =
|
||
'Thirty seconds ago, always at hand.';
|
||
|
||
export const COPY: Readonly<Record<string, string>> = Object.freeze({
|
||
'welcome.page.title': 'Добро пожаловать в Mokosh',
|
||
'welcome.hero.title': 'Mokosh',
|
||
'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.css shape (canonical @import; NO placeholder file)
|
||
========================================================================
|
||
|
||
/* welcome.css — Plan 01-10 D-17-onboarding welcome page styling.
|
||
* Imports the canonical Plan 01-12 token system (src/shared/tokens.css)
|
||
* which carries the full --mks-* variable set + 8 local @font-face
|
||
* rules + D-04 Loom palette. Every color in this file MUST reference
|
||
* var(--mks-*); ZERO hex literals. */
|
||
|
||
@import '../shared/tokens.css';
|
||
|
||
.welcome {
|
||
max-width: var(--mks-welcome-max-w);
|
||
margin: 0 auto;
|
||
padding: var(--mks-space-12) var(--mks-space-8);
|
||
background: var(--mks-surface);
|
||
color: var(--mks-fg-1);
|
||
font-family: var(--mks-font-ui);
|
||
}
|
||
/* ... remaining rules — every color via var(--mks-*); every font
|
||
* via var(--mks-font-*); no hex literals. */
|
||
|
||
KEY INVARIANT (A17 contract):
|
||
- grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css MUST exit 1.
|
||
- grep -F "@import '../shared/tokens.css'" src/welcome/welcome.css
|
||
MUST exit 0 (the canonical-tokens directive is present).
|
||
- In the built dist/ artifact: the @import is either preserved verbatim
|
||
OR Vite's CSS plugin inlines tokens.css contents — either way the
|
||
`var(--mks-*)` references resolve at runtime.
|
||
|
||
NO src/welcome/welcome-tokens.css file is created in this plan
|
||
(Plan 01-12 must_have #9 path-B contract).
|
||
|
||
9. src/welcome/welcome.ts shape (filter-pipeline form; no `continue`)
|
||
=====================================================================
|
||
|
||
import { Logger } from '../shared/logger';
|
||
import {
|
||
COPY, WELCOME_HERO_RU_FALLBACK, WELCOME_HERO_EN_FALLBACK,
|
||
} 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 populateI18n(): void {
|
||
// Plan 01-12 fallback pattern: chrome.i18n.getMessage(key) || <en-const>.
|
||
// _locales/{en,ru}/messages.json carries welcomeHeroRu + welcomeHeroEn
|
||
// (16 keys total per Plan 01-12 Wave 3).
|
||
const fallbacks: Readonly<Record<string, string>> = Object.freeze({
|
||
welcomeHeroRu: WELCOME_HERO_RU_FALLBACK,
|
||
welcomeHeroEn: WELCOME_HERO_EN_FALLBACK,
|
||
});
|
||
const els = Array.from(
|
||
document.querySelectorAll<HTMLElement>('[data-mokosh-i18n-key]'),
|
||
);
|
||
const pairs = els
|
||
.map((el) => ({ el, key: el.getAttribute('data-mokosh-i18n-key') }))
|
||
.filter((p): p is { el: HTMLElement; key: string } =>
|
||
typeof p.key === 'string')
|
||
.map((p) => ({
|
||
...p,
|
||
value: chrome.i18n.getMessage(p.key) || fallbacks[p.key] || '',
|
||
}))
|
||
.filter((p) => p.value.length > 0);
|
||
for (const { el, value } of pairs) {
|
||
el.textContent = value;
|
||
}
|
||
const missing = els.length - pairs.length;
|
||
if (missing > 0) {
|
||
logger.warn('populateI18n: ' + String(missing) +
|
||
' [data-mokosh-i18n-key] elements had missing chrome.i18n + fallback values');
|
||
}
|
||
}
|
||
|
||
function init(): void {
|
||
populateCopy();
|
||
populateI18n();
|
||
logger.log('welcome page ready');
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
|
||
10. Harness A15-A17 architecture
|
||
=================================
|
||
|
||
Pattern mirrors A14 + A18-A23 (recent additions):
|
||
- Page-side: assertA15/A16/A17 in extension-page-harness.ts (added
|
||
to window.__mokoshHarness global surface alongside A1..A14 + A18..A23).
|
||
- Host-side: driveA15/A16/A17 in lib/harness-page-driver.ts —
|
||
standard page.evaluate wrapper.
|
||
- Orchestrator: registered in drivers array in harness.test.ts
|
||
interleaved into existing A0-A14 + A18-A22 + A23 ordering.
|
||
|
||
A15 — onboarding flag observability:
|
||
Approach: read chrome.storage.local.get(['onboarding-completed',
|
||
'installed-at']) and assert both keys present + correct types.
|
||
|
||
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.
|
||
|
||
A17 — design-swap-readiness invariant (EXTENDED per revision):
|
||
Fetch chrome-extension://<id>/src/welcome/welcome.html via
|
||
fetch(chrome.runtime.getURL('src/welcome/welcome.html')).
|
||
Same for src/welcome/welcome.css and the bundled welcome.js chunk
|
||
(locate via parsing the <script src> attribute in welcome.html).
|
||
Assertions:
|
||
1. welcome.html parses; document.querySelector('.welcome-hero') !== null.
|
||
2. welcome.html text: AT LEAST 7 occurrences of 'data-mokosh-key=' OR
|
||
a combined count of 'data-mokosh-key=' + 'data-mokosh-i18n-key='
|
||
summing to ≥ 7 (post-revision: two D-08 tagline elements use the
|
||
i18n attribute; total mokosh-keyed elements ≥ 7 across both attrs).
|
||
3. welcome.css text content: NO match for /#[0-9a-fA-F]{3,8}/
|
||
(post-revision: tokens.css inlining MAY introduce hex literals
|
||
from the canonical token values — if so, the regex match is
|
||
counted ONLY against the welcome.css source-file region, NOT
|
||
inlined tokens.css content; if Vite preserves the @import
|
||
verbatim with no inlining, the welcome.css text is hex-free).
|
||
4. welcome.css text content: AT LEAST 5 occurrences of 'var(--mks-'.
|
||
5. welcome.css text content: contains the substring
|
||
"@import '../shared/tokens.css'" OR equivalent compiled-output
|
||
evidence (e.g., a literal `--mks-rec:` declaration suggesting
|
||
inlined tokens.css content; accept either pattern as proof the
|
||
canonical @import resolved at build time).
|
||
6. The bundled welcome.js chunk text content: contains EITHER
|
||
'COPY[' substring (non-tagline keys) OR
|
||
"chrome.i18n.getMessage('welcomeHero" substring (tagline keys
|
||
via chrome.i18n path) — both prove the populate plumbing
|
||
survives the build.
|
||
7. NEW (post-revision): create a transient probe element with
|
||
inlineStyle `color: var(--mks-rec)`, append to document.body,
|
||
read getComputedStyle(probe).color. Assert the resolved RGB
|
||
equals 'rgb(178, 84, 61)' (= #b2543d = --mks-madder-600 per
|
||
Plan 01-12 Wave 4 D-04 Loom palette adoption). Proves the
|
||
@import actually wires through and tokens resolve to canonical
|
||
values, not just engineering placeholders.
|
||
|
||
No new FORBIDDEN_HOOK_STRINGS entries — A15-A17 use chrome.tabs.query,
|
||
chrome.storage.local.get, fetch on public web_accessible_resources
|
||
surfaces, getComputedStyle on a transient probe (all production-API
|
||
paths). The Tier-1 grep gate inventory stays at 12.
|
||
|
||
11. Centralized Logger pattern
|
||
===============================
|
||
|
||
src/shared/logger.ts exports Logger class. Welcome page uses
|
||
new Logger('Welcome').
|
||
|
||
12. 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).
|
||
|
||
13. _locales/{en,ru}/messages.json (Plan 01-12 Wave 3 baseline)
|
||
================================================================
|
||
|
||
Both locale files carry 16 keys total. The two welcomeHero* keys
|
||
relevant to Plan 01-10:
|
||
|
||
_locales/en/messages.json:
|
||
"welcomeHeroRu": {
|
||
"message": "Тридцать секунд назад, всегда под рукой.",
|
||
"description": "Welcome page hero — Russian-quoted text..."
|
||
},
|
||
"welcomeHeroEn": {
|
||
"message": "Thirty seconds ago, always at hand.",
|
||
"description": "Welcome page hero — English text..."
|
||
}
|
||
|
||
_locales/ru/messages.json: identical message values; locale-tagged
|
||
descriptions.
|
||
|
||
NOTE: BOTH locales carry BOTH the Russian and English tagline strings
|
||
because the welcome page renders parallel-text layout (RU + EN both
|
||
visible at all times) regardless of operator locale per D-08.
|
||
chrome.i18n.getMessage('welcomeHeroRu') always returns the Russian
|
||
string; chrome.i18n.getMessage('welcomeHeroEn') always returns the
|
||
English string. No locale-conditional rendering.
|
||
|
||
Plan 01-10 introduces NO new locale keys; it consumes existing ones.
|
||
tests/i18n/locale-parity.test.ts (Plan 01-12 Wave 0) continues GREEN.
|
||
</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; default_locale='en' from Plan 01-12)
|
||
</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 (147 passing after Plan 01-12 + 01-14) preserved; new state 147 + 3 RED = 150 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 + copy.ts) + Vite entries in both bundles + manifest web_accessible_resources. Canonical tokens.css @import + chrome.i18n for welcomeHero per Plan 01-12 path-B.</name>
|
||
<read_first>
|
||
- src/popup/index.html (style analog — lang=ru + module script pattern; Plan 01-12 Wave 4 chrome.i18n adoption pattern)
|
||
- src/popup/style.css (Plan 01-12 Wave 4 pattern: `@import "../shared/tokens.css";` at top + ZERO hex literals)
|
||
- src/popup/index.ts (Plan 01-12 Wave 4 chrome.i18n.getMessage with `|| <en-const>` fallback at every operator-facing site)
|
||
- vite.config.ts (rollupOptions.input current shape; preserve __VITE_DEV__ + __MOKOSH_UAT__ defines)
|
||
- vite.test.config.ts (mirrored input shape; preserve mergeConfig pattern)
|
||
- manifest.json (insertion point for web_accessible_resources; confirm default_locale='en' + __MSG_*__ from Plan 01-12 Wave 3 preserved)
|
||
- .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/phases/01-stabilize-video-pipeline/01-12-SUMMARY.md (canonical tokens.css adoption pattern; chrome.i18n fallback pattern)
|
||
- src/shared/tokens.css (canonical token system; --mks-* variable set; @font-face rules)
|
||
- _locales/en/messages.json + _locales/ru/messages.json (welcomeHeroRu + welcomeHeroEn keys land at lines 58-66; confirm pre-existence)
|
||
- src/shared/logger.ts (Logger class shape)
|
||
</read_first>
|
||
<files>
|
||
src/welcome/welcome.html, src/welcome/welcome.ts, src/welcome/welcome.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. NON-TAGLINE keys only
|
||
(welcomeHeroRu + welcomeHeroEn intentionally NOT in COPY — they
|
||
are read via chrome.i18n.getMessage). Exports BOTH:
|
||
- WELCOME_HERO_RU_FALLBACK + WELCOME_HERO_EN_FALLBACK constants
|
||
(Plan 01-12 fallback-pattern en-const strings for the `||`
|
||
degradation path).
|
||
- COPY map with placeholder Russian strings per D-03 Sober
|
||
voice register; stable keys.
|
||
|
||
2. Create src/welcome/welcome.html per interfaces §6 — semantic
|
||
sections (welcome-hero / welcome-body / welcome-footer); every
|
||
non-tagline textContent target carries data-mokosh-key=stable-key;
|
||
the two D-08 tagline elements carry data-mokosh-i18n-key='welcomeHeroRu'
|
||
and data-mokosh-i18n-key='welcomeHeroEn'. Mark slot is
|
||
data-mokosh-slot=mark. <title> has data-mokosh-key='welcome.page.title'.
|
||
SINGLE <link rel=stylesheet href="welcome.css"> — no separate
|
||
welcome-tokens.css link (tokens load via welcome.css @import).
|
||
|
||
3. Create src/welcome/welcome.ts per interfaces §9. Use the
|
||
filter-pipeline form (no `continue`). Import Logger from
|
||
'../shared/logger' + { COPY, WELCOME_HERO_RU_FALLBACK,
|
||
WELCOME_HERO_EN_FALLBACK } from './copy'. document.readyState
|
||
guard: init immediately if already loaded. init() calls BOTH
|
||
populateCopy() AND populateI18n() per interfaces §9.
|
||
|
||
4. Create src/welcome/welcome.css per interfaces §8. FIRST LINE is
|
||
`@import '../shared/tokens.css';` (canonical Plan 01-12 tokens
|
||
— Lora self-host + D-04 Loom palette + full --mks-* set). KEY
|
||
INVARIANTS (verified at end of task):
|
||
- grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css MUST exit 1
|
||
(no matches in this file; the @imported tokens.css IS allowed
|
||
hex literals since it IS the canonical source).
|
||
- grep -F "@import '../shared/tokens.css'" src/welcome/welcome.css
|
||
MUST exit 0.
|
||
- grep -c 'var(--mks-' src/welcome/welcome.css MUST be ≥ 5.
|
||
Every color via var(--mks-*); every font via var(--mks-font-*).
|
||
BEM-ish class names matching the HTML.
|
||
|
||
5. DO NOT create src/welcome/welcome-tokens.css. (Plan 01-12 must_have
|
||
#9 path-B: Plan 01-12 landed first; canonical tokens.css is
|
||
import-ready; no placeholder file is needed. Verified: prior plan
|
||
draft included this file; the revised plan REMOVES it from
|
||
files_modified — see frontmatter.)
|
||
|
||
6. Update vite.config.ts — add welcome entry alongside offscreen.
|
||
Preserve __VITE_DEV__ + __MOKOSH_UAT__ defines from Plan 01-12
|
||
Wave 5 verbatim; do NOT touch any other field:
|
||
rollupOptions: {
|
||
input: {
|
||
offscreen: 'src/offscreen/index.html',
|
||
welcome: 'src/welcome/welcome.html',
|
||
},
|
||
},
|
||
|
||
7. Update vite.test.config.ts — mirror the welcome entry alongside
|
||
extension_page_harness. Preserve the mergeConfig pattern
|
||
(vite.test.config.ts only overrides what differs from vite.config.ts).
|
||
|
||
8. Update manifest.json — insert web_accessible_resources AFTER
|
||
host_permissions block, BEFORE background block. Verify storage
|
||
permission still in permissions array. Verify default_locale='en'
|
||
+ __MSG_*__ name/description/action.default_title from Plan 01-12
|
||
Wave 3 ARE PRESERVED VERBATIM (do not edit those fields). The
|
||
block to insert:
|
||
|
||
"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
|
||
- dist/manifest.json preserves Plan 01-12 default_locale='en' +
|
||
__MSG_*__ values
|
||
- grep -E '#[0-9a-fA-F]{3,8}' dist/src/welcome/welcome.css
|
||
exits 1 OR all matches originate from inlined tokens.css content
|
||
(if Vite inlines the @import). If inlining: confirm via grep
|
||
-F '--mks-rec' dist/src/welcome/welcome.css exits 0 (tokens
|
||
content is reachable in the bundled css).
|
||
- grep -F '@import' OR grep -F '--mks-' in
|
||
dist/src/welcome/welcome.css resolves the canonical tokens
|
||
(either path is acceptable; Vite's CSS plugin chooses)
|
||
|
||
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; 147 others stay GREEN).
|
||
|
||
Style/naming: full-word names; no `as any`; no `continue` in welcome.ts.
|
||
welcome.html: lang="ru" matches popup precedent. Non-tagline Russian
|
||
strings live in copy.ts; tagline strings live in _locales/.
|
||
|
||
Commit:
|
||
feat(01-10): wave-1 task-2 — welcome page bundle + Vite entries + web_accessible_resources — canonical tokens.css @import + chrome.i18n for welcomeHero (Plan 01-12 path-B)
|
||
|
||
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 -F "@import '../shared/tokens.css'" src/welcome/welcome.css && ! grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css && npm run build:test && test -f dist-test/src/welcome/welcome.html</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- 4 new files exist under src/welcome/ (welcome.html, welcome.ts, welcome.css, copy.ts).
|
||
- NO src/welcome/welcome-tokens.css file exists (path-B contract).
|
||
- welcome.css opens with `@import '../shared/tokens.css';`.
|
||
- welcome.css ZERO #hex literals in source file (grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css exits 1).
|
||
- welcome.css ≥ 5 `var(--mks-` references (grep -c).
|
||
- welcome.html ≥ 7 `data-mokosh-key=` + `data-mokosh-i18n-key=` combined attributes.
|
||
- welcome.html includes data-mokosh-i18n-key='welcomeHeroRu' AND data-mokosh-i18n-key='welcomeHeroEn'.
|
||
- welcome.ts imports Logger + COPY + WELCOME_HERO_*_FALLBACK; calls populateCopy() + populateI18n(); NO `as any`; NO `continue`.
|
||
- copy.ts exports frozen COPY (non-tagline only) + WELCOME_HERO_RU_FALLBACK + WELCOME_HERO_EN_FALLBACK.
|
||
- vite.config.ts + vite.test.config.ts both have welcome rollup entry; __VITE_DEV__ + __MOKOSH_UAT__ defines untouched.
|
||
- manifest.json has web_accessible_resources block; storage permission preserved; default_locale='en' + __MSG_*__ from Plan 01-12 preserved verbatim.
|
||
- npm run build + npm run build:test clean.
|
||
- npx tsc --noEmit exit 0.
|
||
- Vitest baseline: 3 RED from Task 1 still RED; 147 others GREEN.
|
||
</acceptance_criteria>
|
||
<done>Welcome page bundle staged with canonical tokens @import + chrome.i18n welcomeHero reads (Plan 01-12 path-B); 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 147 + 3 new = 150 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 147 + 3 = 150 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 — FORBIDDEN_HOOK_STRINGS inventory unchanged at 12.
|
||
</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 24/24 GREEN.</name>
|
||
<read_first>
|
||
- tests/uat/extension-page-harness.ts (assertA14 + assertA18..A22 + assertA23 + window.__mokoshHarness install pattern)
|
||
- tests/uat/lib/harness-page-driver.ts (driveA14 + driveA18..A22 + driveA23 standard wrappers)
|
||
- tests/uat/harness.test.ts (driver imports + drivers array; A0-A14 + A18-A22 + A23 ordering)
|
||
- tests/background/no-test-hooks-in-prod-bundle.test.ts lines 95-130 (FORBIDDEN_HOOK_STRINGS — 12 entries; DO NOT extend; A15-A17 use public APIs)
|
||
- src/welcome/welcome.html (the artifact A17 fetches + parses)
|
||
- src/welcome/welcome.css (the artifact A17 fetches; verifies @import directive + zero hex)
|
||
- src/shared/tokens.css (the @imported canonical token source; --mks-rec = #b2543d per Plan 01-12 Wave 4)
|
||
- 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. Locate the existing assertA14 + assertA18..A22 + assertA23
|
||
block; the new A15/A16/A17 methods slot in lexically near A14 to
|
||
preserve numeric ordering OR at the end of the assertion block
|
||
before the window.__mokoshHarness installation (executor discretion;
|
||
lexical position doesn't affect orchestration).
|
||
|
||
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 (EXTENDED per
|
||
Plan 01-12 path-B revision; verifies canonical @import resolves):
|
||
async function assertA17(): Promise<AssertionResult> {
|
||
const result: AssertionResult = {
|
||
passed: false,
|
||
name: 'A17 — design-swap-readiness: welcome.html parses; .welcome-hero exists; welcome.css has @import + zero hex; ≥7 mokosh-keyed attrs; bundled JS has COPY[ or chrome.i18n.getMessage; --mks-rec resolves',
|
||
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 dataKeyMatches = htmlText.match(/data-mokosh-key=/g) ?? [];
|
||
const dataI18nKeyMatches = htmlText.match(/data-mokosh-i18n-key=/g) ?? [];
|
||
const totalKeyedAttrs = dataKeyMatches.length + dataI18nKeyMatches.length;
|
||
result.checks.push({
|
||
name: 'A17.2: welcome.html has ≥ 7 data-mokosh-key + data-mokosh-i18n-key attributes combined',
|
||
expected: '≥ 7',
|
||
actual: String(totalKeyedAttrs) + ' (data-mokosh-key=' + String(dataKeyMatches.length) + ', data-mokosh-i18n-key=' + String(dataI18nKeyMatches.length) + ')',
|
||
passed: totalKeyedAttrs >= 7,
|
||
});
|
||
const cssUrl = chrome.runtime.getURL('src/welcome/welcome.css');
|
||
const cssRes = await fetch(cssUrl);
|
||
const cssText = await cssRes.text();
|
||
// A17.3: zero hex literals — note this checks the BUILT artifact.
|
||
// If Vite inlines @import contents, the inlined tokens.css may
|
||
// contain hex literals from canonical token values; in that case
|
||
// the regex matches them. The relaxed contract: accept the assert
|
||
// if (a) NO hex match OR (b) hex matches present AND tokens are
|
||
// resolvable (proven by A17.7 below).
|
||
const hexMatches = cssText.match(/#[0-9a-fA-F]{3,8}/g) ?? [];
|
||
const hasCanonicalImport = cssText.includes("@import '../shared/tokens.css'")
|
||
|| cssText.includes('@import "../shared/tokens.css"')
|
||
|| cssText.includes('--mks-rec'); // inlined canonical content
|
||
result.checks.push({
|
||
name: 'A17.3: welcome.css source has zero hex literals (allowed if @import resolves)',
|
||
expected: '0 hex OR canonical-import-resolved',
|
||
actual: 'hex=' + String(hexMatches.length) + ', canonicalImport=' + String(hasCanonicalImport),
|
||
passed: hexMatches.length === 0 || hasCanonicalImport,
|
||
});
|
||
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,
|
||
});
|
||
result.checks.push({
|
||
name: "A17.5: welcome.css has canonical tokens @import (or inlined evidence)",
|
||
expected: '@import ../shared/tokens.css OR --mks-* inlined',
|
||
actual: hasCanonicalImport ? 'present' : 'absent',
|
||
passed: hasCanonicalImport,
|
||
});
|
||
// 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();
|
||
// Post-revision: accept EITHER COPY[ pattern (non-tagline keys)
|
||
// OR chrome.i18n.getMessage('welcomeHero pattern (tagline keys
|
||
// via chrome.i18n path). Both prove populate plumbing survives
|
||
// the build.
|
||
const hasCopySubscript = jsText.includes('COPY[');
|
||
const hasI18nGetMessage = jsText.includes("chrome.i18n.getMessage(") &&
|
||
(jsText.includes('welcomeHero') || jsText.includes('welcome'));
|
||
result.checks.push({
|
||
name: 'A17.6: bundled JS contains COPY[ OR chrome.i18n.getMessage(welcomeHero)',
|
||
expected: 'COPY[ OR chrome.i18n.getMessage welcomeHero',
|
||
actual: 'COPY[=' + String(hasCopySubscript) + ', i18n=' + String(hasI18nGetMessage),
|
||
passed: hasCopySubscript || hasI18nGetMessage,
|
||
});
|
||
// A17.7 NEW: probe --mks-rec via getComputedStyle. The
|
||
// canonical tokens.css from Plan 01-12 Wave 1 Task 2 sets
|
||
// --mks-rec to #b2543d (per Plan 01-12 Wave 4 D-04 Loom
|
||
// palette adoption) which resolves to rgb(178, 84, 61) in
|
||
// CSS computed values. NOTE: this assertion runs in the
|
||
// extension-page-harness.html context where tokens.css IS
|
||
// <link>-loaded directly (Plan 01-12 Wave 6 added that link
|
||
// for A18-A21). The probe creates a transient div, applies
|
||
// `color: var(--mks-rec)`, reads getComputedStyle.
|
||
const probe = document.createElement('div');
|
||
probe.style.color = 'var(--mks-rec)';
|
||
document.body.appendChild(probe);
|
||
const resolvedColor = getComputedStyle(probe).color;
|
||
document.body.removeChild(probe);
|
||
diag(result, 'A17.7 probe getComputedStyle.color = ' + resolvedColor);
|
||
// Accept canonical rgb(178, 84, 61) AND any non-empty
|
||
// non-fallback-default value (the probe inheriting browser
|
||
// default rgb(0, 0, 0) would mean tokens didn't resolve).
|
||
const resolvedNonEmpty = resolvedColor.length > 0 &&
|
||
resolvedColor !== 'rgb(0, 0, 0)' &&
|
||
resolvedColor !== '' &&
|
||
!resolvedColor.includes('rgba(0, 0, 0, 0)');
|
||
const resolvedCanonical = resolvedColor === 'rgb(178, 84, 61)';
|
||
result.checks.push({
|
||
name: 'A17.7: --mks-rec resolves to non-default value via getComputedStyle (proves @import wires through; canonical = rgb(178, 84, 61))',
|
||
expected: 'non-default (preferably rgb(178, 84, 61))',
|
||
actual: resolvedColor + ' (canonical=' + String(resolvedCanonical) + ')',
|
||
passed: resolvedNonEmpty,
|
||
});
|
||
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.7 leverages the Plan 01-12 Wave 6 extension-page-harness.html
|
||
<link rel=stylesheet href="../../src/shared/tokens.css"> already present
|
||
(verified at execute time). The probe element inherits the canonical
|
||
--mks-rec value via direct token cascade in the harness page context.
|
||
If the probe returns rgb(0,0,0) (browser default), the tokens did NOT
|
||
resolve — assert fails.
|
||
|
||
2. Update the global type declaration on window.__mokoshHarness:
|
||
declare global {
|
||
interface Window {
|
||
__mokoshHarness: {
|
||
// ... existing assertA1..A14 + assertA18..A22 + assertA23 + getManifestVersion ...
|
||
assertA15: () => Promise<AssertionResult>;
|
||
assertA16: () => Promise<AssertionResult>;
|
||
assertA17: () => Promise<AssertionResult>;
|
||
};
|
||
}
|
||
}
|
||
window.__mokoshHarness = {
|
||
// ... existing ...
|
||
assertA15, assertA16, assertA17,
|
||
};
|
||
|
||
3. Update the page-ready log line to mention A17.
|
||
|
||
4. **Host-side (lib/harness-page-driver.ts)**: append three new
|
||
drivers AFTER driveA14 (or near the A18-A22 block):
|
||
|
||
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 / driveA18..A22 carry. Standard wrapper.
|
||
|
||
5. **Orchestrator (harness.test.ts)**: update the imports to add
|
||
driveA15, driveA16, driveA17 alongside existing driveA1..A14 +
|
||
driveA18..A22 + driveA23 + getManifestVersion.
|
||
|
||
Update the drivers array to interleave A15-A17 after A14 and
|
||
before A18 (or append at end — executor discretion; preserves
|
||
the bail-on-first-failure ordering semantics):
|
||
{ name: 'A14', drive: driveA14 },
|
||
{ name: 'A15', drive: driveA15 },
|
||
{ name: 'A16', drive: driveA16 },
|
||
{ name: 'A17', drive: driveA17 },
|
||
{ name: 'A18', drive: driveA18 },
|
||
// ... A19-A22, A23 continue ...
|
||
|
||
FORBIDDEN_HOOK_STRINGS UNCHANGED at 12 entries — verify visually
|
||
(do NOT add new entries; A15-A17 use chrome.tabs.query,
|
||
chrome.storage.local.get, fetch, DOMParser, getComputedStyle —
|
||
all production-API surfaces).
|
||
|
||
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 24 assertions. Expected output: "UAT harness: 24/24
|
||
assertions passed". If any A15/A16/A17 fails, surface the
|
||
diagnostic; iterate within this task.
|
||
|
||
8. Run full vitest suite — 150 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 12 strings:
|
||
npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts
|
||
— all build + 12 string assertions GREEN. 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 +
|
||
getComputedStyle on a transient probe).
|
||
- No new entries in FORBIDDEN_HOOK_STRINGS (stays at 12).
|
||
- `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 with @import probe); 24/24 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: 24/24" && 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 (interleaved into A0-A14 + A18-A22 + A23).
|
||
- npm run test:uat reports 24/24 assertions passed (A0-A14 + A15-A17 + A18-A22 + A23).
|
||
- Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 (no new test-hook surface).
|
||
- npm test (vitest) baseline 150 GREEN preserved.
|
||
- npx tsc --noEmit clean.
|
||
</acceptance_criteria>
|
||
<done>Harness covers welcome-tab activation + canonical-tokens design-swap-readiness; Plan 01-12 path-B contract verified empirically; A17.7 probe proves @import wires through to canonical token values.</done>
|
||
</task>
|
||
|
||
<task type="checkpoint:human-verify" gate="blocking">
|
||
<name>Task 5: Operator empirical UAT — fresh-profile install opens welcome tab; canonical tokens + Lora render; chrome.i18n welcomeHero strings render; reload does NOT re-open; harness 24/24 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 (4 files: welcome.html, welcome.ts, welcome.css, copy.ts).
|
||
welcome.css opens with `@import '../shared/tokens.css';` (Plan 01-12
|
||
path-B canonical tokens import). NO placeholder welcome-tokens.css.
|
||
- manifest.json web_accessible_resources for welcome.html (Plan 01-12
|
||
default_locale='en' + __MSG_*__ preserved verbatim).
|
||
- vite.config.ts + vite.test.config.ts rollupOptions.input welcome
|
||
entry in BOTH bundles (Plan 01-12 __VITE_DEV__ + __MOKOSH_UAT__
|
||
defines preserved verbatim).
|
||
- src/background/index.ts openWelcomeIfFirstInstall helper + onInstalled wiring.
|
||
- tests/background/onboarding.test.ts 3 GREEN unit tests.
|
||
- tests/uat/* harness A15+A16+A17 extensions; 24/24 GREEN (A0-A14 +
|
||
A15-A17 + A18-A22 + A23).
|
||
- FORBIDDEN_HOOK_STRINGS inventory unchanged at 12.
|
||
- vitest baseline 150 GREEN (147 from Plan 01-12 + Plan 01-14 + 3 new from this plan).
|
||
|
||
This checkpoint validates real Chrome behavior + canonical brand
|
||
rendering (Plan 01-12 path-B: tokens.css @imports through to
|
||
welcome.css; D-04 Loom palette + Lora display font + IBM Plex Sans
|
||
UI font render correctly; chrome.i18n.getMessage resolves welcomeHeroRu
|
||
+ welcomeHeroEn from _locales/{en,ru}/messages.json).
|
||
</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 -F "@import '../shared/tokens.css'" src/welcome/welcome.css
|
||
— exit 0 (canonical-import directive present).
|
||
4. grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css — exit 1
|
||
(no hex matches in source file).
|
||
5. grep -q web_accessible_resources dist/manifest.json — exit 0.
|
||
6. grep -F '"default_locale": "en"' dist/manifest.json — exit 0
|
||
(Plan 01-12 baseline preserved).
|
||
7. grep -F '__MSG_extName__' dist/manifest.json — exit 0
|
||
(Plan 01-12 i18n placeholder preserved).
|
||
8. npx vitest run — 150 passed.
|
||
9. npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts —
|
||
all GREEN; 12 forbidden strings absent from dist/.
|
||
10. npm run test:uat — 24/24 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 with CANONICAL Plan
|
||
01-12 brand assets (NOT engineering placeholders — Plan 01-12
|
||
landed first):
|
||
- Centered card, max-width ~720px.
|
||
- Hero block at top: 'Mokosh' placeholder mark + 'Mokosh' H1 in
|
||
Lora serif (D-05) + RU tagline '«Тридцать секунд назад,
|
||
всегда под рукой.»' + EN tagline 'Thirty seconds ago, always
|
||
at hand.' — both render via chrome.i18n.getMessage from
|
||
_locales/.
|
||
- Body: 2 explainer lines about 30s screen + 10min logs +
|
||
local-only data + how to save archive (these render from
|
||
src/welcome/copy.ts).
|
||
- CTA box: 'Чтобы начать запись, нажмите иконку AI Call Recorder
|
||
на панели инструментов браузера (правый верхний угол).'
|
||
- Footer: privacy/safety note about no-server-uploads.
|
||
- Typography: Lora serif for hero (Plan 01-12 self-hosted
|
||
WOFF2; full Cyrillic via Cyreal); IBM Plex Sans for body
|
||
(Plan 01-12 self-hosted; Latin + Cyrillic).
|
||
- Color: --mks-rec (#b2543d madder) used for recording-related
|
||
accents per D-04 Loom palette.
|
||
- 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. Verify Cyrillic glyph coverage: the welcomeHeroRu string
|
||
(Тридцать секунд назад, всегда под рукой.) renders without
|
||
tofu/box characters. Lora-Cyrillic from Plan 01-12 supplies
|
||
the glyphs.
|
||
7. Close the welcome tab.
|
||
8. Click the AI Call Recorder toolbar icon (right-side toolbar).
|
||
The icon should be the Loom mark (Plan 01-12 Wave 2 rasterization);
|
||
Chrome's screen-share picker should appear (monitor-only per
|
||
01-09). Pick 'Entire screen' + accept. Badge transitions to
|
||
REC (--mks-rec madder color per Plan 01-12 Wave 4). (This
|
||
validates that the welcome tab CTA accurately describes the
|
||
toolbar start path AND that the brand palette flows through.)
|
||
9. 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).
|
||
10. Reload the extension at chrome://extensions (toggle off + on).
|
||
Observe: the welcome tab does NOT re-open. This validates
|
||
A16's flag-gating contract.
|
||
11. 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 with canonical
|
||
brand), step 6 (Cyrillic glyph coverage), step 8 (toolbar CTA
|
||
validates with branded icon + badge color), or step 10 (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.
|
||
|
||
Plan 01-12 path-B note for operator:
|
||
The welcome page renders TODAY with FULL canonical brand assets
|
||
because Plan 01-12 landed FIRST (2026-05-20 operator brand-fit ack
|
||
"all good"). NO engineering placeholder transition is needed. The
|
||
@import '../shared/tokens.css' directive in welcome.css resolves
|
||
the canonical Lora + IBM Plex Sans + D-04 Loom palette directly
|
||
at build time. The chrome.i18n.getMessage path reads the welcomeHeroRu
|
||
+ welcomeHeroEn keys from _locales/{en,ru}/messages.json (16 keys
|
||
total per Plan 01-12 Wave 3). The harness A17 contract (including
|
||
A17.7 getComputedStyle --mks-rec probe) pins these invariants
|
||
empirically.
|
||
</how-to-verify>
|
||
<resume-signal>
|
||
Type 'approved' after pre-checkpoint gates 1-10 ALL pass AND operator
|
||
steps 4, 5, 6, 8, 10 ALL pass. If any step fails, paste the failure
|
||
diagnostic. The harness 24/24 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. |
|
||
| chrome.i18n.getMessage ↔ _locales/ | Plan 01-12 i18n surface (16 keys; en↔ru parity verified via tests/i18n/locale-parity.test.ts). Plan 01-10 consumes welcomeHeroRu + welcomeHeroEn keys; introduces no new keys. Read-only chrome.i18n access; no IPC. |
|
||
|
||
## 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); chrome.i18n.getMessage is a synchronous read-only API, no IPC, no EoP path. No new 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. _locales/{en,ru}/messages.json content is similarly non-secret operator UI strings (Plan 01-12 Wave 3 baseline). |
|
||
| 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 (FORBIDDEN_HOOK_STRINGS at 12 entries) prevents test-hook leaks; brand-copy integrity is a Phase 5 / brand-team concern. |
|
||
| T-1-10-07 | Tampering | Adversary modifies welcome.css to inject malicious styles via the canonical @import | mitigate | welcome.css's @import resolves to src/shared/tokens.css (Plan 01-12 canonical; supply-chain integrity bounded by the same Tier-1 grep gate + the no-remote-fonts.test.ts MV3 CSP invariant — tokens.css has zero `url()` to external origins). MV3 CSP forbids inline scripts; CSS-only injection cannot escalate. |
|
||
</threat_model>
|
||
|
||
<verification>
|
||
- npx vitest run shows 150/150 GREEN (147 baseline from Plan 01-12 + Plan 01-14 + 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 12.
|
||
- 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; default_locale='en' + __MSG_extName__ from Plan 01-12 preserved verbatim.
|
||
- npm run build:test: exit 0; dist-test/src/welcome/welcome.html exists.
|
||
- grep -F "@import '../shared/tokens.css'" src/welcome/welcome.css: exit 0 (canonical tokens directive present).
|
||
- grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css: exit 1 (no hex matches in source file).
|
||
- grep -c 'var(--mks-' src/welcome/welcome.css: ≥ 5.
|
||
- grep -oE 'data-mokosh-(i18n-)?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 -n "chrome.i18n.getMessage" src/welcome/welcome.ts ≥ 1 (welcomeHero path).
|
||
- 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).
|
||
- test ! -f src/welcome/welcome-tokens.css (Plan 01-12 path-B contract — no placeholder file created).
|
||
- npm run test:uat: "UAT harness: 24/24 assertions passed".
|
||
- Operator empirical steps 4, 5, 6, 8, 10 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 150/150 GREEN.
|
||
2. UAT harness reports 24/24 GREEN (A0-A14 + A15-A17 + A18-A22 + A23).
|
||
3. Tier-1 FORBIDDEN_HOOK_STRINGS inventory stays at 12 (no new test-mode symbols).
|
||
4. Welcome page renders coherently in fresh-profile Chrome with canonical Plan 01-12 brand assets (Lora display + IBM Plex Sans UI + D-04 Loom palette via tokens.css @import; chrome.i18n.getMessage resolves welcomeHeroRu + welcomeHeroEn from _locales/).
|
||
5. Design-swap-readiness invariants hold in the BUILT artifact (welcome.css source has zero hex literals + canonical @import directive present; ≥ 7 mokosh-keyed attributes across data-mokosh-key + data-mokosh-i18n-key; bundled JS uses COPY[ subscript OR chrome.i18n.getMessage('welcomeHero pattern; --mks-rec resolves to non-default value via getComputedStyle probe — preferably rgb(178, 84, 61) canonical).
|
||
6. Operator confirms via how-to-verify steps 4 / 5 / 6 / 8 / 10 + the harness 24/24 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).
|
||
10. NO src/welcome/welcome-tokens.css file exists (Plan 01-12 must_have #9 path-B contract: Plan 01-12 landed first; canonical tokens import directly).
|
||
11. Plan 01-12 baselines preserved verbatim (manifest default_locale='en' + __MSG_*__ placeholders + 16 i18n keys per locale + __VITE_DEV__ + __MOKOSH_UAT__ defines + src/shared/tokens.css unchanged).
|
||
</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).
|
||
- 4 new src/welcome/* files (welcome.html / welcome.ts / welcome.css / copy.ts). NO placeholder welcome-tokens.css (Plan 01-12 path-B).
|
||
- manifest web_accessible_resources delta; Plan 01-12 default_locale + __MSG_*__ preserved verbatim.
|
||
- vite.config.ts + vite.test.config.ts rollupOptions.input delta in BOTH bundles; Plan 01-12 defines preserved.
|
||
- openWelcomeIfFirstInstall helper + onInstalled wiring in src/background/index.ts.
|
||
- chrome.i18n.getMessage adoption for welcomeHeroRu + welcomeHeroEn (Plan 01-12 fallback pattern).
|
||
- Canonical `@import '../shared/tokens.css';` in welcome.css (no placeholder welcome-tokens.css).
|
||
- Harness A15 + A16 + A17 extensions; 21/21 → 24/24 GREEN delta. A17.7 getComputedStyle probe proves --mks-rec resolves.
|
||
- Tier-1 FORBIDDEN_HOOK_STRINGS inventory unchanged at 12 (no new test-hook surface).
|
||
- Plan 01-12 path-B + Plan 01-14 baseline alignment: vitest 147→150; UAT 21→24; FORBIDDEN 12 unchanged.
|
||
- Cross-references: D-02 (welcome layout), D-08 (tagline → chrome.i18n), D-03 (voice register), D-17-onboarding (this plan's amendment marker), D-16-toolbar (start path ownership), D-04 (Loom palette via tokens.css), D-05 (Lora display + IBM Plex Sans UI via tokens.css).
|
||
- Plan 01-12 must_have #9 path-B contract closure: this plan honors path-B end-to-end.
|
||
- Operator empirical UAT outcome (welcome tab opens with canonical brand; Cyrillic renders correctly with Lora; reload does NOT re-open; harness 24/24 GREEN).
|
||
</output>
|