@prismatic-io/lux 0.0.2-preview.32 → 0.0.2-preview.35
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/lib/assertions/rubric/index.d.ts.map +1 -1
- package/lib/assertions/rubric/index.js +10 -1
- package/lib/assertions/rubric/index.js.map +1 -1
- package/lib/assertions/rubric/internal.d.ts.map +1 -1
- package/lib/assertions/rubric/internal.js +22 -13
- package/lib/assertions/rubric/internal.js.map +1 -1
- package/lib/cli/output-schemas.d.ts +6 -2
- package/lib/cli/output-schemas.d.ts.map +1 -1
- package/lib/cli/output-schemas.js +5 -2
- package/lib/cli/output-schemas.js.map +1 -1
- package/lib/cli/render/format.d.ts.map +1 -1
- package/lib/cli/render/format.js +1 -1
- package/lib/cli/render/format.js.map +1 -1
- package/lib/cli/render/reporter.js +2 -2
- package/lib/cli/render/reporter.js.map +1 -1
- package/lib/core/artifact-evidence.d.ts +6 -1
- package/lib/core/artifact-evidence.d.ts.map +1 -1
- package/lib/core/artifact-evidence.js +27 -6
- package/lib/core/artifact-evidence.js.map +1 -1
- package/lib/core/experiment.d.ts +1 -0
- package/lib/core/experiment.d.ts.map +1 -1
- package/lib/core/experiment.js +5 -1
- package/lib/core/experiment.js.map +1 -1
- package/lib/drivers/claude-code/index.d.ts +7 -0
- package/lib/drivers/claude-code/index.d.ts.map +1 -1
- package/lib/drivers/claude-code/index.js +12 -1
- package/lib/drivers/claude-code/index.js.map +1 -1
- package/lib/drivers/codex/config.d.ts +3 -0
- package/lib/drivers/codex/config.d.ts.map +1 -1
- package/lib/drivers/codex/config.js +9 -0
- package/lib/drivers/codex/config.js.map +1 -1
- package/lib/drivers/codex/index.d.ts +4 -0
- package/lib/drivers/codex/index.d.ts.map +1 -1
- package/lib/drivers/codex/index.js +2 -0
- package/lib/drivers/codex/index.js.map +1 -1
- package/lib/drivers/shared/experiment-behavior.d.ts +1 -1
- package/lib/drivers/shared/experiment-behavior.d.ts.map +1 -1
- package/lib/drivers/shared/experiment-behavior.js +3 -0
- package/lib/drivers/shared/experiment-behavior.js.map +1 -1
- package/lib/drivers/shared/experiment-policy.d.ts.map +1 -1
- package/lib/drivers/shared/experiment-policy.js +1 -0
- package/lib/drivers/shared/experiment-policy.js.map +1 -1
- package/lib/drivers/shared/skills.d.ts +3 -0
- package/lib/drivers/shared/skills.d.ts.map +1 -0
- package/lib/drivers/shared/skills.js +73 -0
- package/lib/drivers/shared/skills.js.map +1 -0
- package/lib/orchestrator/compare.d.ts +4 -3
- package/lib/orchestrator/compare.d.ts.map +1 -1
- package/lib/orchestrator/compare.js +13 -6
- package/lib/orchestrator/compare.js.map +1 -1
- package/lib/orchestrator/experiment-contracts.d.ts +1 -0
- package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
- package/lib/orchestrator/experiment-effects.d.ts.map +1 -1
- package/lib/orchestrator/experiment-effects.js +4 -2
- package/lib/orchestrator/experiment-effects.js.map +1 -1
- package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.js +15 -2
- package/lib/orchestrator/experiment-evaluation.js.map +1 -1
- package/lib/orchestrator/experiment-report.d.ts +1 -0
- package/lib/orchestrator/experiment-report.d.ts.map +1 -1
- package/lib/orchestrator/experiment-runs.d.ts +2 -0
- package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
- package/lib/orchestrator/experiment-selection.d.ts.map +1 -1
- package/lib/orchestrator/experiment-selection.js +99 -43
- package/lib/orchestrator/experiment-selection.js.map +1 -1
- package/lib/orchestrator/grading.d.ts +1 -0
- package/lib/orchestrator/grading.d.ts.map +1 -1
- package/lib/orchestrator/grading.js +7 -0
- package/lib/orchestrator/grading.js.map +1 -1
- package/lib/orchestrator/list-runs.d.ts.map +1 -1
- package/lib/orchestrator/list-runs.js +3 -5
- package/lib/orchestrator/list-runs.js.map +1 -1
- package/lib/orchestrator/paired-effects.d.ts.map +1 -1
- package/lib/orchestrator/paired-effects.js +7 -1
- package/lib/orchestrator/paired-effects.js.map +1 -1
- package/lib/orchestrator/suite-compare.d.ts.map +1 -1
- package/lib/orchestrator/suite-compare.js +18 -6
- package/lib/orchestrator/suite-compare.js.map +1 -1
- package/lib/orchestrator/suite-summary.d.ts +2 -0
- package/lib/orchestrator/suite-summary.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.js +6 -2
- package/lib/orchestrator/suite-summary.js.map +1 -1
- package/lib/previewer/server.d.ts.map +1 -1
- package/lib/previewer/server.js +2 -1
- package/lib/previewer/server.js.map +1 -1
- package/package.json +2 -1
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/assertions/rubric/index.ts +10 -1
- package/src/assertions/rubric/internal.ts +27 -13
- package/src/cli/output-schemas.ts +5 -2
- package/src/cli/render/format.ts +1 -1
- package/src/cli/render/reporter.ts +2 -2
- package/src/core/artifact-evidence.ts +40 -5
- package/src/core/experiment.ts +7 -1
- package/src/drivers/claude-code/index.ts +12 -1
- package/src/drivers/codex/config.ts +9 -0
- package/src/drivers/codex/index.ts +2 -0
- package/src/drivers/shared/experiment-behavior.ts +5 -2
- package/src/drivers/shared/experiment-policy.ts +1 -0
- package/src/drivers/shared/skills.ts +74 -0
- package/src/orchestrator/compare.ts +23 -9
- package/src/orchestrator/experiment-effects.ts +4 -2
- package/src/orchestrator/experiment-evaluation.ts +18 -2
- package/src/orchestrator/experiment-selection.ts +101 -18
- package/src/orchestrator/grading.ts +11 -0
- package/src/orchestrator/list-runs.ts +5 -6
- package/src/orchestrator/paired-effects.ts +6 -1
- package/src/orchestrator/suite-compare.ts +19 -5
- package/src/orchestrator/suite-summary.ts +7 -2
- package/src/previewer/server.ts +2 -0
- package/viewer/app.css +32 -0
- package/viewer/charts.js +16 -4
- package/viewer/detail.js +26 -8
- package/viewer/format.js +2 -2
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { hasAgentUsage } from "../core/usage.js";
|
|
21
21
|
import { stableDriverIdentity } from "./comparison-identity.js";
|
|
22
22
|
import { safeReadJson, walkFiles } from "./fs-read.js";
|
|
23
|
+
import { gradingScoreBasis } from "./grading.js";
|
|
23
24
|
|
|
24
25
|
export type ComparisonInput = {
|
|
25
26
|
pathA: string;
|
|
@@ -39,9 +40,9 @@ export type GradingDelta = {
|
|
|
39
40
|
id: string;
|
|
40
41
|
type: string;
|
|
41
42
|
name: string;
|
|
42
|
-
before: { passed: boolean | null; score: number } | null;
|
|
43
|
-
after: { passed: boolean | null; score: number } | null;
|
|
44
|
-
status: "added" | "removed" | "improved" | "regressed" | "unchanged";
|
|
43
|
+
before: { passed: boolean | null; score: number | null } | null;
|
|
44
|
+
after: { passed: boolean | null; score: number | null } | null;
|
|
45
|
+
status: "added" | "removed" | "improved" | "regressed" | "unchanged" | "unavailable";
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
export type RunComparison = {
|
|
@@ -67,6 +68,7 @@ export type RunComparison = {
|
|
|
67
68
|
casePassedB: boolean | null;
|
|
68
69
|
caseScoreA: number | null;
|
|
69
70
|
caseScoreB: number | null;
|
|
71
|
+
caseScoreComparable: boolean;
|
|
70
72
|
deltas: GradingDelta[];
|
|
71
73
|
improvements: number;
|
|
72
74
|
regressions: number;
|
|
@@ -156,6 +158,9 @@ const assertionsByIdentity = (
|
|
|
156
158
|
return out;
|
|
157
159
|
};
|
|
158
160
|
|
|
161
|
+
const measuredScore = (entry: GradingReport["assertions"][number]): number | null =>
|
|
162
|
+
entry.errorMessage !== undefined || entry.abstention !== undefined ? null : entry.score;
|
|
163
|
+
|
|
159
164
|
const computeGradingDeltas = (
|
|
160
165
|
a: GradingReport | null,
|
|
161
166
|
b: GradingReport | null,
|
|
@@ -174,7 +179,8 @@ const computeGradingDeltas = (
|
|
|
174
179
|
if (!ax && bx) status = "added";
|
|
175
180
|
else if (ax && !bx) status = "removed";
|
|
176
181
|
else if (ax && bx) {
|
|
177
|
-
if (ax
|
|
182
|
+
if (measuredScore(ax) === null || measuredScore(bx) === null) status = "unavailable";
|
|
183
|
+
else if (ax.passed === bx.passed && ax.score === bx.score) status = "unchanged";
|
|
178
184
|
// A pass/fail flip dominates any score movement.
|
|
179
185
|
else if (ax.passed !== bx.passed) status = bx.passed ? "improved" : "regressed";
|
|
180
186
|
else status = ax.score < bx.score ? "improved" : "regressed";
|
|
@@ -189,8 +195,8 @@ const computeGradingDeltas = (
|
|
|
189
195
|
id: name,
|
|
190
196
|
type: ax?.type ?? bx?.type ?? "unknown",
|
|
191
197
|
name: ax?.id || bx?.id ? (ax?.name ?? bx?.name ?? name) : name,
|
|
192
|
-
before: ax ? { passed: ax.passed, score: ax
|
|
193
|
-
after: bx ? { passed: bx.passed, score: bx
|
|
198
|
+
before: ax ? { passed: ax.passed, score: measuredScore(ax) } : null,
|
|
199
|
+
after: bx ? { passed: bx.passed, score: measuredScore(bx) } : null,
|
|
194
200
|
status,
|
|
195
201
|
});
|
|
196
202
|
}
|
|
@@ -314,6 +320,9 @@ export const compareRuns = async (input: ComparisonInput): Promise<RunComparison
|
|
|
314
320
|
casePassedB: b.grading?.casePassed ?? null,
|
|
315
321
|
caseScoreA: a.grading?.caseScore ?? null,
|
|
316
322
|
caseScoreB: b.grading?.caseScore ?? null,
|
|
323
|
+
caseScoreComparable:
|
|
324
|
+
gradingScoreBasis(a.grading) !== null &&
|
|
325
|
+
gradingScoreBasis(a.grading) === gradingScoreBasis(b.grading),
|
|
317
326
|
...grading,
|
|
318
327
|
},
|
|
319
328
|
artifactsDelta: computeFileDelta(a.fileTree, b.fileTree),
|
|
@@ -327,6 +336,7 @@ export const compareRuns = async (input: ComparisonInput): Promise<RunComparison
|
|
|
327
336
|
export const hasRegression = (cmp: RunComparison): boolean =>
|
|
328
337
|
cmp.caseMismatch.length > 0 ||
|
|
329
338
|
cmp.grading.regressions > 0 ||
|
|
339
|
+
cmp.grading.deltas.some((delta) => delta.after?.score === null && delta.after.passed !== null) ||
|
|
330
340
|
cmp.grading.deltas.some((delta) => delta.status === "removed" && delta.before?.passed !== null) ||
|
|
331
341
|
(cmp.grading.casePassedA === true && cmp.grading.casePassedB !== true);
|
|
332
342
|
|
|
@@ -376,13 +386,17 @@ export const formatComparisonText = (cmp: RunComparison): string => {
|
|
|
376
386
|
lines.push("Grading:");
|
|
377
387
|
lines.push(
|
|
378
388
|
` passed: ${String(cmp.grading.casePassedA)} → ${String(cmp.grading.casePassedB)}; ` +
|
|
379
|
-
`score: ${cmp.grading.caseScoreA ?? "?"} → ${cmp.grading.caseScoreB ?? "?"}`,
|
|
389
|
+
`score: ${cmp.grading.caseScoreA ?? "?"} → ${cmp.grading.caseScoreB ?? "?"}${cmp.grading.caseScoreComparable ? "" : " (change unavailable: scoring basis differs or is unknown)"}`,
|
|
380
390
|
);
|
|
381
391
|
lines.push(` ${cmp.grading.improvements} improved, ${cmp.grading.regressions} regressed`);
|
|
382
392
|
for (const d of cmp.grading.deltas) {
|
|
383
393
|
if (d.status === "unchanged") continue;
|
|
384
|
-
const arrow = { added: "+", removed: "-", improved: "↑", regressed: "↓" }[
|
|
385
|
-
|
|
394
|
+
const arrow = { added: "+", removed: "-", improved: "↑", regressed: "↓", unavailable: "?" }[
|
|
395
|
+
d.status
|
|
396
|
+
];
|
|
397
|
+
lines.push(
|
|
398
|
+
` ${arrow} ${d.name}: ${d.before?.score ?? "unavailable"} → ${d.after?.score ?? "unavailable"}`,
|
|
399
|
+
);
|
|
386
400
|
}
|
|
387
401
|
lines.push("");
|
|
388
402
|
lines.push("Artifacts:");
|
|
@@ -23,7 +23,7 @@ export const experimentEffects = (
|
|
|
23
23
|
const metrics = new Set([
|
|
24
24
|
"quality.passRate",
|
|
25
25
|
experiment.evaluationKind === "grader" ? "harness.total" : "agent.total",
|
|
26
|
-
...(experiment.evaluationKind === "agent" ? ["interrupts.ask", "toolCalls"] : []),
|
|
26
|
+
...(experiment.evaluationKind === "agent" ? ["score", "interrupts.ask", "toolCalls"] : []),
|
|
27
27
|
...experiment.campaignSnapshot.objectives.constraints
|
|
28
28
|
.filter((constraint) => constraint.relativeTo)
|
|
29
29
|
.map((constraint) => constraint.metric),
|
|
@@ -46,7 +46,9 @@ export const experimentEffects = (
|
|
|
46
46
|
const confidence =
|
|
47
47
|
constraint?.confidence ??
|
|
48
48
|
ConfidenceConfigSchema.parse({
|
|
49
|
-
method:
|
|
49
|
+
method: ["quality.passRate", "score"].includes(metric)
|
|
50
|
+
? "hoeffding"
|
|
51
|
+
: "percentile-bootstrap",
|
|
50
52
|
});
|
|
51
53
|
return {
|
|
52
54
|
candidateId,
|
|
@@ -44,6 +44,7 @@ const gradingFailureMessage = (results: readonly SuiteCaseResult[]): string | un
|
|
|
44
44
|
}
|
|
45
45
|
const report = result.result?.grading;
|
|
46
46
|
const failedAssertion = report.assertions.find((assertion) => {
|
|
47
|
+
if (assertion.passed === null) return false;
|
|
47
48
|
if (assertion.errorMessage) return true;
|
|
48
49
|
const detail =
|
|
49
50
|
assertion.detail && typeof assertion.detail === "object"
|
|
@@ -299,10 +300,12 @@ const agentEvaluationFromSuite = (input: {
|
|
|
299
300
|
return {
|
|
300
301
|
[sample.canonicalCaseId]: {
|
|
301
302
|
identityHash,
|
|
303
|
+
scoreBasis: sample.scoreBasis ?? null,
|
|
302
304
|
...(input.clusterId ? { clusterId: input.clusterId } : {}),
|
|
303
305
|
metrics: Object.fromEntries(
|
|
304
306
|
Object.entries({
|
|
305
307
|
...sample.metrics,
|
|
308
|
+
...(sample.score !== undefined ? { score: sample.score } : {}),
|
|
306
309
|
"quality.passRate": sample.passed === true ? 1 : 0,
|
|
307
310
|
}).map(([name, value]) => [name, { mean: value, n: 1 }]),
|
|
308
311
|
),
|
|
@@ -322,7 +325,10 @@ const agentEvaluationFromSuite = (input: {
|
|
|
322
325
|
summary.cases.map((entry) => [entry.canonicalCaseId, entry.passRate]),
|
|
323
326
|
),
|
|
324
327
|
metrics: evaluationMetrics(summary, candidate),
|
|
325
|
-
metricDefinitions:
|
|
328
|
+
metricDefinitions: {
|
|
329
|
+
score: { unit: "score", better: "higher", label: "Quality score" },
|
|
330
|
+
...evaluationMetricDefinitions(summary),
|
|
331
|
+
},
|
|
326
332
|
samples: evaluationSamples(summary),
|
|
327
333
|
...(caseSamples ? { caseSamples } : {}),
|
|
328
334
|
metricCalls: summary.samples.length,
|
|
@@ -406,6 +412,12 @@ const aggregateAgentUnits = (
|
|
|
406
412
|
const caseSamples = clusterCaseSamples(units.map((unit) => unit.evaluation.caseSamples));
|
|
407
413
|
const grouped = groupedCaseMetricSamples(caseSamples, repetitions);
|
|
408
414
|
const samples = grouped ?? ungroupedSamples;
|
|
415
|
+
const completeScore =
|
|
416
|
+
caseSamples &&
|
|
417
|
+
Object.values(caseSamples).every(
|
|
418
|
+
(sample) => sample.metrics.score?.n === repetitions && Boolean(sample.scoreBasis),
|
|
419
|
+
);
|
|
420
|
+
if (!completeScore) delete samples.score;
|
|
409
421
|
return {
|
|
410
422
|
candidateId: candidate.id,
|
|
411
423
|
profileId,
|
|
@@ -417,7 +429,11 @@ const aggregateAgentUnits = (
|
|
|
417
429
|
),
|
|
418
430
|
metrics: Object.fromEntries(
|
|
419
431
|
[...metricValues]
|
|
420
|
-
.filter(([name]) =>
|
|
432
|
+
.filter(([name]) =>
|
|
433
|
+
name === "score"
|
|
434
|
+
? completeScore
|
|
435
|
+
: !grouped || name === "subject.bytes" || Object.hasOwn(grouped, name),
|
|
436
|
+
)
|
|
421
437
|
.map(([name, values]) => [
|
|
422
438
|
name,
|
|
423
439
|
name === "subject.bytes" ? candidate.sourceBytes : average(grouped?.[name] ?? values),
|
|
@@ -252,13 +252,65 @@ const normalizedConstraintViolation = (
|
|
|
252
252
|
return worst;
|
|
253
253
|
};
|
|
254
254
|
|
|
255
|
+
const hasScoreEvidence = (
|
|
256
|
+
experiment: OpenedExperimentRun["manifest"],
|
|
257
|
+
split: ExperimentSplit,
|
|
258
|
+
): boolean =>
|
|
259
|
+
[...new Set(experiment.evaluations.map((entry) => entry.candidateId))]
|
|
260
|
+
.flatMap((id) => candidateEvaluations(experiment, id))
|
|
261
|
+
.some(
|
|
262
|
+
(entry) =>
|
|
263
|
+
entry.split === split &&
|
|
264
|
+
(entry.metricDefinitions?.score !== undefined ||
|
|
265
|
+
entry.metrics.score !== undefined ||
|
|
266
|
+
Object.values(entry.caseSamples ?? {}).some(
|
|
267
|
+
(sample) => sample.metrics.score !== undefined,
|
|
268
|
+
)),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const completeScoreEvidence = (entry: ExperimentEvaluation): boolean =>
|
|
272
|
+
entry.metrics.score !== undefined &&
|
|
273
|
+
(!entry.caseSamples ||
|
|
274
|
+
Object.keys(entry.casePassRates).every((id) => {
|
|
275
|
+
const sample = entry.caseSamples?.[id];
|
|
276
|
+
return sample?.metrics.score?.n === entry.repetitions && Boolean(sample.scoreBasis);
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
const comparableScoreBasis = (
|
|
280
|
+
entry: ExperimentEvaluation,
|
|
281
|
+
reference: ExperimentEvaluation | undefined,
|
|
282
|
+
): boolean => {
|
|
283
|
+
if (!reference || !completeScoreEvidence(reference)) return false;
|
|
284
|
+
if (!entry.caseSamples || !reference.caseSamples)
|
|
285
|
+
return !entry.caseSamples && !reference.caseSamples;
|
|
286
|
+
const ids = new Set([
|
|
287
|
+
...Object.keys(entry.casePassRates),
|
|
288
|
+
...Object.keys(reference.casePassRates),
|
|
289
|
+
]);
|
|
290
|
+
return [...ids].every((id) => {
|
|
291
|
+
const basis = entry.caseSamples?.[id]?.scoreBasis;
|
|
292
|
+
return Boolean(basis) && basis === reference.caseSamples?.[id]?.scoreBasis;
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
|
|
255
296
|
export const rankCandidates = <T extends ExperimentDecisionCandidate>(
|
|
256
297
|
campaign: ExperimentCampaign,
|
|
257
298
|
candidates: readonly T[],
|
|
258
299
|
experiment: OpenedExperimentRun["manifest"],
|
|
259
300
|
constraintSplits: readonly ExperimentSplit[] = ["train", "validation", "test"],
|
|
260
|
-
): T[] =>
|
|
261
|
-
|
|
301
|
+
): T[] => {
|
|
302
|
+
const validationEvaluations = (id: string): ExperimentEvaluation[] =>
|
|
303
|
+
candidateEvaluations(experiment, id).filter((entry) => entry.split === "validation");
|
|
304
|
+
const referenceId = ["baseline", ...candidates.map((candidate) => candidate.id).sort()].find(
|
|
305
|
+
(id) => {
|
|
306
|
+
const entries = validationEvaluations(id);
|
|
307
|
+
return campaign.profiles.every((profile) =>
|
|
308
|
+
entries.some((entry) => entry.profileId === profile && completeScoreEvidence(entry)),
|
|
309
|
+
);
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
const reference = referenceId ? validationEvaluations(referenceId) : [];
|
|
313
|
+
return [...candidates].sort((a, b) => {
|
|
262
314
|
const activeConstraints = campaign.objectives.constraints.filter((constraint) =>
|
|
263
315
|
constraintSplits.includes(constraint.split),
|
|
264
316
|
);
|
|
@@ -310,20 +362,40 @@ export const rankCandidates = <T extends ExperimentDecisionCandidate>(
|
|
|
310
362
|
}
|
|
311
363
|
}
|
|
312
364
|
const quality = (candidate: ExperimentDecisionCandidate): number => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
365
|
+
if (!hasScoreEvidence(experiment, "validation")) {
|
|
366
|
+
const values = metricValues(
|
|
367
|
+
candidate,
|
|
368
|
+
"quality.passRate",
|
|
369
|
+
undefined,
|
|
370
|
+
"validation",
|
|
371
|
+
experiment,
|
|
372
|
+
);
|
|
373
|
+
return values.length > 0 ? Math.min(...values) : -1;
|
|
374
|
+
}
|
|
375
|
+
const evaluations = candidateEvaluations(experiment, candidate.id);
|
|
376
|
+
const scores = campaign.profiles.map((profile) => {
|
|
377
|
+
const entry = evaluations.find(
|
|
378
|
+
(value) => value.profileId === profile && value.split === "validation",
|
|
379
|
+
);
|
|
380
|
+
if (
|
|
381
|
+
!entry ||
|
|
382
|
+
!completeScoreEvidence(entry) ||
|
|
383
|
+
!comparableScoreBasis(
|
|
384
|
+
entry,
|
|
385
|
+
reference.find((value) => value.profileId === profile),
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
return -1;
|
|
389
|
+
return entry.metrics.score ?? -1;
|
|
390
|
+
});
|
|
391
|
+
return scores.length > 0 ? Math.min(...scores) : -1;
|
|
321
392
|
};
|
|
322
393
|
const qualityDelta = quality(b) - quality(a);
|
|
323
394
|
if (qualityDelta !== 0) return qualityDelta;
|
|
324
395
|
if (a.sourceBytes !== b.sourceBytes) return a.sourceBytes - b.sourceBytes;
|
|
325
396
|
return a.id.localeCompare(b.id);
|
|
326
397
|
});
|
|
398
|
+
};
|
|
327
399
|
|
|
328
400
|
const candidateCaseVector = (
|
|
329
401
|
candidate: Candidate,
|
|
@@ -338,10 +410,15 @@ const candidateCaseVector = (
|
|
|
338
410
|
evaluations
|
|
339
411
|
.filter((entry) => entry.split === split)
|
|
340
412
|
.flatMap((entry) =>
|
|
341
|
-
Object.entries(entry.casePassRates).
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
413
|
+
Object.entries(entry.casePassRates).flatMap(([caseId, passRate]) => {
|
|
414
|
+
if (!hasScoreEvidence(experiment, split))
|
|
415
|
+
return [[`${entry.profileId}:${caseId}`, passRate]];
|
|
416
|
+
const sample = entry.caseSamples?.[caseId];
|
|
417
|
+
const score = sample?.metrics.score;
|
|
418
|
+
return score?.n === entry.repetitions && sample?.scoreBasis
|
|
419
|
+
? [[JSON.stringify([entry.profileId, caseId, sample.scoreBasis]), score.mean]]
|
|
420
|
+
: [];
|
|
421
|
+
}),
|
|
345
422
|
),
|
|
346
423
|
);
|
|
347
424
|
};
|
|
@@ -351,8 +428,9 @@ const dominates = (a: Record<string, number>, b: Record<string, number>): boolea
|
|
|
351
428
|
if (keys.size === 0) return false;
|
|
352
429
|
let strictlyBetter = false;
|
|
353
430
|
for (const key of keys) {
|
|
354
|
-
const av = a[key]
|
|
355
|
-
const bv = b[key]
|
|
431
|
+
const av = a[key];
|
|
432
|
+
const bv = b[key];
|
|
433
|
+
if (av === undefined || bv === undefined) return false;
|
|
356
434
|
if (av < bv) return false;
|
|
357
435
|
if (av > bv) strictlyBetter = true;
|
|
358
436
|
}
|
|
@@ -404,8 +482,13 @@ const frontierCoverage = (
|
|
|
404
482
|
const candidateVector = candidateCaseVector(candidate, experiment, "train");
|
|
405
483
|
const keys = new Set(vectors.flatMap((vector) => Object.keys(vector)));
|
|
406
484
|
return [...keys].filter((key) => {
|
|
407
|
-
const
|
|
408
|
-
|
|
485
|
+
const values = vectors.flatMap((vector) => (vector[key] === undefined ? [] : [vector[key]]));
|
|
486
|
+
const best = Math.max(...values);
|
|
487
|
+
return (
|
|
488
|
+
(frontier.length === 1 || values.length > 1) &&
|
|
489
|
+
candidateVector[key] !== undefined &&
|
|
490
|
+
candidateVector[key] === best
|
|
491
|
+
);
|
|
409
492
|
}).length;
|
|
410
493
|
};
|
|
411
494
|
|
|
@@ -131,6 +131,17 @@ const assertionPassed = (result: AssertionResult, threshold: number): boolean =>
|
|
|
131
131
|
result.passed &&
|
|
132
132
|
result.score >= threshold;
|
|
133
133
|
|
|
134
|
+
export const gradingScoreBasis = (report: GradingReport | null): string | null =>
|
|
135
|
+
report?.caseScore === null || !report
|
|
136
|
+
? null
|
|
137
|
+
: valueHash(
|
|
138
|
+
report.assertions
|
|
139
|
+
.map((entry, index) => ({ entry, id: entry.id ?? `${entry.type}:${index + 1}` }))
|
|
140
|
+
.filter(({ entry }) => entry.errorMessage === undefined && entry.abstention === undefined)
|
|
141
|
+
.map(({ entry, id }) => ({ id, weight: entry.weight }))
|
|
142
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
143
|
+
);
|
|
144
|
+
|
|
134
145
|
const aggregateGrading = (
|
|
135
146
|
entries: GradingAssertionEntry[],
|
|
136
147
|
skippedAssertions: NonNullable<GradingReport["skippedAssertions"]>,
|
|
@@ -162,9 +162,7 @@ export const formatRunsTable = (runs: ListedRun[]): string => {
|
|
|
162
162
|
else if (r.casePassed === true) status = "PASS";
|
|
163
163
|
else if (r.casePassed === false) status = "FAIL";
|
|
164
164
|
const score =
|
|
165
|
-
r.caseScore !== null
|
|
166
|
-
? `${(r.caseScore * 100).toFixed(0)}% (${r.assertionPassed}/${r.assertionTotal})`
|
|
167
|
-
: "—";
|
|
165
|
+
r.caseScore !== null ? `${r.caseScore} (${r.assertionPassed}/${r.assertionTotal})` : "—";
|
|
168
166
|
const duration = r.durationMs !== null ? `${(r.durationMs / 1000).toFixed(1)}s` : "—";
|
|
169
167
|
const agents = r.subagentCount > 0 ? `1+${r.subagentCount}` : "1";
|
|
170
168
|
return [status, r.caseSlug, r.driverName, agents, r.ts, r.exitReason ?? "?", score, duration];
|
|
@@ -362,8 +360,7 @@ const appendGradingDetail = (lines: string[], detail: RunDetail): void => {
|
|
|
362
360
|
let status = "ungraded";
|
|
363
361
|
if (detail.grading.casePassed === true) status = "PASS";
|
|
364
362
|
else if (detail.grading.casePassed === false) status = "FAIL";
|
|
365
|
-
const score =
|
|
366
|
-
detail.grading.caseScore !== null ? `${(detail.grading.caseScore * 100).toFixed(0)}%` : "—";
|
|
363
|
+
const score = detail.grading.caseScore !== null ? String(detail.grading.caseScore) : "—";
|
|
367
364
|
lines.push(
|
|
368
365
|
`grading: ${status} (${score}, ${detail.grading.assertions.filter((a) => a.passed).length}/${detail.grading.assertions.filter((a) => a.passed !== null).length})`,
|
|
369
366
|
);
|
|
@@ -373,7 +370,9 @@ const appendGradingDetail = (lines: string[], detail: RunDetail): void => {
|
|
|
373
370
|
if (a.abstention) mark = "?";
|
|
374
371
|
const took = ` (${(a.durationMs / 1000).toFixed(1)}s)`;
|
|
375
372
|
const abstention = a.abstention ? " — insufficient evidence" : "";
|
|
376
|
-
lines.push(
|
|
373
|
+
lines.push(
|
|
374
|
+
` ${mark} ${a.name} [score ${a.errorMessage !== undefined || a.abstention ? "unavailable" : a.score}]${took}${abstention}${a.comment ? ` — ${a.comment}` : ""}`,
|
|
375
|
+
);
|
|
377
376
|
}
|
|
378
377
|
}
|
|
379
378
|
};
|
|
@@ -24,6 +24,7 @@ export const clusterCaseSamples = (
|
|
|
24
24
|
}
|
|
25
25
|
if (current.identityHash !== sample.identityHash) current.identityHash = null;
|
|
26
26
|
if (current.clusterId !== sample.clusterId) current.identityHash = null;
|
|
27
|
+
if (current.scoreBasis !== sample.scoreBasis) current.scoreBasis = null;
|
|
27
28
|
const metrics = new Map(Object.entries(current.metrics));
|
|
28
29
|
for (const [metric, value] of Object.entries(sample.metrics)) {
|
|
29
30
|
const before = metrics.get(metric);
|
|
@@ -65,7 +66,9 @@ export const groupedCaseMetricSamples = (
|
|
|
65
66
|
if (
|
|
66
67
|
Object.values(samples).some(
|
|
67
68
|
(sample) =>
|
|
68
|
-
!Object.hasOwn(sample.metrics, metric) ||
|
|
69
|
+
!Object.hasOwn(sample.metrics, metric) ||
|
|
70
|
+
sample.metrics[metric]?.n !== repetitions ||
|
|
71
|
+
(metric === "score" && !sample.scoreBasis),
|
|
69
72
|
)
|
|
70
73
|
)
|
|
71
74
|
return [];
|
|
@@ -132,6 +135,8 @@ const pairedDeltas = (
|
|
|
132
135
|
const b = after.caseSamples?.[id];
|
|
133
136
|
if (!a?.identityHash || a.identityHash !== b?.identityHash)
|
|
134
137
|
throw new Error(`paired case '${id}' has incompatible evaluation identity`);
|
|
138
|
+
if (metric === "score" && (!a.scoreBasis || a.scoreBasis !== b.scoreBasis))
|
|
139
|
+
throw new Error(`paired score for case '${id}' has different or unavailable scoring basis`);
|
|
135
140
|
if (a.clusterId !== b.clusterId)
|
|
136
141
|
throw new Error(`paired case '${id}' has incompatible cluster membership`);
|
|
137
142
|
const av = Object.hasOwn(a.metrics, metric) ? a.metrics[metric] : undefined;
|
|
@@ -74,10 +74,19 @@ const pairedMetricDeltas = (
|
|
|
74
74
|
const a = before[index];
|
|
75
75
|
const b = after[index];
|
|
76
76
|
if (!a || !b) continue;
|
|
77
|
-
const
|
|
77
|
+
const am: Record<string, number> = {
|
|
78
|
+
...a.metrics,
|
|
79
|
+
...(a.score !== undefined ? { score: a.score } : {}),
|
|
80
|
+
};
|
|
81
|
+
const bm: Record<string, number> = {
|
|
82
|
+
...b.metrics,
|
|
83
|
+
...(b.score !== undefined ? { score: b.score } : {}),
|
|
84
|
+
};
|
|
85
|
+
const names = new Set([...Object.keys(am), ...Object.keys(bm)]);
|
|
78
86
|
for (const name of names) {
|
|
79
|
-
|
|
80
|
-
const
|
|
87
|
+
if (name === "score" && (!a.scoreBasis || a.scoreBasis !== b.scoreBasis)) continue;
|
|
88
|
+
const av = am[name];
|
|
89
|
+
const bv = bm[name];
|
|
81
90
|
if (av === undefined || bv === undefined) continue;
|
|
82
91
|
const values = deltas.get(name) ?? [];
|
|
83
92
|
values.push(bv - av);
|
|
@@ -196,9 +205,14 @@ export const formatSuiteComparisonText = (comparison: SuiteComparison): string =
|
|
|
196
205
|
}
|
|
197
206
|
}
|
|
198
207
|
for (const entry of comparison.cases) {
|
|
199
|
-
if (
|
|
208
|
+
if (entry.regressed) {
|
|
209
|
+
lines.push(
|
|
210
|
+
` ↓ ${suiteGroupKey(entry)}: ${entry.before ? percent(entry.before.passRate) : "—"} → ${entry.after ? percent(entry.after.passRate) : "missing"}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const score = entry.pairedMetricDeltas.score;
|
|
200
214
|
lines.push(
|
|
201
|
-
`
|
|
215
|
+
` ${suiteGroupKey(entry)} score change: ${score ? `${score.mean >= 0 ? "+" : ""}${score.mean} (${score.n} paired)` : "unavailable"}`,
|
|
202
216
|
);
|
|
203
217
|
}
|
|
204
218
|
return lines.join("\n");
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { hasAgentUsage } from "../core/usage.js";
|
|
12
12
|
import { runComparisonIdentity } from "./comparison-identity.js";
|
|
13
13
|
import { safeReadJson } from "./fs-read.js";
|
|
14
|
+
import { gradingScoreBasis } from "./grading.js";
|
|
14
15
|
import type { SuiteCaseResult } from "./orchestrator.js";
|
|
15
16
|
import type { SuiteRun } from "./suite-runs.js";
|
|
16
17
|
|
|
@@ -65,6 +66,7 @@ export const SuiteSampleSchema = z.object({
|
|
|
65
66
|
passed: z.boolean().nullable(),
|
|
66
67
|
identityHash: z.string().nullable(),
|
|
67
68
|
score: z.exactOptional(z.number()),
|
|
69
|
+
scoreBasis: z.exactOptional(z.string().nullable()),
|
|
68
70
|
metrics: z.record(z.string(), z.number()),
|
|
69
71
|
});
|
|
70
72
|
export type SuiteSample = z.infer<typeof SuiteSampleSchema>;
|
|
@@ -354,7 +356,10 @@ export const buildSuiteSummary = (
|
|
|
354
356
|
samples: entries.map((entry, index) => {
|
|
355
357
|
const samples: MetricSamples = new Map();
|
|
356
358
|
const result = entry.result?.result;
|
|
357
|
-
if (result)
|
|
359
|
+
if (result) {
|
|
360
|
+
add(samples, "score", result.grading.caseScore ?? undefined);
|
|
361
|
+
collectMetadata(samples, result.metadata);
|
|
362
|
+
}
|
|
358
363
|
return {
|
|
359
364
|
index,
|
|
360
365
|
caseId: entry.item.caseId,
|
|
@@ -364,7 +369,7 @@ export const buildSuiteSummary = (
|
|
|
364
369
|
passed: entry.result?.casePassed ?? null,
|
|
365
370
|
identityHash: result ? runComparisonIdentity(result.metadata) : null,
|
|
366
371
|
...(result?.grading.caseScore !== null && result?.grading.caseScore !== undefined
|
|
367
|
-
? { score: result.grading.caseScore }
|
|
372
|
+
? { score: result.grading.caseScore, scoreBasis: gradingScoreBasis(result.grading) }
|
|
368
373
|
: {}),
|
|
369
374
|
metrics: Object.fromEntries(
|
|
370
375
|
[...samples.entries()].map(([name, sample]) => [name, sample.values[0] ?? 0]),
|
package/src/previewer/server.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type DriverMetricDescriptor, type DriverMetrics, exec } from "../core/i
|
|
|
7
7
|
import {
|
|
8
8
|
compareRuns,
|
|
9
9
|
compareSuiteRuns,
|
|
10
|
+
hasRegression,
|
|
10
11
|
type ListedRun,
|
|
11
12
|
listExperimentRuns,
|
|
12
13
|
listRuns,
|
|
@@ -424,6 +425,7 @@ const handleRunComparison = async ({
|
|
|
424
425
|
]);
|
|
425
426
|
sendJson(request, response, 200, {
|
|
426
427
|
kind: "run",
|
|
428
|
+
regressed: hasRegression(comparison),
|
|
427
429
|
comparison: {
|
|
428
430
|
...comparison,
|
|
429
431
|
pathA: displayPath(runsRoot, before.runDir),
|
package/viewer/app.css
CHANGED
|
@@ -1256,6 +1256,37 @@ h1 {
|
|
|
1256
1256
|
color: var(--muted);
|
|
1257
1257
|
}
|
|
1258
1258
|
|
|
1259
|
+
.score-bar {
|
|
1260
|
+
position: relative;
|
|
1261
|
+
display: block;
|
|
1262
|
+
width: 100%;
|
|
1263
|
+
min-width: 92px;
|
|
1264
|
+
height: 9px;
|
|
1265
|
+
background: #282d30;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.score-fill {
|
|
1269
|
+
display: block;
|
|
1270
|
+
height: 100%;
|
|
1271
|
+
background: var(--muted);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
.score-fill.tone-positive,
|
|
1275
|
+
.score-fill.tone-after {
|
|
1276
|
+
background: var(--lime);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
.score-fill.tone-negative {
|
|
1280
|
+
background: var(--coral);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
.score-bar > .has-threshold {
|
|
1284
|
+
position: absolute;
|
|
1285
|
+
top: -2px;
|
|
1286
|
+
height: 13px;
|
|
1287
|
+
border-left: 1px solid currentColor;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1259
1290
|
.quantized-bar {
|
|
1260
1291
|
display: grid;
|
|
1261
1292
|
grid-template-columns: repeat(20, minmax(2px, 1fr));
|
|
@@ -1405,6 +1436,7 @@ h1 {
|
|
|
1405
1436
|
text-transform: uppercase;
|
|
1406
1437
|
}
|
|
1407
1438
|
|
|
1439
|
+
.assertion-content > .score-bar,
|
|
1408
1440
|
.assertion-content > .quantized-bar {
|
|
1409
1441
|
max-width: 220px;
|
|
1410
1442
|
margin-top: 8px;
|
package/viewer/charts.js
CHANGED
|
@@ -51,28 +51,40 @@ const distributionBar = (segments, label) => {
|
|
|
51
51
|
</div>`;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
export const scoreBar = (value, tone, label, threshold = null) => {
|
|
55
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
56
|
+
return `<span class="score-bar unavailable" role="img" aria-label="${escapeHtml(label)}"></span>`;
|
|
57
|
+
}
|
|
58
|
+
return `<span class="score-bar" role="img" aria-label="${escapeHtml(label)}">
|
|
59
|
+
<i class="score-fill ${escapeHtml(tone)}" style="width: ${value * 100}%"></i>
|
|
60
|
+
${typeof threshold === "number" ? `<i class="has-threshold" style="left: ${threshold * 100}%"></i>` : ""}
|
|
61
|
+
</span>`;
|
|
62
|
+
};
|
|
63
|
+
|
|
54
64
|
export const comparisonBars = ({
|
|
55
65
|
title,
|
|
56
66
|
before,
|
|
57
67
|
after,
|
|
58
68
|
maximum,
|
|
69
|
+
continuous = false,
|
|
59
70
|
beforeText,
|
|
60
71
|
afterText,
|
|
61
72
|
deltaText,
|
|
62
73
|
deltaTone,
|
|
63
74
|
}) => {
|
|
75
|
+
const bar = continuous ? scoreBar : quantizedBar;
|
|
64
76
|
const scale = Math.max(maximum ?? 0, before ?? 0, after ?? 0, 1);
|
|
65
77
|
return `<figure class="comparison-bars">
|
|
66
78
|
<figcaption><span>${escapeHtml(title)}</span><b class="${escapeHtml(
|
|
67
79
|
deltaTone,
|
|
68
80
|
)}">${escapeHtml(deltaText)}</b></figcaption>
|
|
69
|
-
<div class="bar-lane"><strong>A</strong>${
|
|
70
|
-
|
|
81
|
+
<div class="bar-lane"><strong>A</strong>${bar(
|
|
82
|
+
before === null || before === undefined ? null : before / scale,
|
|
71
83
|
"tone-before",
|
|
72
84
|
`${title} baseline ${beforeText}`,
|
|
73
85
|
)}<span>${escapeHtml(beforeText)}</span></div>
|
|
74
|
-
<div class="bar-lane"><strong>B</strong>${
|
|
75
|
-
|
|
86
|
+
<div class="bar-lane"><strong>B</strong>${bar(
|
|
87
|
+
after === null || after === undefined ? null : after / scale,
|
|
76
88
|
"tone-after",
|
|
77
89
|
`${title} candidate ${afterText}`,
|
|
78
90
|
)}<span>${escapeHtml(afterText)}</span></div>
|