@prismatic-io/lux 0.0.2-preview.30 → 0.0.2-preview.33

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.
Files changed (111) hide show
  1. package/lib/assertions/core/command-exits-zero.d.ts +1 -0
  2. package/lib/assertions/core/command-exits-zero.d.ts.map +1 -1
  3. package/lib/assertions/core/file-checks.d.ts +6 -0
  4. package/lib/assertions/core/file-checks.d.ts.map +1 -1
  5. package/lib/assertions/core/json-pointer-equals.d.ts +1 -0
  6. package/lib/assertions/core/json-pointer-equals.d.ts.map +1 -1
  7. package/lib/assertions/core/output-checks.d.ts +3 -0
  8. package/lib/assertions/core/output-checks.d.ts.map +1 -1
  9. package/lib/assertions/core/predicate.d.ts +1 -0
  10. package/lib/assertions/core/predicate.d.ts.map +1 -1
  11. package/lib/assertions/core/resource-checks.d.ts +6 -0
  12. package/lib/assertions/core/resource-checks.d.ts.map +1 -1
  13. package/lib/assertions/rubric/index.d.ts +2 -0
  14. package/lib/assertions/rubric/index.d.ts.map +1 -1
  15. package/lib/cli/output-schemas.d.ts +11 -5
  16. package/lib/cli/output-schemas.d.ts.map +1 -1
  17. package/lib/cli/output-schemas.js +5 -2
  18. package/lib/cli/output-schemas.js.map +1 -1
  19. package/lib/cli/program.d.ts.map +1 -1
  20. package/lib/cli/program.js +2 -1
  21. package/lib/cli/program.js.map +1 -1
  22. package/lib/cli/render/format.d.ts.map +1 -1
  23. package/lib/cli/render/format.js +1 -1
  24. package/lib/cli/render/format.js.map +1 -1
  25. package/lib/cli/render/reporter.d.ts.map +1 -1
  26. package/lib/cli/render/reporter.js +11 -4
  27. package/lib/cli/render/reporter.js.map +1 -1
  28. package/lib/core/assertion.d.ts +5 -2
  29. package/lib/core/assertion.d.ts.map +1 -1
  30. package/lib/core/assertion.js +3 -1
  31. package/lib/core/assertion.js.map +1 -1
  32. package/lib/core/case.d.ts +5 -1
  33. package/lib/core/case.d.ts.map +1 -1
  34. package/lib/core/case.js +1 -0
  35. package/lib/core/case.js.map +1 -1
  36. package/lib/core/experiment.d.ts +1 -0
  37. package/lib/core/experiment.d.ts.map +1 -1
  38. package/lib/core/experiment.js +5 -1
  39. package/lib/core/experiment.js.map +1 -1
  40. package/lib/core/rubric.d.ts +1 -0
  41. package/lib/core/rubric.d.ts.map +1 -1
  42. package/lib/core/run.d.ts +1 -0
  43. package/lib/core/run.d.ts.map +1 -1
  44. package/lib/orchestrator/compare.d.ts +6 -5
  45. package/lib/orchestrator/compare.d.ts.map +1 -1
  46. package/lib/orchestrator/compare.js +20 -11
  47. package/lib/orchestrator/compare.js.map +1 -1
  48. package/lib/orchestrator/config.d.ts +2 -0
  49. package/lib/orchestrator/config.d.ts.map +1 -1
  50. package/lib/orchestrator/experiment-contracts.d.ts +1 -0
  51. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
  52. package/lib/orchestrator/experiment-effects.d.ts.map +1 -1
  53. package/lib/orchestrator/experiment-effects.js +4 -2
  54. package/lib/orchestrator/experiment-effects.js.map +1 -1
  55. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  56. package/lib/orchestrator/experiment-evaluation.js +15 -2
  57. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  58. package/lib/orchestrator/experiment-report.d.ts +1 -0
  59. package/lib/orchestrator/experiment-report.d.ts.map +1 -1
  60. package/lib/orchestrator/experiment-runs.d.ts +2 -0
  61. package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
  62. package/lib/orchestrator/experiment-selection.d.ts.map +1 -1
  63. package/lib/orchestrator/experiment-selection.js +99 -43
  64. package/lib/orchestrator/experiment-selection.js.map +1 -1
  65. package/lib/orchestrator/grader-experiment.d.ts +4 -2
  66. package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
  67. package/lib/orchestrator/grading.d.ts +1 -0
  68. package/lib/orchestrator/grading.d.ts.map +1 -1
  69. package/lib/orchestrator/grading.js +34 -31
  70. package/lib/orchestrator/grading.js.map +1 -1
  71. package/lib/orchestrator/list-runs.d.ts.map +1 -1
  72. package/lib/orchestrator/list-runs.js +7 -7
  73. package/lib/orchestrator/list-runs.js.map +1 -1
  74. package/lib/orchestrator/paired-effects.d.ts.map +1 -1
  75. package/lib/orchestrator/paired-effects.js +7 -1
  76. package/lib/orchestrator/paired-effects.js.map +1 -1
  77. package/lib/orchestrator/regrade.d.ts +2 -1
  78. package/lib/orchestrator/regrade.d.ts.map +1 -1
  79. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  80. package/lib/orchestrator/suite-compare.js +18 -6
  81. package/lib/orchestrator/suite-compare.js.map +1 -1
  82. package/lib/orchestrator/suite-summary.d.ts +2 -0
  83. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  84. package/lib/orchestrator/suite-summary.js +6 -2
  85. package/lib/orchestrator/suite-summary.js.map +1 -1
  86. package/lib/previewer/server.d.ts.map +1 -1
  87. package/lib/previewer/server.js +2 -1
  88. package/lib/previewer/server.js.map +1 -1
  89. package/package.json +1 -1
  90. package/skills/lux-answerer/SKILL.md +1 -1
  91. package/src/cli/output-schemas.ts +5 -2
  92. package/src/cli/program.ts +3 -1
  93. package/src/cli/render/format.ts +1 -1
  94. package/src/cli/render/reporter.ts +10 -4
  95. package/src/core/assertion.ts +3 -1
  96. package/src/core/case.ts +5 -1
  97. package/src/core/experiment.ts +7 -1
  98. package/src/orchestrator/compare.ts +28 -12
  99. package/src/orchestrator/experiment-effects.ts +4 -2
  100. package/src/orchestrator/experiment-evaluation.ts +18 -2
  101. package/src/orchestrator/experiment-selection.ts +101 -18
  102. package/src/orchestrator/grading.ts +50 -28
  103. package/src/orchestrator/list-runs.ts +8 -8
  104. package/src/orchestrator/paired-effects.ts +6 -1
  105. package/src/orchestrator/suite-compare.ts +19 -5
  106. package/src/orchestrator/suite-summary.ts +7 -2
  107. package/src/previewer/server.ts +2 -0
  108. package/viewer/app.css +36 -0
  109. package/viewer/charts.js +16 -4
  110. package/viewer/detail.js +40 -16
  111. package/viewer/format.js +2 -2
@@ -93,7 +93,7 @@ const summarizeRun = async (
93
93
  casePassed: grading?.casePassed ?? null,
94
94
  caseScore: grading?.caseScore ?? null,
95
95
  assertionPassed: passing,
96
- assertionTotal: grading?.assertions.length ?? 0,
96
+ assertionTotal: grading?.assertions.filter((a) => a.passed !== null).length ?? 0,
97
97
  ...(readErrors.length > 0 ? { readErrors } : {}),
98
98
  };
99
99
  };
@@ -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,17 +360,19 @@ 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
- `grading: ${status} (${score}, ${detail.grading.assertions.filter((a) => a.passed).length}/${detail.grading.assertions.length})`,
365
+ `grading: ${status} (${score}, ${detail.grading.assertions.filter((a) => a.passed).length}/${detail.grading.assertions.filter((a) => a.passed !== null).length})`,
369
366
  );
370
367
  for (const a of detail.grading.assertions) {
371
368
  let mark = a.passed ? "✓" : "✗";
369
+ if (a.passed === null) mark = "ℹ informational";
372
370
  if (a.abstention) mark = "?";
373
371
  const took = ` (${(a.durationMs / 1000).toFixed(1)}s)`;
374
372
  const abstention = a.abstention ? " — insufficient evidence" : "";
375
- lines.push(` ${mark} ${a.name}${took}${abstention}${a.comment ? ` — ${a.comment}` : ""}`);
373
+ lines.push(
374
+ ` ${mark} ${a.name} [score ${a.errorMessage !== undefined || a.abstention ? "unavailable" : a.score}]${took}${abstention}${a.comment ? ` — ${a.comment}` : ""}`,
375
+ );
376
376
  }
377
377
  }
378
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) || sample.metrics[metric]?.n !== repetitions,
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 names = new Set([...Object.keys(a.metrics), ...Object.keys(b.metrics)]);
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
- const av = a.metrics[name];
80
- const bv = b.metrics[name];
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 (!entry.regressed) continue;
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
- ` ↓ ${suiteGroupKey(entry)}: ${entry.before ? percent(entry.before.passRate) : "—"} → ${entry.after ? percent(entry.after.passRate) : "missing"}`,
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) collectMetadata(samples, result.metadata);
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]),
@@ -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
@@ -993,6 +993,10 @@ h1 {
993
993
  monospace;
994
994
  }
995
995
 
996
+ .assertion.informational .assertion-mark {
997
+ color: var(--muted);
998
+ }
999
+
996
1000
  .assertion.passed .assertion-mark {
997
1001
  color: var(--lime);
998
1002
  }
@@ -1252,6 +1256,37 @@ h1 {
1252
1256
  color: var(--muted);
1253
1257
  }
1254
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
+
1255
1290
  .quantized-bar {
1256
1291
  display: grid;
1257
1292
  grid-template-columns: repeat(20, minmax(2px, 1fr));
@@ -1401,6 +1436,7 @@ h1 {
1401
1436
  text-transform: uppercase;
1402
1437
  }
1403
1438
 
1439
+ .assertion-content > .score-bar,
1404
1440
  .assertion-content > .quantized-bar {
1405
1441
  max-width: 220px;
1406
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>${quantizedBar(
70
- (before ?? 0) / scale,
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>${quantizedBar(
75
- (after ?? 0) / scale,
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>
package/viewer/detail.js CHANGED
@@ -1,4 +1,16 @@
1
- import { comparisonBars, compositionFigure, quantizedBar } from "./charts.js";
1
+ const assertionScore = (assertion) =>
2
+ assertion.errorMessage !== undefined || assertion.abstention !== undefined
3
+ ? null
4
+ : assertion.score;
5
+
6
+ const assertionPresentation = (assertion) => {
7
+ if (assertionScore(assertion) === null) return { className: " unavailable", mark: "?", tone: "" };
8
+ if (assertion.passed === null) return { className: " informational", mark: "ℹ", tone: "" };
9
+ if (assertion.passed) return { className: " passed", mark: "✓", tone: "tone-positive" };
10
+ return { className: "", mark: "×", tone: "tone-negative" };
11
+ };
12
+
13
+ import { comparisonBars, compositionFigure, quantizedBar, scoreBar } from "./charts.js";
2
14
  import {
3
15
  badge,
4
16
  compactNumber,
@@ -98,6 +110,7 @@ const assertionMeta = (assertion) =>
98
110
  const assertionDiagnostics = (assertion) => {
99
111
  const rows = [
100
112
  assertion.comment ? `<p class="assertion-comment">${escapeHtml(assertion.comment)}</p>` : "",
113
+ assertion.abstention ? `<p class="assertion-comment">Insufficient evidence</p>` : "",
101
114
  assertion.errorMessage
102
115
  ? `<p class="diagnostic-error">${escapeHtml(assertion.errorMessage)}</p>`
103
116
  : "",
@@ -112,6 +125,7 @@ const assertionDiagnostics = (assertion) => {
112
125
 
113
126
  const inferredMetricUnit = (name, unit) => {
114
127
  if (unit) return unit;
128
+ if (name === "score") return "score";
115
129
  if (name.endsWith("Ms")) return "milliseconds";
116
130
  if (name.endsWith(".bytes")) return "bytes";
117
131
  if (name.endsWith(".usd")) return "usd";
@@ -261,11 +275,11 @@ export const renderRunDetail = (item, detail) => {
261
275
  {
262
276
  key: "regressed",
263
277
  label: "Failed",
264
- value: assertions.filter((assertion) => !assertion.passed).length,
278
+ value: assertions.filter((assertion) => assertion.passed === false).length,
265
279
  },
266
280
  ],
267
281
  `${assertions.filter((assertion) => assertion.passed).length} of ${
268
- assertions.length
282
+ assertions.filter((assertion) => assertion.passed !== null).length
269
283
  } assertions passed`,
270
284
  )}
271
285
  ${
@@ -305,30 +319,30 @@ export const renderRunDetail = (item, detail) => {
305
319
  : ""
306
320
  }
307
321
  <section class="detail-section">
308
- <h3>Judgment <span>${assertions.filter((entry) => entry.passed).length}/${assertions.length} passed</span></h3>
322
+ <h3>Judgment <span>${assertions.filter((entry) => entry.passed).length}/${assertions.filter((entry) => entry.passed !== null).length} passed</span></h3>
309
323
  <div class="assertion-list">
310
324
  ${
311
325
  assertions.length
312
326
  ? assertions
313
327
  .map(
314
328
  (assertion) => `
315
- <article class="assertion${assertion.passed ? " passed" : ""}">
316
- <span class="assertion-mark">${assertion.passed ? "✓" : "×"}</span>
329
+ <article class="assertion${assertionPresentation(assertion).className}">
330
+ <span class="assertion-mark">${assertionPresentation(assertion).mark}</span>
317
331
  <span class="assertion-content">
318
332
  <strong>${escapeHtml(assertion.name)}</strong>
319
333
  <small>${escapeHtml(assertionMeta(assertion))}</small>
320
- ${quantizedBar(
321
- assertion.score,
322
- assertion.passed ? "tone-positive" : "tone-negative",
334
+ ${scoreBar(
335
+ assertionScore(assertion),
336
+ assertionPresentation(assertion).tone,
323
337
  `${assertion.name} scored ${percentage(
324
- assertion.score,
325
- )}; threshold ${percentage(assertion.threshold)}`,
326
- assertion.threshold,
338
+ assertionScore(assertion),
339
+ )}${assertion.passed === null ? " (informational)" : `; threshold ${percentage(assertion.threshold)}`}`,
340
+ assertion.passed === null ? null : assertion.threshold,
327
341
  )}
328
342
  ${assertionDiagnostics(assertion)}
329
343
  </span>
330
344
  <span class="assertion-score">${escapeHtml(
331
- percentage(assertion.score),
345
+ percentage(assertionScore(assertion)),
332
346
  )}</span>
333
347
  </article>`,
334
348
  )
@@ -893,18 +907,20 @@ export const renderRunComparison = (payload, labels) => {
893
907
  runDelta?.agentUsageAvailable?.b === false ? null : totalTokens(runDelta?.usage?.b?.agent);
894
908
  const tokenDelta = tokenA === null || tokenB === null ? null : tokenB - tokenA;
895
909
  const scoreDelta =
910
+ comparison.grading.caseScoreComparable !== false &&
896
911
  typeof comparison.grading.caseScoreA === "number" &&
897
912
  typeof comparison.grading.caseScoreB === "number"
898
913
  ? comparison.grading.caseScoreB - comparison.grading.caseScoreA
899
914
  : null;
915
+ let comparisonStatus = "ungraded";
916
+ if (payload.regressed === true) comparisonStatus = "failed";
917
+ if (payload.regressed === false) comparisonStatus = "passed";
900
918
  const driverMetrics = driverMetricComparisons(runDelta);
901
919
  const subagentsA = subagentTotals(runDelta?.agentActivity?.a);
902
920
  const subagentsB = subagentTotals(runDelta?.agentActivity?.b);
903
921
  return `
904
922
  <header class="detail-header comparison-header">
905
- <div class="detail-kicker">${badge(
906
- comparison.grading.regressions ? "failed" : "passed",
907
- )}<span>run comparison</span></div>
923
+ <div class="detail-kicker">${badge(comparisonStatus)}<span>run comparison</span></div>
908
924
  <div class="detail-title-row">
909
925
  <div><h2>${escapeHtml(labels.before)} → ${escapeHtml(
910
926
  labels.after,
@@ -920,6 +936,7 @@ export const renderRunComparison = (payload, labels) => {
920
936
  )}. Treat deltas as potentially incomparable.</div>`
921
937
  : '<div class="integrity-banner">✓ Case, evaluator, runtime, and driver identities are comparable</div>'
922
938
  }
939
+ ${comparison.grading.caseScoreComparable === false ? '<div class="attention-banner">Score change unavailable: scored assertions or weights differ, or evidence is missing.</div>' : ""}
923
940
  <dl class="comparison-stats">
924
941
  <div><dt>Case score</dt><dd>${escapeHtml(
925
942
  `${percentage(comparison.grading.caseScoreA)} → ${percentage(
@@ -950,6 +967,7 @@ export const renderRunComparison = (payload, labels) => {
950
967
  <div class="comparison-plot-grid">
951
968
  ${comparisonBars({
952
969
  title: "Case score",
970
+ continuous: true,
953
971
  before: comparison.grading.caseScoreA,
954
972
  after: comparison.grading.caseScoreB,
955
973
  maximum: 1,
@@ -1045,6 +1063,12 @@ export const renderRunComparison = (payload, labels) => {
1045
1063
  value: comparison.grading.deltas.filter((delta) => delta.status === "removed")
1046
1064
  .length,
1047
1065
  },
1066
+ {
1067
+ key: "unavailable",
1068
+ label: "Unavailable",
1069
+ value: comparison.grading.deltas.filter((delta) => delta.status === "unavailable")
1070
+ .length,
1071
+ },
1048
1072
  {
1049
1073
  key: "unchanged",
1050
1074
  label: "Unchanged",
package/viewer/format.js CHANGED
@@ -6,11 +6,11 @@ export const escapeHtml = (value) =>
6
6
  );
7
7
 
8
8
  export const percentage = (value) =>
9
- typeof value === "number" ? `${Math.round(value * 100)}%` : "—";
9
+ typeof value === "number" ? `${Number((value * 100).toPrecision(15))}%` : "—";
10
10
 
11
11
  export const signedPercentage = (value) => {
12
12
  if (typeof value !== "number") return "—";
13
- const rounded = Math.round(value * 1_000) / 10;
13
+ const rounded = Number((value * 100).toPrecision(15));
14
14
  return `${rounded > 0 ? "+" : ""}${rounded}%`;
15
15
  };
16
16