@tea-agent/loop-agent 0.39.0-next.8 → 0.39.0

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 (254) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +264 -67
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/dag/generate-task-dag.js +9 -0
  6. package/dist/application/task-lifecycle/advance.js +4 -0
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +16 -5
  9. package/dist/cli/program.js +16 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli.js +2 -1
  12. package/dist/commands/dag-rerun.js +55 -1
  13. package/dist/commands/init-model-catalog.js +464 -0
  14. package/dist/commands/init-upgrade.js +265 -97
  15. package/dist/commands/init.js +428 -27
  16. package/dist/commands/inspect-next.js +8 -0
  17. package/dist/commands/task-advance.js +31 -0
  18. package/dist/executors/dag-pi-executor.js +124 -18
  19. package/dist/executors/pi-event-serializer.js +42 -1
  20. package/dist/executors/pi-executor.js +51 -0
  21. package/dist/executors/pi-extension-resolver.js +233 -0
  22. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  23. package/dist/executors/pi-sdk-executor.js +187 -59
  24. package/dist/executors/shell-executor.js +554 -33
  25. package/dist/executors/shell-write-guard.js +7 -0
  26. package/dist/shared/dag-failure-category.js +138 -0
  27. package/dist/shared/dag-prompt-override.js +28 -0
  28. package/dist/shared/operator/capabilities.js +181 -27
  29. package/dist/shared/playwright-cli-command-policy.js +15 -0
  30. package/dist/shared/update/console-notifier.js +100 -0
  31. package/dist/{cli → shared}/update/npm-client.js +41 -8
  32. package/dist/{cli → shared}/update/state.js +41 -13
  33. package/dist/task/config-types.js +28 -0
  34. package/dist/task/contract/constants.js +1 -0
  35. package/dist/task/contract/diff.js +26 -0
  36. package/dist/task/contract/project.js +5 -0
  37. package/dist/task/contract/schema.js +6 -1
  38. package/dist/task/source-prepare/build-draft.js +15 -0
  39. package/dist/task/source-prepare/parse-intent.js +24 -4
  40. package/dist/worker/cli.js +31 -1
  41. package/dist/worker/console/chat/deferred-turn.js +91 -0
  42. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  43. package/dist/worker/console/chat/operation-card.js +8 -1
  44. package/dist/worker/console/chat/pi-console-config.js +81 -10
  45. package/dist/worker/console/chat/pi-runtime.js +165 -83
  46. package/dist/worker/console/chat/repo-walk.js +10 -1
  47. package/dist/worker/console/chat/routes.js +480 -55
  48. package/dist/worker/console/chat/semantic-activity.js +20 -8
  49. package/dist/worker/console/chat/session-store.js +146 -2
  50. package/dist/worker/console/chat/turn-process.js +10 -10
  51. package/dist/worker/console/dag-execution-receipt.js +33 -0
  52. package/dist/worker/console/inspect-split.js +18 -0
  53. package/dist/worker/console/operation-run-facts.js +19 -3
  54. package/dist/worker/console/operator-actions.js +195 -4
  55. package/dist/worker/console/operator-user-error.js +2 -2
  56. package/dist/worker/console/pi-readiness.js +5 -4
  57. package/dist/worker/console/recovery-cta.js +4 -4
  58. package/dist/worker/console/recovery-error-copy.js +4 -4
  59. package/dist/worker/console/routes.js +43 -2
  60. package/dist/worker/console/security.js +63 -9
  61. package/dist/worker/console/server.js +42 -6
  62. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-Br92EGb7.js} +1 -1
  63. package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-oulImtQq.js} +1 -1
  64. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-tyHc6aqO.js} +1 -1
  65. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-CY86BSpT.js} +1 -1
  66. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-BKKsDmNs.js} +1 -1
  67. package/dist/worker/console/static/assets/channel-TY36zt-i.js +1 -0
  68. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-CXZdjl3B.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-B-aIFDQ5.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-BDgAW_B2.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-DABa4UPR.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-aGbysVOx.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-B8MSZDVs.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-CpelV73g.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-BE04ty3z.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-BUxTkaaR.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-CYYBVKfX.js} +1 -1
  78. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +1 -0
  79. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +1 -0
  80. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-CEQF_xmQ.js} +1 -1
  81. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-CqpxN5IT.js} +1 -1
  82. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-B3PzanVi.js} +1 -1
  83. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-BSUGCNx1.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-CdLevU5T.js} +1 -1
  85. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BjpF_Kok.js} +1 -1
  86. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C4I6NJny.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-C8hecCr5.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-Daod6tOS.js} +1 -1
  89. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-BvvmazGv.js} +1 -1
  90. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-DeN_vfV4.js} +1 -1
  91. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-D6dq1t9z.js} +1 -1
  92. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-DziIdNY4.js} +1 -1
  93. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js} +1 -1
  94. package/dist/worker/console/static/assets/index-C3GgHg-g.css +1 -0
  95. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +406 -0
  96. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-Czv5eYx2.js} +1 -1
  97. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js} +1 -1
  98. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-DxGmNj5K.js} +1 -1
  99. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-CYCFqkO_.js} +1 -1
  100. package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-B0YzRwrd.js} +1 -1
  101. package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-t3EZtIGh.js} +5 -5
  102. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-BTXpTv0T.js} +1 -1
  103. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-DU8UkKqA.js} +1 -1
  104. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-DP4y8ohp.js} +1 -1
  105. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-B07vNaYR.js} +1 -1
  106. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-xDWktb2i.js} +1 -1
  107. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-BLNMKqeS.js} +1 -1
  108. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-n0adalxH.js} +1 -1
  109. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-BbnS-2DL.js} +1 -1
  110. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-B4uPpCLu.js} +1 -1
  111. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA--MMbB0fX.js} +1 -1
  112. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +1 -0
  113. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-BXUgp2QY.js} +2 -2
  114. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +8 -0
  115. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-CsaMxki7.js} +1 -1
  116. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-C2IuG9iH.js} +1 -1
  117. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-CmEWOXCW.js} +1 -1
  118. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-D_9LE0MG.js} +1 -1
  119. package/dist/worker/console/static/index.html +2 -2
  120. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  121. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  122. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  123. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  124. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  125. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +74 -26
  126. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  127. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  128. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  129. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  130. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  131. package/dist/worker/console/static-src/operator-chat/turn-submission.js +54 -0
  132. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  133. package/dist/worker/console/static-src/operator-chat/useChatStream.js +237 -11
  134. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  135. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  136. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  137. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  138. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  139. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  140. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  141. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  142. package/dist/worker/observability/read-model.js +22 -0
  143. package/dist/worker/observe/node-input.js +74 -3
  144. package/dist/worker/observe/node-process.js +377 -0
  145. package/dist/worker/observe/routes.js +36 -3
  146. package/dist/worker/observe/shell-handler-keys.js +34 -0
  147. package/dist/worker/observe/static/api.js +66 -0
  148. package/dist/worker/observe/static/constants.js +8 -1
  149. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  150. package/dist/worker/observe/static/format.js +23 -0
  151. package/dist/worker/observe/static/markdown-render.js +20 -1
  152. package/dist/worker/observe/static/operator-chrome.js +4 -0
  153. package/dist/worker/observe/static/state.js +13 -2
  154. package/dist/worker/observe/static/styles.css +73 -0
  155. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  156. package/dist/worker/observe/static/views/dag.js +8 -1
  157. package/dist/worker/observe/static/views/failures.js +5 -2
  158. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  159. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
  160. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  161. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  162. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  163. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  164. package/dist/workflows/dag/backend-test-layout.js +133 -0
  165. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  166. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  167. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  168. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  169. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  170. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  171. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  172. package/dist/workflows/dag/contract-output-registry.js +15 -0
  173. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  174. package/dist/workflows/dag/convergence/controller.js +141 -0
  175. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  176. package/dist/workflows/dag/dynamic-runtime/map.js +25 -9
  177. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  178. package/dist/workflows/dag/failure-category.js +7 -116
  179. package/dist/workflows/dag/frontend-implementation-contract.js +377 -4
  180. package/dist/workflows/dag/frontend-plan-render.js +1 -1
  181. package/dist/workflows/dag/frontend-prewrite-gate.js +96 -84
  182. package/dist/workflows/dag/frontend-repair.js +14 -0
  183. package/dist/workflows/dag/frontend-risk.js +15 -2
  184. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  185. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  186. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  187. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  188. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  189. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  190. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  191. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  192. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  193. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  194. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  195. package/dist/workflows/dag/init-hybrid.js +894 -205
  196. package/dist/workflows/dag/node-execution.js +183 -15
  197. package/dist/workflows/dag/prompt.js +60 -1
  198. package/dist/workflows/dag/rerun-feedback.js +212 -0
  199. package/dist/workflows/dag/rerun-plan.js +224 -27
  200. package/dist/workflows/dag/rerun-run.js +61 -6
  201. package/dist/workflows/dag/rerun-task.js +61 -2
  202. package/dist/workflows/dag/retry-policy.js +109 -0
  203. package/dist/workflows/dag/structured-output-repair.js +712 -0
  204. package/dist/workflows/dag/types.js +143 -1
  205. package/dist/workflows/dag/validate.js +64 -20
  206. package/docs/init-surface.manifest.json +5 -0
  207. package/docs/operations/README.md +1 -1
  208. package/docs/operations/local-development-environment.md +1 -5
  209. package/docs/skills/vetted-skill-registry.md +14 -0
  210. package/docs/templates/README.md +1 -1
  211. package/docs/templates/agent-dag.schema.json +44 -0
  212. package/docs/templates/backend-test-dag.json +60 -35
  213. package/docs/templates/frontend-implementation-contract.schema.json +1 -1
  214. package/docs/templates/frontend-test-case-checklist.md +2 -2
  215. package/docs/templates/frontend-test-dag.json +8 -10
  216. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  217. package/docs/templates/init-managed-agents.md +13 -3
  218. package/harness.json +6 -4
  219. package/package.json +12 -4
  220. package/scripts/next-info.mjs +356 -0
  221. package/scripts/next-publish-gate.mjs +238 -0
  222. package/scripts/release-source-binding.mjs +251 -0
  223. package/skills/analyze-product-requirements/SKILL.md +8 -5
  224. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  225. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  226. package/skills/analyze-product-requirements/references/example.md +50 -0
  227. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  228. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  229. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  230. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  231. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  232. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  233. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  234. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  235. package/skills/codebase-scout/SKILL.md +1 -1
  236. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  237. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  238. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  239. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  240. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  241. package/skills/improve-codebase-architecture/deepening.md +37 -0
  242. package/skills/improve-codebase-architecture/html-report.md +123 -0
  243. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  244. package/skills/improve-codebase-architecture/language.md +53 -0
  245. package/skills/loop-agent/references/command-reference.md +11 -4
  246. package/skills/playwright-cli/SKILL.md +23 -1
  247. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  248. package/dist/worker/console/static/assets/channel-BBOTJHvD.js +0 -1
  249. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cm8Yf8tD.js +0 -1
  250. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cm8Yf8tD.js +0 -1
  251. package/dist/worker/console/static/assets/index-B-qWGThd.js +0 -325
  252. package/dist/worker/console/static/assets/index-C-xRgY_0.css +0 -1
  253. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DZihfdKc.js +0 -1
  254. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CKnXCjHX.js +0 -8
@@ -5,7 +5,28 @@ import { z } from "zod";
5
5
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
6
6
  import { readPlaywrightCliReceipts, summarizePlaywrightCliReceipts, } from "../../executors/pi-playwright-cli-tool.js";
7
7
  import { validateFrontendCaseContent } from "./frontend-test-case-quality.js";
8
+ import { markdownH1Title, markdownList, markdownSection, } from "./frontend-test-markdown.js";
9
+ import { defaultFrontendTestLayout, frontendTestEvidencePrefix, frontendTestManifestPath, } from "./frontend-test-layout.js";
8
10
  export const FRONTEND_TEST_RESULT_SCHEMA_ID = "frontend-test-result-v1";
11
+ /**
12
+ * Truncated acceptance-id shapes observed in generator output: a bare
13
+ * numeric segment ("AC-002" dropped its AC-FE-<FEATURE> prefix) or an
14
+ * AC-FE-<FEATURE> literal without the trailing -NNN sequence number
15
+ * ("AC-FE-CHAT"). Generic short ids like "AC-1"/"AC-FE-001" stay valid;
16
+ * declared-source cross-checks (unknown-ac) remain the primary guard when a
17
+ * sourceBinding exists.
18
+ */
19
+ export function isTruncatedFrontendAcId(acId) {
20
+ return /^AC-FE-[A-Z]+$/i.test(acId.trim());
21
+ }
22
+ /**
23
+ * Bare numeric ids ("AC-002") only indicate truncation when no declared AC
24
+ * list exists to adjudicate them: with a sourceBinding they are ordinary
25
+ * unknown-ac findings ("AC-999" may be a legitimately declared id style).
26
+ */
27
+ export function isTruncatedWhenUndeclaredFrontendAcId(acId) {
28
+ return /^AC-\d{3}$/i.test(acId.trim()) || isTruncatedFrontendAcId(acId);
29
+ }
9
30
  const safeRelativePathSchema = z.string().min(1).refine((value) => !path.posix.isAbsolute(value) &&
10
31
  !path.win32.isAbsolute(value) &&
11
32
  !value.includes("\\") &&
@@ -56,6 +77,8 @@ export const frontendTestResultContractSchema = z.object({
56
77
  passed: z.number().int().min(0),
57
78
  failed: z.number().int().min(0),
58
79
  blocked: z.number().int().min(0),
80
+ /** Stability metric: passed on attempt 0 (no rerun/self-heal retry). */
81
+ firstTryPassed: z.number().int().min(0).optional(),
59
82
  }).strict(),
60
83
  acceptanceCoverage: z.object({
61
84
  covered: z.array(z.string().min(1)),
@@ -102,14 +125,22 @@ function isSafeEvidenceShellPath(value) {
102
125
  * backslash interpretation for the former long `node -e` command.
103
126
  */
104
127
  export async function validateFrontendCaseEvidence(input) {
105
- const manifestPath = path.join(input.workspaceRoot, "testcase/frontend/cases/manifest.json");
128
+ const layout = input.layout ?? defaultFrontendTestLayout();
129
+ const manifestRel = frontendTestManifestPath(layout);
130
+ const manifestPath = path.join(input.workspaceRoot, manifestRel);
106
131
  try {
107
132
  await stat(manifestPath);
108
133
  }
109
134
  catch {
110
- throw new Error("missing testcase/frontend/cases/manifest.json");
135
+ throw new Error(`missing ${manifestRel}`);
136
+ }
137
+ let manifest;
138
+ try {
139
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
140
+ }
141
+ catch {
142
+ throw new Error("invalid frontend case manifest: malformed JSON");
111
143
  }
112
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
113
144
  if (!Array.isArray(manifest.cases))
114
145
  throw new Error("invalid frontend case manifest");
115
146
  const statuses = new Set(["passed", "failed", "blocked"]);
@@ -119,7 +150,7 @@ export async function validateFrontendCaseEvidence(input) {
119
150
  const item = rawCase;
120
151
  const id = typeof item?.caseId === "string" ? item.caseId : "?";
121
152
  const dir = item?.evidenceDir;
122
- const prefix = `testcase/frontend/evidence/${id}`;
153
+ const prefix = frontendTestEvidencePrefix(layout, id);
123
154
  if (!item ||
124
155
  typeof item.caseId !== "string" ||
125
156
  typeof dir !== "string" ||
@@ -163,12 +194,15 @@ export async function validateFrontendCaseEvidence(input) {
163
194
  continue;
164
195
  }
165
196
  const currentPrefix = `${prefix}/`;
166
- if (evidencePath.startsWith("testcase/frontend/evidence/") && !evidencePath.startsWith(currentPrefix)) {
197
+ if ((evidencePath.startsWith(`${layout.evidenceDir}/`) ||
198
+ evidencePath.startsWith("testcase/frontend/evidence/")) &&
199
+ !evidencePath.startsWith(currentPrefix)) {
167
200
  hardFail = true;
168
201
  issues.push({ ruleId: "cross-case-evidence-path", caseId: id, detail: evidencePath });
169
202
  continue;
170
203
  }
171
- const target = evidencePath.startsWith("testcase/")
204
+ const target = evidencePath.startsWith(`${layout.testRoot}/`) ||
205
+ evidencePath.startsWith("testcase/")
172
206
  ? path.join(input.workspaceRoot, evidencePath)
173
207
  : path.join(input.workspaceRoot, dir, evidencePath);
174
208
  if (!(await isNonEmptyFile(target))) {
@@ -194,11 +228,17 @@ export function evaluatePassedCaseBrowserReceipts(summary, caseId) {
194
228
  }
195
229
  return { ok: true };
196
230
  }
197
- async function loadCaseBrowserReceiptSummary(input) {
231
+ async function loadCaseBrowserReceiptsWithSummary(input) {
198
232
  try {
199
233
  const entries = await readdir(input.runDir, { withFileTypes: true });
200
234
  // A passed chain must belong to exactly one browser child stream. Never
201
- // stitch receipts across DAG nodes or accept ambiguous duplicate authority.
235
+ // stitch receipts across DAG nodes or accept ambiguous duplicate
236
+ // authority — with one exception: bounded rerun creates a second,
237
+ // later stream for the same case (the rerun overwrites the disk
238
+ // case-result, so the final attempt is the authoritative one). When
239
+ // multiple streams authorize, prefer the highest rerun round
240
+ // (rerun-frontend-case-rN-<idx> directories); ties within the same
241
+ // round remain ambiguous and fail closed.
202
242
  const authorizingStreams = [];
203
243
  for (const entry of entries) {
204
244
  if (!entry.isDirectory())
@@ -206,48 +246,123 @@ async function loadCaseBrowserReceiptSummary(input) {
206
246
  const nodeReceipts = (await readPlaywrightCliReceipts(input.runDir, entry.name))
207
247
  .filter((item) => item.caseId === input.caseId);
208
248
  const summary = summarizePlaywrightCliReceipts(nodeReceipts);
209
- if (summary.hasOrderedPassedReceiptChain)
210
- authorizingStreams.push(summary);
249
+ if (summary.hasOrderedPassedReceiptChain) {
250
+ const roundMatch = /rerun-frontend-case-r(\d+)-/i.exec(entry.name);
251
+ authorizingStreams.push({
252
+ receipts: nodeReceipts,
253
+ summary,
254
+ round: roundMatch ? Number(roundMatch[1]) : 0,
255
+ });
256
+ }
257
+ }
258
+ if (authorizingStreams.length === 0) {
259
+ return { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
211
260
  }
212
- return authorizingStreams.length === 1
213
- ? authorizingStreams[0]
214
- : summarizePlaywrightCliReceipts([]);
261
+ const maxRound = Math.max(...authorizingStreams.map((s) => s.round));
262
+ const finalists = authorizingStreams.filter((s) => s.round === maxRound);
263
+ return finalists.length === 1
264
+ ? finalists[0]
265
+ : { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
215
266
  }
216
267
  catch {
217
- return summarizePlaywrightCliReceipts([]);
268
+ return { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
218
269
  }
219
270
  }
220
- function sha256(content) {
221
- return createHash("sha256").update(content).digest("hex");
271
+ /**
272
+ * Extract `find "literal" >=N` / `find "literal"` declarations from a case
273
+ * document. Only quoted literals are binding assertions — the count suffix
274
+ * (`>=N`, `==N`) is the expected minimum/exact match count from the ledger.
275
+ * Placeholder literals ("...", "…", "<字面>", "eX") appear in prose discussing
276
+ * the syntax itself; they are documentation, never declarations.
277
+ */
278
+ export function extractDeclaredFindAssertions(caseMarkdown) {
279
+ const source = stripFencedCodeBlocks(caseMarkdown);
280
+ const out = [];
281
+ const seen = new Set();
282
+ const pattern = /find\s+"((?:[^"\\]|\\.)+)"\s*(>=|==)\s*(\d+)/g;
283
+ const isPlaceholder = (literal) => /^(?:\.{2,}|…+|<[^>]*>|e\d+)$/.test(literal.trim());
284
+ let match;
285
+ while ((match = pattern.exec(source)) !== null) {
286
+ const literal = match[1].replace(/\\(.)/g, "$1");
287
+ if (isPlaceholder(literal))
288
+ continue;
289
+ const operator = match[2];
290
+ const expected = Number(match[3]);
291
+ const key = `${operator}:${literal}`;
292
+ if (seen.has(key))
293
+ continue;
294
+ seen.add(key);
295
+ out.push({ literal, expected, operator });
296
+ }
297
+ return out;
222
298
  }
223
- function markdownSection(body, names) {
224
- const escaped = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
225
- const marker = new RegExp(`^###\\s+(?:${escaped})\\s*$`, "mi");
226
- const hit = marker.exec(body);
227
- if (!hit)
299
+ function stripFencedCodeBlocks(markdown) {
300
+ return markdown.replace(/```[\s\S]*?```/g, "");
301
+ }
302
+ async function readCaseMarkdownForAudit(workspaceRoot, casePath) {
303
+ try {
304
+ return await readFile(path.join(workspaceRoot, casePath), "utf8");
305
+ }
306
+ catch {
228
307
  return "";
229
- const rest = body.slice(hit.index + hit[0].length);
230
- const next = /^###\s+/m.exec(rest);
231
- return rest.slice(0, next?.index ?? rest.length).trim();
308
+ }
309
+ }
310
+ /**
311
+ * Controller-side assertion audit: every find-family declaration in the case
312
+ * document must be backed by a successful find receipt whose parsed match
313
+ * count satisfies the declared operator. This moves pass authority from
314
+ * "the model says it asserted" to "receipts prove it asserted".
315
+ */
316
+ export function auditDeclaredFindAssertions(declared, receipts) {
317
+ if (declared.length === 0)
318
+ return { ok: true };
319
+ const successfulFinds = receipts.filter((receipt) => (receipt.command === "find" || receipt.command === "find-unique") &&
320
+ receipt.exitCode === 0 &&
321
+ !receipt.timedOut);
322
+ const missing = [];
323
+ for (const decl of declared) {
324
+ const backing = successfulFinds.some((receipt) => receipt.argsRedacted.some((arg) => arg === decl.literal || arg === `"${decl.literal}"`) &&
325
+ typeof receipt.matchCount === "number" &&
326
+ (decl.operator === ">="
327
+ ? receipt.matchCount >= decl.expected
328
+ : receipt.matchCount === decl.expected));
329
+ if (!backing) {
330
+ missing.push(`${decl.operator}${decl.expected} "${decl.literal.slice(0, 40)}"`);
331
+ }
332
+ }
333
+ if (missing.length > 0) {
334
+ return {
335
+ ok: false,
336
+ reason: "assertion-not-receipt-backed",
337
+ missing,
338
+ };
339
+ }
340
+ return { ok: true };
232
341
  }
233
- function markdownList(value) {
234
- const items = value.split(/\r?\n/).map((line) => line.replace(/^\s*(?:\d+[.)]|[-*+])\s+/, "").trim()).filter(Boolean);
235
- return items;
342
+ function sha256(content) {
343
+ return createHash("sha256").update(content).digest("hex");
236
344
  }
237
345
  /**
238
346
  * Read planned case facts from the case Markdown. Recognizes both English and
239
- * Chinese headings, including `测试点` (test points) and `测试步骤` (planned
240
- * test steps), alongside the legacy `操作步骤`/`Steps` headings.
347
+ * Chinese headings at `##` or `###`, including `测试点` (test points) and
348
+ * `测试步骤` (planned test steps), alongside the legacy `操作步骤`/`Steps`
349
+ * headings. Numbered and bulleted lists are extracted; a missing 测试目的
350
+ * falls back to the case H1 title.
241
351
  */
242
- async function readFrontendCaseContent(workspaceRoot, casePath) {
243
- if (!safeRelativePathSchema.safeParse(casePath).success || !casePath.startsWith("testcase/frontend/cases/")) {
352
+ async function readFrontendCaseContent(workspaceRoot, casePath, layout) {
353
+ if (!safeRelativePathSchema.safeParse(casePath).success ||
354
+ !(casePath.startsWith(`${layout.casesDir}/`) ||
355
+ casePath.startsWith("testcase/frontend/cases/"))) {
244
356
  throw new Error(`unsafe frontend case path: ${casePath}`);
245
357
  }
246
358
  const body = await readFile(path.resolve(workspaceRoot, casePath), "utf8");
359
+ const purpose = markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) ||
360
+ markdownH1Title(body).replace(/^FE-[^\s::]+[::]\s*/, "").trim() ||
361
+ "未提供测试目的";
247
362
  return {
248
363
  caseContent: {
249
- purpose: markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) || "未提供测试目的",
250
- preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件"])),
364
+ purpose,
365
+ preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件", "前置条件与重置"])),
251
366
  steps: markdownList(markdownSection(body, ["Test Steps", "测试步骤", "Steps", "操作步骤"])),
252
367
  expectedResults: markdownList(markdownSection(body, ["Expected Results", "预期结果"])),
253
368
  },
@@ -290,7 +405,7 @@ function assertInside(root, candidate, label) {
290
405
  throw new Error(`${label} escapes its allowed root`);
291
406
  }
292
407
  }
293
- async function readEvidenceFile(repoRoot, evidenceRoot, relative) {
408
+ async function readEvidenceFile(repoRoot, evidenceRoot, relative, layout) {
294
409
  if (!safeRelativePathSchema.safeParse(relative).success) {
295
410
  throw new Error(`unsafe evidence path: ${relative}`);
296
411
  }
@@ -302,12 +417,16 @@ async function readEvidenceFile(repoRoot, evidenceRoot, relative) {
302
417
  const normalized = relative;
303
418
  const evidenceRootRel = path.relative(repoRootResolved, evidenceRootResolved).replaceAll("\\", "/");
304
419
  const isCurrentCaseRepoPath = normalized === evidenceRootRel || normalized.startsWith(`${evidenceRootRel}/`);
305
- const isRepoRelative = normalized.startsWith("testcase/") && !isCurrentCaseRepoPath;
420
+ const isRepoRelative = (normalized.startsWith(`${layout.testRoot}/`) ||
421
+ normalized.startsWith("testcase/")) &&
422
+ !isCurrentCaseRepoPath;
306
423
  const absolute = isRepoRelative
307
424
  ? path.resolve(repoRootResolved, normalized)
308
425
  : path.resolve(evidenceRootResolved, normalized);
309
426
  assertInside(repoRootResolved, absolute, "evidence path");
310
- if (isRepoRelative && normalized.startsWith("testcase/frontend/evidence/")) {
427
+ if (isRepoRelative &&
428
+ (normalized.startsWith(`${layout.evidenceDir}/`) ||
429
+ normalized.startsWith("testcase/frontend/evidence/"))) {
311
430
  throw new Error(`evidence path escapes evidence root: ${relative}`);
312
431
  }
313
432
  if (!isRepoRelative)
@@ -330,9 +449,16 @@ export async function materializeFrontendTestResult(input) {
330
449
  if (!sourceBinding?.taskId || !sourceBinding.sources?.length) {
331
450
  throw new Error("frontend-test result requires frozen sourceBinding");
332
451
  }
333
- const manifestPath = "testcase/frontend/cases/manifest.json";
452
+ const layout = input.layout ?? defaultFrontendTestLayout();
453
+ const manifestPath = frontendTestManifestPath(layout);
334
454
  const manifestAbsolute = path.resolve(input.workspaceRoot, manifestPath);
335
- const manifest = JSON.parse(await readFile(manifestAbsolute, "utf-8"));
455
+ let manifest;
456
+ try {
457
+ manifest = JSON.parse(await readFile(manifestAbsolute, "utf-8"));
458
+ }
459
+ catch {
460
+ throw new Error("invalid or empty frontend case manifest: malformed JSON");
461
+ }
336
462
  if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.cases) || manifest.cases.length === 0) {
337
463
  throw new Error("invalid or empty frontend case manifest");
338
464
  }
@@ -340,6 +466,7 @@ export async function materializeFrontendTestResult(input) {
340
466
  const contentQuality = await validateFrontendCaseContent({
341
467
  workspaceRoot: input.workspaceRoot,
342
468
  requiredAcIds: sourceBinding.requirementIds ?? [],
469
+ layout,
343
470
  });
344
471
  advisoryFindings.push(...contentQuality.findings);
345
472
  const requiredIds = new Set(sourceBinding.requirementIds ?? []);
@@ -351,7 +478,7 @@ export async function materializeFrontendTestResult(input) {
351
478
  advisoryFindings.push({ ruleId: "invalid-manifest-case", detail: "manifest case is missing caseId, acIds, or evidenceDir" });
352
479
  continue;
353
480
  }
354
- const evidencePrefix = `testcase/frontend/evidence/${item.caseId}`;
481
+ const evidencePrefix = frontendTestEvidencePrefix(layout, item.caseId);
355
482
  if (!(item.evidenceDir === evidencePrefix || item.evidenceDir.startsWith(`${evidencePrefix}/`))) {
356
483
  throw new Error(`unsafe evidenceDir for ${item.caseId}`);
357
484
  }
@@ -382,7 +509,7 @@ export async function materializeFrontendTestResult(input) {
382
509
  if (typeof evidencePath !== "string" || evidencePath === "execution.md" && evidence.some((entry) => entry.path.endsWith("/execution.md")))
383
510
  continue;
384
511
  try {
385
- evidence.push(await readEvidenceFile(input.workspaceRoot, evidenceRoot, evidencePath));
512
+ evidence.push(await readEvidenceFile(input.workspaceRoot, evidenceRoot, evidencePath, layout));
386
513
  }
387
514
  catch (error) {
388
515
  const message = error instanceof Error ? error.message : String(error);
@@ -395,7 +522,7 @@ export async function materializeFrontendTestResult(input) {
395
522
  advisoryFindings.push({ ruleId: "passed-without-browser-evidence", caseId: item.caseId, detail: "passed case has no browser evidence" });
396
523
  let blockedReason = status === "blocked" && typeof resultRaw.blockedReason === "string" && resultRaw.blockedReason.trim() ? resultRaw.blockedReason.trim() : undefined;
397
524
  if (status === "passed") {
398
- const receiptSummary = await loadCaseBrowserReceiptSummary({
525
+ const { receipts: caseReceipts, summary: receiptSummary } = await loadCaseBrowserReceiptsWithSummary({
399
526
  runDir: input.runDir,
400
527
  caseId: item.caseId,
401
528
  });
@@ -409,8 +536,26 @@ export async function materializeFrontendTestResult(input) {
409
536
  detail: "passed case lacks controller-owned open + interaction/assertion + cleanup receipts",
410
537
  });
411
538
  }
539
+ else {
540
+ // Controller-side assertion audit: every `find "literal" >=N` declared
541
+ // in the case document must be backed by a successful find receipt
542
+ // whose parsed matchCount satisfies the operator. Pass authority moves
543
+ // from model prose to receipt evidence.
544
+ const caseMarkdown = await readCaseMarkdownForAudit(input.workspaceRoot, item.casePath);
545
+ const declared = extractDeclaredFindAssertions(caseMarkdown);
546
+ const audit = auditDeclaredFindAssertions(declared, caseReceipts);
547
+ if (!audit.ok) {
548
+ status = "blocked";
549
+ blockedReason = audit.reason;
550
+ advisoryFindings.push({
551
+ ruleId: "assertion-not-receipt-backed",
552
+ caseId: item.caseId,
553
+ detail: `declared find assertions without receipt backing: ${audit.missing.join("; ")}`,
554
+ });
555
+ }
556
+ }
412
557
  }
413
- const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath);
558
+ const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath, layout);
414
559
  // Parse bounded fixed sections from execution.md. Missing optional
415
560
  // summary/steps are advisory only and never downgrade a passed status.
416
561
  let executionSummary = "";
@@ -459,6 +604,7 @@ export async function materializeFrontendTestResult(input) {
459
604
  passed: cases.filter((item) => item.status === "passed").length,
460
605
  failed: cases.filter((item) => item.status === "failed").length,
461
606
  blocked: cases.filter((item) => item.status === "blocked").length,
607
+ firstTryPassed: cases.filter((item) => item.status === "passed" && !item.rerunAttempt).length,
462
608
  };
463
609
  const outcome = totals.failed > 0 ? "failed" : totals.blocked > 0 || missing.length > 0 ? "incomplete" : "passed";
464
610
  const result = frontendTestResultContractSchema.parse({
@@ -0,0 +1,70 @@
1
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { defaultFrontendTestLayout, frontendTestStandardScenariosRel, } from "./frontend-test-layout.js";
4
+ export const FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME = "frontend-test-standard-scenarios.v1.json";
5
+ export const FRONTEND_TEST_STANDARD_SCENARIOS_DEST = "testcase/frontend/rag/standard-scenarios.v1.json";
6
+ const MINIMAL_STANDARD_SCENARIOS = {
7
+ schemaVersion: 1,
8
+ id: "frontend-test-standard-scenarios-v1",
9
+ scenarios: [
10
+ {
11
+ id: "STD-FE-SMOKE-ENTRY",
12
+ title: "入口可打开",
13
+ category: "smoke",
14
+ priority: "must",
15
+ testPoints: ["open"],
16
+ minCases: 1,
17
+ },
18
+ ],
19
+ };
20
+ function isSafeRelativeDir(value) {
21
+ if (!value || path.isAbsolute(value) || path.win32.isAbsolute(value)) {
22
+ return false;
23
+ }
24
+ return value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
25
+ }
26
+ export function listFrontendTestStandardScenarioCandidates(input) {
27
+ const seen = new Set();
28
+ const add = (relative) => {
29
+ const normalized = relative.replaceAll("\\", "/");
30
+ if (!seen.has(normalized))
31
+ seen.add(normalized);
32
+ };
33
+ add(`docs/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
34
+ const governanceRoot = input.governanceRoot?.trim().replaceAll("\\", "/").replace(/\/+$/, "");
35
+ if (governanceRoot && isSafeRelativeDir(governanceRoot)) {
36
+ add(`${governanceRoot}/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
37
+ }
38
+ add(`ai_workspace/loop-agent/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
39
+ return [...seen];
40
+ }
41
+ async function readGovernanceRoot(workspaceRoot) {
42
+ try {
43
+ const raw = JSON.parse(await readFile(path.join(workspaceRoot, "harness.json"), "utf8"));
44
+ return typeof raw.governanceRoot === "string" ? raw.governanceRoot : undefined;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ export async function copyFrontendTestStandardScenarios(input) {
51
+ const layout = input.layout ?? defaultFrontendTestLayout();
52
+ const destRel = frontendTestStandardScenariosRel(layout);
53
+ const destAbs = path.join(input.workspaceRoot, destRel);
54
+ const candidates = listFrontendTestStandardScenarioCandidates({
55
+ governanceRoot: await readGovernanceRoot(input.workspaceRoot),
56
+ });
57
+ await mkdir(path.dirname(destAbs), { recursive: true });
58
+ for (const relative of candidates) {
59
+ const sourceAbs = path.join(input.workspaceRoot, relative);
60
+ try {
61
+ await copyFile(sourceAbs, destAbs);
62
+ return { status: "copied", from: relative, to: destRel };
63
+ }
64
+ catch {
65
+ // try the next candidate
66
+ }
67
+ }
68
+ await writeFile(destAbs, `${JSON.stringify(MINIMAL_STANDARD_SCENARIOS, null, 2)}\n`, "utf8");
69
+ return { status: "fallback", to: destRel };
70
+ }
@@ -2,6 +2,18 @@ import { readFile, stat, writeFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
4
4
  export const FRONTEND_VERIFICATION_TRACE_SCHEMA_ID = "frontend-verification-trace-v1";
5
+ /**
6
+ * Shared marker for a project that exposes no usable verification command at
7
+ * generation time. The marker command runs (and logs not-run honestly), but it
8
+ * must never count as successful verification evidence: labels carrying this
9
+ * text are excluded from successful evidence and targets bound to it fail the
10
+ * trace gate instead of passing it.
11
+ */
12
+ export const FRONTEND_NO_VERIFICATION_MARKER_TEXT = "No frontend verification command configured";
13
+ export function isFrontendNoVerificationMarkerLabel(label) {
14
+ return label.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT);
15
+ }
16
+ export const FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE = `frontend verification unavailable: the only frozen static command is the no-op not-run marker (${FRONTEND_NO_VERIFICATION_MARKER_TEXT}); declare verification commands via task.json --verify or 执行约束.md and regenerate the DAG`;
5
17
  async function readJsonFile(filePath) {
6
18
  return JSON.parse(await readFile(filePath, "utf8"));
7
19
  }
@@ -52,7 +64,14 @@ export async function evaluateVerificationTargets(input) {
52
64
  const hardIssues = [];
53
65
  for (const target of input.contract.verificationTargets) {
54
66
  const issues = [];
55
- const owners = input.labelOwners.get(target.commandLabel);
67
+ if (isFrontendNoVerificationMarkerLabel(target.commandLabel)) {
68
+ // A target bound to the not-run marker has no verification behind it;
69
+ // passing it would let the run finish green without any check.
70
+ issues.push(`${target.id}: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`);
71
+ }
72
+ const owners = isFrontendNoVerificationMarkerLabel(target.commandLabel)
73
+ ? []
74
+ : (input.labelOwners.get(target.commandLabel) ?? []);
56
75
  if (!owners?.length) {
57
76
  issues.push(`commandLabel not executed in current-run frozen evidence: ${target.commandLabel}`);
58
77
  }
@@ -189,11 +208,17 @@ export async function runFrontendVerificationTraceGate(input) {
189
208
  throw new Error("trace: selected Mock strategy requires successful Mock verification commands");
190
209
  }
191
210
  if (input.evidence) {
192
- if (input.evidence.static.commandLabels.length === 0) {
193
- throw new Error("trace: no successful static verification commands in current run");
211
+ // The no-op not-run marker never counts as successful verification
212
+ // evidence, even if it executed with exit code 0.
213
+ const effectiveStaticLabels = input.evidence.static.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
214
+ const effectiveBehaviorLabels = input.evidence.behavior.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
215
+ if (effectiveStaticLabels.length === 0) {
216
+ throw new Error(input.evidence.static.commandLabels.length > 0
217
+ ? `trace: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`
218
+ : "trace: no successful static verification commands in current run");
194
219
  }
195
220
  if (requiresBehaviorVerification &&
196
- input.evidence.behavior.commandLabels.length === 0) {
221
+ effectiveBehaviorLabels.length === 0) {
197
222
  throw new Error("trace: no successful behavior verification commands in current run");
198
223
  }
199
224
  }