fix(debug 04-06): decouple welcome-hero mark stroke via --mks-mark-stroke
Operator-empirical Task 4 checkpoint flagged the dark-mode mark stroke
as muddy ink-on-madder. Root cause: .welcome-hero__mark used
`color: var(--mks-fg-inverse)`, which is a SEMANTIC text-foreground-on-
inverse-surface token that flips to ink-900 in the dark theme
(tokens.css line 244). The mark sits on a theme-independent madder-600
circle, so the stroke must be theme-independent too.
Fix: introduce a dedicated BRAND-COMPONENT token --mks-mark-stroke =
var(--mks-linen-50) in the universal :root block. CRUCIALLY NOT
overridden in the .dark/[data-theme="dark"] block — stays linen-50 on
every surface. Rewire .welcome-hero__mark to point at the new token.
SVG (mokosh-mark.svg) unchanged — `stroke="currentColor"` cascade
plumbing identical; only the wrapper's color source changed.
A35 strengthened: extracted live-DOM probe into a helper, now probes
BOTH light + dark themes (data-theme="dark" toggle on documentElement),
and added A35.5 — the decouple proof that light.computedStroke ===
dark.computedStroke === "rgb(250, 247, 241)" (linen-50). No new
__MOKOSH_UAT__ symbol; FORBIDDEN_HOOK_STRINGS stays at 12.
Scope expansion note: src/welcome/welcome.css was not in Plan 04-06
re-plan iter-2 files_modified. The edit is authorized by the operator's
TWEAK verdict on Task 4 checkpoint.
Verification:
- /tmp/04-06-welcome-hero-{light,dark}.png re-shot — both show identical
crisp linen-on-madder grid icon.
- A35.5 LIVE-DOM probe (UAT): light="rgb(250, 247, 241)", dark=same.
- UAT 36/36 GREEN; vitest 187 + 1 tolerated webm-remux flake.
- 6/6 pre-checkpoint bundle gates PASS; FORBIDDEN_HOOK_STRINGS = 12.
Debug session: .planning/debug/04-06-dark-mode-mark-decouple.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
115
.planning/debug/04-06-dark-mode-mark-decouple.md
Normal file
115
.planning/debug/04-06-dark-mode-mark-decouple.md
Normal 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)
|
||||
@@ -127,6 +127,21 @@
|
||||
--mks-fg-disabled: var(--mks-ink-300);
|
||||
--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-strong: var(--mks-linen-300);
|
||||
--mks-border-focus: var(--mks-ink-900);
|
||||
|
||||
@@ -69,7 +69,16 @@ body {
|
||||
height: var(--mks-space-20);
|
||||
border-radius: var(--mks-radius-full);
|
||||
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 {
|
||||
|
||||
@@ -38,15 +38,18 @@ import {
|
||||
// so the SVG source can be inlined into the welcome page DOM via DOMParser
|
||||
// + replaceChildren (see populateMark below). The CSS `color` property is
|
||||
// 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
|
||||
// the `.welcome-hero__mark` wrapper sets `color: var(--mks-fg-inverse)`
|
||||
// (welcome.css:67), and on a DARK surface the `.dark` token override
|
||||
// (src/shared/tokens.css lines 234-251) flips `--mks-fg-inverse` so the
|
||||
// inline `<svg>` inherits the contrast-correct stroke without any
|
||||
// JS/event-handler/conditional code. An `<img>`-injected SVG would 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.
|
||||
// `stroke="currentColor"` (Plan 04-06 Wave 1 SVG edit); the
|
||||
// `.welcome-hero__mark` wrapper sets `color: var(--mks-mark-stroke)`
|
||||
// (welcome.css), which is a theme-INDEPENDENT brand-component token
|
||||
// always resolving to linen-50 (tokens.css :root, NOT overridden in the
|
||||
// .dark/[data-theme="dark"] block). The mark sits on a theme-independent
|
||||
// madder-600 circle (--mks-rec); a theme-coupled stroke would render
|
||||
// muddy ink-on-madder in dark mode (see Plan 04-06 operator-empirical
|
||||
// Task 4 debug-decouple resolution). An `<img>`-injected SVG would
|
||||
// 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:
|
||||
// - 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
|
||||
* empirically + W3C SVG2 §13.3. By inlining via DOMParser +
|
||||
* replaceChildren, the inline <svg>'s `stroke="currentColor"` resolves
|
||||
* to the wrapper's `color: var(--mks-fg-inverse)` (welcome.css:67),
|
||||
* and on the dark surface the `.dark` token override
|
||||
* (src/shared/tokens.css lines 234-251) automatically flips the
|
||||
* resolved colour — contrast-correct on both surfaces, no JS branching.
|
||||
* to the wrapper's `color: var(--mks-mark-stroke)` (welcome.css), and
|
||||
* because --mks-mark-stroke is a theme-INDEPENDENT brand-component
|
||||
* token (tokens.css :root, NOT overridden in .dark/[data-theme="dark"])
|
||||
* 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
|
||||
* string-assignment path is FORBIDDEN — DOMParser + replaceChildren
|
||||
|
||||
@@ -544,15 +544,19 @@ async function main(): Promise<number> {
|
||||
// RUN — not env-gated; the 5-min wait is A33's, not A34's).
|
||||
{ name: 'A34', drive: driveA34Wrapped },
|
||||
// Plan 04-06 A35: UI-SPEC dark-logo `currentColor` strategy LIVE-DOM
|
||||
// proof. Opens welcome.html as a real Puppeteer tab so populateMark()
|
||||
// actually runs; reads getComputedStyle().stroke on the injected
|
||||
// <svg> to verify the currentColor cascade resolves through
|
||||
// .welcome-hero__mark color: var(--mks-fg-inverse) (UI-SPEC Option A).
|
||||
// Appended LAST in the 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).
|
||||
// proof + Plan 04-06 Task 4 decouple verification. Opens welcome.html
|
||||
// as a real Puppeteer tab so populateMark() actually runs; reads
|
||||
// getComputedStyle().stroke on the injected <svg> in BOTH light and
|
||||
// dark themes to verify (a) the currentColor cascade resolves
|
||||
// through .welcome-hero__mark color: var(--mks-mark-stroke), and
|
||||
// (b) the theme-INDEPENDENT --mks-mark-stroke token is NOT
|
||||
// overridden by the .dark/[data-theme="dark"] override block — both
|
||||
// probes resolve to the same linen-50 RGB. Appended LAST in the
|
||||
// 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 },
|
||||
];
|
||||
|
||||
|
||||
@@ -2961,20 +2961,29 @@ export async function driveA34(
|
||||
// chrome.runtime.getURL('src/welcome/welcome.html') resolves to — so
|
||||
// welcome.ts init() runs populateMark() at DOMContentLoaded and the
|
||||
// inline <svg> actually lands in the page DOM. We then read
|
||||
// getComputedStyle().stroke on the injected <svg> to prove the
|
||||
// `currentColor` cascade resolves through the .welcome-hero__mark
|
||||
// wrapper's `color: var(--mks-fg-inverse)` rule.
|
||||
// getComputedStyle().stroke on the injected <svg> in BOTH the LIGHT
|
||||
// default theme AND the DARK theme (the latter set via
|
||||
// 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
|
||||
// claimed live-DOM injection was delegated to A17.8, but A17.8 is
|
||||
// 100% string-grep on the welcome JS chunk and the harness does NOT
|
||||
// open welcome.html as a live tab (it only fetches the HTML text via
|
||||
// chrome.runtime.getURL + DOMParser.parseFromString — a DETACHED
|
||||
// parse, not a live document). driveA35 is the canonical automated
|
||||
// proof of the runtime injection + cascade. A35 is appended LAST in
|
||||
// the drivers array; nothing reads its return value, so the new tab
|
||||
// cannot pollute later drivers (welcomePage.close() in finally also
|
||||
// guarantees no tab leak).
|
||||
// This is the iter-2 BLOCKER 1 resolution + Plan 04-06 Task 4 decouple
|
||||
// extension: the prior iter-1 re-plan claimed live-DOM injection was
|
||||
// delegated to A17.8, but A17.8 is 100% string-grep on the welcome JS
|
||||
// chunk and the harness does NOT open welcome.html as a live tab (it
|
||||
// only fetches the HTML text via chrome.runtime.getURL +
|
||||
// DOMParser.parseFromString — a DETACHED parse, not a live document).
|
||||
// driveA35 is the canonical automated proof of the runtime injection +
|
||||
// cascade + decouple. A35 is appended LAST in the drivers array;
|
||||
// nothing reads its return value, so the new tab cannot pollute later
|
||||
// drivers (welcomePage.close() in finally also guarantees no tab leak).
|
||||
//
|
||||
// Pattern: HOST-SIDE driver (mirrors driveA32/driveA33/driveA34 — NOT
|
||||
// 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/uat/extension-page-harness.ts A17.8 (the source-bundling
|
||||
// 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
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* `chrome-extension://<id>/src/welcome/welcome.html` (the canonical
|
||||
* web-accessible welcome page path; the same path A17 already fetches),
|
||||
* waits for `populateMark()` to inject the inline <svg> at
|
||||
* DOMContentLoaded, then runs a single `welcomePage.evaluate(...)` that
|
||||
* reads the LIVE DOM and returns the four signals A35 asserts on:
|
||||
* - svgPresent — `.welcome-hero__mark svg` exists (inline <svg>
|
||||
* injected).
|
||||
* - strokeAttr — that <svg>'s `stroke` attribute === 'currentColor'
|
||||
* (the canonical mark recolour landed correctly).
|
||||
* - computedStroke — `getComputedStyle(svgEl).stroke` is a resolved
|
||||
* non-default colour value (the `currentColor`
|
||||
* cascade resolved through
|
||||
* `.welcome-hero__mark { color: var(--mks-fg-inverse) }`).
|
||||
* Empty / 'none' would mean the cascade is broken.
|
||||
* - imgPresent — `.welcome-hero__mark img` is NULL (the legacy
|
||||
* <img> injection path is gone; we're inlining now).
|
||||
* DOMContentLoaded, then probes the LIVE DOM in BOTH the LIGHT default
|
||||
* theme AND the DARK theme (toggled by setting data-theme="dark" on the
|
||||
* documentElement, which fires the `.dark, [data-theme="dark"]` block
|
||||
* in tokens.css). Five sub-checks:
|
||||
* A35.1 — `.welcome-hero__mark svg` injected (populateMark ran).
|
||||
* A35.2 — that <svg> carries stroke="currentColor" (Option A recolor).
|
||||
* A35.3 — getComputedStyle(<svg>).stroke resolves to a non-default
|
||||
* colour in LIGHT (cascade through
|
||||
* `.welcome-hero__mark { color: var(--mks-mark-stroke) }`
|
||||
* actually works).
|
||||
* A35.4 — no legacy <img> in the slot (pre-04-06 path is gone).
|
||||
* A35.5 — Plan 04-06 Task 4 DECOUPLE PROOF: the computed stroke in
|
||||
* 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
|
||||
* 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');
|
||||
|
||||
const probe = await 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 };
|
||||
});
|
||||
// LIGHT probe (default theme; remove any prior data-theme).
|
||||
const lightProbe = await a35Probe(welcomePage, false);
|
||||
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 =
|
||||
probe.computedStroke.length > 0 && probe.computedStroke !== 'none';
|
||||
// DARK probe (set data-theme="dark" on <html>; same selector / same
|
||||
// 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({
|
||||
name: 'A35.1: inline <svg> injected into `.welcome-hero__mark` slot (populateMark ran)',
|
||||
expected: 'non-null `.welcome-hero__mark svg`',
|
||||
actual: probe.svgPresent ? 'present' : 'missing',
|
||||
passed: probe.svgPresent,
|
||||
actual: lightProbe.svgPresent ? 'present' : 'missing',
|
||||
passed: lightProbe.svgPresent,
|
||||
});
|
||||
checks.push({
|
||||
name: 'A35.2: injected <svg> carries stroke="currentColor" (UI-SPEC Option A recolor)',
|
||||
expected: 'currentColor',
|
||||
actual: probe.strokeAttr,
|
||||
passed: probe.strokeAttr === 'currentColor',
|
||||
actual: lightProbe.strokeAttr,
|
||||
passed: lightProbe.strokeAttr === 'currentColor',
|
||||
});
|
||||
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',
|
||||
actual: probe.computedStroke,
|
||||
passed: computedStrokeResolved,
|
||||
actual: lightProbe.computedStroke,
|
||||
passed: lightStrokeResolved,
|
||||
});
|
||||
checks.push({
|
||||
name: 'A35.4: no legacy <img> in `.welcome-hero__mark` slot (the pre-04-06 <img> injection path is gone)',
|
||||
expected: 'null',
|
||||
actual: probe.imgPresent ? 'present (UNEXPECTED — legacy <img> still injected)' : 'null',
|
||||
passed: !probe.imgPresent,
|
||||
actual: lightProbe.imgPresent ? 'present (UNEXPECTED — legacy <img> still injected)' : 'null',
|
||||
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);
|
||||
return {
|
||||
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,
|
||||
diagnostics,
|
||||
};
|
||||
@@ -3104,7 +3181,7 @@ export async function driveA35(
|
||||
diagnostics.push(`A35 THREW: ${errMsg}`);
|
||||
return {
|
||||
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,
|
||||
diagnostics,
|
||||
error: errMsg,
|
||||
|
||||
@@ -13,14 +13,17 @@
|
||||
// It does NOT instantiate any document, does NOT call populateMark, does
|
||||
// NOT invoke DOMParser at runtime. The runtime behavior (live <svg>
|
||||
// injection into the welcome page DOM and the currentColor CSS cascade
|
||||
// through `.welcome-hero__mark { color: var(--mks-fg-inverse); }`) is
|
||||
// verified by the host-side UAT harness assertion A35 (driveA35 in
|
||||
// tests/uat/lib/harness-page-driver.ts) — A35 opens welcome.html as a real
|
||||
// Puppeteer tab so populateMark() actually runs, then reads
|
||||
// `getComputedStyle().stroke` on the injected <svg> to prove the cascade
|
||||
// resolves. 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).
|
||||
// through `.welcome-hero__mark { color: var(--mks-mark-stroke); }` — the
|
||||
// theme-INDEPENDENT brand-component token introduced by Plan 04-06 Task 4
|
||||
// operator-empirical debug-decouple) is verified by the host-side UAT
|
||||
// harness assertion A35 (driveA35 in tests/uat/lib/harness-page-driver.ts)
|
||||
// — A35 opens welcome.html as a real Puppeteer tab so populateMark()
|
||||
// actually runs, then reads `getComputedStyle().stroke` on the injected
|
||||
// <svg> in BOTH light and dark themes to prove (a) the cascade resolves
|
||||
// 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:
|
||||
// - .planning/phases/04-harden-clean-up-optional/04-UI-SPEC.md
|
||||
|
||||
Reference in New Issue
Block a user