/**
* 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;
}
}