@prismatic-io/lux 0.0.2-preview.3 → 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/answerers/terminal/index.js +1 -1
- package/lib/answerers/terminal/index.js.map +1 -1
- 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/helpers.d.ts +2 -7
- package/lib/assertions/core/helpers.d.ts.map +1 -1
- package/lib/assertions/core/helpers.js +2 -6
- package/lib/assertions/core/helpers.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 +12 -67
- package/lib/cli/program.js.map +1 -1
- package/lib/cli/render/reporter.d.ts.map +1 -1
- package/lib/cli/render/reporter.js +16 -3
- package/lib/cli/render/reporter.js.map +1 -1
- package/lib/cli/run-options.d.ts +1 -0
- package/lib/cli/run-options.d.ts.map +1 -1
- package/lib/cli/run-options.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/artifact-evidence.d.ts +8 -0
- package/lib/core/artifact-evidence.d.ts.map +1 -1
- package/lib/core/artifact-evidence.js +36 -4
- package/lib/core/artifact-evidence.js.map +1 -1
- package/lib/core/assertion.d.ts +12 -0
- package/lib/core/assertion.d.ts.map +1 -1
- package/lib/core/assertion.js +13 -0
- package/lib/core/assertion.js.map +1 -1
- 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.d.ts +2 -0
- package/lib/core/events.d.ts.map +1 -1
- package/lib/core/events.js +3 -2
- package/lib/core/events.js.map +1 -1
- 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 +51 -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/core/wire.d.ts +1 -0
- package/lib/core/wire.d.ts.map +1 -1
- package/lib/core/wire.js +5 -1
- package/lib/core/wire.js.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -2
- 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/config.d.ts.map +1 -1
- package/lib/orchestrator/config.js +0 -2
- package/lib/orchestrator/config.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/grader-experiment.d.ts +12 -0
- package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
- package/lib/orchestrator/grading.d.ts +2 -1
- package/lib/orchestrator/grading.d.ts.map +1 -1
- package/lib/orchestrator/grading.js +16 -4
- package/lib/orchestrator/grading.js.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/orchestrator.d.ts +3 -0
- package/lib/orchestrator/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator/orchestrator.js +3 -0
- package/lib/orchestrator/orchestrator.js.map +1 -1
- package/lib/orchestrator/regrade.d.ts +6 -0
- package/lib/orchestrator/regrade.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.d.ts.map +1 -1
- package/lib/orchestrator/run-execution.js +13 -3
- 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 -2
- package/skills/lux/SKILL.md +7 -3
- package/skills/lux/references/cli.md +25 -2
- package/skills/lux/references/eval-authoring.md +158 -4
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/answerers/terminal/index.ts +1 -1
- package/src/assertions/authoring.ts +2 -0
- package/src/assertions/core/helpers.ts +3 -6
- package/src/assertions/core/index.ts +2 -0
- package/src/assertions/core/resource-checks.ts +35 -0
- package/src/cli/program.ts +14 -93
- package/src/cli/render/reporter.ts +18 -3
- package/src/cli/run-options.ts +1 -0
- package/src/cli/view.ts +168 -0
- package/src/core/action-events.ts +59 -0
- package/src/core/artifact-evidence.ts +47 -4
- package/src/core/assertion.ts +15 -0
- package/src/core/driver-metrics.ts +51 -0
- package/src/core/events.ts +3 -2
- package/src/core/experiment.ts +2 -0
- package/src/core/index.ts +2 -0
- package/src/core/run.ts +6 -0
- package/src/core/wire.ts +5 -1
- package/src/index.ts +25 -5
- package/src/orchestrator/compare.ts +12 -0
- package/src/orchestrator/config.ts +0 -2
- package/src/orchestrator/experiment-evaluation.ts +23 -0
- package/src/orchestrator/grading.ts +21 -2
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/list-runs.ts +25 -0
- package/src/orchestrator/orchestrator.ts +6 -0
- package/src/orchestrator/run-execution.ts +14 -1
- 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
- package/lib/answerers/scripted/index.d.ts +0 -8
- package/lib/answerers/scripted/index.d.ts.map +0 -1
- package/lib/answerers/scripted/index.js +0 -163
- package/lib/answerers/scripted/index.js.map +0 -1
- package/src/answerers/scripted/index.ts +0 -186
package/src/cli/view.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { InvalidArgumentError } from "commander";
|
|
3
|
+
import {
|
|
4
|
+
formatExperimentRun,
|
|
5
|
+
formatExperimentRunsTable,
|
|
6
|
+
formatRunDetail,
|
|
7
|
+
formatRunsTable,
|
|
8
|
+
formatSuiteRunDetail,
|
|
9
|
+
formatSuiteRunsTable,
|
|
10
|
+
isExperimentRun,
|
|
11
|
+
listExperimentRuns,
|
|
12
|
+
listRuns,
|
|
13
|
+
listSuiteRuns,
|
|
14
|
+
loadExperimentRun,
|
|
15
|
+
loadLuxConfig,
|
|
16
|
+
loadRunDetail,
|
|
17
|
+
loadSuiteRun,
|
|
18
|
+
} from "../orchestrator/index.js";
|
|
19
|
+
import { openPreviewUrl, startPreviewServer } from "../previewer/server.js";
|
|
20
|
+
import { abortOnInterrupt, ensureDirectory, failWith } from "./command-runtime.js";
|
|
21
|
+
|
|
22
|
+
const SIGINT_EXIT_CODE = 130;
|
|
23
|
+
|
|
24
|
+
export type ViewOptions = {
|
|
25
|
+
runsRoot?: string;
|
|
26
|
+
case?: string;
|
|
27
|
+
suites?: boolean;
|
|
28
|
+
experiments?: boolean;
|
|
29
|
+
web?: boolean;
|
|
30
|
+
port?: number;
|
|
31
|
+
open?: boolean;
|
|
32
|
+
json?: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const previewKind = async (
|
|
36
|
+
initialPath: string | undefined,
|
|
37
|
+
options: ViewOptions,
|
|
38
|
+
): Promise<"run" | "suite" | "experiment" | undefined> => {
|
|
39
|
+
if (initialPath) {
|
|
40
|
+
if (isExperimentRun(initialPath)) return "experiment";
|
|
41
|
+
if (await loadSuiteRun(initialPath)) return "suite";
|
|
42
|
+
return "run";
|
|
43
|
+
}
|
|
44
|
+
if (options.experiments) return "experiment";
|
|
45
|
+
if (options.suites) return "suite";
|
|
46
|
+
return undefined;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const webViewCommand = async (
|
|
50
|
+
runDirArg: string | undefined,
|
|
51
|
+
options: ViewOptions,
|
|
52
|
+
): Promise<void> => {
|
|
53
|
+
if (options.json) {
|
|
54
|
+
throw new InvalidArgumentError("--web and --json are mutually exclusive");
|
|
55
|
+
}
|
|
56
|
+
const config = await loadLuxConfig(process.cwd());
|
|
57
|
+
const runsRoot = options.runsRoot ? resolve(options.runsRoot) : config.runsRoot;
|
|
58
|
+
const initialPath = runDirArg ? resolve(runDirArg) : undefined;
|
|
59
|
+
if (initialPath && !(await ensureDirectory("view", initialPath))) return;
|
|
60
|
+
const initialKind = await previewKind(initialPath, options);
|
|
61
|
+
const controller = abortOnInterrupt();
|
|
62
|
+
const preview = await startPreviewServer({
|
|
63
|
+
runsRoot,
|
|
64
|
+
...(options.port !== undefined ? { port: options.port } : {}),
|
|
65
|
+
...(initialPath ? { initialPath } : {}),
|
|
66
|
+
...(initialKind ? { initialKind } : {}),
|
|
67
|
+
...(options.case ? { caseFilter: options.case } : {}),
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
process.stdout.write(`Lux Review: ${preview.url}\n`);
|
|
71
|
+
process.stdout.write(`Reading evidence from ${runsRoot}\n`);
|
|
72
|
+
if (options.open !== false) {
|
|
73
|
+
try {
|
|
74
|
+
await openPreviewUrl(preview.url);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
process.stderr.write(`lux view: could not open a browser (${message})\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
await preview.closed;
|
|
81
|
+
if (controller.signal.aborted) failWith(SIGINT_EXIT_CODE);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const viewCommand = async (
|
|
85
|
+
runDirArg: string | undefined,
|
|
86
|
+
options: ViewOptions,
|
|
87
|
+
): Promise<void> => {
|
|
88
|
+
if (options.web) {
|
|
89
|
+
await webViewCommand(runDirArg, options);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (options.port !== undefined || options.open === false) {
|
|
93
|
+
throw new InvalidArgumentError("--port and --no-open require --web");
|
|
94
|
+
}
|
|
95
|
+
if (runDirArg) {
|
|
96
|
+
const path = resolve(runDirArg);
|
|
97
|
+
if (!(await ensureDirectory("view", path))) return;
|
|
98
|
+
if (isExperimentRun(path)) {
|
|
99
|
+
const experiment = await loadExperimentRun(path);
|
|
100
|
+
if (!experiment) {
|
|
101
|
+
process.stderr.write(`lux view: invalid experiment at ${path}\n`);
|
|
102
|
+
failWith(1);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(
|
|
106
|
+
options.json
|
|
107
|
+
? `${JSON.stringify({ experimentDir: path, ...experiment }, null, 2)}\n`
|
|
108
|
+
: `${formatExperimentRun(path, experiment)}\n`,
|
|
109
|
+
);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const suite = await loadSuiteRun(path);
|
|
113
|
+
if (suite) {
|
|
114
|
+
process.stdout.write(
|
|
115
|
+
options.json
|
|
116
|
+
? `${JSON.stringify({ suiteDir: path, ...suite }, null, 2)}\n`
|
|
117
|
+
: `${formatSuiteRunDetail(path, suite)}\n`,
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const detail = await loadRunDetail(path);
|
|
122
|
+
process.stdout.write(
|
|
123
|
+
options.json ? `${JSON.stringify(detail, null, 2)}\n` : `${formatRunDetail(detail)}\n`,
|
|
124
|
+
);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const config = await loadLuxConfig(process.cwd());
|
|
129
|
+
const runsRoot = options.runsRoot ? resolve(options.runsRoot) : config.runsRoot;
|
|
130
|
+
if (options.experiments) {
|
|
131
|
+
const experiments = await listExperimentRuns(runsRoot);
|
|
132
|
+
process.stdout.write(
|
|
133
|
+
options.json
|
|
134
|
+
? `${JSON.stringify(experiments, null, 2)}\n`
|
|
135
|
+
: `experiments at ${runsRoot}:\n${formatExperimentRunsTable(experiments)}\n`,
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (options.suites) {
|
|
140
|
+
const suites = await listSuiteRuns(runsRoot);
|
|
141
|
+
process.stdout.write(
|
|
142
|
+
options.json
|
|
143
|
+
? `${JSON.stringify(suites, null, 2)}\n`
|
|
144
|
+
: `suite runs at ${runsRoot}:\n${formatSuiteRunsTable(suites)}\n`,
|
|
145
|
+
);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const all = await listRuns(runsRoot);
|
|
149
|
+
const runs = options.case ? all.filter((run) => run.caseSlug === options.case) : all;
|
|
150
|
+
if (options.json) {
|
|
151
|
+
process.stdout.write(`${JSON.stringify(runs, null, 2)}\n`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!options.case) {
|
|
155
|
+
process.stdout.write(`suite runs at ${runsRoot}:\n`);
|
|
156
|
+
process.stdout.write(`${formatSuiteRunsTable(await listSuiteRuns(runsRoot))}\n\n`);
|
|
157
|
+
}
|
|
158
|
+
process.stdout.write(`runs at ${runsRoot}:\n`);
|
|
159
|
+
process.stdout.write(`${formatRunsTable(runs)}\n`);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const parsePreviewPort = (value: string): number => {
|
|
163
|
+
const port = Number(value);
|
|
164
|
+
if (!Number.isInteger(port) || port < 0 || port > 65_535) {
|
|
165
|
+
throw new InvalidArgumentError("port must be an integer from 0 to 65535");
|
|
166
|
+
}
|
|
167
|
+
return port;
|
|
168
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { RunEvent } from "./events.js";
|
|
3
|
+
|
|
4
|
+
export const ActionCategorySchema = z.enum([
|
|
5
|
+
"query",
|
|
6
|
+
"mutation",
|
|
7
|
+
"durable-mutation",
|
|
8
|
+
"execution",
|
|
9
|
+
"interaction",
|
|
10
|
+
]);
|
|
11
|
+
export type ActionCategory = z.infer<typeof ActionCategorySchema>;
|
|
12
|
+
|
|
13
|
+
export const ActionProgressPayloadSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
name: z.string(),
|
|
16
|
+
category: ActionCategorySchema,
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
export type ActionProgressPayload = z.infer<typeof ActionProgressPayloadSchema>;
|
|
20
|
+
|
|
21
|
+
export const ActionTimingSchema = z.object({
|
|
22
|
+
timeToFirstActionMs: z.exactOptional(z.number().nonnegative()),
|
|
23
|
+
timeToFirstMutationMs: z.exactOptional(z.number().nonnegative()),
|
|
24
|
+
timeToFirstDurableMutationMs: z.exactOptional(z.number().nonnegative()),
|
|
25
|
+
});
|
|
26
|
+
export type ActionTiming = z.infer<typeof ActionTimingSchema>;
|
|
27
|
+
|
|
28
|
+
export const actionTiming = (events: readonly RunEvent[]): ActionTiming => {
|
|
29
|
+
const startEvent = events.find(
|
|
30
|
+
(event) => event.type === "lifecycle" && event.stage === "start-driver",
|
|
31
|
+
);
|
|
32
|
+
const startedAt = startEvent?.type === "lifecycle" ? startEvent.ts : undefined;
|
|
33
|
+
if (startedAt === undefined) return {};
|
|
34
|
+
|
|
35
|
+
const actions = events.flatMap((event) => {
|
|
36
|
+
if (event.type !== "progress" || event.kind !== "action") return [];
|
|
37
|
+
const payload = ActionProgressPayloadSchema.safeParse(event.payload).data;
|
|
38
|
+
return payload ? [{ ...payload, ts: event.ts }] : [];
|
|
39
|
+
});
|
|
40
|
+
const elapsed = (category: "any" | "mutation" | "durable-mutation") => {
|
|
41
|
+
const timestamps = actions.flatMap(({ category: candidate, ts }) => {
|
|
42
|
+
if (category === "any") return [ts];
|
|
43
|
+
if (category === "mutation") {
|
|
44
|
+
return candidate === "mutation" || candidate === "durable-mutation" ? [ts] : [];
|
|
45
|
+
}
|
|
46
|
+
return candidate === category ? [ts] : [];
|
|
47
|
+
});
|
|
48
|
+
return timestamps.length > 0 ? Math.max(0, Math.min(...timestamps) - startedAt) : undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const timeToFirstActionMs = elapsed("any");
|
|
52
|
+
const timeToFirstMutationMs = elapsed("mutation");
|
|
53
|
+
const timeToFirstDurableMutationMs = elapsed("durable-mutation");
|
|
54
|
+
return {
|
|
55
|
+
...(timeToFirstActionMs === undefined ? {} : { timeToFirstActionMs }),
|
|
56
|
+
...(timeToFirstMutationMs === undefined ? {} : { timeToFirstMutationMs }),
|
|
57
|
+
...(timeToFirstDurableMutationMs === undefined ? {} : { timeToFirstDurableMutationMs }),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -7,12 +7,15 @@ import type { Run } from "./run.js";
|
|
|
7
7
|
const DEFAULT_ARTIFACT_READ_BYTES = 5 * 1024 * 1024;
|
|
8
8
|
const MAX_ARTIFACT_READ_BYTES = 10 * 1024 * 1024;
|
|
9
9
|
|
|
10
|
+
export type RunArtifactIssueReason = "absent" | "unreadable";
|
|
11
|
+
|
|
10
12
|
export type RunArtifactInspection = {
|
|
11
13
|
target: string;
|
|
12
14
|
root: string;
|
|
13
15
|
artifact?: Artifact;
|
|
14
16
|
stats?: Stats;
|
|
15
17
|
issue?: string;
|
|
18
|
+
reason?: RunArtifactIssueReason;
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
type RunArtifactEvidence = {
|
|
@@ -82,6 +85,7 @@ export const inspectRunArtifact = async (
|
|
|
82
85
|
target: authoredPath,
|
|
83
86
|
root: runArtifactsRoot(run),
|
|
84
87
|
issue: `${authoredPath} contains a null byte`,
|
|
88
|
+
reason: "unreadable",
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
const matching = run.artifacts.filter((candidate) => candidate.path === authoredPath);
|
|
@@ -91,6 +95,7 @@ export const inspectRunArtifact = async (
|
|
|
91
95
|
target: resolve(root, authoredPath),
|
|
92
96
|
root,
|
|
93
97
|
issue: `${authoredPath} was not captured in the frozen artifact index`,
|
|
98
|
+
reason: "absent",
|
|
94
99
|
};
|
|
95
100
|
}
|
|
96
101
|
if (matching.some((candidate) => candidate.root !== undefined && !isAbsolute(candidate.root))) {
|
|
@@ -98,6 +103,7 @@ export const inspectRunArtifact = async (
|
|
|
98
103
|
target: authoredPath,
|
|
99
104
|
root: defaultArtifactsRoot(run),
|
|
100
105
|
issue: `${authoredPath} declares a non-absolute artifact root`,
|
|
106
|
+
reason: "unreadable",
|
|
101
107
|
};
|
|
102
108
|
}
|
|
103
109
|
const artifact = matching[0];
|
|
@@ -108,6 +114,7 @@ export const inspectRunArtifact = async (
|
|
|
108
114
|
target: authoredPath,
|
|
109
115
|
root: defaultArtifactsRoot(run),
|
|
110
116
|
issue: `${authoredPath} is ambiguous across multiple artifact roots`,
|
|
117
|
+
reason: "unreadable",
|
|
111
118
|
};
|
|
112
119
|
}
|
|
113
120
|
// A non-default root is trusted only for an exact artifact-index entry.
|
|
@@ -116,7 +123,12 @@ export const inspectRunArtifact = async (
|
|
|
116
123
|
const root = matchingRoots[0] ?? defaultArtifactsRoot(run);
|
|
117
124
|
const target = resolve(root, authoredPath);
|
|
118
125
|
if (pathEscapesRoot(root, target)) {
|
|
119
|
-
return {
|
|
126
|
+
return {
|
|
127
|
+
target,
|
|
128
|
+
root,
|
|
129
|
+
issue: `${authoredPath} escapes the captured artifact root`,
|
|
130
|
+
reason: "unreadable",
|
|
131
|
+
};
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
const path = relative(root, target);
|
|
@@ -134,7 +146,13 @@ export const inspectRunArtifact = async (
|
|
|
134
146
|
} catch (error) {
|
|
135
147
|
const code = (error as NodeJS.ErrnoException).code;
|
|
136
148
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
137
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
target,
|
|
151
|
+
root,
|
|
152
|
+
...identified,
|
|
153
|
+
issue: `${authoredPath} not found`,
|
|
154
|
+
reason: "absent",
|
|
155
|
+
};
|
|
138
156
|
}
|
|
139
157
|
throw error;
|
|
140
158
|
}
|
|
@@ -144,16 +162,17 @@ export const inspectRunArtifact = async (
|
|
|
144
162
|
root,
|
|
145
163
|
...identified,
|
|
146
164
|
issue: `${authoredPath} resolves through a symlink`,
|
|
165
|
+
reason: "unreadable",
|
|
147
166
|
};
|
|
148
167
|
}
|
|
149
168
|
if (index < components.length - 1 && !stats.isDirectory()) {
|
|
150
|
-
return { target, root, ...identified, issue: `${authoredPath} not found
|
|
169
|
+
return { target, root, ...identified, issue: `${authoredPath} not found`, reason: "absent" };
|
|
151
170
|
}
|
|
152
171
|
if (index === components.length - 1) {
|
|
153
172
|
return { target, root, ...identified, stats };
|
|
154
173
|
}
|
|
155
174
|
}
|
|
156
|
-
return { target, root, ...identified, issue: `${authoredPath} not found
|
|
175
|
+
return { target, root, ...identified, issue: `${authoredPath} not found`, reason: "absent" };
|
|
157
176
|
};
|
|
158
177
|
|
|
159
178
|
export type ReadRunArtifactTextOptions = {
|
|
@@ -247,3 +266,27 @@ export const readRunArtifactText = async (
|
|
|
247
266
|
): Promise<string> => {
|
|
248
267
|
return (await readRunArtifactBytes(run, authoredPath, options)).toString("utf8");
|
|
249
268
|
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Read optional artifact evidence. Returns null when the artifact was never
|
|
272
|
+
* captured, and still throws when one exists but is unsafe to consume, so a
|
|
273
|
+
* traversal or symlink violation is never mistaken for missing evidence.
|
|
274
|
+
*/
|
|
275
|
+
export const readRunArtifactTextIfPresent = async (
|
|
276
|
+
run: RunArtifactEvidence,
|
|
277
|
+
authoredPath: string,
|
|
278
|
+
options: ReadRunArtifactTextOptions = {},
|
|
279
|
+
): Promise<string | null> => {
|
|
280
|
+
const inspected = await inspectRunArtifact(run, authoredPath);
|
|
281
|
+
if (inspected.reason === "absent") return null;
|
|
282
|
+
if (!inspected.stats?.isFile()) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
inspected.issue ??
|
|
285
|
+
(inspected.stats ? `${authoredPath} is not a file` : `${authoredPath} not found`),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const maxBytes = options.maxBytes ?? DEFAULT_ARTIFACT_READ_BYTES;
|
|
289
|
+
return (
|
|
290
|
+
await readInspectedFileBytes(inspected.target, inspected.stats, maxBytes, authoredPath)
|
|
291
|
+
).toString("utf8");
|
|
292
|
+
};
|
package/src/core/assertion.ts
CHANGED
|
@@ -100,6 +100,13 @@ export const assertionPluginIssue = (definition: {
|
|
|
100
100
|
* Define a schema-validated assertion plugin while inferring the config passed
|
|
101
101
|
* to `evaluate` from the schema.
|
|
102
102
|
*/
|
|
103
|
+
export const baseAssertionFields = {
|
|
104
|
+
id: z.string().min(1).optional(),
|
|
105
|
+
weight: z.number().positive().optional(),
|
|
106
|
+
threshold: z.number().min(0).max(1).optional(),
|
|
107
|
+
name: z.string().optional(),
|
|
108
|
+
} as const;
|
|
109
|
+
|
|
103
110
|
export const defineAssertion = <S extends z.ZodObject>(
|
|
104
111
|
definition: AssertionPluginDefinition<S>,
|
|
105
112
|
): AssertionPlugin<SchemaAssertion<S>, SchemaAssertionInput<S>> => {
|
|
@@ -141,11 +148,19 @@ export const GradingAssertionEntrySchema = z.object({
|
|
|
141
148
|
});
|
|
142
149
|
export type GradingAssertionEntry = z.infer<typeof GradingAssertionEntrySchema>;
|
|
143
150
|
|
|
151
|
+
const SkippedGradingAssertionSchema = z.object({
|
|
152
|
+
id: z.string(),
|
|
153
|
+
type: z.string(),
|
|
154
|
+
name: z.string(),
|
|
155
|
+
reason: z.literal("judge-disabled"),
|
|
156
|
+
});
|
|
157
|
+
|
|
144
158
|
export const GradingReportSchema = z.object({
|
|
145
159
|
schemaVersion: z.literal(1),
|
|
146
160
|
casePassed: z.boolean().nullable(),
|
|
147
161
|
caseScore: z.number().nullable(),
|
|
148
162
|
assertions: z.array(GradingAssertionEntrySchema),
|
|
163
|
+
skippedAssertions: z.array(SkippedGradingAssertionSchema).optional(),
|
|
149
164
|
/**
|
|
150
165
|
* Present when the subject run or grading did not complete. Assertions may
|
|
151
166
|
* still contain partial diagnostics, but the report is not a clean verdict.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const DriverMetricUnitSchema = z.enum([
|
|
4
|
+
"number",
|
|
5
|
+
"count",
|
|
6
|
+
"milliseconds",
|
|
7
|
+
"bytes",
|
|
8
|
+
"tokens",
|
|
9
|
+
"usd",
|
|
10
|
+
"ratio",
|
|
11
|
+
"percent",
|
|
12
|
+
"score",
|
|
13
|
+
]);
|
|
14
|
+
export type DriverMetricUnit = z.infer<typeof DriverMetricUnitSchema>;
|
|
15
|
+
|
|
16
|
+
export const DriverMetricDescriptorSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
unit: DriverMetricUnitSchema.default("number"),
|
|
19
|
+
label: z.string().min(1).max(120).optional(),
|
|
20
|
+
better: z.enum(["lower", "higher", "neutral"]).default("neutral"),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
export type DriverMetricDescriptor = z.infer<typeof DriverMetricDescriptorSchema>;
|
|
24
|
+
|
|
25
|
+
export const DriverMetricSchema = DriverMetricDescriptorSchema.extend({
|
|
26
|
+
value: z.number().finite(),
|
|
27
|
+
}).strict();
|
|
28
|
+
export type DriverMetric = z.infer<typeof DriverMetricSchema>;
|
|
29
|
+
|
|
30
|
+
export const DriverMetricsSchema = z.record(z.string().min(1).max(120), DriverMetricSchema);
|
|
31
|
+
export type DriverMetrics = z.infer<typeof DriverMetricsSchema>;
|
|
32
|
+
|
|
33
|
+
const ReportedDriverMetricSchema = z.union([z.number().finite(), DriverMetricSchema]);
|
|
34
|
+
export const ReportedDriverMetricsSchema = z.record(
|
|
35
|
+
z.string().min(1).max(120),
|
|
36
|
+
ReportedDriverMetricSchema,
|
|
37
|
+
);
|
|
38
|
+
export type ReportedDriverMetrics = z.input<typeof ReportedDriverMetricsSchema>;
|
|
39
|
+
|
|
40
|
+
export const normalizeDriverMetrics = (value: unknown): DriverMetrics => {
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
Object.entries(value).flatMap(([name, metric]) => {
|
|
44
|
+
if (name.length === 0 || name.length > 120) return [];
|
|
45
|
+
const parsed = DriverMetricSchema.safeParse(
|
|
46
|
+
typeof metric === "number" ? { value: metric } : metric,
|
|
47
|
+
).data;
|
|
48
|
+
return parsed ? [[name, parsed]] : [];
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
};
|
package/src/core/events.ts
CHANGED
|
@@ -69,8 +69,8 @@ export const DoneSchema = z.object({
|
|
|
69
69
|
exitReason: z.literal("done"),
|
|
70
70
|
/**
|
|
71
71
|
* Driver-defined summary. The orchestrator recognizes `usage`, `cost`,
|
|
72
|
-
* `modelCalls`, and `
|
|
73
|
-
* for domain-specific
|
|
72
|
+
* `modelCalls`, `unpricedModelCalls`, and scalar `metrics` when present while
|
|
73
|
+
* preserving room for other domain-specific evidence.
|
|
74
74
|
*/
|
|
75
75
|
summary: z.unknown().optional(),
|
|
76
76
|
});
|
|
@@ -82,6 +82,7 @@ export type ErrorEventReason = z.infer<typeof ErrorEventReasonSchema>;
|
|
|
82
82
|
export const ErrorEventSchema = z.object({
|
|
83
83
|
exitReason: ErrorEventReasonSchema,
|
|
84
84
|
reason: z.string(),
|
|
85
|
+
summary: z.unknown().optional(),
|
|
85
86
|
});
|
|
86
87
|
export type ErrorEvent = z.infer<typeof ErrorEventSchema>;
|
|
87
88
|
|
package/src/core/experiment.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { DriverMetricDescriptorSchema } from "./driver-metrics.js";
|
|
2
3
|
import { ConfidenceConfigSchema } from "./statistics.js";
|
|
3
4
|
import { TokenUsageSchema } from "./usage.js";
|
|
4
5
|
import { formatZodIssues } from "./zod.js";
|
|
@@ -415,6 +416,7 @@ export const CandidateEvaluationSchema = z.object({
|
|
|
415
416
|
passRate: z.number().min(0).max(1),
|
|
416
417
|
casePassRates: z.record(z.string(), z.number().min(0).max(1)),
|
|
417
418
|
metrics: z.record(z.string(), z.number()),
|
|
419
|
+
metricDefinitions: z.exactOptional(z.record(z.string(), DriverMetricDescriptorSchema)),
|
|
418
420
|
metricCalls: z.exactOptional(z.number().int().nonnegative()),
|
|
419
421
|
judgeCalls: z.exactOptional(z.number().int().nonnegative()),
|
|
420
422
|
unpricedModelCalls: z.exactOptional(z.number().int().nonnegative()),
|
package/src/core/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./action-events.js";
|
|
1
2
|
export * from "./annotation.js";
|
|
2
3
|
export * from "./answerer.js";
|
|
3
4
|
export * from "./artifact-evidence.js";
|
|
@@ -6,6 +7,7 @@ export * from "./assertion.js";
|
|
|
6
7
|
export * from "./case.js";
|
|
7
8
|
export * from "./conversation-turn.js";
|
|
8
9
|
export * from "./driver.js";
|
|
10
|
+
export * from "./driver-metrics.js";
|
|
9
11
|
export * from "./emitter.js";
|
|
10
12
|
export * from "./events.js";
|
|
11
13
|
export * from "./exec.js";
|
package/src/core/run.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { ActionTimingSchema } from "./action-events.js";
|
|
2
3
|
import { EvalCaseSchema } from "./case.js";
|
|
3
4
|
import { ArtifactSchema } from "./driver.js";
|
|
5
|
+
import { DriverMetricsSchema } from "./driver-metrics.js";
|
|
4
6
|
import { ErrorEventReasonSchema, ReadyStateSchema, RunEventSchema } from "./events.js";
|
|
5
7
|
import { ToolOutcomeSummarySchema } from "./tool-events.js";
|
|
6
8
|
import { RunUsageSchema } from "./usage.js";
|
|
@@ -112,6 +114,8 @@ export const RunMetadataSchema = z.object({
|
|
|
112
114
|
}),
|
|
113
115
|
),
|
|
114
116
|
cost: z.exactOptional(z.object({ usd: z.exactOptional(z.number()) })),
|
|
117
|
+
/** Driver-defined scalar measurements with display and optimization semantics. */
|
|
118
|
+
driverMetrics: z.exactOptional(DriverMetricsSchema),
|
|
115
119
|
interruptCounts: z.object({ ask: z.number(), approve: z.number(), custom: z.number() }),
|
|
116
120
|
/** Legacy raw count of progress events whose kind is `tool-call`. */
|
|
117
121
|
toolCallCount: z.exactOptional(z.number()),
|
|
@@ -124,6 +128,8 @@ export const RunMetadataSchema = z.object({
|
|
|
124
128
|
),
|
|
125
129
|
/** Wall-clock per phase, derived from the lifecycle event stream. */
|
|
126
130
|
phaseDurationMs: z.exactOptional(PhaseDurationsSchema),
|
|
131
|
+
/** Driver-reported semantic action latency, measured from `start-driver`. */
|
|
132
|
+
actionTiming: z.exactOptional(ActionTimingSchema),
|
|
127
133
|
/**
|
|
128
134
|
* Harness/infrastructure failure that invalidates this run as candidate
|
|
129
135
|
* evidence. A driver-reported subject failure intentionally leaves this
|
package/src/core/wire.ts
CHANGED
|
@@ -47,6 +47,7 @@ const ErrorEventSchema = z.object({
|
|
|
47
47
|
type: z.literal("error"),
|
|
48
48
|
exitReason: ErrorEventReasonSchema.default("error"),
|
|
49
49
|
reason: z.string(),
|
|
50
|
+
summary: z.unknown().optional(),
|
|
50
51
|
});
|
|
51
52
|
|
|
52
53
|
export const WireEventSchema = z.discriminatedUnion("type", [
|
|
@@ -85,7 +86,10 @@ const wireEventToParsed = (event: WireEvent): ParsedDriverEvent => {
|
|
|
85
86
|
if (event.type === "done") {
|
|
86
87
|
return { tag: "done", done: { exitReason: "done", summary: event.summary } };
|
|
87
88
|
}
|
|
88
|
-
return {
|
|
89
|
+
return {
|
|
90
|
+
tag: "error",
|
|
91
|
+
error: { exitReason: event.exitReason, reason: event.reason, summary: event.summary },
|
|
92
|
+
};
|
|
89
93
|
};
|
|
90
94
|
|
|
91
95
|
/**
|
package/src/index.ts
CHANGED
|
@@ -16,11 +16,6 @@ export {
|
|
|
16
16
|
PersonaAnswererConfigSchema,
|
|
17
17
|
personaAnswerer,
|
|
18
18
|
} from "./answerers/persona/index.js";
|
|
19
|
-
export {
|
|
20
|
-
type ScriptedAnswererConfig,
|
|
21
|
-
ScriptedAnswererConfigSchema,
|
|
22
|
-
scriptedAnswerer,
|
|
23
|
-
} from "./answerers/scripted/index.js";
|
|
24
19
|
export {
|
|
25
20
|
type TerminalAnswererConfig,
|
|
26
21
|
TerminalAnswererConfigSchema,
|
|
@@ -92,6 +87,8 @@ export {
|
|
|
92
87
|
registerCoreAssertions,
|
|
93
88
|
runSucceededAssertion,
|
|
94
89
|
subagentToolCalls,
|
|
90
|
+
type TimeToFirstActionUnderConfig,
|
|
91
|
+
TimeToFirstActionUnderConfigSchema,
|
|
95
92
|
type TokensUnderConfig,
|
|
96
93
|
TokensUnderConfigSchema,
|
|
97
94
|
type ToolCall,
|
|
@@ -109,6 +106,7 @@ export {
|
|
|
109
106
|
ToolResultIsErrorConfigSchema,
|
|
110
107
|
type ToolResultJsonPointerEqualsConfig,
|
|
111
108
|
ToolResultJsonPointerEqualsConfigSchema,
|
|
109
|
+
timeToFirstActionUnderAssertion,
|
|
112
110
|
tokensUnderAssertion,
|
|
113
111
|
toolCallCountAssertion,
|
|
114
112
|
toolCalledAssertion,
|
|
@@ -146,6 +144,15 @@ export {
|
|
|
146
144
|
rubricAssertion,
|
|
147
145
|
} from "./assertions/rubric/index.js";
|
|
148
146
|
// Authoring and extension contracts.
|
|
147
|
+
export {
|
|
148
|
+
type ActionCategory,
|
|
149
|
+
ActionCategorySchema,
|
|
150
|
+
type ActionProgressPayload,
|
|
151
|
+
ActionProgressPayloadSchema,
|
|
152
|
+
type ActionTiming,
|
|
153
|
+
ActionTimingSchema,
|
|
154
|
+
actionTiming,
|
|
155
|
+
} from "./core/action-events.js";
|
|
149
156
|
export {
|
|
150
157
|
defineGraderAnnotations,
|
|
151
158
|
type GraderAnnotation,
|
|
@@ -232,6 +239,19 @@ export {
|
|
|
232
239
|
ResolvedFixturesSchema,
|
|
233
240
|
type StartContext,
|
|
234
241
|
} from "./core/driver.js";
|
|
242
|
+
export {
|
|
243
|
+
type DriverMetric,
|
|
244
|
+
type DriverMetricDescriptor,
|
|
245
|
+
DriverMetricDescriptorSchema,
|
|
246
|
+
DriverMetricSchema,
|
|
247
|
+
type DriverMetrics,
|
|
248
|
+
DriverMetricsSchema,
|
|
249
|
+
type DriverMetricUnit,
|
|
250
|
+
DriverMetricUnitSchema,
|
|
251
|
+
normalizeDriverMetrics,
|
|
252
|
+
type ReportedDriverMetrics,
|
|
253
|
+
ReportedDriverMetricsSchema,
|
|
254
|
+
} from "./core/driver-metrics.js";
|
|
235
255
|
export { type EventHandler, TypedEmitter } from "./core/emitter.js";
|
|
236
256
|
export {
|
|
237
257
|
type Answer,
|
|
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import {
|
|
5
|
+
type ActionTiming,
|
|
6
|
+
type DriverMetrics,
|
|
5
7
|
emptyRunUsage,
|
|
6
8
|
type GradingReport,
|
|
7
9
|
GradingReportSchema,
|
|
@@ -48,6 +50,8 @@ export type RunComparison = {
|
|
|
48
50
|
exitReason: { a: string; b: string };
|
|
49
51
|
durationMsA: number | null;
|
|
50
52
|
durationMsB: number | null;
|
|
53
|
+
actionTiming: { a: ActionTiming; b: ActionTiming };
|
|
54
|
+
driverMetrics: { a: DriverMetrics; b: DriverMetrics };
|
|
51
55
|
interruptCounts: {
|
|
52
56
|
a: { ask: number; approve: number; custom: number };
|
|
53
57
|
b: { ask: number; approve: number; custom: number };
|
|
@@ -260,6 +264,14 @@ export const compareRuns = async (input: ComparisonInput): Promise<RunComparison
|
|
|
260
264
|
},
|
|
261
265
|
durationMsA: durationFromMeta(a.runJson),
|
|
262
266
|
durationMsB: durationFromMeta(b.runJson),
|
|
267
|
+
actionTiming: {
|
|
268
|
+
a: a.runJson?.actionTiming ?? {},
|
|
269
|
+
b: b.runJson?.actionTiming ?? {},
|
|
270
|
+
},
|
|
271
|
+
driverMetrics: {
|
|
272
|
+
a: a.runJson?.driverMetrics ?? {},
|
|
273
|
+
b: b.runJson?.driverMetrics ?? {},
|
|
274
|
+
},
|
|
263
275
|
interruptCounts: {
|
|
264
276
|
a: a.runJson?.interruptCounts ?? { ask: 0, approve: 0, custom: 0 },
|
|
265
277
|
b: b.runJson?.interruptCounts ?? { ask: 0, approve: 0, custom: 0 },
|
|
@@ -4,7 +4,6 @@ import { pathToFileURL } from "node:url";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { claudeCodeAnswerer } from "../answerers/claude-code/index.js";
|
|
6
6
|
import { personaAnswerer } from "../answerers/persona/index.js";
|
|
7
|
-
import { scriptedAnswerer } from "../answerers/scripted/index.js";
|
|
8
7
|
import { terminalAnswerer } from "../answerers/terminal/index.js";
|
|
9
8
|
import { allCoreAssertions } from "../assertions/core/index.js";
|
|
10
9
|
import { allMcpAssertions } from "../assertions/mcp/index.js";
|
|
@@ -182,7 +181,6 @@ const builtinDrivers: DriverPlugin<never>[] = [
|
|
|
182
181
|
const builtinAnswerers: AnswererPlugin<never>[] = [
|
|
183
182
|
personaAnswerer,
|
|
184
183
|
claudeCodeAnswerer,
|
|
185
|
-
scriptedAnswerer,
|
|
186
184
|
terminalAnswerer,
|
|
187
185
|
];
|
|
188
186
|
export const buildRegistryFromConfig = (config: ResolvedLuxConfig): Registry => {
|