/**
* test-visualizer — throwaway one-pager.
*
* Reads the latest output/run-*.json (or one supplied on argv) and
* writes a single clean output/test-visualizer.html that puts the
* answer front and centre with provenance below. Intentionally minimal
* — no replay, no filters, no JS. Will be deleted once a real UI
* exists; do not invest in this.
*
* npm run viz
* tsx scripts/test-visualizer.ts output/run-2026-05-24T....json
*/
import { readdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUTPUT_DIR = path.resolve(__dirname, '..', 'output');
async function pickLatest(): Promise<string | null> {
const entries = await readdir(OUTPUT_DIR);
const runs = entries
.filter(f => (f.startsWith('run-') || f.startsWith('ask-')) && f.endsWith('.json'))
.sort();
const last = runs.at(-1);
return last ? path.join(OUTPUT_DIR, last) : null;
}
function esc(v: unknown): string {
return String(v ?? '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
function fmtUsd(n: number): string {
if (!Number.isFinite(n)) return '—';
const abs = Math.abs(n);
if (abs >= 1e12) return `$${(n / 1e12).toFixed(2)}T`;
if (abs >= 1e9) return `$${(n / 1e9).toFixed(2)}B`;
if (abs >= 1e6) return `$${(n / 1e6).toFixed(2)}M`;
return `$${n.toLocaleString()}`;
}
interface OutputRecord {
canonicalEntity?: string;
canonicalMetric?: string;
period?: string;
value?: number | null;
canonicalUnit?: string;
rawLabel?: string;
sourceUrl?: string;
confidence?: number;
}
interface ToolCall {
toolName: string;
ok: boolean;
input?: Record<string, unknown>;
resultSummary?: string;
errorMessage?: string;
}
function findToolCalls(audit: any): ToolCall[] {
const records = audit?.repositorySnapshot?.records ?? [];
for (const r of records) {
const tc = r?.metadata?.toolCalls;
if (Array.isArray(tc)) return tc;
}
return [];
}
function render(audit: any): string {
const job = audit.jobRequest ?? {};
const question = job.question ?? '(no question)';
const records: OutputRecord[] = audit?.finalHandoff?.payload?.records ?? [];
const tools = findToolCalls(audit);
const ok = audit.ok === true;
const elapsed = typeof audit.elapsedMs === 'number'
? `${(audit.elapsedMs / 1000).toFixed(1)}s`
: '—';
const status = ok ? 'OK' : 'FAILED';
const escalations = (audit.escalations ?? []).length;
const pipeline = (audit.pipeline ?? [])
.map((p: any) => (p.kind === 'agent' ? `${p.pillar}/${p.agent}` : `[${p.name}]`))
.join(' → ');
const rows = records.map(r => {
const value = typeof r.value === 'number'
? (r.canonicalUnit === 'USD' ? fmtUsd(r.value) : `${r.value.toLocaleString()} ${esc(r.canonicalUnit ?? '')}`)
: '<span style="color:#999">null</span>';
const conf = typeof r.confidence === 'number' ? r.confidence.toFixed(2) : '—';
const src = r.sourceUrl
? `<a href="${esc(r.sourceUrl)}" target="_blank">source</a>`
: '—';
return `
<tr>
<td>${esc(r.canonicalEntity)}</td>
<td>${esc(r.canonicalMetric)}</td>
<td>${esc(r.period)}</td>
<td class="num">${value}</td>
<td><code>${esc(r.rawLabel)}</code></td>
<td>${conf}</td>
<td>${src}</td>
</tr>`;
}).join('');
const toolRows = tools.map(t => {
const args = Object.entries(t.input ?? {})
.map(([k, v]) => `${esc(k)}=${esc(JSON.stringify(v))}`)
.join(', ');
const summary = t.ok
? esc(t.resultSummary ?? 'ok')
: `<span class="err">ERROR: ${esc(t.errorMessage ?? 'unknown')}</span>`;
return `
<tr class="${t.ok ? '' : 'err-row'}">
<td><code>${esc(t.toolName)}</code></td>
<td><code class="args">${args}</code></td>
<td>${summary}</td>
</tr>`;
}).join('');
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>BID test visualizer — ${esc(audit.analysisId ?? '')}</title>
<style>
:root {
--fg: #1a1a1a; --muted: #666; --bg: #fafafa; --card: #fff;
--accent: #0b5; --err: #c33; --border: #e5e5e5;
}
* { box-sizing: border-box; }
body {
margin: 0; padding: 32px;
font: 15px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
color: var(--fg); background: var(--bg);
}
.wrap { max-width: 980px; margin: 0 auto; }
header { margin-bottom: 24px; }
h1 { font-size: 13px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin: 0 0 8px; }
.question { font-size: 22px; font-weight: 500; line-height: 1.35; margin: 0 0 14px; }
.meta { color: var(--muted); font-size: 13px; }
.meta span { margin-right: 18px; }
.ok { color: var(--accent); font-weight: 600; }
.err { color: var(--err); font-weight: 600; }
.card {
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
padding: 20px 24px; margin: 18px 0;
}
.card h2 { font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin: 0 0 14px; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
th { font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
td.num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
code { font: 13px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; color: #444; }
code.args { color: #666; word-break: break-all; }
tr.err-row td { background: #fef2f2; }
a { color: #06c; text-decoration: none; }
a:hover { text-decoration: underline; }
footer { margin-top: 24px; color: var(--muted); font-size: 12px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>BID test visualizer</h1>
<div class="question">${esc(question)}</div>
<div class="meta">
<span class="${ok ? 'ok' : 'err'}">${status}</span>
<span>analysisId: <code>${esc(audit.analysisId ?? '')}</code></span>
<span>elapsed: ${elapsed}</span>
<span>escalations: ${escalations}</span>
</div>
</header>
<section class="card">
<h2>Answer (${records.length} record${records.length === 1 ? '' : 's'})</h2>
${records.length === 0
? '<p style="color:#999">No records produced.</p>'
: `<table>
<thead>
<tr>
<th>Entity</th><th>Metric</th><th>Period</th>
<th style="text-align:right">Value</th>
<th>XBRL tag</th><th>Conf</th><th>Source</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>`}
</section>
<section class="card">
<h2>Tool calls (${tools.length})</h2>
${tools.length === 0
? '<p style="color:#999">No tool calls recorded.</p>'
: `<table>
<thead><tr><th>Tool</th><th>Args</th><th>Result</th></tr></thead>
<tbody>${toolRows}</tbody>
</table>`}
</section>
<section class="card">
<h2>Pipeline</h2>
<code>${esc(pipeline)}</code>
</section>
<footer>
Throwaway viewer. Source: <code>${esc(audit._sourceFile ?? '')}</code>
</footer>
</div>
</body>
</html>`;
}
async function main(): Promise<void> {
const arg = process.argv[2];
const file = arg ? path.resolve(arg) : await pickLatest();
if (!file) {
console.error('No run-*.json found in output/. Run `npm run demo` first.');
process.exit(1);
}
const raw = await readFile(file, 'utf8');
const audit = JSON.parse(raw);
audit._sourceFile = path.relative(process.cwd(), file);
const html = render(audit);
const outFile = path.join(OUTPUT_DIR, 'test-visualizer.html');
await writeFile(outFile, html, 'utf8');
console.log(`Wrote ${path.relative(process.cwd(), outFile)} from ${audit._sourceFile}`);
}
main().catch(err => {
console.error(err);
process.exit(1);
});