@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/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(4,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; }
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>Performance <span class="sub">run volume and reliability by model</span></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>Efficiency <span class="sub">spend and token composition</span></h2>
80
- <div class="grid2">
81
- <div class="panel">
82
- <h3>Cost vs. average duration</h3>
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>Token mix <span class="sub">prompt, completion, reasoning, and cached tokens</span></h2>
96
+ <h2>Run details <span class="sub">generation and benchmarking status for each attempt</span></h2>
94
97
  <div class="panel">
95
- <div id="chart-tokens"></div>
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>generated by <span class="mono">astra bench --report</span> · schema <span id="schema"></span></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: "model", label: "Model", render: function (r) { return esc(r.model); } },
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 || !fModel || !fTask || !fOutcome) return;
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: "task_id", label: "Task", render: function (r) { return esc(r.task_id); } },
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
+ }