Milestone v1 (v2.0.0): Mokosh — Session Capture #1
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-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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user