@prismatic-io/lux 0.0.1 → 0.0.2-preview.11

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 (256) hide show
  1. package/lib/answerers/persona/index.d.ts +2 -1
  2. package/lib/answerers/persona/index.d.ts.map +1 -1
  3. package/lib/answerers/persona/index.js +20 -5
  4. package/lib/answerers/persona/index.js.map +1 -1
  5. package/lib/answerers/terminal/index.js +1 -1
  6. package/lib/answerers/terminal/index.js.map +1 -1
  7. package/lib/assertions/authoring.d.ts +14 -0
  8. package/lib/assertions/authoring.d.ts.map +1 -0
  9. package/lib/assertions/authoring.js +3 -0
  10. package/lib/assertions/authoring.js.map +1 -0
  11. package/lib/assertions/core/helpers.d.ts +2 -7
  12. package/lib/assertions/core/helpers.d.ts.map +1 -1
  13. package/lib/assertions/core/helpers.js +2 -6
  14. package/lib/assertions/core/helpers.js.map +1 -1
  15. package/lib/assertions/core/index.d.ts.map +1 -1
  16. package/lib/assertions/core/index.js +2 -1
  17. package/lib/assertions/core/index.js.map +1 -1
  18. package/lib/assertions/core/resource-checks.d.ts +19 -0
  19. package/lib/assertions/core/resource-checks.d.ts.map +1 -1
  20. package/lib/assertions/core/resource-checks.js +32 -0
  21. package/lib/assertions/core/resource-checks.js.map +1 -1
  22. package/lib/authoring.d.ts +1 -0
  23. package/lib/authoring.d.ts.map +1 -1
  24. package/lib/authoring.js +1 -0
  25. package/lib/authoring.js.map +1 -1
  26. package/lib/cli/program.d.ts.map +1 -1
  27. package/lib/cli/program.js +12 -67
  28. package/lib/cli/program.js.map +1 -1
  29. package/lib/cli/render/reporter.d.ts.map +1 -1
  30. package/lib/cli/render/reporter.js +16 -3
  31. package/lib/cli/render/reporter.js.map +1 -1
  32. package/lib/cli/run-options.d.ts +1 -0
  33. package/lib/cli/run-options.d.ts.map +1 -1
  34. package/lib/cli/run-options.js.map +1 -1
  35. package/lib/cli/view.d.ts +13 -0
  36. package/lib/cli/view.d.ts.map +1 -0
  37. package/lib/cli/view.js +126 -0
  38. package/lib/cli/view.js.map +1 -0
  39. package/lib/core/action-events.d.ts +29 -0
  40. package/lib/core/action-events.d.ts.map +1 -0
  41. package/lib/core/action-events.js +51 -0
  42. package/lib/core/action-events.js.map +1 -0
  43. package/lib/core/agent-activity.d.ts +49 -0
  44. package/lib/core/agent-activity.d.ts.map +1 -0
  45. package/lib/core/agent-activity.js +206 -0
  46. package/lib/core/agent-activity.js.map +1 -0
  47. package/lib/core/agent-events.d.ts +26 -0
  48. package/lib/core/agent-events.d.ts.map +1 -0
  49. package/lib/core/agent-events.js +13 -0
  50. package/lib/core/agent-events.js.map +1 -0
  51. package/lib/core/artifact-evidence.d.ts +8 -0
  52. package/lib/core/artifact-evidence.d.ts.map +1 -1
  53. package/lib/core/artifact-evidence.js +36 -4
  54. package/lib/core/artifact-evidence.js.map +1 -1
  55. package/lib/core/assertion.d.ts +19 -9
  56. package/lib/core/assertion.d.ts.map +1 -1
  57. package/lib/core/assertion.js +26 -0
  58. package/lib/core/assertion.js.map +1 -1
  59. package/lib/core/case-authoring-state.d.ts +12 -0
  60. package/lib/core/case-authoring-state.d.ts.map +1 -0
  61. package/lib/core/case-authoring-state.js +43 -0
  62. package/lib/core/case-authoring-state.js.map +1 -0
  63. package/lib/core/case.d.ts +29 -1
  64. package/lib/core/case.d.ts.map +1 -1
  65. package/lib/core/case.js +14 -6
  66. package/lib/core/case.js.map +1 -1
  67. package/lib/core/driver-metrics.d.ts +98 -0
  68. package/lib/core/driver-metrics.d.ts.map +1 -0
  69. package/lib/core/driver-metrics.js +36 -0
  70. package/lib/core/driver-metrics.js.map +1 -0
  71. package/lib/core/driver.d.ts +3 -0
  72. package/lib/core/driver.d.ts.map +1 -1
  73. package/lib/core/driver.js.map +1 -1
  74. package/lib/core/events.d.ts +36 -0
  75. package/lib/core/events.d.ts.map +1 -1
  76. package/lib/core/events.js +12 -2
  77. package/lib/core/events.js.map +1 -1
  78. package/lib/core/experiment.d.ts +19 -0
  79. package/lib/core/experiment.d.ts.map +1 -1
  80. package/lib/core/experiment.js +2 -0
  81. package/lib/core/experiment.js.map +1 -1
  82. package/lib/core/index.d.ts +4 -0
  83. package/lib/core/index.d.ts.map +1 -1
  84. package/lib/core/index.js +4 -0
  85. package/lib/core/index.js.map +1 -1
  86. package/lib/core/lifecycle-fixtures.d.ts +3 -1
  87. package/lib/core/lifecycle-fixtures.d.ts.map +1 -1
  88. package/lib/core/lifecycle-fixtures.js +4 -2
  89. package/lib/core/lifecycle-fixtures.js.map +1 -1
  90. package/lib/core/lifecycle-hooks.d.ts +9 -0
  91. package/lib/core/lifecycle-hooks.d.ts.map +1 -1
  92. package/lib/core/lifecycle-hooks.js +8 -2
  93. package/lib/core/lifecycle-hooks.js.map +1 -1
  94. package/lib/core/run.d.ts +150 -0
  95. package/lib/core/run.d.ts.map +1 -1
  96. package/lib/core/run.js +9 -0
  97. package/lib/core/run.js.map +1 -1
  98. package/lib/core/wire.d.ts +12 -0
  99. package/lib/core/wire.d.ts.map +1 -1
  100. package/lib/core/wire.js +5 -1
  101. package/lib/core/wire.js.map +1 -1
  102. package/lib/drivers/claude-code/parse-events.d.ts.map +1 -1
  103. package/lib/drivers/claude-code/parse-events.js +73 -8
  104. package/lib/drivers/claude-code/parse-events.js.map +1 -1
  105. package/lib/drivers/codex/app-events.d.ts +9 -2
  106. package/lib/drivers/codex/app-events.d.ts.map +1 -1
  107. package/lib/drivers/codex/app-events.js +93 -12
  108. package/lib/drivers/codex/app-events.js.map +1 -1
  109. package/lib/drivers/codex/index.d.ts.map +1 -1
  110. package/lib/drivers/codex/index.js +10 -14
  111. package/lib/drivers/codex/index.js.map +1 -1
  112. package/lib/drivers/subprocess/index.d.ts.map +1 -1
  113. package/lib/drivers/subprocess/index.js +6 -0
  114. package/lib/drivers/subprocess/index.js.map +1 -1
  115. package/lib/index.d.ts +10 -6
  116. package/lib/index.d.ts.map +1 -1
  117. package/lib/index.js +10 -6
  118. package/lib/index.js.map +1 -1
  119. package/lib/orchestrator/annotation-store.d.ts.map +1 -1
  120. package/lib/orchestrator/annotation-store.js +6 -2
  121. package/lib/orchestrator/annotation-store.js.map +1 -1
  122. package/lib/orchestrator/case-extensions.d.ts +6 -0
  123. package/lib/orchestrator/case-extensions.d.ts.map +1 -0
  124. package/lib/orchestrator/case-extensions.js +28 -0
  125. package/lib/orchestrator/case-extensions.js.map +1 -0
  126. package/lib/orchestrator/compare.d.ts +13 -1
  127. package/lib/orchestrator/compare.d.ts.map +1 -1
  128. package/lib/orchestrator/compare.js +12 -0
  129. package/lib/orchestrator/compare.js.map +1 -1
  130. package/lib/orchestrator/config.d.ts.map +1 -1
  131. package/lib/orchestrator/config.js +0 -2
  132. package/lib/orchestrator/config.js.map +1 -1
  133. package/lib/orchestrator/doctor.d.ts.map +1 -1
  134. package/lib/orchestrator/doctor.js +5 -3
  135. package/lib/orchestrator/doctor.js.map +1 -1
  136. package/lib/orchestrator/experiment-contracts.d.ts +19 -0
  137. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
  138. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  139. package/lib/orchestrator/experiment-evaluation.js +17 -0
  140. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  141. package/lib/orchestrator/experiment-report.d.ts +19 -0
  142. package/lib/orchestrator/experiment-report.d.ts.map +1 -1
  143. package/lib/orchestrator/experiment-runs.d.ts +38 -0
  144. package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
  145. package/lib/orchestrator/grader-experiment.d.ts +12 -0
  146. package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
  147. package/lib/orchestrator/grading.d.ts +2 -1
  148. package/lib/orchestrator/grading.d.ts.map +1 -1
  149. package/lib/orchestrator/grading.js +16 -4
  150. package/lib/orchestrator/grading.js.map +1 -1
  151. package/lib/orchestrator/index.d.ts +1 -1
  152. package/lib/orchestrator/index.d.ts.map +1 -1
  153. package/lib/orchestrator/index.js +1 -1
  154. package/lib/orchestrator/index.js.map +1 -1
  155. package/lib/orchestrator/list-runs.d.ts +7 -1
  156. package/lib/orchestrator/list-runs.d.ts.map +1 -1
  157. package/lib/orchestrator/list-runs.js +50 -6
  158. package/lib/orchestrator/list-runs.js.map +1 -1
  159. package/lib/orchestrator/loader.d.ts.map +1 -1
  160. package/lib/orchestrator/loader.js +2 -0
  161. package/lib/orchestrator/loader.js.map +1 -1
  162. package/lib/orchestrator/orchestrator.d.ts +3 -0
  163. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  164. package/lib/orchestrator/orchestrator.js +42 -14
  165. package/lib/orchestrator/orchestrator.js.map +1 -1
  166. package/lib/orchestrator/owned-lock.d.ts.map +1 -1
  167. package/lib/orchestrator/owned-lock.js +52 -38
  168. package/lib/orchestrator/owned-lock.js.map +1 -1
  169. package/lib/orchestrator/regrade.d.ts +6 -0
  170. package/lib/orchestrator/regrade.d.ts.map +1 -1
  171. package/lib/orchestrator/regrade.js +8 -4
  172. package/lib/orchestrator/regrade.js.map +1 -1
  173. package/lib/orchestrator/run-execution.d.ts +2 -1
  174. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  175. package/lib/orchestrator/run-execution.js +20 -5
  176. package/lib/orchestrator/run-execution.js.map +1 -1
  177. package/lib/orchestrator/suite-compare.d.ts +34 -0
  178. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  179. package/lib/orchestrator/suite-summary.d.ts +68 -0
  180. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  181. package/lib/orchestrator/suite-summary.js +45 -7
  182. package/lib/orchestrator/suite-summary.js.map +1 -1
  183. package/lib/previewer/artifacts.d.ts +25 -0
  184. package/lib/previewer/artifacts.d.ts.map +1 -0
  185. package/lib/previewer/artifacts.js +180 -0
  186. package/lib/previewer/artifacts.js.map +1 -0
  187. package/lib/previewer/server.d.ts +105 -0
  188. package/lib/previewer/server.d.ts.map +1 -0
  189. package/lib/previewer/server.js +542 -0
  190. package/lib/previewer/server.js.map +1 -0
  191. package/package.json +2 -2
  192. package/skills/lux/SKILL.md +7 -3
  193. package/skills/lux/references/cli.md +25 -2
  194. package/skills/lux/references/eval-authoring.md +158 -4
  195. package/skills/lux-answerer/SKILL.md +1 -1
  196. package/src/answerers/persona/index.ts +21 -4
  197. package/src/answerers/terminal/index.ts +1 -1
  198. package/src/assertions/authoring.ts +81 -0
  199. package/src/assertions/core/helpers.ts +3 -6
  200. package/src/assertions/core/index.ts +2 -0
  201. package/src/assertions/core/resource-checks.ts +35 -0
  202. package/src/authoring.ts +1 -0
  203. package/src/cli/program.ts +14 -93
  204. package/src/cli/render/reporter.ts +18 -3
  205. package/src/cli/run-options.ts +1 -0
  206. package/src/cli/view.ts +168 -0
  207. package/src/core/action-events.ts +59 -0
  208. package/src/core/agent-activity.ts +236 -0
  209. package/src/core/agent-events.ts +17 -0
  210. package/src/core/artifact-evidence.ts +47 -4
  211. package/src/core/assertion.ts +41 -22
  212. package/src/core/case-authoring-state.ts +65 -0
  213. package/src/core/case.ts +57 -6
  214. package/src/core/driver-metrics.ts +51 -0
  215. package/src/core/driver.ts +3 -0
  216. package/src/core/events.ts +14 -2
  217. package/src/core/experiment.ts +2 -0
  218. package/src/core/index.ts +4 -0
  219. package/src/core/lifecycle-fixtures.ts +7 -2
  220. package/src/core/lifecycle-hooks.ts +16 -2
  221. package/src/core/run.ts +9 -0
  222. package/src/core/wire.ts +5 -1
  223. package/src/drivers/claude-code/parse-events.ts +98 -12
  224. package/src/drivers/codex/app-events.ts +116 -16
  225. package/src/drivers/codex/index.ts +16 -14
  226. package/src/drivers/subprocess/index.ts +6 -0
  227. package/src/index.ts +50 -6
  228. package/src/orchestrator/annotation-store.ts +5 -1
  229. package/src/orchestrator/case-extensions.ts +44 -0
  230. package/src/orchestrator/compare.ts +18 -0
  231. package/src/orchestrator/config.ts +0 -2
  232. package/src/orchestrator/doctor.ts +5 -3
  233. package/src/orchestrator/experiment-evaluation.ts +23 -0
  234. package/src/orchestrator/grading.ts +21 -2
  235. package/src/orchestrator/index.ts +1 -0
  236. package/src/orchestrator/list-runs.ts +66 -5
  237. package/src/orchestrator/loader.ts +2 -0
  238. package/src/orchestrator/orchestrator.ts +86 -40
  239. package/src/orchestrator/owned-lock.ts +54 -34
  240. package/src/orchestrator/regrade.ts +8 -7
  241. package/src/orchestrator/run-execution.ts +24 -3
  242. package/src/orchestrator/suite-summary.ts +101 -8
  243. package/src/previewer/artifacts.ts +221 -0
  244. package/src/previewer/server.ts +765 -0
  245. package/viewer/app.css +2075 -0
  246. package/viewer/app.js +606 -0
  247. package/viewer/artifact-view.js +48 -0
  248. package/viewer/charts.js +86 -0
  249. package/viewer/detail.js +1214 -0
  250. package/viewer/format.js +129 -0
  251. package/viewer/index.html +151 -0
  252. package/lib/answerers/scripted/index.d.ts +0 -8
  253. package/lib/answerers/scripted/index.d.ts.map +0 -1
  254. package/lib/answerers/scripted/index.js +0 -163
  255. package/lib/answerers/scripted/index.js.map +0 -1
  256. package/src/answerers/scripted/index.ts +0 -186
@@ -1,7 +1,13 @@
1
1
  import { rename, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
- import { harnessUsage, isUsageEmpty, type RunMetadata } from "../core/index.js";
4
+ import {
5
+ type DriverMetric,
6
+ DriverMetricUnitSchema,
7
+ harnessUsage,
8
+ isUsageEmpty,
9
+ type RunMetadata,
10
+ } from "../core/index.js";
5
11
  import { runComparisonIdentity } from "./comparison-identity.js";
6
12
  import { safeReadJson } from "./fs-read.js";
7
13
  import type { SuiteCaseResult } from "./orchestrator.js";
@@ -16,6 +22,9 @@ export const MetricSummarySchema = z.object({
16
22
  mean: z.number(),
17
23
  median: z.number(),
18
24
  p90: z.number(),
25
+ unit: z.exactOptional(DriverMetricUnitSchema),
26
+ label: z.exactOptional(z.string()),
27
+ better: z.exactOptional(z.enum(["lower", "higher", "neutral"])),
19
28
  });
20
29
  export type MetricSummary = z.infer<typeof MetricSummarySchema>;
21
30
 
@@ -59,13 +68,22 @@ export const SuiteSummarySchema = z.object({
59
68
  });
60
69
  export type SuiteSummary = z.infer<typeof SuiteSummarySchema>;
61
70
 
62
- type MetricSamples = Map<string, number[]>;
71
+ type MetricSamples = Map<
72
+ string,
73
+ { values: number[]; descriptor?: Omit<DriverMetric, "value"> | undefined }
74
+ >;
63
75
 
64
- const add = (samples: MetricSamples, name: string, value: number | undefined): void => {
76
+ const add = (
77
+ samples: MetricSamples,
78
+ name: string,
79
+ value: number | undefined,
80
+ descriptor?: Omit<DriverMetric, "value">,
81
+ ): void => {
65
82
  if (value === undefined || !Number.isFinite(value)) return;
66
- const values = samples.get(name) ?? [];
67
- values.push(value);
68
- samples.set(name, values);
83
+ const sample = samples.get(name) ?? { values: [], descriptor };
84
+ if (!sample.descriptor && descriptor) sample.descriptor = descriptor;
85
+ sample.values.push(value);
86
+ samples.set(name, sample);
69
87
  };
70
88
 
71
89
  const cacheHitRate = (usage: {
@@ -137,6 +155,78 @@ const collectMetadata = (samples: MetricSamples, metadata: RunMetadata): void =>
137
155
  add(samples, "phase.agentMs", metadata.phaseDurationMs?.agent);
138
156
  add(samples, "phase.collectMs", metadata.phaseDurationMs?.collect);
139
157
  add(samples, "phase.gradingMs", metadata.phaseDurationMs?.grading);
158
+ add(samples, "timeToFirstActionMs", metadata.actionTiming?.timeToFirstActionMs);
159
+ add(samples, "timeToFirstMutationMs", metadata.actionTiming?.timeToFirstMutationMs);
160
+ add(samples, "timeToFirstDurableMutationMs", metadata.actionTiming?.timeToFirstDurableMutationMs);
161
+ const subagents = (metadata.agentActivity ?? []).filter(
162
+ (activity) => activity.agent.role === "subagent",
163
+ );
164
+ add(samples, "subagents.count", subagents.length, {
165
+ unit: "count",
166
+ label: "Subagents spawned",
167
+ better: "neutral",
168
+ });
169
+ add(
170
+ samples,
171
+ "subagents.events",
172
+ subagents.reduce((sum, activity) => sum + activity.eventCount, 0),
173
+ { unit: "count", label: "Subagent events", better: "neutral" },
174
+ );
175
+ add(
176
+ samples,
177
+ "subagents.toolCalls",
178
+ subagents.reduce((sum, activity) => sum + activity.toolCalls.total, 0),
179
+ { unit: "count", label: "Subagent tool calls", better: "neutral" },
180
+ );
181
+ add(
182
+ samples,
183
+ "subagents.toolCalls.failed",
184
+ subagents.reduce((sum, activity) => sum + activity.toolCalls.failed, 0),
185
+ { unit: "count", label: "Failed subagent tool calls", better: "lower" },
186
+ );
187
+ add(
188
+ samples,
189
+ "subagents.tokens",
190
+ subagents.reduce((sum, activity) => sum + activity.usage.total, 0),
191
+ { unit: "tokens", label: "Subagent tokens", better: "lower" },
192
+ );
193
+ add(
194
+ samples,
195
+ "subagents.actions",
196
+ subagents.reduce((sum, activity) => sum + activity.actions.total, 0),
197
+ { unit: "count", label: "Subagent actions", better: "neutral" },
198
+ );
199
+ add(
200
+ samples,
201
+ "subagents.mutations",
202
+ subagents.reduce((sum, activity) => sum + activity.actions.mutations, 0),
203
+ { unit: "count", label: "Subagent mutations", better: "neutral" },
204
+ );
205
+ add(
206
+ samples,
207
+ "subagents.totalDurationMs",
208
+ subagents.reduce((sum, activity) => sum + activity.durationMs, 0),
209
+ { unit: "milliseconds", label: "Total subagent time", better: "neutral" },
210
+ );
211
+ add(
212
+ samples,
213
+ "subagents.maxDurationMs",
214
+ subagents.length > 0 ? Math.max(...subagents.map((activity) => activity.durationMs)) : 0,
215
+ { unit: "milliseconds", label: "Longest subagent", better: "lower" },
216
+ );
217
+ const firstSubagentAction = subagents
218
+ .map((activity) => activity.actions.timeToFirstActionMs)
219
+ .filter((value): value is number => value !== undefined)
220
+ .sort((a, b) => a - b)[0];
221
+ add(samples, "subagents.timeToFirstActionMs", firstSubagentAction, {
222
+ unit: "milliseconds",
223
+ label: "First subagent action",
224
+ better: "lower",
225
+ });
226
+ for (const [name, metric] of Object.entries(metadata.driverMetrics ?? {})) {
227
+ const { value, ...descriptor } = metric;
228
+ add(samples, `driver.${name}`, value, descriptor);
229
+ }
140
230
  };
141
231
 
142
232
  const percentile = (sorted: readonly number[], fraction: number): number => {
@@ -165,7 +255,10 @@ const summarizeMetrics = (samples: MetricSamples): Record<string, MetricSummary>
165
255
  Object.fromEntries(
166
256
  [...samples.entries()]
167
257
  .sort(([a], [b]) => a.localeCompare(b))
168
- .map(([name, values]) => [name, summarizeNumbers(values)]),
258
+ .map(([name, { values, descriptor }]) => [
259
+ name,
260
+ MetricSummarySchema.parse({ ...summarizeNumbers(values), ...descriptor }),
261
+ ]),
169
262
  );
170
263
 
171
264
  const summarizeOutcomes = (
@@ -258,7 +351,7 @@ export const buildSuiteSummary = (
258
351
  ? { score: result.grading.caseScore }
259
352
  : {}),
260
353
  metrics: Object.fromEntries(
261
- [...samples.entries()].map(([name, values]) => [name, values[0] ?? 0]),
354
+ [...samples.entries()].map(([name, sample]) => [name, sample.values[0] ?? 0]),
262
355
  ),
263
356
  };
264
357
  }),
@@ -0,0 +1,221 @@
1
+ import type { Artifact } from "../core/index.js";
2
+ import { inspectRunArtifact, readRunArtifactBytes } from "../core/index.js";
3
+
4
+ const MAX_PREVIEW_BYTES = 512_000;
5
+ const MAX_DIFF_LINES = 2_000;
6
+ const MAX_DIFF_CELLS = 1_000_000;
7
+
8
+ export type ArtifactPreview = {
9
+ path: string;
10
+ exists: boolean;
11
+ viewable: boolean;
12
+ size: number | null;
13
+ contentType: string | null;
14
+ text: string | null;
15
+ reason: string | null;
16
+ };
17
+
18
+ type ArtifactDiffOperation = {
19
+ kind: "same" | "added" | "removed";
20
+ lines: string[];
21
+ };
22
+
23
+ export type ArtifactComparison = {
24
+ path: string;
25
+ before: ArtifactPreview;
26
+ after: ArtifactPreview;
27
+ operations: ArtifactDiffOperation[] | null;
28
+ reason: string | null;
29
+ };
30
+
31
+ const evidenceArtifacts = (
32
+ indexed: readonly Artifact[],
33
+ knownPaths: readonly string[],
34
+ ): Artifact[] => {
35
+ const out = [...indexed];
36
+ const indexedPaths = new Set(indexed.map((artifact) => artifact.path));
37
+ for (const path of knownPaths) {
38
+ if (!indexedPaths.has(path)) out.push({ path });
39
+ }
40
+ return out;
41
+ };
42
+
43
+ export const loadArtifactPreview = async (
44
+ runDir: string,
45
+ indexed: readonly Artifact[],
46
+ knownPaths: readonly string[],
47
+ path: string,
48
+ ): Promise<ArtifactPreview> => {
49
+ if (!knownPaths.includes(path) && !indexed.some((artifact) => artifact.path === path)) {
50
+ return {
51
+ path,
52
+ exists: false,
53
+ viewable: false,
54
+ size: null,
55
+ contentType: null,
56
+ text: null,
57
+ reason: "artifact was not captured by this run",
58
+ };
59
+ }
60
+ const artifacts = evidenceArtifacts(indexed, knownPaths);
61
+ const run = { runDir, artifacts };
62
+ const inspected = await inspectRunArtifact(run, path);
63
+ const contentType = inspected.artifact?.contentType ?? null;
64
+ if (!inspected.stats?.isFile()) {
65
+ return {
66
+ path,
67
+ exists: false,
68
+ viewable: false,
69
+ size: inspected.stats?.size ?? null,
70
+ contentType,
71
+ text: null,
72
+ reason: inspected.issue ?? "artifact is not a regular file",
73
+ };
74
+ }
75
+ if (inspected.stats.size > MAX_PREVIEW_BYTES) {
76
+ return {
77
+ path,
78
+ exists: true,
79
+ viewable: false,
80
+ size: inspected.stats.size,
81
+ contentType,
82
+ text: null,
83
+ reason: `artifact exceeds the ${MAX_PREVIEW_BYTES}-byte preview limit`,
84
+ };
85
+ }
86
+ const bytes = await readRunArtifactBytes(run, path, { maxBytes: MAX_PREVIEW_BYTES });
87
+ let text: string;
88
+ try {
89
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
90
+ } catch {
91
+ return {
92
+ path,
93
+ exists: true,
94
+ viewable: false,
95
+ size: bytes.length,
96
+ contentType,
97
+ text: null,
98
+ reason: "artifact is not valid UTF-8 text",
99
+ };
100
+ }
101
+ if (text.includes("\0")) {
102
+ return {
103
+ path,
104
+ exists: true,
105
+ viewable: false,
106
+ size: bytes.length,
107
+ contentType,
108
+ text: null,
109
+ reason: "artifact appears to be binary",
110
+ };
111
+ }
112
+ return {
113
+ path,
114
+ exists: true,
115
+ viewable: true,
116
+ size: bytes.length,
117
+ contentType,
118
+ text,
119
+ reason: null,
120
+ };
121
+ };
122
+
123
+ const appendOperation = (
124
+ operations: ArtifactDiffOperation[],
125
+ kind: ArtifactDiffOperation["kind"],
126
+ line: string,
127
+ ): void => {
128
+ const current = operations.at(-1);
129
+ if (current?.kind === kind) current.lines.push(line);
130
+ else operations.push({ kind, lines: [line] });
131
+ };
132
+
133
+ const diffLines = (beforeText: string, afterText: string): ArtifactDiffOperation[] | null => {
134
+ const before = beforeText.split("\n");
135
+ const after = afterText.split("\n");
136
+ if (
137
+ before.length > MAX_DIFF_LINES ||
138
+ after.length > MAX_DIFF_LINES ||
139
+ before.length * after.length > MAX_DIFF_CELLS
140
+ ) {
141
+ return null;
142
+ }
143
+ const width = after.length + 1;
144
+ const table = new Uint32Array((before.length + 1) * width);
145
+ for (let left = before.length - 1; left >= 0; left--) {
146
+ for (let right = after.length - 1; right >= 0; right--) {
147
+ const cell = left * width + right;
148
+ table[cell] =
149
+ before[left] === after[right]
150
+ ? (table[(left + 1) * width + right + 1] ?? 0) + 1
151
+ : Math.max(table[(left + 1) * width + right] ?? 0, table[left * width + right + 1] ?? 0);
152
+ }
153
+ }
154
+ const operations: ArtifactDiffOperation[] = [];
155
+ let left = 0;
156
+ let right = 0;
157
+ while (left < before.length && right < after.length) {
158
+ if (before[left] === after[right]) {
159
+ appendOperation(operations, "same", before[left] ?? "");
160
+ left++;
161
+ right++;
162
+ } else if ((table[(left + 1) * width + right] ?? 0) >= (table[left * width + right + 1] ?? 0)) {
163
+ appendOperation(operations, "removed", before[left] ?? "");
164
+ left++;
165
+ } else {
166
+ appendOperation(operations, "added", after[right] ?? "");
167
+ right++;
168
+ }
169
+ }
170
+ while (left < before.length) {
171
+ appendOperation(operations, "removed", before[left] ?? "");
172
+ left++;
173
+ }
174
+ while (right < after.length) {
175
+ appendOperation(operations, "added", after[right] ?? "");
176
+ right++;
177
+ }
178
+ return operations;
179
+ };
180
+
181
+ export const compareArtifactPreviews = (
182
+ path: string,
183
+ before: ArtifactPreview,
184
+ after: ArtifactPreview,
185
+ ): ArtifactComparison => {
186
+ if (before.viewable && before.text !== null && !after.exists) {
187
+ return {
188
+ path,
189
+ before,
190
+ after,
191
+ operations: [{ kind: "removed", lines: before.text.split("\n") }],
192
+ reason: null,
193
+ };
194
+ }
195
+ if (!before.exists && after.viewable && after.text !== null) {
196
+ return {
197
+ path,
198
+ before,
199
+ after,
200
+ operations: [{ kind: "added", lines: after.text.split("\n") }],
201
+ reason: null,
202
+ };
203
+ }
204
+ if (!before.viewable || !after.viewable || before.text === null || after.text === null) {
205
+ return {
206
+ path,
207
+ before,
208
+ after,
209
+ operations: null,
210
+ reason: before.reason ?? after.reason ?? "artifact text is unavailable",
211
+ };
212
+ }
213
+ const operations = diffLines(before.text, after.text);
214
+ return {
215
+ path,
216
+ before,
217
+ after,
218
+ operations,
219
+ reason: operations ? null : "artifact is too line-dense for an inline diff",
220
+ };
221
+ };