@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.
Files changed (169) hide show
  1. package/lib/answerers/terminal/index.js +1 -1
  2. package/lib/answerers/terminal/index.js.map +1 -1
  3. package/lib/assertions/authoring.d.ts +2 -2
  4. package/lib/assertions/authoring.d.ts.map +1 -1
  5. package/lib/assertions/authoring.js.map +1 -1
  6. package/lib/assertions/core/helpers.d.ts +2 -7
  7. package/lib/assertions/core/helpers.d.ts.map +1 -1
  8. package/lib/assertions/core/helpers.js +2 -6
  9. package/lib/assertions/core/helpers.js.map +1 -1
  10. package/lib/assertions/core/index.d.ts.map +1 -1
  11. package/lib/assertions/core/index.js +2 -1
  12. package/lib/assertions/core/index.js.map +1 -1
  13. package/lib/assertions/core/resource-checks.d.ts +19 -0
  14. package/lib/assertions/core/resource-checks.d.ts.map +1 -1
  15. package/lib/assertions/core/resource-checks.js +32 -0
  16. package/lib/assertions/core/resource-checks.js.map +1 -1
  17. package/lib/cli/program.d.ts.map +1 -1
  18. package/lib/cli/program.js +12 -67
  19. package/lib/cli/program.js.map +1 -1
  20. package/lib/cli/render/reporter.d.ts.map +1 -1
  21. package/lib/cli/render/reporter.js +16 -3
  22. package/lib/cli/render/reporter.js.map +1 -1
  23. package/lib/cli/run-options.d.ts +1 -0
  24. package/lib/cli/run-options.d.ts.map +1 -1
  25. package/lib/cli/run-options.js.map +1 -1
  26. package/lib/cli/view.d.ts +13 -0
  27. package/lib/cli/view.d.ts.map +1 -0
  28. package/lib/cli/view.js +126 -0
  29. package/lib/cli/view.js.map +1 -0
  30. package/lib/core/action-events.d.ts +29 -0
  31. package/lib/core/action-events.d.ts.map +1 -0
  32. package/lib/core/action-events.js +51 -0
  33. package/lib/core/action-events.js.map +1 -0
  34. package/lib/core/artifact-evidence.d.ts +8 -0
  35. package/lib/core/artifact-evidence.d.ts.map +1 -1
  36. package/lib/core/artifact-evidence.js +36 -4
  37. package/lib/core/artifact-evidence.js.map +1 -1
  38. package/lib/core/assertion.d.ts +12 -0
  39. package/lib/core/assertion.d.ts.map +1 -1
  40. package/lib/core/assertion.js +13 -0
  41. package/lib/core/assertion.js.map +1 -1
  42. package/lib/core/driver-metrics.d.ts +98 -0
  43. package/lib/core/driver-metrics.d.ts.map +1 -0
  44. package/lib/core/driver-metrics.js +36 -0
  45. package/lib/core/driver-metrics.js.map +1 -0
  46. package/lib/core/events.d.ts +2 -0
  47. package/lib/core/events.d.ts.map +1 -1
  48. package/lib/core/events.js +3 -2
  49. package/lib/core/events.js.map +1 -1
  50. package/lib/core/experiment.d.ts +19 -0
  51. package/lib/core/experiment.d.ts.map +1 -1
  52. package/lib/core/experiment.js +2 -0
  53. package/lib/core/experiment.js.map +1 -1
  54. package/lib/core/index.d.ts +2 -0
  55. package/lib/core/index.d.ts.map +1 -1
  56. package/lib/core/index.js +2 -0
  57. package/lib/core/index.js.map +1 -1
  58. package/lib/core/run.d.ts +51 -0
  59. package/lib/core/run.d.ts.map +1 -1
  60. package/lib/core/run.js +6 -0
  61. package/lib/core/run.js.map +1 -1
  62. package/lib/core/wire.d.ts +1 -0
  63. package/lib/core/wire.d.ts.map +1 -1
  64. package/lib/core/wire.js +5 -1
  65. package/lib/core/wire.js.map +1 -1
  66. package/lib/index.d.ts +3 -2
  67. package/lib/index.d.ts.map +1 -1
  68. package/lib/index.js +3 -2
  69. package/lib/index.js.map +1 -1
  70. package/lib/orchestrator/compare.d.ts +9 -1
  71. package/lib/orchestrator/compare.d.ts.map +1 -1
  72. package/lib/orchestrator/compare.js +8 -0
  73. package/lib/orchestrator/compare.js.map +1 -1
  74. package/lib/orchestrator/config.d.ts.map +1 -1
  75. package/lib/orchestrator/config.js +0 -2
  76. package/lib/orchestrator/config.js.map +1 -1
  77. package/lib/orchestrator/experiment-contracts.d.ts +19 -0
  78. package/lib/orchestrator/experiment-contracts.d.ts.map +1 -1
  79. package/lib/orchestrator/experiment-evaluation.d.ts.map +1 -1
  80. package/lib/orchestrator/experiment-evaluation.js +17 -0
  81. package/lib/orchestrator/experiment-evaluation.js.map +1 -1
  82. package/lib/orchestrator/experiment-report.d.ts +19 -0
  83. package/lib/orchestrator/experiment-report.d.ts.map +1 -1
  84. package/lib/orchestrator/experiment-runs.d.ts +38 -0
  85. package/lib/orchestrator/experiment-runs.d.ts.map +1 -1
  86. package/lib/orchestrator/grader-experiment.d.ts +12 -0
  87. package/lib/orchestrator/grader-experiment.d.ts.map +1 -1
  88. package/lib/orchestrator/grading.d.ts +2 -1
  89. package/lib/orchestrator/grading.d.ts.map +1 -1
  90. package/lib/orchestrator/grading.js +16 -4
  91. package/lib/orchestrator/grading.js.map +1 -1
  92. package/lib/orchestrator/index.d.ts +1 -1
  93. package/lib/orchestrator/index.d.ts.map +1 -1
  94. package/lib/orchestrator/index.js +1 -1
  95. package/lib/orchestrator/index.js.map +1 -1
  96. package/lib/orchestrator/list-runs.d.ts +6 -1
  97. package/lib/orchestrator/list-runs.d.ts.map +1 -1
  98. package/lib/orchestrator/list-runs.js +15 -1
  99. package/lib/orchestrator/list-runs.js.map +1 -1
  100. package/lib/orchestrator/orchestrator.d.ts +3 -0
  101. package/lib/orchestrator/orchestrator.d.ts.map +1 -1
  102. package/lib/orchestrator/orchestrator.js +3 -0
  103. package/lib/orchestrator/orchestrator.js.map +1 -1
  104. package/lib/orchestrator/regrade.d.ts +6 -0
  105. package/lib/orchestrator/regrade.d.ts.map +1 -1
  106. package/lib/orchestrator/run-execution.d.ts.map +1 -1
  107. package/lib/orchestrator/run-execution.js +13 -3
  108. package/lib/orchestrator/run-execution.js.map +1 -1
  109. package/lib/orchestrator/suite-compare.d.ts +34 -0
  110. package/lib/orchestrator/suite-compare.d.ts.map +1 -1
  111. package/lib/orchestrator/suite-summary.d.ts +68 -0
  112. package/lib/orchestrator/suite-summary.d.ts.map +1 -1
  113. package/lib/orchestrator/suite-summary.js +22 -7
  114. package/lib/orchestrator/suite-summary.js.map +1 -1
  115. package/lib/previewer/artifacts.d.ts +25 -0
  116. package/lib/previewer/artifacts.d.ts.map +1 -0
  117. package/lib/previewer/artifacts.js +180 -0
  118. package/lib/previewer/artifacts.js.map +1 -0
  119. package/lib/previewer/server.d.ts +104 -0
  120. package/lib/previewer/server.d.ts.map +1 -0
  121. package/lib/previewer/server.js +541 -0
  122. package/lib/previewer/server.js.map +1 -0
  123. package/package.json +2 -2
  124. package/skills/lux/SKILL.md +7 -3
  125. package/skills/lux/references/cli.md +25 -2
  126. package/skills/lux/references/eval-authoring.md +158 -4
  127. package/skills/lux-answerer/SKILL.md +1 -1
  128. package/src/answerers/terminal/index.ts +1 -1
  129. package/src/assertions/authoring.ts +2 -0
  130. package/src/assertions/core/helpers.ts +3 -6
  131. package/src/assertions/core/index.ts +2 -0
  132. package/src/assertions/core/resource-checks.ts +35 -0
  133. package/src/cli/program.ts +14 -93
  134. package/src/cli/render/reporter.ts +18 -3
  135. package/src/cli/run-options.ts +1 -0
  136. package/src/cli/view.ts +168 -0
  137. package/src/core/action-events.ts +59 -0
  138. package/src/core/artifact-evidence.ts +47 -4
  139. package/src/core/assertion.ts +15 -0
  140. package/src/core/driver-metrics.ts +51 -0
  141. package/src/core/events.ts +3 -2
  142. package/src/core/experiment.ts +2 -0
  143. package/src/core/index.ts +2 -0
  144. package/src/core/run.ts +6 -0
  145. package/src/core/wire.ts +5 -1
  146. package/src/index.ts +25 -5
  147. package/src/orchestrator/compare.ts +12 -0
  148. package/src/orchestrator/config.ts +0 -2
  149. package/src/orchestrator/experiment-evaluation.ts +23 -0
  150. package/src/orchestrator/grading.ts +21 -2
  151. package/src/orchestrator/index.ts +1 -0
  152. package/src/orchestrator/list-runs.ts +25 -0
  153. package/src/orchestrator/orchestrator.ts +6 -0
  154. package/src/orchestrator/run-execution.ts +14 -1
  155. package/src/orchestrator/suite-summary.ts +36 -8
  156. package/src/previewer/artifacts.ts +221 -0
  157. package/src/previewer/server.ts +763 -0
  158. package/viewer/app.css +2022 -0
  159. package/viewer/app.js +601 -0
  160. package/viewer/artifact-view.js +48 -0
  161. package/viewer/charts.js +86 -0
  162. package/viewer/detail.js +1108 -0
  163. package/viewer/format.js +129 -0
  164. package/viewer/index.html +151 -0
  165. package/lib/answerers/scripted/index.d.ts +0 -8
  166. package/lib/answerers/scripted/index.d.ts.map +0 -1
  167. package/lib/answerers/scripted/index.js +0 -163
  168. package/lib/answerers/scripted/index.js.map +0 -1
  169. package/src/answerers/scripted/index.ts +0 -186
@@ -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 { target, root, issue: `${authoredPath} escapes the captured artifact root` };
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 { target, root, ...identified, issue: `${authoredPath} not found` };
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
+ };
@@ -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
+ };
@@ -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 `unpricedModelCalls` when present while preserving room
73
- * for domain-specific measurements.
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
 
@@ -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 { tag: "error", error: { exitReason: event.exitReason, reason: event.reason } };
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 => {