Files
open-design/scripts/design-system-batch-report.test.ts
T
chaoxiaocheandCursor a9dca0cdce feat(scripts): add design-system batch reporter
Generates a single-file control-vs-treatment HTML report from two
batch-design-system-test (#1515) JSONL outputs. For each brand that
appears in both arms, the reporter:

- pulls the agent-produced HTML artifact from the daemon (via the
  existing /api/projects/:id/files + /api/projects/:id/raw/* routes)
- renders a desktop screenshot via headless chromium (playwright-core
  added to root devDeps; Chromium binary is reused from the e2e
  workspace's existing Playwright install)
- runs token-usage analysis on the artifact:
  - varRefCount: total var(--*) references outside the :root paste
  - hardcodedColorCount: literal #hex / rgb / hsl / oklab values
  - tokenHitRate: varRefs / (varRefs + literals)
  - brandTokenRecall: fraction of the brand's declared tokens that
    the artifact actually references (loaded from the brand's
    structured tokens.css if present)

The output bundle is self-contained — report.html with embedded
screenshots, plus data.json and the raw downloaded artifacts /
PNGs — so it can be attached to a PR or shared in chat without
re-running the batch.

Why now: PR-D (#1544) made the structured-token channel default-on
and PR-C (#1385) added the prerequisite tokens.css/components.html
extension point. Track A is hand-authoring 5 high-priority brands
(cursor first in #1652). Once enough brands have structured assets,
we can run a full control-vs-treatment batch and need a way to see
the result without scrolling 7×2 raw artifact tabs.

The reporter is also useful before all 5 brands ship — running
control vs treatment on default + kami today already validates the
end-to-end pipeline.

Verified:
- pnpm guard — 13/13 design-system + style-policy checks pass
- 11/11 unit tests pass:
  - 5 new analyzer / renderer tests in
    scripts/design-system-batch-report.test.ts:
    - analyzer reports near-perfect hit rate on default's reference
      fixture (varRefCount > 20, hardcodedColorCount = 0,
      tokenHitRate = 1.0, brandTokenRecall ≥ 0.5)
    - same for kami
    - analyzer downgrades a synthetic literal-heavy artifact
      (4 literals, 0 var refs → tokenHitRate = 0)
    - analyzer ignores literals INSIDE the :root paste (so brand
      :root values do not double-count as hardcoded)
    - renderReportHtml emits a self-contained HTML doc with
      sign-aware delta cells and key data fields
  - 6 existing style-policy.test.ts tests
- pnpm exec tsc -p scripts/tsconfig.json --noEmit clean

Open questions deferred to follow-up:
- Daemon-API integration tested only via fetch types; first real
  end-to-end batch + report run will happen in track C1 once 5
  structured brands are in.
- Mobile viewport rendering deferred (per-batch desktop-only was
  the agreed scope).
- Token-usage analyzer is regex-based (good enough for scoring;
  not a real CSS parser). Promote to a parser-based version if
  artifacts start using nested CSS or complex calc() expressions.

Usage:
  OD_DESIGN_TOKEN_CHANNEL=0 node --experimental-strip-types \\
    scripts/batch-design-system-test.ts \\
    --prompt "..." --design-systems default,kami \\
    --output .tmp/batch-control.jsonl

  node --experimental-strip-types scripts/batch-design-system-test.ts \\
    --prompt "..." --design-systems default,kami \\
    --output .tmp/batch-treatment.jsonl

  node --experimental-strip-types scripts/design-system-batch-report.ts \\
    --control .tmp/batch-control.jsonl \\
    --treatment .tmp/batch-treatment.jsonl \\
    --out .tmp/design-system-batch-report

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 15:07:41 +08:00

180 lines
7.0 KiB
TypeScript

// Sanity checks on the design-system-batch-report analyzer + report
// renderer, run as part of `pnpm guard` (see package.json `guard` script).
//
// Concretely we want three guarantees in CI:
//
// 1. The analyzer says "near-perfect" on each brand's reference
// components.html. If the reference fixture itself has hardcoded
// colors outside the :root paste, the structured channel is broken
// at its source — agents will copy those literals into artifacts.
//
// 2. The analyzer correctly DOWNGRADES a synthetic bad artifact (one
// that hardcodes hex colors and doesn't reference any tokens).
// This pins the analyzer's discriminative power: if the formula
// ever flips signs, the test breaks.
//
// 3. renderReportHtml emits a self-contained HTML document with the
// key data fields (control vs treatment delta, summary cards) so
// the report stays usable without re-running batch tests.
import { access, readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import assert from 'node:assert/strict';
import {
analyzeTokenUsage,
loadBrandTokens,
renderReportHtml,
} from './design-system-batch-report.ts';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(HERE, '..');
async function readBrandFixture(brand: string): Promise<string> {
return await readFile(path.join(REPO_ROOT, 'design-systems', brand, 'components.html'), 'utf8');
}
/** Discover every brand on this branch that ships a components.html. */
async function discoverStructuredBrands(): Promise<string[]> {
const root = path.join(REPO_ROOT, 'design-systems');
const entries = await readdir(root, { withFileTypes: true });
const brands: string[] = [];
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('_')) continue;
const fixture = path.join(root, entry.name, 'components.html');
try {
await access(fixture);
brands.push(entry.name);
} catch {
// Brand has no structured fixture on this branch — fine, skip.
}
}
return brands;
}
const structuredBrands = await discoverStructuredBrands();
assert.ok(structuredBrands.length >= 2, `expected ≥2 structured brands, got ${structuredBrands.length}: ${structuredBrands.join(', ')}`);
for (const brand of structuredBrands) {
test(`analyzer reports near-perfect hit rate on ${brand}'s reference fixture`, async () => {
const [html, tokens] = await Promise.all([readBrandFixture(brand), loadBrandTokens(brand)]);
const usage = analyzeTokenUsage(html, tokens);
assert.ok(
usage.varRefCount > 20,
`${brand} fixture should reference 20+ tokens, got ${usage.varRefCount}`,
);
// The reference fixtures intentionally use NO literal colors outside
// their :root paste. If a fixture starts leaking literals, agents
// pattern-matching it will copy those literals into artifacts.
assert.equal(
usage.hardcodedColorCount,
0,
`${brand} fixture leaks ${usage.hardcodedColorCount} literal colors outside :root: ` +
usage.hardcodedColorSamples.join(', '),
);
assert.equal(usage.tokenHitRate, 1, `${brand} hit rate should be 1.0, got ${usage.tokenHitRate}`);
assert.ok(
usage.brandTokenRecall >= 0.5,
`${brand} reference fixture should use ≥50% of declared tokens, got ${usage.brandTokenRecall}`,
);
});
}
test('analyzer downgrades a synthetic literal-heavy artifact', () => {
const html = `
<html>
<head>
<style>
.danger { color: #ff0000; background: rgb(255, 200, 0); border: 1px solid hsl(120, 80%, 50%); }
</style>
</head>
<body>
<div style="color: #abcdef">Hi</div>
</body>
</html>
`;
const tokens = { declared: new Set(['fg', 'bg', 'accent']), hasStructuredTokens: true };
const usage = analyzeTokenUsage(html, tokens);
assert.equal(usage.varRefCount, 0);
// Three literal colors in the <style> (#ff0000, rgb(...), hsl(...)) +
// one in the inline style attribute (#abcdef).
assert.equal(usage.hardcodedColorCount, 4);
assert.equal(usage.tokenHitRate, 0);
assert.equal(usage.brandTokenRecall, 0);
assert.equal(usage.brandTokensMissed.length, 3);
});
test('analyzer ignores literals INSIDE the :root paste', () => {
// Agents that paste a brand's :root block verbatim will technically
// include the brand's own #hex values inside :root. Those should NOT
// be counted as "hardcoded" since they are the brand's source of truth.
const html = `
<style>
:root { --fg: #111111; --bg: #fafafa; }
.card { color: var(--fg); background: var(--bg); }
</style>
`;
const tokens = { declared: new Set(['fg', 'bg']), hasStructuredTokens: true };
const usage = analyzeTokenUsage(html, tokens);
assert.equal(usage.hardcodedColorCount, 0, 'literals inside :root must not count');
assert.equal(usage.varRefCount, 2);
assert.equal(usage.tokenHitRate, 1);
assert.equal(usage.brandTokenRecall, 1);
});
test('renderReportHtml produces a self-contained HTML report with key data', () => {
const fakeUsage = (varRefs: number, literals: number, recall: number) => ({
varRefCount: varRefs,
uniqueVarRefs: ['fg', 'bg'].slice(0, Math.min(2, varRefs)),
hardcodedColorCount: literals,
hardcodedColorSamples: literals > 0 ? ['#ff0000'] : [],
brandTokenRecall: recall,
brandTokensUsed: recall > 0 ? ['fg', 'bg'] : [],
brandTokensMissed: recall < 1 ? ['accent'] : [],
tokenHitRate: varRefs / Math.max(1, varRefs + literals),
});
const rows = [
{
brand: 'default',
control: {
brand: 'default',
arm: 'control' as const,
projectId: 'p1',
primaryFile: null,
htmlBytes: 1234,
htmlPath: 'artifacts/default-control.html',
screenshotPath: 'screenshots/default-control.png',
usage: fakeUsage(10, 4, 0.5),
},
treatment: {
brand: 'default',
arm: 'treatment' as const,
projectId: 'p2',
primaryFile: null,
htmlBytes: 1500,
htmlPath: 'artifacts/default-treatment.html',
screenshotPath: 'screenshots/default-treatment.png',
usage: fakeUsage(20, 0, 1.0),
},
delta: { tokenHitRate: 0.286, brandTokenRecall: 0.5, hardcodedColorCount: -4 },
},
];
const html = renderReportHtml(rows, {
generatedAt: '2026-05-14T10:00:00Z',
controlPath: '.tmp/control.jsonl',
treatmentPath: '.tmp/treatment.jsonl',
});
assert.match(html, /<!doctype html>/i);
assert.match(html, /Design system batch/);
assert.match(html, /default/);
assert.match(html, /default-control\.png/);
assert.match(html, /default-treatment\.png/);
assert.match(html, /\+50pp/, 'positive recall delta must render with explicit + sign');
assert.match(html, /-4/, 'negative literal-color delta should render with - sign');
assert.match(html, /Brands compared/);
assert.match(html, /delta-pos/, 'positive delta should carry the delta-pos class');
assert.match(html, /delta-neg/, 'negative delta should carry the delta-neg class');
});