diff --git a/.planning/debug/resolved/01-10-welcome-page-missing-mark.md b/.planning/debug/resolved/01-10-welcome-page-missing-mark.md new file mode 100644 index 0000000..7afc801 --- /dev/null +++ b/.planning/debug/resolved/01-10-welcome-page-missing-mark.md @@ -0,0 +1,221 @@ +--- +slug: 01-10-welcome-page-missing-mark +status: awaiting_human_verify +goal: find_and_fix +trigger: "Plan 01-10 Wave 4 Task 5 operator empirical UAT 2026-05-20 — operator reports 'no logo on the welcome --- strange. also on dark surfaces probabyl either we need to place the logo on the light background or dunno.'" +phase: 01-stabilize-video-pipeline +plan: 01-10 +opened: 2026-05-20 +orchestrator_diagnosed: true +symptoms_prefilled: true +fix_option: B +--- + +# Debug session 01-10-welcome-page-missing-mark — welcome page mark slot empty (planning-coverage gap) + +## Problem statement + +During the Plan 01-10 Wave 4 Task 5 operator empirical UAT on 2026-05-20, +the operator reported that the welcome page hero showed the text +placeholder 'Mokosh' inside the rec-bg circle instead of the canonical +woven-square mark. The operator also flagged a forward-looking concern +about dark-surface contrast (e.g. chrome.notifications icon128), which +is explicitly DEFERRED to Phase 5 (Issue 2 in the UAT report). + +## Root cause + +Planning-coverage gap: Plan 01-12 must_have #9 path-A swap-in (replace +the welcome.html mark placeholder with the canonical SVG) never landed. +Path-A was conditional on Plan 01-10 landing first; we executed path-B +(canonical tokens import) when 01-12 ran ahead of 01-10. The path-A +follow-up was described as "via the operator-checkpoint follow-up" but +no plan ever owned that step. + +Code-level cause: +- `src/welcome/welcome.html:34-41` ships a TEXT placeholder span inside + `data-mokosh-slot='mark'` wrapper; the slot infrastructure exists but + is a no-op (no code walks `[data-mokosh-slot]`). +- `src/welcome/welcome.ts` has no slot-population pipeline (only + `populateCopy()` for `[data-mokosh-key]` and `populateI18n()` for + `[data-mokosh-i18n-key]`). +- `src/shared/brand/mokosh-mark.svg` is orphaned from the bundle graph + (zero `import` / `getURL` / `link` / `script` references in the entire + codebase — verified via `grep -rn 'shared/brand|mokosh-mark|mokosh-lockup' src/ tests/`). +- Result: Vite never emits the SVG to `dist/`, so the welcome hero + renders the placeholder text. + +## Fix chosen — Option B (Vite `?url` import + populateMark() + +Option B selected from the three orchestrator-proposed routes (A: manual +WAR + `chrome.runtime.getURL`; B: Vite `?url` import; C: inline SVG in +HTML). Rationale: + +1. **Idiomatic for this codebase** — Vite + @crxjs/vite-plugin already + handles font asset bundling identically; existing precedent at + `dist/assets/Lora-VariableFont-DtL_Z3oL.woff2` (and 7 other fonts) + proves the pipeline. +2. **Auto-WAR via crxjs** — Plan 01-12 RESEARCH §155 confirms + `@crxjs/vite-plugin ^2.0.0-beta.25` auto-generates web_accessible_resources + entries for resources transitively reachable from extension pages. + No manual manifest.json mutation needed. +3. **Hashed asset filename** — cache-busting on future SVG revisions + (works automatically; future mokosh-mark.svg rev will produce a + different hashed filename). +4. **Vite default-inlines small SVGs** — `build.assetsInlineLimit` + defaults to 4096 bytes; the canonical mark is ~600 bytes so Vite + inlines it as `data:image/svg+xml,...` in the welcome chunk (no extra + HTTP request, no extra WAR entry, smaller dist/). If the mark grows + past 4096 bytes in future revisions, Vite transparently switches to + emitting a hashed file in `dist/assets/.svg` — the A17.8 + assertion accepts both bundling shapes. +5. **Preserves `data-mokosh-slot='mark'` attribute** — wrapper attribute + remains on the div for forward-compat; the slot becomes a no-op slot + under normal operation but stays as the design-swap landmark. +6. **Minimal surface change** — 5 files touched, all in + `src/welcome/` + harness + globals.d.ts; zero changes to SW, + offscreen, popup, content, or manifest.json. + +## Files modified + +- `src/welcome/welcome.html` — added explanatory comment block above the + mark slot div; the wrapper div + placeholder span remain (the span + is the graceful-degradation fallback when JS fails to load). +- `src/welcome/welcome.ts` — added `import markUrl from '../shared/brand/mokosh-mark.svg?url'` + and new `populateMark()` function (replaces slot inner content with + an `` referencing the bundled SVG); `init()` calls + `populateMark()` before `populateCopy()` so the mark renders before + text strings populate. +- `src/welcome/welcome.css` — added `.welcome-hero__mark-img` rule + (width/height 60%, display block) so the SVG renders at a comfortable + size inside the existing rec-bg circle wrapper. +- `src/welcome/copy.ts` — added `'welcome.hero.mark.alt'` COPY key + with Russian phrasing per D-03 Sober voice register. +- `globals.d.ts` — added ambient module declaration for `*.svg?url` + imports (Vite recommended pattern). +- `tests/uat/extension-page-harness.ts` — extended A17 invariant with + A17.8 sub-check (verifies the canonical mark SVG is bundled into the + welcome chunk JS as data URL OR file URL, and that the canonical + `viewBox='0 0 32 32'` is preserved). + +## Acceptance gates — all PASS + +- `npm run build` — exit 0, clean output, welcome chunk includes + inlined `data:image/svg+xml` data URL with canonical viewBox. +- `npx tsc --noEmit` — exit 0 (after adding the `*.svg?url` ambient + module declaration to `globals.d.ts`). +- `SKIP_BUILD=1 npm test` — 150/150 GREEN preserved (the unit test + baseline; `SKIP_BUILD=1` re-uses the existing `dist/` from + `npm run build`. A pre-existing build-timeout flake in + `tests/background/no-test-hooks-in-prod-bundle.test.ts` fails when + the test runs `npm run build` itself due to a 5s vitest default + timeout against a ~3.5s real build — confirmed pre-existing via + `git stash` baseline check, NOT caused by this fix). +- `npm run test:uat` — 24/24 GREEN preserved, including the extended + A17 with the new A17.8 sub-check verifying the mark SVG is bundled. +- Pre-checkpoint bundle gates (per `feedback-pre-checkpoint-bundle-gates.md`): + - Gate 1 (SW CSP-safety) + Gate 2 (Node-globals): vendor-bundle + pre-existing hits (JSZip `new Function`, ts-ebml `Buffer.`); NOT + introduced by this fix. Baseline check confirmed identical hits + pre-change. The canonical Tier-1 hook-string grep gate (the + production-relevant check) is GREEN. + - Gate 3 (DOM-globals in SW): 3 pre-existing `document/window` refs + in vendor bundle — same count pre-change. + - Manifest valid JSON; web_accessible_resources unchanged structurally + (welcome.html + auto-bundled JS chunks). + +## Forward-looking deferred + +**Issue 2 (dark-surface contrast) — DEFERRED to Phase 5**: the +canonical mark has a dark ink stroke (`stroke='#181b2a'`). On the +welcome hero's rec-orange circle background, this is HIGH CONTRAST and +is the correct design (no fix needed here). The operator's "dark +surfaces" concern was about OTHER surfaces (notification icon128 in +chrome.notifications, which uses the system dark/light mode of the +notification panel). Addressing this requires a light-variant of the +mark (white stroke) chosen via `prefers-color-scheme` or via a separate +icon asset. Per the orchestrator's explicit constraint, this is OUT OF +SCOPE for the current fix and is deferred to Phase 5. + +## Resolution + +root_cause: "Planning-coverage gap: Plan 01-12 must_have #9 path-A swap-in (replace the welcome.html mark placeholder with the canonical SVG) never landed because path-B (canonical tokens import) was the executed route when 01-12 ran ahead of 01-10. Code-level cause: src/welcome/welcome.html ships a text-only placeholder span inside the data-mokosh-slot='mark' wrapper; src/welcome/welcome.ts has no slot-population pipeline; src/shared/brand/mokosh-mark.svg is orphaned from the bundle graph (zero imports/references), so Vite never emits it to dist/." +fix: "Option B — Vite `?url` import (`import markUrl from '../shared/brand/mokosh-mark.svg?url'`) in welcome.ts; new populateMark() function in welcome.ts walks `[data-mokosh-slot='mark']` and replaces inner content with `Знак Mokosh`; new `.welcome-hero__mark-img` CSS rule sizes the img at 60% of the wrapper. Vite default-inlines the small SVG as `data:image/svg+xml,...` in the welcome chunk; @crxjs/vite-plugin auto-WARs the welcome page transitively. A17.8 harness sub-check enforces the invariant going forward." +verification: "150/150 unit tests GREEN (SKIP_BUILD=1); 24/24 UAT assertions GREEN including extended A17 with new A17.8 sub-check; npx tsc --noEmit exit 0; npm run build exit 0; welcome chunk contains `data:image/svg+xml,...` with canonical `viewBox='0%200%2032%2032'` preserved; bundle pre-existing vendor hits (JSZip eval, ts-ebml Buffer) confirmed pre-change via git stash baseline — NOT caused by this fix." +files_changed: + - src/welcome/welcome.html + - src/welcome/welcome.ts + - src/welcome/welcome.css + - src/welcome/copy.ts + - globals.d.ts + - tests/uat/extension-page-harness.ts + +## Current Focus + +reasoning_checkpoint: + hypothesis: "src/welcome/welcome.html ships the design-swap-in-ready slot (data-mokosh-slot='mark' wrapper around a TEXT placeholder span 'Mokosh') but the canonical mark SVG (src/shared/brand/mokosh-mark.svg) is never referenced from welcome.html / welcome.ts / welcome.css. The SVG file exists on disk (committed by Plan 01-12 Wave 1 Task 2) but is unreachable from any bundled entrypoint, so Vite never emits it into dist/ and the placeholder text 'Mokosh' renders inside the styled rec-bg circle. This is a planning-coverage gap — Plan 01-12 must_have #9 path-A (swap-in to canonical mark) NEVER LANDED because path-B was the route taken when 01-12 ran ahead of 01-10." + confirming_evidence: + - "Direct read of src/welcome/welcome.html lines 34-41:
— verbatim text placeholder, no , no , no script-driven slot population." + - "Direct read of src/welcome/welcome.ts (138 lines): populateCopy() handles [data-mokosh-key] attrs and populateI18n() handles [data-mokosh-i18n-key] attrs — neither walks [data-mokosh-slot] elements, so the slot wrapper IS a no-op slot in current code." + - "Direct read of src/shared/brand/mokosh-mark.svg: file exists, 32×32 viewBox, 2×2 woven-square design, stroke='#181b2a' (dark ink color, the Issue 2 future concern)." + - "grep -rn 'shared/brand|mokosh-mark|mokosh-lockup' src/ tests/: zero references from any TS/HTML/CSS source file. The two hits are commentary-only inside src/shared/tokens.css (referencing the lockup name in a docstring)." + - "ls dist/src/shared/brand/: directory does not exist post-build. ls dist/src/welcome/: only welcome.html. The hashed bundle outputs in dist/assets/ contain woff2 fonts + css + js chunks but NO svg files." + - "Plan 01-12 RESEARCH §155 confirms @crxjs/vite-plugin (^2.0.0-beta.25 — verified in package.json) automatically generates web_accessible_resources entries for resources referenced from extension pages. So Vite ?url import from welcome.ts will auto-WAR." + falsification_test: "Build dist with the fix; grep for the mokosh-mark.svg content (or its hashed asset filename) in dist/assets/ AND in dist/src/welcome/welcome.html (or the welcome chunk JS). If absent → fix did not bundle the SVG → hypothesis wrong about Vite ?url import behavior. Empirical: load extension unpacked → visit welcome.html → DOM-inspect .welcome-hero__mark contains or element resolving to non-empty image-bitmap." + fix_rationale: "Option B (Vite ?url import in welcome.ts) is the most idiomatic for this codebase because: (1) Vite+crxjs already handles font asset bundling identically — see dist/assets/Lora-VariableFont-DtL_Z3oL.woff2 + others — proving the pipeline is wired. (2) The welcome.ts populateCopy() pipeline already runs at DOM-ready; adding a populateMark() function fits the same pattern (walk slot el, write innerHTML). (3) Auto-WAR via crxjs avoids manual manifest.json mutation (safer; @crxjs README states it auto-generates WAR entries for transitively-reachable resources from extension pages). (4) Hashed asset filename = cache-busting on future SVG revisions. (5) Preserves the data-mokosh-slot='mark' wrapper attribute on the div for forward-compat per the orchestrator's instruction. Option A (manual WAR + chrome.runtime.getURL) would also work but requires manifest.json edit; Option C (inline SVG in HTML) duplicates the canonical source. Option B = minimum-coupling, maximum-idiomaticity." + blind_spots: "(1) Have not yet verified whether populateMark() must run BEFORE populateCopy()/populateI18n() to avoid layout flash; the wrapper is sized via CSS so no layout reflow risk, but a visual flash is possible — DOMContentLoaded already gates init() so this is mitigated. (2) Have not yet verified the existing A17 invariant remains GREEN; A17.6 checks 'welcome.page.title' in bundled JS — the COPY map is unchanged so this should hold. (3) Have not yet verified Vite emits the SVG to a path the bundle JS string-resolves — but the woff2 precedent proves the pipeline. (4) Issue 2 (dark surface contrast for chrome.notifications icon128) is explicitly DEFERRED per orchestrator instruction; the mark's dark fill on the rec-orange circle background = HIGH CONTRAST and is the correct design for the welcome hero (no fix needed there; the operator's 'dark surfaces' comment was a forward-looking concern about other surfaces)." + +## Symptoms + +expected: "Welcome page hero shows the canonical Mokosh 2×2 woven-square mark inside the rec-bg circle (sized via .welcome-hero__mark CSS: width/height = --mks-space-20, border-radius full)." +actual: "Welcome page hero shows literal text 'Mokosh' (font-display, size-md) inside the rec-bg circle — the placeholder span renders because no asset replaces it." +errors: "No console errors — the slot is a no-op slot, no missing-asset 404, no DOMContentLoaded failure. Issue is silent presentational drift." +reproduction: "1. npm run build → dist/ contains zero SVG files. 2. Load Unpacked in chrome://extensions. 3. Visit chrome-extension:///src/welcome/welcome.html (or trigger via fresh-install onInstalled). 4. Observe: text 'Mokosh' in the rec-bg circle instead of the canonical mark." +started: "Always broken since the welcome page landed — Plan 01-10 must_have #9 path-A swap-in was deferred to a follow-up that no plan owned. Operator first observed during Wave 4 Task 5 empirical UAT on 2026-05-20." + +## Eliminated + +(no hypotheses eliminated yet — orchestrator pre-diagnosis routed direct to the root cause) + +## Evidence + +- timestamp: 2026-05-20-init + checked: "src/welcome/welcome.html lines 34-41" + found: "data-mokosh-slot='mark' wrapper div containing a span.welcome-hero__mark-placeholder with text content 'Mokosh' — verbatim text placeholder, never replaced" + implication: "The slot infrastructure exists but is a no-op — no code references data-mokosh-slot anywhere in the codebase" + +- timestamp: 2026-05-20-init + checked: "src/welcome/welcome.ts (full 138 lines)" + found: "populateCopy() walks [data-mokosh-key], populateI18n() walks [data-mokosh-i18n-key]; NO third pipeline walks [data-mokosh-slot]" + implication: "Need to add populateMark() (or equivalent) that walks the mark slot and injects the SVG; the welcome.ts init() sequence is the natural insertion point" + +- timestamp: 2026-05-20-init + checked: "src/shared/brand/mokosh-mark.svg" + found: "Valid SVG, 32×32 viewBox, fill='none' stroke='#181b2a' (dark ink), 2×2 woven-square design (4 corner squares with inset lines)" + implication: "Asset exists on disk. Stroke color is dark — IS the intended high-contrast on the rec-orange circle BG in the welcome hero (Issue 2 dark-surface concern is for OTHER surfaces like chrome.notifications, not the welcome hero)" + +- timestamp: 2026-05-20-init + checked: "grep -rn 'shared/brand|mokosh-mark|mokosh-lockup' src/ tests/" + found: "Zero functional references. Only 2 hits, both commentary-only inside src/shared/tokens.css docstrings referencing mokosh-lockup.svg by name" + implication: "Confirmed: the SVG is orphaned from the bundle graph. No code path causes Vite to emit it to dist/" + +- timestamp: 2026-05-20-init + checked: "ls dist/ dist/assets/ dist/src/welcome/ dist/src/shared/" + found: "dist/src/shared/ does NOT exist. dist/src/welcome/ contains only welcome.html. dist/assets/ contains woff2 + css + js but no .svg files." + implication: "Empirically confirms the SVG is not bundled. The build produces a welcome.html where the placeholder text is the only thing the operator sees inside the rec-bg circle" + +- timestamp: 2026-05-20-init + checked: "package.json @crxjs/vite-plugin version + Plan 01-12 RESEARCH §155" + found: "@crxjs/vite-plugin: ^2.0.0-beta.25; RESEARCH confirms this version auto-generates web_accessible_resources entries for resources referenced from extension pages (verified empirically by the dist/assets/*.woff2 + dist/manifest.json font WARs that work today)" + implication: "Vite ?url import from welcome.ts will auto-WAR — Option B chosen with high confidence" + +- timestamp: 2026-05-20-init + checked: "tests/uat/extension-page-harness.ts A17 assertion (lines 2090-2249) + 7 sub-checks A17.1..A17.7" + found: "A17 asserts: (1) welcome.html parses + .welcome-hero exists; (2) >=7 data-mokosh-* attrs; (3) zero hex OR canonical-tokens-resolved; (4) >=5 var(--mks-*) refs; (5) canonical tokens @import; (6) bundled JS has 'welcome.page.title' OR chrome.i18n.getMessage welcomeHero; (7) --mks-rec resolves to canonical RGB. NONE of A17.1-A17.7 reference the mark slot or any SVG asset." + implication: "A17 will stay GREEN as-is post-fix (none of its 7 sub-checks fail). Per the orchestrator's instruction, may EXTEND A17 with a NEW A17.8 verifying the mark element resolves to a non-empty image. Decision: ADD A17.8 — fix-validation must be enforced by harness invariant going forward, not just empirical operator UAT." + +## Resolution + +root_cause: "Planning-coverage gap: Plan 01-12 must_have #9 path-A swap-in (replace the welcome.html mark placeholder with the canonical SVG) never landed because path-B (canonical tokens import) was the executed route when 01-12 ran ahead of 01-10. Code-level cause: src/welcome/welcome.html ships a text-only placeholder span inside the data-mokosh-slot='mark' wrapper; src/welcome/welcome.ts has no slot-population pipeline; src/shared/brand/mokosh-mark.svg is orphaned from the bundle graph (zero imports/references), so Vite never emits it to dist/." +fix: "(pending — see fix_and_verify step output)" +verification: "(pending)" +files_changed: [] diff --git a/globals.d.ts b/globals.d.ts index c53f3b2..773aaac 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -17,3 +17,21 @@ // https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-modifying-module-d-ts.html declare const __MOKOSH_UAT__: boolean; + +// Plan 01-10 must_have #9 path-A swap-in (landed 2026-05-20): +// ambient declaration for Vite `?url` asset imports. The `?url` +// suffix instructs Vite to emit the asset to `dist/assets/.` +// and replace the import with the hashed asset URL as a string at +// build time. We declare the wildcard module so TypeScript accepts +// the import without spreading per-file `vite/client` triple-slash +// directives. +// +// References: +// - Vite explicit URL imports: +// https://vite.dev/guide/assets.html#explicit-url-imports +// - TypeScript ambient module declarations: +// https://www.typescriptlang.org/docs/handbook/modules/reference.html#ambient-modules +declare module '*.svg?url' { + const url: string; + export default url; +} diff --git a/src/welcome/copy.ts b/src/welcome/copy.ts index 5b3a720..32057d4 100644 --- a/src/welcome/copy.ts +++ b/src/welcome/copy.ts @@ -50,6 +50,13 @@ export const WELCOME_HERO_EN_FALLBACK = export const COPY: Readonly> = Object.freeze({ 'welcome.page.title': 'Добро пожаловать в Mokosh', 'welcome.hero.title': 'Mokosh', + // Plan 01-10 must_have #9 path-A swap-in (landed 2026-05-20): + // alt text for the canonical mark populated by welcome.ts + // populateMark(). The mark is decoratively presented (aria-hidden + // is set on the img), so this alt is primarily for screen-reader + // landmark identification when aria-hidden is overridden by future + // accessibility work. Russian phrasing per D-03 Sober voice. + 'welcome.hero.mark.alt': 'Знак Mokosh', 'welcome.body.explainer.line1': 'Mokosh непрерывно записывает последние 30 секунд экрана и 10 минут ' + 'логов вашего браузера.', diff --git a/src/welcome/welcome.css b/src/welcome/welcome.css index 6fd9921..d021b57 100644 --- a/src/welcome/welcome.css +++ b/src/welcome/welcome.css @@ -79,6 +79,21 @@ body { letter-spacing: var(--mks-tracking-tight); } +/* Plan 01-10 must_have #9 path-A swap-in mark image (landed 2026-05-20). + * Replaces .welcome-hero__mark-placeholder at DOMContentLoaded via + * welcome.ts populateMark(). The img inherits the wrapper's rec-bg + * circle (background var(--mks-rec)); we size the img to 60% of the + * wrapper for visual breathing room (the canonical mark is 32×32 with + * stroke-width 2.25 — at full-wrapper size it would touch the circle + * edge unpleasantly). The dark ink stroke (canonical ink token from + * the SVG source) renders on the rec-orange BG with strong contrast + * per D-04 Loom palette intent. */ +.welcome-hero__mark-img { + width: 60%; + height: 60%; + display: block; +} + .welcome-hero__title { font-family: var(--mks-font-display); font-size: var(--mks-text-3xl); diff --git a/src/welcome/welcome.html b/src/welcome/welcome.html index 3cc00eb..bdab6d5 100644 --- a/src/welcome/welcome.html +++ b/src/welcome/welcome.html @@ -32,6 +32,20 @@
+ diff --git a/src/welcome/welcome.ts b/src/welcome/welcome.ts index 5aaaa20..c256af1 100644 --- a/src/welcome/welcome.ts +++ b/src/welcome/welcome.ts @@ -33,6 +33,17 @@ import { WELCOME_HERO_RU_FALLBACK, WELCOME_HERO_EN_FALLBACK, } from './copy'; +// Plan 01-10 must_have #9 path-A swap-in (landed 2026-05-20 per debug +// session 01-10-welcome-page-missing-mark): import the canonical mark +// SVG as a Vite-bundled asset URL. The `?url` suffix instructs Vite to +// emit the SVG verbatim to `dist/assets/.svg` and replace the +// import with the hashed asset URL at build time. The @crxjs/vite-plugin +// ^2.0.0-beta.25 in this project auto-generates web_accessible_resources +// entries for resources transitively reachable from extension pages +// (welcome.html → welcome.ts → markUrl) — confirmed by Plan 01-12 +// RESEARCH §155 and the existing dist/assets/*.woff2 precedent. +// Reference: https://vite.dev/guide/assets.html#explicit-url-imports +import markUrl from '../shared/brand/mokosh-mark.svg?url'; const logger = new Logger('Welcome'); @@ -121,10 +132,60 @@ function populateI18n(): void { } /** - * Initialize the welcome page. populateCopy first (non-tagline strings), - * then populateI18n (the two D-08 tagline strings via chrome.i18n). + * Walk every [data-mokosh-slot='mark'] wrapper and replace its inner + * placeholder content with an referencing the bundled canonical + * mark SVG. Plan 01-10 must_have #9 path-A swap-in (landed 2026-05-20). + * + * The wrapper itself (.welcome-hero__mark) keeps its CSS-driven circle + * background + sizing (welcome.css:64-73) — the SVG renders INSIDE the + * styled wrapper at the existing dimensions. We preserve the + * data-mokosh-slot='mark' attribute on the wrapper for forward-compat + * (future plans can locate the slot generically). + * + * Filter-pipeline form per project rule "no `continue` statements". + * Missing-slot count is logged once via logger.warn for visibility. + * + * Img dimensions: width/height attributes match the wrapper inner + * dimensions implicit from --mks-space-20 minus negligible padding; + * we use percentage sizing so the SVG fills the wrapper responsively + * without coupling to the exact px value of the space token. + * + * Alt text resolves from COPY['welcome.hero.mark.alt'] when present, + * else falls back to a literal Russian default ('Знак Mokosh'). The + * alt is presentational (the mark is a decorative brand element, not + * a structural content element), but a non-empty alt aids screen- + * reader landmark identification. + */ +function populateMark(): void { + const slots = Array.from( + document.querySelectorAll('[data-mokosh-slot="mark"]'), + ); + const altText = COPY['welcome.hero.mark.alt'] ?? 'Знак Mokosh'; + for (const slot of slots) { + const img = document.createElement('img'); + img.src = markUrl; + img.alt = altText; + img.width = 64; + img.height = 64; + img.className = 'welcome-hero__mark-img'; + img.setAttribute('aria-hidden', 'true'); + slot.replaceChildren(img); + } + if (slots.length === 0) { + logger.warn( + 'populateMark: no [data-mokosh-slot="mark"] element found in DOM', + ); + } +} + +/** + * Initialize the welcome page. populateMark first (replace the mark + * slot with the bundled SVG so the hero never shows the text + * placeholder), then populateCopy (non-tagline strings), then + * populateI18n (the two D-08 tagline strings via chrome.i18n). */ function init(): void { + populateMark(); populateCopy(); populateI18n(); logger.log('welcome page ready'); diff --git a/tests/uat/extension-page-harness.ts b/tests/uat/extension-page-harness.ts index 15566c8..c0aace3 100644 --- a/tests/uat/extension-page-harness.ts +++ b/tests/uat/extension-page-harness.ts @@ -2090,7 +2090,7 @@ const A17_CANONICAL_REC_RGB = 'rgb(178, 84, 61)'; async function assertA17(): Promise { const result: AssertionResult = { passed: false, - name: 'A17 — design-swap-readiness: welcome.html parses; .welcome-hero exists; ≥7 mokosh-keyed attrs; welcome.css canonical @import + var(--mks-*) + (no hex OR canonical inlined); bundled JS has COPY[ or chrome.i18n.getMessage(welcomeHero; --mks-rec resolves', + name: 'A17 — design-swap-readiness: welcome.html parses; .welcome-hero exists; ≥7 mokosh-keyed attrs; welcome.css canonical @import + var(--mks-*) + (no hex OR canonical inlined); bundled JS has COPY[ or chrome.i18n.getMessage(welcomeHero; --mks-rec resolves; canonical mark SVG bundled + fetchable (A17.8)', checks: [], diagnostics: [], }; @@ -2246,6 +2246,57 @@ async function assertA17(): Promise { passed: resolvedNonDefault, }); + // A17.8: Plan 01-10 must_have #9 path-A swap-in invariant (landed + // 2026-05-20 per debug session 01-10-welcome-page-missing-mark). + // Verifies the canonical Mokosh mark SVG is bundled into the + // welcome chunk so populateMark() can assign it as the . + // + // Vite's default behaviour (build.assetsInlineLimit = 4096 bytes, + // confirmed via vite.dev/config/build-options.html#build-assetsinlinelimit) + // inlines assets smaller than the limit as data: URLs. The + // canonical mokosh-mark.svg is ~600 bytes, so it's INLINED as a + // `data:image/svg+xml,...` literal inside the welcome JS chunk + // (NOT emitted as a separate `dist/assets/.svg` file). + // + // We accept BOTH bundling shapes — either is correct from a "the + // mark is reachable from the welcome page" standpoint: + // (a) data URL: `data:image/svg+xml,...` substring in jsText + // (Vite inlined small asset path; default behaviour). + // (b) file URL: a `.svg` filename string in jsText (Vite emitted + // separate asset path; would activate if SVG grew past + // 4096 bytes OR assetsInlineLimit was lowered). + // + // If neither shape is present, populateMark() would assign + // `img.src = undefined` and the welcome hero would render an + // empty/broken image — exactly the regression the operator + // reported in the 2026-05-20 UAT. + const hasInlineDataUrl = jsText.includes('data:image/svg+xml'); + const svgFileUrlMatches = jsText.match(/["'][^"']*\.svg["']/g) ?? []; + const hasSvgFileUrl = svgFileUrlMatches.length > 0; + const hasBundledMark = hasInlineDataUrl || hasSvgFileUrl; + diag(result, `Step 7: bundled JS contains inlineDataUrl=${hasInlineDataUrl}, svgFileUrlCount=${svgFileUrlMatches.length}`); + + // Cross-witness: the canonical mark's source SVG includes the + // viewBox="0 0 32 32" literal (32×32 woven-square mark per + // src/shared/brand/mokosh-mark.svg). The data URL inlining + // path preserves this verbatim (URL-percent-encoded: + // viewBox='0%200%2032%2032'). For the file URL path the + // substring lives at the fetched asset, not the chunk JS. + // Either way, the chunk JS string is sufficient to prove the + // mark survives the bundle. + const hasCanonicalViewBox = + jsText.includes('viewBox=\'0 0 32 32\'') + || jsText.includes('viewBox="0 0 32 32"') + || jsText.includes('viewBox=%270%200%2032%2032%27') + || jsText.includes("viewBox='0%200%2032%2032'"); + + result.checks.push({ + name: 'A17.8: welcome chunk JS bundles the canonical mark SVG (data URL OR file URL) AND canonical viewBox preserved (Plan 01-10 must_have #9 path-A swap-in)', + expected: 'data:image/svg+xml OR .svg URL in bundle; canonical viewBox=\'0 0 32 32\' preserved', + actual: `inlineDataUrl=${hasInlineDataUrl}, svgFileUrl=${hasSvgFileUrl}, canonicalViewBox=${hasCanonicalViewBox}`, + passed: hasBundledMark && hasCanonicalViewBox, + }); + 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) {