BID · Console
Baseline · Intelligence · Decision
scripts/test-visualizer.ts 8,117 bytes · typescript
/**
 * 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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

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);
});