@tea-agent/loop-agent 0.35.1-beta.3 → 0.35.1

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 (48) hide show
  1. package/AGENTS.md +0 -2
  2. package/CHANGELOG.md +25 -24
  3. package/bin/loop-agent.js +1 -37
  4. package/dist/application/dag/generate-task-dag.js +4 -1
  5. package/dist/application/task-lifecycle/advance.js +14 -0
  6. package/dist/cli/program.js +2 -2
  7. package/dist/commands/task-advance.js +1 -0
  8. package/dist/executors/dag-pi-executor.js +0 -44
  9. package/dist/shared/package-metadata.js +0 -42
  10. package/dist/task/config-types.js +2 -0
  11. package/dist/task/contract/project.js +3 -0
  12. package/dist/task/contract/schema.js +1 -0
  13. package/dist/task/source-prepare/build-draft.js +7 -0
  14. package/dist/task/source-prepare/semantic-intake.js +37 -10
  15. package/dist/task/task-demand-routing.js +10 -0
  16. package/dist/worker/console/operator-actions.js +72 -6
  17. package/dist/worker/console/prd-intake-bridge.js +10 -3
  18. package/dist/worker/console/prd-reference-discovery.js +124 -0
  19. package/dist/worker/console/static/assets/{index-hJqCPs_g.css → index-HX1pbOyl.css} +1 -1
  20. package/dist/worker/console/static/assets/{index-CvsQgALl.js → index-M0BLEBfh.js} +25 -25
  21. package/dist/worker/console/static/index.html +2 -2
  22. package/dist/worker/console/static-src/app/useOperatorActions.js +19 -1
  23. package/dist/worker/console/static-src/app/useRecoveryConsole.js +0 -5
  24. package/dist/worker/console/static-src/app/useTaskWizard.js +12 -0
  25. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  26. package/dist/worker/observability/read-model.js +0 -20
  27. package/dist/worker/preflight.js +1 -2
  28. package/dist/workflows/dag/backend-test-scenario-param.js +23 -33
  29. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  30. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  31. package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
  32. package/dist/workflows/dag/frontend-repair.js +18 -219
  33. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  34. package/dist/workflows/dag/init-hybrid.js +26 -49
  35. package/dist/workflows/dag/node-execution.js +0 -89
  36. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  37. package/dist/workflows/dag/runner.js +11 -245
  38. package/dist/workflows/dag/scheduler.js +3 -257
  39. package/dist/workflows/dag/types.js +2 -130
  40. package/package.json +2 -2
  41. package/dist/build-stamp.json +0 -6
  42. package/dist/workflows/dag/contract-output-registry.js +0 -14
  43. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  44. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  45. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  46. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  47. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  48. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
@@ -4,69 +4,10 @@ import { readFile, stat } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { z } from "zod";
7
- import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
7
+ import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
8
8
  import { findPackageRoot } from "../../shared/package-metadata.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
10
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
11
- export const frontendNormalizationActionSchema = z.enum([
12
- "remove-trailing-commas",
13
- "strip-comments",
14
- "canonicalize-requirement-ids",
15
- "canonicalize-verification-alias",
16
- "inject-source-binding",
17
- ]);
18
- export function sha256Hex(input) {
19
- return createHash("sha256").update(input, "utf8").digest("hex");
20
- }
21
- function sortKeysDeep(value) {
22
- if (Array.isArray(value))
23
- return value.map(sortKeysDeep);
24
- if (value && typeof value === "object") {
25
- const record = value;
26
- const sorted = {};
27
- for (const key of Object.keys(record).sort()) {
28
- sorted[key] = sortKeysDeep(record[key]);
29
- }
30
- return sorted;
31
- }
32
- return value;
33
- }
34
- /** Deterministic JSON serialization: fixed key order, 2-space indent, no
35
- * trailing newline. Shared by candidate/canonical/digest hashes so the same
36
- * input always reproduces the same digest. */
37
- export function serializeDeterministicJson(value) {
38
- return JSON.stringify(sortKeysDeep(value), null, 2);
39
- }
40
- export function deterministicSha256(value) {
41
- return sha256Hex(serializeDeterministicJson(value));
42
- }
43
- /** A contract materialization failure carries its classification plus the
44
- * audit context collected up to the failure point, so the prewrite gate can
45
- * persist a complete frontend-prewrite-result-v1 even when the canonical
46
- * contract is never materialized. */
47
- export class FrontendContractFailure extends Error {
48
- kind;
49
- candidateRawSha256;
50
- candidateJsonSha256;
51
- normalizationActions;
52
- constructor(options) {
53
- super(options.message);
54
- this.name = "FrontendContractFailure";
55
- this.kind = options.kind;
56
- this.candidateRawSha256 = options.candidateRawSha256;
57
- this.candidateJsonSha256 = options.candidateJsonSha256 ?? null;
58
- this.normalizationActions = options.normalizationActions ?? [];
59
- }
60
- }
61
- /** Write a JSON artifact with deterministic key order and return its sha256
62
- * over the exact file bytes, so the canonical contract identity matches what
63
- * downstream verify/repair/review re-read from disk. */
64
- export async function writeDeterministicJsonArtifact(runDir, relativePath, value) {
65
- const json = `${serializeDeterministicJson(value)}\n`;
66
- const targetPath = path.join(runDir, relativePath);
67
- await writeTextArtifactFile(targetPath, json);
68
- return { path: targetPath, sha256: sha256Hex(json) };
69
- }
70
11
  /**
71
12
  * Extract frozen command labels from the DAG run spec (run.json).
72
13
  * The run spec is written before any node executes, so it is always available
@@ -191,32 +132,7 @@ const gap = z
191
132
  blocking: z.boolean(),
192
133
  })
193
134
  .strict();
194
- /**
195
- * Normalize unstable model emissions before the strict schema sees them:
196
- * recursively drop null values (object entries and array elements). A null for
197
- * an optional field becomes "absent" (accepted); a null for a required field
198
- * still fails, but with a clean Required issue instead of a confusing type
199
- * mismatch. Unknown keys and misspellings remain strict rejections.
200
- */
201
- function stripNullValuesDeep(value) {
202
- if (Array.isArray(value)) {
203
- return value
204
- .filter((item) => item !== null)
205
- .map((item) => stripNullValuesDeep(item));
206
- }
207
- if (value && typeof value === "object") {
208
- const record = value;
209
- const stripped = {};
210
- for (const [key, item] of Object.entries(record)) {
211
- if (item !== null)
212
- stripped[key] = stripNullValuesDeep(item);
213
- }
214
- return stripped;
215
- }
216
- return value;
217
- }
218
135
  export const frontendImplementationContractSchema = z
219
- .preprocess(stripNullValuesDeep, z
220
136
  .object({
221
137
  schemaVersion: z.literal(1),
222
138
  sourceBinding: z
@@ -273,9 +189,7 @@ export const frontendImplementationContractSchema = z
273
189
  "not-needed",
274
190
  ]),
275
191
  productionDefaultOff: z.literal(true),
276
- activation: z.preprocess((value) => value === "" || value === null || value === undefined
277
- ? "explicit activation boundary"
278
- : value, z.string().min(1)),
192
+ activation: z.preprocess((value) => (value === "" || value === null ? "explicit activation boundary" : value), z.string().min(1)),
279
193
  endpoints: z.array(z
280
194
  .object({
281
195
  method: z.enum([
@@ -425,7 +339,7 @@ export const frontendImplementationContractSchema = z
425
339
  }
426
340
  });
427
341
  }
428
- }));
342
+ });
429
343
  export async function assertFrontendSourceBindingFresh(input) {
430
344
  for (const source of input.binding.sources) {
431
345
  const absolute = resolveDagTaskSourcePath({
@@ -464,13 +378,8 @@ function secretIssues(value, at = "$", issues = []) {
464
378
  }
465
379
  return issues;
466
380
  }
467
- function extractFrontendImplementationJsonWithAudit(text) {
381
+ export function extractFrontendImplementationJson(text) {
468
382
  const trimmed = text.trim();
469
- const actions = [];
470
- const recordAction = (action) => {
471
- if (!actions.includes(action))
472
- actions.push(action);
473
- };
474
383
  const parse = (source) => {
475
384
  try {
476
385
  return JSON.parse(source);
@@ -483,10 +392,6 @@ function extractFrontendImplementationJsonWithAudit(text) {
483
392
  .replace(/\/\*[\s\S]*?\*\//g, "")
484
393
  .replace(/^\s*\/\/.*$/gm, "");
485
394
  const withoutTrailingCommas = withoutComments.replace(/,\s*([}\]])/g, "$1");
486
- if (withoutComments !== source)
487
- recordAction("strip-comments");
488
- if (withoutTrailingCommas !== withoutComments)
489
- recordAction("remove-trailing-commas");
490
395
  try {
491
396
  return JSON.parse(withoutTrailingCommas);
492
397
  }
@@ -545,7 +450,7 @@ function extractFrontendImplementationJsonWithAudit(text) {
545
450
  Array.isArray(record.verificationTargets));
546
451
  };
547
452
  if (trimmed.startsWith("{") && trimmed.endsWith("}"))
548
- return { value: parse(trimmed), actions };
453
+ return parse(trimmed);
549
454
  const balancedObjects = [];
550
455
  for (let start = 0; start < trimmed.length; start += 1) {
551
456
  if (trimmed[start] !== "{")
@@ -583,9 +488,9 @@ function extractFrontendImplementationJsonWithAudit(text) {
583
488
  }
584
489
  const balancedContracts = balancedObjects.filter(isContract);
585
490
  if (balancedContracts.length === 1)
586
- return { value: balancedContracts[0], actions };
491
+ return balancedContracts[0];
587
492
  if (balancedObjects.length === 1)
588
- return { value: balancedObjects[0], actions };
493
+ return balancedObjects[0];
589
494
  const blocks = [...trimmed.matchAll(/```json\s*\n([\s\S]*?)\n```/gi)];
590
495
  if (blocks.length === 0)
591
496
  throw new Error("output must contain exactly one fenced json object");
@@ -603,16 +508,13 @@ function extractFrontendImplementationJsonWithAudit(text) {
603
508
  }
604
509
  const contractCandidates = candidates.filter(isContract);
605
510
  if (contractCandidates.length === 1)
606
- return { value: contractCandidates[0], actions };
511
+ return contractCandidates[0];
607
512
  if (candidates.length === 1)
608
- return { value: candidates[0], actions };
513
+ return candidates[0];
609
514
  if (candidates.length === 0)
610
515
  throw new Error("output must contain exactly one valid fenced json object (found 0)");
611
516
  throw new Error(`output must contain exactly one valid frontend contract json object (found ${contractCandidates.length || candidates.length})`);
612
517
  }
613
- export function extractFrontendImplementationJson(text) {
614
- return extractFrontendImplementationJsonWithAudit(text).value;
615
- }
616
518
  /**
617
519
  * Build the authoritative frontend-implementation-contract sourceBinding from
618
520
  * the DAG-owned binding. Model JSON must not invent taskId/path/sha256; the
@@ -850,14 +752,7 @@ function deriveFrontendVerificationCoverage(value, canonicalBinding) {
850
752
  const requirementId = canonicalizeRequirementId(asString(gap.requirementId));
851
753
  // Model gaps are advisory. Blocking status is reconstructed below
852
754
  // from the source binding and executable verification targets.
853
- // Drop a null/empty optional requirementId so the strict schema
854
- // accepts model emissions (null is not a valid string).
855
- const { requirementId: _rawRequirementId, ...rest } = gap;
856
- return {
857
- ...rest,
858
- ...(requirementId ? { requirementId } : {}),
859
- blocking: false,
860
- };
755
+ return { ...gap, blocking: false };
861
756
  })
862
757
  : [];
863
758
  const derivedBlockingGaps = canonicalBinding.requirementIds
@@ -1333,87 +1228,40 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1333
1228
  evidenceGaps,
1334
1229
  };
1335
1230
  }
1336
- /**
1337
- * Deterministic contract validation shared by the prewrite gate and the plan
1338
- * node output self-check. Extracts, security-checks, coerces, and schema-parses
1339
- * a model-emitted frontend contract, so schema/typo/null violations surface as
1340
- * invalid-output at the producing node (where they can retry) instead of only
1341
- * failing later at the prewrite gate.
1342
- */
1343
- export async function analyzeFrontendImplementationContract(input) {
1231
+ export async function materializeFrontendImplementationContract(input) {
1344
1232
  if (!input.sourceBinding)
1345
1233
  throw new Error("frontend implementation contract gate requires DAG sourceBinding");
1346
- let rawOutput;
1347
- if (input.rawContractText !== undefined) {
1348
- rawOutput = input.rawContractText;
1349
- }
1350
- else if (input.fromNodeId !== undefined) {
1351
- const record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
1352
- rawOutput = record.assistantText ?? record.stdout ?? "";
1353
- }
1354
- else {
1355
- throw new Error("analyzeFrontendImplementationContract requires fromNodeId or rawContractText");
1356
- }
1357
- const rawContractText = rawOutput.trim();
1358
- const candidateRawSha256 = sha256Hex(rawOutput);
1359
- const normalizationActions = [];
1360
- const pushAction = (action) => {
1361
- if (!normalizationActions.includes(action))
1362
- normalizationActions.push(action);
1363
- };
1364
- function fail(kind, message, candidateJsonSha256) {
1365
- throw new FrontendContractFailure({
1366
- kind,
1367
- message,
1368
- candidateRawSha256,
1369
- candidateJsonSha256,
1370
- normalizationActions,
1371
- });
1372
- }
1234
+ const record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
1235
+ let parsed;
1236
+ const rawContractText = record.assistantText?.trim() || record.stdout?.trim() || "";
1373
1237
  if (rawContractText.includes('"files":["/'))
1374
- fail("blocked", "invalid-output: absolute frontend contract path is forbidden", null);
1238
+ throw new Error("invalid-output: absolute frontend contract path is forbidden");
1375
1239
  if (/(?:"(?:files|file|implementationTargets|fixture|consumer)"\s*:\s*(?:\[\s*)?)"\//.test(rawContractText))
1376
- fail("blocked", "invalid-output: absolute frontend contract path is forbidden", null);
1240
+ throw new Error("invalid-output: absolute frontend contract path is forbidden");
1377
1241
  if (/(?:"strategy"\s*:\s*")(?!native\b|browser-intercept\b|request-adapter\b|not-needed\b)[^"]+"/.test(rawContractText))
1378
- fail("blocked", "invalid-output: unsupported mock strategy", null);
1379
- let parsed;
1242
+ throw new Error("invalid-output: unsupported mock strategy");
1380
1243
  try {
1381
- const extracted = extractFrontendImplementationJsonWithAudit(rawContractText);
1382
- parsed = extracted.value;
1383
- for (const action of extracted.actions)
1384
- pushAction(action);
1244
+ parsed = extractFrontendImplementationJson(rawContractText);
1385
1245
  }
1386
1246
  catch (error) {
1387
- fail("retryable-invalid", `invalid-output: ${error instanceof Error ? error.message : String(error)}`, null);
1247
+ throw new Error(`invalid-output: ${error instanceof Error ? error.message : String(error)}`);
1388
1248
  }
1389
- const candidateJsonSha256 = deterministicSha256(parsed);
1390
1249
  const secrets = secretIssues(parsed);
1391
1250
  if (secrets.length)
1392
- fail("blocked", `invalid-output: ${secrets.join("; ")}`, candidateJsonSha256);
1251
+ throw new Error(`invalid-output: ${secrets.join("; ")}`);
1393
1252
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1394
- fail("retryable-invalid", "invalid-output: frontend contract must be a JSON object", candidateJsonSha256);
1253
+ throw new Error("invalid-output: frontend contract must be a JSON object");
1395
1254
  const parsedTargetObject = parsed.targets;
1396
1255
  const parsedFiles = parsedTargetObject && typeof parsedTargetObject === "object"
1397
1256
  ? parsedTargetObject.files
1398
1257
  : undefined;
1399
1258
  if (Array.isArray(parsedFiles) && parsedFiles.some((file) => typeof file === "string" && file.startsWith("/")))
1400
- fail("blocked", "invalid-output: absolute frontend contract path is forbidden", candidateJsonSha256);
1259
+ throw new Error("invalid-output: absolute frontend contract path is forbidden");
1401
1260
  // Normalize model-emitted compact requirement IDs before deriving the
1402
1261
  // canonical binding or invoking the strict zod schema.
1403
- const beforeIds = JSON.stringify(parsed);
1404
1262
  parsed = canonicalizeRequirementIdsInPayload(parsed);
1405
- if (JSON.stringify(parsed) !== beforeIds)
1406
- pushAction("canonicalize-requirement-ids");
1407
- const beforeAliases = JSON.stringify(parsed);
1408
1263
  parsed = canonicalizeVerificationTargetAliases(parsed);
1409
- if (JSON.stringify(parsed) !== beforeAliases)
1410
- pushAction("canonicalize-verification-alias");
1411
- try {
1412
- assertFrontendContractPathsSafe(parsed);
1413
- }
1414
- catch (error) {
1415
- fail("blocked", error instanceof Error ? error.message : String(error), candidateJsonSha256);
1416
- }
1264
+ assertFrontendContractPathsSafe(parsed);
1417
1265
  // Validate verificationTarget commandLabels against frozen command set.
1418
1266
  // The frozen set is derived from DAG verification shell task verifyEvidence.
1419
1267
  const frozenLabels = await deriveFrozenCommandLabelsFromRun(input.runDir);
@@ -1424,7 +1272,7 @@ export async function analyzeFrontendImplementationContract(input) {
1424
1272
  for (const vt of parsedVt) {
1425
1273
  const label = asString(asRecord(vt)?.commandLabel);
1426
1274
  if (label && !frozen.has(label)) {
1427
- fail("blocked", `invalid-output: verificationTarget commandLabel "${label}" is not in the frozen command set [${[...frozen].join(", ")}]`, candidateJsonSha256);
1275
+ throw new Error(`invalid-output: verificationTarget commandLabel "${label}" is not in the frozen command set [${[...frozen].join(", ")}]`);
1428
1276
  }
1429
1277
  }
1430
1278
  }
@@ -1432,94 +1280,52 @@ export async function analyzeFrontendImplementationContract(input) {
1432
1280
  const parsedTargets = asRecord(parsed)?.targets;
1433
1281
  const parsedTargetFiles = asStringArray(asRecord(parsedTargets)?.files);
1434
1282
  if (parsedTargetFiles.some((file) => file.startsWith("/") || file.includes("\\")))
1435
- fail("blocked", "invalid-output: frontend contract target paths must be relative POSIX paths", candidateJsonSha256);
1283
+ throw new Error("invalid-output: frontend contract target paths must be relative POSIX paths");
1436
1284
  const parsedStates = asRecord(parsed)?.uiStates;
1437
1285
  if (Array.isArray(parsedStates) && parsedStates.some((item) => {
1438
1286
  const state = asRecord(item);
1439
1287
  return state?.applicable === true &&
1440
1288
  (!asString(state.expectedBehavior) || asStringArray(state.implementationTargets).length === 0 || asStringArray(state.verificationTargetIds).length === 0);
1441
1289
  }))
1442
- fail("retryable-invalid", "invalid-output: applicable UI state requires behavior, implementation, and verification", candidateJsonSha256);
1290
+ throw new Error("invalid-output: applicable UI state requires behavior, implementation, and verification");
1443
1291
  const parsedMockApi = asRecord(parsed)?.mockApi;
1444
1292
  if (asRecord(parsedMockApi) &&
1445
1293
  typeof asRecord(parsedMockApi)?.strategy === "string" &&
1446
1294
  !["native", "browser-intercept", "request-adapter", "not-needed"].includes(String(asRecord(parsedMockApi)?.strategy)))
1447
- fail("blocked", `invalid-output: unsupported mock strategy ${String(asRecord(parsedMockApi)?.strategy)}`, candidateJsonSha256);
1295
+ throw new Error(`invalid-output: unsupported mock strategy ${String(asRecord(parsedMockApi)?.strategy)}`);
1448
1296
  const baseCanonicalBinding = canonicalFrontendContractSourceBinding(input.sourceBinding);
1449
1297
  const canonicalBinding = withDerivedRequirementIdsWhenUnscoped(baseCanonicalBinding, parsed);
1450
1298
  // Always inject DAG-owned identity. Model-provided sourceBinding is advisory
1451
- // only and must not fail a otherwise-valid contract. Record the action only
1452
- // when the model's emitted binding actually differs from canonical identity.
1453
- const modelBinding = asRecord(asRecord(parsed)?.sourceBinding);
1454
- if (serializeDeterministicJson(modelBinding ?? null) !== serializeDeterministicJson(canonicalBinding))
1455
- pushAction("inject-source-binding");
1299
+ // only and must not fail a otherwise-valid contract (common live failure:
1300
+ // wrong requirementPath/sha, extra referencePaths, or omitted binding).
1301
+ const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
1456
1302
  // There is exactly one post-security candidate. A fallback candidate would
1457
1303
  // allow malformed raw fields to bypass the boundary checks above.
1458
- const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
1459
1304
  const candidate = deriveFrontendVerificationCoverage({
1460
1305
  ...(asRecord(normalizedContract) ?? parsed),
1461
1306
  sourceBinding: canonicalBinding,
1462
1307
  }, canonicalBinding);
1463
1308
  const result = frontendImplementationContractSchema.safeParse(candidate);
1464
1309
  if (!result.success)
1465
- fail("retryable-invalid", `invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, candidateJsonSha256);
1310
+ throw new Error(`invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
1466
1311
  const blockingGaps = [
1467
1312
  ...result.data.evidenceGaps,
1468
1313
  ...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
1469
1314
  ].filter((item) => item.blocking);
1470
1315
  if (blockingGaps.length > 0)
1471
- fail("blocked", `frontend contract has blocking evidence gap: ${blockingGaps
1316
+ throw new Error(`frontend contract has blocking evidence gap: ${blockingGaps
1472
1317
  .map((item) => item.requirementId ?? item.description)
1473
- .join(", ")}`, candidateJsonSha256);
1318
+ .join(", ")}`);
1474
1319
  for (const requirementId of canonicalBinding.requirementIds)
1475
1320
  if (!result.data.requirements.some((item) => item.id === requirementId) &&
1476
1321
  !result.data.evidenceGaps.some((item) => item.requirementId === requirementId))
1477
- fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
1478
- return {
1479
- canonical: result.data,
1480
- candidateRawSha256,
1481
- candidateJsonSha256,
1482
- normalizationActions,
1483
- };
1484
- }
1485
- export async function writeFrontendImplementationContractArtifact(input) {
1486
- const written = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(input.outputDir, input.artifactName), input.canonical);
1322
+ throw new Error(`frontend contract does not cover ${requirementId}`);
1323
+ const artifactPath = await writeDagRunJsonArtifact(input.runDir, path.posix.join(input.outputDir, input.artifactName), result.data);
1487
1324
  return {
1488
- path: written.path,
1489
- sha256: written.sha256,
1325
+ path: artifactPath,
1326
+ sha256: createHash("sha256")
1327
+ .update(`${JSON.stringify(result.data, null, 2)}\n`)
1328
+ .digest("hex"),
1490
1329
  schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
1491
1330
  };
1492
1331
  }
1493
- /**
1494
- * Node-output self-check for plan nodes that produce the implementation
1495
- * contract. Mirrors the prewrite gate validation so schema/typo/null violations
1496
- * are caught at the producing node and retried instead of failing the run later.
1497
- * Skipped when no sourceBinding is available (the gate remains the authority).
1498
- */
1499
- export async function validateFrontendContractNodeOutput(input) {
1500
- if (!input.sourceBinding)
1501
- return { ok: true };
1502
- try {
1503
- const analysis = await analyzeFrontendImplementationContract({
1504
- runDir: input.runDir,
1505
- rawContractText: input.text,
1506
- sourceBinding: input.sourceBinding,
1507
- });
1508
- return { ok: true, contract: analysis.canonical };
1509
- }
1510
- catch (error) {
1511
- return {
1512
- ok: false,
1513
- reason: error instanceof Error ? error.message : String(error),
1514
- };
1515
- }
1516
- }
1517
- export async function materializeFrontendImplementationContract(input) {
1518
- const analysis = await analyzeFrontendImplementationContract(input);
1519
- return writeFrontendImplementationContractArtifact({
1520
- runDir: input.runDir,
1521
- outputDir: input.outputDir,
1522
- artifactName: input.artifactName,
1523
- canonical: analysis.canonical,
1524
- });
1525
- }