@tea-agent/loop-agent 0.43.0-next.18 → 0.43.0-next.19

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 (161) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +60 -2308
  3. package/README.md +7 -0
  4. package/dist/application/task-lifecycle/advance.js +38 -7
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/commands/init.js +13 -9
  7. package/dist/commands/task-advance.js +32 -0
  8. package/dist/executors/dag-pi-executor.js +1615 -188
  9. package/dist/executors/pi-executor.js +85 -3
  10. package/dist/executors/pi-sdk-executor.js +18 -0
  11. package/dist/executors/shell-executor.js +92 -7
  12. package/dist/shared/backend-dogfood-preflight.js +47 -0
  13. package/dist/shared/dag-failure-category.js +3 -0
  14. package/dist/shared/operator/capabilities.js +180 -0
  15. package/dist/task/config-types.js +29 -0
  16. package/dist/task/contract/project.js +9 -0
  17. package/dist/task/contract/schema.js +17 -0
  18. package/dist/task/source-prepare/build-draft.js +60 -0
  19. package/dist/worker/console/chat/pi-mode-loop-isolation.js +155 -0
  20. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +24 -16
  21. package/dist/worker/console/chat/pi-runtime/custom-tools/scheduled-goal-tools.js +22 -0
  22. package/dist/worker/console/chat/pi-runtime/progressive-tools.js +195 -0
  23. package/dist/worker/console/chat/pi-runtime.js +86 -27
  24. package/dist/worker/console/chat/routes.js +19 -0
  25. package/dist/worker/console/chat/scheduled-goal-booking.js +59 -0
  26. package/dist/worker/console/chat/scheduled-goal-delivery.js +27 -0
  27. package/dist/worker/console/chat/scheduled-goal-request.js +190 -0
  28. package/dist/worker/console/chat/session-mode.js +7 -3
  29. package/dist/worker/console/chat/session-store.js +42 -9
  30. package/dist/worker/console/chat/tool-preview.js +115 -0
  31. package/dist/worker/console/chat/turn-order.js +13 -0
  32. package/dist/worker/console/chat/turn-process.js +32 -30
  33. package/dist/worker/console/operator-actions.js +50 -0
  34. package/dist/worker/console/prd-intake-bridge.js +54 -1
  35. package/dist/worker/console/scheduled-goal-host.js +98 -0
  36. package/dist/worker/console/scheduled-goal-operation-adapter.js +127 -0
  37. package/dist/worker/console/server.js +15 -0
  38. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B_UDknhR.js → abnfDiagram-N423BO3Z-8-j6y-sd.js} +1 -1
  39. package/dist/worker/console/static/assets/{arc-D6hU0drN.js → arc-eoQiMvuk.js} +1 -1
  40. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DmVikdRK.js → architectureDiagram-T3A2C74G-C4A3uMcI.js} +1 -1
  41. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DydABCRg.js → blockDiagram-VBNYF7ZC-D4zD2F-Q.js} +1 -1
  42. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DrXKC2Jl.js → c4Diagram-5PPSVZJV-j1RkJziL.js} +1 -1
  43. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +1 -0
  44. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D2-qFxr7.js → chunk-2GRJ4B5K-YBHmhik1.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ChfeF41P.js → chunk-2Q5K7J3B-COfWyo9P.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Drwrz55x.js → chunk-5RXB4S5H-I99OUkHY.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CAQGErL7.js → chunk-5VM5RSS4-XKxoJNJ4.js} +1 -1
  48. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DHO38w5U.js → chunk-6Q2QTUOP-DLT_cYx1.js} +1 -1
  49. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-EYIN0EOI.js → chunk-GF5L2VYU-CxcKZ9mV.js} +1 -1
  50. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CrFLKqkT.js → chunk-JWPE2WC7-V1EqXdjY.js} +1 -1
  51. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMchffoo.js → chunk-KBJHAD2P-DebTtdFp.js} +1 -1
  52. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DaSvtKQa.js → chunk-RYQCIY6F-B-ivNRDf.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DcW8W9Qj.js → chunk-XXDRQBXY-DC_Ds11b.js} +1 -1
  54. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +1 -0
  55. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +1 -0
  56. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BUirrMEi.js → cose-bilkent-JH36ORCC-sWEqKwIb.js} +1 -1
  57. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXzO8iXR.js → cynefin-VYW2F7L2-BXa_dcu4.js} +1 -1
  58. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DAtgaxIn.js → cynefinDiagram-MW4NZA55-C-Mle84F.js} +1 -1
  59. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C1re9Noh.js → dagre-VZM6K2ZE-CXPDBITe.js} +1 -1
  60. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-N8Kj08J9.js → diagram-7IWD3JNH-CC-WJQfa.js} +1 -1
  61. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-D6BpSLs3.js → diagram-B4RE2ZJO-CDAV5Vs4.js} +1 -1
  62. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C2Vlgnl4.js → diagram-LBJQPF4R-CmFiAcNz.js} +1 -1
  63. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-CMbqtVLS.js → diagram-Q27KOJAE-DPBZHuyn.js} +1 -1
  64. package/dist/worker/console/static/assets/{diagram-UB23O5K3-A770Eb-0.js → diagram-UB23O5K3-jUlm_Ds3.js} +1 -1
  65. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-LV2w_2pT.js → ebnfDiagram-BXEA7PRR-DWhQ3mfY.js} +1 -1
  66. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-bwqf56ah.js → erDiagram-JOGREHBK-Tr2gMqet.js} +1 -1
  67. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BnVtHhZh.js → flowDiagram-UKHOOZJN-DJjVQHPA.js} +1 -1
  68. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BGVToEqc.js → ganttDiagram-PKOTCBZU-D74dQ4u0.js} +1 -1
  69. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DL2l7Vne.js → gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js} +1 -1
  70. package/dist/worker/console/static/assets/index-BWkIfcrK.css +1 -0
  71. package/dist/worker/console/static/assets/{index-B_V4wvXs.js → index-Cdkvw_H6.js} +97 -97
  72. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BWIpubOx.js → infoDiagram-6WML65LV-ILCbxJyb.js} +1 -1
  73. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DjYBD8vv.js → ishikawaDiagram-WSZJBQD7-DeuWBQ89.js} +1 -1
  74. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C0_mBaHb.js → journeyDiagram-NVQOT4AX-CF5ih8Fk.js} +1 -1
  75. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Cy71zbX6.js → kanban-definition-27J2QSJJ-C7yOSuRO.js} +1 -1
  76. package/dist/worker/console/static/assets/{linear-Dt3_w3Vn.js → linear-BDZ9riWi.js} +1 -1
  77. package/dist/worker/console/static/assets/{mermaid.core-DnNOlfEP.js → mermaid.core-7pKqYtpZ.js} +5 -5
  78. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DAbTspwq.js → mindmap-definition-FAOFIHXS-LeJDybSU.js} +1 -1
  79. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-NNiM141p.js → pegDiagram-VL7TDLO6-BJvT3pMD.js} +1 -1
  80. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CN80Z1Do.js → pieDiagram-7S7Q4E2Y-_rGqpMan.js} +1 -1
  81. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bxyfh0JU.js → quadrantDiagram-CIZ2JOQS-DPcSv5aA.js} +1 -1
  82. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BlWnC79M.js → railroadDiagram-AXF67PYL-Drxx4hkJ.js} +1 -1
  83. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BrEu4P_e.js → requirementDiagram-LRYGKXZP-BCTUdU4z.js} +1 -1
  84. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bt70igBr.js → sankeyDiagram-W5VNT64P-B6wzbZmB.js} +1 -1
  85. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C8DQR-r9.js → sequenceDiagram-SI44F4Z6-BGt8d3QQ.js} +1 -1
  86. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DEMinJAT.js → sizeCapture-X5ZJPWSS-7nlhJKo7.js} +1 -1
  87. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-DDdDBzQc.js → stateDiagram-OKZ733FA-Cv2stqMA.js} +1 -1
  88. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +1 -0
  89. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-G2wcBs3T.js → swimlanes-SLNWSIFB-qDAo4Yc1.js} +2 -2
  90. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +8 -0
  91. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Dzn4g2gJ.js → timeline-definition-Z64GVDOM-CKbDNKTr.js} +1 -1
  92. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-5v2rP9oO.js → vennDiagram-T6HMQDX7-DI-9EHic.js} +1 -1
  93. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-B4EOx7ew.js → wardleyDiagram-T6FBY63Y-BrzzRDpX.js} +1 -1
  94. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQ1v8eYP.js → xychartDiagram-ELKLHX3M-BclH5hGh.js} +1 -1
  95. package/dist/worker/console/static/index.html +2 -2
  96. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +41 -11
  97. package/dist/worker/console/static-src/operator-chat/compaction-message.js +3 -17
  98. package/dist/worker/console/static-src/operator-chat/timeline-merge.js +29 -0
  99. package/dist/worker/console/static-src/operator-chat/useChatThread.js +2 -5
  100. package/dist/worker/console/workspace-context.js +11 -0
  101. package/dist/worker/observe/static/operator-chrome.js +3 -1
  102. package/dist/worker/scheduler/scheduled-goal-dispatch.js +117 -0
  103. package/dist/worker/scheduler/scheduled-goal-evidence.js +167 -0
  104. package/dist/worker/scheduler/scheduled-goal-recovery.js +62 -0
  105. package/dist/worker/scheduler/scheduled-goal-store.js +699 -0
  106. package/dist/worker/scheduler/scheduled-goal-supervisor.js +132 -0
  107. package/dist/worker/scheduler/scheduled-goal-time.js +102 -0
  108. package/dist/worker/scheduler/scheduled-goal-types.js +95 -0
  109. package/dist/workflows/dag/backend-test-markdown-workflow.js +6 -24
  110. package/dist/workflows/dag/backend-test-plan-protocol.js +82 -5
  111. package/dist/workflows/dag/dag-retry-schema.js +11 -0
  112. package/dist/workflows/dag/frontend-closeout.js +4 -2
  113. package/dist/workflows/dag/frontend-committed-facts.js +461 -0
  114. package/dist/workflows/dag/frontend-durable-tools.js +15 -3
  115. package/dist/workflows/dag/frontend-implementation-contract.js +365 -8
  116. package/dist/workflows/dag/frontend-plan-canary.js +53 -0
  117. package/dist/workflows/dag/frontend-plan-decision-contract.js +803 -0
  118. package/dist/workflows/dag/frontend-plan-render.js +0 -2
  119. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  120. package/dist/workflows/dag/frontend-provider-capability-matrix.js +8 -61
  121. package/dist/workflows/dag/frontend-recovery-run.js +57 -0
  122. package/dist/workflows/dag/frontend-review-context.js +43 -70
  123. package/dist/workflows/dag/frontend-risk.js +92 -10
  124. package/dist/workflows/dag/frontend-session-budget.js +117 -3
  125. package/dist/workflows/dag/frontend-shape.js +11 -55
  126. package/dist/workflows/dag/frontend-test-execution-evidence.js +35 -10
  127. package/dist/workflows/dag/frontend-typed-event-store.js +32 -25
  128. package/dist/workflows/dag/frontend-verification-trace.js +11 -2
  129. package/dist/workflows/dag/frontend-writer-admission.js +3 -47
  130. package/dist/workflows/dag/frontend-writer-status.js +0 -23
  131. package/dist/workflows/dag/init-hybrid.js +182 -185
  132. package/dist/workflows/dag/lifecycle.js +7 -2
  133. package/dist/workflows/dag/node-execution.js +48 -13
  134. package/dist/workflows/dag/rerun-plan.js +10 -0
  135. package/dist/workflows/dag/rerun-task.js +77 -1
  136. package/dist/workflows/dag/retry-policy.js +18 -0
  137. package/dist/workflows/dag/scheduler.js +2 -4
  138. package/dist/workflows/dag/types.js +23 -0
  139. package/docs/README.md +1 -0
  140. package/docs/init-surface.manifest.json +1 -0
  141. package/docs/operations/README.md +2 -0
  142. package/docs/templates/README.md +1 -1
  143. package/docs/templates/agent-dag.schema.json +2 -2
  144. package/docs/templates/backend-test-dag.json +14 -10
  145. package/docs/templates/frontend-implementation-contract.schema.json +0 -7
  146. package/docs/templates/frontend-implementation-dag.json +8 -9
  147. package/package.json +6 -3
  148. package/skills/frontend-bounded-implement/SKILL.md +3 -4
  149. package/skills/frontend-design-review/SKILL.md +7 -12
  150. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  151. package/skills/frontend-plan/SKILL.md +8 -18
  152. package/skills/frontend-plan/references/decision-contract.md +19 -26
  153. package/skills/frontend-review/SKILL.md +21 -25
  154. package/skills/frontend-review/references/review-findings.md +48 -24
  155. package/dist/worker/console/static/assets/channel-Drecd94a.js +0 -1
  156. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-8Udu0t8-.js +0 -1
  157. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-8Udu0t8-.js +0 -1
  158. package/dist/worker/console/static/assets/index-DcudonhZ.css +0 -1
  159. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CyCiS0Lt.js +0 -1
  160. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-U0lhi6N0.js +0 -8
  161. package/dist/workflows/dag/frontend-shadow-dual-write.js +0 -975
@@ -443,6 +443,306 @@ async function deriveWriteSetFromRun(runDir) {
443
443
  }
444
444
  return [...writeSet];
445
445
  }
446
+ /** Read the frozen task source paths from run.json sourceBinding so the plan
447
+ * precheck can require new/specified component choices to cite a traceable
448
+ * task source. Smoke r37/r38: specReference.path pointed at a to-be-implemented
449
+ * file or an AC id — a demand that only lived in review prose could never be
450
+ * closed by the plan retry ladder. The allowed set is enumerable here. */
451
+ async function deriveFrontendSourceBindingPathsFromRun(runDir) {
452
+ const specPath = path.join(runDir, "run.json");
453
+ let raw;
454
+ try {
455
+ raw = await readFile(specPath, "utf8");
456
+ }
457
+ catch {
458
+ return [];
459
+ }
460
+ let spec;
461
+ try {
462
+ spec = JSON.parse(raw);
463
+ }
464
+ catch {
465
+ return [];
466
+ }
467
+ const paths = [];
468
+ for (const source of spec.sourceBinding?.sources ?? []) {
469
+ if (source &&
470
+ typeof source === "object" &&
471
+ typeof source.path === "string" &&
472
+ (source.path).trim()) {
473
+ paths.push((source.path).trim());
474
+ }
475
+ }
476
+ return paths;
477
+ }
478
+ function stripSourcePathFragment(value) {
479
+ const hash = value.indexOf("#");
480
+ return (hash >= 0 ? value.slice(0, hash) : value).trim().replace(/\/$/, "");
481
+ }
482
+ /** decision=new/specified component choices must cite a frozen task source
483
+ * path (requirement/constraint), never a to-be-implemented file or a bare
484
+ * acceptance-criterion id. Absent sourceBinding paths (legacy fixtures) the
485
+ * check is skipped — generation always freezes them. */
486
+ function checkComponentChoiceSourceReferences(canonical, sourcePaths) {
487
+ const findings = [];
488
+ if (sourcePaths.length === 0)
489
+ return findings;
490
+ for (const choice of canonical.uiComponentChoices ?? []) {
491
+ if (choice.decision !== "new" && choice.decision !== "specified") {
492
+ continue;
493
+ }
494
+ const rawPath = choice.specReference?.path ?? "";
495
+ const normalized = stripSourcePathFragment(rawPath);
496
+ const traceable = normalized.length > 0 &&
497
+ sourcePaths.some((source) => stripSourcePathFragment(source) === normalized);
498
+ if (!traceable) {
499
+ findings.push({
500
+ code: "component-choice-source-reference-invalid",
501
+ message: `component choice "${choice.component}" (decision=${choice.decision}) must cite a frozen task source path as specReference.path; allowed: [${sourcePaths.join(", ")}]${rawPath ? ` — got "${rawPath}"` : " — missing"}`,
502
+ path: "uiComponentChoices",
503
+ });
504
+ }
505
+ }
506
+ return findings;
507
+ }
508
+ const CAPABILITY_BOUNDARY_MARKER = "测试能力边界(冻结)";
509
+ export async function deriveFrontendInteractionIdsFromRun(runDir) {
510
+ const specPath = path.join(runDir, "run.json");
511
+ let raw;
512
+ try {
513
+ raw = await readFile(specPath, "utf8");
514
+ }
515
+ catch {
516
+ return [];
517
+ }
518
+ let spec;
519
+ try {
520
+ spec = JSON.parse(raw);
521
+ }
522
+ catch {
523
+ return [];
524
+ }
525
+ for (const task of spec.tasks ?? []) {
526
+ const ids = task.shell?.frontendDesignPolicy?.interactionIds;
527
+ if (ids && ids.length > 0)
528
+ return ids.filter((id) => typeof id === "string" && id.trim());
529
+ }
530
+ return [];
531
+ }
532
+ function checkInteractionIdVocabulary(canonical, declaredIds) {
533
+ const findings = [];
534
+ if (declaredIds.length === 0)
535
+ return findings;
536
+ for (const interaction of canonical.interactions ?? []) {
537
+ if (!declaredIds.includes(interaction.name)) {
538
+ findings.push({
539
+ code: "interaction-name-not-declared",
540
+ message: `interaction "${interaction.name}" is not in the frozen interaction id vocabulary; allowed: [${declaredIds.join(", ")}] — retract the wrong record via record_data_flow {"interaction": "${interaction.name}", "retracted": true} or retract_data_flow, then record the declared id`,
541
+ path: "interactions",
542
+ });
543
+ }
544
+ }
545
+ return findings;
546
+ }
547
+ export async function deriveFrontendAllowedMockStrategiesFromRun(runDir) {
548
+ const specPath = path.join(runDir, "run.json");
549
+ let raw;
550
+ try {
551
+ raw = await readFile(specPath, "utf8");
552
+ }
553
+ catch {
554
+ return [];
555
+ }
556
+ let spec;
557
+ try {
558
+ spec = JSON.parse(raw);
559
+ }
560
+ catch {
561
+ return [];
562
+ }
563
+ for (const task of spec.tasks ?? []) {
564
+ const strategies = task.shell?.frontendDesignPolicy?.allowedMockStrategies;
565
+ if (strategies &&
566
+ strategies.length > 0 &&
567
+ strategies.every((s) => typeof s === "string" && s.trim())) {
568
+ return strategies;
569
+ }
570
+ }
571
+ return [];
572
+ }
573
+ function checkMockApiStrategyAllowed(canonical, allowedStrategies) {
574
+ const findings = [];
575
+ if (allowedStrategies.length === 0)
576
+ return findings;
577
+ const strategy = canonical.mockApi?.strategy;
578
+ if (strategy && !allowedStrategies.includes(strategy)) {
579
+ const onlyNotNeeded = allowedStrategies.length === 1 && allowedStrategies[0] === "not-needed";
580
+ findings.push({
581
+ code: "mock-strategy-not-allowed",
582
+ message: `mockApi.strategy "${strategy}" is not in the frozen allowed strategies; allowed: [${allowedStrategies.join(", ")}] — re-record the api mock decision with an allowed strategy (relationship path: record_mock_api {"mockApi": {...}, "replace": true}; decision path: record_api_mock_boundary with the same boundary and a mode that maps to an allowed strategy)${onlyNotNeeded ? "; the run has no deterministic Mock verification capability, so keep strategy \"not-needed\" and preserve the real integration gap instead of inventing Mock evidence" : ""}`,
583
+ path: "mockApi",
584
+ });
585
+ }
586
+ return findings;
587
+ }
588
+ async function deriveFrontendCapabilityBoundaryFromRun(runDir) {
589
+ const specPath = path.join(runDir, "run.json");
590
+ let raw;
591
+ try {
592
+ raw = await readFile(specPath, "utf8");
593
+ }
594
+ catch {
595
+ return undefined;
596
+ }
597
+ let spec;
598
+ try {
599
+ spec = JSON.parse(raw);
600
+ }
601
+ catch {
602
+ return undefined;
603
+ }
604
+ for (const task of spec.tasks ?? []) {
605
+ const boundary = task.shell?.frontendDesignPolicy?.capabilityBoundary;
606
+ if (boundary &&
607
+ ((boundary.allowed?.length ?? 0) > 0 ||
608
+ (boundary.forbidden?.length ?? 0) > 0)) {
609
+ return boundary;
610
+ }
611
+ }
612
+ return undefined;
613
+ }
614
+ /** Runtime composes the frozen capability boundary into dependencyPolicy —
615
+ * the planner never maintains it (smoke r42: the boundary lived in PRD prose,
616
+ * the plan omitted it twice, and recovery could not close a prose gap). */
617
+ export function composeDependencyPolicyWithBoundary(policy, boundary) {
618
+ if (!boundary)
619
+ return policy;
620
+ const allowed = (boundary.allowed ?? []).filter((item) => item.trim());
621
+ const forbidden = (boundary.forbidden ?? []).filter((item) => item.trim());
622
+ if (allowed.length === 0 && forbidden.length === 0)
623
+ return policy;
624
+ if (policy?.includes(CAPABILITY_BOUNDARY_MARKER))
625
+ return policy;
626
+ const sentence = `${CAPABILITY_BOUNDARY_MARKER}:` +
627
+ (allowed.length > 0 ? `仅可使用 ${allowed.join("、")}` : "无允许清单") +
628
+ (forbidden.length > 0 ? `;禁止 ${forbidden.join("、")}` : "") +
629
+ "。";
630
+ return policy?.trim() ? `${policy.trim()};${sentence}` : sentence;
631
+ }
632
+ /** Read the generation-frozen requirement→file ownership from run.json
633
+ * (frontendDesignPolicy spec). Absent (legacy fixtures) → no ownership gate. */
634
+ export async function deriveRequirementOwnershipFromRun(runDir) {
635
+ const specPath = path.join(runDir, "run.json");
636
+ let raw;
637
+ try {
638
+ raw = await readFile(specPath, "utf8");
639
+ }
640
+ catch {
641
+ return [];
642
+ }
643
+ let spec;
644
+ try {
645
+ spec = JSON.parse(raw);
646
+ }
647
+ catch {
648
+ return [];
649
+ }
650
+ const merged = new Map();
651
+ for (const task of spec.tasks ?? []) {
652
+ for (const entry of task.shell?.frontendDesignPolicy?.requirementOwnership ?? []) {
653
+ const key = [...(entry.requirementIds ?? [])].sort().join("|");
654
+ if (!merged.has(key))
655
+ merged.set(key, entry);
656
+ }
657
+ }
658
+ return [...merged.values()];
659
+ }
660
+ /** Deterministic requirement→file ownership check. A requirement's
661
+ * implementation targets must live inside its declared implementationFiles,
662
+ * and every verification target bound to it must file under its declared
663
+ * verificationFiles. Unknown requirement ids are findings too: a typo in the
664
+ * declaration must fail the declaration, not deadlock the planner. */
665
+ function checkRequirementOwnership(canonical, ownership) {
666
+ const findings = [];
667
+ if (ownership.length === 0)
668
+ return findings;
669
+ const requirementById = new Map((canonical.requirements ?? []).map((requirement) => [requirement.id, requirement]));
670
+ const targetsByRequirement = new Map();
671
+ for (const target of canonical.verificationTargets ?? []) {
672
+ for (const id of target.requirementIds ?? []) {
673
+ if (!targetsByRequirement.has(id))
674
+ targetsByRequirement.set(id, new Set());
675
+ targetsByRequirement.get(id).add(target.file);
676
+ }
677
+ }
678
+ const declaredRequirementIds = new Set();
679
+ for (const entry of ownership) {
680
+ for (const id of entry.requirementIds)
681
+ declaredRequirementIds.add(id);
682
+ }
683
+ for (const entry of ownership) {
684
+ for (const id of entry.requirementIds) {
685
+ if (!requirementById.has(id)) {
686
+ findings.push({
687
+ code: "requirement-ownership-unknown-requirement",
688
+ message: `requirementOwnership declares unknown requirement id "${id}"; frozen requirement ids: [${[...requirementById.keys()].join(", ")}]`,
689
+ path: "requirements",
690
+ });
691
+ }
692
+ }
693
+ }
694
+ for (const entry of ownership) {
695
+ const implementationFiles = entry.implementationFiles ?? [];
696
+ const verificationFiles = entry.verificationFiles ?? [];
697
+ for (const id of entry.requirementIds) {
698
+ const requirement = requirementById.get(id);
699
+ if (!requirement)
700
+ continue;
701
+ if (implementationFiles.length > 0) {
702
+ const allTargets = requirement.implementationTargets ?? [];
703
+ // Test-shaped placements belong to the verification lane (a shared
704
+ // behavior group's placement carries the test file for the
705
+ // test-writing requirement); they are governed by allowedPaths and
706
+ // the entry's verificationFiles, not by implementationFiles.
707
+ const implementationFilesAreTestShaped = implementationFiles.some((file) => isFrontendTestFilePath(file));
708
+ const targets = implementationFilesAreTestShaped
709
+ ? allTargets
710
+ : allTargets.filter((file) => !isFrontendTestFilePath(file));
711
+ // Contract deliverables are existence obligations, not ownership
712
+ // claims: a contract may require a mounting file to exist for a
713
+ // requirement implemented elsewhere (smoke r55 — a deliverable
714
+ // otherwise made plan finalize permanently unfixable).
715
+ const deliverables = new Set(requirement.deliverableTargets ?? []);
716
+ const ownedTargets = targets.filter((file) => !deliverables.has(file));
717
+ // Only files OUTSIDE the declared set violate. An empty owned set
718
+ // means every mapped file is a deliverable/test-shaped obligation —
719
+ // unplaced requirements are rejected upstream, not here (r56:
720
+ // "maps [X] expects [X]" still violated when deliverables covered
721
+ // the whole mapping).
722
+ const outside = ownedTargets.filter((file) => !implementationFiles.includes(file));
723
+ if (outside.length > 0) {
724
+ findings.push({
725
+ code: "requirement-ownership-violated",
726
+ message: `requirement ${id} expects implementation within [${implementationFiles.join(", ")}] but plan maps [${targets.join(", ") || "none"}] — re-map the owning behavior group's module placement`,
727
+ path: "requirements",
728
+ });
729
+ }
730
+ }
731
+ if (verificationFiles.length > 0) {
732
+ const files = [...(targetsByRequirement.get(id) ?? [])];
733
+ const outside = files.filter((file) => !verificationFiles.includes(file));
734
+ if (outside.length > 0) {
735
+ findings.push({
736
+ code: "requirement-ownership-violated",
737
+ message: `requirement ${id} expects verification evidence within [${verificationFiles.join(", ")}] but verification targets bind [${files.join(", ") || "none"}]`,
738
+ path: "verificationTargets",
739
+ });
740
+ }
741
+ }
742
+ }
743
+ }
744
+ return findings;
745
+ }
446
746
  export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
447
747
  export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
448
748
  export const FRONTEND_CONTRACT_REVIEW_INDEX_SCHEMA_ID = "frontend-contract-review-index-v1";
@@ -463,7 +763,7 @@ const FRONTEND_CONTRACT_REVIEW_GROUPS = [
463
763
  },
464
764
  {
465
765
  id: "verification",
466
- fields: ["verificationTargets", "evidenceGaps", "implementationSteps", "residualRisks"],
766
+ fields: ["verificationTargets", "evidenceGaps", "residualRisks"],
467
767
  },
468
768
  ];
469
769
  const FRONTEND_CONTRACT_CAPACITY_WARNING_RATIO = 0.6;
@@ -714,6 +1014,11 @@ export const frontendImplementationContractSchema = z
714
1014
  expectedOutcome: z.string().min(1),
715
1015
  execution: frontendExecutionSchema.optional(),
716
1016
  implementationTargets: z.array(safePath),
1017
+ /** Contract-bound "files that must exist" (from the plan
1018
+ * relationship patch). Ownership-exempt: a deliverable is an
1019
+ * existence obligation, not an implementation-ownership claim
1020
+ * (smoke r55 regression). */
1021
+ deliverableTargets: z.array(safePath).optional(),
717
1022
  verificationTargetIds: z.array(z.string().min(1)),
718
1023
  evidenceGap: gap.optional(),
719
1024
  // Source fidelity ledger binding (AC-005/AC-006): 每个 requirement
@@ -799,7 +1104,6 @@ export const frontendImplementationContractSchema = z
799
1104
  })
800
1105
  .strict()).min(1),
801
1106
  evidenceGaps: z.array(gap).optional(),
802
- implementationSteps: z.array(z.string().min(1)).optional(),
803
1107
  stylingStrategy: z.string().min(1).optional(),
804
1108
  uiComponentChoices: uiComponentChoicesSchema.optional(),
805
1109
  dependencyPolicy: z.string().min(1).optional(),
@@ -1681,6 +1985,7 @@ export function compactFrontendImplementationContract(value) {
1681
1985
  }
1682
1986
  compact.requirements = compactRecordArrayField(record.requirements, [
1683
1987
  "implementationTargets",
1988
+ "deliverableTargets",
1684
1989
  "verificationTargetIds",
1685
1990
  "sourceFragmentIds",
1686
1991
  "sourceRefs",
@@ -1716,7 +2021,7 @@ export function compactFrontendImplementationContract(value) {
1716
2021
  conflicts: dedupeJsonArray(designEvidence.conflicts),
1717
2022
  };
1718
2023
  }
1719
- for (const field of ["evidenceGaps", "implementationSteps", "residualRisks"]) {
2024
+ for (const field of ["evidenceGaps", "residualRisks"]) {
1720
2025
  if (Object.hasOwn(compact, field))
1721
2026
  compact[field] = dedupeJsonArray(compact[field]);
1722
2027
  }
@@ -2071,6 +2376,9 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
2071
2376
  ? { sourceFragmentIds: req.sourceFragmentIds }
2072
2377
  : {}),
2073
2378
  ...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
2379
+ ...(Array.isArray(req.deliverableTargets)
2380
+ ? { deliverableTargets: req.deliverableTargets }
2381
+ : {}),
2074
2382
  ...(req.execution !== undefined ? { execution: req.execution } : {}),
2075
2383
  };
2076
2384
  // Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
@@ -2250,7 +2558,6 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
2250
2558
  },
2251
2559
  verificationTargets,
2252
2560
  evidenceGaps,
2253
- implementationSteps: asStringArray(record.implementationSteps),
2254
2561
  stylingStrategy: asString(record.stylingStrategy) || undefined,
2255
2562
  dependencyPolicy: asString(record.dependencyPolicy) || undefined,
2256
2563
  residualRisks: asStringArray(record.residualRisks),
@@ -2486,6 +2793,7 @@ export async function analyzeFrontendImplementationContract(input) {
2486
2793
  if (!result.data.requirements.some((item) => item.id === requirementId) &&
2487
2794
  !(result.data.evidenceGaps ?? []).some((item) => item.requirementId === requirementId))
2488
2795
  fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
2796
+ result.data.dependencyPolicy = composeDependencyPolicyWithBoundary(result.data.dependencyPolicy, await deriveFrontendCapabilityBoundaryFromRun(input.runDir));
2489
2797
  return {
2490
2798
  canonical: result.data,
2491
2799
  candidateRawSha256,
@@ -2506,11 +2814,12 @@ function splitFrontendContractReviewGroup(canonical, fields) {
2506
2814
  slices = [];
2507
2815
  };
2508
2816
  for (const field of fields) {
2509
- if (!Object.hasOwn(canonical, field))
2510
- continue;
2511
2817
  const fieldValue = canonical[field];
2512
- // JSON omits undefined object properties; the index must describe the
2513
- // persisted contract, not phantom keys retained by normalization.
2818
+ // Normalization keeps optional fields as present keys with an undefined
2819
+ // value; JSON serialization drops them, so a slice emitted here would
2820
+ // bind the section to a field the on-disk canonical never contains and
2821
+ // every reader (which re-parses the canonical from JSON) would fail to
2822
+ // resolve it.
2514
2823
  if (fieldValue === undefined)
2515
2824
  continue;
2516
2825
  if (!Array.isArray(fieldValue)) {
@@ -2881,6 +3190,27 @@ function assertVerificationSymbolShapes(contract) {
2881
3190
  * (finalize_plan receipt) can synthesize fix suggestions instead of making
2882
3191
  * the model re-derive them from prose.
2883
3192
  */
3193
+ /** Normalized-content sha256 of a parent run's canonical contract, used by the
3194
+ * recovery identical-plan guard. Both sides hash serializeDeterministicJson of
3195
+ * the parsed canonical, so on-disk formatting never changes the verdict.
3196
+ * Unreadable/missing parent contract → undefined (guard skipped). */
3197
+ export async function readParentCanonical(parentRunDir) {
3198
+ try {
3199
+ const raw = await readFile(path.join(parentRunDir, "contracts", "frontend-implementation-contract.json"), "utf8");
3200
+ return JSON.parse(raw);
3201
+ }
3202
+ catch {
3203
+ return undefined;
3204
+ }
3205
+ }
3206
+ export async function parentCanonicalContentSha256(parentRunDir) {
3207
+ const canonical = await readParentCanonical(parentRunDir);
3208
+ if (!canonical)
3209
+ return undefined;
3210
+ return createHash("sha256")
3211
+ .update(serializeDeterministicJson(canonical))
3212
+ .digest("hex");
3213
+ }
2884
3214
  export class PlanPolicyPrecheckFailure extends Error {
2885
3215
  findings;
2886
3216
  constructor(message, findings) {
@@ -2924,7 +3254,34 @@ export async function analyzeFrontendPlanPatchCandidate(input) {
2924
3254
  ...checkUxNameCollisions(analysis.canonical),
2925
3255
  ...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
2926
3256
  ...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
3257
+ ...checkComponentChoiceSourceReferences(analysis.canonical, await deriveFrontendSourceBindingPathsFromRun(input.runDir)),
3258
+ ...checkRequirementOwnership(analysis.canonical, await deriveRequirementOwnershipFromRun(input.runDir)),
3259
+ ...checkInteractionIdVocabulary(analysis.canonical, await deriveFrontendInteractionIdsFromRun(input.runDir)),
3260
+ ...checkMockApiStrategyAllowed(analysis.canonical, await deriveFrontendAllowedMockStrategiesFromRun(input.runDir)),
2927
3261
  ];
3262
+ // Front-load the static verification coverage gap (smoke r16-r20): when
3263
+ // the frozen command directory offers static entrypoints and no
3264
+ // verification target binds one, type-level/build constraints cannot be
3265
+ // proven. Surface the offered commands here so the plan retry ladder
3266
+ // records a static focus instead of the review flagging it post-hoc.
3267
+ // Every frozen static entry is a required check (smoke r27-r31): binding
3268
+ // only `verify-build` used to satisfy an "at least one" rule, after which
3269
+ // design review rejected the plan for the uncovered `verify-typecheck`
3270
+ // entry — a demand that reached the plan as prose and could never be
3271
+ // machine-checked. Coverage is enumerable, so it is enforced here.
3272
+ const verifyDirectory = await deriveFrontendVerifyCommandDirectoryFromRun(input.runDir);
3273
+ const staticCommands = verifyDirectory.filter((entry) => entry.mode === "static");
3274
+ if (staticCommands.length > 0) {
3275
+ const boundCommandIds = new Set(analysis.canonical.verificationTargets.map((target) => target.commandId));
3276
+ const uncovered = staticCommands.filter((entry) => !boundCommandIds.has(entry.commandId));
3277
+ if (uncovered.length > 0) {
3278
+ policyPreFindings.push({
3279
+ code: "static-verification-coverage-missing",
3280
+ message: `verification targets miss frozen static command(s): [${uncovered.map((entry) => `${entry.commandId} (${entry.label})`).join(", ")}]; the frozen directory offers: [${staticCommands.map((entry) => `${entry.commandId} (${entry.label})`).join(", ")}] — record record_verification_focus for every static commandId`,
3281
+ path: "verificationTargets",
3282
+ });
3283
+ }
3284
+ }
2928
3285
  if (policyPreFindings.length > 0) {
2929
3286
  const message = `frontend plan policy pre-check failed (fix these plan facts, then re-commit and finalize): ${policyPreFindings
2930
3287
  .map((finding) => `${finding.code}: ${finding.message}`)
@@ -0,0 +1,53 @@
1
+ import { readFrontendPlanCanaryRunMetrics, } from "./frontend-session-budget.js";
2
+ export const FRONTEND_PLAN_CANARY_SCENARIOS = [
3
+ "missing-idle-loading-empty",
4
+ "loading-not-mounted",
5
+ "test-layer-does-not-prove-wiring",
6
+ "mock-claimed-as-real-integration",
7
+ ];
8
+ const reduction = (before, after) => before !== null && after !== null && before > 0 ? ((before - after) / before) * 100 : null;
9
+ /** Compare two same-input runs. Missing observations fail the canary rather
10
+ * than being interpreted as a zero-cost candidate. */
11
+ export function evaluateFrontendPlanCanary(input) {
12
+ const required = { planSessionsPct: 30, planWallClockPct: 30, protocolRetryPct: 50, ...input.requiredReductions };
13
+ const reductions = {
14
+ planSessionsPct: reduction(input.baseline.planSessions, input.candidate.planSessions),
15
+ planWallClockPct: reduction(input.baseline.planWallClockMs, input.candidate.planWallClockMs),
16
+ protocolRetryPct: reduction(input.baseline.retries, input.candidate.retries),
17
+ };
18
+ const failedChecks = [];
19
+ if (reductions.planSessionsPct === null || reductions.planSessionsPct < required.planSessionsPct)
20
+ failedChecks.push("plan-session-reduction");
21
+ if (reductions.planWallClockPct === null || reductions.planWallClockPct < required.planWallClockPct)
22
+ failedChecks.push("plan-wall-clock-reduction");
23
+ // A zero retry baseline is already optimal: a zero->zero candidate is not a
24
+ // regression and must not block, while zero->positive or missing evidence
25
+ // still fails closed.
26
+ const retriesAlreadyOptimal = input.baseline.retries === 0 && input.candidate.retries === 0;
27
+ if (!retriesAlreadyOptimal &&
28
+ (reductions.protocolRetryPct === null || reductions.protocolRetryPct < required.protocolRetryPct)) {
29
+ failedChecks.push("protocol-retry-reduction");
30
+ }
31
+ for (const scenario of FRONTEND_PLAN_CANARY_SCENARIOS) {
32
+ const result = input.scenarios[scenario];
33
+ if (result !== "blocked" && result !== "reported")
34
+ failedChecks.push(`regression:${scenario}`);
35
+ }
36
+ return { pass: failedChecks.length === 0, reductions, failedChecks };
37
+ }
38
+ /** Read two frozen run directories and evaluate the same-input canary. Plan
39
+ * cost metrics come from persisted session receipts; supplemental DAG/review/
40
+ * recovery values come from each run's `frontend-plan-canary-metrics.json`.
41
+ * Missing observations surface as `null` and fail the evaluation, never as a
42
+ * manufactured zero-cost improvement. */
43
+ export async function compareFrontendPlanCanaryRuns(input) {
44
+ const baseline = await readFrontendPlanCanaryRunMetrics(input.baselineRunDir);
45
+ const candidate = await readFrontendPlanCanaryRunMetrics(input.candidateRunDir);
46
+ const evaluation = evaluateFrontendPlanCanary({
47
+ baseline,
48
+ candidate,
49
+ scenarios: input.scenarios,
50
+ requiredReductions: input.requiredReductions,
51
+ });
52
+ return { ...evaluation, baseline, candidate };
53
+ }