@prismatic-io/lux 0.0.2-preview.4 → 0.0.2-preview.5
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/authoring.d.ts +2 -2
- package/lib/assertions/authoring.d.ts.map +1 -1
- package/lib/assertions/authoring.js.map +1 -1
- package/lib/assertions/core/index.d.ts.map +1 -1
- package/lib/assertions/core/index.js +2 -1
- package/lib/assertions/core/index.js.map +1 -1
- package/lib/assertions/core/resource-checks.d.ts +19 -0
- package/lib/assertions/core/resource-checks.d.ts.map +1 -1
- package/lib/assertions/core/resource-checks.js +32 -0
- package/lib/assertions/core/resource-checks.js.map +1 -1
- package/lib/cli/program.d.ts.map +1 -1
- package/lib/cli/program.js +6 -65
- package/lib/cli/program.js.map +1 -1
- package/lib/cli/view.d.ts +13 -0
- package/lib/cli/view.d.ts.map +1 -0
- package/lib/cli/view.js +126 -0
- package/lib/cli/view.js.map +1 -0
- package/lib/core/action-events.d.ts +29 -0
- package/lib/core/action-events.d.ts.map +1 -0
- package/lib/core/action-events.js +51 -0
- package/lib/core/action-events.js.map +1 -0
- package/lib/core/driver-metrics.d.ts +98 -0
- package/lib/core/driver-metrics.d.ts.map +1 -0
- package/lib/core/driver-metrics.js +36 -0
- package/lib/core/driver-metrics.js.map +1 -0
- package/lib/core/events.js +2 -2
- package/lib/core/experiment.d.ts +19 -0
- package/lib/core/experiment.d.ts.map +1 -1
- package/lib/core/experiment.js +2 -0
- package/lib/core/experiment.js.map +1 -1
- package/lib/core/index.d.ts +2 -0
- package/lib/core/index.d.ts.map +1 -1
- package/lib/core/index.js +2 -0
- package/lib/core/index.js.map +1 -1
- package/lib/core/run.d.ts +50 -0
- package/lib/core/run.d.ts.map +1 -1
- package/lib/core/run.js +6 -0
- package/lib/core/run.js.map +1 -1
- package/lib/index.d.ts +3 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/orchestrator/compare.d.ts +9 -1
- package/lib/orchestrator/compare.d.ts.map +1 -1
- package/lib/orchestrator/compare.js +8 -0
- package/lib/orchestrator/compare.js.map +1 -1
- package/lib/orchestrator/experiment-contracts.d.ts +19 -0
- package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
- package/lib/orchestrator/experiment-evaluation.js +17 -0
- package/lib/orchestrator/experiment-evaluation.js.map +1 -1
- package/lib/orchestrator/experiment-report.d.ts +19 -0
- package/lib/orchestrator/experiment-report.d.ts.map +1 -1
- package/lib/orchestrator/experiment-runs.d.ts +38 -0
- package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
- package/lib/orchestrator/index.d.ts +1 -1
- package/lib/orchestrator/index.d.ts.map +1 -1
- package/lib/orchestrator/index.js +1 -1
- package/lib/orchestrator/index.js.map +1 -1
- package/lib/orchestrator/list-runs.d.ts +6 -1
- package/lib/orchestrator/list-runs.d.ts.map +1 -1
- package/lib/orchestrator/list-runs.js +15 -1
- package/lib/orchestrator/list-runs.js.map +1 -1
- package/lib/orchestrator/run-execution.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.js +11 -1
- package/lib/orchestrator/run-execution.js.map +1 -1
- package/lib/orchestrator/suite-compare.d.ts +34 -0
- package/lib/orchestrator/suite-compare.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.d.ts +68 -0
- package/lib/orchestrator/suite-summary.d.ts.map +1 -1
- package/lib/orchestrator/suite-summary.js +22 -7
- package/lib/orchestrator/suite-summary.js.map +1 -1
- package/lib/previewer/artifacts.d.ts +25 -0
- package/lib/previewer/artifacts.d.ts.map +1 -0
- package/lib/previewer/artifacts.js +180 -0
- package/lib/previewer/artifacts.js.map +1 -0
- package/lib/previewer/server.d.ts +104 -0
- package/lib/previewer/server.d.ts.map +1 -0
- package/lib/previewer/server.js +541 -0
- package/lib/previewer/server.js.map +1 -0
- package/package.json +2 -1
- package/skills/lux/references/eval-authoring.md +4 -2
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/assertions/authoring.ts +2 -0
- package/src/assertions/core/index.ts +2 -0
- package/src/assertions/core/resource-checks.ts +35 -0
- package/src/cli/program.ts +5 -91
- package/src/cli/view.ts +168 -0
- package/src/core/action-events.ts +59 -0
- package/src/core/driver-metrics.ts +51 -0
- package/src/core/events.ts +2 -2
- package/src/core/experiment.ts +2 -0
- package/src/core/index.ts +2 -0
- package/src/core/run.ts +6 -0
- package/src/index.ts +25 -0
- package/src/orchestrator/compare.ts +12 -0
- package/src/orchestrator/experiment-evaluation.ts +23 -0
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/list-runs.ts +25 -0
- package/src/orchestrator/run-execution.ts +12 -0
- package/src/orchestrator/suite-summary.ts +36 -8
- package/src/previewer/artifacts.ts +221 -0
- package/src/previewer/server.ts +763 -0
- package/viewer/app.css +2022 -0
- package/viewer/app.js +601 -0
- package/viewer/artifact-view.js +48 -0
- package/viewer/charts.js +86 -0
- package/viewer/detail.js +1108 -0
- package/viewer/format.js +129 -0
- package/viewer/index.html +151 -0
|
@@ -2,6 +2,9 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { constants, lstat, open, readdir } from "node:fs/promises";
|
|
3
3
|
import { basename, join, relative, sep } from "node:path";
|
|
4
4
|
import {
|
|
5
|
+
ActionProgressPayloadSchema,
|
|
6
|
+
type Artifact,
|
|
7
|
+
ArtifactSchema,
|
|
5
8
|
type GradingReport,
|
|
6
9
|
GradingReportSchema,
|
|
7
10
|
harnessUsage,
|
|
@@ -31,6 +34,10 @@ export type ListedRun = {
|
|
|
31
34
|
startedAt: string | null;
|
|
32
35
|
endedAt: string | null;
|
|
33
36
|
durationMs: number | null;
|
|
37
|
+
timeToFirstActionMs: number | null;
|
|
38
|
+
timeToFirstMutationMs: number | null;
|
|
39
|
+
timeToFirstDurableMutationMs: number | null;
|
|
40
|
+
driverMetrics: NonNullable<RunMetadata["driverMetrics"]>;
|
|
34
41
|
casePassed: boolean | null;
|
|
35
42
|
caseScore: number | null;
|
|
36
43
|
assertionPassed: number;
|
|
@@ -63,6 +70,10 @@ const summarizeRun = async (
|
|
|
63
70
|
startedAt,
|
|
64
71
|
endedAt,
|
|
65
72
|
durationMs,
|
|
73
|
+
timeToFirstActionMs: meta?.actionTiming?.timeToFirstActionMs ?? null,
|
|
74
|
+
timeToFirstMutationMs: meta?.actionTiming?.timeToFirstMutationMs ?? null,
|
|
75
|
+
timeToFirstDurableMutationMs: meta?.actionTiming?.timeToFirstDurableMutationMs ?? null,
|
|
76
|
+
driverMetrics: meta?.driverMetrics ?? {},
|
|
66
77
|
casePassed: grading?.casePassed ?? null,
|
|
67
78
|
caseScore: grading?.caseScore ?? null,
|
|
68
79
|
assertionPassed: passing,
|
|
@@ -156,6 +167,9 @@ export type RunDetail = {
|
|
|
156
167
|
artifacts: string[];
|
|
157
168
|
};
|
|
158
169
|
|
|
170
|
+
export const loadRunArtifacts = async (runDir: string): Promise<Artifact[]> =>
|
|
171
|
+
(await safeReadJson(join(runDir, "artifacts.json"), ArtifactSchema.array())) ?? [];
|
|
172
|
+
|
|
159
173
|
const readEventsJsonl = async (path: string): Promise<RunEvent[]> => {
|
|
160
174
|
if (!existsSync(path)) return [];
|
|
161
175
|
if ((await lstat(path)).isSymbolicLink()) {
|
|
@@ -291,6 +305,13 @@ const appendMetadataDetail = (lines: string[], detail: RunDetail): void => {
|
|
|
291
305
|
`phases: agent=${secs(phases.agent)} collect=${secs(phases.collect)} grading=${secs(phases.grading)}`,
|
|
292
306
|
);
|
|
293
307
|
}
|
|
308
|
+
const actions = detail.metadata.actionTiming;
|
|
309
|
+
if (actions) {
|
|
310
|
+
const value = (ms: number | undefined): string => (ms === undefined ? "—" : `${ms}ms`);
|
|
311
|
+
lines.push(
|
|
312
|
+
`first action: any=${value(actions.timeToFirstActionMs)} mutation=${value(actions.timeToFirstMutationMs)} durable-mutation=${value(actions.timeToFirstDurableMutationMs)}`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
294
315
|
}
|
|
295
316
|
};
|
|
296
317
|
|
|
@@ -362,6 +383,10 @@ const formatEventLine = (event: RunEvent): string => {
|
|
|
362
383
|
}
|
|
363
384
|
return `responded(custom): ${JSON.stringify(event.payload).slice(0, 80)}`;
|
|
364
385
|
case "progress":
|
|
386
|
+
if (event.kind === "action") {
|
|
387
|
+
const payload = ActionProgressPayloadSchema.safeParse(event.payload).data;
|
|
388
|
+
return payload ? `action: ${payload.name} (${payload.category})` : "action: malformed";
|
|
389
|
+
}
|
|
365
390
|
if (event.kind === "tool-call") {
|
|
366
391
|
const payload = ToolCallPayloadSchema.safeParse(event.payload).data;
|
|
367
392
|
return payload ? `tool-call: ${payload.name}` : "tool-call: malformed";
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
AnswererModelCallAccountingSchema,
|
|
11
11
|
type AnswerHistoryEntry,
|
|
12
12
|
type Artifact,
|
|
13
|
+
actionTiming,
|
|
13
14
|
type EvalCase,
|
|
14
15
|
emptyUsage,
|
|
15
16
|
type FixturesRef,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
guardRunObserver,
|
|
19
20
|
inspectRunArtifact,
|
|
20
21
|
type LifecycleStage,
|
|
22
|
+
normalizeDriverMetrics,
|
|
21
23
|
type OperationalFailure,
|
|
22
24
|
type PhaseDurations,
|
|
23
25
|
type ReadyState,
|
|
@@ -183,6 +185,11 @@ const modelAccountingFromSummary = (
|
|
|
183
185
|
): { modelCalls: number; unpricedModelCalls?: number | undefined } | null =>
|
|
184
186
|
SummaryModelAccountingSchema.safeParse(summary).data ?? null;
|
|
185
187
|
|
|
188
|
+
const metricsFromSummary = (summary: unknown) => {
|
|
189
|
+
if (!summary || typeof summary !== "object") return {};
|
|
190
|
+
return normalizeDriverMetrics((summary as { metrics?: unknown }).metrics);
|
|
191
|
+
};
|
|
192
|
+
|
|
186
193
|
const artifactMeasurements = async (
|
|
187
194
|
artifacts: readonly Artifact[],
|
|
188
195
|
runDir: string,
|
|
@@ -390,6 +397,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
390
397
|
metadata.exitReason = "error";
|
|
391
398
|
metadata.operationalFailure = { phase: operationalPhase, reason };
|
|
392
399
|
metadata.phaseDurationMs = phaseDurations(this.runDir.recorded);
|
|
400
|
+
metadata.actionTiming = actionTiming(this.runDir.recorded);
|
|
393
401
|
await this.runDir.writeJson("run.json", metadata);
|
|
394
402
|
await this.args.request.recordPreGradingAccounting?.(metadata);
|
|
395
403
|
await this.args.request.recordModelAttemptsComplete?.(this.subjectAttemptIds);
|
|
@@ -407,6 +415,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
407
415
|
this.lifecycle("collect-end");
|
|
408
416
|
await this.runDir.events.drain();
|
|
409
417
|
const completed = await this.completedAgentRun();
|
|
418
|
+
completed.metadata.actionTiming = actionTiming(this.runDir.recorded);
|
|
410
419
|
this.metadata = completed.metadata;
|
|
411
420
|
this.usage = completed.usage;
|
|
412
421
|
this.run = completed.run;
|
|
@@ -430,6 +439,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
430
439
|
grading.assertions.flatMap((assertion) => (assertion.usage ? [assertion.usage] : [])),
|
|
431
440
|
);
|
|
432
441
|
metadata.phaseDurationMs = phaseDurations(this.runDir.recorded);
|
|
442
|
+
metadata.actionTiming = actionTiming(this.runDir.recorded);
|
|
433
443
|
await this.runDir.events.drain();
|
|
434
444
|
await this.runDir.writeJson("run.json", metadata);
|
|
435
445
|
await this.runDir.writeJson("artifacts.json", this.collection.artifacts);
|
|
@@ -563,6 +573,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
563
573
|
const summary = outcome.kind === "done" ? outcome.done.summary : outcome.error.summary;
|
|
564
574
|
const driverCost = costFromSummary(summary);
|
|
565
575
|
const reportedModelAccounting = modelAccountingFromSummary(summary);
|
|
576
|
+
const driverMetrics = metricsFromSummary(summary);
|
|
566
577
|
const answererAccounting = readAnswererModelAccounting(this.args.answerer);
|
|
567
578
|
const agentCalls = this.ready ? (reportedModelAccounting?.modelCalls ?? 1) : 0;
|
|
568
579
|
const operationalFailure = loopFailure(outcome, this.collection.error);
|
|
@@ -597,6 +608,7 @@ class RunExecution implements RunLifecycleEffects<RunResult> {
|
|
|
597
608
|
...(operationalFailure ? { operationalFailure } : {}),
|
|
598
609
|
provenance: this.runProvenance(),
|
|
599
610
|
...(driverCost !== undefined ? { cost: { usd: driverCost } } : {}),
|
|
611
|
+
...(Object.keys(driverMetrics).length > 0 ? { driverMetrics } : {}),
|
|
600
612
|
};
|
|
601
613
|
return {
|
|
602
614
|
metadata,
|
|
@@ -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 {
|
|
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<
|
|
71
|
+
type MetricSamples = Map<
|
|
72
|
+
string,
|
|
73
|
+
{ values: number[]; descriptor?: Omit<DriverMetric, "value"> | undefined }
|
|
74
|
+
>;
|
|
63
75
|
|
|
64
|
-
const add = (
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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,13 @@ 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
|
+
for (const [name, metric] of Object.entries(metadata.driverMetrics ?? {})) {
|
|
162
|
+
const { value, ...descriptor } = metric;
|
|
163
|
+
add(samples, `driver.${name}`, value, descriptor);
|
|
164
|
+
}
|
|
140
165
|
};
|
|
141
166
|
|
|
142
167
|
const percentile = (sorted: readonly number[], fraction: number): number => {
|
|
@@ -165,7 +190,10 @@ const summarizeMetrics = (samples: MetricSamples): Record<string, MetricSummary>
|
|
|
165
190
|
Object.fromEntries(
|
|
166
191
|
[...samples.entries()]
|
|
167
192
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
168
|
-
.map(([name, values]) => [
|
|
193
|
+
.map(([name, { values, descriptor }]) => [
|
|
194
|
+
name,
|
|
195
|
+
MetricSummarySchema.parse({ ...summarizeNumbers(values), ...descriptor }),
|
|
196
|
+
]),
|
|
169
197
|
);
|
|
170
198
|
|
|
171
199
|
const summarizeOutcomes = (
|
|
@@ -258,7 +286,7 @@ export const buildSuiteSummary = (
|
|
|
258
286
|
? { score: result.grading.caseScore }
|
|
259
287
|
: {}),
|
|
260
288
|
metrics: Object.fromEntries(
|
|
261
|
-
[...samples.entries()].map(([name,
|
|
289
|
+
[...samples.entries()].map(([name, sample]) => [name, sample.values[0] ?? 0]),
|
|
262
290
|
),
|
|
263
291
|
};
|
|
264
292
|
}),
|
|
@@ -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
|
+
};
|