@@ -2,26 +2,81 @@ import * as fs from "node:fs";
22import * as path from "node:path" ;
33import type { RunOutput , RunResult } from "../types/output.js" ;
44import { isScoredResult } from "../types/output.js" ;
5- import type { ScoredOutput , ScoredRunResult } from "../types/scoring.js" ;
5+ import type { ScoredOutput , ScoredRunResult , SparseIndex } from "../types/scoring.js" ;
66import type { ReportManifest , ReportResultEntry } from "../types/report.js" ;
77import { generateReportHtml } from "./html.js" ;
88
99const 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 ( / \. j s o n $ / , ".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 ( / \. j s o n $ / , ".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