@hackerrank/astra-cli 0.1.30 → 0.1.32
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/package.json +1 -1
- package/src/bench.js +0 -5
- package/src/ledger.js +2 -2
- package/src/project-bench.js +5 -54
- package/src/project.js +0 -39
- package/src/repl.js +2 -2
- package/src/report.html +2 -104
- package/src/report.js +4 -119
- package/src/result-contract.js +1 -21
- package/src/verifier-runner.js +0 -222
package/package.json
CHANGED
package/src/bench.js
CHANGED
package/src/ledger.js
CHANGED
|
@@ -90,7 +90,7 @@ export function transitionCell(ledger, cellKey, status, patch = {}) {
|
|
|
90
90
|
const cell = ledger.cells[cellKey] || localCell;
|
|
91
91
|
if (!cell) throw new Error(`unknown benchmark cell: ${cellKey}`);
|
|
92
92
|
const next = { ...cell, ...patch, status, updatedAt: new Date().toISOString() };
|
|
93
|
-
if (["generating"
|
|
93
|
+
if (["generating"].includes(status) && patch.leaseAt === undefined) next.leaseAt = next.updatedAt;
|
|
94
94
|
ledger.cells[cellKey] = next;
|
|
95
95
|
atomicWrite(ledger.path, { version: LEDGER_VERSION, cells: ledger.cells });
|
|
96
96
|
return next;
|
|
@@ -103,7 +103,7 @@ export function selectWork(ledger, desiredCells, now = Date.now(), { force = fal
|
|
|
103
103
|
if (!existing) return true;
|
|
104
104
|
if (["completed", "failed"].includes(existing.status)) return false;
|
|
105
105
|
if (sessionId && existing.sessionId !== sessionId) return false;
|
|
106
|
-
if (["generating"
|
|
106
|
+
if (["generating"].includes(existing.status)) {
|
|
107
107
|
return force || !existing.leaseAt || now - Date.parse(existing.leaseAt) > STALE_AFTER_MS;
|
|
108
108
|
}
|
|
109
109
|
return !sessionId || existing.sessionId === sessionId;
|
package/src/project-bench.js
CHANGED
|
@@ -5,7 +5,6 @@ import { matrixCells } from "./project.js";
|
|
|
5
5
|
import { runCell as defaultRunCell, writeMetrics } from "./bench.js";
|
|
6
6
|
import { candidateTreeHash, mergeRunResult } from "./result-contract.js";
|
|
7
7
|
import { loadLedger, claimCell, selectWork, transitionCell } from "./ledger.js";
|
|
8
|
-
import { runVerifier as defaultRunVerifier } from "./verifier-runner.js";
|
|
9
8
|
import { newSessionId } from "./session.js";
|
|
10
9
|
import { refreshReport } from "./report.js";
|
|
11
10
|
|
|
@@ -14,7 +13,7 @@ function writeJson(file, value) {
|
|
|
14
13
|
fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n");
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
export async function runProjectBench({ project, apiKey, baseUrl, quiet = false, resume = false, resumeSessionId = null, runCellFn = defaultRunCell,
|
|
16
|
+
export async function runProjectBench({ project, apiKey, baseUrl, quiet = false, resume = false, resumeSessionId = null, runCellFn = defaultRunCell, onCell = () => {} } = {}) {
|
|
18
17
|
const root = project.outputRoot;
|
|
19
18
|
const ledger = loadLedger(path.join(root, "benchmark.json"));
|
|
20
19
|
const desired = matrixCells({ project });
|
|
@@ -25,7 +24,7 @@ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false,
|
|
|
25
24
|
const record = claimCell(ledger, cell);
|
|
26
25
|
const runDir = path.join(root, record.runPath);
|
|
27
26
|
const candidateDir = path.join(runDir, "workspace");
|
|
28
|
-
const reuseFrozen = ["candidate-frozen"
|
|
27
|
+
const reuseFrozen = ["candidate-frozen"].includes(record.status);
|
|
29
28
|
const isResume = Boolean(record.sessionId && ["generating", "interrupted"].includes(record.status));
|
|
30
29
|
const sessionId = record.sessionId || newSessionId();
|
|
31
30
|
|
|
@@ -42,7 +41,7 @@ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false,
|
|
|
42
41
|
} catch (error) {
|
|
43
42
|
const failure = { status: "error", failureOwner: "infrastructure", error: String(error?.message || error) };
|
|
44
43
|
transitionCell(ledger, cell.cellKey, "failed", { failureOwner: "infrastructure", error: failure.error });
|
|
45
|
-
results.push({ cell, generation: { status: "error" }
|
|
44
|
+
results.push({ cell, generation: { status: "error" } });
|
|
46
45
|
continue;
|
|
47
46
|
}
|
|
48
47
|
onCell({ phase: "candidate-frozen", cell, record });
|
|
@@ -70,7 +69,7 @@ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false,
|
|
|
70
69
|
} catch (error) {
|
|
71
70
|
const failure = { status: "error", failureOwner: "infrastructure", error: String(error?.message || error) };
|
|
72
71
|
transitionCell(ledger, cell.cellKey, "failed", { failureOwner: "infrastructure", error: failure.error });
|
|
73
|
-
results.push({ cell, generation: { status: "error" }
|
|
72
|
+
results.push({ cell, generation: { status: "error" } });
|
|
74
73
|
continue;
|
|
75
74
|
}
|
|
76
75
|
|
|
@@ -81,53 +80,10 @@ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false,
|
|
|
81
80
|
|
|
82
81
|
const candidateSha256 = record.candidateSha256 || candidateTreeHash(candidateDir);
|
|
83
82
|
|
|
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
|
-
gateway: { apiKey, baseUrl },
|
|
101
|
-
expected: { taskId: project.id, taskVersion: project.version, candidateSha256 },
|
|
102
|
-
});
|
|
103
|
-
} catch (error) {
|
|
104
|
-
verification = {
|
|
105
|
-
status: "error",
|
|
106
|
-
failureOwner: "infrastructure",
|
|
107
|
-
score: null,
|
|
108
|
-
solvedScore: "NA",
|
|
109
|
-
error: String(error?.message || error),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
if (verification.score && typeof verification.score.percentage === "number") {
|
|
113
|
-
verification.solvedScore = verification.score.percentage;
|
|
114
|
-
}
|
|
115
|
-
writeJson(path.join(runDir, "verifier.json"), verification);
|
|
116
|
-
} else if (generation.status !== "completed") {
|
|
117
|
-
verification = {
|
|
118
|
-
status: "not_run",
|
|
119
|
-
failureOwner: "candidate",
|
|
120
|
-
score: null,
|
|
121
|
-
solvedScore: "NA",
|
|
122
|
-
reason: "generation did not submit a candidate; verification was skipped",
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
83
|
const merged = mergeRunResult({
|
|
127
84
|
task: { id: project.id, version: project.version, type: project.type || "brownfield" },
|
|
128
85
|
cell,
|
|
129
86
|
generation: { ...generation, candidateSha256 },
|
|
130
|
-
verification,
|
|
131
87
|
artifacts: { runPath: record.runPath, candidateSha256 },
|
|
132
88
|
});
|
|
133
89
|
writeMetrics({
|
|
@@ -137,15 +93,10 @@ export async function runProjectBench({ project, apiKey, baseUrl, quiet = false,
|
|
|
137
93
|
...generation,
|
|
138
94
|
model: cell.model,
|
|
139
95
|
reasoning: cell.reasoning,
|
|
140
|
-
verification_status: verification.status,
|
|
141
|
-
verification_score: verification.score?.percentage ?? "",
|
|
142
|
-
solved_score: verification.solvedScore ?? "NA",
|
|
143
|
-
verification_hard_pass: verification.score?.hardFailPassed ?? "",
|
|
144
|
-
failure_owner: verification.failureOwner ?? "",
|
|
145
96
|
},
|
|
146
97
|
});
|
|
147
98
|
writeJson(path.join(runDir, "result.json"), merged);
|
|
148
|
-
transitionCell(ledger, cell.cellKey, "completed", { candidateSha256, resultPath: path.join(record.runPath, "result.json")
|
|
99
|
+
transitionCell(ledger, cell.cellKey, "completed", { candidateSha256, resultPath: path.join(record.runPath, "result.json") });
|
|
149
100
|
results.push(merged);
|
|
150
101
|
try { refreshReport(root); } catch { /* report regeneration is best-effort per cell */ }
|
|
151
102
|
onCell({ phase: "completed", cell, record, result: merged });
|
package/src/project.js
CHANGED
|
@@ -19,7 +19,6 @@ const DEFAULT_BENCH = {
|
|
|
19
19
|
command_timeout_seconds: 60,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const DEFAULT_VERIFICATION = null;
|
|
23
22
|
const TASK_TYPES = new Set(["brownfield", "greenfield"]);
|
|
24
23
|
|
|
25
24
|
function requireObject(value, name) {
|
|
@@ -65,25 +64,6 @@ function repeatOverrides(value) {
|
|
|
65
64
|
return result;
|
|
66
65
|
}
|
|
67
66
|
|
|
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
|
-
evaluatorModel: verification.evaluator_model === undefined ? null : requireString(verification.evaluator_model, "verification.evaluator_model"),
|
|
79
|
-
evaluatorReasoning: verification.evaluator_reasoning === undefined ? null : requireString(verification.evaluator_reasoning, "verification.evaluator_reasoning"),
|
|
80
|
-
// Zero means no verifier deadline. Tasks with agentic verification choose
|
|
81
|
-
// completion over an arbitrary wall-clock cutoff.
|
|
82
|
-
timeoutSeconds: positiveInteger(verification.timeout_seconds, "verification.timeout_seconds", 0, { allowZero: true }),
|
|
83
|
-
report: verification.report === undefined ? "verifier-report.json" : requireString(verification.report, "verification.report"),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
67
|
function taskType(project) {
|
|
88
68
|
const value = project.type ?? project.profile ?? "brownfield";
|
|
89
69
|
if (typeof value !== "string" || !TASK_TYPES.has(value)) {
|
|
@@ -157,7 +137,6 @@ export function loadProject(projectPath) {
|
|
|
157
137
|
|
|
158
138
|
const project = requireObject(config.project, "project");
|
|
159
139
|
const bench = requireObject(config.bench ?? {}, "bench");
|
|
160
|
-
let verification = verificationConfig(config.verification);
|
|
161
140
|
const output = requireObject(config.output ?? {}, "output");
|
|
162
141
|
const templates = requireObject(config.templates ?? {}, "templates");
|
|
163
142
|
const extensionConfig = requireObject(config.extensions ?? {}, "extensions");
|
|
@@ -181,23 +160,6 @@ export function loadProject(projectPath) {
|
|
|
181
160
|
.filter(Boolean)
|
|
182
161
|
.join("\n\n");
|
|
183
162
|
|
|
184
|
-
// Verifiers are task-owned. A conventional runner is enough metadata for
|
|
185
|
-
// Astra to invoke it; all domain behavior remains under verifier/.
|
|
186
|
-
const conventionalVerifier = path.join(root, "verifier", "run_verifier.py");
|
|
187
|
-
if (!verification && fs.existsSync(conventionalVerifier)) {
|
|
188
|
-
verification = {
|
|
189
|
-
command: `python3 verifier/run_verifier.py --report "$ASTRA_VERIFIER_REPORT"`,
|
|
190
|
-
startCommand: null,
|
|
191
|
-
stopCommand: null,
|
|
192
|
-
baseUrl: null,
|
|
193
|
-
readinessUrl: null,
|
|
194
|
-
tokenEnv: null,
|
|
195
|
-
timeoutSeconds: 3600,
|
|
196
|
-
report: "verifier-report.json",
|
|
197
|
-
discovered: true,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
163
|
const baselineSha256 = provenance.baseline_sha256;
|
|
202
164
|
if (baselineSha256 !== undefined && (typeof baselineSha256 !== "string" || !/^[a-f0-9]{64}$/.test(baselineSha256))) {
|
|
203
165
|
throw new ProjectConfigError("provenance.baseline_sha256 must be a lowercase SHA-256 hash");
|
|
@@ -237,7 +199,6 @@ export function loadProject(projectPath) {
|
|
|
237
199
|
wall: positiveInteger(bench.wall_seconds, "bench.wall_seconds", DEFAULT_BENCH.wall_seconds, { allowZero: true }),
|
|
238
200
|
timeout: positiveInteger(bench.command_timeout_seconds, "bench.command_timeout_seconds", DEFAULT_BENCH.command_timeout_seconds),
|
|
239
201
|
},
|
|
240
|
-
verification,
|
|
241
202
|
};
|
|
242
203
|
}
|
|
243
204
|
|
package/src/repl.js
CHANGED
|
@@ -265,13 +265,13 @@ export function computeVisualLines(buf, promptPrefix, cols = 80) {
|
|
|
265
265
|
/** Check if an escape sequence or key code is a newline-insertion shortcut (Shift+Enter, Option+Enter, etc.). */
|
|
266
266
|
export function isNewlineKey(s, pendingEsc = false) {
|
|
267
267
|
if (!s || typeof s !== "string") return false;
|
|
268
|
+
// Shift+Enter, Ctrl+Enter, Alt+Enter in CSI u / modifyOtherKeys forms
|
|
268
269
|
if (/^\x1b\[(?:13|10);[2-8]u$/.test(s)) return true;
|
|
269
270
|
if (/^\x1b\[27;[2-8];(?:13|10)~$/.test(s)) return true;
|
|
270
271
|
if (/^\x1b\[(?:13|10);[2-8]~$/.test(s)) return true;
|
|
271
272
|
if (s === "\x1bOM") return true;
|
|
273
|
+
// Alt/Option+Enter on macOS Terminal / iTerm2
|
|
272
274
|
if (s === "\x1b\r" || s === "\x1b\n" || s === "\x1b\x0d") return true;
|
|
273
|
-
if (pendingEsc && (s === "\r" || s === "\n" || s === "\x0d" || s === "\x0a")) return true;
|
|
274
|
-
if (s === "\x0a" || s === "\n") return true;
|
|
275
275
|
return false;
|
|
276
276
|
}
|
|
277
277
|
|
package/src/report.html
CHANGED
|
@@ -74,10 +74,6 @@
|
|
|
74
74
|
|
|
75
75
|
<div class="kpis" id="kpis"></div>
|
|
76
76
|
|
|
77
|
-
<section id="section-score-distribution">
|
|
78
|
-
<h2>Score distribution</h2>
|
|
79
|
-
<div class="score-chart-scroll"><div id="chart-score-distribution" role="img" aria-label="Verified score by model and reasoning configuration"></div></div>
|
|
80
|
-
</section>
|
|
81
77
|
|
|
82
78
|
<section>
|
|
83
79
|
<h2>Run outcomes</h2>
|
|
@@ -92,13 +88,6 @@
|
|
|
92
88
|
</div>
|
|
93
89
|
</section>
|
|
94
90
|
|
|
95
|
-
<section>
|
|
96
|
-
<h2>Test-case results</h2>
|
|
97
|
-
<div class="toolbar"><label for="verifier-model-select">Model</label><select id="verifier-model-select"></select></div>
|
|
98
|
-
<div class="panel">
|
|
99
|
-
<table id="tbl-verifier-matrix"></table>
|
|
100
|
-
</div>
|
|
101
|
-
</section>
|
|
102
91
|
|
|
103
92
|
<section id="section-series">
|
|
104
93
|
<h2>Trajectory</h2>
|
|
@@ -197,8 +186,7 @@
|
|
|
197
186
|
var k = DATA.kpis || {};
|
|
198
187
|
var cards = [
|
|
199
188
|
["Solutions", fmt(k.models)],
|
|
200
|
-
|
|
201
|
-
["Average duration", fmtDuration(k.runs ? k.elapsed_seconds / k.runs : 0)],
|
|
189
|
+
["Average duration", fmtDuration(k.runs ? k.elapsed_seconds / k.runs : 0)],
|
|
202
190
|
["Total cost", fmtUsd(k.cost_usd) + (k.cost_source === "estimated" ? "~" : "")],
|
|
203
191
|
["Tokens used", fmtTokens(k.tokens)],
|
|
204
192
|
];
|
|
@@ -208,37 +196,6 @@
|
|
|
208
196
|
});
|
|
209
197
|
})();
|
|
210
198
|
|
|
211
|
-
// ---------------------------------------------------------------- score distribution
|
|
212
|
-
(function renderScoreDistribution() {
|
|
213
|
-
var host = document.getElementById("chart-score-distribution");
|
|
214
|
-
if (!host) return;
|
|
215
|
-
var rows = (DATA.leaderboard || []).map(function (row) {
|
|
216
|
-
var verified = typeof row.average_score === "number";
|
|
217
|
-
return { slug: row.slug, score: verified ? Math.max(0, Math.min(100, row.average_score)) : null, verified: verified };
|
|
218
|
-
}).sort(function (a, b) {
|
|
219
|
-
if (a.verified !== b.verified) return a.verified ? -1 : 1;
|
|
220
|
-
return (b.score || 0) - (a.score || 0) || a.slug.localeCompare(b.slug);
|
|
221
|
-
});
|
|
222
|
-
if (!rows.length) {
|
|
223
|
-
document.getElementById("section-score-distribution").style.display = "none";
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
host.className = "score-chart";
|
|
227
|
-
host.style.minWidth = Math.max(760, rows.length * 45) + "px";
|
|
228
|
-
[["top", "100"], ["mid", "50"], ["zero", "0"]].forEach(function (axis) {
|
|
229
|
-
host.appendChild(el("span", { class: "score-axis " + axis[0] }, axis[1]));
|
|
230
|
-
});
|
|
231
|
-
rows.forEach(function (row) {
|
|
232
|
-
var item = el("div", { class: "score-item" });
|
|
233
|
-
var bar = el("div", { class: "score-bar", title: row.slug + ": " + (row.verified ? fmt1(row.score) + "%" : "n/a") });
|
|
234
|
-
if (row.verified) bar.style.height = Math.max(2, row.score) + "%";
|
|
235
|
-
bar.appendChild(el("span", { class: "score-value" }, row.verified ? fmt1(row.score) + "%" : "n/a"));
|
|
236
|
-
item.appendChild(bar);
|
|
237
|
-
item.appendChild(el("span", { class: "score-label" }, esc(row.slug)));
|
|
238
|
-
host.appendChild(item);
|
|
239
|
-
});
|
|
240
|
-
})();
|
|
241
|
-
|
|
242
199
|
// ---------------------------------------------------------------- sortable table helper
|
|
243
200
|
function sortableTable(container, columns, rows, opts) {
|
|
244
201
|
opts = opts || {};
|
|
@@ -333,7 +290,6 @@
|
|
|
333
290
|
var columns = [
|
|
334
291
|
{ key: "slug", label: "Model · reasoning", render: function (r) { return esc(r.slug); } },
|
|
335
292
|
{ key: "runs", label: "Runs", render: function (r) { return fmt(r.runs); } },
|
|
336
|
-
{ key: "average_score", label: "Score", render: function (r) { return r.average_score == null ? '<span class="n-a">n/a</span>' : fmt1(r.average_score) + "%"; } },
|
|
337
293
|
{ key: "sum_steps", label: "Total steps", render: function (r) { return fmt(r.sum_steps); } },
|
|
338
294
|
{ key: "sum_elapsed_seconds", label: "Time taken", render: function (r) { return fmtDuration(r.sum_elapsed_seconds); } },
|
|
339
295
|
{ key: "sum_tokens", label: "Tokens", render: function (r) { return fmtTokens(r.sum_tokens); } },
|
|
@@ -349,70 +305,12 @@
|
|
|
349
305
|
var selected = Array.prototype.slice.call(options.querySelectorAll("input:checked")).map(function (o) { return o.value; });
|
|
350
306
|
var rows = selected.length ? (DATA.leaderboard || []).filter(function (r) { return selected.indexOf(r.slug) !== -1; }) : DATA.leaderboard || [];
|
|
351
307
|
filterLabel.textContent = selected.length ? selected.length + " model" + (selected.length === 1 ? "" : "s") + " selected" : "All models";
|
|
352
|
-
sortableTable(container, columns, rows, { defaultKey: "
|
|
308
|
+
sortableTable(container, columns, rows, { defaultKey: "runs" });
|
|
353
309
|
}
|
|
354
310
|
options.addEventListener("change", draw);
|
|
355
311
|
draw();
|
|
356
312
|
})();
|
|
357
313
|
|
|
358
|
-
// ---------------------------------------------------------------- verifier test-case matrix
|
|
359
|
-
(function () {
|
|
360
|
-
var container = document.getElementById("tbl-verifier-matrix");
|
|
361
|
-
var selector = document.getElementById("verifier-model-select");
|
|
362
|
-
if (!container) return;
|
|
363
|
-
var matrix = DATA.verifier_matrix || { models: [], criteria: [] };
|
|
364
|
-
// Prefer a model with actual verifier evidence. Alphabetical-first often
|
|
365
|
-
// selects an unscored gateway failure and makes a healthy report appear
|
|
366
|
-
// empty on first open.
|
|
367
|
-
var selected = matrix.models.find(function (model) {
|
|
368
|
-
return matrix.criteria.some(function (criterion) { return criterion.cells[model]; });
|
|
369
|
-
}) || matrix.models[0] || "";
|
|
370
|
-
var GROUP_TITLES = {
|
|
371
|
-
"scim.discovery-auth": "Discovery & authentication",
|
|
372
|
-
"scim.read-existing": "Reading existing resources",
|
|
373
|
-
"scim.create-user": "User creation",
|
|
374
|
-
"scim.user-lifecycle": "User lifecycle",
|
|
375
|
-
"scim.group-lifecycle": "Group lifecycle",
|
|
376
|
-
"scim.membership": "Group membership",
|
|
377
|
-
"scim.errors-atomicity": "Errors & atomicity"
|
|
378
|
-
};
|
|
379
|
-
function groupTitle(parentId) {
|
|
380
|
-
if (GROUP_TITLES[parentId]) return GROUP_TITLES[parentId];
|
|
381
|
-
return String(parentId || "Other checks")
|
|
382
|
-
.replace(/[._-]+/g, " ")
|
|
383
|
-
.replace(/\b\w/g, function (letter) { return letter.toUpperCase(); });
|
|
384
|
-
}
|
|
385
|
-
function draw() {
|
|
386
|
-
var groups = {};
|
|
387
|
-
matrix.criteria.forEach(function (criterion) {
|
|
388
|
-
var parentId = criterion.parentId || criterion.id;
|
|
389
|
-
(groups[parentId] || (groups[parentId] = [])).push(criterion);
|
|
390
|
-
});
|
|
391
|
-
var bodies = Object.keys(groups).sort().map(function (parentId) {
|
|
392
|
-
var rows = groups[parentId].map(function (criterion) {
|
|
393
|
-
var cell = criterion.cells[selected];
|
|
394
|
-
if (!cell) return '<tr><td>' + esc(criterion.title || criterion.id) + '</td><td class="n-a">NA</td><td><span class="result-badge unscored">Unscored</span></td></tr>';
|
|
395
|
-
var max = cell.maxPoints || 0, earned = cell.earnedPoints || 0;
|
|
396
|
-
var passed = cell.passed === cell.total;
|
|
397
|
-
return '<tr><td>' + esc(criterion.title || criterion.id) + '</td><td>' + fmt1(earned) + ' / ' + fmt1(max) + '</td><td><span class="result-badge ' + (passed ? 'passed' : 'failed') + '">' + (passed ? 'Passed' : 'Failed') + '</span></td></tr>';
|
|
398
|
-
}).join('');
|
|
399
|
-
return '<tbody><tr class="criterion-group"><th colspan="3">' + esc(groupTitle(parentId)) + '</th></tr>' + rows + '</tbody>';
|
|
400
|
-
}).join('');
|
|
401
|
-
container.innerHTML = '<thead><tr><th>Test case</th><th>Points</th><th>Result</th></tr></thead>' + bodies;
|
|
402
|
-
}
|
|
403
|
-
matrix.models.forEach(function (model) {
|
|
404
|
-
var option = el("option", { value: model }, esc(model));
|
|
405
|
-
if (model === selected) option.selected = true;
|
|
406
|
-
selector.appendChild(option);
|
|
407
|
-
});
|
|
408
|
-
selector.addEventListener("change", function () { selected = selector.value; draw(); });
|
|
409
|
-
if (!matrix.criteria.length) {
|
|
410
|
-
container.innerHTML = '<tbody><tr><td class="muted">No verifier criteria have produced results yet.</td></tr></tbody>';
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
draw();
|
|
414
|
-
})();
|
|
415
|
-
|
|
416
314
|
// ---------------------------------------------------------------- token mix stacked bar
|
|
417
315
|
(function () {
|
|
418
316
|
var host = document.getElementById("chart-tokens");
|
package/src/report.js
CHANGED
|
@@ -102,9 +102,6 @@ function latestTaskRuns(runs) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
export function buildSummary(runs) {
|
|
105
|
-
runs = runs.map((run) => ({ ...run, criteria: flattenCriteria(run.criteria) }));
|
|
106
|
-
const verifierCriteria = summarizeCriteria(runs);
|
|
107
|
-
const verifierMatrix = buildVerifierMatrix(runs);
|
|
108
105
|
const leaderboard = groupBy(runs, (r) => r.slug).map(([slug, rs]) => {
|
|
109
106
|
const costRuns = rs.filter((r) => r.cost_source);
|
|
110
107
|
const sources = new Set(costRuns.map((r) => r.cost_source));
|
|
@@ -114,7 +111,6 @@ export function buildSummary(runs) {
|
|
|
114
111
|
outcomes[k] = (outcomes[k] || 0) + 1;
|
|
115
112
|
}
|
|
116
113
|
const completed = rs.filter((r) => r.completed).length;
|
|
117
|
-
const verified = rs.filter((r) => r.verification?.score);
|
|
118
114
|
return {
|
|
119
115
|
model: rs[0].model,
|
|
120
116
|
reasoning: rs[0].reasoning,
|
|
@@ -122,11 +118,6 @@ export function buildSummary(runs) {
|
|
|
122
118
|
runs: rs.length,
|
|
123
119
|
completed,
|
|
124
120
|
completion_rate: rs.length ? completed / rs.length : 0,
|
|
125
|
-
verification_runs: verified.length,
|
|
126
|
-
verification_rate: rs.length ? verified.length / rs.length : 0,
|
|
127
|
-
average_score: verified.length ? avg(verified, (r) => r.verification.score.percentage) : null,
|
|
128
|
-
hard_passes: verified.filter((r) => r.verification.score.hardFailPassed === true).length,
|
|
129
|
-
criteria_pass_rate: criterionRate(rs),
|
|
130
121
|
outcomes,
|
|
131
122
|
avg_steps: avg(rs, (r) => r.steps),
|
|
132
123
|
sum_steps: sum(rs, (r) => r.steps),
|
|
@@ -148,18 +139,14 @@ export function buildSummary(runs) {
|
|
|
148
139
|
avg_n_retries: avg(rs, (r) => r.n_retries),
|
|
149
140
|
};
|
|
150
141
|
});
|
|
151
|
-
leaderboard.sort((a, b) =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return (b.average_score ?? 0) - (a.average_score ?? 0)
|
|
156
|
-
|| a.slug.localeCompare(b.slug);
|
|
157
|
-
});
|
|
142
|
+
leaderboard.sort((a, b) =>
|
|
143
|
+
(b.completion_rate ?? 0) - (a.completion_rate ?? 0)
|
|
144
|
+
|| a.slug.localeCompare(b.slug)
|
|
145
|
+
);
|
|
158
146
|
|
|
159
147
|
const matrix = groupBy(runs, (r) => `${r.slug}\u0000${r.task_id}`).map(([, rs]) => {
|
|
160
148
|
const completed = rs.filter((r) => r.completed).length;
|
|
161
149
|
const costRuns = rs.filter((r) => r.cost_source);
|
|
162
|
-
const verified = rs.filter((r) => r.verification?.score);
|
|
163
150
|
return {
|
|
164
151
|
model: rs[0].model,
|
|
165
152
|
reasoning: rs[0].reasoning,
|
|
@@ -169,11 +156,6 @@ export function buildSummary(runs) {
|
|
|
169
156
|
k: rs.length,
|
|
170
157
|
completed,
|
|
171
158
|
completion_rate: rs.length ? completed / rs.length : 0,
|
|
172
|
-
verification_runs: verified.length,
|
|
173
|
-
verification_rate: rs.length ? verified.length / rs.length : 0,
|
|
174
|
-
average_score: verified.length ? avg(verified, (r) => r.verification.score.percentage) : null,
|
|
175
|
-
hard_passes: verified.filter((r) => r.verification.score.hardFailPassed === true).length,
|
|
176
|
-
criteria_pass_rate: criterionRate(rs),
|
|
177
159
|
avg_steps: avg(rs, (r) => r.steps),
|
|
178
160
|
avg_tokens: avg(rs, (r) => r.tokens.total),
|
|
179
161
|
avg_elapsed_seconds: avg(rs, (r) => r.elapsed_seconds),
|
|
@@ -233,7 +215,6 @@ export function buildSummary(runs) {
|
|
|
233
215
|
|
|
234
216
|
const costRuns = runs.filter((r) => r.cost_source);
|
|
235
217
|
const completed = runs.filter((r) => r.completed).length;
|
|
236
|
-
const verified = runs.filter((r) => r.verification?.score);
|
|
237
218
|
const sources = new Set(costRuns.map((r) => r.cost_source));
|
|
238
219
|
|
|
239
220
|
return {
|
|
@@ -245,10 +226,6 @@ export function buildSummary(runs) {
|
|
|
245
226
|
runs: runs.length,
|
|
246
227
|
completed,
|
|
247
228
|
completion_rate: runs.length ? completed / runs.length : 0,
|
|
248
|
-
verification_runs: verified.length,
|
|
249
|
-
verification_rate: runs.length ? verified.length / runs.length : 0,
|
|
250
|
-
average_score: verified.length ? avg(verified, (r) => r.verification.score.percentage) : null,
|
|
251
|
-
hard_passes: verified.filter((r) => r.verification.score.hardFailPassed === true).length,
|
|
252
229
|
cost_usd: costRuns.length ? round(sum(costRuns, (r) => r.cost_usd), 6) : null,
|
|
253
230
|
cost_source: sources.size === 0 ? "unknown" : sources.size === 1 ? [...sources][0] : "mixed",
|
|
254
231
|
tokens: sum(runs, (r) => r.tokens.total),
|
|
@@ -256,8 +233,6 @@ export function buildSummary(runs) {
|
|
|
256
233
|
},
|
|
257
234
|
leaderboard,
|
|
258
235
|
matrix,
|
|
259
|
-
verifier_criteria: verifierCriteria,
|
|
260
|
-
verifier_matrix: verifierMatrix,
|
|
261
236
|
runs,
|
|
262
237
|
step_series,
|
|
263
238
|
};
|
|
@@ -347,12 +322,6 @@ function readRunDir({ rootDir, slug, runId, dir }) {
|
|
|
347
322
|
status,
|
|
348
323
|
completed: status === "completed",
|
|
349
324
|
error: null,
|
|
350
|
-
verification: result?.verification ?? null,
|
|
351
|
-
solved_score: result?.verification?.solvedScore ?? (result?.verification?.score?.percentage ?? "NA"),
|
|
352
|
-
verifier_status: result?.verification?.status ?? "not_configured",
|
|
353
|
-
verifier_error: result?.verification?.error ?? null,
|
|
354
|
-
failure_owner: result?.verification?.failureOwner ?? null,
|
|
355
|
-
criteria: flattenCriteria(result?.verification?.criteria),
|
|
356
325
|
steps: num(metricsRow?.steps ?? traj?.info?.n_steps),
|
|
357
326
|
n_calls: num(metricsRow?.n_calls ?? traj?.info?.n_calls),
|
|
358
327
|
n_commands: num(metricsRow?.n_commands),
|
|
@@ -503,90 +472,6 @@ function groupBy(arr, keyFn) {
|
|
|
503
472
|
return [...m.entries()];
|
|
504
473
|
}
|
|
505
474
|
|
|
506
|
-
function criterionRate(runs) {
|
|
507
|
-
const values = [];
|
|
508
|
-
for (const run of runs) {
|
|
509
|
-
for (const criterion of run.criteria || []) {
|
|
510
|
-
values.push(criterion.status === "passed" ? 1 : 0);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return values.length ? sum(values, (value) => value) / values.length : null;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// A task may group related assertions under one scored criterion. Benchmark
|
|
517
|
-
// reports should still expose each assertion as a named test case, while score
|
|
518
|
-
// ownership remains entirely with the task verifier.
|
|
519
|
-
function flattenCriteria(criteria) {
|
|
520
|
-
if (!Array.isArray(criteria)) return [];
|
|
521
|
-
return criteria.flatMap((criterion) => {
|
|
522
|
-
if (!Array.isArray(criterion?.checks) || criterion.checks.length === 0) {
|
|
523
|
-
return [criterion];
|
|
524
|
-
}
|
|
525
|
-
const leafWeight = Math.max(0, Number(criterion.weight) || 0) / criterion.checks.length;
|
|
526
|
-
return criterion.checks.map((check, index) => ({
|
|
527
|
-
id: `${criterion.id}.${check.name || `check-${index + 1}`}`,
|
|
528
|
-
title: check.description || check.name || criterion.title || criterion.id,
|
|
529
|
-
status: check.passed === true ? "passed" : check.status === "blocked" ? "blocked" : check.status === "error" ? "error" : "failed",
|
|
530
|
-
parentId: criterion.id,
|
|
531
|
-
weight: leafWeight,
|
|
532
|
-
}));
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function summarizeCriteria(runs) {
|
|
537
|
-
const grouped = new Map();
|
|
538
|
-
for (const run of runs) {
|
|
539
|
-
for (const criterion of run.criteria || []) {
|
|
540
|
-
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 });
|
|
541
|
-
const item = grouped.get(criterion.id);
|
|
542
|
-
item.runs += 1;
|
|
543
|
-
if (criterion.status === "passed") item.passed += 1;
|
|
544
|
-
else if (criterion.status === "blocked") item.blocked += 1;
|
|
545
|
-
else if (criterion.status === "error") item.error += 1;
|
|
546
|
-
else item.failed += 1;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
return [...grouped.values()].map((item) => ({
|
|
550
|
-
...item,
|
|
551
|
-
pass_rate: item.runs ? item.passed / item.runs : 0,
|
|
552
|
-
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function buildVerifierMatrix(runs) {
|
|
556
|
-
const models = [...new Set(runs.map((run) => run.slug))].sort();
|
|
557
|
-
const byCriterion = new Map();
|
|
558
|
-
for (const run of runs) {
|
|
559
|
-
for (const criterion of run.criteria || []) {
|
|
560
|
-
if (!byCriterion.has(criterion.id)) byCriterion.set(criterion.id, {
|
|
561
|
-
title: criterion.title || criterion.id,
|
|
562
|
-
parentId: criterion.parentId || criterion.id,
|
|
563
|
-
byModel: new Map(),
|
|
564
|
-
});
|
|
565
|
-
const entry = byCriterion.get(criterion.id);
|
|
566
|
-
const byModel = entry.byModel;
|
|
567
|
-
if (!byModel.has(run.slug)) byModel.set(run.slug, { passed: 0, failed: 0, blocked: 0, error: 0, total: 0, maxPoints: 0, earnedPoints: 0 });
|
|
568
|
-
const cell = byModel.get(run.slug);
|
|
569
|
-
cell.total += 1;
|
|
570
|
-
const weight = Math.max(0, Number(criterion.weight) || 0);
|
|
571
|
-
cell.maxPoints += weight;
|
|
572
|
-
if (criterion.status === "passed") cell.earnedPoints += weight;
|
|
573
|
-
if (criterion.status === "passed") cell.passed += 1;
|
|
574
|
-
else if (criterion.status === "blocked") cell.blocked += 1;
|
|
575
|
-
else if (criterion.status === "error") cell.error += 1;
|
|
576
|
-
else cell.failed += 1;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
return {
|
|
580
|
-
models,
|
|
581
|
-
criteria: [...byCriterion.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([id, entry]) => ({
|
|
582
|
-
id,
|
|
583
|
-
title: entry.title,
|
|
584
|
-
parentId: entry.parentId,
|
|
585
|
-
cells: Object.fromEntries(models.map((model) => [model, entry.byModel.get(model) || null])),
|
|
586
|
-
})),
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
|
|
590
475
|
function sum(arr, fn) {
|
|
591
476
|
return arr.reduce((a, x) => a + (Number(fn(x)) || 0), 0);
|
|
592
477
|
}
|
package/src/result-contract.js
CHANGED
|
@@ -27,32 +27,12 @@ function finiteNumber(value) {
|
|
|
27
27
|
return typeof value === "number" && Number.isFinite(value);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function
|
|
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 }) {
|
|
30
|
+
export function mergeRunResult({ task = null, cell, generation, artifacts }) {
|
|
50
31
|
return {
|
|
51
32
|
schemaVersion: 1,
|
|
52
33
|
task,
|
|
53
34
|
cell,
|
|
54
35
|
generation,
|
|
55
|
-
verification,
|
|
56
36
|
artifacts,
|
|
57
37
|
};
|
|
58
38
|
}
|
package/src/verifier-runner.js
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
|
|
5
|
-
import { validateVerifierResult } from "./result-contract.js";
|
|
6
|
-
|
|
7
|
-
function redactOutput(value) {
|
|
8
|
-
return String(value || "")
|
|
9
|
-
.replace(/(authorization\s*:\s*bearer\s+)[^\s,;]+/gi, "$1[REDACTED]")
|
|
10
|
-
.replace(/(--token\s+)[^\s,;]+/gi, "$1[REDACTED]")
|
|
11
|
-
.slice(-2000);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function runCommand(command, { cwd, env, timeoutSeconds }) {
|
|
15
|
-
return new Promise((resolve) => {
|
|
16
|
-
const child = spawn(command, { cwd, env, shell: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
|
|
17
|
-
let stdout = "";
|
|
18
|
-
let stderr = "";
|
|
19
|
-
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
20
|
-
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
21
|
-
let timedOut = false;
|
|
22
|
-
const limit = Number(timeoutSeconds);
|
|
23
|
-
const timer = limit > 0 ? setTimeout(() => {
|
|
24
|
-
timedOut = true;
|
|
25
|
-
stopProcessTree(child, "SIGTERM");
|
|
26
|
-
setTimeout(() => stopProcessTree(child, "SIGKILL"), 250);
|
|
27
|
-
}, limit * 1000) : null;
|
|
28
|
-
child.on("close", (code, signal) => {
|
|
29
|
-
if (timer) clearTimeout(timer);
|
|
30
|
-
resolve({ code, signal, timedOut, stdout, stderr });
|
|
31
|
-
});
|
|
32
|
-
child.on("error", (error) => {
|
|
33
|
-
if (timer) clearTimeout(timer);
|
|
34
|
-
resolve({ code: null, signal: null, timedOut: false, stdout, stderr: `${stderr}${error.message}` });
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function stopProcessTree(child, signal) {
|
|
40
|
-
try {
|
|
41
|
-
if (process.platform !== "win32" && child.pid) process.kill(-child.pid, signal);
|
|
42
|
-
else child.kill(signal);
|
|
43
|
-
} catch {
|
|
44
|
-
// The command may already have exited between the timer and the signal.
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function startProcess(command, { cwd, env }) {
|
|
49
|
-
const child = spawn(command, { cwd, env, shell: true, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
|
|
50
|
-
let output = "";
|
|
51
|
-
child.stdout.on("data", (chunk) => { output += chunk; });
|
|
52
|
-
child.stderr.on("data", (chunk) => { output += chunk; });
|
|
53
|
-
child.startupOutput = () => redactOutput(output);
|
|
54
|
-
return child;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function acquireVerifierLock(projectRoot, timeoutSeconds) {
|
|
58
|
-
const lockPath = path.join(projectRoot, ".astra", "verifier.lock");
|
|
59
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
60
|
-
const limit = Number(timeoutSeconds);
|
|
61
|
-
const deadline = limit > 0 ? Date.now() + limit * 1000 : Infinity;
|
|
62
|
-
while (Date.now() < deadline) {
|
|
63
|
-
try {
|
|
64
|
-
const descriptor = fs.openSync(lockPath, "wx");
|
|
65
|
-
fs.writeFileSync(descriptor, `${process.pid}\n`);
|
|
66
|
-
return () => {
|
|
67
|
-
try { fs.closeSync(descriptor); } catch {}
|
|
68
|
-
try { fs.rmSync(lockPath, { force: true }); } catch {}
|
|
69
|
-
};
|
|
70
|
-
} catch (error) {
|
|
71
|
-
if (error?.code !== "EEXIST") throw error;
|
|
72
|
-
// A killed benchmark worker cannot run its finally block. Reclaim only
|
|
73
|
-
// a lock whose recorded owner is no longer alive; active workers keep
|
|
74
|
-
// exclusive ownership of fixed Docker ports.
|
|
75
|
-
try {
|
|
76
|
-
const owner = Number(fs.readFileSync(lockPath, "utf8").trim());
|
|
77
|
-
process.kill(owner, 0);
|
|
78
|
-
} catch (ownerError) {
|
|
79
|
-
if (ownerError?.code === "ESRCH" || !Number.isInteger(Number(fs.readFileSync(lockPath, "utf8").trim()))) {
|
|
80
|
-
try { fs.rmSync(lockPath, { force: true }); } catch {}
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
throw new Error("timed out waiting for the task verifier lock");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function waitReady(url, timeoutSeconds, candidateProcess) {
|
|
91
|
-
const limit = Number(timeoutSeconds);
|
|
92
|
-
const deadline = limit > 0 ? Date.now() + limit * 1000 : Infinity;
|
|
93
|
-
while (Date.now() < deadline) {
|
|
94
|
-
if (candidateProcess && candidateProcess.exitCode !== null) {
|
|
95
|
-
return { ready: false, exited: true, output: candidateProcess.startupOutput?.() || "" };
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(1000) });
|
|
99
|
-
if (response.ok) return { ready: true, exited: false, output: "" };
|
|
100
|
-
} catch {
|
|
101
|
-
// Candidate is still starting.
|
|
102
|
-
}
|
|
103
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
104
|
-
}
|
|
105
|
-
return { ready: false, exited: false, output: candidateProcess?.startupOutput?.() || "" };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function runVerifier({ project, cell, runDir, candidateDir, config, expected = {}, gateway = {} }) {
|
|
109
|
-
const started = Date.now();
|
|
110
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
111
|
-
const reportPath = path.resolve(runDir, config.report || "verifier.json");
|
|
112
|
-
if (reportPath !== path.resolve(runDir) && !reportPath.startsWith(`${path.resolve(runDir)}${path.sep}`)) {
|
|
113
|
-
return { status: "error", failureOwner: "infrastructure", solvedScore: "NA", durationSeconds: 0, error: "verifier report must stay inside the run directory" };
|
|
114
|
-
}
|
|
115
|
-
// Never accept a report left by an earlier interrupted attempt.
|
|
116
|
-
try { fs.rmSync(reportPath, { force: true }); } catch (error) {
|
|
117
|
-
return { status: "error", failureOwner: "infrastructure", solvedScore: "NA", durationSeconds: 0, error: `cannot clear previous verifier report: ${error.message}` };
|
|
118
|
-
}
|
|
119
|
-
const env = {
|
|
120
|
-
PATH: process.env.PATH,
|
|
121
|
-
HOME: process.env.HOME,
|
|
122
|
-
...(process.env.DOCKER_HOST ? { DOCKER_HOST: process.env.DOCKER_HOST } : {}),
|
|
123
|
-
ASTRA_VERIFIER_REPORT: reportPath,
|
|
124
|
-
ASTRA_CANDIDATE_WORKSPACE: candidateDir,
|
|
125
|
-
ASTRA_TASK_ID: project.id,
|
|
126
|
-
ASTRA_TASK_VERSION: String(project.version),
|
|
127
|
-
ASTRA_TASK_TYPE: project.type || "brownfield",
|
|
128
|
-
ASTRA_CELL_KEY: cell.cellKey,
|
|
129
|
-
ASTRA_PROJECT_ROOT: project.root,
|
|
130
|
-
// The task verifier invokes ASTRA's evaluator bridge for dynamic discovery
|
|
131
|
-
// and source judgment. Keep this explicit and task-scoped: the candidate
|
|
132
|
-
// command environment is separately scrubbed by the verifier.
|
|
133
|
-
ASTRA_EVALUATOR_BIN: new URL("./evaluator-cli.js", import.meta.url).pathname,
|
|
134
|
-
...(gateway.apiKey ? { ASTRA_GATEWAY_API_KEY: gateway.apiKey } : {}),
|
|
135
|
-
...(gateway.baseUrl ? { ASTRA_GATEWAY_BASE_URL: gateway.baseUrl } : {}),
|
|
136
|
-
ASTRA_EVALUATOR_MODEL: config.evaluatorModel || cell.model,
|
|
137
|
-
ASTRA_EVALUATOR_REASONING: config.evaluatorReasoning || cell.reasoning || "high",
|
|
138
|
-
...(config.baseUrl ? { SCIM_BASE_URL: config.baseUrl } : {}),
|
|
139
|
-
...(config.tokenEnv && process.env[config.tokenEnv] ? { [config.tokenEnv]: process.env[config.tokenEnv] } : {}),
|
|
140
|
-
};
|
|
141
|
-
let candidateProcess = null;
|
|
142
|
-
let releaseLock = null;
|
|
143
|
-
try {
|
|
144
|
-
// Candidate Docker Compose files commonly claim fixed host ports. Serialize
|
|
145
|
-
// verification per task while allowing costly model generation to run in
|
|
146
|
-
// parallel safely.
|
|
147
|
-
releaseLock = await acquireVerifierLock(project.root, config.timeoutSeconds);
|
|
148
|
-
if (config.startCommand) {
|
|
149
|
-
candidateProcess = startProcess(config.startCommand, { cwd: candidateDir, env });
|
|
150
|
-
// A cold Docker build legitimately exceeds a minute. Give the candidate
|
|
151
|
-
// a bounded five-minute startup window, still capped by task timeout.
|
|
152
|
-
if (config.readinessUrl) {
|
|
153
|
-
const readiness = await waitReady(config.readinessUrl, config.timeoutSeconds > 0 ? Math.min(config.timeoutSeconds, 300) : 0, candidateProcess);
|
|
154
|
-
if (!readiness.ready) {
|
|
155
|
-
const durationSeconds = (Date.now() - started) / 1000;
|
|
156
|
-
const detail = readiness.output;
|
|
157
|
-
if (readiness.exited) {
|
|
158
|
-
const portCollision = /EADDRINUSE|address already in use|port is already allocated/i.test(detail);
|
|
159
|
-
return {
|
|
160
|
-
status: "error",
|
|
161
|
-
failureOwner: portCollision ? "infrastructure" : "candidate",
|
|
162
|
-
solvedScore: "NA",
|
|
163
|
-
durationSeconds,
|
|
164
|
-
error: detail
|
|
165
|
-
? `${portCollision ? "candidate startup blocked by host-port collision" : "candidate startup failed"}: ${detail}`
|
|
166
|
-
: "candidate startup process exited before readiness",
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
return {
|
|
170
|
-
status: "error",
|
|
171
|
-
failureOwner: "infrastructure",
|
|
172
|
-
solvedScore: "NA",
|
|
173
|
-
durationSeconds,
|
|
174
|
-
error: detail ? `candidate readiness timed out: ${detail}` : "candidate readiness timed out",
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
const commandResult = await runCommand(config.command, {
|
|
180
|
-
cwd: project.root,
|
|
181
|
-
env,
|
|
182
|
-
timeoutSeconds: config.timeoutSeconds,
|
|
183
|
-
});
|
|
184
|
-
const durationSeconds = (Date.now() - started) / 1000;
|
|
185
|
-
if (commandResult.timedOut) return { status: "error", failureOwner: "infrastructure", solvedScore: "NA", durationSeconds, error: "verifier timed out" };
|
|
186
|
-
if (!fs.existsSync(reportPath)) {
|
|
187
|
-
const detail = redactOutput(commandResult.stderr);
|
|
188
|
-
return {
|
|
189
|
-
status: "error",
|
|
190
|
-
failureOwner: "infrastructure",
|
|
191
|
-
solvedScore: "NA",
|
|
192
|
-
durationSeconds,
|
|
193
|
-
error: detail ? `verifier report was not produced: ${detail}` : "verifier report was not produced",
|
|
194
|
-
exitCode: commandResult.code,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
let raw;
|
|
198
|
-
try {
|
|
199
|
-
raw = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
200
|
-
} catch (error) {
|
|
201
|
-
return { status: "error", failureOwner: "infrastructure", solvedScore: "NA", durationSeconds, error: `invalid verifier JSON: ${error.message}`, exitCode: commandResult.code };
|
|
202
|
-
}
|
|
203
|
-
const validation = validateVerifierResult(raw, expected);
|
|
204
|
-
if (!validation.ok) return { status: "error", failureOwner: "infrastructure", solvedScore: "NA", durationSeconds, error: validation.error, exitCode: commandResult.code };
|
|
205
|
-
return {
|
|
206
|
-
status: raw.status,
|
|
207
|
-
failureOwner: raw.status === "failed" || raw.status === "blocked" ? "candidate" : raw.status === "passed" ? null : "infrastructure",
|
|
208
|
-
score: raw.score,
|
|
209
|
-
solvedScore: raw.score?.percentage ?? "NA",
|
|
210
|
-
criteria: raw.criteria,
|
|
211
|
-
reportPath,
|
|
212
|
-
durationSeconds,
|
|
213
|
-
exitCode: commandResult.code,
|
|
214
|
-
};
|
|
215
|
-
} finally {
|
|
216
|
-
if (config.stopCommand) {
|
|
217
|
-
await runCommand(config.stopCommand, { cwd: candidateDir, env, timeoutSeconds: config.timeoutSeconds > 0 ? Math.min(Number(config.timeoutSeconds), 30) : 0 });
|
|
218
|
-
}
|
|
219
|
-
if (candidateProcess && !candidateProcess.killed) stopProcessTree(candidateProcess, "SIGTERM");
|
|
220
|
-
if (releaseLock) releaseLock();
|
|
221
|
-
}
|
|
222
|
-
}
|