/** * Shared rendering for the PBL v2 A/B comparison viewer. * * Two consumers: * - `build-compare.ts` / the runner → `buildCompareHtml(runDir)` writes a * self-contained static `compare.html` (data embedded). * - `serve.ts` → a small dev server that lists every run under `results/` * and loads each run's JSON on demand (data fetched). * * Both share `VIEWER_CSS` + `CLIENT_RENDER_JS` so the rendering stays in one * place. `collectRunData(runDir)` reads `projects/__.json` * plus the optional `results.json` (judge scores) into one blob. */ import { readFileSync, writeFileSync, readdirSync, existsSync } from 'fs'; import { join } from 'path'; export interface EmbeddedVariant { project?: unknown; result?: unknown; } export interface EmbeddedCase { id: string; variants: Record; } export interface EmbeddedData { runDir: string; cases: EmbeddedCase[]; } function safeReadJson(path: string): unknown | undefined { try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return undefined; } } /** Collect `__.json` + results.json for a run into one blob. */ export function collectRunData(runDir: string): EmbeddedData { const projectsDir = join(runDir, 'projects'); const cases = new Map(); if (existsSync(projectsDir)) { for (const file of readdirSync(projectsDir)) { if (!file.endsWith('.json')) continue; const base = file.slice(0, -'.json'.length); const sep = base.lastIndexOf('__'); if (sep === -1) continue; const caseId = base.slice(0, sep); const variant = base.slice(sep + 2); if (!cases.has(caseId)) cases.set(caseId, { id: caseId, variants: {} }); cases.get(caseId)!.variants[variant] = { ...cases.get(caseId)!.variants[variant], project: safeReadJson(join(projectsDir, file)), }; } } const results = safeReadJson(join(runDir, 'results.json')); if (Array.isArray(results)) { for (const r of results as Array>) { const caseId = String(r.caseId ?? ''); const variant = String(r.variant ?? ''); if (!caseId || !variant) continue; if (!cases.has(caseId)) cases.set(caseId, { id: caseId, variants: {} }); const v = cases.get(caseId)!.variants[variant] ?? {}; v.result = r; cases.get(caseId)!.variants[variant] = v; } } return { runDir, cases: [...cases.values()].sort((a, b) => a.id.localeCompare(b.id)), }; } /** Embed JSON safely inside a `; } export function buildCompareHtml(runDir: string): string { const data = collectRunData(runDir); const outPath = join(runDir, 'compare.html'); writeFileSync(outPath, staticTemplate(data), 'utf-8'); return outPath; }