BID · Console
Baseline · Intelligence · Decision
web/runs.ts 3,838 bytes · typescript
/**
 * Past-run loader — reads bid-poc/output/run-*.json files.
 *
 * A "run id" is the timestamp portion of the filename
 * (e.g. "2026-05-25T04-07-40-541Z"). The UI uses that as the URL key.
 */

import { readdir, readFile, stat } 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');

export interface RunSummary {
  readonly id: string;
  readonly file: string;
  readonly question: string;
  readonly analysisId: string;
  readonly ok: boolean;
  readonly elapsedMs: number;
  readonly mtime: string;
  readonly totalCalls: number;
  readonly totalInputTokens: number;
  readonly totalOutputTokens: number;
  readonly estCostUsd: number;
  readonly stages: number;
  readonly insightCount: number;
  readonly failureCategory: string | null;
}

export interface Run extends RunSummary {
  readonly audit: Record<string, unknown>;
}

function idFromFile(file: string): string | null {
  const m = /^run-(.+)\.json$/.exec(file);
  return m && m[1] ? m[1] : null;
}

function summarise(id: string, file: string, mtimeIso: string, audit: Record<string, unknown>): RunSummary {
  const job = (audit.jobRequest ?? {}) as { question?: string; analysisId?: string };
  const usage = (audit.usage ?? {}) as {
    totals?: { calls?: number; inputTokens?: number; outputTokens?: number };
    byAgent?: { agent: string; model: string; inputTokens: number; outputTokens: number }[];
  };
  const finalHandoff = (audit.finalHandoff ?? null) as { payload?: { insights?: unknown[] } } | null;
  const failure = (audit.failure ?? null) as { agent?: string; category?: string } | null;
  const pipeline = Array.isArray(audit.pipeline) ? (audit.pipeline as unknown[]) : [];

  let estCostUsd = 0;
  for (const row of usage.byAgent ?? []) {
    if (row.model.startsWith('claude-haiku-4-5')) {
      estCostUsd += (row.inputTokens / 1_000_000) * 1.0 + (row.outputTokens / 1_000_000) * 5.0;
    }
  }

  return {
    id,
    file,
    question: job.question ?? '(no question)',
    analysisId: job.analysisId ?? id,
    ok: audit.ok === true,
    elapsedMs: typeof audit.elapsedMs === 'number' ? audit.elapsedMs : 0,
    mtime: mtimeIso,
    totalCalls: usage.totals?.calls ?? 0,
    totalInputTokens: usage.totals?.inputTokens ?? 0,
    totalOutputTokens: usage.totals?.outputTokens ?? 0,
    estCostUsd,
    stages: pipeline.length,
    insightCount: finalHandoff?.payload?.insights?.length ?? 0,
    failureCategory: failure?.category ?? null,
  };
}

export async function listRuns(): Promise<RunSummary[]> {
  let names: string[];
  try { names = await readdir(OUTPUT_DIR); }
  catch { return []; }
  const out: RunSummary[] = [];
  for (const name of names) {
    const id = idFromFile(name);
    if (!id) continue;
    const file = path.join(OUTPUT_DIR, name);
    try {
      const [statRes, body] = await Promise.all([stat(file), readFile(file, 'utf8')]);
      const audit = JSON.parse(body) as Record<string, unknown>;
      out.push(summarise(id, file, statRes.mtime.toISOString(), audit));
    } catch {
      /* skip unreadable / malformed audits — they show up in the file
       * list but a broken file shouldn't break the index page. */
    }
  }
  out.sort((a, b) => (a.mtime < b.mtime ? 1 : -1));
  return out;
}

export async function loadRunById(id: string): Promise<Run | null> {
  if (!/^[A-Za-z0-9\-:.]+$/.test(id)) return null;
  const file = path.join(OUTPUT_DIR, `run-${id}.json`);
  try {
    const [statRes, body] = await Promise.all([stat(file), readFile(file, 'utf8')]);
    const audit = JSON.parse(body) as Record<string, unknown>;
    return { ...summarise(id, file, statRes.mtime.toISOString(), audit), audit };
  } catch {
    return null;
  }
}