@prismatic-io/lux 0.0.2-preview.32 → 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 (72) hide show
  1. package/lib/cli/output-schemas.d.ts +6 -2
  2. package/lib/cli/output-schemas.d.ts.map +1 -1
  3. package/lib/cli/output-schemas.js +5 -2
  4. package/lib/cli/output-schemas.js.map +1 -1
  5. package/lib/cli/render/format.d.ts.map +1 -1
  6. package/lib/cli/render/format.js +1 -1
  7. package/lib/cli/render/format.js.map +1 -1
  8. package/lib/cli/render/reporter.js +2 -2
  9. package/lib/cli/render/reporter.js.map +1 -1
  10. package/lib/core/experiment.d.ts +1 -0
  11. package/lib/core/experiment.d.ts.map +1 -1
  12. package/lib/core/experiment.js +5 -1
  13. package/lib/core/experiment.js.map +1 -1
  14. package/lib/orchestrator/compare.d.ts +4 -3
  15. package/lib/orchestrator/compare.d.ts.map +1 -1
  16. package/lib/orchestrator/compare.js +13 -6
  17. package/lib/orchestrator/compare.js.map +1 -1
  18. package/lib/orchestrator/experiment-contracts.d.ts +1 -0
  19. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
  20. package/lib/orchestrator/experiment-effects.d.ts.map +1 -1
  21. package/lib/orchestrator/experiment-effects.js +4 -2
  22. package/lib/orchestrator/experiment-effects.js.map +1 -1
  23. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  24. package/lib/orchestrator/experiment-evaluation.js +15 -2
  25. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  26. package/lib/orchestrator/experiment-report.d.ts +1 -0
  27. package/lib/orchestrator/experiment-report.d.ts.map +1 -1
  28. package/lib/orchestrator/experiment-runs.d.ts +2 -0
  29. package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
  30. package/lib/orchestrator/experiment-selection.d.ts.map +1 -1
  31. package/lib/orchestrator/experiment-selection.js +99 -43
  32. package/lib/orchestrator/experiment-selection.js.map +1 -1
  33. package/lib/orchestrator/grading.d.ts +1 -0
  34. package/lib/orchestrator/grading.d.ts.map +1 -1
  35. package/lib/orchestrator/grading.js +7 -0
  36. package/lib/orchestrator/grading.js.map +1 -1
  37. package/lib/orchestrator/list-runs.d.ts.map +1 -1
  38. package/lib/orchestrator/list-runs.js +3 -5
  39. package/lib/orchestrator/list-runs.js.map +1 -1
  40. package/lib/orchestrator/paired-effects.d.ts.map +1 -1
  41. package/lib/orchestrator/paired-effects.js +7 -1
  42. package/lib/orchestrator/paired-effects.js.map +1 -1
  43. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  44. package/lib/orchestrator/suite-compare.js +18 -6
  45. package/lib/orchestrator/suite-compare.js.map +1 -1
  46. package/lib/orchestrator/suite-summary.d.ts +2 -0
  47. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  48. package/lib/orchestrator/suite-summary.js +6 -2
  49. package/lib/orchestrator/suite-summary.js.map +1 -1
  50. package/lib/previewer/server.d.ts.map +1 -1
  51. package/lib/previewer/server.js +2 -1
  52. package/lib/previewer/server.js.map +1 -1
  53. package/package.json +1 -1
  54. package/skills/lux-answerer/SKILL.md +1 -1
  55. package/src/cli/output-schemas.ts +5 -2
  56. package/src/cli/render/format.ts +1 -1
  57. package/src/cli/render/reporter.ts +2 -2
  58. package/src/core/experiment.ts +7 -1
  59. package/src/orchestrator/compare.ts +23 -9
  60. package/src/orchestrator/experiment-effects.ts +4 -2
  61. package/src/orchestrator/experiment-evaluation.ts +18 -2
  62. package/src/orchestrator/experiment-selection.ts +101 -18
  63. package/src/orchestrator/grading.ts +11 -0
  64. package/src/orchestrator/list-runs.ts +5 -6
  65. package/src/orchestrator/paired-effects.ts +6 -1
  66. package/src/orchestrator/suite-compare.ts +19 -5
  67. package/src/orchestrator/suite-summary.ts +7 -2
  68. package/src/previewer/server.ts +2 -0
  69. package/viewer/app.css +32 -0
  70. package/viewer/charts.js +16 -4
  71. package/viewer/detail.js +26 -8
  72. package/viewer/format.js +2 -2
@@ -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
@@ -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>${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,10 +1,16 @@
1
+ const assertionScore = (assertion) =>
2
+ assertion.errorMessage !== undefined || assertion.abstention !== undefined
3
+ ? null
4
+ : assertion.score;
5
+
1
6
  const assertionPresentation = (assertion) => {
7
+ if (assertionScore(assertion) === null) return { className: " unavailable", mark: "?", tone: "" };
2
8
  if (assertion.passed === null) return { className: " informational", mark: "ℹ", tone: "" };
3
9
  if (assertion.passed) return { className: " passed", mark: "✓", tone: "tone-positive" };
4
10
  return { className: "", mark: "×", tone: "tone-negative" };
5
11
  };
6
12
 
7
- import { comparisonBars, compositionFigure, quantizedBar } from "./charts.js";
13
+ import { comparisonBars, compositionFigure, quantizedBar, scoreBar } from "./charts.js";
8
14
  import {
9
15
  badge,
10
16
  compactNumber,
@@ -104,6 +110,7 @@ const assertionMeta = (assertion) =>
104
110
  const assertionDiagnostics = (assertion) => {
105
111
  const rows = [
106
112
  assertion.comment ? `<p class="assertion-comment">${escapeHtml(assertion.comment)}</p>` : "",
113
+ assertion.abstention ? `<p class="assertion-comment">Insufficient evidence</p>` : "",
107
114
  assertion.errorMessage
108
115
  ? `<p class="diagnostic-error">${escapeHtml(assertion.errorMessage)}</p>`
109
116
  : "",
@@ -118,6 +125,7 @@ const assertionDiagnostics = (assertion) => {
118
125
 
119
126
  const inferredMetricUnit = (name, unit) => {
120
127
  if (unit) return unit;
128
+ if (name === "score") return "score";
121
129
  if (name.endsWith("Ms")) return "milliseconds";
122
130
  if (name.endsWith(".bytes")) return "bytes";
123
131
  if (name.endsWith(".usd")) return "usd";
@@ -323,18 +331,18 @@ export const renderRunDetail = (item, detail) => {
323
331
  <span class="assertion-content">
324
332
  <strong>${escapeHtml(assertion.name)}</strong>
325
333
  <small>${escapeHtml(assertionMeta(assertion))}</small>
326
- ${quantizedBar(
327
- assertion.score,
334
+ ${scoreBar(
335
+ assertionScore(assertion),
328
336
  assertionPresentation(assertion).tone,
329
337
  `${assertion.name} scored ${percentage(
330
- assertion.score,
338
+ assertionScore(assertion),
331
339
  )}${assertion.passed === null ? " (informational)" : `; threshold ${percentage(assertion.threshold)}`}`,
332
340
  assertion.passed === null ? null : assertion.threshold,
333
341
  )}
334
342
  ${assertionDiagnostics(assertion)}
335
343
  </span>
336
344
  <span class="assertion-score">${escapeHtml(
337
- percentage(assertion.score),
345
+ percentage(assertionScore(assertion)),
338
346
  )}</span>
339
347
  </article>`,
340
348
  )
@@ -899,18 +907,20 @@ export const renderRunComparison = (payload, labels) => {
899
907
  runDelta?.agentUsageAvailable?.b === false ? null : totalTokens(runDelta?.usage?.b?.agent);
900
908
  const tokenDelta = tokenA === null || tokenB === null ? null : tokenB - tokenA;
901
909
  const scoreDelta =
910
+ comparison.grading.caseScoreComparable !== false &&
902
911
  typeof comparison.grading.caseScoreA === "number" &&
903
912
  typeof comparison.grading.caseScoreB === "number"
904
913
  ? comparison.grading.caseScoreB - comparison.grading.caseScoreA
905
914
  : null;
915
+ let comparisonStatus = "ungraded";
916
+ if (payload.regressed === true) comparisonStatus = "failed";
917
+ if (payload.regressed === false) comparisonStatus = "passed";
906
918
  const driverMetrics = driverMetricComparisons(runDelta);
907
919
  const subagentsA = subagentTotals(runDelta?.agentActivity?.a);
908
920
  const subagentsB = subagentTotals(runDelta?.agentActivity?.b);
909
921
  return `
910
922
  <header class="detail-header comparison-header">
911
- <div class="detail-kicker">${badge(
912
- comparison.grading.regressions ? "failed" : "passed",
913
- )}<span>run comparison</span></div>
923
+ <div class="detail-kicker">${badge(comparisonStatus)}<span>run comparison</span></div>
914
924
  <div class="detail-title-row">
915
925
  <div><h2>${escapeHtml(labels.before)} → ${escapeHtml(
916
926
  labels.after,
@@ -926,6 +936,7 @@ export const renderRunComparison = (payload, labels) => {
926
936
  )}. Treat deltas as potentially incomparable.</div>`
927
937
  : '<div class="integrity-banner">✓ Case, evaluator, runtime, and driver identities are comparable</div>'
928
938
  }
939
+ ${comparison.grading.caseScoreComparable === false ? '<div class="attention-banner">Score change unavailable: scored assertions or weights differ, or evidence is missing.</div>' : ""}
929
940
  <dl class="comparison-stats">
930
941
  <div><dt>Case score</dt><dd>${escapeHtml(
931
942
  `${percentage(comparison.grading.caseScoreA)} → ${percentage(
@@ -956,6 +967,7 @@ export const renderRunComparison = (payload, labels) => {
956
967
  <div class="comparison-plot-grid">
957
968
  ${comparisonBars({
958
969
  title: "Case score",
970
+ continuous: true,
959
971
  before: comparison.grading.caseScoreA,
960
972
  after: comparison.grading.caseScoreB,
961
973
  maximum: 1,
@@ -1051,6 +1063,12 @@ export const renderRunComparison = (payload, labels) => {
1051
1063
  value: comparison.grading.deltas.filter((delta) => delta.status === "removed")
1052
1064
  .length,
1053
1065
  },
1066
+ {
1067
+ key: "unavailable",
1068
+ label: "Unavailable",
1069
+ value: comparison.grading.deltas.filter((delta) => delta.status === "unavailable")
1070
+ .length,
1071
+ },
1054
1072
  {
1055
1073
  key: "unchanged",
1056
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