fix(01-10): welcome page mark — bundle canonical mokosh-mark.svg + replace placeholder

Plan 01-10 must_have #9 path-A swap-in (landed 2026-05-20 per debug
session 01-10-welcome-page-missing-mark). Closes the planning-coverage
gap where Plan 01-12 path-B (canonical tokens import) ran ahead of
01-10, leaving the welcome hero with a text placeholder 'Mokosh'
inside the rec-bg circle instead of the canonical 2×2 woven-square
mark from src/shared/brand/mokosh-mark.svg.

Why Option B (Vite ?url import) over manual WAR (A) or inline SVG (C):
- @crxjs/vite-plugin ^2.0.0-beta.25 auto-WARs transitively-reachable
  resources from extension pages — no manifest.json edit needed.
- Vite default-inlines small SVGs (~600 bytes < 4096 byte default
  assetsInlineLimit) as data:image/svg+xml URLs in the welcome chunk
  — no extra HTTP request, no extra WAR entry.
- Hashed asset fallback works automatically if the SVG grows past
  the inline limit in future revisions.
- Existing font-bundling precedent (dist/assets/Lora-*.woff2 +
  IBMPlex*.woff2) proves the Vite + crxjs pipeline.

Files modified:
- src/welcome/welcome.ts — added markUrl import + populateMark() that
  walks [data-mokosh-slot='mark'] and injects an <img>.
- src/welcome/welcome.html — added explanatory comment block; preserved
  the data-mokosh-slot wrapper for forward-compat (the placeholder
  span remains as the JS-fail-gracefully fallback).
- src/welcome/welcome.css — added .welcome-hero__mark-img rule
  (60% sizing inside the existing styled circle wrapper).
- src/welcome/copy.ts — added 'welcome.hero.mark.alt' COPY key
  (Russian per D-03 Sober voice).
- globals.d.ts — added *.svg?url ambient module declaration
  (Vite recommended pattern; keeps tsconfig.json types: ['chrome']
  clean by not requiring vite/client triple-slash directives).
- tests/uat/extension-page-harness.ts — extended A17 with A17.8
  sub-check verifying the canonical mark SVG is bundled into the
  welcome chunk (data URL OR file URL form) AND that the canonical
  viewBox='0 0 32 32' is preserved through bundling.

Acceptance gates passed:
- npx tsc --noEmit exit 0
- npm run build exit 0
- SKIP_BUILD=1 npm test → 150/150 GREEN
- npm run test:uat → 24/24 GREEN including A17.8
- Tier-1 hook-string grep gate PASS (no FORBIDDEN_HOOK_STRINGS
  in production bundle).
- Manifest valid JSON; web_accessible_resources auto-bundled.
- Pre-checkpoint bundle gates 1/2/3: vendor pre-existing hits
  (JSZip + ts-ebml) confirmed identical pre-change via git stash
  baseline; not caused by this fix.

Forward-looking deferred (out of scope):
- Issue 2 dark-surface contrast (e.g. chrome.notifications icon128
  may need a light-stroke variant). The welcome hero's rec-orange
  BG already provides high contrast with the dark ink stroke — this
  is correct design. Per the orchestrator's explicit constraint,
  light-variant mark for dark notification panels is deferred to
  Phase 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 10:28:58 +02:00
parent 4bba679e39
commit d48a715da5
7 changed files with 390 additions and 3 deletions

View File

@@ -2090,7 +2090,7 @@ const A17_CANONICAL_REC_RGB = 'rgb(178, 84, 61)';
async function assertA17(): Promise<AssertionResult> {
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<AssertionResult> {
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 <img src>.
//
// 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/<hash>.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) {