@tea-agent/loop-agent 0.39.0-next.3 → 0.39.0-next.31

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 (238) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +270 -65
  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 +118 -14
  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 +51 -12
  23. package/dist/executors/pi-sdk-executor.js +187 -59
  24. package/dist/executors/shell-executor.js +533 -29
  25. package/dist/executors/shell-write-guard.js +7 -0
  26. package/dist/shared/dag-prompt-override.js +28 -0
  27. package/dist/shared/operator/capabilities.js +181 -27
  28. package/dist/shared/playwright-cli-command-policy.js +15 -0
  29. package/dist/shared/update/console-notifier.js +100 -0
  30. package/dist/{cli → shared}/update/npm-client.js +41 -8
  31. package/dist/{cli → shared}/update/state.js +41 -13
  32. package/dist/task/config-types.js +22 -0
  33. package/dist/task/contract/constants.js +1 -0
  34. package/dist/task/contract/diff.js +26 -0
  35. package/dist/task/contract/project.js +5 -0
  36. package/dist/task/contract/schema.js +6 -1
  37. package/dist/task/source-prepare/build-draft.js +15 -0
  38. package/dist/task/source-prepare/parse-intent.js +24 -4
  39. package/dist/worker/cli.js +31 -1
  40. package/dist/worker/console/chat/deferred-turn.js +91 -0
  41. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  42. package/dist/worker/console/chat/pi-runtime.js +165 -83
  43. package/dist/worker/console/chat/repo-walk.js +10 -1
  44. package/dist/worker/console/chat/routes.js +478 -55
  45. package/dist/worker/console/chat/semantic-activity.js +20 -8
  46. package/dist/worker/console/chat/session-store.js +146 -2
  47. package/dist/worker/console/chat/turn-process.js +17 -30
  48. package/dist/worker/console/dag-execution-receipt.js +33 -0
  49. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  50. package/dist/worker/console/operator-actions.js +244 -4
  51. package/dist/worker/console/operator-user-error.js +20 -1
  52. package/dist/worker/console/pi-readiness.js +12 -5
  53. package/dist/worker/console/recovery-cta.js +4 -4
  54. package/dist/worker/console/recovery-error-copy.js +4 -4
  55. package/dist/worker/console/routes.js +43 -2
  56. package/dist/worker/console/security.js +83 -10
  57. package/dist/worker/console/server.js +42 -6
  58. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
  59. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-pm3c2Rsl.js} +1 -1
  60. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
  61. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
  62. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
  63. package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
  64. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
  74. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
  75. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
  76. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
  77. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
  78. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
  79. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
  81. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
  82. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
  83. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
  85. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
  86. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
  87. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
  88. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
  89. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
  90. package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
  91. package/dist/worker/console/static/assets/index-DEbyrEfW.js +404 -0
  92. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
  93. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
  94. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
  95. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
  96. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-CAyXjQjN.js} +1 -1
  97. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-CcWKn6tS.js} +5 -5
  98. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
  99. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
  100. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
  101. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
  102. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
  103. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
  104. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
  105. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
  106. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
  107. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
  108. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
  109. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
  110. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
  111. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
  112. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
  113. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
  114. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
  115. package/dist/worker/console/static/index.html +2 -2
  116. package/dist/worker/console/static-src/app/console-types.js +39 -0
  117. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  118. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  119. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
  120. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  121. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  122. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  123. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  124. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  125. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
  126. package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
  127. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  128. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  129. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  130. package/dist/worker/observe/node-input.js +74 -3
  131. package/dist/worker/observe/node-process.js +377 -0
  132. package/dist/worker/observe/routes.js +36 -3
  133. package/dist/worker/observe/shell-handler-keys.js +34 -0
  134. package/dist/worker/observe/static/api.js +66 -0
  135. package/dist/worker/observe/static/constants.js +8 -1
  136. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  137. package/dist/worker/observe/static/format.js +23 -0
  138. package/dist/worker/observe/static/markdown-render.js +20 -1
  139. package/dist/worker/observe/static/operator-chrome.js +4 -0
  140. package/dist/worker/observe/static/state.js +13 -2
  141. package/dist/worker/observe/static/styles.css +73 -0
  142. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  143. package/dist/worker/observe/static/views/dag.js +8 -1
  144. package/dist/worker/observe/static/views/failures.js +5 -2
  145. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  146. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
  147. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  148. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  149. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  150. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  151. package/dist/workflows/dag/backend-test-layout.js +133 -0
  152. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  153. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  154. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  155. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  156. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  157. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  158. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  159. package/dist/workflows/dag/contract-output-registry.js +15 -0
  160. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  161. package/dist/workflows/dag/convergence/controller.js +141 -0
  162. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  163. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  164. package/dist/workflows/dag/failure-category.js +12 -0
  165. package/dist/workflows/dag/frontend-implementation-contract.js +493 -2
  166. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  167. package/dist/workflows/dag/frontend-prewrite-gate.js +221 -31
  168. package/dist/workflows/dag/frontend-repair.js +14 -0
  169. package/dist/workflows/dag/frontend-risk.js +15 -2
  170. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  171. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  172. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  173. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  174. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  175. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  176. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  177. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  178. package/dist/workflows/dag/init-hybrid.js +895 -127
  179. package/dist/workflows/dag/node-execution.js +194 -6
  180. package/dist/workflows/dag/prompt.js +60 -1
  181. package/dist/workflows/dag/rerun-feedback.js +212 -0
  182. package/dist/workflows/dag/rerun-plan.js +224 -27
  183. package/dist/workflows/dag/rerun-run.js +61 -6
  184. package/dist/workflows/dag/rerun-task.js +61 -2
  185. package/dist/workflows/dag/retry-policy.js +113 -0
  186. package/dist/workflows/dag/structured-output-repair.js +712 -0
  187. package/dist/workflows/dag/types.js +157 -1
  188. package/dist/workflows/dag/validate.js +41 -1
  189. package/docs/init-surface.manifest.json +5 -0
  190. package/docs/operations/README.md +1 -1
  191. package/docs/operations/local-development-environment.md +1 -5
  192. package/docs/skills/vetted-skill-registry.md +14 -0
  193. package/docs/templates/README.md +1 -1
  194. package/docs/templates/agent-dag.schema.json +44 -0
  195. package/docs/templates/backend-test-dag.json +60 -35
  196. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  197. package/docs/templates/frontend-test-dag.json +8 -10
  198. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  199. package/docs/templates/init-managed-agents.md +13 -3
  200. package/harness.json +5 -3
  201. package/package.json +12 -4
  202. package/scripts/next-info.mjs +356 -0
  203. package/scripts/next-publish-gate.mjs +238 -0
  204. package/scripts/release-source-binding.mjs +251 -0
  205. package/skills/analyze-product-requirements/SKILL.md +8 -5
  206. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  207. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  208. package/skills/analyze-product-requirements/references/example.md +50 -0
  209. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  210. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  211. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  212. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  213. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  214. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  215. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  216. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  217. package/skills/codebase-scout/SKILL.md +1 -1
  218. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  219. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  220. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  221. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  222. package/skills/frontend-implementation/references/design-spec.md +16 -19
  223. package/skills/frontend-implementation/references/node-contracts.md +9 -6
  224. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  225. package/skills/improve-codebase-architecture/deepening.md +37 -0
  226. package/skills/improve-codebase-architecture/html-report.md +123 -0
  227. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  228. package/skills/improve-codebase-architecture/language.md +53 -0
  229. package/skills/loop-agent/references/command-reference.md +11 -4
  230. package/skills/playwright-cli/SKILL.md +22 -0
  231. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  232. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  233. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  234. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  235. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  236. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  237. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  238. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -1,13 +1,19 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { readFile, stat } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { z } from "zod";
4
5
  import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
6
+ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
5
7
  import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
6
- import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
8
+ import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
9
+ import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
7
10
  import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
8
11
  import { pathMatchesPattern } from "../../shared/git-progress.js";
12
+ import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
9
13
  export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
10
14
  export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
15
+ /** Canonical contract serialization must stay at or below this byte size. */
16
+ export const FRONTEND_CONTRACT_MAX_BYTES = 65536;
11
17
  /**
12
18
  * Machine-readable failure classification for blocked/retryable-invalid
13
19
  * prewrite outcomes. Kept alongside the human-readable failureReason so
@@ -31,6 +37,11 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
31
37
  "openspec-citation-block-unparseable",
32
38
  "openspec-not-cited",
33
39
  "openspec-citation-not-read",
40
+ "component-spec-reference-invalid",
41
+ "component-spec-not-cited",
42
+ "component-choices-missing",
43
+ "contract-too-large",
44
+ "revision-patch-unmergeable",
34
45
  ]);
35
46
  export const frontendPrewriteResultV1Schema = z
36
47
  .object({
@@ -147,32 +158,80 @@ async function selectNode(runDir, primary, fallbacks) {
147
158
  }
148
159
  throw new Error(`frontend prewrite gate no FINISHED candidate: ${rejections.join("; ")}`);
149
160
  }
150
- async function readNodeText(runDir, nodeId) {
151
- const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
161
+ async function readFinishedNodeRecord(runDir, nodeId) {
162
+ let record;
163
+ try {
164
+ record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
165
+ }
166
+ catch (error) {
167
+ throw new Error(`frontend prewrite gate cannot parse node record ${nodeId}: ${error instanceof Error ? error.message : String(error)}`);
168
+ }
152
169
  if (record.status !== "FINISHED") {
153
170
  throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
154
171
  }
155
- const text = [record.assistantText, record.stdout]
156
- .filter((value) => Boolean(value?.trim()))
157
- .join("\n");
172
+ return record;
173
+ }
174
+ async function readNodeText(runDir, nodeId) {
175
+ const record = await readFinishedNodeRecord(runDir, nodeId);
176
+ const text = record.assistantText?.trim() || record.stdout?.trim() || "";
158
177
  if (!text)
159
178
  throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
160
179
  return text;
161
180
  }
162
- async function readNodeRawText(runDir, nodeId) {
163
- const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
164
- return record.assistantText ?? record.stdout ?? "";
181
+ async function readCanonicalContractFromNode(runDir, nodeId) {
182
+ const record = await readFinishedNodeRecord(runDir, nodeId);
183
+ if (!record.structuredArtifactPath) {
184
+ throw new Error(`frontend prewrite gate missing structured artifact for ${nodeId}`);
185
+ }
186
+ const artifactPath = path.isAbsolute(record.structuredArtifactPath)
187
+ ? record.structuredArtifactPath
188
+ : path.join(runDir, record.structuredArtifactPath);
189
+ const resolvedRun = path.resolve(runDir);
190
+ const resolvedArtifact = path.resolve(artifactPath);
191
+ const relative = path.relative(resolvedRun, resolvedArtifact);
192
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
193
+ throw new Error(`frontend prewrite gate structured artifact escapes runDir: ${record.structuredArtifactPath}`);
194
+ }
195
+ const bytes = await readFile(resolvedArtifact);
196
+ const digest = createHash("sha256").update(bytes).digest("hex");
197
+ if (record.structuredArtifactSha256 &&
198
+ record.structuredArtifactSha256 !== digest) {
199
+ throw new Error(`frontend prewrite gate structured artifact hash mismatch for ${nodeId}`);
200
+ }
201
+ if (record.structuredArtifactSchemaId &&
202
+ record.structuredArtifactSchemaId !== FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID) {
203
+ throw new Error(`frontend prewrite gate structured artifact schema mismatch for ${nodeId}: ${record.structuredArtifactSchemaId}`);
204
+ }
205
+ const contractText = bytes.toString("utf8");
206
+ let citationsText = "";
207
+ try {
208
+ const citationsRaw = await readFile(path.join(path.dirname(resolvedArtifact), "openspec-citations.json"), "utf8");
209
+ try {
210
+ const parsed = JSON.parse(citationsRaw);
211
+ citationsText =
212
+ typeof parsed.block === "string" ? parsed.block : citationsRaw;
213
+ }
214
+ catch {
215
+ citationsText = citationsRaw;
216
+ }
217
+ }
218
+ catch (error) {
219
+ if (error.code !== "ENOENT")
220
+ throw error;
221
+ }
222
+ return { contractText, citationsText, rawSha256: digest };
165
223
  }
166
224
  function firstNonEmptyVerdictLine(text) {
167
225
  const lines = text
168
226
  .split(/\r?\n/)
169
227
  .map((line) => line.trim())
170
228
  .filter(Boolean);
171
- const normalize = (line) => {
172
- const emphasized = line.match(/^(?:`{1,3}|\*{1,3})\s*(VERDICT:[^`*]+?)\s*(?:`{1,3}|\*{1,3})$/);
173
- return (emphasized?.[1] ?? line).trim();
174
- };
175
- const normalizedLines = lines.map(normalize);
229
+ // Emphasis stripping (backtick + asterisk) is shared with the runIf
230
+ // selector and node-level output-protocol validation via
231
+ // normalizeVerdictCandidateLine. The gate keeps its own loose
232
+ // pass/request-revision normalization below (prefix match + trailing
233
+ // prose tolerance) for deterministic fail-closed behavior.
234
+ const normalizedLines = lines.map(normalizeVerdictCandidateLine);
176
235
  const first = normalizedLines[0] ?? "";
177
236
  // The prompt requires VERDICT to be the first non-empty line, but model
178
237
  // output can still prepend a summary. Keep the protocol strict in the
@@ -389,7 +448,30 @@ export async function runFrontendPrewriteGate(input) {
389
448
  // remains an ERROR and is intentionally not classified into prewrite-result.
390
449
  const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
391
450
  const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
392
- const candidateRawSha256 = sha256Hex(await readNodeRawText(input.runDir, planNodeId));
451
+ let planArtifact;
452
+ try {
453
+ planArtifact = await readCanonicalContractFromNode(input.runDir, planNodeId);
454
+ }
455
+ catch (error) {
456
+ return finalizePrewrite(input, {
457
+ planNodeId,
458
+ reviewNodeId,
459
+ verdict: "",
460
+ candidateRawSha256: sha256Hex(""),
461
+ candidateJsonSha256: null,
462
+ canonicalSha256: null,
463
+ normalizationActions: [],
464
+ mockStrategy: undefined,
465
+ artifact: undefined,
466
+ failureReason: error instanceof Error ? error.message : String(error),
467
+ failureCode: "plan-text-unreadable",
468
+ classification: "retryable-invalid",
469
+ openspecReadPaths: [],
470
+ openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
471
+ openspecPolicy: input.config.openspecPolicy ?? "scan-strict",
472
+ });
473
+ }
474
+ const candidateRawSha256 = planArtifact.rawSha256;
393
475
  const openspecPolicy = input.config.openspecPolicy ?? "scan-strict";
394
476
  const basePending = {
395
477
  planNodeId,
@@ -408,18 +490,9 @@ export async function runFrontendPrewriteGate(input) {
408
490
  openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
409
491
  openspecPolicy,
410
492
  };
411
- let planText;
412
- try {
413
- planText = await readNodeText(input.runDir, planNodeId);
414
- }
415
- catch (error) {
416
- return finalizePrewrite(input, {
417
- ...basePending,
418
- classification: "retryable-invalid",
419
- failureReason: error instanceof Error ? error.message : String(error),
420
- failureCode: "plan-text-unreadable",
421
- });
422
- }
493
+ const planText = [planArtifact.contractText, planArtifact.citationsText]
494
+ .filter(Boolean)
495
+ .join("\n");
423
496
  let reviewText;
424
497
  try {
425
498
  reviewText = await readNodeText(input.runDir, reviewNodeId);
@@ -442,7 +515,8 @@ export async function runFrontendPrewriteGate(input) {
442
515
  failureCode: "verdict-not-pass",
443
516
  });
444
517
  }
445
- const missingIds = input.config.requiredRequirementIds.filter((id) => !planText.includes(id));
518
+ const requirementIdSearchText = planArtifact.contractText;
519
+ const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
446
520
  if (missingIds.length > 0) {
447
521
  return finalizePrewrite(input, {
448
522
  ...basePending,
@@ -489,13 +563,13 @@ export async function runFrontendPrewriteGate(input) {
489
563
  workspaceRoot,
490
564
  });
491
565
  }
492
- // Candidate -> canonical conversion. The gate consumes the selected
493
- // plan/revision assistant output directly and is the only conversion point.
566
+ // Candidate -> canonical conversion. The gate consumes the producing node's
567
+ // hash-bound canonical artifact and never re-merges plan/revision stdout.
494
568
  let analysis;
495
569
  try {
496
570
  analysis = await analyzeFrontendImplementationContract({
497
571
  runDir: input.runDir,
498
- fromNodeId: planNodeId,
572
+ rawContractText: planArtifact.contractText,
499
573
  sourceBinding: input.sourceBinding,
500
574
  });
501
575
  }
@@ -515,6 +589,21 @@ export async function runFrontendPrewriteGate(input) {
515
589
  }
516
590
  throw error;
517
591
  }
592
+ // Size guard: a canonical contract above 64KB is a retryable-invalid defect
593
+ // (compress the contract) rather than a writer authorization failure.
594
+ const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
595
+ if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
596
+ return finalizePrewrite(input, {
597
+ ...basePending,
598
+ verdict,
599
+ candidateJsonSha256: analysis.candidateJsonSha256,
600
+ normalizationActions: [],
601
+ mockStrategy: analysis.canonical.mockApi.strategy,
602
+ classification: "retryable-invalid",
603
+ failureReason: `frontend prewrite gate: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit. 压缩契约(移除冗余 prose、重复目标或过长描述)后重试。`,
604
+ failureCode: "contract-too-large",
605
+ });
606
+ }
518
607
  const mockStrategy = analysis.canonical.mockApi.strategy;
519
608
  if (!input.config.allowedMockStrategies.includes(mockStrategy)) {
520
609
  const quotedAllowed = input.config.allowedMockStrategies
@@ -711,6 +800,105 @@ export async function runFrontendPrewriteGate(input) {
711
800
  }
712
801
  }
713
802
  }
803
+ // Component selection spec conformance (ADR 0016). Deterministic cross-check
804
+ // that declared component/token selections trace to the frontend spec
805
+ // (openspec/ai_workspace component + theme) instead of a model's ad-hoc
806
+ // "better" choice. Runs after the openspec read gate so citation + read
807
+ // evidence are already resolved.
808
+ const componentChoices = analysis.canonical.uiComponentChoices ?? [];
809
+ const componentSpecCandidates = input.config.componentSpecCandidatePaths ?? [];
810
+ const hasUiVisibleWork = analysis.canonical.uiStates.some((state) => state.applicable) ||
811
+ analysis.canonical.interactions.length > 0;
812
+ if (componentSpecCandidates.length > 0 &&
813
+ hasUiVisibleWork &&
814
+ componentChoices.length === 0) {
815
+ return finalizePrewrite(input, {
816
+ ...basePending,
817
+ verdict,
818
+ candidateJsonSha256: analysis.candidateJsonSha256,
819
+ normalizationActions: [],
820
+ mockStrategy,
821
+ classification: "retryable-invalid",
822
+ failureReason: "frontend prewrite gate: the canonical contract has UI-visible work but uiComponentChoices is missing or empty while the generation-frozen component/theme specification bucket is non-empty. 为每个 UI 用途声明组件选型(decision: specified | reuse-existing | new),specified 必须给 specReference.path/section/line,new 必须给 rationale 声明偏差。",
823
+ failureCode: "component-choices-missing",
824
+ openspecReadPaths,
825
+ openspecCandidatePaths: candidatePaths,
826
+ });
827
+ }
828
+ const componentSpecReferences = componentChoices
829
+ .map((choice) => ({
830
+ raw: choice.specReference?.path ?? "",
831
+ normalized: choice.specReference?.path
832
+ ? normalizeCitationPath(choice.specReference.path, candidateRoot)
833
+ : null,
834
+ }))
835
+ .filter((ref) => ref.raw.length > 0);
836
+ if (componentSpecReferences.length > 0) {
837
+ const invalidRefs = [
838
+ ...new Set(componentSpecReferences
839
+ .filter((ref) => !ref.normalized ||
840
+ !isOpenspecSpecFilePath(ref.normalized))
841
+ .map((ref) => ref.raw)),
842
+ ];
843
+ if (invalidRefs.length > 0) {
844
+ return finalizePrewrite(input, {
845
+ ...basePending,
846
+ verdict,
847
+ candidateJsonSha256: analysis.candidateJsonSha256,
848
+ normalizationActions: [],
849
+ mockStrategy,
850
+ classification: "retryable-invalid",
851
+ failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths are not supported openspec/ai_workspace spec files: ${invalidRefs.join(", ")}`,
852
+ failureCode: "component-spec-reference-invalid",
853
+ openspecReadPaths,
854
+ openspecCandidatePaths: candidatePaths,
855
+ });
856
+ }
857
+ if (openspecPolicy === "cited") {
858
+ const parsedCitations = parseOpenspecCitations(planText, candidateRoot);
859
+ const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
860
+ const readSet = new Set(openspecReadPaths);
861
+ const uncorroborated = [
862
+ ...new Set(componentSpecReferences
863
+ .filter((ref) => !ref.normalized ||
864
+ !citedPaths.has(ref.normalized) ||
865
+ !readSet.has(ref.normalized))
866
+ .map((ref) => ref.raw)),
867
+ ];
868
+ if (uncorroborated.length > 0) {
869
+ return finalizePrewrite(input, {
870
+ ...basePending,
871
+ verdict,
872
+ candidateJsonSha256: analysis.candidateJsonSha256,
873
+ normalizationActions: [],
874
+ mockStrategy,
875
+ classification: "retryable-invalid",
876
+ failureReason: `frontend prewrite gate: these uiComponentChoices specReference paths were not cited in the plan openspec-citations block or lack a successful read event: ${uncorroborated.join(", ")}`,
877
+ failureCode: "component-spec-not-cited",
878
+ openspecReadPaths,
879
+ openspecCandidatePaths: candidatePaths,
880
+ });
881
+ }
882
+ }
883
+ }
884
+ // Render the deterministic plan.md from the canonical contract (fail-closed
885
+ // before any artifact is written: no contract, no writer authorization).
886
+ let planMarkdown;
887
+ try {
888
+ planMarkdown = renderFrontendPlanMarkdown(analysis.canonical);
889
+ }
890
+ catch (error) {
891
+ return finalizePrewrite(input, {
892
+ ...basePending,
893
+ verdict,
894
+ candidateJsonSha256: analysis.candidateJsonSha256,
895
+ normalizationActions: [],
896
+ mockStrategy,
897
+ classification: "retryable-invalid",
898
+ failureReason: `frontend prewrite gate: failed to render ${input.config.planMdArtifactName ?? "frontend-plan.md"}: ${error instanceof Error ? error.message : String(error)}`,
899
+ failureCode: "contract-invalid",
900
+ });
901
+ }
714
902
  // Materialize the canonical contract only after every governance check passed.
715
903
  const artifact = await writeFrontendImplementationContractArtifact({
716
904
  runDir: input.runDir,
@@ -718,6 +906,8 @@ export async function runFrontendPrewriteGate(input) {
718
906
  artifactName: input.config.artifactName,
719
907
  canonical: analysis.canonical,
720
908
  });
909
+ // Materialize the rendered plan.md in the same outputDir as the contract.
910
+ await writeTextArtifactFile(path.join(input.runDir, path.posix.join(input.config.outputDir, input.config.planMdArtifactName ?? "frontend-plan.md")), planMarkdown);
721
911
  const classification = analysis.normalizationActions.length > 0 ? "accepted-normalized" : "accepted";
722
912
  return finalizePrewrite(input, {
723
913
  ...basePending,
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { z } from "zod";
4
4
  import { pathMatchesPattern } from "../../shared/git-progress.js";
5
5
  import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
6
+ import { FRONTEND_NO_VERIFICATION_MARKER_TEXT, FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE, } from "./frontend-verification-trace.js";
6
7
  export const FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID = "frontend-repair-assessment-v1";
7
8
  export const frontendRepairFailureClassSchema = z.enum([
8
9
  "typecheck",
@@ -82,6 +83,12 @@ export function classifyFrontendFailure(input) {
82
83
  if (input.failureCategory === "write-guard") {
83
84
  return "path";
84
85
  }
86
+ if (`${input.stderr ?? ""}\n${input.stdout ?? ""}`.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT)) {
87
+ // Verification unavailability is a generation-time contract gap: no
88
+ // authorized verification command exists. Bounded repair cannot fix it;
89
+ // the operator must declare commands and regenerate the DAG.
90
+ return "contract";
91
+ }
85
92
  if (input.traceTarget || input.commandCategory === "trace") {
86
93
  return "trace";
87
94
  }
@@ -424,6 +431,13 @@ export async function runFrontendFailureAssessGate(input) {
424
431
  : !baseEligible
425
432
  ? `non-repairable or ineligible: failureClass=${failureClass} attempt=${attempt}`
426
433
  : `missing structured evidence for failureClass=${failureClass} on ${primary.nodeId}`;
434
+ if (!eligible &&
435
+ `${primary.record.stderr ?? ""}`.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT)) {
436
+ // Surface the actionable unavailable-verification reason in the
437
+ // assessment (and the non-repairable gate error) instead of a
438
+ // generic class label.
439
+ reason = FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE;
440
+ }
427
441
  }
428
442
  // fixScope and changedPaths must stay inside the implement writeSet.
429
443
  if (evidence.fixScope?.length) {
@@ -2,6 +2,12 @@
2
2
  * Deterministic frontend-implementation risk classifier (M4).
3
3
  * Pure function: no I/O, no model, no side effects.
4
4
  */
5
+ /**
6
+ * Dependency-installation language in task text. Mentioning package.json or
7
+ * allowing its path is NOT dependency introduction by itself; the manifest
8
+ * path check in classifyFrontendRisk requires this language to escalate.
9
+ */
10
+ const DEPENDENCY_INSTALL_LANGUAGE = /new\s+dependency|add\s+dependencies?|install\s+dependencies?|npm\s+(?:install|i)\b|yarn\s+(?:add|install)\b|pnpm\s+(?:add|install)\b|bun\s+(?:add|install)\b|新增依赖|添加依赖|安装依赖|引入依赖/i;
5
11
  const HIGH_RISK_PATTERNS = [
6
12
  {
7
13
  id: "new-route",
@@ -25,7 +31,7 @@ const HIGH_RISK_PATTERNS = [
25
31
  },
26
32
  {
27
33
  id: "new-dependency",
28
- re: /new\s+dependency|add\s+dependency|npm\s+install|新增依赖|package\.json/i,
34
+ re: DEPENDENCY_INSTALL_LANGUAGE,
29
35
  },
30
36
  {
31
37
  id: "schema-api-change",
@@ -88,7 +94,14 @@ export function classifyFrontendRisk(input) {
88
94
  const paths = input.allowedPaths ?? [];
89
95
  const spread = pathSpreadScore(paths);
90
96
  if (paths.some((p) => /package\.json|pnpm-lock|yarn\.lock|package-lock/.test(p))) {
91
- signals.push("new-dependency");
97
+ if (DEPENDENCY_INSTALL_LANGUAGE.test(blob)) {
98
+ signals.push("new-dependency");
99
+ }
100
+ else {
101
+ // Editing manifest scripts/config is not dependency introduction;
102
+ // record the rejected escalation instead of forcing full gates.
103
+ rejectedSignals.push("new-dependency:manifest-path-without-install-language");
104
+ }
92
105
  }
93
106
  // Explicit multi-domain language only — path count alone is not high-risk
94
107
  if (spread >= 3 &&
@@ -1,16 +1,19 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { isPlaywrightCliCommand } from "../../shared/playwright-cli-command-policy.js";
4
+ import { isTruncatedFrontendAcId, isTruncatedWhenUndeclaredFrontendAcId, } from "./frontend-test-result-contract.js";
4
5
  const COMMAND_FENCE_LANGUAGE = /^(?:|bash|sh|shell|zsh|fish|console|terminal|command|cmd|powershell|pwsh|javascript|js|typescript|ts|python|py)$/i;
5
6
  const LIST_ITEM = /^\s*(?:[-+*]|\d+[.)])\s+(.+)$/;
6
7
  const INDENTED_CODE = /^(?: {4,}|\t+)(\S.*)$/;
7
8
  const EXPLICIT_COMMAND = /^(?:(?:shell|terminal)(?:\s+command)?|command|run command|execute command)\s*:\s*(.+)$/i;
8
9
  const BLOCKED_REASON_PREFIX = /^(?:(?:blocked|forbidden|reject(?:ed)?|disallow(?:ed)?|prohibited)(?:\s+reason)?\s*(?::|\bbecause\b)|(?:do not|must not|never)\s+(?:run|execute|use)\b)/i;
9
10
  /** Standalone blockedReason / environment tokens (not executable commands). */
10
- const BLOCKED_REASON_TOKEN = /^(?:playwright-cli-unavailable|frontend-base-url-unreachable|curl-unavailable|browser-command-capability-unavailable|browser-command-evidence-missing|token-budget-exhausted|current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable|invalid-evidence-shape)(?:\s|$|[.,;:)`'"\]])/i;
11
+ const BLOCKED_REASON_TOKEN = /^(?:playwright-cli-unavailable|frontend-base-url-unreachable|curl-unavailable|browser-command-capability-unavailable|browser-command-evidence-missing|token-budget-exhausted|current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable|unique-control-unavailable|invalid-evidence-shape)(?:\s|$|[.,;:)`'"\]])/i;
11
12
  const BLOCKED_PROSE_HINT = /(?:blockedReason|blocked\s*reason|environmentProbe|evidenceDir|\bunavailable\b|不可用|写\s*blocked|写入\s*blocked|伪命令|自然语言|元数据|fail-closed|探测失败)/i;
12
13
  const IMPERATIVE_COMMAND = /^(?:(?:run|execute|use)(?:\s+(?:the\s+)?(?:(?:shell|terminal)\s+)?command)?|in\s+(?:the\s+)?(?:shell|terminal|console)\s*,?\s*(?:run|execute|use))\s*:?\s+(.+)$/i;
13
14
  const INLINE_CODE_STEP = /^`([^`\r\n]+)`[.!?]?$/;
15
+ /** Assertion-count annotations appended to a playwright-cli literal (e.g. `find "锚点">=1`, `>=N`, `≥N`, `(≥1)`) document an expected match count; they are documentation, not shell arguments. */
16
+ const ASSERTION_COUNT_SUFFIX = /\s*[((]?\s*(?:>=|≥|>|超过|至少)\s*\d+\s*[))]?\s*$/;
14
17
  const AUTOMATION_EXECUTABLE = /^(?:playwright-cli\b|playwright\b|@playwright\/test\b|npx\b|npm\b|pnpm\b|yarn\b|bunx?\b|node(?:js)?\b|python(?:3)?\b|bash\b|sh\b|zsh\b|fish\b|powershell\b|pwsh\b|cmd(?:\.exe)?\b|cypress\b|selenium\b|webdriverio\b|chromedriver\b|google-chrome\b|chrome\b|firefox\b|curl\b|wget\b)/i;
15
18
  const SHELL_WRAPPED_EXECUTABLE = /^(?:sudo\s+|env(?:\s+[A-Za-z_][A-Za-z0-9_]*=[^\s]+)*\s+|(?:\.{1,2}[\\/]|~[\\/]|[A-Za-z]:[\\/]|\/)[^\s]+)/i;
16
19
  const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]*)/;
@@ -44,6 +47,11 @@ function isBlockedReason(value) {
44
47
  return true;
45
48
  if (/^blockedReason\s*[:=]/i.test(trimmed))
46
49
  return true;
50
+ // Expected-outcome field lines ("status=blocked" / "status: failed") document
51
+ // the intended terminal state of a designed-negative case; they are
52
+ // case-result metadata, never executable commands.
53
+ if (/^["'`]?status["'`]?\s*[:=]\s*["'`]?(passed|failed|blocked)["'`]?\s*[.。]?$/i.test(trimmed))
54
+ return true;
47
55
  return false;
48
56
  }
49
57
  /**
@@ -238,6 +246,12 @@ function commandGateIssue(input) {
238
246
  if (input.instruction.fromFence && isBlockedReason(input.instruction.command)) {
239
247
  return null;
240
248
  }
249
+ // An expected-match-count annotation (`find "锚点">=1`, with or without a
250
+ // playwright-cli prefix) documents the assertion; it is not part of the
251
+ // command and never executable input.
252
+ if (/^(?:playwright-cli\s+)?find\s+"[^"]*"/i.test(input.instruction.command) && ASSERTION_COUNT_SUFFIX.test(input.instruction.command)) {
253
+ return null;
254
+ }
241
255
  const commandMatch = input.instruction.command.match(/^playwright-cli\s+([^\s`]+)/i);
242
256
  if (commandMatch) {
243
257
  const command = commandMatch[1].toLowerCase().replace(/^[`'"]+|[`'":,,。→]+$/g, "");
@@ -282,7 +296,13 @@ export async function validateFrontendCaseChecklist(input) {
282
296
  const manifestPath = await exists(draft) ? draft : await exists(final) ? final : "";
283
297
  if (!manifestPath)
284
298
  throw new Error("checklist: missing manifest.draft.json or manifest.json");
285
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
299
+ let manifest;
300
+ try {
301
+ manifest = JSON.parse(await readFile(manifestPath, "utf8"));
302
+ }
303
+ catch {
304
+ throw new Error("checklist: manifest is malformed JSON");
305
+ }
286
306
  if (!Array.isArray(manifest.cases) || manifest.cases.length === 0)
287
307
  throw new Error("checklist: empty cases");
288
308
  const declaredAc = new Set(input.declaredAcIds ?? []);
@@ -334,6 +354,10 @@ export async function validateFrontendCaseChecklist(input) {
334
354
  for (const ac of item.acIds) {
335
355
  if (typeof ac !== "string" || !acIdRe.test(ac))
336
356
  issues.push({ ruleId: "ac-id-shape", caseId: id, casePath, detail: String(ac) });
357
+ else if (declaredAc.size > 0 && isTruncatedFrontendAcId(ac))
358
+ issues.push({ ruleId: "ac-id-truncated", caseId: id, casePath, detail: `${ac} is a truncated acceptance id (missing feature prefix or sequence number)` });
359
+ else if (declaredAc.size === 0 && isTruncatedWhenUndeclaredFrontendAcId(ac))
360
+ issues.push({ ruleId: "ac-id-truncated", caseId: id, casePath, detail: `${ac} is a truncated acceptance id (missing feature prefix or sequence number)` });
337
361
  else if (declaredAc.size > 0 && !declaredAc.has(ac))
338
362
  issues.push({ ruleId: "unknown-ac", caseId: id, casePath, detail: `${ac} not in sourceBinding` });
339
363
  }
@@ -1,5 +1,6 @@
1
1
  import { readFile, rename, unlink, writeFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { isTruncatedFrontendAcId, isTruncatedWhenUndeclaredFrontendAcId, } from "./frontend-test-result-contract.js";
3
4
  const DIMENSIONS = new Set(["core", "boundary", "flow", "backend"]);
4
5
  function fail(ruleId, detail) {
5
6
  const msg = JSON.stringify({ ruleId, detail: String(detail ?? "") });
@@ -62,6 +63,10 @@ export async function materializeFrontendTestCaseManifest(input) {
62
63
  for (const ac of c.acIds) {
63
64
  if (typeof ac !== "string" || !acIdRe.test(ac))
64
65
  fail("ac-id-shape", `acIds entry must be AC-* acceptance id, not caseId: ${ac}`);
66
+ if ((declaredAc.size > 0
67
+ ? isTruncatedFrontendAcId(ac)
68
+ : isTruncatedWhenUndeclaredFrontendAcId(ac)))
69
+ fail("ac-id-truncated", `acIds entry is a truncated acceptance id (missing feature prefix or sequence number): ${ac}`);
65
70
  if (declaredAc.size > 0 && !declaredAc.has(ac))
66
71
  fail("unknown-ac", `${ac} not in sourceBinding; repair generator input or AC list`);
67
72
  acIds.push(ac);
@@ -1,25 +1,17 @@
1
1
  import { readFile, writeFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { markdownSection } from "./frontend-test-markdown.js";
3
4
  const CASE_ID = /^FE-[A-Za-z0-9]+-\d{3}-(?:core|boundary|flow|backend)$/i;
4
5
  const AC_ID = /^AC(?:-[A-Z0-9]+)+$/i;
5
6
  const PLACEHOLDER = /\b(?:TODO|TBD|FIXME)\b|结果正确|页面正常|按实际情况处理|验证成功/i;
6
7
  const SECTION_ALIASES = {
7
8
  purpose: ["Test Purpose", "测试目的", "测试场景"],
8
9
  source: ["Source References", "需求依据"],
9
- preconditions: ["Preconditions", "前置条件"],
10
- steps: ["Steps", "操作步骤"],
10
+ preconditions: ["Preconditions", "前置条件", "前置条件与重置"],
11
+ steps: ["Test Steps", "测试步骤", "Steps", "操作步骤"],
11
12
  expected: ["Expected Results", "预期结果"],
12
13
  automation: ["Automation Notes", "自动化映射", "自动化说明"],
13
14
  };
14
- function section(body, names) {
15
- const marker = new RegExp(`^###\\s+(?:${names.map((v) => v.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")).join("|")})\\s*$`, "mi");
16
- const hit = marker.exec(body);
17
- if (!hit)
18
- return "";
19
- const rest = body.slice(hit.index + hit[0].length);
20
- const next = /^###\s+/m.exec(rest);
21
- return rest.slice(0, next?.index ?? rest.length).trim();
22
- }
23
15
  function hasList(value) { return /^\s*(?:\d+[.)]|[-*+])\s+\S+/m.test(value); }
24
16
  function hasAssertion(value) { return /(?:status|状态|包含|显示|等于|为|可见|不可见|跳转|错误|成功|失败|should|expect|assert|must)/i.test(value); }
25
17
  export async function validateFrontendCaseContent(input) {
@@ -69,9 +61,9 @@ export async function validateFrontendCaseContent(input) {
69
61
  if (PLACEHOLDER.test(body))
70
62
  findings.push({ ruleId: "placeholder-wording", caseId: id, detail: "case contains placeholder or non-assertable wording" });
71
63
  for (const [key, names] of Object.entries(SECTION_ALIASES))
72
- if (!section(body, names))
64
+ if (!markdownSection(body, names))
73
65
  findings.push({ ruleId: `missing-${key}`, caseId: id, detail: `missing section: ${names.join(" or ")}` });
74
- const steps = section(body, SECTION_ALIASES.steps), expected = section(body, SECTION_ALIASES.expected);
66
+ const steps = markdownSection(body, SECTION_ALIASES.steps), expected = markdownSection(body, SECTION_ALIASES.expected);
75
67
  if (!hasList(steps))
76
68
  findings.push({ ruleId: "unstructured-steps", caseId: id, detail: "steps should contain numbered or bulleted executable actions" });
77
69
  if (!hasList(expected) || !hasAssertion(expected))