Skip to content

Commit d191417

Browse files
author
Sean Roberts
committed
feat: monolithic scoring to multi-category scoring
1 parent 11abff5 commit d191417

12 files changed

Lines changed: 987 additions & 214 deletions

File tree

src/cli.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Command } from "commander";
77
import { run } from "./runner/runner.js";
88
import { loadConfig } from "./config/loader.js";
99
import { scoreRunResult, buildScoredOutput } from "./scoring/index.js";
10-
import { writeReportToStore } from "./reports/writer.js";
10+
import { initReport, finalizeReport } from "./reports/writer.js";
1111
import { listReports, readReport, readScenarioResults } from "./reports/reader.js";
1212
import { setBaseline, readBaseline, listBaselines, deleteBaseline, DEFAULT_BASELINE_NAME } from "./baselines/store.js";
1313
import { compareBaseline } from "./baselines/compare.js";
@@ -84,6 +84,9 @@ async function executeRunPipeline(
8484

8585
const concurrency = opts.concurrency ?? config.settings?.concurrency;
8686

87+
// Create report directory early so scoring can write raw data for judges to read
88+
const { reportId, reportDir } = initReport(new Date().toISOString(), configDir);
89+
8790
const runOutput = await run({
8891
configPath: opts.configPath,
8992
scenarioFilter: opts.scenario ? [opts.scenario] : undefined,
@@ -98,6 +101,7 @@ async function executeRunPipeline(
98101
const scoring = scoreRunResult(result, {
99102
weights: config.settings?.scoring_weights,
100103
logger,
104+
reportDir,
101105
onProgress: (scenarioKey, agentName, phase) => {
102106
if (phase === "start") onScoringStart?.(scenarioKey, agentName);
103107
},
@@ -120,7 +124,8 @@ async function executeRunPipeline(
120124
output = runOutput;
121125
}
122126

123-
const reportId = writeReportToStore(output, configDir, config.name);
127+
// Finalize: write scenario JSON, manifest, and HTML
128+
finalizeReport(reportDir, output, config.name);
124129

125130
if (opts.outputDir) {
126131
const reportPath = writeReportFile(output, opts.outputDir);

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export type {
2525
EntryAnalysis,
2626
TranscriptAnalysis,
2727
} from "./transcript/index.js";
28-
export { writeReportToStore } from "./reports/writer.js";
28+
export { writeReportToStore, initReport, writeScenarioRawData, finalizeReport } from "./reports/writer.js";
2929
export { listReports, readReport, readScenarioResult } from "./reports/reader.js";
3030
export { generateReportHtml } from "./reports/html.js";
3131
export {

src/reports/writer.ts

Lines changed: 88 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,81 @@ import * as fs from "node:fs";
22
import * as path from "node:path";
33
import type { RunOutput, RunResult } from "../types/output.js";
44
import { isScoredResult } from "../types/output.js";
5-
import type { ScoredOutput, ScoredRunResult } from "../types/scoring.js";
5+
import type { ScoredOutput, ScoredRunResult, SparseIndex } from "../types/scoring.js";
66
import type { ReportManifest, ReportResultEntry } from "../types/report.js";
77
import { generateReportHtml } from "./html.js";
88

99
const REPORTS_DIR = ".axis/reports";
1010

11+
// --- Phase 1: Create report directory ---
12+
1113
/**
12-
* Write a run's output to the persistent report store.
13-
* Returns the reportId (used to recall the report later).
14-
*
15-
* Structure:
16-
* .axis/reports/{reportId}/report.json
17-
* .axis/reports/{reportId}/scenarios/{scenarioKey}/{agentName}.json
14+
* Create the report directory for a run.
15+
* Call this early — before scoring — so the report dir is available
16+
* for writing raw data that judges can read.
1817
*/
19-
export function writeReportToStore(output: ScoredOutput | RunOutput, configDir: string, name?: string): string {
20-
const reportId = generateReportId(output.timestamp);
18+
export function initReport(
19+
timestamp: string,
20+
configDir: string,
21+
): { reportId: string; reportDir: string } {
22+
const reportId = generateReportId(timestamp);
2123
const reportDir = path.join(configDir, REPORTS_DIR, reportId);
22-
2324
fs.mkdirSync(reportDir, { recursive: true });
25+
return { reportId, reportDir };
26+
}
27+
28+
// --- Phase 2: Write raw data (before scoring judges run) ---
29+
30+
/**
31+
* Write raw run data for a single scenario×agent to the report directory.
32+
* Call this after building the sparse index but before running LLM judges,
33+
* so judges can read these files for context.
34+
*
35+
* Writes:
36+
* - `{agent}.raw.ndjson` — raw agent stdout lines (if available)
37+
* - `{agent}.sparse-index.txt` — human-readable sparse index (always)
38+
*/
39+
export function writeScenarioRawData(
40+
reportDir: string,
41+
result: RunResult | ScoredRunResult,
42+
sparseIndex?: SparseIndex,
43+
): void {
44+
const scenarioDir = path.join(reportDir, "scenarios", result.scenarioKey);
45+
fs.mkdirSync(scenarioDir, { recursive: true });
46+
47+
const baseName = result.agentName;
48+
49+
// Write raw NDJSON (if available)
50+
const rawOutput = result.output.rawOutput;
51+
if (rawOutput?.length) {
52+
const rawPath = path.join(scenarioDir, `${baseName}.raw.ndjson`);
53+
fs.writeFileSync(rawPath, rawOutput.join("\n") + "\n");
54+
}
55+
56+
// Write sparse index (always, when available)
57+
if (sparseIndex) {
58+
const indexPath = path.join(scenarioDir, `${baseName}.sparse-index.txt`);
59+
const header = [
60+
`# Sparse Index: ${result.scenarioKey} / ${result.agentName}`,
61+
`# ${sparseIndex.stats.totalInteractions} interactions | ` +
62+
`env: ${sparseIndex.stats.byCategory.environment} | ` +
63+
`svc: ${sparseIndex.stats.byCategory.service} | ` +
64+
`agent: ${sparseIndex.stats.byCategory.agent} | ` +
65+
`errors: ${sparseIndex.stats.totalErrors}`,
66+
"",
67+
];
68+
fs.writeFileSync(indexPath, header.join("\n") + sparseIndex.lines.join("\n") + "\n");
69+
}
70+
}
2471

72+
// --- Phase 3: Finalize report (after scoring completes) ---
73+
74+
/**
75+
* Finalize a report: write scored scenario JSON, manifest, and HTML.
76+
* Call this after all scoring is complete.
77+
*/
78+
export function finalizeReport(reportDir: string, output: ScoredOutput | RunOutput, name?: string): void {
79+
const reportId = path.basename(reportDir);
2580
const entries: ReportResultEntry[] = [];
2681

2782
for (const result of output.results) {
@@ -30,39 +85,17 @@ export function writeReportToStore(output: ScoredOutput | RunOutput, configDir:
3085

3186
fs.mkdirSync(path.dirname(absPath), { recursive: true });
3287

33-
// Strip rawOutput from scenario JSON — written as a separate file
34-
const { rawOutput, ...outputWithoutRaw } = result.output;
88+
// Strip rawOutput from scenario JSON — written separately in phase 2
89+
const { rawOutput: _rawOutput, ...outputWithoutRaw } = result.output;
3590

36-
// Strip sparseIndex from score — written as a separate file in debug mode
91+
// Strip sparseIndex from score — written separately in phase 2
3792
let resultToWrite: typeof result = { ...result, output: outputWithoutRaw };
3893
if (isScoredResult(result) && result.score.sparseIndex) {
3994
const { sparseIndex: _sparseIndex, ...scoreWithoutIndex } = result.score;
4095
resultToWrite = { ...resultToWrite, score: scoreWithoutIndex } as typeof result;
4196
}
4297

4398
fs.writeFileSync(absPath, JSON.stringify(resultToWrite, null, 2));
44-
45-
if (rawOutput?.length) {
46-
const rawPath = absPath.replace(/\.json$/, ".raw.ndjson");
47-
fs.writeFileSync(rawPath, rawOutput.join("\n") + "\n");
48-
}
49-
50-
// Write sparse index as a human-readable file in debug mode
51-
if (rawOutput && isScoredResult(result) && result.score.sparseIndex) {
52-
const indexPath = absPath.replace(/\.json$/, ".sparse-index.txt");
53-
const { sparseIndex } = result.score;
54-
const header = [
55-
`# Sparse Index: ${result.scenarioKey} / ${result.agentName}`,
56-
`# ${sparseIndex.stats.totalInteractions} interactions | ` +
57-
`env: ${sparseIndex.stats.byCategory.environment} | ` +
58-
`svc: ${sparseIndex.stats.byCategory.service} | ` +
59-
`agent: ${sparseIndex.stats.byCategory.agent} | ` +
60-
`errors: ${sparseIndex.stats.totalErrors}`,
61-
"",
62-
];
63-
fs.writeFileSync(indexPath, header.join("\n") + sparseIndex.lines.join("\n") + "\n");
64-
}
65-
6699
entries.push(buildResultEntry(result, relPath));
67100
}
68101

@@ -83,6 +116,26 @@ export function writeReportToStore(output: ScoredOutput | RunOutput, configDir:
83116
} catch {
84117
/* HTML generation is optional — template may not be built yet */
85118
}
119+
}
120+
121+
// --- Convenience wrapper (backward compat) ---
122+
123+
/**
124+
* Write a run's output to the persistent report store in a single call.
125+
* Combines initReport + writeScenarioRawData + finalizeReport.
126+
* Returns the reportId.
127+
*/
128+
export function writeReportToStore(output: ScoredOutput | RunOutput, configDir: string, name?: string): string {
129+
const { reportId, reportDir } = initReport(output.timestamp, configDir);
130+
131+
// Write raw data for each result
132+
for (const result of output.results) {
133+
const sparseIndex = isScoredResult(result) ? result.score.sparseIndex : undefined;
134+
writeScenarioRawData(reportDir, result, sparseIndex);
135+
}
136+
137+
// Finalize with scored results, manifest, and HTML
138+
finalizeReport(reportDir, output, name);
86139

87140
return reportId;
88141
}

0 commit comments

Comments
 (0)