@tea-agent/loop-agent 0.39.0-next.2 → 0.39.0-next.21

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 (137) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/dist/application/dag/generate-task-dag.js +9 -0
  3. package/dist/build-stamp.json +2 -2
  4. package/dist/commands/init-upgrade.js +97 -19
  5. package/dist/executors/dag-pi-executor.js +42 -4
  6. package/dist/executors/shell-executor.js +269 -15
  7. package/dist/shared/operator/capabilities.js +149 -27
  8. package/dist/task/config-types.js +22 -0
  9. package/dist/worker/console/chat/deferred-turn.js +91 -0
  10. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  11. package/dist/worker/console/chat/pi-runtime.js +165 -83
  12. package/dist/worker/console/chat/repo-walk.js +10 -1
  13. package/dist/worker/console/chat/routes.js +477 -55
  14. package/dist/worker/console/chat/semantic-activity.js +20 -8
  15. package/dist/worker/console/chat/session-store.js +146 -2
  16. package/dist/worker/console/chat/turn-process.js +17 -30
  17. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  18. package/dist/worker/console/operator-actions.js +128 -4
  19. package/dist/worker/console/operator-user-error.js +19 -0
  20. package/dist/worker/console/pi-readiness.js +8 -1
  21. package/dist/worker/console/recovery-cta.js +4 -4
  22. package/dist/worker/console/recovery-error-copy.js +4 -4
  23. package/dist/worker/console/security.js +20 -1
  24. package/dist/worker/console/server.js +6 -1
  25. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
  26. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-SAD7McOS.js} +1 -1
  27. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
  28. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
  29. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-D1GRGZax.js} +1 -1
  30. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +1 -0
  31. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-BTYuvuWN.js} +1 -1
  41. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +1 -0
  42. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +1 -0
  43. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
  44. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
  45. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
  46. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
  52. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
  53. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
  54. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
  55. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
  56. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-BizhCSw9.js} +1 -1
  57. package/dist/worker/console/static/assets/index-BXN89VPX.js +325 -0
  58. package/dist/worker/console/static/assets/index-CuH5CNFu.css +1 -0
  59. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
  60. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
  61. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
  62. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
  63. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-D9Py31Ld.js} +1 -1
  64. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-D7Occzk_.js} +5 -5
  65. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
  66. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
  67. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
  68. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
  69. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
  70. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
  71. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
  72. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
  73. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
  74. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-BX1FbrhJ.js} +1 -1
  75. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +1 -0
  76. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DOSnT7hb.js} +2 -2
  77. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +8 -0
  78. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
  79. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
  80. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
  81. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-snuW9Mjw.js} +1 -1
  82. package/dist/worker/console/static/index.html +2 -2
  83. package/dist/worker/console/static-src/app/console-types.js +39 -0
  84. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  85. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  86. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
  87. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  88. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  89. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  90. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  91. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
  92. package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
  93. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  94. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  95. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
  96. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  97. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  98. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  99. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  100. package/dist/workflows/dag/backend-test-layout.js +133 -0
  101. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  102. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  103. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  104. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  105. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  106. package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
  107. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  108. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  109. package/dist/workflows/dag/convergence/controller.js +141 -0
  110. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  111. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  112. package/dist/workflows/dag/frontend-implementation-contract.js +386 -0
  113. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  114. package/dist/workflows/dag/frontend-prewrite-gate.js +243 -8
  115. package/dist/workflows/dag/init-hybrid.js +793 -104
  116. package/dist/workflows/dag/node-execution.js +61 -2
  117. package/dist/workflows/dag/prompt.js +45 -0
  118. package/dist/workflows/dag/rerun-feedback.js +212 -0
  119. package/dist/workflows/dag/rerun-plan.js +93 -27
  120. package/dist/workflows/dag/rerun-run.js +3 -0
  121. package/dist/workflows/dag/rerun-task.js +61 -2
  122. package/dist/workflows/dag/retry-policy.js +22 -0
  123. package/dist/workflows/dag/types.js +125 -0
  124. package/docs/templates/backend-test-dag.json +60 -35
  125. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  126. package/harness.json +3 -1
  127. package/package.json +1 -1
  128. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  129. package/skills/frontend-implementation/references/design-spec.md +16 -19
  130. package/skills/frontend-implementation/references/node-contracts.md +9 -6
  131. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  132. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  133. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  134. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  135. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  136. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  137. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -2,12 +2,17 @@ import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
4
  import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
5
+ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
5
6
  import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
6
- import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
7
+ import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, applyFrontendImplementationContractPatch, extractFrontendImplementationJson, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
8
+ import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
7
9
  import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
8
10
  import { pathMatchesPattern } from "../../shared/git-progress.js";
11
+ import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
9
12
  export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
10
13
  export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
14
+ /** Canonical contract serialization must stay at or below this byte size. */
15
+ export const FRONTEND_CONTRACT_MAX_BYTES = 65536;
11
16
  /**
12
17
  * Machine-readable failure classification for blocked/retryable-invalid
13
18
  * prewrite outcomes. Kept alongside the human-readable failureReason so
@@ -31,6 +36,11 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
31
36
  "openspec-citation-block-unparseable",
32
37
  "openspec-not-cited",
33
38
  "openspec-citation-not-read",
39
+ "component-spec-reference-invalid",
40
+ "component-spec-not-cited",
41
+ "component-choices-missing",
42
+ "contract-too-large",
43
+ "revision-patch-unmergeable",
34
44
  ]);
35
45
  export const frontendPrewriteResultV1Schema = z
36
46
  .object({
@@ -163,16 +173,53 @@ async function readNodeRawText(runDir, nodeId) {
163
173
  const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
164
174
  return record.assistantText ?? record.stdout ?? "";
165
175
  }
176
+ /**
177
+ * Same shape predicate as the internal `isContract` inside
178
+ * `extractFrontendImplementationJson` (frontend-implementation-contract.ts):
179
+ * schemaVersion 1 + a plain `targets` object + `requirements` and
180
+ * `verificationTargets` arrays. A merge-patch delta does not carry these
181
+ * fields, so this is the deterministic boundary between a full contract and a
182
+ * patch. Keep the two predicates in sync.
183
+ */
184
+ function isFullContractShape(value) {
185
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
186
+ return false;
187
+ }
188
+ const record = value;
189
+ const targets = record.targets;
190
+ return (record.schemaVersion === 1 &&
191
+ targets !== null &&
192
+ typeof targets === "object" &&
193
+ !Array.isArray(targets) &&
194
+ Array.isArray(record.requirements) &&
195
+ Array.isArray(record.verificationTargets));
196
+ }
197
+ /**
198
+ * Shared merge pipeline for revisionPatch mode and content-driven
199
+ * compatibility: read the original contract node, extract the original
200
+ * contract and the effective node's patch, apply the RFC 7386 merge-patch, and
201
+ * serialize deterministically. Throws on any failure; callers map the error to
202
+ * their own classification (retryable-invalid for explicit revisionPatch mode,
203
+ * blocked for content-driven fallback).
204
+ */
205
+ async function mergeRevisionPatch(runDir, planText, originalNodeId) {
206
+ const originalText = await readNodeText(runDir, originalNodeId);
207
+ const originalValue = extractFrontendImplementationJson(originalText);
208
+ const patchValue = extractFrontendImplementationJson(planText);
209
+ const merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
210
+ return serializeDeterministicJson(merged);
211
+ }
166
212
  function firstNonEmptyVerdictLine(text) {
167
213
  const lines = text
168
214
  .split(/\r?\n/)
169
215
  .map((line) => line.trim())
170
216
  .filter(Boolean);
171
- const normalize = (line) => {
172
- const emphasized = line.match(/^(?:`{1,3}|\*{1,3})\s*(VERDICT:[^`*]+?)\s*(?:`{1,3}|\*{1,3})$/);
173
- return (emphasized?.[1] ?? line).trim();
174
- };
175
- const normalizedLines = lines.map(normalize);
217
+ // Emphasis stripping (backtick + asterisk) is shared with the runIf
218
+ // selector and node-level output-protocol validation via
219
+ // normalizeVerdictCandidateLine. The gate keeps its own loose
220
+ // pass/request-revision normalization below (prefix match + trailing
221
+ // prose tolerance) for deterministic fail-closed behavior.
222
+ const normalizedLines = lines.map(normalizeVerdictCandidateLine);
176
223
  const first = normalizedLines[0] ?? "";
177
224
  // The prompt requires VERDICT to be the first non-empty line, but model
178
225
  // output can still prepend a summary. Keep the protocol strict in the
@@ -442,7 +489,78 @@ export async function runFrontendPrewriteGate(input) {
442
489
  failureCode: "verdict-not-pass",
443
490
  });
444
491
  }
445
- const missingIds = input.config.requiredRequirementIds.filter((id) => !planText.includes(id));
492
+ // revisionPatch mode: the effective plan node emits an RFC 7386 merge-patch
493
+ // delta against the original contract node (planFallbackFromNodeIds[0]).
494
+ // Legacy DAGs without revisionPatch (undefined) keep full-contract semantics.
495
+ const revisionPatchMode = input.config.revisionPatch === true &&
496
+ planNodeId === input.config.planFromNodeId &&
497
+ input.config.planFallbackFromNodeIds.length > 0;
498
+ let effectiveContractText = null;
499
+ if (revisionPatchMode) {
500
+ const originalNodeId = input.config.planFallbackFromNodeIds[0];
501
+ try {
502
+ effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
503
+ }
504
+ catch (error) {
505
+ return finalizePrewrite(input, {
506
+ ...basePending,
507
+ verdict,
508
+ classification: "retryable-invalid",
509
+ failureReason: `frontend prewrite gate revisionPatch mode requires original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}`,
510
+ failureCode: "contract-invalid",
511
+ });
512
+ }
513
+ }
514
+ else {
515
+ // Content-driven compatibility (Bug 1): legacy DAGs without
516
+ // revisionPatch can still receive a merge-patch delta from the revision
517
+ // node. Detect the output shape instead of trusting the DAG flag alone.
518
+ // - Full contract shape → keep legacy full-contract semantics unchanged.
519
+ // - Patch shape with a usable original contract node → auto-merge and
520
+ // run the existing analysis/materialization pipeline.
521
+ // - Patch shape but unmergeable → blocked + revision-patch-unmergeable
522
+ // (no retryable loop).
523
+ let planJsonValue = null;
524
+ try {
525
+ planJsonValue = extractFrontendImplementationJson(planText);
526
+ }
527
+ catch {
528
+ // Not machine-readable JSON: fall back to the legacy full-contract
529
+ // path where analyzeFrontendImplementationContract surfaces the
530
+ // original diagnostic (fail-closed without a new failure mode).
531
+ }
532
+ if (planJsonValue !== null && !isFullContractShape(planJsonValue)) {
533
+ const originalNodeId = input.config.planFallbackFromNodeIds[0];
534
+ if (!originalNodeId || originalNodeId === planNodeId) {
535
+ return finalizePrewrite(input, {
536
+ ...basePending,
537
+ verdict,
538
+ classification: "blocked",
539
+ failureReason: "frontend prewrite gate: the effective plan output is a merge-patch delta but no distinct original contract node is available. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.",
540
+ failureCode: "revision-patch-unmergeable",
541
+ });
542
+ }
543
+ try {
544
+ const originalText = await readNodeText(input.runDir, originalNodeId);
545
+ const originalValue = extractFrontendImplementationJson(originalText);
546
+ if (!isFullContractShape(originalValue)) {
547
+ throw new Error(`original contract node ${originalNodeId} is not a full frontend implementation contract`);
548
+ }
549
+ effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
550
+ }
551
+ catch (error) {
552
+ return finalizePrewrite(input, {
553
+ ...basePending,
554
+ verdict,
555
+ classification: "blocked",
556
+ failureReason: `frontend prewrite gate: the effective plan output is a merge-patch delta but it cannot be merged onto the original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.`,
557
+ failureCode: "revision-patch-unmergeable",
558
+ });
559
+ }
560
+ }
561
+ }
562
+ const requirementIdSearchText = effectiveContractText ?? planText;
563
+ const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
446
564
  if (missingIds.length > 0) {
447
565
  return finalizePrewrite(input, {
448
566
  ...basePending,
@@ -495,7 +613,8 @@ export async function runFrontendPrewriteGate(input) {
495
613
  try {
496
614
  analysis = await analyzeFrontendImplementationContract({
497
615
  runDir: input.runDir,
498
- fromNodeId: planNodeId,
616
+ fromNodeId: effectiveContractText ? undefined : planNodeId,
617
+ rawContractText: effectiveContractText ?? undefined,
499
618
  sourceBinding: input.sourceBinding,
500
619
  });
501
620
  }
@@ -515,6 +634,21 @@ export async function runFrontendPrewriteGate(input) {
515
634
  }
516
635
  throw error;
517
636
  }
637
+ // Size guard: a canonical contract above 64KB is a retryable-invalid defect
638
+ // (compress the contract) rather than a writer authorization failure.
639
+ const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
640
+ if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
641
+ return finalizePrewrite(input, {
642
+ ...basePending,
643
+ verdict,
644
+ candidateJsonSha256: analysis.candidateJsonSha256,
645
+ normalizationActions: [],
646
+ mockStrategy: analysis.canonical.mockApi.strategy,
647
+ classification: "retryable-invalid",
648
+ failureReason: `frontend prewrite gate: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit. 压缩契约(移除冗余 prose、重复目标或过长描述)后重试。`,
649
+ failureCode: "contract-too-large",
650
+ });
651
+ }
518
652
  const mockStrategy = analysis.canonical.mockApi.strategy;
519
653
  if (!input.config.allowedMockStrategies.includes(mockStrategy)) {
520
654
  const quotedAllowed = input.config.allowedMockStrategies
@@ -711,6 +845,105 @@ export async function runFrontendPrewriteGate(input) {
711
845
  }
712
846
  }
713
847
  }
848
+ // Component selection spec conformance (ADR 0016). Deterministic cross-check
849
+ // that declared component/token selections trace to the frontend spec
850
+ // (openspec/ai_workspace component + theme) instead of a model's ad-hoc
851
+ // "better" choice. Runs after the openspec read gate so citation + read
852
+ // evidence are already resolved.
853
+ const componentChoices = analysis.canonical.uiComponentChoices ?? [];
854
+ const componentSpecCandidates = input.config.componentSpecCandidatePaths ?? [];
855
+ const hasUiVisibleWork = analysis.canonical.uiStates.some((state) => state.applicable) ||
856
+ analysis.canonical.interactions.length > 0;
857
+ if (componentSpecCandidates.length > 0 &&
858
+ hasUiVisibleWork &&
859
+ componentChoices.length === 0) {
860
+ return finalizePrewrite(input, {
861
+ ...basePending,
862
+ verdict,
863
+ candidateJsonSha256: analysis.candidateJsonSha256,
864
+ normalizationActions: [],
865
+ mockStrategy,
866
+ classification: "retryable-invalid",
867
+ failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new),specified 必须给 specReference.path/section/line,new 必须给 rationale 声明偏差。",
868
+ failureCode: "component-choices-missing",
869
+ openspecReadPaths,
870
+ openspecCandidatePaths: candidatePaths,
871
+ });
872
+ }
873
+ const componentSpecReferences = componentChoices
874
+ .map((choice) => ({
875
+ raw: choice.specReference?.path ?? "",
876
+ normalized: choice.specReference?.path
877
+ ? normalizeCitationPath(choice.specReference.path, candidateRoot)
878
+ : null,
879
+ }))
880
+ .filter((ref) => ref.raw.length > 0);
881
+ if (componentSpecReferences.length > 0) {
882
+ const invalidRefs = [
883
+ ...new Set(componentSpecReferences
884
+ .filter((ref) => !ref.normalized ||
885
+ !isOpenspecSpecFilePath(ref.normalized))
886
+ .map((ref) => ref.raw)),
887
+ ];
888
+ if (invalidRefs.length > 0) {
889
+ return finalizePrewrite(input, {
890
+ ...basePending,
891
+ verdict,
892
+ candidateJsonSha256: analysis.candidateJsonSha256,
893
+ normalizationActions: [],
894
+ mockStrategy,
895
+ classification: "retryable-invalid",
896
+ failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths are not supported openspec/ai_workspace spec files: ${invalidRefs.join(", ")}`,
897
+ failureCode: "component-spec-reference-invalid",
898
+ openspecReadPaths,
899
+ openspecCandidatePaths: candidatePaths,
900
+ });
901
+ }
902
+ if (openspecPolicy === "cited") {
903
+ const parsedCitations = parseOpenspecCitations(planText, candidateRoot);
904
+ const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
905
+ const readSet = new Set(openspecReadPaths);
906
+ const uncorroborated = [
907
+ ...new Set(componentSpecReferences
908
+ .filter((ref) => !ref.normalized ||
909
+ !citedPaths.has(ref.normalized) ||
910
+ !readSet.has(ref.normalized))
911
+ .map((ref) => ref.raw)),
912
+ ];
913
+ if (uncorroborated.length > 0) {
914
+ return finalizePrewrite(input, {
915
+ ...basePending,
916
+ verdict,
917
+ candidateJsonSha256: analysis.candidateJsonSha256,
918
+ normalizationActions: [],
919
+ mockStrategy,
920
+ classification: "retryable-invalid",
921
+ failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths were not cited in the plan openspec-citations block or lack a successful read event: ${uncorroborated.join(", ")}`,
922
+ failureCode: "component-spec-not-cited",
923
+ openspecReadPaths,
924
+ openspecCandidatePaths: candidatePaths,
925
+ });
926
+ }
927
+ }
928
+ }
929
+ // Render the deterministic plan.md from the canonical contract (fail-closed
930
+ // before any artifact is written: no contract, no writer authorization).
931
+ let planMarkdown;
932
+ try {
933
+ planMarkdown = renderFrontendPlanMarkdown(analysis.canonical);
934
+ }
935
+ catch (error) {
936
+ return finalizePrewrite(input, {
937
+ ...basePending,
938
+ verdict,
939
+ candidateJsonSha256: analysis.candidateJsonSha256,
940
+ normalizationActions: [],
941
+ mockStrategy,
942
+ classification: "retryable-invalid",
943
+ failureReason: `frontend prewrite gate: failed to render ${input.config.planMdArtifactName ?? "frontend-plan.md"}: ${error instanceof Error ? error.message : String(error)}`,
944
+ failureCode: "contract-invalid",
945
+ });
946
+ }
714
947
  // Materialize the canonical contract only after every governance check passed.
715
948
  const artifact = await writeFrontendImplementationContractArtifact({
716
949
  runDir: input.runDir,
@@ -718,6 +951,8 @@ export async function runFrontendPrewriteGate(input) {
718
951
  artifactName: input.config.artifactName,
719
952
  canonical: analysis.canonical,
720
953
  });
954
+ // Materialize the rendered plan.md in the same outputDir as the contract.
955
+ await writeTextArtifactFile(path.join(input.runDir, path.posix.join(input.config.outputDir, input.config.planMdArtifactName ?? "frontend-plan.md")), planMarkdown);
721
956
  const classification = analysis.normalizationActions.length > 0 ? "accepted-normalized" : "accepted";
722
957
  return finalizePrewrite(input, {
723
958
  ...basePending,