testronaut 1.10.2 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -0
- package/TODO.md +5 -0
- package/bin/cli.js +234 -5
- package/bin/terminalUi.js +51 -0
- package/core/agent.js +1 -0
- package/core/browserEvidence.js +184 -0
- package/core/jevRouting.js +77 -0
- package/core/jevShadow.js +325 -0
- package/core/jevSuiteSelection.js +39 -0
- package/core/jevUsage.js +62 -0
- package/core/optimization.js +98 -0
- package/core/turnLoop.js +174 -10
- package/experiments/CROSS_PROVIDER.md +19 -0
- package/experiments/README.md +62 -0
- package/experiments/buildExperimentPublicationPackage.js +219 -0
- package/experiments/buildOptimizationDataPackage.js +390 -0
- package/experiments/crossProviderBenchmark.js +227 -0
- package/experiments/optimizationBenchmark.js +339 -0
- package/package.json +5 -1
- package/tests/binTests/cli.helpers.test.js +16 -0
- package/tests/binTests/cli.integration.test.js +14 -0
- package/tests/binTests/terminalUi.test.js +34 -0
- package/tests/coreTests/browserEvidence.test.js +70 -0
- package/tests/coreTests/jevRouting.test.js +57 -0
- package/tests/coreTests/jevShadow.test.js +158 -0
- package/tests/coreTests/jevSuiteSelection.test.js +24 -0
- package/tests/coreTests/jevUsage.test.js +58 -0
- package/tests/coreTests/optimization.test.js +80 -0
- package/tests/coreTests/turnLoop.test.js +304 -0
- package/tests/experimentTests/buildOptimizationDataPackage.test.js +25 -0
- package/tests/experimentTests/crossProviderBenchmark.test.js +37 -0
- package/tests/experimentTests/optimizationBenchmark.test.js +49 -0
- package/tests/toolsTests/generateHtmlReport.test.js +83 -0
- package/tests/toolsTests/turnLoopUtils.test.js +17 -0
- package/tools/generateHtmlReport.js +96 -26
- package/tools/turnLoopUtils.js +7 -11
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { BENCHMARK_TARGETS } from './optimizationBenchmark.js';
|
|
9
|
+
import { CROSS_PROVIDER_CONDITIONS } from './crossProviderBenchmark.js';
|
|
10
|
+
|
|
11
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const PUBLICATION_ID = 'jev-optimization-experiments-2026-10';
|
|
13
|
+
const RESULTS = path.join(HERE, 'results');
|
|
14
|
+
const OUTPUT = path.join(RESULTS, PUBLICATION_ID);
|
|
15
|
+
const PACKAGE = path.join(OUTPUT, 'data-package');
|
|
16
|
+
|
|
17
|
+
function csvEscape(value) {
|
|
18
|
+
const text = typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value ?? '');
|
|
19
|
+
return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeCsv(file, columns, rows) {
|
|
23
|
+
fs.writeFileSync(file, `${columns.join(',')}\n${rows.map(row => columns.map(column => csvEscape(row[column])).join(',')).join('\n')}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mean(values) { return values.reduce((sum, value) => sum + value, 0) / values.length; }
|
|
27
|
+
function round(value, digits = 1) { return Number(value.toFixed(digits)); }
|
|
28
|
+
function sha256(file) { return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); }
|
|
29
|
+
|
|
30
|
+
function readCrossProvider() {
|
|
31
|
+
const stateFile = path.join(RESULTS, 'cross-provider-pilot-v1/state.json');
|
|
32
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
33
|
+
const runs = Object.values(state.runs).map(run => {
|
|
34
|
+
if (run.status !== 'completed' || !run.reportPath || !run.metrics) throw new Error(`Incomplete cross-provider run: ${run.key}`);
|
|
35
|
+
const report = JSON.parse(fs.readFileSync(run.reportPath, 'utf8'));
|
|
36
|
+
const selectorUsage = run.selection?.evaluation?.usage || {};
|
|
37
|
+
return {
|
|
38
|
+
experimentId: state.experimentId,
|
|
39
|
+
key: run.key,
|
|
40
|
+
block: run.block,
|
|
41
|
+
condition: run.id,
|
|
42
|
+
mission: run.unit,
|
|
43
|
+
candidateModels: run.candidates.map(item => `${item.provider}/${item.model}`).join('|'),
|
|
44
|
+
selectedProvider: run.selection.selected.provider,
|
|
45
|
+
selectedModel: run.selection.selected.model,
|
|
46
|
+
selectedBy: run.selection.selectedBy,
|
|
47
|
+
selectionScore: run.selection.score,
|
|
48
|
+
strictPass: run.metrics.failed === 0,
|
|
49
|
+
phasePassed: run.metrics.passed,
|
|
50
|
+
phaseFailed: run.metrics.failed,
|
|
51
|
+
turns: run.metrics.turns,
|
|
52
|
+
modelTokens: run.metrics.tokens,
|
|
53
|
+
jevInputTokens: run.metrics.jevInputTokens,
|
|
54
|
+
jevOutputTokens: run.metrics.jevOutputTokens,
|
|
55
|
+
selectorInputTokens: Number(selectorUsage.input_tokens || 0),
|
|
56
|
+
selectorOutputTokens: Number(selectorUsage.output_tokens || 0),
|
|
57
|
+
durationSeconds: (new Date(report.endTime) - new Date(report.startTime)) / 1000,
|
|
58
|
+
runId: report.runId,
|
|
59
|
+
cliVersion: report.cli?.version,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
const conditions = CROSS_PROVIDER_CONDITIONS.map(condition => {
|
|
63
|
+
const values = runs.filter(run => run.condition === condition.id);
|
|
64
|
+
return {
|
|
65
|
+
condition: condition.id,
|
|
66
|
+
type: condition.id.startsWith('c') ? 'fixed-control' : 'cross-provider-treatment',
|
|
67
|
+
candidates: condition.candidates.map(item => `${item.provider}/${item.model}`).join('|'),
|
|
68
|
+
n: values.length,
|
|
69
|
+
strictPassed: values.filter(run => run.strictPass).length,
|
|
70
|
+
strictPassRate: round(values.filter(run => run.strictPass).length / values.length, 4),
|
|
71
|
+
meanModelTokens: round(mean(values.map(run => run.modelTokens)), 0),
|
|
72
|
+
meanJevTokens: round(mean(values.map(run => run.jevInputTokens + run.jevOutputTokens)), 0),
|
|
73
|
+
meanSelectorTokens: round(mean(values.map(run => run.selectorInputTokens + run.selectorOutputTokens)), 0),
|
|
74
|
+
meanDurationSeconds: round(mean(values.map(run => run.durationSeconds)), 1),
|
|
75
|
+
selections: Object.fromEntries([...new Set(values.map(run => run.selectedModel))].map(model => [model, values.filter(run => run.selectedModel === model).length])),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
return { state, runs, conditions };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function crossTable(conditions) {
|
|
82
|
+
const rows = conditions.map(row => `| ${row.condition} | ${row.type} | ${row.strictPassed}/${row.n} (${Math.round(row.strictPassRate * 100)}%) | ${row.meanModelTokens.toLocaleString()} | ${row.meanJevTokens.toLocaleString()} | ${row.meanSelectorTokens.toLocaleString()} | ${row.meanDurationSeconds}s |`);
|
|
83
|
+
return ['| Condition | Design | Strict pass | Mean model tokens | Mean in-mission Jev tokens | Mean selector tokens | Mean duration |', '|---|---|---:|---:|---:|---:|---:|', ...rows].join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function optimizationMatrix() {
|
|
87
|
+
return Object.fromEntries(Object.entries(BENCHMARK_TARGETS).map(([target, value]) => [target, {
|
|
88
|
+
provider: value.provider,
|
|
89
|
+
missions: value.missions,
|
|
90
|
+
conditions: value.conditions,
|
|
91
|
+
}]));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function reportMarkdown(cross) {
|
|
95
|
+
return `# Jev optimization experiments: guardrails and model routing
|
|
96
|
+
|
|
97
|
+
Publication package: \`${PUBLICATION_ID}\`
|
|
98
|
+
Generated: ${new Date().toISOString()}
|
|
99
|
+
CLI baseline: Testronaut 1.11.0
|
|
100
|
+
|
|
101
|
+
## Executive summary
|
|
102
|
+
|
|
103
|
+
Two blocked experiments evaluated Jev-assisted browser-testing optimization. The first contained **70 aggregate runs and 210 mission-file attempts** across OpenAI and Gemini workloads. The second contained **120 isolated mission executions** testing fixed-model controls and mission-boundary cross-provider selection. All conditions used five randomized blocks.
|
|
104
|
+
|
|
105
|
+
The most defensible result is that **Jev guardrails are useful without model switching**. Guarded GPT-5.5 completed 15/15 OpenAI mission-file attempts and used 13.7% fewer model tokens than the unguarded Terra control, with a paired 95% interval of -19.8% to -7.5%. Guarded Terra also improved strict completion and nominally reduced tokens. On Gemini, guardrails preserved the already-perfect reliability of Gemini 3.8 Flash with a smaller nominal token reduction.
|
|
106
|
+
|
|
107
|
+
Same-provider routing is promising but not yet broadly proven. Gemini 3.8 → 3.7 retained 15/15 strict passes with a nominal 5.1% token reduction and similar duration. OpenAI routing coverage was below one routed turn per aggregate run, making attribution underpowered.
|
|
108
|
+
|
|
109
|
+
Mission-boundary cross-provider routing did **not** reduce token volume. The strongest fixed control, guarded GPT-5.5, achieved 15/15 strict passes at 87,422 mean model tokens per isolated mission. GPT-5.5 ↔ Gemini 3.5 Flash-Lite also achieved 15/15, but required 104,897 mean model tokens. Jev's extra selector call averaged only about 621–724 tokens, so selector overhead was not the cause; the model choices themselves produced longer executions.
|
|
110
|
+
|
|
111
|
+
## Experiment 1: optimization ladder
|
|
112
|
+
|
|
113
|
+
| Provider | Condition | Strict pass | Mean model tokens | Paired token delta vs control | Mean duration |
|
|
114
|
+
|---|---|---:|---:|---:|---:|
|
|
115
|
+
| OpenAI | Terra control | 5/15 (33%) | 300,073 | baseline | 293s |
|
|
116
|
+
| OpenAI | Guarded GPT-5.5 | **15/15 (100%)** | **258,635** | **-13.7% [-19.8, -7.5]** | 416s |
|
|
117
|
+
| OpenAI | Guarded Terra | 8/15 (53%) | 275,114 | -8.2% [-18.0, 1.7] | 328s |
|
|
118
|
+
| OpenAI | Terra → Luna | 11/15 (73%) | 296,299 | -1.1% [-12.1, 9.9] | 376s |
|
|
119
|
+
| Gemini | 3.8 control | **15/15 (100%)** | 491,428 | baseline | 529s |
|
|
120
|
+
| Gemini | Guarded 3.5 Flash-Lite | **15/15 (100%)** | **427,766** | -12.0% [-34.7, 10.8] | 582s |
|
|
121
|
+
| Gemini | 3.8 → 3.7 | **15/15 (100%)** | 461,256 | -5.1% [-19.7, 9.5] | 538s |
|
|
122
|
+
|
|
123
|
+
The table highlights decision-relevant conditions; complete condition, run, and mission tables are included in \`data/optimization/\`.
|
|
124
|
+
|
|
125
|
+
## Experiment 2: cross-provider mission-boundary routing
|
|
126
|
+
|
|
127
|
+
${crossTable(cross.conditions)}
|
|
128
|
+
|
|
129
|
+
Jev chose a Gemini candidate for 50 of 60 treatment missions. That bias was not aligned with the observed workload: GPT-5.5 was the only fixed model with perfect strict completion, and it used the fewest mean model tokens. The file-transfer mission was particularly discriminating: GPT-5.5 passed 5/5, Terra and Gemini 3.8 passed 0/5, and Gemini 3.5 Flash-Lite passed 3/5 in their fixed controls.
|
|
130
|
+
|
|
131
|
+
## Product implications
|
|
132
|
+
|
|
133
|
+
1. Stabilize guardrail-only operation: completion/evidence checks, fail-open behavior, primary-model continuity, and explicit telemetry.
|
|
134
|
+
2. Keep execution-model routing beta and opt-in. Limit a run to one primary and one compatible same-provider secondary.
|
|
135
|
+
3. Treat Gemini 3.8 → 3.7 as a conservative candidate for a larger confirmation study, not a universal default.
|
|
136
|
+
4. Do not ship zero-shot cross-provider selection as an optimization default.
|
|
137
|
+
5. Next test history-informed routing using versioned per-model reliability, tokens, latency, and price, with a reliability floor and cold-start fallback.
|
|
138
|
+
|
|
139
|
+
## Methodology and interpretation
|
|
140
|
+
|
|
141
|
+
- Both experiments used five randomized blocks (\`n=5\` per condition design).
|
|
142
|
+
- Strict success required every expected phase in a mission file to exist and pass.
|
|
143
|
+
- Experiment 1 aggregate runs each invoked three mission files. Experiment 2 intentionally isolated each mission to make mission-boundary selection attributable.
|
|
144
|
+
- Model tokens and Jev tokens are reported separately. Token counts are not price-weighted spend.
|
|
145
|
+
- Experiment 1 paired intervals use a two-sided 95% Student-t interval with four degrees of freedom.
|
|
146
|
+
- Six initial Gemini 3.8 requests in experiment 2 returned provider HTTP 500 errors. The resumable benchmark reran those incomplete units using their persisted selections; the final dataset contains 120 completed reports.
|
|
147
|
+
- Results describe these repositories, missions, model versions, dates, and provider behavior. They are not universal model rankings.
|
|
148
|
+
|
|
149
|
+
## Data and privacy
|
|
150
|
+
|
|
151
|
+
This public package contains normalized measurements, matrices, methodology, and checksums. It intentionally excludes raw browser events, DOM content, screenshots, HTML reports, environment files, credentials, absolute report paths, and verbose logs. The private source reports remain the audit source for the normalized rows.
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function walk(root) {
|
|
156
|
+
return fs.readdirSync(root, { withFileTypes: true }).flatMap(entry => {
|
|
157
|
+
const full = path.join(root, entry.name);
|
|
158
|
+
return entry.isDirectory() ? walk(full) : [full];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function buildPublicationPackage() {
|
|
163
|
+
const optimizationPackage = path.join(RESULTS, 'optimization-pilot-v1/data-package/data');
|
|
164
|
+
if (!fs.existsSync(optimizationPackage)) throw new Error('Build optimization-pilot-v1 package first with npm run benchmark:package');
|
|
165
|
+
const cross = readCrossProvider();
|
|
166
|
+
fs.mkdirSync(path.join(PACKAGE, 'data/optimization'), { recursive: true });
|
|
167
|
+
fs.mkdirSync(path.join(PACKAGE, 'data/cross-provider'), { recursive: true });
|
|
168
|
+
|
|
169
|
+
for (const name of ['conditions.csv', 'missions.csv', 'runs.csv', 'matrix.json']) {
|
|
170
|
+
fs.copyFileSync(path.join(optimizationPackage, name), path.join(PACKAGE, `data/optimization/${name}`));
|
|
171
|
+
}
|
|
172
|
+
const crossColumns = Object.keys(cross.runs[0]);
|
|
173
|
+
writeCsv(path.join(PACKAGE, 'data/cross-provider/runs.csv'), crossColumns, cross.runs);
|
|
174
|
+
writeCsv(path.join(PACKAGE, 'data/cross-provider/conditions.csv'), Object.keys(cross.conditions[0]), cross.conditions);
|
|
175
|
+
fs.writeFileSync(path.join(PACKAGE, 'data/cross-provider/matrix.json'), `${JSON.stringify({
|
|
176
|
+
experimentId: cross.state.experimentId,
|
|
177
|
+
repetitions: cross.state.repetitions,
|
|
178
|
+
conditions: CROSS_PROVIDER_CONDITIONS,
|
|
179
|
+
missions: [...new Set(cross.runs.map(run => run.mission))],
|
|
180
|
+
}, null, 2)}\n`);
|
|
181
|
+
fs.writeFileSync(path.join(PACKAGE, 'data/combined-matrix.json'), `${JSON.stringify({ optimization: optimizationMatrix(), crossProvider: CROSS_PROVIDER_CONDITIONS }, null, 2)}\n`);
|
|
182
|
+
|
|
183
|
+
const report = reportMarkdown(cross);
|
|
184
|
+
fs.writeFileSync(path.join(PACKAGE, 'REPORT.md'), report);
|
|
185
|
+
fs.writeFileSync(path.join(PACKAGE, 'README.md'), `# Jev optimization experiment publication package\n\nStart with [REPORT.md](REPORT.md). The normalized, publication-safe datasets are in \`data/optimization/\` and \`data/cross-provider/\`. See [DATA_DICTIONARY.md](DATA_DICTIONARY.md) for field definitions and \`SHA256SUMS\` for integrity.\n`);
|
|
186
|
+
fs.writeFileSync(path.join(PACKAGE, 'DATA_DICTIONARY.md'), `# Data dictionary\n\n## Optimization ladder\n\n- \`runs.csv\`: one aggregate CLI run (three mission files) per row.\n- \`missions.csv\`: one expected mission-file outcome per aggregate run.\n- \`conditions.csv\`: condition-level means, paired deltas, intervals, routing, and completion-gate counts.\n- \`matrix.json\`: frozen provider, mission, and condition definitions.\n\n## Cross-provider benchmark\n\n- \`runs.csv\`: one isolated mission execution per row. \`strictPass\` means all reported phases passed. Selector tokens are separated from in-mission Jev tokens.\n- \`conditions.csv\`: condition-level strict completion, mean token, selection, and duration summaries.\n- \`matrix.json\`: frozen candidates, missions, and repetition count.\n\nEmpty secondary models indicate fixed-model conditions. Durations are wall-clock seconds. Token fields are raw token volume, not price-weighted cost.\n`);
|
|
187
|
+
|
|
188
|
+
const filesBeforeManifest = walk(PACKAGE)
|
|
189
|
+
.filter(file => !['manifest.json', 'SHA256SUMS'].includes(path.basename(file)))
|
|
190
|
+
.map(file => ({ path: path.relative(PACKAGE, file), bytes: fs.statSync(file).size, sha256: sha256(file) }))
|
|
191
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
192
|
+
const manifest = {
|
|
193
|
+
schemaVersion: 1,
|
|
194
|
+
publicationId: PUBLICATION_ID,
|
|
195
|
+
generatedAt: new Date().toISOString(),
|
|
196
|
+
cliBaseline: '1.11.0',
|
|
197
|
+
experiments: [
|
|
198
|
+
{ id: 'optimization-pilot-v1', aggregateRuns: 70, missionFileAttempts: 210 },
|
|
199
|
+
{ id: 'cross-provider-pilot-v1', isolatedMissionRuns: 120 },
|
|
200
|
+
],
|
|
201
|
+
privacyExclusions: ['raw browser events and DOM', 'screenshots', 'HTML reports', 'environment files', 'credentials', 'absolute report paths', 'verbose logs'],
|
|
202
|
+
files: filesBeforeManifest,
|
|
203
|
+
};
|
|
204
|
+
fs.writeFileSync(path.join(PACKAGE, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
205
|
+
const checksumFiles = walk(PACKAGE).filter(file => path.basename(file) !== 'SHA256SUMS').sort();
|
|
206
|
+
fs.writeFileSync(path.join(PACKAGE, 'SHA256SUMS'), `${checksumFiles.map(file => `${sha256(file)} ${path.relative(PACKAGE, file)}`).join('\n')}\n`);
|
|
207
|
+
|
|
208
|
+
fs.mkdirSync(OUTPUT, { recursive: true });
|
|
209
|
+
fs.copyFileSync(path.join(PACKAGE, 'REPORT.md'), path.join(OUTPUT, `${PUBLICATION_ID}-report.md`));
|
|
210
|
+
const archive = path.join(OUTPUT, `${PUBLICATION_ID}-data-package.tar.gz`);
|
|
211
|
+
const tar = spawnSync('tar', ['-czf', archive, '-C', OUTPUT, 'data-package'], { encoding: 'utf8' });
|
|
212
|
+
if (tar.status !== 0) throw new Error(tar.stderr || 'Could not create archive');
|
|
213
|
+
fs.writeFileSync(`${archive}.sha256`, `${sha256(archive)} ${path.basename(archive)}\n`);
|
|
214
|
+
return { output: OUTPUT, package: PACKAGE, archive, report: path.join(OUTPUT, `${PUBLICATION_ID}-report.md`) };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
218
|
+
console.log(buildPublicationPackage());
|
|
219
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { BENCHMARK_TARGETS } from './optimizationBenchmark.js';
|
|
9
|
+
|
|
10
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const DEFAULT_EXPERIMENT_ID = 'optimization-pilot-v1';
|
|
12
|
+
const T95_DF4 = 2.776;
|
|
13
|
+
const EXPECTED_RECORDS = {
|
|
14
|
+
openai: {
|
|
15
|
+
'missions/login.mission.js': 1,
|
|
16
|
+
'missions/removeTask.mission.js': 2,
|
|
17
|
+
'missions/fileTransfer.mission.js': 3,
|
|
18
|
+
},
|
|
19
|
+
gemini: {
|
|
20
|
+
'missions/profileNavigation.mission.js': 2,
|
|
21
|
+
'missions/projectsDashboard.mission.js': 2,
|
|
22
|
+
'missions/projectWorkspaceNavigation.mission.js': 2,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const CONTROL_CONDITION = {
|
|
26
|
+
openai: 'l1_terra_control',
|
|
27
|
+
gemini: 'l1_gemini38_control',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function mean(values) {
|
|
31
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sampleSd(values) {
|
|
35
|
+
if (values.length < 2) return 0;
|
|
36
|
+
const average = mean(values);
|
|
37
|
+
return Math.sqrt(values.reduce((sum, value) => sum + (value - average) ** 2, 0) / (values.length - 1));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function round(value, digits = 2) {
|
|
41
|
+
return Number.isFinite(value) ? Number(value.toFixed(digits)) : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function csvEscape(value) {
|
|
45
|
+
const text = typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value ?? '');
|
|
46
|
+
return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writeCsv(file, columns, rows) {
|
|
50
|
+
fs.writeFileSync(file, `${columns.join(',')}\n${rows.map(row => columns.map(column => csvEscape(row[column])).join(',')).join('\n')}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readReports(targetName, experimentId) {
|
|
54
|
+
const target = BENCHMARK_TARGETS[targetName];
|
|
55
|
+
const reportDir = path.join(target.repo, 'missions/mission_reports');
|
|
56
|
+
return fs.readdirSync(reportDir)
|
|
57
|
+
.filter(file => /^run_\d+\.json$/.test(file))
|
|
58
|
+
.map(file => ({ file, path: path.join(reportDir, file) }))
|
|
59
|
+
.map(entry => ({ ...entry, report: JSON.parse(fs.readFileSync(entry.path, 'utf8')) }))
|
|
60
|
+
.filter(entry => entry.report.experiment?.id === experimentId)
|
|
61
|
+
.map(entry => ({ ...entry, target: targetName }));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function stepMetrics(steps, report) {
|
|
65
|
+
const models = {};
|
|
66
|
+
for (const step of steps) {
|
|
67
|
+
const model = step.model || report.llm?.model || 'unknown';
|
|
68
|
+
models[model] ||= { turns: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
|
|
69
|
+
models[model].turns += 1;
|
|
70
|
+
models[model].tokens += Number(step.tokensUsed || 0);
|
|
71
|
+
models[model].inputTokens += Number(step.inputTokens || 0);
|
|
72
|
+
models[model].outputTokens += Number(step.outputTokens || 0);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
turns: steps.length,
|
|
76
|
+
tokens: steps.reduce((sum, step) => sum + Number(step.tokensUsed || 0), 0),
|
|
77
|
+
inputTokens: steps.reduce((sum, step) => sum + Number(step.inputTokens || 0), 0),
|
|
78
|
+
outputTokens: steps.reduce((sum, step) => sum + Number(step.outputTokens || 0), 0),
|
|
79
|
+
routedTurns: steps.filter(step => step.modelRouting?.applied).length,
|
|
80
|
+
forcedPrimaryRetries: steps.filter(step => step.modelRouting?.forcedPrimaryRetry).length,
|
|
81
|
+
requestFallbacks: steps.filter(step => step.modelRouting?.fallback).length,
|
|
82
|
+
models,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeRun(entry) {
|
|
87
|
+
const { report, target } = entry;
|
|
88
|
+
const expected = EXPECTED_RECORDS[target];
|
|
89
|
+
const steps = report.missions.flatMap(mission => mission.steps || []);
|
|
90
|
+
const step = stepMetrics(steps, report);
|
|
91
|
+
const missionOutcomes = Object.entries(expected).map(([file, expectedRecords]) => {
|
|
92
|
+
const records = report.missions.filter(mission => mission.file === file);
|
|
93
|
+
const recordSteps = records.flatMap(record => record.steps || []);
|
|
94
|
+
return {
|
|
95
|
+
experimentId: report.experiment.id,
|
|
96
|
+
target,
|
|
97
|
+
condition: report.experiment.condition,
|
|
98
|
+
block: report.experiment.block,
|
|
99
|
+
runId: report.runId,
|
|
100
|
+
file,
|
|
101
|
+
expectedRecords,
|
|
102
|
+
actualRecords: records.length,
|
|
103
|
+
passed: records.length === expectedRecords && records.every(record => record.status === 'passed'),
|
|
104
|
+
phasePassed: records.filter(record => record.status === 'passed').length,
|
|
105
|
+
phaseFailed: records.filter(record => record.status === 'failed').length,
|
|
106
|
+
turns: recordSteps.length,
|
|
107
|
+
tokens: recordSteps.reduce((sum, item) => sum + Number(item.tokensUsed || 0), 0),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
const condition = BENCHMARK_TARGETS[target].conditions.find(item => item.id === report.experiment.condition);
|
|
111
|
+
return {
|
|
112
|
+
experimentId: report.experiment.id,
|
|
113
|
+
target,
|
|
114
|
+
condition: report.experiment.condition,
|
|
115
|
+
level: condition?.level,
|
|
116
|
+
block: report.experiment.block,
|
|
117
|
+
runId: report.runId,
|
|
118
|
+
provider: report.llm?.provider,
|
|
119
|
+
primaryModel: condition?.primary || report.llm?.model,
|
|
120
|
+
secondaryModel: condition?.secondary || '',
|
|
121
|
+
objective: report.optimization?.objective || condition?.objective,
|
|
122
|
+
mode: report.optimization?.mode || condition?.mode,
|
|
123
|
+
phasePassed: Number(report.summary?.passed || 0),
|
|
124
|
+
phaseFailed: Number(report.summary?.failed || 0),
|
|
125
|
+
phaseTotal: Number(report.summary?.totalMissions || 0),
|
|
126
|
+
missionFilesPassed: missionOutcomes.filter(outcome => outcome.passed).length,
|
|
127
|
+
missionFilesTotal: missionOutcomes.length,
|
|
128
|
+
durationSeconds: (new Date(report.endTime) - new Date(report.startTime)) / 1000,
|
|
129
|
+
...step,
|
|
130
|
+
jevInputTokens: Number(report.jevShadow?.inputTokens || 0),
|
|
131
|
+
jevOutputTokens: Number(report.jevShadow?.outputTokens || 0),
|
|
132
|
+
jevEvaluations: Number(report.jevShadow?.evaluations || 0),
|
|
133
|
+
completionGates: Number(report.jevShadow?.triggeredCompletions || 0),
|
|
134
|
+
reportPath: entry.path,
|
|
135
|
+
reportFile: entry.file,
|
|
136
|
+
missionOutcomes,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function pairedDelta(conditionRuns, controlRuns, metric) {
|
|
141
|
+
const controlByBlock = Object.fromEntries(controlRuns.map(run => [run.block, run]));
|
|
142
|
+
const values = conditionRuns.map(run => {
|
|
143
|
+
const control = controlByBlock[run.block];
|
|
144
|
+
return control?.[metric] ? (run[metric] - control[metric]) / control[metric] * 100 : 0;
|
|
145
|
+
});
|
|
146
|
+
const average = mean(values);
|
|
147
|
+
const halfWidth = T95_DF4 * sampleSd(values) / Math.sqrt(values.length);
|
|
148
|
+
return { average, low: average - halfWidth, high: average + halfWidth };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function summarizeConditions(runs) {
|
|
152
|
+
const grouped = runs.reduce((groups, run) => {
|
|
153
|
+
(groups[`${run.target}:${run.condition}`] ||= []).push(run);
|
|
154
|
+
return groups;
|
|
155
|
+
}, {});
|
|
156
|
+
const controls = Object.fromEntries(Object.entries(CONTROL_CONDITION).map(([target, condition]) => [
|
|
157
|
+
target,
|
|
158
|
+
runs.filter(run => run.target === target && run.condition === condition),
|
|
159
|
+
]));
|
|
160
|
+
return Object.values(grouped).map(conditionRuns => {
|
|
161
|
+
conditionRuns.sort((a, b) => a.block - b.block);
|
|
162
|
+
const first = conditionRuns[0];
|
|
163
|
+
const tokenDelta = pairedDelta(conditionRuns, controls[first.target], 'tokens');
|
|
164
|
+
const turnDelta = pairedDelta(conditionRuns, controls[first.target], 'turns');
|
|
165
|
+
const durationDelta = pairedDelta(conditionRuns, controls[first.target], 'durationSeconds');
|
|
166
|
+
return {
|
|
167
|
+
target: first.target,
|
|
168
|
+
condition: first.condition,
|
|
169
|
+
level: first.level,
|
|
170
|
+
primaryModel: first.primaryModel,
|
|
171
|
+
secondaryModel: first.secondaryModel,
|
|
172
|
+
objective: first.objective,
|
|
173
|
+
mode: first.mode,
|
|
174
|
+
n: conditionRuns.length,
|
|
175
|
+
missionFileAttempts: conditionRuns.reduce((sum, run) => sum + run.missionFilesTotal, 0),
|
|
176
|
+
missionFilePassed: conditionRuns.reduce((sum, run) => sum + run.missionFilesPassed, 0),
|
|
177
|
+
missionFilePassRate: conditionRuns.reduce((sum, run) => sum + run.missionFilesPassed, 0)
|
|
178
|
+
/ conditionRuns.reduce((sum, run) => sum + run.missionFilesTotal, 0),
|
|
179
|
+
phasePassed: conditionRuns.reduce((sum, run) => sum + run.phasePassed, 0),
|
|
180
|
+
phaseTotal: conditionRuns.reduce((sum, run) => sum + run.phaseTotal, 0),
|
|
181
|
+
phasePassRate: conditionRuns.reduce((sum, run) => sum + run.phasePassed, 0)
|
|
182
|
+
/ conditionRuns.reduce((sum, run) => sum + run.phaseTotal, 0),
|
|
183
|
+
meanTurns: mean(conditionRuns.map(run => run.turns)),
|
|
184
|
+
sdTurns: sampleSd(conditionRuns.map(run => run.turns)),
|
|
185
|
+
turnDeltaPct: turnDelta.average,
|
|
186
|
+
turnDeltaLow95: turnDelta.low,
|
|
187
|
+
turnDeltaHigh95: turnDelta.high,
|
|
188
|
+
meanTokens: mean(conditionRuns.map(run => run.tokens)),
|
|
189
|
+
sdTokens: sampleSd(conditionRuns.map(run => run.tokens)),
|
|
190
|
+
tokenDeltaPct: tokenDelta.average,
|
|
191
|
+
tokenDeltaLow95: tokenDelta.low,
|
|
192
|
+
tokenDeltaHigh95: tokenDelta.high,
|
|
193
|
+
meanDurationSeconds: mean(conditionRuns.map(run => run.durationSeconds)),
|
|
194
|
+
sdDurationSeconds: sampleSd(conditionRuns.map(run => run.durationSeconds)),
|
|
195
|
+
durationDeltaPct: durationDelta.average,
|
|
196
|
+
durationDeltaLow95: durationDelta.low,
|
|
197
|
+
durationDeltaHigh95: durationDelta.high,
|
|
198
|
+
meanRoutedTurns: mean(conditionRuns.map(run => run.routedTurns)),
|
|
199
|
+
meanForcedPrimaryRetries: mean(conditionRuns.map(run => run.forcedPrimaryRetries)),
|
|
200
|
+
meanCompletionGates: mean(conditionRuns.map(run => run.completionGates)),
|
|
201
|
+
meanJevTokens: mean(conditionRuns.map(run => run.jevInputTokens + run.jevOutputTokens)),
|
|
202
|
+
};
|
|
203
|
+
}).sort((a, b) => a.target.localeCompare(b.target) || a.level - b.level || a.condition.localeCompare(b.condition));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function markdownTable(rows) {
|
|
207
|
+
const header = '| Condition | Strict pass | Mean tokens | Δ tokens vs control (paired 95% CI) | Mean turns | Mean duration | Routes/run | Gates/run |';
|
|
208
|
+
const divider = '|---|---:|---:|---:|---:|---:|---:|---:|';
|
|
209
|
+
const body = rows.map(row => [
|
|
210
|
+
`| ${row.condition}`,
|
|
211
|
+
`${row.missionFilePassed}/${row.missionFileAttempts} (${(row.missionFilePassRate * 100).toFixed(0)}%)`,
|
|
212
|
+
Math.round(row.meanTokens).toLocaleString('en-US'),
|
|
213
|
+
`${row.tokenDeltaPct >= 0 ? '+' : ''}${row.tokenDeltaPct.toFixed(1)}% [${row.tokenDeltaLow95.toFixed(1)}, ${row.tokenDeltaHigh95.toFixed(1)}]`,
|
|
214
|
+
row.meanTurns.toFixed(1),
|
|
215
|
+
`${row.meanDurationSeconds.toFixed(0)}s`,
|
|
216
|
+
row.meanRoutedTurns.toFixed(1),
|
|
217
|
+
row.meanCompletionGates.toFixed(1),
|
|
218
|
+
].join(' | ') + ' |').join('\n');
|
|
219
|
+
return `${header}\n${divider}\n${body}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function buildReport(experimentId, runs, summaries) {
|
|
223
|
+
const openai = summaries.filter(row => row.target === 'openai');
|
|
224
|
+
const gemini = summaries.filter(row => row.target === 'gemini');
|
|
225
|
+
const periods = runs.map(run => JSON.parse(fs.readFileSync(run.reportPath, 'utf8')));
|
|
226
|
+
const starts = periods.map(report => new Date(report.startTime)).filter(date => !Number.isNaN(date.valueOf()));
|
|
227
|
+
const ends = periods.map(report => new Date(report.endTime)).filter(date => !Number.isNaN(date.valueOf()));
|
|
228
|
+
return `# Testronaut Jev optimization pilot report
|
|
229
|
+
|
|
230
|
+
Experiment: \`${experimentId}\`
|
|
231
|
+
Generated: ${new Date().toISOString()}
|
|
232
|
+
Benchmark period: ${new Date(Math.min(...starts)).toISOString()} to ${new Date(Math.max(...ends)).toISOString()}
|
|
233
|
+
|
|
234
|
+
## Executive summary
|
|
235
|
+
|
|
236
|
+
This blocked pilot completed **${runs.length} runs** across two repositories, seven conditions per provider, five repetitions per condition, and **${runs.length * 3} mission-file attempts**. Each routed condition used at most two execution models. Jev remained the guardrail/router and was not counted as an execution model.
|
|
237
|
+
|
|
238
|
+
The clearest OpenAI result was guarded GPT-5.5: **15/15 strict mission-file passes** and **${Math.abs(openai.find(row => row.condition === 'l2_gpt55_guarded').tokenDeltaPct).toFixed(1)}% fewer model tokens** than the Terra control. Its paired 95% interval excluded zero, but it was materially slower. OpenAI routing coverage was too low—less than one routed turn per run—to distinguish secondary-model quality confidently.
|
|
239
|
+
|
|
240
|
+
Gemini 3.8 Flash was already perfectly reliable on this workload. Guarded Gemini 3.5 Flash-Lite also achieved **15/15 strict passes** with a nominal **${Math.abs(gemini.find(row => row.condition === 'l2_gemini35lite_guarded').tokenDeltaPct).toFixed(1)}% token reduction**, but the paired interval included zero and duration increased. Gemini 3.8 → 3.7 was the strongest conservative routing policy: 15/15 strict passes, modest nominal token reduction, and nearly unchanged duration. Routing 3.8 → 3.5 Flash-Lite was less reliable and caused primary-model retries.
|
|
241
|
+
|
|
242
|
+
## Methodology
|
|
243
|
+
|
|
244
|
+
- Five randomized blocks per condition (\`n=5\`).
|
|
245
|
+
- OpenAI repository: login, self-cleaning task creation/removal, and file transfer.
|
|
246
|
+
- Gemini repository: profile navigation, projects dashboard, and project workspace navigation.
|
|
247
|
+
- Level 1: single-model control without Jev.
|
|
248
|
+
- Level 2: single model with live Jev guardrails and completion gating.
|
|
249
|
+
- Level 3: current-generation/recent two-model routing with guardrails.
|
|
250
|
+
- Level 4: current primary routed to older same-provider generations with guardrails.
|
|
251
|
+
- Routing probability threshold: 0.40.
|
|
252
|
+
- Strict mission-file success requires every expected phase for that file to exist and pass. This avoids treating a failed prerequisite that suppresses later phases as a smaller denominator.
|
|
253
|
+
- Token, turn, and duration deltas are paired by randomized block against the provider's level-1 control. Intervals use a two-sided 95% Student-t interval with four degrees of freedom.
|
|
254
|
+
|
|
255
|
+
## OpenAI results
|
|
256
|
+
|
|
257
|
+
${markdownTable(openai)}
|
|
258
|
+
|
|
259
|
+
### OpenAI interpretation
|
|
260
|
+
|
|
261
|
+
- **Guarded GPT-5.5 was the pilot winner** for reliability and model-token volume: all 15 mission-file attempts passed.
|
|
262
|
+
- GPT-5.5 used fewer model tokens and turns than Terra control, but mean duration increased substantially; it is a quality/cost candidate rather than a speed candidate.
|
|
263
|
+
- Guarded Terra improved nominal tokens and strict completion, showing that completion gating contributed independently of model switching.
|
|
264
|
+
- Terra → Luna was the best OpenAI routing condition by strict pass rate, but it routed only 0.4 turns per run and produced almost no model-token reduction.
|
|
265
|
+
- Terra → GPT-5.4 and Terra → GPT-5.5 also routed fewer than one turn per run. Their secondary-model effects remain underpowered.
|
|
266
|
+
- Guarded Luna showed high variance and incomplete mission-file execution in two blocks.
|
|
267
|
+
|
|
268
|
+
## Gemini results
|
|
269
|
+
|
|
270
|
+
${markdownTable(gemini)}
|
|
271
|
+
|
|
272
|
+
### Gemini interpretation
|
|
273
|
+
|
|
274
|
+
- The Gemini 3.8 control achieved 15/15 strict passes, leaving no reliability headroom.
|
|
275
|
+
- Guarded Gemini 3.5 Flash-Lite retained 15/15 passes and had the largest nominal reduction among reliable Gemini conditions, at the cost of higher duration.
|
|
276
|
+
- Gemini 3.8 → 3.7 retained perfect reliability and near-control duration; it is the strongest conservative Gemini routing candidate.
|
|
277
|
+
- Gemini 3.8 → 3.6 retained reliability but offered little token or latency benefit.
|
|
278
|
+
- Direct Gemini 3.6 and Gemini 3.8 → 3.5 Flash-Lite each fell to 11/15 strict passes. The latter also generated frequent primary-model retries.
|
|
279
|
+
- No Gemini token delta had a paired 95% interval excluding zero at \`n=5\`; larger samples are required.
|
|
280
|
+
|
|
281
|
+
## Cross-provider conclusions
|
|
282
|
+
|
|
283
|
+
1. Keep the execution-model limit at two per run: one primary and one secondary. This preserved attribution and made fallback behavior auditable.
|
|
284
|
+
2. Guardrails and completion gating produced more consistent savings than routing because routing coverage remained low.
|
|
285
|
+
3. Model choice mattered more than routing sophistication on the OpenAI workload: guarded GPT-5.5 outperformed Terra-based policies.
|
|
286
|
+
4. For Gemini, use guarded 3.5 Flash-Lite when cost is dominant and its slower runtime is acceptable; use 3.8 → 3.7 when preserving current-model reliability and latency is more important.
|
|
287
|
+
5. Treat this as a pilot, not a universal model ranking. The mission subsets are intentionally small, provider pricing was not embedded, and application/model behavior may drift.
|
|
288
|
+
|
|
289
|
+
## Limitations and next steps
|
|
290
|
+
|
|
291
|
+
- Five repetitions provide useful variance estimates but limited statistical power.
|
|
292
|
+
- Results apply to these six mission files and their test environments.
|
|
293
|
+
- The package reports raw model-token volume, not price-weighted spend; pricing metadata should be versioned before asserting monetary savings.
|
|
294
|
+
- Routed OpenAI conditions need either a larger sample or a routing policy that yields more eligible turns.
|
|
295
|
+
- Repeat the winning and control conditions at a powered sample size derived from these observed variances.
|
|
296
|
+
- Evaluate cross-provider routing first at mission boundaries to avoid transcript/tool-history incompatibilities.
|
|
297
|
+
|
|
298
|
+
## Data package
|
|
299
|
+
|
|
300
|
+
The accompanying package includes normalized run-, mission-, and condition-level CSV files; all 70 raw JSON reports; the frozen benchmark matrix; source state/CSV files where available; and SHA-256 checksums. Screenshots, HTML reports, environment files, credentials, and verbose logs are intentionally excluded.
|
|
301
|
+
`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function sha256(file) {
|
|
305
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function walkFiles(root) {
|
|
309
|
+
return fs.readdirSync(root, { withFileTypes: true }).flatMap(entry => {
|
|
310
|
+
const full = path.join(root, entry.name);
|
|
311
|
+
return entry.isDirectory() ? walkFiles(full) : [full];
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function buildOptimizationDataPackage({ experimentId = DEFAULT_EXPERIMENT_ID } = {}) {
|
|
316
|
+
const root = path.resolve(HERE, 'results', experimentId);
|
|
317
|
+
const packageDir = path.join(root, 'data-package');
|
|
318
|
+
fs.rmSync(packageDir, { recursive: true, force: true });
|
|
319
|
+
fs.mkdirSync(path.join(packageDir, 'data/raw-reports/openai'), { recursive: true });
|
|
320
|
+
fs.mkdirSync(path.join(packageDir, 'data/raw-reports/gemini'), { recursive: true });
|
|
321
|
+
fs.mkdirSync(path.join(packageDir, 'data/source-state'), { recursive: true });
|
|
322
|
+
|
|
323
|
+
const entries = ['openai', 'gemini'].flatMap(target => readReports(target, experimentId));
|
|
324
|
+
if (entries.length !== 70) throw new Error(`Expected 70 reports for ${experimentId}; found ${entries.length}.`);
|
|
325
|
+
const runs = entries.map(normalizeRun).sort((a, b) => a.target.localeCompare(b.target) || a.block - b.block || a.condition.localeCompare(b.condition));
|
|
326
|
+
const summaries = summarizeConditions(runs);
|
|
327
|
+
const missionRows = runs.flatMap(run => run.missionOutcomes);
|
|
328
|
+
|
|
329
|
+
const runColumns = [
|
|
330
|
+
'experimentId', 'target', 'condition', 'level', 'block', 'runId', 'provider', 'primaryModel',
|
|
331
|
+
'secondaryModel', 'objective', 'mode', 'missionFilesPassed', 'missionFilesTotal', 'phasePassed',
|
|
332
|
+
'phaseFailed', 'phaseTotal', 'turns', 'tokens', 'inputTokens', 'outputTokens', 'durationSeconds',
|
|
333
|
+
'routedTurns', 'forcedPrimaryRetries', 'requestFallbacks', 'completionGates', 'jevEvaluations',
|
|
334
|
+
'jevInputTokens', 'jevOutputTokens', 'models', 'reportFile',
|
|
335
|
+
];
|
|
336
|
+
const conditionColumns = Object.keys(summaries[0]);
|
|
337
|
+
const missionColumns = Object.keys(missionRows[0]);
|
|
338
|
+
writeCsv(path.join(packageDir, 'data/runs.csv'), runColumns, runs);
|
|
339
|
+
writeCsv(path.join(packageDir, 'data/conditions.csv'), conditionColumns, summaries.map(row => Object.fromEntries(Object.entries(row).map(([key, value]) => [key, typeof value === 'number' ? round(value, 6) : value]))));
|
|
340
|
+
writeCsv(path.join(packageDir, 'data/missions.csv'), missionColumns, missionRows);
|
|
341
|
+
|
|
342
|
+
for (const entry of entries) {
|
|
343
|
+
fs.copyFileSync(entry.path, path.join(packageDir, `data/raw-reports/${entry.target}/${entry.file}`));
|
|
344
|
+
}
|
|
345
|
+
for (const name of ['state-openai.json', 'state-gemini.json', 'runs-openai.csv', 'runs-gemini.csv']) {
|
|
346
|
+
const source = path.join(root, name);
|
|
347
|
+
if (fs.existsSync(source)) fs.copyFileSync(source, path.join(packageDir, `data/source-state/${name}`));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const matrix = Object.fromEntries(Object.entries(BENCHMARK_TARGETS).map(([target, value]) => [target, {
|
|
351
|
+
provider: value.provider,
|
|
352
|
+
repoName: path.basename(value.repo),
|
|
353
|
+
missions: value.missions,
|
|
354
|
+
conditions: value.conditions,
|
|
355
|
+
}]));
|
|
356
|
+
fs.writeFileSync(path.join(packageDir, 'data/matrix.json'), `${JSON.stringify(matrix, null, 2)}\n`);
|
|
357
|
+
const report = buildReport(experimentId, runs, summaries);
|
|
358
|
+
fs.writeFileSync(path.join(packageDir, 'REPORT.md'), report);
|
|
359
|
+
fs.writeFileSync(path.join(root, `${experimentId}-report.md`), report);
|
|
360
|
+
fs.writeFileSync(path.join(packageDir, 'README.md'), `# ${experimentId} data package\n\nStart with [REPORT.md](REPORT.md). Normalized datasets are under \`data/\`; raw Testronaut JSON reports are under \`data/raw-reports/\`. See \`manifest.json\` and \`SHA256SUMS\` for provenance and integrity. Screenshots, HTML, credentials, environment files, and verbose logs are excluded.\n`);
|
|
361
|
+
|
|
362
|
+
const dataFiles = walkFiles(packageDir).filter(file => !['manifest.json', 'SHA256SUMS'].includes(path.basename(file)));
|
|
363
|
+
const manifest = {
|
|
364
|
+
schemaVersion: 1,
|
|
365
|
+
experimentId,
|
|
366
|
+
generatedAt: new Date().toISOString(),
|
|
367
|
+
counts: { reports: entries.length, runs: runs.length, conditions: summaries.length, missionFileAttempts: missionRows.length },
|
|
368
|
+
exclusions: ['screenshots', 'HTML reports', 'environment files', 'credentials', 'verbose CLI logs'],
|
|
369
|
+
files: dataFiles.map(file => ({ path: path.relative(packageDir, file), bytes: fs.statSync(file).size, sha256: sha256(file) })),
|
|
370
|
+
};
|
|
371
|
+
fs.writeFileSync(path.join(packageDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
372
|
+
const checksumFiles = walkFiles(packageDir).filter(file => path.basename(file) !== 'SHA256SUMS');
|
|
373
|
+
fs.writeFileSync(path.join(packageDir, 'SHA256SUMS'), checksumFiles.map(file => `${sha256(file)} ${path.relative(packageDir, file)}`).sort().join('\n') + '\n');
|
|
374
|
+
|
|
375
|
+
const archive = path.join(root, `${experimentId}-data-package.tar.gz`);
|
|
376
|
+
fs.rmSync(archive, { force: true });
|
|
377
|
+
const tar = spawnSync('tar', ['-czf', archive, '-C', root, 'data-package'], { encoding: 'utf8' });
|
|
378
|
+
if (tar.status !== 0) throw new Error(`Could not create archive: ${tar.stderr || tar.stdout}`);
|
|
379
|
+
fs.writeFileSync(`${archive}.sha256`, `${sha256(archive)} ${path.basename(archive)}\n`);
|
|
380
|
+
return { root, packageDir, archive, manifest };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
384
|
+
const experimentId = process.argv.find(arg => arg.startsWith('--experiment-id='))?.split('=')[1] || DEFAULT_EXPERIMENT_ID;
|
|
385
|
+
const result = buildOptimizationDataPackage({ experimentId });
|
|
386
|
+
console.log(`Report and data package generated at ${result.packageDir}`);
|
|
387
|
+
console.log(`Archive: ${result.archive}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export const __dataPackageInternals = { mean, sampleSd, normalizeRun, summarizeConditions, pairedDelta };
|