@tea-agent/loop-agent 0.44.0-next.10 → 0.44.0-next.12

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 (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/dag-pi/sessions/index.js +6 -0
  4. package/dist/executors/dag-pi/sessions/plan-batches.js +280 -0
  5. package/dist/executors/dag-pi/sessions/plan-prompts.js +367 -0
  6. package/dist/executors/dag-pi/sessions/scout-parallel.js +197 -0
  7. package/dist/executors/dag-pi/sessions/segmented-plan.js +1184 -0
  8. package/dist/executors/dag-pi/sessions/writer-evidence.js +60 -0
  9. package/dist/executors/dag-pi-executor.js +12 -2075
  10. package/dist/executors/shell-executor.js +39 -11
  11. package/dist/worker/console/routes.js +5 -0
  12. package/dist/worker/console/static/app-icon.svg +39 -0
  13. package/dist/worker/console/static/index.html +3 -0
  14. package/dist/worker/console/static/manifest.webmanifest +19 -0
  15. package/dist/worker/observe/static/console-theme.js +38 -0
  16. package/dist/workflows/dag/checkpoint.js +686 -0
  17. package/dist/workflows/dag/frontend-repair.js +24 -0
  18. package/dist/workflows/dag/frontend-review-scopes.js +10 -2
  19. package/dist/workflows/dag/frontend-test-execution-evidence.js +163 -67
  20. package/dist/workflows/dag/frontend-test-framework-adapters.js +309 -0
  21. package/dist/workflows/dag/hybrid/sources.js +1812 -0
  22. package/dist/workflows/dag/hybrid/templates/backend-test.js +1807 -0
  23. package/dist/workflows/dag/hybrid/templates/frontend-test.js +702 -0
  24. package/dist/workflows/dag/hybrid/templates/frontend.js +1529 -0
  25. package/dist/workflows/dag/hybrid/templates/index.js +8 -0
  26. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +449 -0
  27. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +495 -0
  28. package/dist/workflows/dag/hybrid/templates/shared.js +101 -0
  29. package/dist/workflows/dag/hybrid/templates/standard.js +265 -0
  30. package/dist/workflows/dag/hybrid/types.js +32 -0
  31. package/dist/workflows/dag/init-hybrid.js +42 -7162
  32. package/dist/workflows/dag/runner.js +11 -1178
  33. package/dist/workflows/dag/terminal-status.js +502 -0
  34. package/docs/architecture/dag-execution.md +4 -4
  35. package/package.json +1 -1
@@ -1,5 +1,19 @@
1
1
  import { z } from "zod";
2
2
  import { FRONTEND_NO_VERIFICATION_MARKER_TEXT, } from "./frontend-verification-trace.js";
3
+ import { FRONTEND_TEST_OBSERVATION_FAILURES } from "./frontend-test-execution-evidence.js";
4
+ /**
5
+ * Substrings that prove a behavior-verification failure came from the
6
+ * verification configuration rather than the candidate's code. Kept next to the
7
+ * taxonomy so a new failure code is routed deliberately instead of falling
8
+ * through to the string-blob heuristics below.
9
+ */
10
+ const FRONTEND_TEST_VERIFICATION_CONFIG_MARKERS = [
11
+ FRONTEND_TEST_OBSERVATION_FAILURES.bindingDrift,
12
+ FRONTEND_TEST_OBSERVATION_FAILURES.reportProtocol,
13
+ FRONTEND_TEST_OBSERVATION_FAILURES.capabilityGap,
14
+ "FRONTEND_TEST_OBSERVATION_INVALID",
15
+ "frontend test execution report unavailable",
16
+ ];
3
17
  export const frontendRepairFailureClassSchema = z.enum([
4
18
  "typecheck",
5
19
  "build",
@@ -73,6 +87,16 @@ export function classifyFrontendFailure(input) {
73
87
  if (input.failureCategory === "write-guard") {
74
88
  return "path";
75
89
  }
90
+ // Verification-config faults must never be mistaken for product defects.
91
+ // A missing/unreadable report, a stale report, an unsupported runner or a
92
+ // drifted command binding are all tooling/contract problems: rewriting the
93
+ // candidate's business code cannot fix them and burns a repair round. These
94
+ // are detected structurally, before any blob heuristic can see a runner name
95
+ // (the `unit-test` rule matches the literal word "jest") and misroute the
96
+ // failure to the implementation owner.
97
+ if (FRONTEND_TEST_VERIFICATION_CONFIG_MARKERS.some((marker) => `${input.stderr ?? ""}\n${input.stdout ?? ""}`.includes(marker))) {
98
+ return "contract";
99
+ }
76
100
  if (`${input.stderr ?? ""}\n${input.stdout ?? ""}`.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT)) {
77
101
  // Verification unavailability is a generation-time contract gap: no
78
102
  // authorized verification command exists. Bounded repair cannot fix it;
@@ -1,4 +1,5 @@
1
1
  import { parseFrontendTestExecutionReport } from "./frontend-test-execution-evidence.js";
2
+ import { isFrontendTestReportScheme } from "./frontend-test-framework-adapters.js";
2
3
  import { readFileBoundarySafe } from "../../shared/path-safety.js";
3
4
  import { createHash, randomUUID } from "node:crypto";
4
5
  import { readFile, realpath, lstat } from "node:fs/promises";
@@ -109,12 +110,16 @@ export async function loadFrontendReviewScopes(runDir, phase, targetBytes, works
109
110
  const reportCoverage = new Map();
110
111
  const reportedTests = [];
111
112
  for (const ref of payload.verificationTrace?.testExecutionEvidence ?? []) {
112
- if (!isArtifactReference(ref) || typeof ref.reportPath !== "string" || !/^[a-f0-9]{64}$/.test(ref.reportSha256))
113
+ if (!isArtifactReference(ref) || typeof ref.reportPath !== "string" || !/^[a-f0-9]{64}$/.test(ref.reportSha256) || !isFrontendTestReportScheme(ref.scheme))
113
114
  throw invalidScope("invalid test execution reference");
114
115
  const excerpt = await read(ref.path, ref.sha256);
115
116
  const data = JSON.parse(excerpt.text);
116
117
  if (data.reportSha256 !== ref.reportSha256)
117
118
  throw invalidScope("test execution report binding mismatch");
119
+ // The excerpt records the protocol that produced it. A mismatch means the
120
+ // trace and its report disagree, so refuse rather than guess an adapter.
121
+ if (data.scheme !== ref.scheme)
122
+ throw invalidScope("test execution report protocol binding mismatch");
118
123
  const coverage = reportCoverage.get(ref.reportPath) ?? { count: 0, total: data.total };
119
124
  if (!Number.isInteger(data.total) || data.total < 1 || data.total !== coverage.total || data.offset !== coverage.count || !Array.isArray(data.tests) || !data.tests.length || data.tests.length > 24)
120
125
  throw invalidScope("test report excerpt coverage invalid");
@@ -128,7 +133,10 @@ export async function loadFrontendReviewScopes(runDir, phase, targetBytes, works
128
133
  const cwdPath = await realpath(path.resolve(workspaceRoot, data.cwd));
129
134
  if (cwdPath !== rootPath && !cwdPath.startsWith(rootPath + path.sep))
130
135
  throw invalidScope("test report cwd escaped workspace");
131
- const actual = parseFrontendTestExecutionReport(JSON.parse(report.text), cwdPath, rootPath);
136
+ // Parse with the protocol the runner actually used. Defaulting to
137
+ // Vitest here failed every Playwright/Jest report at review time even
138
+ // though execution and trace had accepted it.
139
+ const actual = parseFrontendTestExecutionReport(JSON.parse(report.text), cwdPath, rootPath, ref.scheme);
132
140
  const expected = actual.slice(data.offset, data.offset + 24).map(test => ({ file: test.file, status: test.status, title: test.title.slice(0, 500), ...(test.title.length > 500 ? { titleTruncated: true } : {}) }));
133
141
  if (data.total !== actual.length || !expected.length || sha256OfCanonicalJson(expected) !== sha256OfCanonicalJson(data.tests))
134
142
  throw invalidScope("test report excerpt differs from actual runner report");
@@ -3,14 +3,32 @@ import { createHash } from "node:crypto";
3
3
  import { pathMatchesPattern } from "../../shared/git-progress.js";
4
4
  import { readFile, realpath } from "node:fs/promises";
5
5
  import { z } from "zod";
6
+ import { FRONTEND_TEST_ADAPTERS, detectUnadaptedRunner, frontendTestReportSchemeSchema, resolveFrontendTestAdapter, resolveRunnerArgv, } from "./frontend-test-framework-adapters.js";
7
+ /** Scheme → adapter lookup, built once from the adapter registry. */
8
+ const FRONTEND_TEST_ADAPTERS_BY_SCHEME = Object.fromEntries(FRONTEND_TEST_ADAPTERS.map((adapter) => [adapter.scheme, adapter]));
9
+ /**
10
+ * Failure taxonomy for behavior verification. Kept separate from the writer's
11
+ * `failureCategory` so a broken report protocol never reads as broken product
12
+ * code and trigger a pointless implementation rewrite.
13
+ */
14
+ export const FRONTEND_TEST_OBSERVATION_FAILURES = {
15
+ /** The frozen command drift from what the contract bound. */
16
+ bindingDrift: "FRONTEND_TEST_BINDING_DRIFT",
17
+ /** The runner produced no readable report, or the report was stale. */
18
+ reportProtocol: "FRONTEND_TEST_REPORT_PROTOCOL",
19
+ /** The runner itself is not adapted (Cypress, `node --test`, …). */
20
+ capabilityGap: "FRONTEND_TEST_CAPABILITY_GAP",
21
+ };
6
22
  export const frontendTestObservationSchema = z.object({
7
- scheme: z.literal("vitest-json-v1"),
23
+ scheme: frontendTestReportSchemeSchema,
8
24
  commandId: z.string().optional(),
9
25
  status: z.enum(["supported", "deferred", "unsupported"]),
10
26
  commandSha256: z.string().regex(/^[a-f0-9]{64}$/),
11
27
  cwd: z.string(),
12
28
  packageScript: z.object({ file: z.string(), name: z.string(), sha256: z.string().nullable() }).optional(),
13
29
  reason: z.string().optional(),
30
+ /** Set when `status: "unsupported"` because no adapter exists yet. */
31
+ capabilityGap: z.string().optional(),
14
32
  }).strict();
15
33
  const hash = (text) => createHash("sha256").update(text).digest("hex");
16
34
  const quote = (text) => `'${text.replace(/'/g, `'"'"'`)}'`;
@@ -124,20 +142,68 @@ function literalCommand(command, cwd, depth = 0) {
124
142
  }
125
143
  return { argv, cwd, append: suffix => command.trimEnd() + suffix };
126
144
  }
127
- function directVitest(argv) {
128
- const args = [...argv];
129
- if (["npx"].includes(args[0] ?? ""))
130
- args.shift();
131
- else if (["pnpm", "yarn"].includes(args[0] ?? "") && args[1] === "exec")
132
- args.splice(0, 2);
133
- return args[0] === "vitest" && args[1] === "run";
134
- }
135
145
  function hasReporter(argv) { return argv.some(arg => /^--(?:reporter|outputFile)(?:[.=]|$)/.test(arg)); }
136
- /** Shared read-only capability resolver, used at generation, admission and
137
- * immediately before execution. It never executes or rewrites an npm script. */
146
+ /** Locate the `npm|pnpm|yarn` script invocation in a literal argv. */
147
+ function scriptInvocation(argv) {
148
+ const manager = argv[0];
149
+ if (manager !== "npm" && manager !== "pnpm" && manager !== "yarn")
150
+ return undefined;
151
+ // `exec`/`dlx`/`create` run a binary rather than a package script, so they
152
+ // are not script invocations — `yarn exec vitest run` must resolve as a
153
+ // direct runner command, not as a script literally named "exec".
154
+ const subcommands = new Set(["exec", "dlx", "create", "init", "add", "install", "why", "config"]);
155
+ // npm test | npm run <name> | pnpm test | pnpm run <name> | yarn <name>
156
+ let name;
157
+ let rest = [];
158
+ if (argv[1] === "test") {
159
+ name = "test";
160
+ rest = argv.slice(2);
161
+ }
162
+ else if (argv[1] === "run") {
163
+ name = argv[2];
164
+ rest = argv.slice(3);
165
+ }
166
+ else if (manager === "yarn" && argv[1] && !argv[1].startsWith("-") && !subcommands.has(argv[1])) {
167
+ name = argv[1];
168
+ rest = argv.slice(2);
169
+ }
170
+ if (!name || !/^[\w:-]+$/.test(name))
171
+ return undefined;
172
+ if (rest.length && rest[0] !== "--")
173
+ throw Error("package manager script arguments require --");
174
+ return { name, rest };
175
+ }
176
+ /**
177
+ * Build the report invocation for an adapter.
178
+ *
179
+ * When the adapter names a `reportPathEnv`, the path travels in the environment
180
+ * and the suffix only selects the reporter — appending the path to the command
181
+ * would hand it to the runner as a positional argument (Playwright treats a
182
+ * stray argument as a test-name filter and then finds no tests at all).
183
+ *
184
+ * Otherwise the path is joined to the suffix: a suffix ending in `=` (Vitest's
185
+ * `--outputFile=`) binds it directly, and any other suffix names a flag
186
+ * (`--outputFile`, `--json`) that takes the path as the next argument.
187
+ */
188
+ function reportInvocation(adapter, append, suffix, reportPath) {
189
+ if (adapter.reportPathEnv) {
190
+ return {
191
+ command: append(suffix),
192
+ env: { [adapter.reportPathEnv]: reportPath },
193
+ };
194
+ }
195
+ const joiner = suffix.endsWith("=") ? "" : " ";
196
+ return { command: append(`${suffix}${joiner}${quote(reportPath)}`) };
197
+ }
198
+ /**
199
+ * Shared read-only capability resolver, used at generation, admission and
200
+ * immediately before execution. It never executes or rewrites an npm script.
201
+ */
138
202
  export async function inspectFrontendTestCommand(input) {
139
203
  const root = path.resolve(input.workspaceRoot ?? input.cwd);
140
204
  let cwd = path.resolve(input.cwd);
205
+ // `scheme` is unknowable until an adapter resolves; the placeholder is only
206
+ // ever surfaced on the unsupported path, where no report is produced.
141
207
  const base = { scheme: "vitest-json-v1", status: "unsupported", commandSha256: hash(input.command), cwd: path.relative(root, cwd).replace(/\\/g, "/") || "." };
142
208
  try {
143
209
  const parsed = literalCommand(input.command, cwd);
@@ -154,79 +220,109 @@ export async function inspectFrontendTestCommand(input) {
154
220
  throw Error("test cwd resolves outside the workspace");
155
221
  if (hasReporter(parsed.argv))
156
222
  throw Error("frozen command already owns reporter/outputFile flags");
157
- let separator = "";
158
- if (!directVitest(parsed.argv)) {
159
- const argv = parsed.argv;
160
- const name = argv[0] === "npm" ? argv[1] === "test" ? "test" : argv[1] === "run" ? argv[2] : undefined : undefined;
161
- if (!name || !/^[\w:-]+$/.test(name))
162
- throw Error("no supported test reporter adapter for this command");
163
- const rest = argv.slice(argv[1] === "test" ? 2 : 3);
164
- if (rest.length && rest[0] !== "--")
165
- throw Error("npm script arguments require --");
166
- const file = path.relative(root, path.join(cwd, "package.json")).replace(/\\/g, "/");
167
- let manifest = {};
168
- try {
169
- manifest = JSON.parse(await readFile(path.join(cwd, "package.json"), "utf8"));
170
- }
171
- catch (error) {
172
- if (error.code !== "ENOENT")
173
- throw Error("package.json is unreadable or invalid");
174
- }
175
- const script = manifest?.scripts?.[name];
176
- base.packageScript = { file, name, sha256: typeof script === "string" ? hash(JSON.stringify({ script, pre: manifest.scripts?.[`pre${name}`] ?? null, post: manifest.scripts?.[`post${name}`] ?? null })) : null };
177
- if (script === undefined) {
178
- const authorized = input.allowedPaths?.some(pattern => pathMatchesPattern(file, pattern)) && !input.forbiddenPaths?.some(pattern => pathMatchesPattern(file, pattern));
179
- if (!authorized)
180
- throw Error(`missing npm script ${name}; ${file} is not an authorized delivery`);
181
- return { observation: { ...base, status: "deferred", reason: `Awaiting authorized ${file} script ${name} with vitest-json-v1 capability` }, effectiveCwd: cwd };
182
- }
183
- if (typeof script !== "string")
184
- throw Error("npm script must be a string");
185
- const scriptWords = words(script);
186
- if (scriptWords.some(word => word.operator) || !directVitest(scriptWords.map(w => w.value)) || hasReporter(scriptWords.map(w => w.value)))
187
- throw Error("npm script has no supported Vitest JSON reporter capability");
188
- separator = rest.length ? "" : " --";
223
+ const runner = resolveRunnerArgv(parsed.argv);
224
+ const invocation = scriptInvocation(parsed.argv);
225
+ if (!invocation) {
226
+ const adapter = resolveFrontendTestAdapter({ argv: parsed.argv, runner });
227
+ if (!adapter)
228
+ throw capabilityGapError(runner);
229
+ const suffix = adapter.reportSuffix({ argv: parsed.argv, runner });
230
+ if (suffix === undefined)
231
+ throw Error(`${adapter.runner} project configuration owns report output; declare an explicit regression command instead of overriding it`);
232
+ return { observation: { ...base, scheme: adapter.scheme, status: "supported" }, effectiveCwd: cwd,
233
+ withReporter: reportPath => reportInvocation(adapter, inner => parsed.append(` ${inner}`), suffix, reportPath) };
234
+ }
235
+ const file = path.relative(root, path.join(cwd, "package.json")).replace(/\\/g, "/");
236
+ let manifest = {};
237
+ try {
238
+ manifest = JSON.parse(await readFile(path.join(cwd, "package.json"), "utf8"));
189
239
  }
190
- return { observation: { ...base, status: "supported" }, effectiveCwd: cwd,
191
- withReporter: reportPath => parsed.append(`${separator} --reporter=json --outputFile=${quote(reportPath)}`) };
240
+ catch (error) {
241
+ if (error.code !== "ENOENT")
242
+ throw Error("package.json is unreadable or invalid");
243
+ }
244
+ const script = manifest?.scripts?.[invocation.name];
245
+ base.packageScript = { file, name: invocation.name, sha256: typeof script === "string" ? hash(JSON.stringify({ script, pre: manifest.scripts?.[`pre${invocation.name}`] ?? null, post: manifest.scripts?.[`post${invocation.name}`] ?? null })) : null };
246
+ if (script === undefined) {
247
+ const authorized = input.allowedPaths?.some(pattern => pathMatchesPattern(file, pattern)) && !input.forbiddenPaths?.some(pattern => pathMatchesPattern(file, pattern));
248
+ if (!authorized)
249
+ throw Error(`missing npm script ${invocation.name}; ${file} is not an authorized delivery`);
250
+ return { observation: { ...base, status: "deferred", reason: `Awaiting authorized ${file} script ${invocation.name} with a supported test report capability` }, effectiveCwd: cwd };
251
+ }
252
+ if (typeof script !== "string")
253
+ throw Error("npm script must be a string");
254
+ const scriptWords = words(script);
255
+ if (scriptWords.some(word => word.operator))
256
+ throw Error("npm script must be a single literal command");
257
+ const scriptArgv = scriptWords.map(w => w.value);
258
+ if (hasReporter(scriptArgv))
259
+ throw Error("npm script already owns reporter/outputFile flags");
260
+ const scriptRunner = resolveRunnerArgv(scriptArgv);
261
+ const adapter = resolveFrontendTestAdapter({ argv: parsed.argv, runner: scriptRunner, scriptName: invocation.name, scriptArgv });
262
+ if (!adapter)
263
+ throw capabilityGapError(scriptRunner);
264
+ const suffix = adapter.reportSuffix({ argv: scriptArgv, runner: scriptRunner });
265
+ if (suffix === undefined)
266
+ throw Error(`${adapter.runner} project configuration owns report output; declare an explicit regression command instead of overriding it`);
267
+ // `npm test -- --json` forwards the flags to the script; a bare script
268
+ // uses the same `--` separator so npm never eats the report flags.
269
+ // Space-prefixed because `append` concatenates onto the trimmed
270
+ // command; a bare script needs `--` so npm forwards the report flags.
271
+ const separator = invocation.rest.length ? "" : " -- ";
272
+ return { observation: { ...base, scheme: adapter.scheme, status: "supported" }, effectiveCwd: cwd,
273
+ withReporter: reportPath => reportInvocation(adapter, inner => parsed.append(invocation.rest.length ? ` ${inner}` : `${separator}${inner}`), suffix, reportPath) };
192
274
  }
193
275
  catch (error) {
194
- return { observation: { ...base, status: "unsupported", reason: error instanceof Error ? error.message : String(error) }, effectiveCwd: cwd };
276
+ const gap = error.capabilityGap;
277
+ return { observation: { ...base, status: "unsupported", reason: error instanceof Error ? error.message : String(error), ...(gap ? { capabilityGap: gap } : {}) }, effectiveCwd: cwd };
195
278
  }
196
279
  }
280
+ /**
281
+ * An unadapted runner is a capability gap in this tool, not a user mistake: the
282
+ * message must never suggest switching frameworks.
283
+ */
284
+ function capabilityGapError(runner) {
285
+ const known = detectUnadaptedRunner(runner);
286
+ const binary = known ?? (runner[0] ?? "unknown");
287
+ const error = new Error(`${FRONTEND_TEST_OBSERVATION_FAILURES.capabilityGap}: no test report adapter for ${binary}; ` +
288
+ `frontend behavior verification currently adapts vitest, jest and playwright test, ` +
289
+ `so this command cannot produce execution evidence`);
290
+ error.capabilityGap = binary;
291
+ return error;
292
+ }
197
293
  export function compareFrontendTestObservation(frozen, observed, phase, writeSet = []) {
198
294
  if (frozen.status === "unsupported")
199
295
  return frozen.reason ?? "unsupported test observation scheme";
200
296
  if (observed.status === "unsupported")
201
297
  return observed.reason ?? "unsupported test observation scheme";
202
- if (frozen.scheme !== observed.scheme || frozen.commandSha256 !== observed.commandSha256 || frozen.cwd !== observed.cwd || frozen.packageScript?.file !== observed.packageScript?.file || frozen.packageScript?.name !== observed.packageScript?.name)
203
- return "frozen test command/cwd/script binding changed";
298
+ // A `deferred` observation has no resolved adapter, so its `scheme` is the
299
+ // placeholder rather than a binding. Comparing it would reject every new
300
+ // Jest/Playwright project the moment the writer adds the very script the
301
+ // contract asked for. The scheme only becomes a binding once the frozen
302
+ // observation is `supported`; script identity is still checked below.
303
+ const schemeBound = frozen.status !== "deferred";
304
+ if ((schemeBound && frozen.scheme !== observed.scheme) || frozen.commandSha256 !== observed.commandSha256 || frozen.cwd !== observed.cwd || frozen.packageScript?.file !== observed.packageScript?.file || frozen.packageScript?.name !== observed.packageScript?.name)
305
+ return `${FRONTEND_TEST_OBSERVATION_FAILURES.bindingDrift}: frozen test command/cwd/script binding changed`;
204
306
  if (frozen.status === "supported" && (observed.status !== "supported" || frozen.packageScript?.sha256 !== observed.packageScript?.sha256))
205
- return "frozen npm test script changed";
307
+ return `${FRONTEND_TEST_OBSERVATION_FAILURES.bindingDrift}: frozen npm test script changed`;
206
308
  if (frozen.status === "deferred" && phase === "admission" && (!frozen.packageScript || !writeSet.includes(frozen.packageScript.file)))
207
309
  return "future test script is outside the admitted concrete writeSet";
208
310
  if (phase === "execution" && observed.status !== "supported")
209
311
  return "future test reporter capability is still unavailable";
210
312
  return undefined;
211
313
  }
212
- /** Observe supported frozen Vitest commands without changing their selection. */
314
+ /** Observe supported frozen commands without changing their selection. */
213
315
  export async function withFrontendTestReporter(input) {
214
- return (await inspectFrontendTestCommand(input)).withReporter?.(input.reportPath);
316
+ return (await inspectFrontendTestCommand(input)).withReporter?.(input.reportPath)?.command;
215
317
  }
216
- const resultSchema = z.object({
217
- testResults: z.array(z.object({
218
- name: z.string().min(1),
219
- assertionResults: z.array(z.object({
220
- title: z.string(), fullName: z.string().optional(),
221
- ancestorTitles: z.array(z.string()).optional(), status: z.string(),
222
- })),
223
- })),
224
- });
225
- /** Resolve runner paths against its cwd, then use the contract workspace coordinates. */
226
- export function parseFrontendTestExecutionReport(raw, cwd, workspaceRoot = cwd) {
227
- return resultSchema.parse(raw).testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
228
- file: path.relative(workspaceRoot, path.resolve(cwd, suite.name)).replace(/\\/g, "/"),
229
- title: test.fullName ?? [...(test.ancestorTitles ?? []), test.title].join(" "),
230
- status: test.status,
231
- })));
318
+ /**
319
+ * Parse a runner report into uniform execution records using the adapter for
320
+ * `scheme`. A malformed report throws; callers classify that as a report
321
+ * protocol failure, never as a product defect.
322
+ */
323
+ export function parseFrontendTestExecutionReport(raw, cwd, workspaceRoot = cwd, scheme = "vitest-json-v1") {
324
+ const adapter = FRONTEND_TEST_ADAPTERS_BY_SCHEME[scheme];
325
+ if (!adapter)
326
+ throw Error(`no test report adapter for scheme ${scheme}`);
327
+ return adapter.parse(raw, cwd, workspaceRoot);
232
328
  }