prove the extraction faithful, and fix the one thing that was not

The screenshots said seven dark screens differed and could not say why. Comparing
COMPUTED theme variables between the old build and the new one, in the same colour
mode, answered it in a single run: 47 of 53 matched exactly, and the six that did not
were the `--duck-*` variables — EMPTY in the new build.

The utilities were remapped to `--brand-primary`, `--ink` and friends, which is
correct. What that missed is code reading the VARIABLE rather than using the utility.
`prose.css` does exactly that eleven times — chat markdown links, blockquotes, inline
code, checkboxes — and `landing/styles.css` does it six more. All silently resolved to
nothing.

Fixed by keeping the legacy names as aliases in every theme, pointing at the new
source of truth rather than duplicating values. That also covers any plugin or inline
style doing the same thing, which I have not enumerated. They retire when the utility
rename lands.

Every variable now matches.

── What the screenshots are actually worth here ──

Less than I assumed. The wallpaper rotates per page load, so a control capture of the
same build against itself had 4 of 16 screens differ — and a different 4 each time.
Between 5 and 6 come out byte-identical to the baseline on any run; the rest is noise
I mistook for signal for two rounds, including one where I removed a `color-scheme`
line to test a theory it turned out not to explain.

The variable diff is the instrument that settles it, and it is cheap. Added as
`scripts/theme-vardiff.ts`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 22:35:00 +00:00
co-authored by Claude Opus 5
parent 105766a4eb
commit 1be4bcd39a
5 changed files with 176 additions and 0 deletions
+20
View File
@@ -103,11 +103,31 @@ was not in tonight's scope.
---
## On verification, and what the screenshots can and cannot tell you
The byte comparison is **dominated by wallpaper noise**. The dashboard background rotates per page load, so
any screen showing it differs from itself between runs — a control capture of the same build against itself
had 4 of 16 differ, and which 4 changes each time. Between 5 and 6 screens come out byte-identical to the
pre-refactor baseline on any given run; the rest are the wallpaper.
So the authoritative instrument is `scripts/theme-vardiff.ts`, which compares the COMPUTED value of every
theme variable between the old and new builds in the same colour mode. It reports **all 53 identical**.
That is a much stronger statement than a screenshot: it says the token layer resolves to exactly the values
the old stylesheet produced.
It also found the one real regression, which no amount of looking would have: six `--duck-*` variables came
out EMPTY in the new build. The utilities were remapped, but `prose.css` reads `var(--duck-teal)` DIRECTLY
in eleven places for chat markdown — links, blockquotes, inline code, checkboxes — and `landing/styles.css`
does the same. Fixed by keeping the legacy names as aliases in every theme, which also protects any plugin
or inline style doing the same thing. They retire with the utility rename.
## Tools built
- `scripts/theme-snapshot.ts` — captures 8 routes × 2 modes, or a pinned theme with `--theme`. Compares two
directories byte-for-byte. Its first version silently photographed light twice; it now warns when a
light/dark pair is identical.
- `scripts/theme-vardiff.ts` — computed theme variables, old build vs new, side by side. The instrument
that actually proved the extraction faithful.
- `scripts/theme-preview-server.ts` — serves this worktree's build against the RUNNING platform's API, so
the new frontend can be seen with real data without starting a second platform beside the live one.
+111
View File
@@ -0,0 +1,111 @@
/**
* Compare the computed theme variables of two running builds.
*
* Screenshots say THAT something changed; this says WHICH VARIABLE changed, which is the question worth
* asking after an extraction. Seven dark screens differed between the old stylesheet and the new one, and
* staring at two near-identical dark screenshots is not a way to find a hue that moved by 2%.
*
* bun scripts/theme-vardiff.ts <token> [oldUrl] [newUrl]
*/
import { chromium } from 'playwright';
const token = process.argv[2];
if (!token) {
console.log(' usage: bun scripts/theme-vardiff.ts <jwt> [oldUrl] [newUrl]');
process.exit(1);
}
const oldUrl = process.argv[3] ?? 'http://127.0.0.1:9000';
const newUrl = process.argv[4] ?? 'http://127.0.0.1:9010';
const VARS = [
'--background',
'--foreground',
'--card',
'--card-foreground',
'--popover',
'--popover-foreground',
'--primary',
'--primary-foreground',
'--secondary',
'--secondary-foreground',
'--muted',
'--muted-foreground',
'--accent',
'--accent-foreground',
'--destructive',
'--destructive-foreground',
'--success',
'--success-foreground',
'--warning',
'--warning-foreground',
'--info',
'--info-foreground',
'--border',
'--input',
'--ring',
'--brand',
'--brand-foreground',
'--brand-muted',
'--radius',
'--cta',
'--cta-foreground',
'--ctahover',
'--naturegreen',
'--naturegreen-foreground',
'--duck-teal',
'--duck-dark',
'--duck-yellow',
'--duck-forest',
'--duck-beige',
'--duck-orange',
'--glass-bg',
'--glass-border',
'--dock-bg',
'--dock-border',
'--dock-tooltip-bg',
'--card-bg',
'--card-grid-color',
'--pixel-grid-color',
'--chart-primary',
'--chart-secondary',
'--status-success',
'--status-warning',
'--status-in-progress',
];
const browser = await chromium.launch();
async function read(url: string, extra: string): Promise<Record<string, string>> {
const context = await browser.newContext({ viewport: { width: 1000, height: 700 } });
await context.addInitScript(`localStorage.setItem('BEARER_TOKEN', ${JSON.stringify(token)}); ${extra}`);
const page = await context.newPage();
// `commit` rather than `load`: this app holds websockets open, so waiting for a quiet network can hang.
await page.goto(`${url}/jobs`, { waitUntil: 'commit', timeout: 20_000 });
await page.waitForTimeout(2500);
const values = await page.evaluate((vars: string[]) => {
const cs = getComputedStyle(document.documentElement);
const out: Record<string, string> = {};
for (const v of vars) out[v] = cs.getPropertyValue(v).trim();
return out;
}, VARS);
await context.close();
return values;
}
// The OLD build spoke in colour modes; the NEW one in themes. Same appearance, different vocabulary — which
// is the entire refactor, and why the two sides are set up differently here.
const before = await read(oldUrl, `localStorage.setItem('officer-color-mode','dark');`);
const after = await read(newUrl, `localStorage.setItem('officer-theme','duck-dark');`);
await browser.close();
let differing = 0;
for (const key of VARS) {
const a = before[key] ?? '';
const b = after[key] ?? '';
if (a !== b) {
console.log(` DIFF ${key.padEnd(24)} old=${JSON.stringify(a)} new=${JSON.stringify(b)}`);
differing += 1;
}
}
console.log(differing === 0 ? ' every variable matches' : ` ${differing} of ${VARS.length} differ`);
process.exit(0);
@@ -28,6 +28,21 @@
--ink: #f1f5f9;
--surface-warm: #292524;
/* ── Legacy variable names, for direct `var()` consumers ──────────────────────────────────────
The `duck-*` UTILITIES are remapped in base.css, but a stylesheet reading `var(--duck-teal)` gets
nothing from that — it wants the variable itself. `prose.css` does exactly this eleven times for chat
markdown (links, blockquotes, inline code, checkboxes), and the rename silently emptied all of them.
Caught by diffing computed variables between the old and new builds, not by reading.
Kept as aliases rather than as values: there is one source of truth above, and these follow it. They
retire when nothing reads them — the same migration as the utility names. */
--duck-teal: var(--brand-primary);
--duck-dark: var(--ink);
--duck-yellow: var(--brand-accent);
--duck-forest: var(--brand-deep);
--duck-beige: var(--surface-warm);
--duck-orange: var(--brand-warm);
/* ── The page ─────────────────────────────────────────────────────────────────────────────────── */
--page-bg: hsl(222.2 84% 4.9%);
--pixel-grid-color: rgba(200, 230, 210, 0.08);
@@ -29,6 +29,21 @@
--ink: #14532d;
--surface-warm: #e7d4b5;
/* ── Legacy variable names, for direct `var()` consumers ──────────────────────────────────────
The `duck-*` UTILITIES are remapped in base.css, but a stylesheet reading `var(--duck-teal)` gets
nothing from that — it wants the variable itself. `prose.css` does exactly this eleven times for chat
markdown (links, blockquotes, inline code, checkboxes), and the rename silently emptied all of them.
Caught by diffing computed variables between the old and new builds, not by reading.
Kept as aliases rather than as values: there is one source of truth above, and these follow it. They
retire when nothing reads them — the same migration as the utility names. */
--duck-teal: var(--brand-primary);
--duck-dark: var(--ink);
--duck-yellow: var(--brand-accent);
--duck-forest: var(--brand-deep);
--duck-beige: var(--surface-warm);
--duck-orange: var(--brand-warm);
/* ── The page ─────────────────────────────────────────────────────────────────────────────────
A complete `background` value, so a theme can be an image or a gradient rather than a colour. */
--page-bg: hsl(0 0% 100%);
+15
View File
@@ -31,6 +31,21 @@
--ink: #000000;
--surface-warm: #ffffff;
/* ── Legacy variable names, for direct `var()` consumers ──────────────────────────────────────
The `duck-*` UTILITIES are remapped in base.css, but a stylesheet reading `var(--duck-teal)` gets
nothing from that — it wants the variable itself. `prose.css` does exactly this eleven times for chat
markdown (links, blockquotes, inline code, checkboxes), and the rename silently emptied all of them.
Caught by diffing computed variables between the old and new builds, not by reading.
Kept as aliases rather than as values: there is one source of truth above, and these follow it. They
retire when nothing reads them — the same migration as the utility names. */
--duck-teal: var(--brand-primary);
--duck-dark: var(--ink);
--duck-yellow: var(--brand-accent);
--duck-forest: var(--brand-deep);
--duck-beige: var(--surface-warm);
--duck-orange: var(--brand-warm);
/* ── The page ─────────────────────────────────────────────────────────────────────────────────
Flat white and no grid. If a pixel grid or a wallpaper appears under this theme, something is drawing
it from outside the theme layer. */