@tea-agent/loop-agent 0.16.1-beta.2 → 0.16.2

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 (113) hide show
  1. package/AGENTS.md +4 -8
  2. package/CHANGELOG.md +76 -18
  3. package/README.md +76 -299
  4. package/dist/application/evaluation/alias.js +184 -0
  5. package/dist/application/evaluation/budget.js +192 -0
  6. package/dist/application/evaluation/campaign-hash.js +47 -0
  7. package/dist/application/evaluation/campaign-matrix.js +372 -0
  8. package/dist/application/evaluation/campaign-scorecard.js +135 -0
  9. package/dist/application/evaluation/campaign.js +370 -0
  10. package/dist/application/evaluation/candidate.js +23 -6
  11. package/dist/application/evaluation/corpus-hash.js +38 -0
  12. package/dist/application/evaluation/corpus.js +56 -0
  13. package/dist/application/evaluation/experiment.js +294 -0
  14. package/dist/application/evaluation/ignition.js +198 -0
  15. package/dist/application/evaluation/integrity-audit.js +162 -0
  16. package/dist/application/evaluation/outer-loop.js +132 -0
  17. package/dist/application/evaluation/pi-cell-executor.js +39 -0
  18. package/dist/application/evaluation/private-verifier.js +46 -0
  19. package/dist/application/evaluation/promotion-policy.js +151 -0
  20. package/dist/application/evaluation/proposer.js +98 -0
  21. package/dist/application/evaluation/types.js +522 -0
  22. package/dist/cli/command-definitions.js +19 -3
  23. package/dist/commands/dag-reconcile-run.js +3 -116
  24. package/dist/commands/eval.js +1176 -13
  25. package/dist/commands/init.js +7 -1
  26. package/dist/executors/dag-pi-executor.js +8 -46
  27. package/dist/executors/pi-sdk-executor.js +66 -3
  28. package/dist/executors/shell-executor.js +213 -30
  29. package/dist/executors/shell-presets.js +12 -2
  30. package/dist/executors/shell-write-guard.js +20 -1
  31. package/dist/infrastructure/evaluation/alias-store.js +199 -0
  32. package/dist/infrastructure/evaluation/campaign-store.js +154 -0
  33. package/dist/infrastructure/evaluation/corpus-store.js +181 -0
  34. package/dist/infrastructure/evaluation/experiment-store.js +124 -0
  35. package/dist/infrastructure/evaluation/ignition-store.js +82 -0
  36. package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
  37. package/dist/infrastructure/evaluation/proposer-store.js +78 -0
  38. package/dist/records/promotion.js +3 -1
  39. package/dist/shared/git-progress.js +9 -2
  40. package/dist/worker/cli.js +83 -0
  41. package/dist/worker/delivery/git-transaction.js +75 -0
  42. package/dist/worker/delivery/verification-bundle.js +13 -2
  43. package/dist/worker/feature/review.js +3 -2
  44. package/dist/worker/observability/read-model.js +56 -0
  45. package/dist/worker/observe/server.js +6 -3
  46. package/dist/worker/observe/static/dag-helpers.js +0 -62
  47. package/dist/worker/observe/static/styles.css +18 -55
  48. package/dist/worker/observe/static/views/dag.js +13 -5
  49. package/dist/worker/outcomes/adapters.js +4 -1
  50. package/dist/worker/outcomes/declared-artifacts.js +103 -0
  51. package/dist/worker/outcomes/evidence-tokens.js +29 -0
  52. package/dist/worker/outcomes/gate.js +10 -11
  53. package/dist/worker/outcomes/projector.js +30 -4
  54. package/dist/worker/outcomes/types.js +3 -0
  55. package/dist/worker/pool/reconcile.js +285 -0
  56. package/dist/worker/run-task/run-task.js +81 -4
  57. package/dist/worker/runner/run-ready.js +25 -2
  58. package/dist/worker/task-graph/ready-planner.js +14 -8
  59. package/dist/worker/task-graph/task-graph-schema.js +5 -3
  60. package/dist/workflows/dag/backend-test-analysis-contract.js +87 -30
  61. package/dist/workflows/dag/backend-test-case-manifest.js +71 -8
  62. package/dist/workflows/dag/backend-test-execution-contract.js +63 -11
  63. package/dist/workflows/dag/backend-test-repair-contract.js +94 -0
  64. package/dist/workflows/dag/backend-test-result-contract.js +6 -4
  65. package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
  66. package/dist/workflows/dag/budget-enforcement.js +67 -0
  67. package/dist/workflows/dag/context-policy.js +137 -0
  68. package/dist/workflows/dag/dynamic-runtime/condition.js +1 -1
  69. package/dist/workflows/dag/dynamic-runtime/shared.js +42 -0
  70. package/dist/workflows/dag/failure-routing.js +8 -1
  71. package/dist/workflows/dag/frontend-implementation-contract.js +32 -93
  72. package/dist/workflows/dag/init-hybrid.js +624 -172
  73. package/dist/workflows/dag/knowledge-curator.js +3 -0
  74. package/dist/workflows/dag/lifecycle.js +33 -2
  75. package/dist/workflows/dag/node-execution.js +11 -4
  76. package/dist/workflows/dag/prompt.js +1 -1
  77. package/dist/workflows/dag/reconcile-run.js +121 -0
  78. package/dist/workflows/dag/report.js +12 -0
  79. package/dist/workflows/dag/runner.js +43 -16
  80. package/dist/workflows/dag/scheduler.js +87 -17
  81. package/dist/workflows/dag/skill-snapshot.js +11 -7
  82. package/dist/workflows/dag/types.js +49 -1
  83. package/dist/workflows/dag/validate.js +35 -15
  84. package/docs/README.md +3 -1
  85. package/docs/architecture/runtime-boundaries.md +3 -2
  86. package/docs/init-surface.manifest.json +4 -0
  87. package/docs/local-development-environment.md +52 -0
  88. package/docs/templates/agent-dag.schema.json +25 -7
  89. package/docs/templates/agent-dag.supervised-implementation.json +23 -4
  90. package/docs/templates/backend-test-analysis.schema.json +9 -16
  91. package/docs/templates/backend-test-dag.json +493 -197
  92. package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -4
  93. package/docs/templates/backend-test-execution.schema.json +6 -1
  94. package/docs/templates/branch-merge-report.md +14 -0
  95. package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
  96. package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
  97. package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
  98. package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
  99. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
  100. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
  101. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
  102. package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
  103. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
  104. package/docs/templates/product-line/AGENTS.md +1 -0
  105. package/docs/templates/product-line/README.md +17 -0
  106. package/docs/templates/product-line/acceptance.yaml +9 -0
  107. package/docs/templates/product-line/feature.yaml +11 -0
  108. package/docs/templates/product-line/task-graph.yaml +8 -0
  109. package/docs/templates/product-line/task.yaml +4 -0
  110. package/package.json +2 -1
  111. package/skills/frontend-implementation/references/node-contracts.md +3 -3
  112. package/skills/loop-agent/references/command-reference.md +5 -0
  113. package/skills/loop-agent/references/hybrid-dag.md +7 -4
@@ -0,0 +1,137 @@
1
+ import { DEFAULT_SKILL_INSTRUCTION_MAX_CHARS, DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS, } from "./skill-instructions.js";
2
+ import { MAX_UPSTREAM_CHARS } from "./prompt.js";
3
+ import { resolveDagNodeSkills } from "./skills.js";
4
+ export const CONTEXT_POLICY_IDS = [
5
+ "baseline-v1",
6
+ "role-specialized-v1",
7
+ ];
8
+ export const DEFAULT_CONTEXT_POLICY_ID = "baseline-v1";
9
+ function roleOrUndefined(task) {
10
+ return task.role;
11
+ }
12
+ function pickByRole(table, role) {
13
+ if (role && table[role] !== undefined)
14
+ return table[role];
15
+ return table.default;
16
+ }
17
+ class BaselineContextPolicy {
18
+ id = "baseline-v1";
19
+ description = "Current DAG context assembly: shared upstream char budget, role skill defaults, learned patterns only for implementer.";
20
+ resolveSkills(spec, task) {
21
+ return resolveDagNodeSkills(spec, task);
22
+ }
23
+ resolveSkillInstructionBudget(task) {
24
+ return {
25
+ includeLearnedPatterns: task.role === "implementer",
26
+ perSkillMaxChars: DEFAULT_SKILL_INSTRUCTION_MAX_CHARS,
27
+ totalMaxChars: DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS,
28
+ };
29
+ }
30
+ resolveMaxUpstreamChars(_task) {
31
+ return MAX_UPSTREAM_CHARS;
32
+ }
33
+ toManifest() {
34
+ return {
35
+ schemaVersion: 1,
36
+ policyId: this.id,
37
+ description: this.description,
38
+ knobs: {
39
+ maxUpstreamCharsByRole: { default: MAX_UPSTREAM_CHARS },
40
+ includeLearnedPatternsRoles: ["implementer"],
41
+ perSkillMaxCharsByRole: {
42
+ default: DEFAULT_SKILL_INSTRUCTION_MAX_CHARS,
43
+ },
44
+ totalMaxCharsByRole: {
45
+ default: DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS,
46
+ },
47
+ },
48
+ };
49
+ }
50
+ }
51
+ /**
52
+ * First A/B challenger: keep skill name resolution identical to baseline, but
53
+ * specialize upstream / skill-instruction budgets by role so scouts see less
54
+ * noise and implementers retain more upstream + learned patterns surface.
55
+ */
56
+ class RoleSpecializedContextPolicy {
57
+ id = "role-specialized-v1";
58
+ description = "Role-specialized upstream and skill-instruction budgets; skill names still resolve via baseline merge order.";
59
+ maxUpstreamCharsByRole = {
60
+ default: MAX_UPSTREAM_CHARS,
61
+ scout: 1_200,
62
+ reviewer: 1_200,
63
+ implementer: 3_000,
64
+ verifier: 1_600,
65
+ closeout: 1_600,
66
+ planner: MAX_UPSTREAM_CHARS,
67
+ supervisor: MAX_UPSTREAM_CHARS,
68
+ };
69
+ perSkillMaxCharsByRole = {
70
+ default: DEFAULT_SKILL_INSTRUCTION_MAX_CHARS,
71
+ scout: 2_500,
72
+ implementer: 3_500,
73
+ };
74
+ totalMaxCharsByRole = {
75
+ default: DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS,
76
+ scout: 10_000,
77
+ implementer: 14_000,
78
+ };
79
+ learnedPatternRoles = new Set([
80
+ "implementer",
81
+ "closeout",
82
+ ]);
83
+ resolveSkills(spec, task) {
84
+ return resolveDagNodeSkills(spec, task);
85
+ }
86
+ resolveSkillInstructionBudget(task) {
87
+ const role = roleOrUndefined(task);
88
+ return {
89
+ includeLearnedPatterns: role
90
+ ? this.learnedPatternRoles.has(role)
91
+ : false,
92
+ perSkillMaxChars: pickByRole(this.perSkillMaxCharsByRole, role),
93
+ totalMaxChars: pickByRole(this.totalMaxCharsByRole, role),
94
+ };
95
+ }
96
+ resolveMaxUpstreamChars(task) {
97
+ return pickByRole(this.maxUpstreamCharsByRole, roleOrUndefined(task));
98
+ }
99
+ toManifest() {
100
+ return {
101
+ schemaVersion: 1,
102
+ policyId: this.id,
103
+ description: this.description,
104
+ knobs: {
105
+ maxUpstreamCharsByRole: this.maxUpstreamCharsByRole,
106
+ includeLearnedPatternsRoles: [...this.learnedPatternRoles],
107
+ perSkillMaxCharsByRole: this.perSkillMaxCharsByRole,
108
+ totalMaxCharsByRole: this.totalMaxCharsByRole,
109
+ },
110
+ };
111
+ }
112
+ }
113
+ const POLICIES = {
114
+ "baseline-v1": new BaselineContextPolicy(),
115
+ "role-specialized-v1": new RoleSpecializedContextPolicy(),
116
+ };
117
+ export function isContextPolicyId(value) {
118
+ return CONTEXT_POLICY_IDS.includes(value);
119
+ }
120
+ export function getContextPolicy(policyId) {
121
+ return POLICIES[policyId];
122
+ }
123
+ export function listContextPolicies() {
124
+ return CONTEXT_POLICY_IDS.map((id) => POLICIES[id]);
125
+ }
126
+ export function resolveContextPolicyId(spec) {
127
+ const raw = spec.defaults?.contextPolicyId;
128
+ if (!raw)
129
+ return DEFAULT_CONTEXT_POLICY_ID;
130
+ if (!isContextPolicyId(raw)) {
131
+ throw new Error(`unknown contextPolicyId "${raw}"; expected one of ${CONTEXT_POLICY_IDS.join(", ")}`);
132
+ }
133
+ return raw;
134
+ }
135
+ export function resolveContextPolicy(spec) {
136
+ return getContextPolicy(resolveContextPolicyId(spec));
137
+ }
@@ -1,5 +1,5 @@
1
1
  import { parseConditionLiteral, resolveOutputSelector } from "./shared.js";
2
- function evaluateConditionExpression(expression, state) {
2
+ export function evaluateConditionExpression(expression, state) {
3
3
  const equality = expression.match(/^\s*(\$\..+?)\s*==\s*(.+?)\s*$/);
4
4
  if (!equality?.[1]) {
5
5
  throw new Error(`unsupported condition expression: ${expression}`);
@@ -28,7 +28,49 @@ export function getNodeOutputAsJson(state, nodeId) {
28
28
  throw new Error(`itemsFrom upstream node "${nodeId}" output is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
29
29
  }
30
30
  }
31
+ /** Normalize a line the same way verdict gates do (strip whole-line Markdown emphasis). */
32
+ export function normalizeVerdictCandidateLine(value) {
33
+ const trimmed = String(value).trim();
34
+ const emphasized = trimmed.match(/^\*{1,3}\s*(VERDICT:[^*]+?)\s*\*{1,3}$/);
35
+ return (emphasized ? emphasized[1] : trimmed).trim();
36
+ }
37
+ /** First line matching /^VERDICT:/ from node assistantText/stdout (markdown emphasis OK). */
38
+ export function extractFirstVerdictLineFromNodeText(text) {
39
+ for (const line of String(text).split(/\r?\n/)) {
40
+ const normalized = normalizeVerdictCandidateLine(line);
41
+ if (/^VERDICT:/.test(normalized))
42
+ return normalized;
43
+ }
44
+ return "";
45
+ }
46
+ function getNodeRawText(state, nodeId) {
47
+ const record = state.nodes[nodeId];
48
+ if (!record || record.status !== "FINISHED") {
49
+ throw new Error(`selector upstream node "${nodeId}" is not finished`);
50
+ }
51
+ return record.assistantText?.trim() || record.stdout?.trim() || "";
52
+ }
31
53
  export function resolveOutputSelector(selector, state) {
54
+ const firstVerdictMatch = selector.match(/^\$\.nodes\[['"]([^'"]+)['"]\]\.firstVerdictLine\s*$/);
55
+ if (firstVerdictMatch?.[1]) {
56
+ const nodeId = firstVerdictMatch[1];
57
+ return extractFirstVerdictLineFromNodeText(getNodeRawText(state, nodeId));
58
+ }
59
+ const jsonFieldMatch = selector.match(/^\$\.nodes\[['"]([^'"]+)['"]\]\.json\.([A-Za-z0-9_.-]+)\s*$/);
60
+ if (jsonFieldMatch?.[1] && jsonFieldMatch[2]) {
61
+ let current = parseJsonFromText(getNodeRawText(state, jsonFieldMatch[1]));
62
+ if (current === undefined) {
63
+ throw new Error(`selector upstream node "${jsonFieldMatch[1]}" output is not valid JSON`);
64
+ }
65
+ for (const segment of jsonFieldMatch[2].split(".")) {
66
+ if (current && typeof current === "object" && segment in current) {
67
+ current = current[segment];
68
+ continue;
69
+ }
70
+ throw new Error(`output selector path not found: ${selector} (missing "${segment}")`);
71
+ }
72
+ return current;
73
+ }
32
74
  const match = selector.match(/^\$\.nodes\[['"]([^'"]+)['"]\]\.output(?:\.(.+))?$/);
33
75
  if (!match?.[1]) {
34
76
  throw new Error(`unsupported itemsFrom selector: ${selector}`);
@@ -46,6 +46,13 @@ function routeToProductLine(input) {
46
46
  case "skipped":
47
47
  return "DependencyFailure";
48
48
  case "shell-command":
49
+ if (/\b(ENOENT|PATH|command not found|No such file|not found in PATH|bash: .*: No such file)\b/i.test(raw)) {
50
+ return "EnvFailure";
51
+ }
52
+ if (/\b(missing VERDICT line|verdict gate blocked|VERDICT)\b/i.test(raw) &&
53
+ /\b(missing|malformed|unexpected|blocked)\b/i.test(raw)) {
54
+ return "ContractMismatch";
55
+ }
49
56
  if (raw.includes("flaky"))
50
57
  return "FlakyTest";
51
58
  if (nodeId.includes("test") || raw.includes("test-bug")) {
@@ -79,7 +86,7 @@ function routeToProductLine(input) {
79
86
  }
80
87
  }
81
88
  export function routeDagFailure(input) {
82
- const productLineFailureCategory = routeToProductLine(input);
89
+ const productLineFailureCategory = input.productLineFailureCategory ?? routeToProductLine(input);
83
90
  if (!productLineFailureCategory)
84
91
  return {};
85
92
  const recommendedFollowUp = productLineFailureCategory === "ContractMismatch" &&
@@ -1,86 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFileSync } from "node:fs";
3
2
  import { readFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
4
  import { z } from "zod";
7
5
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
8
- import { findPackageRoot } from "../../shared/package-metadata.js";
9
6
  export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
10
- /**
11
- * Load the canonical frontend-implementation-contract-v1 JSON Schema from the
12
- * installed loop-agent package docs/templates/ path. Package-root discovery
13
- * works from both the source module and the compiled dist module without
14
- * relying on CommonJS globals in the ESM runtime.
15
- *
16
- * Validation is fail-closed: missing file, malformed JSON, mismatched $id,
17
- * missing additionalProperties: false, or incomplete top-level required keys
18
- * all throw before any DAG prompt is assembled.
19
- */
20
- export function loadFrontendImplementationContractJsonSchema(startDir = path.dirname(fileURLToPath(import.meta.url))) {
21
- const packageRoot = findPackageRoot(startDir);
22
- if (!packageRoot) {
23
- throw new Error(`cannot locate loop-agent package root from ${path.resolve(startDir)}`);
24
- }
25
- const schemaPath = path.join(packageRoot, "docs", "templates", "frontend-implementation-contract.schema.json");
26
- let content;
27
- try {
28
- content = readFileSync(schemaPath, "utf-8");
29
- }
30
- catch (error) {
31
- throw new Error(`cannot load frontend-implementation-contract.schema.json from current loop-agent package at ${schemaPath}: ${error.code ?? String(error)}`);
32
- }
33
- let parsed;
34
- try {
35
- parsed = JSON.parse(content);
36
- }
37
- catch (error) {
38
- throw new Error(`frontend-implementation-contract.schema.json is not valid JSON: ${error.message}`);
39
- }
40
- if (parsed === null || typeof parsed !== "object") {
41
- throw new Error("frontend-implementation-contract.schema.json root is not a JSON object");
42
- }
43
- const schema = parsed;
44
- if (schema.$id !== FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID) {
45
- throw new Error(`schema $id mismatch: expected ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID}, got ${String(schema.$id)}`);
46
- }
47
- if (schema.additionalProperties !== false) {
48
- throw new Error("schema must have additionalProperties: false at top level");
49
- }
50
- const expectedRequired = [
51
- "schemaVersion",
52
- "sourceBinding",
53
- "riskLevel",
54
- "targets",
55
- "requirements",
56
- "uiStates",
57
- "interactions",
58
- "mockApi",
59
- "designEvidence",
60
- "verificationTargets",
61
- "evidenceGaps",
62
- ];
63
- const actualRequired = Array.isArray(schema.required) ? schema.required : [];
64
- const missing = expectedRequired.filter((key) => !actualRequired.includes(key));
65
- if (missing.length > 0) {
66
- throw new Error(`schema required fields missing: ${missing.join(", ")}`);
67
- }
68
- const properties = schema.properties && typeof schema.properties === "object"
69
- ? schema.properties
70
- : {};
71
- const missingProperties = expectedRequired.filter((key) => !Object.hasOwn(properties, key));
72
- if (missingProperties.length > 0) {
73
- throw new Error(`schema properties missing: ${missingProperties.join(", ")}`);
74
- }
75
- const schemaVersion = properties.schemaVersion;
76
- const mockApi = properties.mockApi;
77
- const mockApiProperties = mockApi?.properties;
78
- const productionDefaultOff = mockApiProperties?.productionDefaultOff;
79
- if (schemaVersion?.const !== 1 || productionDefaultOff?.const !== true) {
80
- throw new Error("schema fixed values are incomplete: schemaVersion.const must be 1 and mockApi.productionDefaultOff.const must be true");
81
- }
82
- return JSON.stringify(parsed);
83
- }
84
7
  const id = z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/);
85
8
  const safePath = z
86
9
  .string()
@@ -263,6 +186,26 @@ export function extractFrontendImplementationJson(text) {
263
186
  throw new Error("output must contain exactly one fenced json object");
264
187
  return JSON.parse(blocks[0][1]);
265
188
  }
189
+ /**
190
+ * Build the authoritative frontend-implementation-contract sourceBinding from
191
+ * the DAG-owned binding. Model JSON must not invent taskId/path/sha256; the
192
+ * gate overwrites whatever the model emitted so identity cannot drift.
193
+ */
194
+ export function canonicalFrontendContractSourceBinding(binding) {
195
+ const requirement = binding.sources.find((source) => source.kind === "requirement");
196
+ if (!requirement)
197
+ throw new Error("frontend implementation contract gate requires a requirement source in DAG sourceBinding");
198
+ return {
199
+ taskId: binding.taskId,
200
+ requirementPath: requirement.path,
201
+ requirementSha256: requirement.sha256,
202
+ referencePaths: binding.sources
203
+ .filter((source) => source.kind === "reference")
204
+ .map((source) => source.path)
205
+ .sort(),
206
+ requirementIds: [...binding.requirementIds],
207
+ };
208
+ }
266
209
  export async function materializeFrontendImplementationContract(input) {
267
210
  if (!input.sourceBinding)
268
211
  throw new Error("frontend implementation contract gate requires DAG sourceBinding");
@@ -277,23 +220,19 @@ export async function materializeFrontendImplementationContract(input) {
277
220
  const secrets = secretIssues(parsed);
278
221
  if (secrets.length)
279
222
  throw new Error(`invalid-output: ${secrets.join("; ")}`);
280
- const result = frontendImplementationContractSchema.safeParse(parsed);
223
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
224
+ throw new Error("invalid-output: frontend contract must be a JSON object");
225
+ const canonicalBinding = canonicalFrontendContractSourceBinding(input.sourceBinding);
226
+ // Always inject DAG-owned identity. Model-provided sourceBinding is advisory
227
+ // only and must not fail a otherwise-valid contract (common live failure:
228
+ // wrong requirementPath/sha, extra referencePaths, or omitted binding).
229
+ const withCanonicalBinding = {
230
+ ...parsed,
231
+ sourceBinding: canonicalBinding,
232
+ };
233
+ const result = frontendImplementationContractSchema.safeParse(withCanonicalBinding);
281
234
  if (!result.success)
282
235
  throw new Error(`invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
283
- const requirement = input.sourceBinding.sources.find((source) => source.kind === "requirement");
284
- const refs = input.sourceBinding.sources
285
- .filter((source) => source.kind === "reference")
286
- .map((source) => source.path)
287
- .sort();
288
- const bound = result.data.sourceBinding;
289
- if (!requirement ||
290
- bound.taskId !== input.sourceBinding.taskId ||
291
- bound.requirementPath !== requirement.path ||
292
- bound.requirementSha256 !== requirement.sha256 ||
293
- JSON.stringify([...bound.referencePaths].sort()) !== JSON.stringify(refs) ||
294
- JSON.stringify(bound.requirementIds) !==
295
- JSON.stringify(input.sourceBinding.requirementIds))
296
- throw new Error("frontend contract source binding does not match DAG source binding");
297
236
  const blockingGaps = [
298
237
  ...result.data.evidenceGaps,
299
238
  ...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
@@ -302,7 +241,7 @@ export async function materializeFrontendImplementationContract(input) {
302
241
  throw new Error(`frontend contract has blocking evidence gap: ${blockingGaps
303
242
  .map((item) => item.requirementId ?? item.description)
304
243
  .join(", ")}`);
305
- for (const requirementId of bound.requirementIds)
244
+ for (const requirementId of canonicalBinding.requirementIds)
306
245
  if (!result.data.requirements.some((item) => item.id === requirementId) &&
307
246
  !result.data.evidenceGaps.some((item) => item.requirementId === requirementId))
308
247
  throw new Error(`frontend contract does not cover ${requirementId}`);