@skill-harness/core 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/types.d.ts +8 -0
- package/dist/adjudication.d.ts +1 -0
- package/dist/adjudication.js +26 -4
- package/dist/capture-trace-types.d.ts +19 -0
- package/dist/comparison.d.ts +96 -0
- package/dist/comparison.js +183 -0
- package/dist/execution-trace.js +93 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/lint.js +6 -0
- package/dist/regate.d.ts +1 -1
- package/dist/regate.js +94 -21
- package/dist/regrade.d.ts +4 -0
- package/dist/regrade.js +29 -5
- package/dist/report.d.ts +7 -0
- package/dist/report.js +6 -0
- package/dist/reps.d.ts +8 -1
- package/dist/reps.js +47 -13
- package/dist/rescore.js +2 -2
- package/dist/results.d.ts +28 -0
- package/dist/results.js +56 -4
- package/dist/run.js +96 -35
- package/dist/sandbox.d.ts +32 -0
- package/dist/sandbox.js +12 -0
- package/dist/score.d.ts +3 -1
- package/dist/score.js +20 -5
- package/dist/seeded.d.ts +7 -1
- package/dist/seeded.js +29 -8
- package/dist/sources.js +11 -3
- package/dist/spec.d.ts +15 -0
- package/dist/spec.js +53 -1
- package/dist/trace-gates.d.ts +2 -1
- package/dist/trace-gates.js +4 -1
- package/dist/trajectory-gates.d.ts +137 -0
- package/dist/trajectory-gates.js +592 -0
- package/package.json +5 -3
- package/schemas/results-v2.schema.json +133 -0
- package/schemas/specification-v1.schema.json +217 -0
- package/schemas/trajectory-event-v1.schema.json +61 -0
package/dist/regate.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
import { parseVerdict, detectMisfire } from "./grade.js";
|
|
4
4
|
import { evaluateNeedleGates, hasNeedleGates } from "./seeded.js";
|
|
5
5
|
import { evaluateTraceGates } from "./trace-gates.js";
|
|
6
6
|
import { mergeTraces, deserializeTrace } from "./execution-trace.js";
|
|
7
|
+
import { deserializeTrajectoryEvents, evaluateTrajectoryGates } from "./trajectory-gates.js";
|
|
7
8
|
import { judgeOneRep } from "./regrade.js";
|
|
8
|
-
import { readResults, writeResults, transcriptPath, judgeRawPath, repIndexOf, findDiffFiles, findTraceFiles, tracePath, effectiveThreshold, scoreContextFor, rebuildScenarioResult, } from "./results.js";
|
|
9
|
+
import { readResults, writeResults, diffPath, transcriptPath, judgeRawPath, repIndexOf, findDiffFiles, findTraceFiles, findTrajectoryFiles, tracePath, trajectoryPath, effectiveThreshold, scoreContextFor, rebuildScenarioResult, mergeScenarioMetrics, } from "./results.js";
|
|
9
10
|
import { outcomesToResult } from "./reps.js";
|
|
10
11
|
import { appendJournal } from "./journal.js";
|
|
11
12
|
import { gatesDigest, GATES_PREFIX } from "./sources.js";
|
|
@@ -88,30 +89,39 @@ export async function regateRun(opts) {
|
|
|
88
89
|
const s = specById.get(rec.id);
|
|
89
90
|
const needles = hasNeedleGates(s ?? {});
|
|
90
91
|
const traceGated = Boolean(s?.traceAssert);
|
|
91
|
-
|
|
92
|
+
const trajectoryGated = Boolean(s?.trajectoryAssert);
|
|
93
|
+
if (!s || (!needles && !traceGated && !trajectoryGated))
|
|
92
94
|
continue; // nothing for regate to re-decide
|
|
93
95
|
if (s.assert?.vitest || s.assert?.post_test) {
|
|
94
96
|
blocked.push(`${s.id}: declares ${s.assert.vitest ? "assert.vitest" : "assert.post_test"}, which needs the workspace — ` +
|
|
95
97
|
`no saved artifact can stand in for it, so this scenario needs a re-run`);
|
|
96
98
|
continue;
|
|
97
99
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
const expectedReps = rec.reps ?? 1;
|
|
101
|
+
const expectedSuffixes = expectedReps === 1 ? [undefined] : Array.from({ length: expectedReps }, (_, index) => index);
|
|
102
|
+
const complete = (paths, expectedPaths) => paths.length === expectedPaths.length && expectedPaths.every((path) => paths.includes(basename(path)));
|
|
103
|
+
if (needles && !complete(findDiffFiles(opts.runDir, s.id, mode), expectedSuffixes.map((rep) => diffPath(opts.runDir, s.id, mode, rep)))) {
|
|
104
|
+
blocked.push(`${s.id}: staged-diff artifacts are incomplete for ${expectedReps} recorded rep(s) — regate refuses a partial repetition set`);
|
|
100
105
|
continue;
|
|
101
106
|
}
|
|
102
107
|
// A trace gate is only re-decidable from a saved trace. A run recorded before
|
|
103
108
|
// traces existed has none, and saying so is the whole point — pretending
|
|
104
109
|
// regate can answer would report a verdict derived from no evidence.
|
|
105
|
-
if (traceGated && findTraceFiles(opts.runDir, s.id, mode).
|
|
106
|
-
blocked.push(`${s.id}:
|
|
107
|
-
`(
|
|
110
|
+
if (traceGated && !complete(findTraceFiles(opts.runDir, s.id, mode), expectedSuffixes.map((rep) => tracePath(opts.runDir, s.id, mode, rep)))) {
|
|
111
|
+
blocked.push(`${s.id}: assert.trace artifacts are incomplete for ${expectedReps} recorded rep(s) ` +
|
|
112
|
+
`(traces are gitignored, or capture failed) — it needs a re-run`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (trajectoryGated && !complete(findTrajectoryFiles(opts.runDir, s.id, mode), expectedSuffixes.map((rep) => trajectoryPath(opts.runDir, s.id, mode, rep)))) {
|
|
116
|
+
blocked.push(`${s.id}: assert.trajectory artifacts are incomplete for ${expectedReps} recorded rep(s) ` +
|
|
117
|
+
`(events are gitignored, or required native evidence was missing) — it needs a re-run`);
|
|
108
118
|
continue;
|
|
109
119
|
}
|
|
110
120
|
targets.push(s);
|
|
111
121
|
}
|
|
112
122
|
if (targets.length === 0) {
|
|
113
123
|
throw new Error(`nothing to regate in ${opts.runDir}` +
|
|
114
|
-
(blocked.length > 0 ? `:\n ${blocked.join("\n ")}` : " — no scenario declares diff_contains/diff_excludes or assert.
|
|
124
|
+
(blocked.length > 0 ? `:\n ${blocked.join("\n ")}` : " — no scenario declares diff_contains/diff_excludes, assert.trace, or assert.trajectory"));
|
|
115
125
|
}
|
|
116
126
|
const changes = [];
|
|
117
127
|
let judgeCalls = 0;
|
|
@@ -124,17 +134,21 @@ export async function regateRun(opts) {
|
|
|
124
134
|
}
|
|
125
135
|
const diffFiles = findDiffFiles(opts.runDir, scenario.id, mode);
|
|
126
136
|
const traceFiles = findTraceFiles(opts.runDir, scenario.id, mode);
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
//
|
|
137
|
+
const eventFiles = findTrajectoryFiles(opts.runDir, scenario.id, mode);
|
|
138
|
+
// Reps come from whichever artifact this scenario actually has. A trajectory-only
|
|
139
|
+
// scenario has neither diff nor trace, so falling back only once would regate
|
|
140
|
+
// nothing and report success.
|
|
130
141
|
const repKeys = diffFiles.length > 0
|
|
131
142
|
? diffFiles.map((f) => ({ rep: repIndexOf(f) ?? undefined, diffFile: f }))
|
|
132
|
-
: traceFiles.
|
|
143
|
+
: traceFiles.length > 0
|
|
144
|
+
? traceFiles.map((f) => ({ rep: repIndexOf(f) ?? undefined, diffFile: undefined }))
|
|
145
|
+
: eventFiles.map((f) => ({ rep: repIndexOf(f) ?? undefined, diffFile: undefined }));
|
|
133
146
|
const outcomes = [];
|
|
134
147
|
// Per scenario, not run-wide: with several regated scenarios, a global counter
|
|
135
148
|
// would report every change as "re-judged" because some other scenario was.
|
|
136
149
|
let judgedHere = 0;
|
|
137
150
|
let gateFailedHere = false;
|
|
151
|
+
let gateErroredHere = false;
|
|
138
152
|
for (const { rep, diffFile } of repKeys) {
|
|
139
153
|
const diff = diffFile ? readFileSync(join(opts.runDir, diffFile), "utf8") : "";
|
|
140
154
|
const needleGate = diffFile ? evaluateNeedleGates(scenario, diff) : { lines: [], failure: null };
|
|
@@ -166,14 +180,69 @@ export async function regateRun(opts) {
|
|
|
166
180
|
}
|
|
167
181
|
else {
|
|
168
182
|
const g = evaluateTraceGates(scenario.traceAssert, merged);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
const expectedHash = rec.objective?.rep_trace_sha256?.[rep ?? 0] ?? (rec.reps === undefined ? rec.objective?.trace_sha256 : undefined);
|
|
184
|
+
const digestMismatch = expectedHash !== undefined && expectedHash !== merged.trace_sha256;
|
|
185
|
+
const integrityAssertion = digestMismatch
|
|
186
|
+
? [{ kind: "trace_evidence", status: "ERROR", detail: "saved trace no longer matches the hash recorded by the run" }]
|
|
187
|
+
: [];
|
|
188
|
+
objective = { status: digestMismatch ? "ERROR" : g.status, trace_version: merged.trace_version, trace_sha256: merged.trace_sha256, assertions: [...integrityAssertion, ...g.assertions] };
|
|
189
|
+
if (g.status === "FAIL" || g.status === "ERROR" || digestMismatch) {
|
|
190
|
+
const bad = [...integrityAssertion.map((x) => x.detail), ...g.assertions.filter((x) => x.status === g.status && g.status !== "PASS").map((x) => x.detail)];
|
|
172
191
|
traceFailure = `objective: ${bad.join("; ")}`;
|
|
173
192
|
}
|
|
174
193
|
}
|
|
175
194
|
}
|
|
176
|
-
|
|
195
|
+
// Adapter-neutral trajectory gate, replayed from the saved normalized
|
|
196
|
+
// event artifact. Native ledgers are deliberately not re-read here: the
|
|
197
|
+
// artifact is the immutable evidence this run actually captured.
|
|
198
|
+
let trajectoryFailure = null;
|
|
199
|
+
if (scenario.trajectoryAssert) {
|
|
200
|
+
const ep = trajectoryPath(opts.runDir, scenario.id, mode, rep);
|
|
201
|
+
const events = existsSync(ep) ? deserializeTrajectoryEvents(readFileSync(ep, "utf8")) : null;
|
|
202
|
+
// The normalized artifact contains only events that were successfully captured.
|
|
203
|
+
// Source-read/normalization failures are persisted on the original objective;
|
|
204
|
+
// dropping them here would let a partial ledger become PASS on replay.
|
|
205
|
+
const priorEvidenceErrors = (rec.objective?.assertions ?? [])
|
|
206
|
+
.filter((result) => result.kind === "trajectory_evidence" && result.status === "ERROR");
|
|
207
|
+
if (!events) {
|
|
208
|
+
trajectoryFailure = "objective: saved normalized events are missing, malformed, or from an unsupported version";
|
|
209
|
+
objective = {
|
|
210
|
+
...(objective ?? { assertions: [] }),
|
|
211
|
+
status: "ERROR",
|
|
212
|
+
assertions: [...(objective?.assertions ?? []), { kind: "trajectory_evidence", status: "ERROR", detail: trajectoryFailure }],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const g = evaluateTrajectoryGates(scenario.trajectoryAssert, events);
|
|
217
|
+
const expectedHash = rec.objective?.rep_events_sha256?.[rep ?? 0] ?? (rec.reps === undefined ? rec.objective?.events_sha256 : undefined);
|
|
218
|
+
const digestMismatch = expectedHash !== undefined && expectedHash !== g.events_sha256;
|
|
219
|
+
const integrityAssertion = digestMismatch
|
|
220
|
+
? [{ kind: "trajectory_evidence", status: "ERROR", detail: "saved normalized events no longer match the hash recorded by the run" }]
|
|
221
|
+
: [];
|
|
222
|
+
const priorStatus = objective?.status ?? "PASS";
|
|
223
|
+
const status = priorStatus === "ERROR" || g.status === "ERROR" || priorEvidenceErrors.length || digestMismatch
|
|
224
|
+
? "ERROR"
|
|
225
|
+
: priorStatus === "FAIL" || g.status === "FAIL"
|
|
226
|
+
? "FAIL"
|
|
227
|
+
: "PASS";
|
|
228
|
+
objective = {
|
|
229
|
+
...(objective ?? {}),
|
|
230
|
+
status,
|
|
231
|
+
trajectory_version: g.event_version,
|
|
232
|
+
events_sha256: g.events_sha256,
|
|
233
|
+
assertions: [...(objective?.assertions ?? []), ...priorEvidenceErrors, ...integrityAssertion, ...g.assertions],
|
|
234
|
+
};
|
|
235
|
+
if (g.status !== "PASS" || priorEvidenceErrors.length || digestMismatch) {
|
|
236
|
+
const details = [
|
|
237
|
+
...priorEvidenceErrors.map((result) => result.detail),
|
|
238
|
+
...integrityAssertion.map((result) => result.detail),
|
|
239
|
+
...g.assertions.filter((result) => result.status === g.status && g.status !== "PASS").map((result) => result.detail),
|
|
240
|
+
];
|
|
241
|
+
trajectoryFailure = `objective: ${details.join("; ")}`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const gate = { lines: needleGate.lines, failure: needleGate.failure ?? traceFailure ?? trajectoryFailure };
|
|
177
246
|
const tPath = transcriptPath(opts.runDir, scenario.id, mode, rep);
|
|
178
247
|
const before = existsSync(tPath) ? readFileSync(tPath, "utf8") : "";
|
|
179
248
|
// Two sources, because the two gate kinds record their prior state
|
|
@@ -189,8 +258,11 @@ export async function regateRun(opts) {
|
|
|
189
258
|
if (existsSync(tPath))
|
|
190
259
|
rewriteTranscript(tPath, gate.lines);
|
|
191
260
|
if (gate.failure) {
|
|
192
|
-
|
|
193
|
-
|
|
261
|
+
if (objective?.status === "ERROR")
|
|
262
|
+
gateErroredHere = true;
|
|
263
|
+
else
|
|
264
|
+
gateFailedHere = true;
|
|
265
|
+
outcomes.push({ verdict: objective?.status === "ERROR" ? "ERROR" : "FAIL", reason: gate.failure, suspect: false, objective });
|
|
194
266
|
continue;
|
|
195
267
|
}
|
|
196
268
|
if (!oldGateFailed) {
|
|
@@ -205,13 +277,14 @@ export async function regateRun(opts) {
|
|
|
205
277
|
outcomes.push({ ...(await judgeOneRep({
|
|
206
278
|
runDir: opts.runDir, spec: opts.spec, scenario, transcript,
|
|
207
279
|
adapter: opts.adapter, judge: opts.judge, specDir: opts.specDir,
|
|
208
|
-
mode, rep, now,
|
|
280
|
+
mode, rep, now, rejudge: true,
|
|
209
281
|
})), objective });
|
|
210
282
|
judgeCalls++;
|
|
211
283
|
judgedHere++;
|
|
212
284
|
}
|
|
213
285
|
const threshold = effectiveThreshold(rec, scenario);
|
|
214
286
|
const next = outcomesToResult(scenario.id, outcomes, outcomes.length, threshold);
|
|
287
|
+
next.metrics = mergeScenarioMetrics(rec.metrics, next.metrics);
|
|
215
288
|
// Overrides and their notes survive: a regate re-decides the gate, and an author
|
|
216
289
|
// override is a statement about the judge, not about the needle.
|
|
217
290
|
// `regate` re-evaluates gates from saved artifacts and asks no judge anything:
|
|
@@ -222,7 +295,7 @@ export async function regateRun(opts) {
|
|
|
222
295
|
if (to !== rec.judge_verdict) {
|
|
223
296
|
changes.push({
|
|
224
297
|
id: scenario.id, from: rec.judge_verdict, to,
|
|
225
|
-
gate: gateFailedHere ? "fail" : "pass",
|
|
298
|
+
gate: gateErroredHere ? "error" : gateFailedHere ? "fail" : "pass",
|
|
226
299
|
judged: judgedHere > 0,
|
|
227
300
|
});
|
|
228
301
|
}
|
package/dist/regrade.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface RegradeOptions {
|
|
|
32
32
|
* with "nothing to re-grade", which is how ten scorable runs stayed ungraded.
|
|
33
33
|
*/
|
|
34
34
|
mode?: string;
|
|
35
|
+
/** Recorded repetition count; when supplied, missing/extra rep artifacts fail before judge calls. */
|
|
36
|
+
expectedReps?: number;
|
|
35
37
|
now?: () => string;
|
|
36
38
|
}
|
|
37
39
|
/** Judge one saved transcript: writes the judge-raw artifact, emits a `judge-verdict` journal event (plus `misfire-flag` when the verdict is suspect), and returns the outcome. */
|
|
@@ -46,6 +48,8 @@ export declare function judgeOneRep(opts: {
|
|
|
46
48
|
mode: string;
|
|
47
49
|
rep: number | undefined;
|
|
48
50
|
now: () => string;
|
|
51
|
+
/** True when this call revisits a saved subject transcript. */
|
|
52
|
+
rejudge?: boolean;
|
|
49
53
|
}): Promise<RepOutcome>;
|
|
50
54
|
/**
|
|
51
55
|
* Re-judge a scenario's saved transcript(s) for the run's mode with `judge` — no
|
package/dist/regrade.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { buildJudgePrompt, judgeInWorkspace } from "./grade.js";
|
|
4
|
-
import { findTranscriptFiles, judgeRawPath, repIndexOf, readResults, writeResults, effectiveThreshold, scoreContextFor, rebuildScenarioResult, } from "./results.js";
|
|
4
|
+
import { findTranscriptFiles, judgeRawPath, repIndexOf, readResults, writeResults, effectiveThreshold, scoreContextFor, rebuildScenarioResult, mergeScenarioMetrics, } from "./results.js";
|
|
5
5
|
import { outcomesToResult } from "./reps.js";
|
|
6
6
|
import { appendJournal } from "./journal.js";
|
|
7
7
|
import { rubricDigest, personaDigest, RUBRIC_PREFIX, PERSONA_KEY } from "./sources.js";
|
|
@@ -36,6 +36,7 @@ export function refreshRubricHashes(recorded, spec, judgedIds) {
|
|
|
36
36
|
/** Judge one saved transcript: writes the judge-raw artifact, emits a `judge-verdict` journal event (plus `misfire-flag` when the verdict is suspect), and returns the outcome. */
|
|
37
37
|
export async function judgeOneRep(opts) {
|
|
38
38
|
const { runDir, spec, scenario, transcript, adapter, judge, specDir, mode, rep, now } = opts;
|
|
39
|
+
const startedAt = performance.now();
|
|
39
40
|
const prompt = buildJudgePrompt({ skill: spec.skill, persona: spec.judge_persona, scenario, transcript });
|
|
40
41
|
const g = await judgeInWorkspace(adapter, judge, prompt, specDir);
|
|
41
42
|
writeFileSync(judgeRawPath(runDir, scenario.id, mode, rep), g.raw, "utf8");
|
|
@@ -43,7 +44,14 @@ export async function judgeOneRep(opts) {
|
|
|
43
44
|
appendJournal(runDir, { event: "judge-verdict", ts: now(), id: scenario.id, verdict: g.verdict, reason: g.reason, suspect: g.suspect, ...repField });
|
|
44
45
|
if (g.suspect)
|
|
45
46
|
appendJournal(runDir, { event: "misfire-flag", ts: now(), id: scenario.id, reason: g.reason, ...repField });
|
|
46
|
-
return {
|
|
47
|
+
return {
|
|
48
|
+
verdict: g.verdict, reason: g.reason, suspect: g.suspect,
|
|
49
|
+
metrics: {
|
|
50
|
+
wall_time_ms: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
51
|
+
judge_calls: 1,
|
|
52
|
+
judge_rejudge_calls: opts.rejudge ? 1 : 0,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
49
57
|
* Re-judge a scenario's saved transcript(s) for the run's mode with `judge` — no
|
|
@@ -58,14 +66,20 @@ export async function regradeScenario(opts) {
|
|
|
58
66
|
const files = findTranscriptFiles(opts.runDir, opts.scenario.id, mode);
|
|
59
67
|
if (files.length === 0)
|
|
60
68
|
throw new Error(`no ${mode} transcripts for ${opts.scenario.id} in ${opts.runDir}`);
|
|
61
|
-
const
|
|
69
|
+
const expected = opts.expectedReps ?? files.length;
|
|
70
|
+
const expectedIndices = expected === 1 ? [null] : Array.from({ length: expected }, (_, index) => index);
|
|
71
|
+
const actualIndices = files.map((file) => repIndexOf(file)).sort((a, b) => (a ?? -1) - (b ?? -1));
|
|
72
|
+
if (files.length !== expected || JSON.stringify(actualIndices) !== JSON.stringify(expectedIndices)) {
|
|
73
|
+
throw new Error(`${opts.scenario.id}: transcript artifacts are incomplete for ${expected} recorded rep(s) — re-run instead of grading a smaller repetition set`);
|
|
74
|
+
}
|
|
75
|
+
const repCount = expected;
|
|
62
76
|
const outcomes = [];
|
|
63
77
|
for (const file of files) {
|
|
64
78
|
const rep = repIndexOf(file) ?? undefined;
|
|
65
79
|
const transcript = readFileSync(join(opts.runDir, file), "utf8");
|
|
66
80
|
outcomes.push(await judgeOneRep({
|
|
67
81
|
runDir: opts.runDir, spec: opts.spec, scenario: opts.scenario, transcript,
|
|
68
|
-
adapter: opts.adapter, judge: opts.judge, specDir: opts.specDir, mode, rep, now,
|
|
82
|
+
adapter: opts.adapter, judge: opts.judge, specDir: opts.specDir, mode, rep, now, rejudge: true,
|
|
69
83
|
}));
|
|
70
84
|
}
|
|
71
85
|
return outcomesToResult(opts.scenario.id, outcomes, repCount, opts.threshold);
|
|
@@ -120,7 +134,15 @@ export async function regradeRun(opts) {
|
|
|
120
134
|
return prev;
|
|
121
135
|
}
|
|
122
136
|
}
|
|
123
|
-
const
|
|
137
|
+
const completeTranscripts = (record) => {
|
|
138
|
+
const expected = record.reps ?? 1;
|
|
139
|
+
const files = findTranscriptFiles(runDir, record.id, mode);
|
|
140
|
+
const expectedIndices = expected === 1 ? [null] : Array.from({ length: expected }, (_, index) => index);
|
|
141
|
+
const actualIndices = files.map((file) => repIndexOf(file)).sort((a, b) => (a ?? -1) - (b ?? -1));
|
|
142
|
+
return files.length === expected && JSON.stringify(actualIndices) === JSON.stringify(expectedIndices);
|
|
143
|
+
};
|
|
144
|
+
const recordedById = new Map(recorded.map((record) => [record.id, record]));
|
|
145
|
+
const missing = targets.filter((id) => !specById.has(id) || !completeTranscripts(recordedById.get(id)));
|
|
124
146
|
if (missing.length === targets.length) {
|
|
125
147
|
throw new Error(`no ${mode} transcripts in ${runDir} — nothing to re-grade`);
|
|
126
148
|
}
|
|
@@ -140,8 +162,10 @@ export async function regradeRun(opts) {
|
|
|
140
162
|
const threshold = effectiveThreshold(prevScenario, scenario);
|
|
141
163
|
const rr = await regradeScenario({
|
|
142
164
|
runDir, spec, scenario, adapter, judge, specDir, threshold, mode, now,
|
|
165
|
+
expectedReps: prevScenario?.reps ?? 1,
|
|
143
166
|
});
|
|
144
167
|
const carry = overrides.get(id);
|
|
168
|
+
rr.metrics = mergeScenarioMetrics(carry?.metrics, rr.metrics);
|
|
145
169
|
// `grade` re-judges the saved transcript. It does not re-evaluate trace gates
|
|
146
170
|
// (that is `regate`), so `objective` still describes this run; and it replaced
|
|
147
171
|
// the judgments a prior adjudication described, so that panel must go.
|
package/dist/report.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ShipBar } from "./spec.js";
|
|
2
2
|
import { type ResultsFile } from "./results.js";
|
|
3
3
|
import { type Lift } from "./lift.js";
|
|
4
|
+
import { type AggregateMetrics } from "./comparison.js";
|
|
4
5
|
export interface RunColumn {
|
|
5
6
|
index: number;
|
|
6
7
|
label: string;
|
|
@@ -8,8 +9,10 @@ export interface RunColumn {
|
|
|
8
9
|
runDir: string;
|
|
9
10
|
timestamp: string;
|
|
10
11
|
mode: string;
|
|
12
|
+
partial: boolean;
|
|
11
13
|
grade: ResultsFile["effective_grade"];
|
|
12
14
|
judge: ResultsFile["judge"];
|
|
15
|
+
metrics: AggregateMetrics;
|
|
13
16
|
cells: Record<string, {
|
|
14
17
|
judge_verdict: string;
|
|
15
18
|
judge_reason: string;
|
|
@@ -30,6 +33,7 @@ export interface RunColumn {
|
|
|
30
33
|
passes?: number;
|
|
31
34
|
clean?: number;
|
|
32
35
|
flakiness?: number;
|
|
36
|
+
metrics?: import("./results.js").ScenarioMetrics;
|
|
33
37
|
override: string | null;
|
|
34
38
|
note: string;
|
|
35
39
|
/**
|
|
@@ -92,11 +96,13 @@ export declare function publicView(data: ReportData): {
|
|
|
92
96
|
tag: string;
|
|
93
97
|
timestamp: string;
|
|
94
98
|
mode: string;
|
|
99
|
+
partial: boolean;
|
|
95
100
|
grade: import("./results.js").GradeSummary;
|
|
96
101
|
judge: {
|
|
97
102
|
provider: string;
|
|
98
103
|
model: string;
|
|
99
104
|
};
|
|
105
|
+
metrics: AggregateMetrics;
|
|
100
106
|
cells: Record<string, {
|
|
101
107
|
judge_verdict: string;
|
|
102
108
|
judge_reason: string;
|
|
@@ -117,6 +123,7 @@ export declare function publicView(data: ReportData): {
|
|
|
117
123
|
passes?: number;
|
|
118
124
|
clean?: number;
|
|
119
125
|
flakiness?: number;
|
|
126
|
+
metrics?: import("./results.js").ScenarioMetrics;
|
|
120
127
|
override: string | null;
|
|
121
128
|
note: string;
|
|
122
129
|
/**
|
package/dist/report.js
CHANGED
|
@@ -4,6 +4,7 @@ import { loadSpec } from "./spec.js";
|
|
|
4
4
|
import { readResults } from "./results.js";
|
|
5
5
|
import { collectLift, liftHeadline } from "./lift.js";
|
|
6
6
|
import { boundaryCells, collectStability, stabilityNote } from "./stability.js";
|
|
7
|
+
import { aggregateMetrics } from "./comparison.js";
|
|
7
8
|
/** Most-recent run dir (by name, which is an ISO-ish slug) under a model-tag dir. */
|
|
8
9
|
function latestRunDir(tagDir) {
|
|
9
10
|
if (!statSync(tagDir).isDirectory())
|
|
@@ -83,6 +84,7 @@ export function collectReport(skillDir) {
|
|
|
83
84
|
passes: s.passes,
|
|
84
85
|
clean: s.clean,
|
|
85
86
|
flakiness: s.flakiness,
|
|
87
|
+
metrics: s.metrics,
|
|
86
88
|
override: s.override,
|
|
87
89
|
note: s.note,
|
|
88
90
|
};
|
|
@@ -105,8 +107,10 @@ export function collectReport(skillDir) {
|
|
|
105
107
|
runDir,
|
|
106
108
|
timestamp: r.timestamp,
|
|
107
109
|
mode: r.mode,
|
|
110
|
+
partial: r.partial === true,
|
|
108
111
|
grade: r.effective_grade,
|
|
109
112
|
judge: r.judge,
|
|
113
|
+
metrics: aggregateMetrics(r.scenarios),
|
|
110
114
|
cells,
|
|
111
115
|
...(lift ? { lift, liftHeadline: liftHeadline(lift) } : {}),
|
|
112
116
|
});
|
|
@@ -127,8 +131,10 @@ export function publicView(data) {
|
|
|
127
131
|
tag: c.tag,
|
|
128
132
|
timestamp: c.timestamp,
|
|
129
133
|
mode: c.mode,
|
|
134
|
+
partial: c.partial,
|
|
130
135
|
grade: c.grade,
|
|
131
136
|
judge: c.judge,
|
|
137
|
+
metrics: c.metrics,
|
|
132
138
|
cells: c.cells,
|
|
133
139
|
...(c.lift ? { lift: c.lift, liftHeadline: c.liftHeadline } : {}),
|
|
134
140
|
})),
|
package/dist/reps.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { Verdict } from "./score.js";
|
|
2
2
|
import type { ScenarioResult, ObjectiveResult } from "./results.js";
|
|
3
|
+
import type { TraceMetrics } from "./capture-trace-types.js";
|
|
3
4
|
/** One rep's outcome (subject run + judge). */
|
|
4
5
|
export interface RepOutcome {
|
|
5
6
|
verdict: Verdict;
|
|
6
7
|
reason: string;
|
|
7
8
|
suspect: boolean;
|
|
8
|
-
/** Present only when the scenario declared
|
|
9
|
+
/** Present only when the scenario declared structured objective assertions. */
|
|
9
10
|
objective?: ObjectiveResult;
|
|
11
|
+
metrics?: {
|
|
12
|
+
wall_time_ms: number;
|
|
13
|
+
judge_calls: number;
|
|
14
|
+
judge_rejudge_calls: number;
|
|
15
|
+
subject?: TraceMetrics;
|
|
16
|
+
};
|
|
10
17
|
}
|
|
11
18
|
/**
|
|
12
19
|
* Collapse per-rep objective results.
|
package/dist/reps.js
CHANGED
|
@@ -12,13 +12,16 @@ export function aggregateObjective(outcomes) {
|
|
|
12
12
|
const present = outcomes.map((o) => o.objective).filter((o) => o !== undefined);
|
|
13
13
|
if (present.length === 0)
|
|
14
14
|
return undefined;
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
17
|
-
return
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
const picked = present.find((o) => o.status === "ERROR") ?? present.find((o) => o.status === "FAIL") ?? present[0];
|
|
16
|
+
if (present.length === 1)
|
|
17
|
+
return picked;
|
|
18
|
+
const eventHashes = present.map((objective) => objective.events_sha256);
|
|
19
|
+
const traceHashes = present.map((objective) => objective.trace_sha256);
|
|
20
|
+
return {
|
|
21
|
+
...picked,
|
|
22
|
+
...(eventHashes.every((hash) => typeof hash === "string") ? { rep_events_sha256: eventHashes } : {}),
|
|
23
|
+
...(traceHashes.every((hash) => typeof hash === "string") ? { rep_trace_sha256: traceHashes } : {}),
|
|
24
|
+
};
|
|
22
25
|
}
|
|
23
26
|
/**
|
|
24
27
|
* Collapse N rep outcomes into one scenario verdict. A rep is "clean" when its
|
|
@@ -31,14 +34,17 @@ export function aggregateReps(outcomes, threshold) {
|
|
|
31
34
|
const reps = outcomes.length;
|
|
32
35
|
const clean = outcomes.filter((o) => !o.suspect);
|
|
33
36
|
const passes = clean.filter((o) => o.verdict === "PASS").length;
|
|
37
|
+
const errored = outcomes.filter((o) => o.verdict === "ERROR").length;
|
|
38
|
+
// Infrastructure/judge/tooling errors are not behavioral failures, and they
|
|
39
|
+
// cannot be voted into a pass by successful siblings. Preserve the third
|
|
40
|
+
// state so a release blocks for missing evidence without blaming the skill.
|
|
41
|
+
if (errored > 0) {
|
|
42
|
+
return { verdict: "ERROR", reason: `${errored}/${reps} reps errored — infrastructure, not behavior`, passes, reps, clean: clean.length, flakiness: 0, suspect: false };
|
|
43
|
+
}
|
|
34
44
|
if (clean.length * 2 < reps) {
|
|
35
45
|
// majority of reps misfired → untrustworthy
|
|
36
46
|
return { verdict: "FAIL", reason: `${reps - clean.length}/${reps} reps misfired — re-judge`, passes, reps, clean: clean.length, flakiness: 0, suspect: true };
|
|
37
47
|
}
|
|
38
|
-
const errored = clean.filter((o) => o.verdict === "ERROR").length;
|
|
39
|
-
if (clean.length > 0 && errored === clean.length) {
|
|
40
|
-
return { verdict: "ERROR", reason: `${errored}/${reps} reps errored`, passes: 0, reps, clean: clean.length, flakiness: 0, suspect: false };
|
|
41
|
-
}
|
|
42
48
|
const passRate = passes / clean.length;
|
|
43
49
|
const verdict = passRate >= threshold ? "PASS" : "FAIL";
|
|
44
50
|
const flakiness = 1 - Math.abs(2 * passRate - 1);
|
|
@@ -57,15 +63,43 @@ export function outcomesToResult(id, outcomes, repCount, threshold) {
|
|
|
57
63
|
// produce a result byte-identical to one from before this field existed.
|
|
58
64
|
const objective = aggregateObjective(outcomes);
|
|
59
65
|
const objectiveField = objective ? { objective } : {};
|
|
66
|
+
const metrics = aggregateMetrics(outcomes);
|
|
67
|
+
const metricsField = metrics ? { metrics } : {};
|
|
60
68
|
if (repCount === 1) {
|
|
61
69
|
const o = outcomes[0];
|
|
62
|
-
return { id, judge_verdict: o.verdict, judge_reason: o.reason, suspect: o.suspect, override: null, note: "", ...objectiveField };
|
|
70
|
+
return { id, judge_verdict: o.verdict, judge_reason: o.reason, suspect: o.suspect, ...metricsField, override: null, note: "", ...objectiveField };
|
|
63
71
|
}
|
|
64
72
|
const agg = aggregateReps(outcomes, threshold);
|
|
65
73
|
return {
|
|
66
74
|
id, judge_verdict: agg.verdict, judge_reason: agg.reason, suspect: agg.suspect,
|
|
67
75
|
reps: agg.reps, passes: agg.passes, clean: agg.clean, flakiness: agg.flakiness,
|
|
68
|
-
pass_threshold: threshold, override: null, note: "", ...objectiveField,
|
|
76
|
+
pass_threshold: threshold, ...metricsField, override: null, note: "", ...objectiveField,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function aggregateMetrics(outcomes) {
|
|
80
|
+
const present = outcomes.map((outcome) => outcome.metrics).filter((metrics) => metrics !== undefined);
|
|
81
|
+
if (present.length === 0)
|
|
82
|
+
return undefined;
|
|
83
|
+
const subjects = present.map((metrics) => metrics.subject).filter((metrics) => metrics !== undefined);
|
|
84
|
+
const base = {
|
|
85
|
+
wall_time_ms: present.reduce((sum, metrics) => sum + metrics.wall_time_ms, 0),
|
|
86
|
+
judge_calls: present.reduce((sum, metrics) => sum + metrics.judge_calls, 0),
|
|
87
|
+
judge_rejudge_calls: present.reduce((sum, metrics) => sum + metrics.judge_rejudge_calls, 0),
|
|
88
|
+
subject_metrics_reps: subjects.length,
|
|
89
|
+
total_reps: outcomes.length,
|
|
90
|
+
};
|
|
91
|
+
if (subjects.length === 0)
|
|
92
|
+
return base;
|
|
93
|
+
return {
|
|
94
|
+
...base,
|
|
95
|
+
input_tokens: subjects.reduce((sum, metrics) => sum + metrics.input_tokens, 0),
|
|
96
|
+
output_tokens: subjects.reduce((sum, metrics) => sum + metrics.output_tokens, 0),
|
|
97
|
+
cache_read_tokens: subjects.reduce((sum, metrics) => sum + metrics.cache_read_tokens, 0),
|
|
98
|
+
cache_write_tokens: subjects.reduce((sum, metrics) => sum + metrics.cache_write_tokens, 0),
|
|
99
|
+
subject_cost_usd: subjects.reduce((sum, metrics) => sum + metrics.cost_usd, 0),
|
|
100
|
+
tool_calls: subjects.reduce((sum, metrics) => sum + metrics.tool_calls, 0),
|
|
101
|
+
delegated_children: subjects.reduce((sum, metrics) => sum + metrics.delegated_children, 0),
|
|
102
|
+
max_concurrency: Math.max(...subjects.map((metrics) => metrics.max_concurrency)),
|
|
69
103
|
};
|
|
70
104
|
}
|
|
71
105
|
//# sourceMappingURL=reps.js.map
|
package/dist/rescore.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { readResults, writeResults, scoreContextFor, rebuildScenarioResult } from "./results.js";
|
|
3
|
+
import { readResults, writeResults, scoreContextFor, rebuildScenarioResult, effectiveThreshold } from "./results.js";
|
|
4
4
|
import { appendJournal } from "./journal.js";
|
|
5
5
|
import { policyDigest, POLICY_PREFIX } from "./sources.js";
|
|
6
6
|
/**
|
|
@@ -56,7 +56,7 @@ export function rescoreRun(opts) {
|
|
|
56
56
|
return s;
|
|
57
57
|
if (s.clean === 0)
|
|
58
58
|
return s;
|
|
59
|
-
const toThreshold = scenario
|
|
59
|
+
const toThreshold = effectiveThreshold(undefined, scenario);
|
|
60
60
|
const fromThreshold = s.pass_threshold ?? toThreshold;
|
|
61
61
|
if (toThreshold === fromThreshold)
|
|
62
62
|
return s;
|
package/dist/results.d.ts
CHANGED
|
@@ -2,6 +2,23 @@ import { type ModelRef } from "./adapters/types.js";
|
|
|
2
2
|
import { type ScenarioVerdict } from "./score.js";
|
|
3
3
|
import type { Verdict } from "./score.js";
|
|
4
4
|
import type { ShipBar, Scenario } from "./spec.js";
|
|
5
|
+
export interface ScenarioMetrics {
|
|
6
|
+
wall_time_ms: number;
|
|
7
|
+
judge_calls: number;
|
|
8
|
+
judge_rejudge_calls: number;
|
|
9
|
+
subject_metrics_reps: number;
|
|
10
|
+
total_reps: number;
|
|
11
|
+
input_tokens?: number;
|
|
12
|
+
output_tokens?: number;
|
|
13
|
+
cache_read_tokens?: number;
|
|
14
|
+
cache_write_tokens?: number;
|
|
15
|
+
subject_cost_usd?: number;
|
|
16
|
+
tool_calls?: number;
|
|
17
|
+
delegated_children?: number;
|
|
18
|
+
max_concurrency?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Merge bookkeeping from a later judge/gate pass without duplicating subject usage. */
|
|
21
|
+
export declare function mergeScenarioMetrics(prior: ScenarioMetrics | undefined, fresh: ScenarioMetrics | undefined): ScenarioMetrics | undefined;
|
|
5
22
|
export interface ScenarioResult {
|
|
6
23
|
id: string;
|
|
7
24
|
judge_verdict: Verdict;
|
|
@@ -14,6 +31,8 @@ export interface ScenarioResult {
|
|
|
14
31
|
clean?: number;
|
|
15
32
|
flakiness?: number;
|
|
16
33
|
pass_threshold?: number;
|
|
34
|
+
/** Cost/latency counters. Separate from verdicts: cheaper failure is still failure. */
|
|
35
|
+
metrics?: ScenarioMetrics;
|
|
17
36
|
/**
|
|
18
37
|
* Objective trace-gate evidence. ADDITIVE and optional.
|
|
19
38
|
*
|
|
@@ -59,6 +78,11 @@ export interface ObjectiveResult {
|
|
|
59
78
|
status: "PASS" | "FAIL" | "ERROR";
|
|
60
79
|
trace_version?: number;
|
|
61
80
|
trace_sha256?: string;
|
|
81
|
+
trajectory_version?: string;
|
|
82
|
+
events_sha256?: string;
|
|
83
|
+
/** Per-repetition hashes retained when an aggregate has more than one rep. */
|
|
84
|
+
rep_events_sha256?: string[];
|
|
85
|
+
rep_trace_sha256?: string[];
|
|
62
86
|
assertions: {
|
|
63
87
|
kind: string;
|
|
64
88
|
status: "PASS" | "FAIL" | "ERROR";
|
|
@@ -327,6 +351,10 @@ export declare function rebuildScenarioResult(fresh: ScenarioResult, prior: Scen
|
|
|
327
351
|
* traces append without a wrapper.
|
|
328
352
|
*/
|
|
329
353
|
export declare function tracePath(runDir: string, scenarioId: string, mode: string, rep?: number): string;
|
|
354
|
+
/** Saved adapter-neutral workflow events for one scenario rep. */
|
|
355
|
+
export declare function trajectoryPath(runDir: string, scenarioId: string, mode: string, rep?: number): string;
|
|
356
|
+
/** A scenario's normalized-event files, sorted (plain first, then numeric rep). */
|
|
357
|
+
export declare function findTrajectoryFiles(runDir: string, scenarioId: string, mode?: string): string[];
|
|
330
358
|
/** A scenario's staged-diff files, sorted (plain first, then numeric rep). Mode-scoped when given. */
|
|
331
359
|
export declare function findDiffFiles(runDir: string, scenarioId: string, mode?: string): string[];
|
|
332
360
|
/** A scenario's execution-trace files, sorted (plain first, then numeric rep). Mode-scoped when given. */
|