@hackerrank/astra-cli 0.1.5 → 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.
@@ -0,0 +1,154 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { matrixCells } from "./project.js";
5
+ import { runCell as defaultRunCell, writeMetrics } from "./bench.js";
6
+ import { candidateTreeHash, mergeRunResult } from "./result-contract.js";
7
+ import { loadLedger, claimCell, selectWork, transitionCell } from "./ledger.js";
8
+ import { runVerifier as defaultRunVerifier } from "./verifier-runner.js";
9
+ import { newSessionId } from "./session.js";
10
+ import { refreshReport } from "./report.js";
11
+
12
+ function writeJson(file, value) {
13
+ fs.mkdirSync(path.dirname(file), { recursive: true });
14
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n");
15
+ }
16
+
17
+ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false, resume = false, resumeSessionId = null, runCellFn = defaultRunCell, verifierFn = defaultRunVerifier, onCell = () => {} } = {}) {
18
+ const root = project.outputRoot;
19
+ const ledger = loadLedger(path.join(root, "benchmark.json"));
20
+ const desired = matrixCells({ project });
21
+ const work = selectWork(ledger, desired, Date.now(), { force: resume, sessionId: resumeSessionId });
22
+ const results = [];
23
+
24
+ for (const cell of work) {
25
+ const record = claimCell(ledger, cell);
26
+ const runDir = path.join(root, record.runPath);
27
+ const candidateDir = path.join(runDir, "workspace");
28
+ const reuseFrozen = ["candidate-frozen", "verifying"].includes(record.status);
29
+ const isResume = Boolean(record.sessionId && ["generating", "interrupted"].includes(record.status));
30
+ const sessionId = record.sessionId || newSessionId();
31
+
32
+ let generation;
33
+ if (reuseFrozen) {
34
+ const generationPath = path.join(runDir, "generation.json");
35
+ try {
36
+ generation = JSON.parse(fs.readFileSync(generationPath, "utf8"));
37
+ if (!fs.existsSync(candidateDir)) throw new Error("frozen candidate workspace is missing");
38
+ const actualHash = candidateTreeHash(candidateDir);
39
+ if (record.candidateSha256 && actualHash !== record.candidateSha256) {
40
+ throw new Error("frozen candidate workspace hash does not match the ledger");
41
+ }
42
+ } catch (error) {
43
+ const failure = { status: "error", failureOwner: "infrastructure", error: String(error?.message || error) };
44
+ transitionCell(ledger, cell.cellKey, "failed", { failureOwner: "infrastructure", error: failure.error });
45
+ results.push({ cell, generation: { status: "error" }, verification: failure });
46
+ continue;
47
+ }
48
+ onCell({ phase: "candidate-frozen", cell, record });
49
+ } else {
50
+ transitionCell(ledger, cell.cellKey, "generating", { sessionId, leaseAt: new Date().toISOString() });
51
+ onCell({ phase: "generating", cell, record });
52
+ try {
53
+ generation = await runCellFn({
54
+ model: cell.model,
55
+ reasoning: cell.reasoning,
56
+ apiKey,
57
+ baseUrl,
58
+ task: project.taskText,
59
+ taskPath: project.workspace,
60
+ projectMetadata: project.metadata,
61
+ root,
62
+ runDir,
63
+ sessionId,
64
+ resume: isResume,
65
+ cell,
66
+ steps: project.bench.steps,
67
+ wall: project.bench.wall,
68
+ timeout: project.bench.timeout,
69
+ });
70
+ } catch (error) {
71
+ const failure = { status: "error", failureOwner: "infrastructure", error: String(error?.message || error) };
72
+ transitionCell(ledger, cell.cellKey, "failed", { failureOwner: "infrastructure", error: failure.error });
73
+ results.push({ cell, generation: { status: "error" }, verification: failure });
74
+ continue;
75
+ }
76
+
77
+ const candidateSha256 = candidateTreeHash(candidateDir);
78
+ writeJson(path.join(runDir, "generation.json"), generation);
79
+ transitionCell(ledger, cell.cellKey, "candidate-frozen", { candidateSha256, sessionId });
80
+ }
81
+
82
+ const candidateSha256 = record.candidateSha256 || candidateTreeHash(candidateDir);
83
+
84
+ let verification = {
85
+ status: "not_configured",
86
+ failureOwner: null,
87
+ score: null,
88
+ solvedScore: "NA",
89
+ reason: "task verifier is not present or not configured",
90
+ };
91
+ if (project.verification && generation.status === "completed") {
92
+ transitionCell(ledger, cell.cellKey, "verifying", { candidateSha256 });
93
+ try {
94
+ verification = await verifierFn({
95
+ project,
96
+ cell,
97
+ runDir,
98
+ candidateDir,
99
+ config: project.verification,
100
+ expected: { taskId: project.id, taskVersion: project.version, candidateSha256 },
101
+ });
102
+ } catch (error) {
103
+ verification = {
104
+ status: "error",
105
+ failureOwner: "infrastructure",
106
+ score: null,
107
+ solvedScore: "NA",
108
+ error: String(error?.message || error),
109
+ };
110
+ }
111
+ if (verification.score && typeof verification.score.percentage === "number") {
112
+ verification.solvedScore = verification.score.percentage;
113
+ }
114
+ writeJson(path.join(runDir, "verifier.json"), verification);
115
+ } else if (generation.status !== "completed") {
116
+ verification = {
117
+ status: "not_run",
118
+ failureOwner: "candidate",
119
+ score: null,
120
+ solvedScore: "NA",
121
+ reason: "generation did not submit a candidate; verification was skipped",
122
+ };
123
+ }
124
+
125
+ const merged = mergeRunResult({
126
+ task: { id: project.id, version: project.version, type: project.type || "brownfield" },
127
+ cell,
128
+ generation: { ...generation, candidateSha256 },
129
+ verification,
130
+ artifacts: { runPath: record.runPath, candidateSha256 },
131
+ });
132
+ writeMetrics({
133
+ runDir,
134
+ root,
135
+ metrics: {
136
+ ...generation,
137
+ model: cell.model,
138
+ reasoning: cell.reasoning,
139
+ verification_status: verification.status,
140
+ verification_score: verification.score?.percentage ?? "",
141
+ solved_score: verification.solvedScore ?? "NA",
142
+ verification_hard_pass: verification.score?.hardFailPassed ?? "",
143
+ failure_owner: verification.failureOwner ?? "",
144
+ },
145
+ });
146
+ writeJson(path.join(runDir, "result.json"), merged);
147
+ transitionCell(ledger, cell.cellKey, "completed", { candidateSha256, resultPath: path.join(record.runPath, "result.json"), failureOwner: verification.failureOwner });
148
+ results.push(merged);
149
+ try { refreshReport(root); } catch { /* report regeneration is best-effort per cell */ }
150
+ onCell({ phase: "completed", cell, record, result: merged });
151
+ }
152
+ try { refreshReport(root); } catch { /* report regeneration is best-effort */ }
153
+ return results;
154
+ }
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
  })();