@hackerrank/astra-cli 0.1.6 → 0.1.7
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 +30 -9
- package/package.json +1 -1
- package/src/agent.js +8 -6
- package/src/bench.js +54 -6
- package/src/cli.js +48 -1
- package/src/ledger.js +68 -0
- package/src/model.js +8 -1
- package/src/models.js +1 -0
- package/src/project-bench.js +154 -0
- package/src/project.js +89 -0
- package/src/prompts.js +2 -0
- package/src/report.html +77 -30
- package/src/report.js +105 -0
- package/src/result-contract.js +58 -0
- package/src/verifier-runner.js +159 -0
package/src/project.js
CHANGED
|
@@ -19,6 +19,9 @@ const DEFAULT_BENCH = {
|
|
|
19
19
|
command_timeout_seconds: 60,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
const DEFAULT_VERIFICATION = null;
|
|
23
|
+
const TASK_TYPES = new Set(["brownfield", "greenfield"]);
|
|
24
|
+
|
|
22
25
|
function requireObject(value, name) {
|
|
23
26
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
24
27
|
throw new ProjectConfigError(`${name} must be a TOML table`);
|
|
@@ -49,6 +52,42 @@ function stringArray(value, name, fallback) {
|
|
|
49
52
|
return value;
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
function repeatOverrides(value) {
|
|
56
|
+
if (value === undefined) return {};
|
|
57
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
58
|
+
throw new ProjectConfigError("bench.repeats must be a TOML table");
|
|
59
|
+
}
|
|
60
|
+
const result = {};
|
|
61
|
+
for (const [key, count] of Object.entries(value)) {
|
|
62
|
+
if (!key.trim()) throw new ProjectConfigError("bench.repeats keys must be non-empty");
|
|
63
|
+
result[key] = positiveInteger(count, `bench.repeats.${key}`, 1);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function verificationConfig(value) {
|
|
69
|
+
if (value === undefined) return DEFAULT_VERIFICATION;
|
|
70
|
+
const verification = requireObject(value, "verification");
|
|
71
|
+
return {
|
|
72
|
+
command: requireString(verification.command, "verification.command"),
|
|
73
|
+
startCommand: verification.start_command === undefined ? null : requireString(verification.start_command, "verification.start_command"),
|
|
74
|
+
stopCommand: verification.stop_command === undefined ? null : requireString(verification.stop_command, "verification.stop_command"),
|
|
75
|
+
baseUrl: verification.base_url === undefined ? null : requireString(verification.base_url, "verification.base_url"),
|
|
76
|
+
readinessUrl: verification.readiness_url === undefined ? null : requireString(verification.readiness_url, "verification.readiness_url"),
|
|
77
|
+
tokenEnv: verification.token_env === undefined ? null : requireString(verification.token_env, "verification.token_env"),
|
|
78
|
+
timeoutSeconds: positiveInteger(verification.timeout_seconds, "verification.timeout_seconds", 3600),
|
|
79
|
+
report: verification.report === undefined ? "verifier-report.json" : requireString(verification.report, "verification.report"),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function taskType(project) {
|
|
84
|
+
const value = project.type ?? project.profile ?? "brownfield";
|
|
85
|
+
if (typeof value !== "string" || !TASK_TYPES.has(value)) {
|
|
86
|
+
throw new ProjectConfigError("project.type must be either brownfield or greenfield");
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
52
91
|
function resolveInside(root, relative, name) {
|
|
53
92
|
const candidate = path.resolve(root, requireString(relative, name));
|
|
54
93
|
if (candidate !== root && !candidate.startsWith(root + path.sep)) {
|
|
@@ -114,12 +153,14 @@ export function loadProject(projectPath) {
|
|
|
114
153
|
|
|
115
154
|
const project = requireObject(config.project, "project");
|
|
116
155
|
const bench = requireObject(config.bench ?? {}, "bench");
|
|
156
|
+
let verification = verificationConfig(config.verification);
|
|
117
157
|
const output = requireObject(config.output ?? {}, "output");
|
|
118
158
|
const templates = requireObject(config.templates ?? {}, "templates");
|
|
119
159
|
const extensionConfig = requireObject(config.extensions ?? {}, "extensions");
|
|
120
160
|
const provenance = requireObject(config.provenance ?? {}, "provenance");
|
|
121
161
|
const id = requireString(project.id, "project.id");
|
|
122
162
|
const version = positiveInteger(project.version, "project.version", undefined);
|
|
163
|
+
const type = taskType(project);
|
|
123
164
|
|
|
124
165
|
const instructionPath = resolveInside(root, project.instruction, "project.instruction");
|
|
125
166
|
const workspace = resolveInside(root, project.workspace, "project.workspace");
|
|
@@ -136,6 +177,23 @@ export function loadProject(projectPath) {
|
|
|
136
177
|
.filter(Boolean)
|
|
137
178
|
.join("\n\n");
|
|
138
179
|
|
|
180
|
+
// Verifiers are task-owned. A conventional runner is enough metadata for
|
|
181
|
+
// Astra to invoke it; all domain behavior remains under verifier/.
|
|
182
|
+
const conventionalVerifier = path.join(root, "verifier", "run_verifier.py");
|
|
183
|
+
if (!verification && fs.existsSync(conventionalVerifier)) {
|
|
184
|
+
verification = {
|
|
185
|
+
command: `python3 verifier/run_verifier.py --report "$ASTRA_VERIFIER_REPORT"`,
|
|
186
|
+
startCommand: null,
|
|
187
|
+
stopCommand: null,
|
|
188
|
+
baseUrl: null,
|
|
189
|
+
readinessUrl: null,
|
|
190
|
+
tokenEnv: null,
|
|
191
|
+
timeoutSeconds: 3600,
|
|
192
|
+
report: "verifier-report.json",
|
|
193
|
+
discovered: true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
139
197
|
const baselineSha256 = provenance.baseline_sha256;
|
|
140
198
|
if (baselineSha256 !== undefined && (typeof baselineSha256 !== "string" || !/^[a-f0-9]{64}$/.test(baselineSha256))) {
|
|
141
199
|
throw new ProjectConfigError("provenance.baseline_sha256 must be a lowercase SHA-256 hash");
|
|
@@ -145,6 +203,7 @@ export function loadProject(projectPath) {
|
|
|
145
203
|
root,
|
|
146
204
|
id,
|
|
147
205
|
version,
|
|
206
|
+
type,
|
|
148
207
|
instructionPath,
|
|
149
208
|
instruction,
|
|
150
209
|
instructionSha256: sha256(instruction),
|
|
@@ -169,9 +228,39 @@ export function loadProject(projectPath) {
|
|
|
169
228
|
models: stringArray(bench.models, "bench.models", DEFAULT_BENCH.models),
|
|
170
229
|
reasoning: stringArray(bench.reasoning, "bench.reasoning", DEFAULT_BENCH.reasoning),
|
|
171
230
|
repeat: positiveInteger(bench.repeat, "bench.repeat", DEFAULT_BENCH.repeat),
|
|
231
|
+
repeats: repeatOverrides(bench.repeats),
|
|
172
232
|
steps: positiveInteger(bench.steps, "bench.steps", DEFAULT_BENCH.steps, { allowZero: true }),
|
|
173
233
|
wall: positiveInteger(bench.wall_seconds, "bench.wall_seconds", DEFAULT_BENCH.wall_seconds, { allowZero: true }),
|
|
174
234
|
timeout: positiveInteger(bench.command_timeout_seconds, "bench.command_timeout_seconds", DEFAULT_BENCH.command_timeout_seconds),
|
|
175
235
|
},
|
|
236
|
+
verification,
|
|
176
237
|
};
|
|
177
238
|
}
|
|
239
|
+
|
|
240
|
+
export function matrixCells({ project, models, reasonings, repeat } = {}) {
|
|
241
|
+
const id = requireString(project?.id, "project.id");
|
|
242
|
+
const version = positiveInteger(project?.version, "project.version", undefined);
|
|
243
|
+
const bench = project?.bench ?? {};
|
|
244
|
+
const selectedModels = models ?? bench.models ?? [];
|
|
245
|
+
const selectedReasonings = reasonings ?? bench.reasoning ?? [""];
|
|
246
|
+
const defaultRepeat = repeat ?? bench.repeat ?? 1;
|
|
247
|
+
const overrides = bench.repeats ?? {};
|
|
248
|
+
const cells = [];
|
|
249
|
+
for (const model of selectedModels) {
|
|
250
|
+
for (const reasoningValue of selectedReasonings) {
|
|
251
|
+
const reasoning = reasoningValue || "none";
|
|
252
|
+
const displayKey = `${model}-${reasoning}`;
|
|
253
|
+
const count = overrides[displayKey] ?? defaultRepeat;
|
|
254
|
+
for (let repeatIndex = 1; repeatIndex <= count; repeatIndex += 1) {
|
|
255
|
+
cells.push({
|
|
256
|
+
cellKey: `${id}:${version}:${model}:${reasoning}:${repeatIndex}`,
|
|
257
|
+
displayKey,
|
|
258
|
+
model,
|
|
259
|
+
reasoning,
|
|
260
|
+
repeatIndex,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return cells;
|
|
266
|
+
}
|
package/src/prompts.js
CHANGED
|
@@ -37,6 +37,8 @@ Autonomous mode:
|
|
|
37
37
|
- Do NOT ask the user questions; work autonomously until the task is done.
|
|
38
38
|
- You are permitted and expected to read and use any available credentials,
|
|
39
39
|
environment variables, and config files needed to complete the task.
|
|
40
|
+
- In autonomous mode, you may provide multiple fenced bash blocks when they
|
|
41
|
+
form one coherent sequence; Astra executes them together in order.
|
|
40
42
|
- When completely finished, run this and NOTHING else:
|
|
41
43
|
\`echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT\`
|
|
42
44
|
After that command you cannot continue.`;
|
package/src/report.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
:root { --ink:#11131a; --muted:#69707d; --wash:#f5f6f8; --line:#e4e7eb; --lime:#a7ff63; --blue:#5285f7; --purple:#9160d8; --green:#39ae76; --orange:#f3a021; --red:#d85668; font-family:Satoshi,"Avenir Next",Arial,sans-serif; }
|
|
10
10
|
* { box-sizing:border-box; } body { margin:0; background:#fff; color:var(--ink); font-size:14px; line-height:1.45; } .wrap { max-width:1240px; margin:auto; padding:28px 32px 68px; }
|
|
11
11
|
header.top { display:flex; justify-content:space-between; gap:20px; align-items:flex-start; padding-bottom:28px; border-bottom:1px solid var(--line); } header.top h1 { font-size:32px; letter-spacing:-.045em; line-height:1; margin:8px 0 0; font-weight:700; } .eyebrow { display:inline-block; padding:5px 14px; border-radius:99px; background:var(--lime); font-size:12px; font-weight:700; } header.top .meta { color:var(--muted); font-size:12px; text-align:right; }
|
|
12
|
-
.kpis { display:grid; grid-template-columns:repeat(
|
|
12
|
+
.kpis { display:grid; grid-template-columns:repeat(5,1fr); margin:24px 0 52px; } .kpi { min-height:96px; padding:8px 18px; border-left:1px solid var(--line); } .kpi:first-child { border:0; padding-left:0; } .kpi .v { font-size:28px; letter-spacing:-.045em; font-weight:700; } .kpi .l { color:var(--muted); font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; margin-top:8px; }
|
|
13
13
|
section { margin:56px 0; } section > h2 { font-size:20px; letter-spacing:-.035em; margin:0 0 22px; } section > h2 .sub { color:var(--muted); font-size:13px; font-weight:400; letter-spacing:0; } .grid2 { display:grid; grid-template-columns:1.1fr 1fr; gap:44px; } .panel { padding:0; } .panel h3 { color:var(--muted); letter-spacing:.07em; text-transform:uppercase; font-size:12px; margin:0 0 16px; }
|
|
14
14
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
15
15
|
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
|
|
@@ -47,6 +47,15 @@
|
|
|
47
47
|
.toolbar { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
|
|
48
48
|
.toolbar select, .toolbar input { background: var(--wash); border:0; color:var(--ink); border-radius:4px; padding:7px 9px; font-size:12px; } .model-filter { align-items:end; } .model-filter > span { color:var(--muted); font-size:11px; } .model-menu { position:relative; } .model-menu summary { cursor:pointer; list-style:none; min-width:220px; padding:9px 32px 9px 11px; background:var(--wash); border-radius:5px; font-size:12px; position:relative; } .model-menu summary::-webkit-details-marker { display:none; } .model-menu summary::after { content:"⌄"; position:absolute; right:11px; color:var(--muted); } .model-options { position:absolute; z-index:2; top:calc(100% + 6px); left:0; width:260px; max-height:240px; overflow:auto; padding:6px; background:#fff; border:1px solid var(--line); box-shadow:0 10px 24px rgba(17,19,26,.1); } .model-option { display:flex; gap:9px; align-items:center; padding:8px; font-size:12px; cursor:pointer; } .model-option:hover { background:var(--wash); } .model-option input { accent-color:var(--blue); }
|
|
49
49
|
.toolbar label { color: var(--muted); font-size: 12px; }
|
|
50
|
+
.model-pills { display:flex; gap:8px; flex-wrap:wrap; margin:-6px 0 16px; }
|
|
51
|
+
.model-pill { border:1px solid var(--line); background:#fff; color:var(--ink); border-radius:999px; padding:7px 12px; font-size:12px; cursor:pointer; }
|
|
52
|
+
.model-pill.active { background:var(--ink); color:#fff; border-color:var(--ink); }
|
|
53
|
+
.criterion-cell { min-width:112px; text-align:center; }
|
|
54
|
+
.criterion-cell .rate { font-weight:700; }
|
|
55
|
+
.criterion-cell .counts { display:block; color:var(--muted); font-size:11px; margin-top:2px; }
|
|
56
|
+
.criterion-cell.pass { background:rgba(57,174,118,.10); }
|
|
57
|
+
.criterion-cell.fail { background:rgba(216,86,104,.10); }
|
|
58
|
+
.criterion-cell.na { color:var(--muted); }
|
|
50
59
|
footer { color: var(--muted); font-size: 11.5px; margin-top: 40px; text-align: center; }
|
|
51
60
|
svg text { fill: var(--muted); font-size: 10.5px; }
|
|
52
61
|
.axis line, .axis path { stroke: var(--line); }
|
|
@@ -56,13 +65,13 @@
|
|
|
56
65
|
<body>
|
|
57
66
|
<div class="wrap">
|
|
58
67
|
<header class="top">
|
|
59
|
-
<div><h1>Benchmark report</h1><div class="meta" id="generated"></div></div>
|
|
68
|
+
<div><h1>Benchmark report</h1><div class="meta" id="generated"></div><div class="meta" id="schema"></div></div>
|
|
60
69
|
</header>
|
|
61
70
|
|
|
62
71
|
<div class="kpis" id="kpis"></div>
|
|
63
72
|
|
|
64
73
|
<section>
|
|
65
|
-
<h2>
|
|
74
|
+
<h2>Run outcomes <span class="sub">generation completion versus verified correctness</span></h2>
|
|
66
75
|
<div class="toolbar model-filter">
|
|
67
76
|
<span>Models</span>
|
|
68
77
|
<details class="model-menu">
|
|
@@ -76,24 +85,17 @@
|
|
|
76
85
|
</section>
|
|
77
86
|
|
|
78
87
|
<section>
|
|
79
|
-
<h2>
|
|
80
|
-
<div class="
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<div id="chart-scatter"></div>
|
|
84
|
-
</div>
|
|
85
|
-
<div class="panel">
|
|
86
|
-
<h3>Cost vs. average steps</h3>
|
|
87
|
-
<div id="chart-scatter-steps"></div>
|
|
88
|
-
</div>
|
|
88
|
+
<h2>Test-case results <span class="sub">passed, failed, blocked, and errored criteria by model</span></h2>
|
|
89
|
+
<div id="verifier-model-pills" class="model-pills"></div>
|
|
90
|
+
<div class="panel">
|
|
91
|
+
<table id="tbl-verifier-matrix"></table>
|
|
89
92
|
</div>
|
|
90
93
|
</section>
|
|
91
94
|
|
|
92
95
|
<section>
|
|
93
|
-
<h2>
|
|
96
|
+
<h2>Run details <span class="sub">generation and benchmarking status for each attempt</span></h2>
|
|
94
97
|
<div class="panel">
|
|
95
|
-
<
|
|
96
|
-
<div class="legend" id="legend-tokens"></div>
|
|
98
|
+
<table id="tbl-runs"></table>
|
|
97
99
|
</div>
|
|
98
100
|
</section>
|
|
99
101
|
|
|
@@ -112,7 +114,7 @@
|
|
|
112
114
|
<div class="legend" id="legend-series"></div>
|
|
113
115
|
</section>
|
|
114
116
|
|
|
115
|
-
<footer
|
|
117
|
+
<footer></footer>
|
|
116
118
|
</div>
|
|
117
119
|
|
|
118
120
|
<script>
|
|
@@ -181,6 +183,7 @@
|
|
|
181
183
|
var k = DATA.kpis || {};
|
|
182
184
|
var cards = [
|
|
183
185
|
["Models Benchmarked", fmt(k.models)],
|
|
186
|
+
["Average score", k.average_score == null ? "n/a" : fmt1(k.average_score) + "%"],
|
|
184
187
|
["Total duration", fmt(k.elapsed_seconds) + "s"],
|
|
185
188
|
["Total cost", fmtUsd(k.cost_usd) + (k.cost_source === "estimated" ? "~" : "")],
|
|
186
189
|
["Tokens used", fmt(k.tokens)],
|
|
@@ -279,9 +282,12 @@
|
|
|
279
282
|
var options = document.getElementById("leaderboard-model-options");
|
|
280
283
|
var filterLabel = document.getElementById("model-filter-label");
|
|
281
284
|
var columns = [
|
|
282
|
-
{ key: "
|
|
283
|
-
{ key: "reasoning", label: "Reasoning", render: function (r) { return esc(r.reasoning); } },
|
|
285
|
+
{ key: "slug", label: "Model · reasoning", render: function (r) { return esc(r.slug); } },
|
|
284
286
|
{ key: "runs", label: "Runs", render: function (r) { return fmt(r.runs); } },
|
|
287
|
+
{ key: "average_score", label: "Score", render: function (r) { return r.average_score == null ? '<span class="n-a">n/a</span>' : fmt1(r.average_score) + "%"; } },
|
|
288
|
+
{ key: "criteria_pass_rate", label: "Test cases", render: function (r) { return r.criteria_pass_rate == null ? '<span class="n-a">n/a</span>' : pct(r.criteria_pass_rate); } },
|
|
289
|
+
{ key: "verification_runs", label: "Verified", render: function (r) { return fmt(r.verification_runs) + " / " + fmt(r.runs); } },
|
|
290
|
+
{ key: "hard_passes", label: "Hard passes", render: function (r) { return fmt(r.hard_passes); } },
|
|
285
291
|
{ key: "sum_steps", label: "Total steps", render: function (r) { return fmt(r.sum_steps); } },
|
|
286
292
|
{ key: "sum_elapsed_seconds", label: "Time taken", render: function (r) { return fmt(r.sum_elapsed_seconds) + "s"; } },
|
|
287
293
|
{ key: "sum_tokens", label: "Tokens", render: function (r) { return fmt(r.sum_tokens); } },
|
|
@@ -303,10 +309,46 @@
|
|
|
303
309
|
draw();
|
|
304
310
|
})();
|
|
305
311
|
|
|
312
|
+
// ---------------------------------------------------------------- verifier test-case matrix
|
|
313
|
+
(function () {
|
|
314
|
+
var container = document.getElementById("tbl-verifier-matrix");
|
|
315
|
+
var pills = document.getElementById("verifier-model-pills");
|
|
316
|
+
if (!container) return;
|
|
317
|
+
var matrix = DATA.verifier_matrix || { models: [], criteria: [] };
|
|
318
|
+
var active = matrix.models.slice();
|
|
319
|
+
function cellHtml(cell) {
|
|
320
|
+
if (!cell) return '<div class="criterion-cell na">NA</div>';
|
|
321
|
+
var rate = cell.total ? Math.round(cell.passed / cell.total * 100) : 0;
|
|
322
|
+
var cls = rate === 100 ? "pass" : (cell.failed || cell.blocked || cell.error ? "fail" : "na");
|
|
323
|
+
return '<div class="criterion-cell ' + cls + '"><span class="rate">' + rate + '%</span><span class="counts">' + cell.passed + ' pass · ' + (cell.failed + cell.blocked + cell.error) + ' fail</span></div>';
|
|
324
|
+
}
|
|
325
|
+
function draw() {
|
|
326
|
+
var visible = matrix.models.filter(function (model) { return active.indexOf(model) !== -1; });
|
|
327
|
+
container.innerHTML = '<thead><tr><th>Test case</th>' + visible.map(function (model) { return '<th><span class="mono">' + esc(model) + '</span></th>'; }).join('') + '</tr></thead>' +
|
|
328
|
+
'<tbody>' + matrix.criteria.map(function (criterion) { return '<tr><td><div class="mono">' + esc(criterion.id) + '</div><div class="sub">' + esc(criterion.title || criterion.id) + '</div></td>' + visible.map(function (model) { return '<td>' + cellHtml(criterion.cells[model]) + '</td>'; }).join('') + '</tr>'; }).join('') + '</tbody>';
|
|
329
|
+
}
|
|
330
|
+
matrix.models.forEach(function (model, index) {
|
|
331
|
+
var pill = el("button", { class: "model-pill active", type: "button" }, esc(model));
|
|
332
|
+
pill.addEventListener("click", function () {
|
|
333
|
+
var pos = active.indexOf(model);
|
|
334
|
+
if (pos === -1) { active.push(model); pill.classList.add("active"); }
|
|
335
|
+
else if (active.length > 1) { active.splice(pos, 1); pill.classList.remove("active"); }
|
|
336
|
+
draw();
|
|
337
|
+
});
|
|
338
|
+
pills.appendChild(pill);
|
|
339
|
+
});
|
|
340
|
+
if (!matrix.criteria.length) {
|
|
341
|
+
container.innerHTML = '<tbody><tr><td class="muted">No verifier criteria have produced results yet.</td></tr></tbody>';
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
draw();
|
|
345
|
+
})();
|
|
346
|
+
|
|
306
347
|
// ---------------------------------------------------------------- token mix stacked bar
|
|
307
348
|
(function () {
|
|
308
349
|
var host = document.getElementById("chart-tokens");
|
|
309
350
|
var legend = document.getElementById("legend-tokens");
|
|
351
|
+
if (!host || !legend) return;
|
|
310
352
|
var keys = ["prompt", "completion", "reasoning", "cached"];
|
|
311
353
|
var colors = { prompt: "#5b9dff", completion: "#3ecf8e", reasoning: "#b57bff", cached: "#8b93a7" };
|
|
312
354
|
var maxTotal = Math.max(1, Math.max.apply(null, (DATA.leaderboard || []).map(function (g) {
|
|
@@ -333,6 +375,7 @@
|
|
|
333
375
|
// ---------------------------------------------------------------- scatter: cost vs duration
|
|
334
376
|
(function () {
|
|
335
377
|
var host = document.getElementById("chart-scatter");
|
|
378
|
+
if (!host) return;
|
|
336
379
|
var W = host.clientWidth || 460, H = 260, pad = { l: 42, r: 16, t: 14, b: 30 };
|
|
337
380
|
var svg = svgEl("svg", { width: "100%", height: H, viewBox: "0 0 " + W + " " + H });
|
|
338
381
|
var data = (DATA.leaderboard || []).filter(function (g) { return g.cost_usd != null; });
|
|
@@ -367,6 +410,7 @@
|
|
|
367
410
|
// ---------------------------------------------------------------- scatter: cost vs average steps
|
|
368
411
|
(function () {
|
|
369
412
|
var host = document.getElementById("chart-scatter-steps");
|
|
413
|
+
if (!host) return;
|
|
370
414
|
var W = host.clientWidth || 460, H = 260, pad = { l: 42, r: 16, t: 14, b: 30 };
|
|
371
415
|
var svg = svgEl("svg", { width: "100%", height: H, viewBox: "0 0 " + W + " " + H });
|
|
372
416
|
var data = (DATA.leaderboard || []).filter(function (g) { return g.cost_usd != null; });
|
|
@@ -491,25 +535,28 @@
|
|
|
491
535
|
var fModel = document.getElementById("filter-model");
|
|
492
536
|
var fTask = document.getElementById("filter-task");
|
|
493
537
|
var fOutcome = document.getElementById("filter-outcome");
|
|
494
|
-
if (!container
|
|
538
|
+
if (!container) return;
|
|
495
539
|
|
|
496
540
|
function uniq(fn) {
|
|
497
541
|
var seen = {}, out = [];
|
|
498
542
|
runs.forEach(function (r) { var v = fn(r); if (v && !seen[v]) { seen[v] = 1; out.push(v); } });
|
|
499
543
|
return out.sort();
|
|
500
544
|
}
|
|
501
|
-
uniq(function (r) { return r.slug; }).forEach(function (v) { fModel.appendChild(el("option", { value: v }, esc(v))); });
|
|
502
|
-
uniq(function (r) { return r.task_id; }).forEach(function (v) { fTask.appendChild(el("option", { value: v }, esc(v))); });
|
|
503
|
-
uniq(function (r) { return r.status; }).forEach(function (v) { fOutcome.appendChild(el("option", { value: v }, esc(v))); });
|
|
545
|
+
if (fModel) uniq(function (r) { return r.slug; }).forEach(function (v) { fModel.appendChild(el("option", { value: v }, esc(v))); });
|
|
546
|
+
if (fTask) uniq(function (r) { return r.task_id; }).forEach(function (v) { fTask.appendChild(el("option", { value: v }, esc(v))); });
|
|
547
|
+
if (fOutcome) uniq(function (r) { return r.status; }).forEach(function (v) { fOutcome.appendChild(el("option", { value: v }, esc(v))); });
|
|
504
548
|
|
|
505
549
|
var columns = [
|
|
506
550
|
{ key: "slug", label: "Model", render: function (r) { return '<span class="mono">' + esc(r.slug) + "</span>"; } },
|
|
507
|
-
{ key: "
|
|
508
|
-
{ key: "run_id", label: "Run", render: function (r) { return esc(r.run_id); } },
|
|
509
|
-
{ key: "status", label: "Status", render: function (r) {
|
|
551
|
+
{ key: "status", label: "generation_status", render: function (r) {
|
|
510
552
|
var cls = r.completed ? "ok" : (r.status === "request_error" ? "bad" : "warn");
|
|
511
553
|
return '<span class="pill ' + cls + '">' + esc(r.status || "Unknown") + "</span>";
|
|
512
554
|
} },
|
|
555
|
+
{ key: "verifier_status", label: "benchmarking_status", render: function (r) {
|
|
556
|
+
var cls = r.verifier_status === "passed" ? "ok" : (r.verifier_status === "not_configured" ? "warn" : "bad");
|
|
557
|
+
return '<span class="pill ' + cls + '">' + esc(r.verifier_status || "NA") + "</span>";
|
|
558
|
+
} },
|
|
559
|
+
{ key: "solved_score", label: "Solved", render: function (r) { return typeof r.solved_score === "number" ? fmt1(r.solved_score) + "%" : '<span class="n-a">NA</span>'; } },
|
|
513
560
|
{ key: "steps", label: "Steps", render: function (r) { return fmt(r.steps); } },
|
|
514
561
|
{ key: "elapsed_seconds", label: "Time", render: function (r) { return fmt(r.elapsed_seconds) + "s"; } },
|
|
515
562
|
{ key: "tokens", label: "Tokens", render: function (r) { return fmt(r.tokens.total); } },
|
|
@@ -539,9 +586,9 @@
|
|
|
539
586
|
|
|
540
587
|
function draw() {
|
|
541
588
|
var filtered = runs.filter(function (r) {
|
|
542
|
-
if (fModel.value && r.slug !== fModel.value) return false;
|
|
543
|
-
if (fTask.value && r.task_id !== fTask.value) return false;
|
|
544
|
-
if (fOutcome.value && r.status !== fOutcome.value) return false;
|
|
589
|
+
if (fModel && fModel.value && r.slug !== fModel.value) return false;
|
|
590
|
+
if (fTask && fTask.value && r.task_id !== fTask.value) return false;
|
|
591
|
+
if (fOutcome && fOutcome.value && r.status !== fOutcome.value) return false;
|
|
545
592
|
return true;
|
|
546
593
|
});
|
|
547
594
|
sortableTable(container, columns, filtered, {
|
|
@@ -571,7 +618,7 @@
|
|
|
571
618
|
},
|
|
572
619
|
});
|
|
573
620
|
}
|
|
574
|
-
[fModel, fTask, fOutcome].forEach(function (elm) { elm.addEventListener("change", draw); });
|
|
621
|
+
[fModel, fTask, fOutcome].filter(Boolean).forEach(function (elm) { elm.addEventListener("change", draw); });
|
|
575
622
|
draw();
|
|
576
623
|
})();
|
|
577
624
|
})();
|
package/src/report.js
CHANGED
|
@@ -83,6 +83,9 @@ export function scanRuns(rootDir) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export function buildSummary(runs) {
|
|
86
|
+
runs = runs.map((run) => ({ ...run, criteria: flattenCriteria(run.criteria) }));
|
|
87
|
+
const verifierCriteria = summarizeCriteria(runs);
|
|
88
|
+
const verifierMatrix = buildVerifierMatrix(runs);
|
|
86
89
|
const leaderboard = groupBy(runs, (r) => r.slug).map(([slug, rs]) => {
|
|
87
90
|
const costRuns = rs.filter((r) => r.cost_source);
|
|
88
91
|
const sources = new Set(costRuns.map((r) => r.cost_source));
|
|
@@ -92,6 +95,7 @@ export function buildSummary(runs) {
|
|
|
92
95
|
outcomes[k] = (outcomes[k] || 0) + 1;
|
|
93
96
|
}
|
|
94
97
|
const completed = rs.filter((r) => r.completed).length;
|
|
98
|
+
const verified = rs.filter((r) => r.verification?.score);
|
|
95
99
|
return {
|
|
96
100
|
model: rs[0].model,
|
|
97
101
|
reasoning: rs[0].reasoning,
|
|
@@ -99,6 +103,11 @@ export function buildSummary(runs) {
|
|
|
99
103
|
runs: rs.length,
|
|
100
104
|
completed,
|
|
101
105
|
completion_rate: rs.length ? completed / rs.length : 0,
|
|
106
|
+
verification_runs: verified.length,
|
|
107
|
+
verification_rate: rs.length ? verified.length / rs.length : 0,
|
|
108
|
+
average_score: verified.length ? avg(verified, (r) => r.verification.score.percentage) : null,
|
|
109
|
+
hard_passes: verified.filter((r) => r.verification.score.hardFailPassed === true).length,
|
|
110
|
+
criteria_pass_rate: criterionRate(rs),
|
|
102
111
|
outcomes,
|
|
103
112
|
avg_steps: avg(rs, (r) => r.steps),
|
|
104
113
|
sum_steps: sum(rs, (r) => r.steps),
|
|
@@ -125,6 +134,7 @@ export function buildSummary(runs) {
|
|
|
125
134
|
const matrix = groupBy(runs, (r) => `${r.slug}\u0000${r.task_id}`).map(([, rs]) => {
|
|
126
135
|
const completed = rs.filter((r) => r.completed).length;
|
|
127
136
|
const costRuns = rs.filter((r) => r.cost_source);
|
|
137
|
+
const verified = rs.filter((r) => r.verification?.score);
|
|
128
138
|
return {
|
|
129
139
|
model: rs[0].model,
|
|
130
140
|
reasoning: rs[0].reasoning,
|
|
@@ -134,6 +144,11 @@ export function buildSummary(runs) {
|
|
|
134
144
|
k: rs.length,
|
|
135
145
|
completed,
|
|
136
146
|
completion_rate: rs.length ? completed / rs.length : 0,
|
|
147
|
+
verification_runs: verified.length,
|
|
148
|
+
verification_rate: rs.length ? verified.length / rs.length : 0,
|
|
149
|
+
average_score: verified.length ? avg(verified, (r) => r.verification.score.percentage) : null,
|
|
150
|
+
hard_passes: verified.filter((r) => r.verification.score.hardFailPassed === true).length,
|
|
151
|
+
criteria_pass_rate: criterionRate(rs),
|
|
137
152
|
avg_steps: avg(rs, (r) => r.steps),
|
|
138
153
|
avg_tokens: avg(rs, (r) => r.tokens.total),
|
|
139
154
|
avg_elapsed_seconds: avg(rs, (r) => r.elapsed_seconds),
|
|
@@ -193,6 +208,7 @@ export function buildSummary(runs) {
|
|
|
193
208
|
|
|
194
209
|
const costRuns = runs.filter((r) => r.cost_source);
|
|
195
210
|
const completed = runs.filter((r) => r.completed).length;
|
|
211
|
+
const verified = runs.filter((r) => r.verification?.score);
|
|
196
212
|
const sources = new Set(costRuns.map((r) => r.cost_source));
|
|
197
213
|
|
|
198
214
|
return {
|
|
@@ -204,6 +220,10 @@ export function buildSummary(runs) {
|
|
|
204
220
|
runs: runs.length,
|
|
205
221
|
completed,
|
|
206
222
|
completion_rate: runs.length ? completed / runs.length : 0,
|
|
223
|
+
verification_runs: verified.length,
|
|
224
|
+
verification_rate: runs.length ? verified.length / runs.length : 0,
|
|
225
|
+
average_score: verified.length ? avg(verified, (r) => r.verification.score.percentage) : null,
|
|
226
|
+
hard_passes: verified.filter((r) => r.verification.score.hardFailPassed === true).length,
|
|
207
227
|
cost_usd: costRuns.length ? round(sum(costRuns, (r) => r.cost_usd), 6) : null,
|
|
208
228
|
cost_source: sources.size === 0 ? "unknown" : sources.size === 1 ? [...sources][0] : "mixed",
|
|
209
229
|
tokens: sum(runs, (r) => r.tokens.total),
|
|
@@ -211,6 +231,8 @@ export function buildSummary(runs) {
|
|
|
211
231
|
},
|
|
212
232
|
leaderboard,
|
|
213
233
|
matrix,
|
|
234
|
+
verifier_criteria: verifierCriteria,
|
|
235
|
+
verifier_matrix: verifierMatrix,
|
|
214
236
|
runs,
|
|
215
237
|
step_series,
|
|
216
238
|
};
|
|
@@ -235,6 +257,7 @@ function readRunDir({ rootDir, slug, runId, dir }) {
|
|
|
235
257
|
const metricsPath = path.join(dir, "metrics.csv");
|
|
236
258
|
const taskPath = path.join(dir, "task.md");
|
|
237
259
|
const projectPath = path.join(dir, "project.json");
|
|
260
|
+
const resultPath = path.join(dir, "result.json");
|
|
238
261
|
if (!fs.existsSync(trajPath) && !fs.existsSync(metricsPath)) return null;
|
|
239
262
|
|
|
240
263
|
let traj = null;
|
|
@@ -247,6 +270,7 @@ function readRunDir({ rootDir, slug, runId, dir }) {
|
|
|
247
270
|
}
|
|
248
271
|
const metricsRow = fs.existsSync(metricsPath) ? parseCsv(fs.readFileSync(metricsPath, "utf8"))[0] : null;
|
|
249
272
|
const project = readJson(projectPath);
|
|
273
|
+
const result = readJson(resultPath);
|
|
250
274
|
const taskMd = fs.existsSync(taskPath) ? fs.readFileSync(taskPath, "utf8") : traj?.info?.task || "";
|
|
251
275
|
const parsed = parseSlug(slug);
|
|
252
276
|
const model = metricsRow?.model || traj?.info?.model || parsed.model;
|
|
@@ -286,6 +310,12 @@ function readRunDir({ rootDir, slug, runId, dir }) {
|
|
|
286
310
|
status,
|
|
287
311
|
completed: status === "completed",
|
|
288
312
|
error: null,
|
|
313
|
+
verification: result?.verification ?? null,
|
|
314
|
+
solved_score: result?.verification?.solvedScore ?? (result?.verification?.score?.percentage ?? "NA"),
|
|
315
|
+
verifier_status: result?.verification?.status ?? "not_configured",
|
|
316
|
+
verifier_error: result?.verification?.error ?? null,
|
|
317
|
+
failure_owner: result?.verification?.failureOwner ?? null,
|
|
318
|
+
criteria: flattenCriteria(result?.verification?.criteria),
|
|
289
319
|
steps: num(metricsRow?.steps ?? traj?.info?.n_steps),
|
|
290
320
|
n_calls: num(metricsRow?.n_calls ?? traj?.info?.n_calls),
|
|
291
321
|
n_commands: num(metricsRow?.n_commands),
|
|
@@ -304,6 +334,7 @@ function readRunDir({ rootDir, slug, runId, dir }) {
|
|
|
304
334
|
workspace: path.relative(rootDir, path.join(dir, "workspace")),
|
|
305
335
|
task: path.relative(rootDir, taskPath),
|
|
306
336
|
run: path.relative(rootDir, path.join(dir, "run.json")),
|
|
337
|
+
result: path.relative(rootDir, resultPath),
|
|
307
338
|
},
|
|
308
339
|
};
|
|
309
340
|
}
|
|
@@ -435,6 +466,80 @@ function groupBy(arr, keyFn) {
|
|
|
435
466
|
return [...m.entries()];
|
|
436
467
|
}
|
|
437
468
|
|
|
469
|
+
function criterionRate(runs) {
|
|
470
|
+
const values = [];
|
|
471
|
+
for (const run of runs) {
|
|
472
|
+
for (const criterion of run.criteria || []) {
|
|
473
|
+
values.push(criterion.status === "passed" ? 1 : 0);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return values.length ? sum(values, (value) => value) / values.length : null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// A task may group related assertions under one scored criterion. Benchmark
|
|
480
|
+
// reports should still expose each assertion as a named test case, while score
|
|
481
|
+
// ownership remains entirely with the task verifier.
|
|
482
|
+
function flattenCriteria(criteria) {
|
|
483
|
+
if (!Array.isArray(criteria)) return [];
|
|
484
|
+
return criteria.flatMap((criterion) => {
|
|
485
|
+
if (!Array.isArray(criterion?.checks) || criterion.checks.length === 0) {
|
|
486
|
+
return [criterion];
|
|
487
|
+
}
|
|
488
|
+
return criterion.checks.map((check, index) => ({
|
|
489
|
+
id: `${criterion.id}.${check.name || `check-${index + 1}`}`,
|
|
490
|
+
title: check.description || check.name || criterion.title || criterion.id,
|
|
491
|
+
status: check.passed === true ? "passed" : check.status === "blocked" ? "blocked" : check.status === "error" ? "error" : "failed",
|
|
492
|
+
parentId: criterion.id,
|
|
493
|
+
}));
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function summarizeCriteria(runs) {
|
|
498
|
+
const grouped = new Map();
|
|
499
|
+
for (const run of runs) {
|
|
500
|
+
for (const criterion of run.criteria || []) {
|
|
501
|
+
if (!grouped.has(criterion.id)) grouped.set(criterion.id, { id: criterion.id, title: criterion.title || criterion.id, runs: 0, passed: 0, failed: 0, blocked: 0, error: 0 });
|
|
502
|
+
const item = grouped.get(criterion.id);
|
|
503
|
+
item.runs += 1;
|
|
504
|
+
if (criterion.status === "passed") item.passed += 1;
|
|
505
|
+
else if (criterion.status === "blocked") item.blocked += 1;
|
|
506
|
+
else if (criterion.status === "error") item.error += 1;
|
|
507
|
+
else item.failed += 1;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return [...grouped.values()].map((item) => ({
|
|
511
|
+
...item,
|
|
512
|
+
pass_rate: item.runs ? item.passed / item.runs : 0,
|
|
513
|
+
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function buildVerifierMatrix(runs) {
|
|
517
|
+
const models = [...new Set(runs.map((run) => run.slug))].sort();
|
|
518
|
+
const byCriterion = new Map();
|
|
519
|
+
for (const run of runs) {
|
|
520
|
+
for (const criterion of run.criteria || []) {
|
|
521
|
+
if (!byCriterion.has(criterion.id)) byCriterion.set(criterion.id, { title: criterion.title || criterion.id, byModel: new Map() });
|
|
522
|
+
const entry = byCriterion.get(criterion.id);
|
|
523
|
+
const byModel = entry.byModel;
|
|
524
|
+
if (!byModel.has(run.slug)) byModel.set(run.slug, { passed: 0, failed: 0, blocked: 0, error: 0, total: 0 });
|
|
525
|
+
const cell = byModel.get(run.slug);
|
|
526
|
+
cell.total += 1;
|
|
527
|
+
if (criterion.status === "passed") cell.passed += 1;
|
|
528
|
+
else if (criterion.status === "blocked") cell.blocked += 1;
|
|
529
|
+
else if (criterion.status === "error") cell.error += 1;
|
|
530
|
+
else cell.failed += 1;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
models,
|
|
535
|
+
criteria: [...byCriterion.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([id, entry]) => ({
|
|
536
|
+
id,
|
|
537
|
+
title: entry.title,
|
|
538
|
+
cells: Object.fromEntries(models.map((model) => [model, entry.byModel.get(model) || null])),
|
|
539
|
+
})),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
438
543
|
function sum(arr, fn) {
|
|
439
544
|
return arr.reduce((a, x) => a + (Number(fn(x)) || 0), 0);
|
|
440
545
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const TRANSIENT = new Set([".git", "node_modules", "dist", "build", "coverage", "__pycache__", ".pytest_cache"]);
|
|
6
|
+
|
|
7
|
+
function walk(root, current = root, entries = []) {
|
|
8
|
+
for (const item of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
9
|
+
if (TRANSIENT.has(item.name)) continue;
|
|
10
|
+
const absolute = path.join(current, item.name);
|
|
11
|
+
const relative = path.relative(root, absolute).split(path.sep).join("/");
|
|
12
|
+
if (item.isDirectory()) walk(root, absolute, entries);
|
|
13
|
+
else if (item.isFile() && !item.isSymbolicLink()) entries.push({ relative, bytes: fs.readFileSync(absolute) });
|
|
14
|
+
}
|
|
15
|
+
return entries;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function candidateTreeHash(root) {
|
|
19
|
+
const hash = crypto.createHash("sha256");
|
|
20
|
+
for (const entry of walk(path.resolve(root))) {
|
|
21
|
+
hash.update(entry.relative).update("\0").update(entry.bytes).update("\0");
|
|
22
|
+
}
|
|
23
|
+
return hash.digest("hex");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function finiteNumber(value) {
|
|
27
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateVerifierResult(value, expected = {}) {
|
|
31
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { ok: false, error: "verifier result must be an object" };
|
|
32
|
+
if (value.version !== 1) return { ok: false, error: "unsupported verifier result version" };
|
|
33
|
+
if (!["passed", "failed", "blocked", "error"].includes(value.status)) return { ok: false, error: "invalid verifier status" };
|
|
34
|
+
if (!Array.isArray(value.criteria)) return { ok: false, error: "verifier criteria must be an array" };
|
|
35
|
+
const score = value.score;
|
|
36
|
+
if (!score || !finiteNumber(score.points) || !finiteNumber(score.maxPoints) || !finiteNumber(score.percentage)) {
|
|
37
|
+
return { ok: false, error: "verifier score must contain finite numbers" };
|
|
38
|
+
}
|
|
39
|
+
if (score.maxPoints <= 0 || score.points < 0 || score.points > score.maxPoints || score.percentage < 0 || score.percentage > 100) {
|
|
40
|
+
return { ok: false, error: "verifier score is outside its valid range" };
|
|
41
|
+
}
|
|
42
|
+
if (Math.abs(score.percentage - (score.points / score.maxPoints) * 100) > 0.01) return { ok: false, error: "verifier score arithmetic is inconsistent" };
|
|
43
|
+
if (typeof score.hardFailPassed !== "boolean" || !Array.isArray(score.hardFailCriteria)) return { ok: false, error: "verifier hard-fail fields are invalid" };
|
|
44
|
+
if (value.task && (value.task.id !== expected.taskId || value.task.version !== expected.taskVersion)) return { ok: false, error: "verifier task identity mismatch" };
|
|
45
|
+
if (value.candidateSha256 && value.candidateSha256 !== expected.candidateSha256) return { ok: false, error: "verifier candidate identity mismatch" };
|
|
46
|
+
return { ok: true, result: value };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function mergeRunResult({ task = null, cell, generation, verification, artifacts }) {
|
|
50
|
+
return {
|
|
51
|
+
schemaVersion: 1,
|
|
52
|
+
task,
|
|
53
|
+
cell,
|
|
54
|
+
generation,
|
|
55
|
+
verification,
|
|
56
|
+
artifacts,
|
|
57
|
+
};
|
|
58
|
+
}
|