@tea-agent/loop-agent 0.38.0 → 0.38.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 (147) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/dist/application/dag/generate-task-dag.js +9 -0
  3. package/dist/build-stamp.json +3 -3
  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/infrastructure/harness/atomic-write.js +2 -1
  8. package/dist/shared/openspec-spec.js +76 -0
  9. package/dist/shared/operator/capabilities.js +149 -27
  10. package/dist/task/config-types.js +43 -0
  11. package/dist/task/frontend-project-capability.js +292 -38
  12. package/dist/worker/console/chat/deferred-turn.js +91 -0
  13. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  14. package/dist/worker/console/chat/pi-runtime.js +165 -83
  15. package/dist/worker/console/chat/repo-walk.js +10 -1
  16. package/dist/worker/console/chat/routes.js +477 -55
  17. package/dist/worker/console/chat/semantic-activity.js +20 -8
  18. package/dist/worker/console/chat/session-store.js +146 -2
  19. package/dist/worker/console/chat/turn-process.js +17 -30
  20. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  21. package/dist/worker/console/operator-actions.js +128 -4
  22. package/dist/worker/console/operator-user-error.js +19 -0
  23. package/dist/worker/console/pi-readiness.js +8 -1
  24. package/dist/worker/console/recovery-cta.js +4 -4
  25. package/dist/worker/console/recovery-error-copy.js +4 -4
  26. package/dist/worker/console/security.js +20 -1
  27. package/dist/worker/console/server.js +6 -1
  28. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
  29. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-SAD7McOS.js} +1 -1
  30. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
  31. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
  32. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-D1GRGZax.js} +1 -1
  33. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +1 -0
  34. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-BTYuvuWN.js} +1 -1
  44. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +1 -0
  45. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +1 -0
  46. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
  47. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
  48. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
  49. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
  55. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
  56. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
  57. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
  58. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
  59. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-BizhCSw9.js} +1 -1
  60. package/dist/worker/console/static/assets/index-BXN89VPX.js +325 -0
  61. package/dist/worker/console/static/assets/index-CuH5CNFu.css +1 -0
  62. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
  63. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
  64. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
  65. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
  66. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-D9Py31Ld.js} +1 -1
  67. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-D7Occzk_.js} +5 -5
  68. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
  69. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
  70. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
  71. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
  72. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
  73. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
  74. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
  75. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
  76. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
  77. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-BX1FbrhJ.js} +1 -1
  78. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +1 -0
  79. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DOSnT7hb.js} +2 -2
  80. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +8 -0
  81. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
  82. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
  83. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
  84. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-snuW9Mjw.js} +1 -1
  85. package/dist/worker/console/static/index.html +2 -2
  86. package/dist/worker/console/static-src/app/console-types.js +39 -0
  87. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  88. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  89. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
  90. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  91. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  92. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  93. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  94. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
  95. package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
  96. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  97. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  98. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
  99. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  100. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  101. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  102. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  103. package/dist/workflows/dag/backend-test-layout.js +133 -0
  104. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  105. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  106. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  107. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  108. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  109. package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
  110. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  111. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  112. package/dist/workflows/dag/convergence/controller.js +141 -0
  113. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  114. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  115. package/dist/workflows/dag/failure-routing.js +9 -4
  116. package/dist/workflows/dag/frontend-implementation-contract.js +386 -0
  117. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  118. package/dist/workflows/dag/frontend-prewrite-gate.js +479 -28
  119. package/dist/workflows/dag/init-hybrid.js +920 -110
  120. package/dist/workflows/dag/lifecycle.js +4 -0
  121. package/dist/workflows/dag/node-execution.js +77 -8
  122. package/dist/workflows/dag/prompt.js +45 -0
  123. package/dist/workflows/dag/report.js +6 -0
  124. package/dist/workflows/dag/rerun-feedback.js +212 -0
  125. package/dist/workflows/dag/rerun-plan.js +93 -27
  126. package/dist/workflows/dag/rerun-run.js +3 -0
  127. package/dist/workflows/dag/rerun-task.js +61 -2
  128. package/dist/workflows/dag/retry-policy.js +33 -0
  129. package/dist/workflows/dag/types.js +148 -4
  130. package/dist/workflows/dag/validate.js +7 -2
  131. package/docs/templates/backend-test-dag.json +60 -35
  132. package/docs/templates/frontend-design-contract.md +40 -14
  133. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  134. package/docs/templates/frontend-task-constraints.md +40 -13
  135. package/docs/templates/frontend-task-requirement.md +54 -20
  136. package/harness.json +3 -1
  137. package/package.json +1 -1
  138. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  139. package/skills/frontend-implementation/references/design-spec.md +16 -19
  140. package/skills/frontend-implementation/references/node-contracts.md +15 -11
  141. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  142. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  144. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  145. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  147. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -13,6 +13,7 @@ export const frontendNormalizationActionSchema = z.enum([
13
13
  "strip-comments",
14
14
  "canonicalize-requirement-ids",
15
15
  "canonicalize-verification-alias",
16
+ "drop-unresolved-verification-symbols",
16
17
  "inject-source-binding",
17
18
  ]);
18
19
  export function sha256Hex(input) {
@@ -102,6 +103,21 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
102
103
  return [...labels];
103
104
  }
104
105
  export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
106
+ export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
107
+ /**
108
+ * Build the deterministic, run-owned portion of the frontend contract. The
109
+ * planner is allowed to fill semantic fields with an RFC 7386 merge patch but
110
+ * cannot redefine task identity, risk, or the implementation write boundary.
111
+ */
112
+ export function buildFrontendImplementationContractSkeleton(input) {
113
+ return {
114
+ schemaVersion: 1,
115
+ sourceBinding: canonicalFrontendContractSourceBinding(input.sourceBinding),
116
+ riskLevel: input.riskLevel,
117
+ targets: { files: [...input.targetFiles] },
118
+ mockApi: { productionDefaultOff: true },
119
+ };
120
+ }
105
121
  /**
106
122
  * Load the canonical frontend-implementation-contract-v1 JSON Schema from the
107
123
  * installed loop-agent package docs/templates/ path. Package-root discovery
@@ -191,6 +207,42 @@ const gap = z
191
207
  blocking: z.boolean(),
192
208
  })
193
209
  .strict();
210
+ /**
211
+ * Structured component/token selection contract (ADR 0016). Every user-visible
212
+ * UI purpose the plan touches must be declared here so the prewrite gate can
213
+ * deterministically cross-check that the selection comes from the frontend
214
+ * specification (openspec/ai_workspace component + theme specs) instead of
215
+ * relying on design-review's soft model judgment.
216
+ *
217
+ * - `specified`: the frontend spec mandates this component for the purpose →
218
+ * `specReference` is required (path/section/line hit).
219
+ * - `reuse-existing`: reuse an existing repo component/convention not named by
220
+ * the spec → `specReference` may be null, rationale explains the basis.
221
+ * - `new`: neither spec nor existing code fits → `specReference` must be null,
222
+ * rationale must declare the deviation (design-review approves it).
223
+ *
224
+ * `specReference` is nullable + optional because `stripNullValuesDeep` strips
225
+ * null object values before validation, so a `new` choice's `specReference:
226
+ * null` becomes an absent field; the superRefine below treats absent and null
227
+ * identically.
228
+ */
229
+ const uiComponentSpecReferenceSchema = z
230
+ .object({
231
+ path: safePath,
232
+ section: z.string(),
233
+ line: z.number().int().nullable().optional(),
234
+ })
235
+ .strict();
236
+ const uiComponentChoiceSchema = z
237
+ .object({
238
+ purpose: z.string().min(1),
239
+ component: z.string().min(1),
240
+ decision: z.enum(["specified", "reuse-existing", "new"]),
241
+ specReference: uiComponentSpecReferenceSchema.nullable().optional(),
242
+ rationale: z.string().min(1),
243
+ })
244
+ .strict();
245
+ const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
194
246
  /**
195
247
  * Normalize unstable model emissions before the strict schema sees them:
196
248
  * recursively drop null values (object entries and array elements). A null for
@@ -316,6 +368,12 @@ export const frontendImplementationContractSchema = z
316
368
  })
317
369
  .strict()).min(1),
318
370
  evidenceGaps: z.array(gap),
371
+ implementationSteps: z.array(z.string().min(1)).optional(),
372
+ stylingStrategy: z.string().min(1).optional(),
373
+ uiComponentChoices: uiComponentChoicesSchema.optional(),
374
+ dependencyPolicy: z.string().min(1).optional(),
375
+ residualRisks: z.array(z.string().min(1)).optional(),
376
+ realIntegrationGap: z.string().min(1).optional(),
319
377
  })
320
378
  .strict()
321
379
  .superRefine((value, ctx) => {
@@ -391,6 +449,26 @@ export const frontendImplementationContractSchema = z
391
449
  path: ["uiStates"],
392
450
  });
393
451
  }
452
+ for (const [index, choice] of (value.uiComponentChoices ?? []).entries()) {
453
+ if (choice.decision === "specified") {
454
+ if (!choice.specReference || !choice.specReference.path) {
455
+ ctx.addIssue({
456
+ code: "custom",
457
+ message: "specified component choice requires a non-empty specReference.path",
458
+ path: ["uiComponentChoices", index, "specReference"],
459
+ });
460
+ }
461
+ }
462
+ else if (choice.decision === "new") {
463
+ if (choice.specReference) {
464
+ ctx.addIssue({
465
+ code: "custom",
466
+ message: "new component choice must not carry a specReference",
467
+ path: ["uiComponentChoices", index, "specReference"],
468
+ });
469
+ }
470
+ }
471
+ }
394
472
  if (value.mockApi.strategy !== "not-needed") {
395
473
  if (value.mockApi.endpoints.length === 0) {
396
474
  ctx.addIssue({
@@ -426,6 +504,47 @@ export const frontendImplementationContractSchema = z
426
504
  });
427
505
  }
428
506
  }));
507
+ function isPlainObject(value) {
508
+ return value !== null && typeof value === "object" && !Array.isArray(value);
509
+ }
510
+ /**
511
+ * Apply an RFC 7386 merge-patch on a target contract without mutating it.
512
+ * `null` removes the key; plain objects merge recursively when the target value
513
+ * is also a plain object; arrays and scalars replace the whole value. The
514
+ * result is re-validated by analyzeFrontendImplementationContract before it is
515
+ * ever materialized, so a patch that removes a required field still fails
516
+ * closed at the gate.
517
+ */
518
+ export function applyFrontendContractMergePatch(target, patch) {
519
+ if (!isPlainObject(patch)) {
520
+ throw new Error("frontend contract patch must be a JSON object");
521
+ }
522
+ const merge = (base, delta) => {
523
+ if (delta === null)
524
+ return undefined;
525
+ if (!isPlainObject(delta))
526
+ return delta;
527
+ const result = isPlainObject(base)
528
+ ? { ...base }
529
+ : {};
530
+ for (const [key, value] of Object.entries(delta)) {
531
+ if (value === null) {
532
+ delete result[key];
533
+ }
534
+ else if (isPlainObject(value) && isPlainObject(result[key])) {
535
+ result[key] = merge(result[key], value);
536
+ }
537
+ else {
538
+ result[key] = value;
539
+ }
540
+ }
541
+ return result;
542
+ };
543
+ return merge(target, patch);
544
+ }
545
+ export function applyFrontendImplementationContractPatch(target, patch) {
546
+ return applyFrontendContractMergePatch(target, patch);
547
+ }
429
548
  export async function assertFrontendSourceBindingFresh(input) {
430
549
  for (const source of input.binding.sources) {
431
550
  const absolute = resolveDagTaskSourcePath({
@@ -648,6 +767,18 @@ function asStringArray(value) {
648
767
  .map((item) => asString(item))
649
768
  .filter((item) => item.length > 0);
650
769
  }
770
+ /**
771
+ * Pass structured component choices through the free-form compatibility
772
+ * branch. The strict-shape branch already spreads unknown fields, so only this
773
+ * explicit constructor needs to re-emit `uiComponentChoices`; a missing/empty
774
+ * array normalizes to undefined (absent) so the optional schema field stays
775
+ * compatible with legacy free-form contracts.
776
+ */
777
+ function normalizeUiComponentChoices(value) {
778
+ if (!Array.isArray(value) || value.length === 0)
779
+ return undefined;
780
+ return value;
781
+ }
651
782
  function normalizeFrontendRoute(value) {
652
783
  const route = asString(value).replaceAll("\\", "/").replace(/^\/+/, "");
653
784
  if (!route || route === "." || route.split("/").includes(".."))
@@ -1331,6 +1462,12 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1331
1462
  },
1332
1463
  verificationTargets,
1333
1464
  evidenceGaps,
1465
+ implementationSteps: asStringArray(record.implementationSteps),
1466
+ stylingStrategy: asString(record.stylingStrategy) || undefined,
1467
+ dependencyPolicy: asString(record.dependencyPolicy) || undefined,
1468
+ residualRisks: asStringArray(record.residualRisks),
1469
+ realIntegrationGap: asString(record.realIntegrationGap) || undefined,
1470
+ uiComponentChoices: normalizeUiComponentChoices(record.uiComponentChoices),
1334
1471
  };
1335
1472
  }
1336
1473
  /**
@@ -1490,6 +1627,206 @@ export async function writeFrontendImplementationContractArtifact(input) {
1490
1627
  schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
1491
1628
  };
1492
1629
  }
1630
+ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
1631
+ "/schemaVersion",
1632
+ "/sourceBinding",
1633
+ "/riskLevel",
1634
+ "/targets/files",
1635
+ "/mockApi/productionDefaultOff",
1636
+ ];
1637
+ function frontendPlanPatchProtectedPathViolations(patch) {
1638
+ const record = asRecord(patch);
1639
+ if (!record)
1640
+ return ["/"];
1641
+ const violations = [];
1642
+ for (const key of ["schemaVersion", "sourceBinding", "riskLevel"]) {
1643
+ if (Object.hasOwn(record, key))
1644
+ violations.push(`/${key}`);
1645
+ }
1646
+ const targets = asRecord(record.targets);
1647
+ if (targets && Object.hasOwn(targets, "files"))
1648
+ violations.push("/targets/files");
1649
+ const mockApi = asRecord(record.mockApi);
1650
+ if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
1651
+ violations.push("/mockApi/productionDefaultOff");
1652
+ return violations;
1653
+ }
1654
+ function workspaceRootFromDagRunDir(runDir) {
1655
+ const absolute = path.resolve(runDir);
1656
+ const segments = absolute.split(path.sep);
1657
+ const harnessIndex = segments.lastIndexOf(".harness");
1658
+ if (harnessIndex < 0 ||
1659
+ segments[harnessIndex + 1] !== "dag-runs" ||
1660
+ harnessIndex + 3 >= segments.length) {
1661
+ return null;
1662
+ }
1663
+ const joined = segments.slice(0, harnessIndex).join(path.sep);
1664
+ return joined || path.parse(absolute).root;
1665
+ }
1666
+ function verificationSymbolMatchesFile(symbol, content) {
1667
+ if (symbol.trim().toLowerCase() === "all describe blocks")
1668
+ return content.includes("describe(");
1669
+ const symbols = symbol.includes(" / ")
1670
+ ? symbol
1671
+ .split(/[((]/, 1)[0]
1672
+ .split("/")
1673
+ .map((item) => item.trim())
1674
+ .filter(Boolean)
1675
+ : [symbol];
1676
+ return symbols.every((item) => {
1677
+ const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
1678
+ return [item, describeTitle, describeTitle ? "describe(" : undefined]
1679
+ .filter((candidate) => Boolean(candidate))
1680
+ .some((candidate) => content.includes(candidate));
1681
+ });
1682
+ }
1683
+ async function dropUnresolvedVerificationSymbols(input) {
1684
+ const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
1685
+ const targets = asRecord(input.contract)?.verificationTargets;
1686
+ if (!workspaceRoot || !Array.isArray(targets))
1687
+ return false;
1688
+ let changed = false;
1689
+ for (const targetValue of targets) {
1690
+ const target = asRecord(targetValue);
1691
+ const file = asString(target?.file);
1692
+ const symbol = asString(target?.symbol);
1693
+ if (!target || !file || !symbol)
1694
+ continue;
1695
+ const absolute = path.resolve(workspaceRoot, file);
1696
+ const relative = path.relative(workspaceRoot, absolute);
1697
+ if (relative.startsWith("..") ||
1698
+ path.isAbsolute(relative) ||
1699
+ relative.includes("..")) {
1700
+ continue;
1701
+ }
1702
+ let content;
1703
+ try {
1704
+ content = await readFile(absolute, "utf8");
1705
+ }
1706
+ catch {
1707
+ // Missing files remain a trace-time hard failure; do not hide them by
1708
+ // changing the optional symbol during candidate normalization.
1709
+ continue;
1710
+ }
1711
+ if (verificationSymbolMatchesFile(symbol, content))
1712
+ continue;
1713
+ delete target.symbol;
1714
+ changed = true;
1715
+ }
1716
+ return changed;
1717
+ }
1718
+ function extractSingleOpenspecCitationsBlock(text) {
1719
+ const blocks = text.match(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g) ?? [];
1720
+ if (blocks.length !== 1) {
1721
+ throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
1722
+ }
1723
+ return blocks[0];
1724
+ }
1725
+ async function writeFrontendPlanCandidateRaw(input) {
1726
+ const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
1727
+ await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text.endsWith("\n") ? input.text : `${input.text}\n`);
1728
+ return relativePath;
1729
+ }
1730
+ /**
1731
+ * Compile the planner's editable RFC 7386 patch against the deterministic
1732
+ * runtime skeleton. Every attempt is preserved under contracts/candidates so
1733
+ * format/schema failures can be inspected and replayed without trusting model
1734
+ * prose. A successful result returns canonical full-contract text for all
1735
+ * downstream review and prewrite nodes.
1736
+ */
1737
+ export async function validateFrontendPlanPatchNodeOutput(input) {
1738
+ const nodeId = input.nodeId ?? "frontend-plan-pi";
1739
+ const attempt = input.attempt ?? 1;
1740
+ const candidateDir = path.posix.join("contracts", "candidates", nodeId);
1741
+ const rawPath = await writeFrontendPlanCandidateRaw({
1742
+ runDir: input.runDir,
1743
+ nodeId,
1744
+ attempt,
1745
+ text: input.text,
1746
+ });
1747
+ const reportBase = {
1748
+ schemaVersion: 1,
1749
+ schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID,
1750
+ nodeId,
1751
+ attempt,
1752
+ rawPath,
1753
+ protectedPaths: [...FRONTEND_PLAN_PATCH_PROTECTED_PATHS],
1754
+ candidateRawSha256: sha256Hex(input.text),
1755
+ };
1756
+ let extractedPatchPath;
1757
+ let extractedPatchSha256;
1758
+ const writeReport = async (value) => {
1759
+ await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.validation.json`), {
1760
+ ...reportBase,
1761
+ ...(extractedPatchPath
1762
+ ? { extractedPatchPath, extractedPatchSha256 }
1763
+ : {}),
1764
+ ...value,
1765
+ });
1766
+ };
1767
+ try {
1768
+ if (!input.sourceBinding)
1769
+ throw new Error("frontend plan patch validator requires DAG sourceBinding");
1770
+ const skeleton = input.structuredContractOutput?.skeleton;
1771
+ if (!skeleton)
1772
+ throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
1773
+ const patch = extractFrontendImplementationJson(input.text);
1774
+ if (!isPlainObject(patch))
1775
+ throw new Error("frontend plan patch must extract to one JSON object");
1776
+ extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
1777
+ const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
1778
+ extractedPatchSha256 = extractedArtifact.sha256;
1779
+ const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
1780
+ const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1781
+ if (protectedViolations.length > 0)
1782
+ throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
1783
+ const merged = applyFrontendContractMergePatch(skeleton, patch);
1784
+ const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
1785
+ runDir: input.runDir,
1786
+ contract: merged,
1787
+ });
1788
+ const analysis = await analyzeFrontendImplementationContract({
1789
+ runDir: input.runDir,
1790
+ rawContractText: serializeDeterministicJson(merged),
1791
+ sourceBinding: input.sourceBinding,
1792
+ });
1793
+ const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
1794
+ await writeReport({
1795
+ classification: "accepted-normalized",
1796
+ normalizedContractPath: path.posix.relative(input.runDir.replaceAll(path.sep, "/"), normalizedArtifact.path.replaceAll(path.sep, "/")),
1797
+ normalizedContractSha256: normalizedArtifact.sha256,
1798
+ normalizationActions: [
1799
+ "apply-runtime-skeleton",
1800
+ ...(droppedUnresolvedSymbols
1801
+ ? ["drop-unresolved-verification-symbols"]
1802
+ : []),
1803
+ ...analysis.normalizationActions,
1804
+ ],
1805
+ errors: [],
1806
+ });
1807
+ return {
1808
+ ok: true,
1809
+ contract: analysis.canonical,
1810
+ normalizedText: [
1811
+ "Frontend implementation contract compiled from the runtime skeleton.",
1812
+ "",
1813
+ "```json",
1814
+ serializeDeterministicJson(analysis.canonical),
1815
+ "```",
1816
+ citationsBlock,
1817
+ ].join("\n"),
1818
+ };
1819
+ }
1820
+ catch (error) {
1821
+ const reason = error instanceof Error ? error.message : String(error);
1822
+ await writeReport({
1823
+ classification: "retryable-invalid",
1824
+ normalizationActions: [],
1825
+ errors: [{ path: "$", code: "invalid-output", message: reason }],
1826
+ });
1827
+ return { ok: false, reason };
1828
+ }
1829
+ }
1493
1830
  /**
1494
1831
  * Node-output self-check for plan nodes that produce the implementation
1495
1832
  * contract. Mirrors the prewrite gate validation so schema/typo/null violations
@@ -1514,6 +1851,55 @@ export async function validateFrontendContractNodeOutput(input) {
1514
1851
  };
1515
1852
  }
1516
1853
  }
1854
+ /**
1855
+ * Node-output self-check for the plan revision node, which emits an RFC 7386
1856
+ * merge-patch delta against the original contract instead of a full contract.
1857
+ * Deliberately loose and format-level: the output must contain exactly one
1858
+ * ```openspec-citations fenced block and must extract (after the citations
1859
+ * blocks are stripped) to exactly one JSON object. The delta carries no
1860
+ * schemaVersion/targets, so no full-contract schema/semantic checks run here;
1861
+ * content parsing, patch application, and merged-contract validation stay with
1862
+ * the prewrite gate, which remains the only authority.
1863
+ */
1864
+ export async function validateFrontendRevisionPatchNodeOutput(input) {
1865
+ let citationsBlock;
1866
+ try {
1867
+ citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
1868
+ }
1869
+ catch (error) {
1870
+ return {
1871
+ ok: false,
1872
+ reason: error instanceof Error ? error.message : String(error),
1873
+ };
1874
+ }
1875
+ // Strip the citation blocks before extraction: their per-row JSON objects
1876
+ // would otherwise count as competing balanced objects in the extractor.
1877
+ const textWithoutCitations = input.text.replace(citationsBlock, "");
1878
+ let patch;
1879
+ try {
1880
+ patch = extractFrontendImplementationJson(textWithoutCitations);
1881
+ }
1882
+ catch (error) {
1883
+ return {
1884
+ ok: false,
1885
+ reason: `revision patch output must contain exactly one valid JSON object: ${error instanceof Error ? error.message : String(error)}`,
1886
+ };
1887
+ }
1888
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
1889
+ return {
1890
+ ok: false,
1891
+ reason: "revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)",
1892
+ };
1893
+ }
1894
+ const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1895
+ if (protectedViolations.length > 0) {
1896
+ return {
1897
+ ok: false,
1898
+ reason: `frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`,
1899
+ };
1900
+ }
1901
+ return { ok: true, contract: patch };
1902
+ }
1517
1903
  export async function materializeFrontendImplementationContract(input) {
1518
1904
  const analysis = await analyzeFrontendImplementationContract(input);
1519
1905
  return writeFrontendImplementationContractArtifact({
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Deterministic Markdown renderer for a canonical frontend implementation
3
+ * contract. The prewrite gate materializes this as `contracts/frontend-plan.md`
4
+ * next to the contract JSON, so plan prose is never produced by the model — it
5
+ * is derived from the single machine contract at materialization time.
6
+ *
7
+ * The function is pure: the same contract always renders byte-identical
8
+ * Markdown, and absent optional fields render as placeholders without throwing.
9
+ */
10
+ function bulletList(items) {
11
+ if (!items || items.length === 0)
12
+ return "";
13
+ return items.map((item) => `- ${item}`).join("\n");
14
+ }
15
+ function requirementsCoverage(contract) {
16
+ const lines = contract.requirements.map((requirement) => `- ${requirement.id}: ${requirement.expectedOutcome}`);
17
+ return lines.length > 0 ? lines.join("\n") : "_(no requirements)_";
18
+ }
19
+ function uiStateHandling(contract) {
20
+ const lines = contract.uiStates.map((state) => {
21
+ if (state.applicable) {
22
+ return `- ${state.name}: ${state.expectedBehavior ?? ""}`;
23
+ }
24
+ return `- ${state.name}: not applicable — ${state.notApplicableReason ?? ""}`;
25
+ });
26
+ return lines.length > 0 ? lines.join("\n") : "_(none)_";
27
+ }
28
+ function interactionNotes(contract) {
29
+ const lines = contract.interactions.map((interaction) => `- ${interaction.name}: ${interaction.trigger} → ${interaction.expectedBehavior}`);
30
+ return lines.length > 0 ? lines.join("\n") : "_(none)_";
31
+ }
32
+ function verificationPlan(contract) {
33
+ const lines = contract.verificationTargets.map((target) => `- ${target.id}: ${target.commandLabel} (${target.file})`);
34
+ return lines.length > 0 ? lines.join("\n") : "_(none)_";
35
+ }
36
+ function componentSelection(contract) {
37
+ const choices = contract.uiComponentChoices ?? [];
38
+ if (choices.length === 0)
39
+ return "_(not specified)_";
40
+ return choices
41
+ .map((choice) => {
42
+ const specReference = choice.specReference;
43
+ const refText = specReference
44
+ ? ` ${specReference.path}${specReference.section ? `#${specReference.section}` : ""}${specReference.line != null ? `:${specReference.line}` : ""}`
45
+ : "";
46
+ return `- ${choice.purpose} → ${choice.component} (${choice.decision})${refText ? `:${refText}` : ""} — ${choice.rationale}`;
47
+ })
48
+ .join("\n");
49
+ }
50
+ export function renderFrontendPlanMarkdown(contract) {
51
+ return [
52
+ "# Frontend Implementation Plan",
53
+ "",
54
+ "## Requirement Coverage",
55
+ requirementsCoverage(contract),
56
+ "",
57
+ "## Implementation Steps",
58
+ bulletList(contract.implementationSteps) || "_(not specified)_",
59
+ "",
60
+ "## Target Files",
61
+ bulletList(contract.targets.files) || "_(none)_",
62
+ "",
63
+ "## UI State Handling",
64
+ uiStateHandling(contract),
65
+ "",
66
+ "## Styling & Component Strategy",
67
+ contract.stylingStrategy || "_(not specified)_",
68
+ "",
69
+ "## Component Selection (spec conformance)",
70
+ componentSelection(contract),
71
+ "",
72
+ "## Interaction Notes",
73
+ interactionNotes(contract),
74
+ "",
75
+ "## Mock & API Strategy",
76
+ contract.mockApi.strategy,
77
+ "",
78
+ "## Dependency Policy",
79
+ contract.dependencyPolicy || "_(not specified)_",
80
+ "",
81
+ "## Verification Plan",
82
+ verificationPlan(contract),
83
+ "",
84
+ "## Real Integration Gap",
85
+ contract.realIntegrationGap || "_(not specified)_",
86
+ "",
87
+ "## Residual Risks",
88
+ bulletList(contract.residualRisks) || "_(none)_",
89
+ ].join("\n") + "\n";
90
+ }