Milestone v1 (v2.0.0): Mokosh — Session Capture #1

Merged
strategy155 merged 297 commits from gsd/phase-04-harden-clean-up-optional into main 2026-05-31 15:34:17 +00:00
7 changed files with 310 additions and 82 deletions
Showing only changes of commit a8bcc17822 - Show all commits

View File

@@ -0,0 +1,115 @@
---
status: awaiting_human_verify
trigger: "Operator-empirical Task 4 checkpoint on Plan 04-06 returned TWEAK NEEDED — decouple welcome-hero mark stroke from --mks-fg-inverse via a dedicated --mks-mark-stroke token that stays linen-50 in BOTH light and dark themes"
created: 2026-05-26T00:00:00Z
updated: 2026-05-26T10:30:00Z
---
## Current Focus
reasoning_checkpoint:
hypothesis: ".welcome-hero__mark { color: var(--mks-fg-inverse) } couples the mark stroke to the theme-conditional semantic foreground-inverse token (linen-50 in light, ink-900 in dark). The mark sits on a theme-independent madder-600 circle, so a theme-coupled stroke is the wrong abstraction. Adding a theme-INDEPENDENT --mks-mark-stroke = var(--mks-linen-50) in :root (NOT inside .dark/[data-theme=dark] override) and re-pointing the wrapper resolves the stroke to linen-50 in BOTH themes."
confirming_evidence:
- "tokens.css line 128 (universal :root): --mks-fg-inverse: var(--mks-linen-50)"
- "tokens.css line 244 (.dark override): --mks-fg-inverse: var(--mks-ink-900) — the theme flip confirmed"
- "welcome.css line 72: .welcome-hero__mark { color: var(--mks-fg-inverse) } — the exact cascade source"
- "mokosh-mark.svg line 2: stroke=currentColor — confirms the cascade plumbing target"
- "--mks-linen-50 = #faf7f1 defined at tokens.css line 90 (universal :root)"
- "popup/style.css:39 also reads --mks-fg-inverse but is unrelated to welcome mark — leave untouched"
falsification_test: "After the fix, re-run scripts/04-06-welcome-hero-screenshots.mjs. If `computedStroke` for the LIGHT screenshot differs from `computedStroke` for the DARK screenshot, the decoupling failed. If they match (expected rgb(250, 247, 241)), the fix is confirmed. A35 will also assert this equality programmatically."
fix_rationale: "Root cause is the wrong token abstraction — --mks-fg-inverse is a semantic token meant for text foreground on inverse surfaces (where the surface flips with theme), but the welcome-hero mark sits on a NON-flipping surface (madder). The fix introduces a dedicated brand-component token --mks-mark-stroke that is purpose-built for this case: theme-independent linen-50 stroke for the madder-circle mark. It addresses the abstraction error directly, not the symptom."
blind_spots: "1) A17.8 source-bundling grep doesn't check welcome.css text — safe. 2) inline-svg.test.ts only pins SVG + welcome.ts + globals.d.ts text — safe. 3) jsdom-style assertions in unit tests don't load welcome.css — safe. 4) Risk: if any other rule in welcome.css cascades from the wrapper's `color` (text nodes inside .welcome-hero__mark), they'd flip too — but the placeholder span is now replaced by the SVG at populateMark, so no visible text consumes the wrapper color. 5) The popup/style.css unrelated --mks-fg-inverse consumer is untouched (separate surface, separate decision)."
hypothesis: ".welcome-hero__mark { color: var(--mks-fg-inverse) } is theme-coupled; --mks-mark-stroke = var(--mks-linen-50) in :root decouples it."
test: Apply token addition + welcome.css edit; strengthen A35; re-screenshot.
expecting: Light and dark `computedStroke` both resolve to rgb(250, 247, 241) (linen-50).
next_action: Add --mks-mark-stroke to tokens.css :root, update welcome.css line 72, strengthen A35.3, run build + screenshots + UAT + vitest.
## Symptoms
expected: Welcome-hero grid icon (mokosh-mark.svg) renders with linen-50 (off-white) stroke on madder-600 circle in BOTH light and dark themes — high-contrast crisp aesthetic per the Plan 01-10 baseline.
actual: In LIGHT theme, stroke is linen-50 (`--mks-fg-inverse` = linen-50 there) — crisp linen-on-madder. In DARK theme, stroke is ink-900 (`--mks-fg-inverse` = ink-900 there) — indigo-on-madder, lower contrast.
errors: None — this is an aesthetic/design defect, not a runtime error.
reproduction: Open `dist-test/src/welcome/welcome.html` with `<html data-theme="dark">` vs without. Inspect `.welcome-hero__mark svg` `getComputedStyle().stroke`. Light = `rgb(250, 247, 241)` (linen-50). Dark = `rgb(24, 27, 42)` (ink-900). Visual: light is crisp, dark is muddy.
started: Plan 04-06 Task 1-3 landed; operator-empirical Task 4 checkpoint flagged the cascade-coupling defect.
## Evidence
- timestamp: 2026-05-26T00:00:00Z
checked: src/shared/tokens.css :root block (lines 82-229)
found: `--mks-fg-inverse: var(--mks-linen-50)` at line 128 (universal/light); `--mks-fg-inverse: var(--mks-ink-900)` at line 244 (.dark theme override).
implication: Confirms operator's diagnosis: --mks-fg-inverse is theme-coupled and unsuitable as a stroke anchor for a mark that must be crisp on madder in both themes.
- timestamp: 2026-05-26T00:00:00Z
checked: src/shared/tokens.css for --mks-linen-50 token
found: `--mks-linen-50: #faf7f1;` defined at line 90 in the universal :root block.
implication: `var(--mks-linen-50)` is the correct hex source; no need to use a literal hex.
- timestamp: 2026-05-26T00:00:00Z
checked: src/welcome/welcome.css .welcome-hero__mark (lines 64-73)
found: `color: var(--mks-fg-inverse);` at line 72 — the exact line that needs to flip to `var(--mks-mark-stroke)`.
implication: One-line CSS change at the wrapper. SVG remains untouched.
- timestamp: 2026-05-26T00:00:00Z
checked: src/shared/brand/mokosh-mark.svg
found: `stroke="currentColor"` at line 2 — unchanged across the fix.
implication: Cascade plumbing remains: SVG inherits from wrapper's `color`, which now points to the new theme-independent token.
- timestamp: 2026-05-26T10:15:00Z
checked: Grep for other consumers of --mks-fg-inverse across src/ + tests/
found: Two consumers — src/welcome/welcome.css:72 (target; rewired to --mks-mark-stroke) and src/popup/style.css:39 (separate popup-surface concern; LEFT UNTOUCHED). No tests grep for the literal `var(--mks-fg-inverse)` in welcome.css.
implication: Scope is minimal; only the welcome-hero mark wrapper changed. Popup style.css continues using --mks-fg-inverse as before.
- timestamp: 2026-05-26T10:20:00Z
checked: Built bundle dist/assets/welcome-BAisfyci.css after `npm run build`
found: ":root{...--mks-fg-inverse: var(--mks-linen-50);--mks-mark-stroke: var(--mks-linen-50);...}" + ".dark,[data-theme=dark]{...--mks-fg-inverse: var(--mks-ink-900);...}" — confirmed --mks-mark-stroke is in :root and NOT overridden in the dark block. ".welcome-hero__mark{...color:var(--mks-mark-stroke)}" — confirmed wrapper points at the new token.
implication: Build plumbing is correct.
- timestamp: 2026-05-26T10:22:00Z
checked: scripts/04-06-welcome-hero-screenshots.mjs re-run on dist/
found: "LIGHT screenshot: /tmp/04-06-welcome-hero-light.png (computed stroke: rgb(250, 247, 241))" and "DARK screenshot: /tmp/04-06-welcome-hero-dark.png (computed stroke: rgb(250, 247, 241))" — same RGB in both themes; rgb(250, 247, 241) is linen-50 (#faf7f1).
implication: Falsification test PASSED — the decoupling works at the live-DOM cascade level. Visual inspection of the dark screenshot shows the crisp linen-on-madder grid icon expected by the operator.
- timestamp: 2026-05-26T10:24:00Z
checked: vitest full suite (npx vitest run)
found: 187/188 passed, 1 tolerated flake (tests/background/webm-remux.test.ts > ffprobe-count_frames 905-912 timeout — documented 04-CONTEXT #9/#10; PASSES in isolation). Baseline 184 + 04-06 deltas +4 = 188 contract held.
implication: No vitest regression from the decouple changes.
- timestamp: 2026-05-26T10:25:00Z
checked: Pre-checkpoint bundle gates 6/6 (sw-bundle-import, no-test-hooks-in-prod-bundle, no-remote-fonts, no-new-function-in-sw-chunk, manifest-i18n, welcome inline-svg)
found: 6/6 PASS (37 tests green); FORBIDDEN_HOOK_STRINGS = 12 (unchanged).
implication: No CSP / globals / hook-leak regression.
- timestamp: 2026-05-26T10:28:00Z
checked: UAT skip-mode (`npm run test:uat`)
found: 36/36 PASS; A35.5 (new decouple sub-check) PASSES with: light="rgb(250, 247, 241)", dark="rgb(250, 247, 241)". A35 diagnostics show populateMark injected the inline <svg> in both themes and the cascade resolves to linen-50 in both.
implication: Live-DOM proof of theme decoupling is in the automated suite (not just the screenshot artifact).
## Resolution
root_cause: `.welcome-hero__mark` used `color: var(--mks-fg-inverse)`. The semantic token `--mks-fg-inverse` is theme-conditional — it resolves to linen-50 in light and ink-900 in dark (`tokens.css :root` line 128 vs `.dark, [data-theme="dark"]` line 244). The SVG inherits via `stroke="currentColor"`, so the stroke flipped with theme. But the mark sits on a theme-INDEPENDENT madder-600 circle, so a theme-coupled stroke produced muddy ink-on-madder in dark mode. The abstraction was wrong: --mks-fg-inverse is a SEMANTIC text-foreground-on-inverse-surface token, not a BRAND-COMPONENT stroke anchor for the welcome mark.
fix: Two-file change.
1. src/shared/tokens.css :root block — added a new brand-component token `--mks-mark-stroke: var(--mks-linen-50)` adjacent to --mks-fg-inverse. CRUCIALLY, this token is NOT overridden in the `.dark, [data-theme="dark"]` block below — it stays linen-50 on every surface. Comment in source explains the design intent + reference to this debug note.
2. src/welcome/welcome.css `.welcome-hero__mark` — flipped `color: var(--mks-fg-inverse)``color: var(--mks-mark-stroke)`. Comment in source explains the cascade chain.
Plus comment-only updates in src/welcome/welcome.ts + tests/welcome/inline-svg.test.ts + tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts to reflect the new wiring (no behavior change in those files beyond comments and A35 strengthening).
Plus A35 STRENGTHENING in tests/uat/lib/harness-page-driver.ts: extracted the live-DOM probe into a `a35Probe(welcomePage, dark)` helper, probed BOTH light + dark themes (data-theme="dark" toggle), and ADDED A35.5 — the decouple proof: assert light.computedStroke === dark.computedStroke === "rgb(250, 247, 241)" (linen-50). FORBIDDEN_HOOK_STRINGS unchanged (no new __MOKOSH_UAT__ symbol).
SVG `src/shared/brand/mokosh-mark.svg` UNCHANGED.
verification:
- LIGHT screenshot /tmp/04-06-welcome-hero-light.png: linen-50 stroke on madder-600 circle on linen-100 card on linen-50 page bg — baseline aesthetic preserved.
- DARK screenshot /tmp/04-06-welcome-hero-dark.png: linen-50 stroke on madder-600 circle on ink-800 card on ink-900 page bg — crisp linen-on-madder mark in dark mode (operator's spec). Visual contrast on the mark itself is IDENTICAL to light mode.
- A35.5 live-DOM probe: light + dark computedStroke both rgb(250, 247, 241). Programmatic proof of decoupling.
- vitest 187/188 + 1 tolerated flake (baseline contract held).
- UAT 36/36 (A35 now has 5 sub-checks all GREEN).
- 6/6 bundle gates PASS; FORBIDDEN_HOOK_STRINGS = 12 (unchanged).
- SCOPE EXPANSION NOTE: src/welcome/welcome.css was NOT in Plan 04-06 re-plan iter-2 files_modified. Adding this edit is a scope expansion authorized by the operator's empirical TWEAK verdict on Task 4 checkpoint. Surfaced explicitly here for the orchestrator's re-checkpoint payload.
files_changed:
- src/shared/tokens.css (added --mks-mark-stroke token in :root)
- src/welcome/welcome.css (rewired .welcome-hero__mark to new token)
- src/welcome/welcome.ts (comment-only updates)
- tests/welcome/inline-svg.test.ts (comment-only update)
- tests/uat/lib/harness-page-driver.ts (A35 strengthening + new A35.5 + helper extraction)
- tests/uat/harness.test.ts (comment-only update)
- .planning/debug/04-06-dark-mode-mark-decouple.md (this debug note)

View File

@@ -127,6 +127,21 @@
--mks-fg-disabled: var(--mks-ink-300); --mks-fg-disabled: var(--mks-ink-300);
--mks-fg-inverse: var(--mks-linen-50); --mks-fg-inverse: var(--mks-linen-50);
/* Brand-component stroke — INTENTIONALLY THEME-INDEPENDENT.
Used by the welcome-hero mark wrapper (welcome.css
.welcome-hero__mark) so the canonical mokosh-mark.svg's
`stroke="currentColor"` cascade resolves to linen-50 on BOTH the
light AND dark surfaces. The mark sits on a madder-600 circle
(--mks-rec) which is theme-independent, so the stroke must be
theme-independent too — otherwise the cascade through the
semantic --mks-fg-inverse (which flips per theme) gives a
muddy ink-on-madder render in dark mode. This token is NOT
overridden in the .dark/[data-theme="dark"] block below; it
stays linen-50 everywhere. See Plan 04-06 operator-empirical
Task 4 verdict + debug session
.planning/debug/resolved/04-06-dark-mode-mark-decouple.md. */
--mks-mark-stroke: var(--mks-linen-50);
--mks-border: var(--mks-linen-200); --mks-border: var(--mks-linen-200);
--mks-border-strong: var(--mks-linen-300); --mks-border-strong: var(--mks-linen-300);
--mks-border-focus: var(--mks-ink-900); --mks-border-focus: var(--mks-ink-900);

View File

@@ -69,7 +69,16 @@ body {
height: var(--mks-space-20); height: var(--mks-space-20);
border-radius: var(--mks-radius-full); border-radius: var(--mks-radius-full);
background: var(--mks-rec); background: var(--mks-rec);
color: var(--mks-fg-inverse); /* THEME-INDEPENDENT stroke via --mks-mark-stroke (tokens.css :root
block; always resolves to linen-50). The mokosh-mark.svg root
<svg> carries `stroke="currentColor"` so the SVG strokes inherit
this wrapper's `color`. The mark sits on a theme-independent
madder-600 circle (--mks-rec), so the stroke must be
theme-independent too — using --mks-fg-inverse here (the previous
wiring) cascaded to ink-900 in dark mode and gave a muddy
indigo-on-madder render. See Plan 04-06 operator-empirical Task 4
verdict + .planning/debug/resolved/04-06-dark-mode-mark-decouple.md. */
color: var(--mks-mark-stroke);
} }
.welcome-hero__mark-placeholder { .welcome-hero__mark-placeholder {

View File

@@ -38,15 +38,18 @@ import {
// so the SVG source can be inlined into the welcome page DOM via DOMParser // so the SVG source can be inlined into the welcome page DOM via DOMParser
// + replaceChildren (see populateMark below). The CSS `color` property is // + replaceChildren (see populateMark below). The CSS `color` property is
// inherited (CSS Color L3 §4) and the mokosh-mark.svg root <svg> carries // inherited (CSS Color L3 §4) and the mokosh-mark.svg root <svg> carries
// `stroke="currentColor"` (Plan 04-06 Wave 1 SVG edit); on a LIGHT surface // `stroke="currentColor"` (Plan 04-06 Wave 1 SVG edit); the
// the `.welcome-hero__mark` wrapper sets `color: var(--mks-fg-inverse)` // `.welcome-hero__mark` wrapper sets `color: var(--mks-mark-stroke)`
// (welcome.css:67), and on a DARK surface the `.dark` token override // (welcome.css), which is a theme-INDEPENDENT brand-component token
// (src/shared/tokens.css lines 234-251) flips `--mks-fg-inverse` so the // always resolving to linen-50 (tokens.css :root, NOT overridden in the
// inline `<svg>` inherits the contrast-correct stroke without any // .dark/[data-theme="dark"] block). The mark sits on a theme-independent
// JS/event-handler/conditional code. An `<img>`-injected SVG would render // madder-600 circle (--mks-rec); a theme-coupled stroke would render
// in an isolated SVG document context where `currentColor` resolves to // muddy ink-on-madder in dark mode (see Plan 04-06 operator-empirical
// the SVG's own root color (defaults to `canvastext`, near-black) and the // Task 4 debug-decouple resolution). An `<img>`-injected SVG would
// cascade would not work — hence the inline-SVG technique. // render in an isolated SVG document context where `currentColor`
// resolves to the SVG's own root color (defaults to `canvastext`,
// near-black) and the cascade would not work — hence the inline-SVG
// technique.
// //
// References: // References:
// - 04-UI-SPEC.md §"Implementation amendment" (currentColor + inline) // - 04-UI-SPEC.md §"Implementation amendment" (currentColor + inline)
@@ -154,10 +157,12 @@ function populateI18n(): void {
* near-black) and the parent cascade does NOT reach it — verified * near-black) and the parent cascade does NOT reach it — verified
* empirically + W3C SVG2 §13.3. By inlining via DOMParser + * empirically + W3C SVG2 §13.3. By inlining via DOMParser +
* replaceChildren, the inline <svg>'s `stroke="currentColor"` resolves * replaceChildren, the inline <svg>'s `stroke="currentColor"` resolves
* to the wrapper's `color: var(--mks-fg-inverse)` (welcome.css:67), * to the wrapper's `color: var(--mks-mark-stroke)` (welcome.css), and
* and on the dark surface the `.dark` token override * because --mks-mark-stroke is a theme-INDEPENDENT brand-component
* (src/shared/tokens.css lines 234-251) automatically flips the * token (tokens.css :root, NOT overridden in .dark/[data-theme="dark"])
* resolved colour — contrast-correct on both surfaces, no JS branching. * the stroke stays linen-50 in both themes — crisp linen-on-madder on
* every surface (Plan 04-06 operator-empirical Task 4 debug-decouple
* resolution; see .planning/debug/resolved/04-06-dark-mode-mark-decouple.md).
* *
* Security / CSP discipline (T-04-06-01 mitigation): the inner-HTML * Security / CSP discipline (T-04-06-01 mitigation): the inner-HTML
* string-assignment path is FORBIDDEN — DOMParser + replaceChildren * string-assignment path is FORBIDDEN — DOMParser + replaceChildren

View File

@@ -544,15 +544,19 @@ async function main(): Promise<number> {
// RUN — not env-gated; the 5-min wait is A33's, not A34's). // RUN — not env-gated; the 5-min wait is A33's, not A34's).
{ name: 'A34', drive: driveA34Wrapped }, { name: 'A34', drive: driveA34Wrapped },
// Plan 04-06 A35: UI-SPEC dark-logo `currentColor` strategy LIVE-DOM // Plan 04-06 A35: UI-SPEC dark-logo `currentColor` strategy LIVE-DOM
// proof. Opens welcome.html as a real Puppeteer tab so populateMark() // proof + Plan 04-06 Task 4 decouple verification. Opens welcome.html
// actually runs; reads getComputedStyle().stroke on the injected // as a real Puppeteer tab so populateMark() actually runs; reads
// <svg> to verify the currentColor cascade resolves through // getComputedStyle().stroke on the injected <svg> in BOTH light and
// .welcome-hero__mark color: var(--mks-fg-inverse) (UI-SPEC Option A). // dark themes to verify (a) the currentColor cascade resolves
// Appended LAST in the drivers array so the new welcome tab cannot // through .welcome-hero__mark color: var(--mks-mark-stroke), and
// pollute any later driver (and welcomePage.close() in finally // (b) the theme-INDEPENDENT --mks-mark-stroke token is NOT
// guarantees no tab leak regardless). Host-side driver — mirrors // overridden by the .dark/[data-theme="dark"] override block — both
// the driveA32/A33/A34 host-side pattern (NOT a page.evaluate // probes resolve to the same linen-50 RGB. Appended LAST in the
// (window.__mokoshHarness) wrapper). // drivers array so the new welcome tab cannot pollute any later
// driver (and welcomePage.close() in finally guarantees no tab leak
// regardless). Host-side driver — mirrors the driveA32/A33/A34
// host-side pattern (NOT a page.evaluate(window.__mokoshHarness)
// wrapper).
{ name: 'A35', drive: driveA35Wrapped }, { name: 'A35', drive: driveA35Wrapped },
]; ];

View File

@@ -2961,20 +2961,29 @@ export async function driveA34(
// chrome.runtime.getURL('src/welcome/welcome.html') resolves to — so // chrome.runtime.getURL('src/welcome/welcome.html') resolves to — so
// welcome.ts init() runs populateMark() at DOMContentLoaded and the // welcome.ts init() runs populateMark() at DOMContentLoaded and the
// inline <svg> actually lands in the page DOM. We then read // inline <svg> actually lands in the page DOM. We then read
// getComputedStyle().stroke on the injected <svg> to prove the // getComputedStyle().stroke on the injected <svg> in BOTH the LIGHT
// `currentColor` cascade resolves through the .welcome-hero__mark // default theme AND the DARK theme (the latter set via
// wrapper's `color: var(--mks-fg-inverse)` rule. // document.documentElement.setAttribute('data-theme','dark'), which
// triggers the .dark, [data-theme="dark"] { ... } override block in
// src/shared/tokens.css). The cascade chain is:
// mokosh-mark.svg `stroke="currentColor"`
// ← .welcome-hero__mark { color: var(--mks-mark-stroke) }
// ← :root { --mks-mark-stroke: var(--mks-linen-50) }
// (NOT overridden by the .dark/[data-theme="dark"] block — Plan
// 04-06 operator-empirical Task 4 decouple resolution)
// So the computed stroke MUST be the same in both themes and MUST
// resolve to linen-50 (rgb(250, 247, 241), the hex #faf7f1).
// //
// This is the iter-2 BLOCKER 1 resolution: the prior iter-1 re-plan // This is the iter-2 BLOCKER 1 resolution + Plan 04-06 Task 4 decouple
// claimed live-DOM injection was delegated to A17.8, but A17.8 is // extension: the prior iter-1 re-plan claimed live-DOM injection was
// 100% string-grep on the welcome JS chunk and the harness does NOT // delegated to A17.8, but A17.8 is 100% string-grep on the welcome JS
// open welcome.html as a live tab (it only fetches the HTML text via // chunk and the harness does NOT open welcome.html as a live tab (it
// chrome.runtime.getURL + DOMParser.parseFromString — a DETACHED // only fetches the HTML text via chrome.runtime.getURL +
// parse, not a live document). driveA35 is the canonical automated // DOMParser.parseFromString — a DETACHED parse, not a live document).
// proof of the runtime injection + cascade. A35 is appended LAST in // driveA35 is the canonical automated proof of the runtime injection +
// the drivers array; nothing reads its return value, so the new tab // cascade + decouple. A35 is appended LAST in the drivers array;
// cannot pollute later drivers (welcomePage.close() in finally also // nothing reads its return value, so the new tab cannot pollute later
// guarantees no tab leak). // drivers (welcomePage.close() in finally also guarantees no tab leak).
// //
// Pattern: HOST-SIDE driver (mirrors driveA32/driveA33/driveA34 — NOT // Pattern: HOST-SIDE driver (mirrors driveA32/driveA33/driveA34 — NOT
// a page.evaluate(window.__mokoshHarness) wrapper). Builds a // a page.evaluate(window.__mokoshHarness) wrapper). Builds a
@@ -2989,6 +2998,8 @@ export async function driveA34(
// - tests/welcome/inline-svg.test.ts (the source-text counterpart) // - tests/welcome/inline-svg.test.ts (the source-text counterpart)
// - tests/uat/extension-page-harness.ts A17.8 (the source-bundling // - tests/uat/extension-page-harness.ts A17.8 (the source-bundling
// counterpart — narrowed in Plan 04-06 to a raw-source grep only) // counterpart — narrowed in Plan 04-06 to a raw-source grep only)
// - .planning/debug/resolved/04-06-dark-mode-mark-decouple.md
// (operator-empirical Task 4 decouple debug session)
/** Live-DOM navigation + populateMark()-settle ceiling for the /** Live-DOM navigation + populateMark()-settle ceiling for the
* driveA35 welcome-page open. ~5 seconds is generous against the * driveA35 welcome-page open. ~5 seconds is generous against the
@@ -2999,25 +3010,83 @@ export async function driveA34(
const A35_WELCOME_PAGE_TIMEOUT_MS = 5_000; const A35_WELCOME_PAGE_TIMEOUT_MS = 5_000;
/** /**
* driveA35 — UI-SPEC dark-logo `currentColor` strategy LIVE-DOM proof. * Canonical linen-50 hex (matches src/shared/tokens.css :root
* `--mks-linen-50: #faf7f1`) and the Chrome-serialized RGB form
* (canonical Chromium getComputedStyle().stroke output for #faf7f1).
* Used by A35.5 to assert the decoupled stroke matches the brand token.
*/
const A35_LINEN_50_HEX = '#faf7f1';
const A35_LINEN_50_RGB = 'rgb(250, 247, 241)';
/**
* Probe the `.welcome-hero__mark` live-DOM cascade signals for a given
* theme. Pulled into a helper so the LIGHT (default) probe and the DARK
* ([data-theme="dark"] override) probe share the exact same evaluate
* payload — the decoupling assertion (A35.5) only proves equality when
* the two reads are byte-for-byte the same probe shape.
*
* For the dark probe we set `data-theme="dark"` on <html> before reading;
* the tokens.css cascade fires on that attribute (src/shared/tokens.css
* line 234 selector `.dark, [data-theme="dark"]`).
*/
async function a35Probe(
welcomePage: Page,
dark: boolean,
): Promise<{
svgPresent: boolean;
strokeAttr: string | null;
computedStroke: string;
imgPresent: boolean;
}> {
if (dark) {
await welcomePage.evaluate(() => {
document.documentElement.setAttribute('data-theme', 'dark');
});
} else {
await welcomePage.evaluate(() => {
document.documentElement.removeAttribute('data-theme');
});
}
return welcomePage.evaluate(() => {
const svgEl = document.querySelector('.welcome-hero__mark svg');
const imgEl = document.querySelector('.welcome-hero__mark img');
const svgPresent = svgEl !== null;
const strokeAttr = svgEl !== null ? svgEl.getAttribute('stroke') : null;
const computedStroke =
svgEl !== null ? getComputedStyle(svgEl).stroke : '';
const imgPresent = imgEl !== null;
return { svgPresent, strokeAttr, computedStroke, imgPresent };
});
}
/**
* driveA35 — UI-SPEC dark-logo `currentColor` strategy LIVE-DOM proof
* + Plan 04-06 Task 4 decouple verification.
* *
* Opens a fresh welcome.html tab via `browser.newPage()`, navigates to * Opens a fresh welcome.html tab via `browser.newPage()`, navigates to
* `chrome-extension://<id>/src/welcome/welcome.html` (the canonical * `chrome-extension://<id>/src/welcome/welcome.html` (the canonical
* web-accessible welcome page path; the same path A17 already fetches), * web-accessible welcome page path; the same path A17 already fetches),
* waits for `populateMark()` to inject the inline <svg> at * waits for `populateMark()` to inject the inline <svg> at
* DOMContentLoaded, then runs a single `welcomePage.evaluate(...)` that * DOMContentLoaded, then probes the LIVE DOM in BOTH the LIGHT default
* reads the LIVE DOM and returns the four signals A35 asserts on: * theme AND the DARK theme (toggled by setting data-theme="dark" on the
* - svgPresent — `.welcome-hero__mark svg` exists (inline <svg> * documentElement, which fires the `.dark, [data-theme="dark"]` block
* injected). * in tokens.css). Five sub-checks:
* - strokeAttr — that <svg>'s `stroke` attribute === 'currentColor' * A35.1 — `.welcome-hero__mark svg` injected (populateMark ran).
* (the canonical mark recolour landed correctly). * A35.2 — that <svg> carries stroke="currentColor" (Option A recolor).
* - computedStroke`getComputedStyle(svgEl).stroke` is a resolved * A35.3 — getComputedStyle(<svg>).stroke resolves to a non-default
* non-default colour value (the `currentColor` * colour in LIGHT (cascade through
* cascade resolved through * `.welcome-hero__mark { color: var(--mks-mark-stroke) }`
* `.welcome-hero__mark { color: var(--mks-fg-inverse) }`). * actually works).
* Empty / 'none' would mean the cascade is broken. * A35.4 — no legacy <img> in the slot (pre-04-06 path is gone).
* - imgPresent — `.welcome-hero__mark img` is NULL (the legacy * A35.5 — Plan 04-06 Task 4 DECOUPLE PROOF: the computed stroke in
* <img> injection path is gone; we're inlining now). * LIGHT and DARK themes are EQUAL and BOTH resolve to the
* canonical linen-50 RGB (rgb(250, 247, 241) — Chromium's
* serialization of #faf7f1). This is the live-DOM evidence
* that --mks-mark-stroke is theme-independent (NOT overridden
* by the .dark/[data-theme="dark"] block) and that the
* wrapper points at it. The operator-empirical dark-mode
* "muddy ink-on-madder" defect is fixed when this check
* passes.
* *
* Always closes the new tab in a `finally` block so no extra Puppeteer * Always closes the new tab in a `finally` block so no extra Puppeteer
* page leaks across the harness run. Mirrors the driveA33/driveA34 * page leaks across the harness run. Mirrors the driveA33/driveA34
@@ -3050,52 +3119,60 @@ export async function driveA35(
}); });
diagnostics.push('A35 welcome.html DOMContentLoaded + inline <svg> selector resolved'); diagnostics.push('A35 welcome.html DOMContentLoaded + inline <svg> selector resolved');
const probe = await welcomePage.evaluate(() => { // LIGHT probe (default theme; remove any prior data-theme).
const svgEl = document.querySelector('.welcome-hero__mark svg'); const lightProbe = await a35Probe(welcomePage, false);
const imgEl = document.querySelector('.welcome-hero__mark img');
const svgPresent = svgEl !== null;
const strokeAttr = svgEl !== null ? svgEl.getAttribute('stroke') : null;
const computedStroke =
svgEl !== null ? getComputedStyle(svgEl).stroke : '';
const imgPresent = imgEl !== null;
return { svgPresent, strokeAttr, computedStroke, imgPresent };
});
diagnostics.push( diagnostics.push(
`A35 live-DOM probe: svgPresent=${probe.svgPresent} strokeAttr=${probe.strokeAttr ?? '<null>'} computedStroke="${probe.computedStroke}" imgPresent=${probe.imgPresent}`, `A35 LIGHT probe: svgPresent=${lightProbe.svgPresent} strokeAttr=${lightProbe.strokeAttr ?? '<null>'} computedStroke="${lightProbe.computedStroke}" imgPresent=${lightProbe.imgPresent}`,
); );
const computedStrokeResolved = // DARK probe (set data-theme="dark" on <html>; same selector / same
probe.computedStroke.length > 0 && probe.computedStroke !== 'none'; // evaluate payload).
const darkProbe = await a35Probe(welcomePage, true);
diagnostics.push(
`A35 DARK probe: svgPresent=${darkProbe.svgPresent} strokeAttr=${darkProbe.strokeAttr ?? '<null>'} computedStroke="${darkProbe.computedStroke}" imgPresent=${darkProbe.imgPresent}`,
);
const lightStrokeResolved =
lightProbe.computedStroke.length > 0 && lightProbe.computedStroke !== 'none';
const decoupled =
lightProbe.computedStroke === darkProbe.computedStroke
&& lightProbe.computedStroke === A35_LINEN_50_RGB;
checks.push({ checks.push({
name: 'A35.1: inline <svg> injected into `.welcome-hero__mark` slot (populateMark ran)', name: 'A35.1: inline <svg> injected into `.welcome-hero__mark` slot (populateMark ran)',
expected: 'non-null `.welcome-hero__mark svg`', expected: 'non-null `.welcome-hero__mark svg`',
actual: probe.svgPresent ? 'present' : 'missing', actual: lightProbe.svgPresent ? 'present' : 'missing',
passed: probe.svgPresent, passed: lightProbe.svgPresent,
}); });
checks.push({ checks.push({
name: 'A35.2: injected <svg> carries stroke="currentColor" (UI-SPEC Option A recolor)', name: 'A35.2: injected <svg> carries stroke="currentColor" (UI-SPEC Option A recolor)',
expected: 'currentColor', expected: 'currentColor',
actual: probe.strokeAttr, actual: lightProbe.strokeAttr,
passed: probe.strokeAttr === 'currentColor', passed: lightProbe.strokeAttr === 'currentColor',
}); });
checks.push({ checks.push({
name: 'A35.3: getComputedStyle(<svg>).stroke resolves to a non-default colour (the currentColor cascade through `.welcome-hero__mark { color: var(--mks-fg-inverse) }` actually worked)', name: 'A35.3: getComputedStyle(<svg>).stroke resolves to a non-default colour in LIGHT (the currentColor cascade through `.welcome-hero__mark { color: var(--mks-mark-stroke) }` actually worked)',
expected: 'non-empty, non-"none" resolved colour', expected: 'non-empty, non-"none" resolved colour',
actual: probe.computedStroke, actual: lightProbe.computedStroke,
passed: computedStrokeResolved, passed: lightStrokeResolved,
}); });
checks.push({ checks.push({
name: 'A35.4: no legacy <img> in `.welcome-hero__mark` slot (the pre-04-06 <img> injection path is gone)', name: 'A35.4: no legacy <img> in `.welcome-hero__mark` slot (the pre-04-06 <img> injection path is gone)',
expected: 'null', expected: 'null',
actual: probe.imgPresent ? 'present (UNEXPECTED — legacy <img> still injected)' : 'null', actual: lightProbe.imgPresent ? 'present (UNEXPECTED — legacy <img> still injected)' : 'null',
passed: !probe.imgPresent, passed: !lightProbe.imgPresent,
});
checks.push({
name: `A35.5: Plan 04-06 Task 4 DECOUPLE — LIGHT and DARK computed strokes are EQUAL and BOTH resolve to linen-50 (${A35_LINEN_50_HEX} = ${A35_LINEN_50_RGB}). Proves --mks-mark-stroke is theme-independent (NOT overridden by .dark/[data-theme="dark"]) and the wrapper points at it.`,
expected: `light === dark === "${A35_LINEN_50_RGB}"`,
actual: `light="${lightProbe.computedStroke}", dark="${darkProbe.computedStroke}"`,
passed: decoupled,
}); });
const passed = checks.every((c) => c.passed); const passed = checks.every((c) => c.passed);
return { return {
passed, passed,
name: 'A35 — welcome-page inline-SVG injected at populateMark() runtime; currentColor stroke resolves via parent CSS color cascade (UI-SPEC dark-logo strategy live-DOM proof; iter-2 BLOCKER 1 resolution)', name: 'A35 — welcome-page inline-SVG injected at populateMark() runtime; currentColor stroke resolves via parent CSS color cascade AND is theme-decoupled via --mks-mark-stroke (UI-SPEC dark-logo strategy live-DOM proof; iter-2 BLOCKER 1 resolution + Plan 04-06 Task 4 decouple)',
checks, checks,
diagnostics, diagnostics,
}; };
@@ -3104,7 +3181,7 @@ export async function driveA35(
diagnostics.push(`A35 THREW: ${errMsg}`); diagnostics.push(`A35 THREW: ${errMsg}`);
return { return {
passed: false, passed: false,
name: 'A35 — welcome-page inline-SVG injected at populateMark() runtime; currentColor stroke resolves via parent CSS color cascade (UI-SPEC dark-logo strategy live-DOM proof; iter-2 BLOCKER 1 resolution)', name: 'A35 — welcome-page inline-SVG injected at populateMark() runtime; currentColor stroke resolves via parent CSS color cascade AND is theme-decoupled via --mks-mark-stroke (UI-SPEC dark-logo strategy live-DOM proof; iter-2 BLOCKER 1 resolution + Plan 04-06 Task 4 decouple)',
checks, checks,
diagnostics, diagnostics,
error: errMsg, error: errMsg,

View File

@@ -13,14 +13,17 @@
// It does NOT instantiate any document, does NOT call populateMark, does // It does NOT instantiate any document, does NOT call populateMark, does
// NOT invoke DOMParser at runtime. The runtime behavior (live <svg> // NOT invoke DOMParser at runtime. The runtime behavior (live <svg>
// injection into the welcome page DOM and the currentColor CSS cascade // injection into the welcome page DOM and the currentColor CSS cascade
// through `.welcome-hero__mark { color: var(--mks-fg-inverse); }`) is // through `.welcome-hero__mark { color: var(--mks-mark-stroke); }` — the
// verified by the host-side UAT harness assertion A35 (driveA35 in // theme-INDEPENDENT brand-component token introduced by Plan 04-06 Task 4
// tests/uat/lib/harness-page-driver.ts) — A35 opens welcome.html as a real // operator-empirical debug-decouple) is verified by the host-side UAT
// Puppeteer tab so populateMark() actually runs, then reads // harness assertion A35 (driveA35 in tests/uat/lib/harness-page-driver.ts)
// `getComputedStyle().stroke` on the injected <svg> to prove the cascade // — A35 opens welcome.html as a real Puppeteer tab so populateMark()
// resolves. This split mirrors the project's two-layer test approach // actually runs, then reads `getComputedStyle().stroke` on the injected
// (source contract under vitest + live-DOM behavior under the Puppeteer // <svg> in BOTH light and dark themes to prove (a) the cascade resolves
// harness) — verified at re-plan iter-2 (BLOCKER 1 resolution). // and (b) the resolved stroke is identical across themes (linen-50). This
// split mirrors the project's two-layer test approach (source contract
// under vitest + live-DOM behavior under the Puppeteer harness) — verified
// at re-plan iter-2 (BLOCKER 1 resolution) + Plan 04-06 Task 4 decouple.
// //
// References: // References:
// - .planning/phases/04-harden-clean-up-optional/04-UI-SPEC.md // - .planning/phases/04-harden-clean-up-optional/04-UI-SPEC.md