@guanlin4924/devflow-pi 0.1.93 → 0.1.95

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 (369) hide show
  1. package/README.md +10 -10
  2. package/node_modules/@playwright/test/LICENSE +202 -0
  3. package/node_modules/@playwright/test/NOTICE +5 -0
  4. package/node_modules/@playwright/test/README.md +318 -0
  5. package/node_modules/@playwright/test/cli.js +19 -0
  6. package/node_modules/@playwright/test/index.d.ts +18 -0
  7. package/node_modules/@playwright/test/index.js +17 -0
  8. package/node_modules/@playwright/test/index.mjs +18 -0
  9. package/node_modules/@playwright/test/package.json +35 -0
  10. package/node_modules/@playwright/test/reporter.d.ts +17 -0
  11. package/node_modules/@playwright/test/reporter.js +17 -0
  12. package/node_modules/@playwright/test/reporter.mjs +17 -0
  13. package/node_modules/playwright/LICENSE +202 -0
  14. package/node_modules/playwright/NOTICE +5 -0
  15. package/node_modules/playwright/README.md +318 -0
  16. package/node_modules/playwright/ThirdPartyNotices.txt +14 -0
  17. package/node_modules/playwright/cli.js +19 -0
  18. package/node_modules/playwright/index.d.ts +17 -0
  19. package/node_modules/playwright/index.js +17 -0
  20. package/node_modules/playwright/index.mjs +18 -0
  21. package/node_modules/playwright/jsx-runtime.js +42 -0
  22. package/node_modules/playwright/jsx-runtime.mjs +21 -0
  23. package/node_modules/playwright/lib/agents/agentParser.js +89 -0
  24. package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +34 -0
  25. package/node_modules/playwright/lib/agents/generateAgents.js +395 -0
  26. package/node_modules/playwright/lib/agents/playwright-test-coverage.prompt.md +31 -0
  27. package/node_modules/playwright/lib/agents/playwright-test-generate.prompt.md +8 -0
  28. package/node_modules/playwright/lib/agents/playwright-test-generator.agent.md +88 -0
  29. package/node_modules/playwright/lib/agents/playwright-test-heal.prompt.md +6 -0
  30. package/node_modules/playwright/lib/agents/playwright-test-healer.agent.md +56 -0
  31. package/node_modules/playwright/lib/agents/playwright-test-plan.prompt.md +9 -0
  32. package/node_modules/playwright/lib/agents/playwright-test-planner.agent.md +74 -0
  33. package/node_modules/playwright/lib/cli/reportActions.js +78 -0
  34. package/node_modules/playwright/lib/cli/testActions.js +214 -0
  35. package/node_modules/playwright/lib/common/index.js +2923 -0
  36. package/node_modules/playwright/lib/common/index.js.txt +34 -0
  37. package/node_modules/playwright/lib/errorContext.js +129 -0
  38. package/node_modules/playwright/lib/globals.js +58 -0
  39. package/node_modules/playwright/lib/index.js +815 -0
  40. package/node_modules/playwright/lib/isomorphic.js +260 -0
  41. package/node_modules/playwright/lib/isomorphic.js.txt +9 -0
  42. package/node_modules/playwright/lib/loader/loaderProcessEntry.js +34 -0
  43. package/node_modules/playwright/lib/loader/loaderProcessEntry.js.txt +9 -0
  44. package/node_modules/playwright/lib/matchers/expect.js +13490 -0
  45. package/node_modules/playwright/lib/matchers/expect.js.LICENSE +640 -0
  46. package/node_modules/playwright/lib/matchers/expect.js.txt +67 -0
  47. package/node_modules/playwright/lib/mcp/test/browserBackend.js +135 -0
  48. package/node_modules/playwright/lib/mcp/test/generatorTools.js +122 -0
  49. package/node_modules/playwright/lib/mcp/test/plannerTools.js +150 -0
  50. package/node_modules/playwright/lib/mcp/test/seed.js +82 -0
  51. package/node_modules/playwright/lib/mcp/test/streams.js +44 -0
  52. package/node_modules/playwright/lib/mcp/test/testBackend.js +99 -0
  53. package/node_modules/playwright/lib/mcp/test/testContext.js +306 -0
  54. package/node_modules/playwright/lib/mcp/test/testTool.js +30 -0
  55. package/node_modules/playwright/lib/mcp/test/testTools.js +98 -0
  56. package/node_modules/playwright/lib/package.js +47 -0
  57. package/node_modules/playwright/lib/program.js +241 -0
  58. package/node_modules/playwright/lib/runner/index.js +8527 -0
  59. package/node_modules/playwright/lib/runner/index.js.txt +61 -0
  60. package/node_modules/playwright/lib/transform/babelBundle.js +72274 -0
  61. package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +2384 -0
  62. package/node_modules/playwright/lib/transform/babelBundle.js.txt +325 -0
  63. package/node_modules/playwright/lib/transform/esmLoader.js +8054 -0
  64. package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +355 -0
  65. package/node_modules/playwright/lib/transform/esmLoader.js.txt +60 -0
  66. package/node_modules/playwright/lib/util.js +373 -0
  67. package/node_modules/playwright/lib/worker/workerProcessEntry.js +1861 -0
  68. package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +19 -0
  69. package/node_modules/playwright/package.json +55 -0
  70. package/node_modules/playwright/test.d.ts +18 -0
  71. package/node_modules/playwright/test.js +24 -0
  72. package/node_modules/playwright/test.mjs +35 -0
  73. package/node_modules/playwright/types/test.d.ts +11074 -0
  74. package/node_modules/playwright/types/testReporter.d.ts +950 -0
  75. package/node_modules/playwright-core/LICENSE +202 -0
  76. package/node_modules/playwright-core/NOTICE +5 -0
  77. package/node_modules/playwright-core/README.md +3 -0
  78. package/node_modules/playwright-core/ThirdPartyNotices.txt +13 -0
  79. package/node_modules/playwright-core/bin/install_media_pack.ps1 +5 -0
  80. package/node_modules/playwright-core/bin/install_webkit_wsl.ps1 +33 -0
  81. package/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh +42 -0
  82. package/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh +13 -0
  83. package/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 +24 -0
  84. package/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh +42 -0
  85. package/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh +12 -0
  86. package/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 +24 -0
  87. package/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh +48 -0
  88. package/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh +11 -0
  89. package/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 +23 -0
  90. package/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh +48 -0
  91. package/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh +11 -0
  92. package/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 +23 -0
  93. package/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh +48 -0
  94. package/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh +11 -0
  95. package/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 +24 -0
  96. package/node_modules/playwright-core/browsers.json +52 -0
  97. package/node_modules/playwright-core/cli.js +21 -0
  98. package/node_modules/playwright-core/index.d.ts +17 -0
  99. package/node_modules/playwright-core/index.js +17 -0
  100. package/node_modules/playwright-core/index.mjs +28 -0
  101. package/node_modules/playwright-core/lib/bootstrap.js +88 -0
  102. package/node_modules/playwright-core/lib/coreBundle.js +75813 -0
  103. package/node_modules/playwright-core/lib/entry/cliDaemon.js +5 -0
  104. package/node_modules/playwright-core/lib/entry/dashboardApp.js +3 -0
  105. package/node_modules/playwright-core/lib/entry/mcp.js +10 -0
  106. package/node_modules/playwright-core/lib/entry/oopBrowserDownload.js +3 -0
  107. package/node_modules/playwright-core/lib/package.js +50 -0
  108. package/node_modules/playwright-core/lib/server/chromium/appIcon.png +0 -0
  109. package/node_modules/playwright-core/lib/server/electron/loader.js +116 -0
  110. package/node_modules/playwright-core/lib/serverRegistry.js +1939 -0
  111. package/node_modules/playwright-core/lib/serverRegistry.js.LICENSE +65 -0
  112. package/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js +141 -0
  113. package/node_modules/playwright-core/lib/tools/cli-client/cli.js +6 -0
  114. package/node_modules/playwright-core/lib/tools/cli-client/help.json +721 -0
  115. package/node_modules/playwright-core/lib/tools/cli-client/minimist.js +128 -0
  116. package/node_modules/playwright-core/lib/tools/cli-client/output.js +351 -0
  117. package/node_modules/playwright-core/lib/tools/cli-client/program.js +418 -0
  118. package/node_modules/playwright-core/lib/tools/cli-client/registry.js +180 -0
  119. package/node_modules/playwright-core/lib/tools/cli-client/session.js +259 -0
  120. package/node_modules/playwright-core/lib/tools/dashboard/appIcon.png +0 -0
  121. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md +425 -0
  122. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/element-attributes.md +23 -0
  123. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/playwright-tests.md +39 -0
  124. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/request-mocking.md +87 -0
  125. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md +241 -0
  126. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md +225 -0
  127. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/storage-state.md +275 -0
  128. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md +433 -0
  129. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/tracing.md +139 -0
  130. package/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md +143 -0
  131. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +136 -0
  132. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
  133. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +89 -0
  134. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +69 -0
  135. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/typing.md +173 -0
  136. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +77 -0
  137. package/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md +174 -0
  138. package/node_modules/playwright-core/lib/tools/utils/extension.js +112 -0
  139. package/node_modules/playwright-core/lib/tools/utils/socketConnection.js +108 -0
  140. package/node_modules/playwright-core/lib/utilsBundle.js +85357 -0
  141. package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +1890 -0
  142. package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule--QdMvsKi.css +1 -0
  143. package/node_modules/playwright-core/lib/vite/dashboard/assets/codeMirrorModule-CQ8RZGBm.js +32 -0
  144. package/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
  145. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
  146. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
  147. package/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
  148. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-AwgwcYie.css +1 -0
  149. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-hRvO4yU2.js +12 -0
  150. package/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
  151. package/node_modules/playwright-core/lib/vite/dashboard/index.html +29 -0
  152. package/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg +24 -0
  153. package/node_modules/playwright-core/lib/vite/htmlReport/index.html +16 -0
  154. package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -0
  155. package/node_modules/playwright-core/lib/vite/htmlReport/report.js +32 -0
  156. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
  157. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CVQWJZAA.js +32 -0
  158. package/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
  159. package/node_modules/playwright-core/lib/vite/recorder/assets/index-BZpYJZ-P.css +1 -0
  160. package/node_modules/playwright-core/lib/vite/recorder/assets/index-hrlLqLtq.js +129 -0
  161. package/node_modules/playwright-core/lib/vite/recorder/index.html +29 -0
  162. package/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg +9 -0
  163. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-BbkfBe3n.js +32 -0
  164. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Ds6CBOo0.js +182 -0
  165. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
  166. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-DywYcAf8.js +7 -0
  167. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
  168. package/node_modules/playwright-core/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
  169. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.Bqk9acqE.css +1 -0
  170. package/node_modules/playwright-core/lib/vite/traceViewer/index.B4cLoZK3.js +1 -0
  171. package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
  172. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +44 -0
  173. package/node_modules/playwright-core/lib/vite/traceViewer/manifest.webmanifest +16 -0
  174. package/node_modules/playwright-core/lib/vite/traceViewer/playwright-logo.svg +9 -0
  175. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
  176. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +10 -0
  177. package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +4 -0
  178. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CU5KtEkS.js +5 -0
  179. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CyMfwkXJ.css +1 -0
  180. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +18 -0
  181. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
  182. package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
  183. package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  184. package/node_modules/playwright-core/lib/xdg-open +1267 -0
  185. package/node_modules/playwright-core/package.json +34 -0
  186. package/node_modules/playwright-core/types/protocol.d.ts +24645 -0
  187. package/node_modules/playwright-core/types/structs.d.ts +52 -0
  188. package/node_modules/playwright-core/types/types.d.ts +26632 -0
  189. package/package.json +12 -2
  190. package/plugins/devflow-core/extensions/devflow-core.ts +518 -40
  191. package/plugins/devflow-core/runtime/hooks/context_sync_gate.py +3 -3
  192. package/plugins/devflow-core/runtime/hooks/hook_common.py +334 -11
  193. package/plugins/devflow-core/runtime/hooks/index_gate.py +29 -16
  194. package/plugins/devflow-core/runtime/hooks/start.py +13 -13
  195. package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +2 -2
  196. package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +4 -4
  197. package/plugins/devflow-core/skills/workflow-orchestrator/SKILL.md +19 -19
  198. package/plugins/devflow-core/skills/workflow-orchestrator/agents/openai.yaml +2 -2
  199. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +412 -76
  200. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +60 -9
  201. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/api_test_matrix.py +2 -2
  202. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_pins.py +433 -0
  203. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/approval_store.py +137 -0
  204. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_assessment.rb +1 -1
  205. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_brief.rb +1 -1
  206. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_feasibility.rb +1 -1
  207. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_invalidation.rb +5 -5
  208. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_commit.rb +4 -4
  209. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_model_validate.py +1 -1
  210. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/business_source_resolver.rb +4 -4
  211. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_adapter.rb +7 -7
  212. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/control_io.rb +51 -7
  213. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/cross_track_consistency_check.rb +1 -1
  214. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_coverage.py +87 -0
  215. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/dsl_v1.py +318 -0
  216. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/environment_profile.py +134 -0
  217. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +7 -6
  218. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_surface.py +138 -0
  219. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +3 -3
  220. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +5 -5
  221. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +5 -5
  222. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +13 -13
  223. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +3 -3
  224. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/manage_lessons.rb +7 -7
  225. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_recover.rb +2 -2
  226. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/parallel_run.rb +3 -3
  227. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/phase_ownership.rb +6 -6
  228. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/playwright_adapter.py +515 -0
  229. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/repair_control_plane.rb +1 -1
  230. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_coverage.rb +1 -1
  231. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +3 -3
  232. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_preflight.py +3 -3
  233. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_run.py +18 -18
  234. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/runtime_context_collector.rb +3 -3
  235. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/session_rollover.rb +4 -4
  236. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/static_runtime_context_collector.rb +2 -2
  237. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/sync_artifacts.rb +5 -5
  238. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tc_hash.py +132 -0
  239. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/tdd_evidence.py +3 -3
  240. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_command_policy.py +104 -10
  241. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/test_task_binding_check.py +3 -3
  242. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/update_triage_route.rb +3 -3
  243. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +19 -6
  244. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_commit.rb +5 -5
  245. package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_validate.py +1 -1
  246. package/plugins/devflow-core/skills/workflow-orchestrator/templates/acceptance-remediation.yaml +12 -12
  247. package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +201 -222
  248. package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +6 -4
  249. package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +68 -140
  250. package/plugins/devflow-core/skills/workflow-project-context/SKILL.md +14 -14
  251. package/plugins/devflow-core/skills/workflow-project-context/assets/agents-md-template.md +7 -7
  252. package/plugins/devflow-core/skills/workflow-project-context/assets/codebase-backend-spec-template.md +4 -4
  253. package/plugins/devflow-core/skills/workflow-project-context/assets/codebase-frontend-spec-template.md +4 -4
  254. package/plugins/devflow-core/skills/workflow-project-context/assets/codebase-navigation-template.md +1 -1
  255. package/plugins/devflow-core/skills/workflow-project-context/assets/context-domain-index-template.md +4 -4
  256. package/plugins/devflow-core/skills/workflow-project-context/assets/context-spec-index-template.md +10 -10
  257. package/plugins/devflow-core/skills/workflow-project-context/assets/execution-contract-template.md +2 -2
  258. package/plugins/devflow-core/skills/workflow-project-context/assets/navigation-index-template.md +18 -18
  259. package/plugins/devflow-core/skills/workflow-project-context/assets/project-initialization-template.md +32 -32
  260. package/plugins/devflow-core/skills/workflow-project-context/assets/rules-index-template.md +30 -30
  261. package/plugins/devflow-core/skills/workflow-project-context/assets/state-template.md +7 -7
  262. package/plugins/devflow-core/skills/workflow-project-context/references/navigation-guide.md +37 -37
  263. package/plugins/devflow-core/skills/workflow-project-context/scripts/validate_initialization.py +16 -16
  264. package/plugins/devflow-core/skills/workflow-project-context/scripts/validate_initialization_database.py +1 -1
  265. package/plugins/devflow-design/skills/workflow-api-call-guide/SKILL.md +4 -4
  266. package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +8 -8
  267. package/plugins/devflow-design/skills/workflow-api-supplement/SKILL.md +5 -5
  268. package/plugins/devflow-design/skills/workflow-architecture-feasibility/SKILL.md +1 -1
  269. package/plugins/devflow-design/skills/workflow-backend-tech-spec/SKILL.md +39 -39
  270. package/plugins/devflow-design/skills/workflow-backend-tech-spec/assets/backend-spec-template.md +10 -10
  271. package/plugins/devflow-design/skills/workflow-backend-tech-spec/references/affected-module-exploration.md +2 -2
  272. package/plugins/devflow-design/skills/workflow-backend-tech-spec/references/code-explore-guide.md +1 -1
  273. package/plugins/devflow-design/skills/workflow-backend-tech-spec/references/project-rule-contract.md +20 -20
  274. package/plugins/devflow-design/skills/workflow-design-rule-pack/SKILL.md +8 -8
  275. package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/index.md +3 -3
  276. package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/accessibility.md +2 -2
  277. package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/component-library.md +2 -2
  278. package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/design-systems.md +1 -1
  279. package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/responsive-grid.md +2 -2
  280. package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/stack-guidelines.md +6 -6
  281. package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/recommendations/ux-patterns.md +2 -2
  282. package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/tokens/project-tokens.md +1 -1
  283. package/plugins/devflow-design/skills/workflow-design-rule-pack/assets/design/tools/ui-ux-pro-max/SKILL.md +12 -12
  284. package/plugins/devflow-design/skills/workflow-design-rule-pack/scripts/install.py +2 -2
  285. package/plugins/devflow-design/skills/workflow-feature-breakdown/SKILL.md +30 -30
  286. package/plugins/devflow-design/skills/workflow-feature-breakdown/assets/output-template.md +2 -2
  287. package/plugins/devflow-design/skills/workflow-frontend-tech-spec/SKILL.md +41 -41
  288. package/plugins/devflow-design/skills/workflow-frontend-tech-spec/assets/frontend-spec-template.md +15 -15
  289. package/plugins/devflow-design/skills/workflow-frontend-tech-spec/references/affected-module-exploration.md +2 -2
  290. package/plugins/devflow-design/skills/workflow-frontend-tech-spec/references/project-rule-contract.md +25 -25
  291. package/plugins/devflow-design/skills/workflow-integration-verify/SKILL.md +13 -13
  292. package/plugins/devflow-design/skills/workflow-ui-spec/SKILL.md +23 -23
  293. package/plugins/devflow-design/skills/workflow-ui-spec/assets/ui-spec-template.md +3 -3
  294. package/plugins/devflow-implementation/agents/implementation-worker.md +2 -2
  295. package/plugins/devflow-implementation/skills/workflow-bug-fix/SKILL.md +9 -9
  296. package/plugins/devflow-implementation/skills/workflow-bug-fix/references/lesson-format.md +2 -2
  297. package/plugins/devflow-implementation/skills/workflow-bug-fix/references/root-cause-ladder.md +1 -1
  298. package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +17 -17
  299. package/plugins/devflow-implementation/skills/workflow-change-plan/references/code-conventions.md +1 -1
  300. package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +3 -3
  301. package/plugins/devflow-implementation/skills/workflow-change-plan/references/project-rule-contract.md +16 -16
  302. package/plugins/devflow-implementation/skills/workflow-execute-phase/SKILL.md +38 -38
  303. package/plugins/devflow-implementation/skills/workflow-execute-phase/references/execute-phase.md +4 -2
  304. package/plugins/devflow-intel/agents/project-scout.md +1 -1
  305. package/plugins/devflow-intel/extensions/devflow-intel.ts +20 -19
  306. package/plugins/devflow-intel/skills/code-index-query/SKILL.md +6 -6
  307. package/plugins/devflow-intel/skills/workflow-code-index/SKILL.md +12 -12
  308. package/plugins/devflow-intel/skills/workflow-code-index/references/discovery.md +2 -2
  309. package/plugins/devflow-intel/skills/workflow-code-index/references/query-guide.md +1 -1
  310. package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +31 -12
  311. package/plugins/devflow-intel/skills/workflow-code-index/scripts/core/discovery.py +2 -2
  312. package/plugins/devflow-intel/skills/workflow-code-index/scripts/core/scanner.py +4 -1
  313. package/plugins/devflow-intel/skills/workflow-code-index/scripts/validators/project_binding_check.py +4 -4
  314. package/plugins/devflow-intel/skills/workflow-context-sync/SKILL.md +14 -14
  315. package/plugins/devflow-intel/skills/workflow-context-sync/scripts/context_sync_cursor.py +2 -2
  316. package/plugins/devflow-intel/skills/workflow-update-navigation/SKILL.md +26 -26
  317. package/plugins/devflow-intel/skills/workflow-update-navigation/references/incremental-update-guide.md +2 -2
  318. package/plugins/devflow-quality/runtime/playwright/devflow-test-dsl-v1.schema.json +872 -0
  319. package/plugins/devflow-quality/runtime/playwright/playwright.config.mjs +46 -0
  320. package/plugins/devflow-quality/runtime/playwright/reporter.mjs +311 -0
  321. package/plugins/devflow-quality/runtime/playwright/runner.spec.mjs +1308 -0
  322. package/plugins/devflow-quality/runtime/playwright/runtime-manifest.json +11 -0
  323. package/plugins/devflow-quality/skills/code-review/SKILL.md +12 -12
  324. package/plugins/devflow-quality/skills/code-review/references/spec-checklist.md +1 -1
  325. package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +11 -8
  326. package/plugins/devflow-quality/skills/workflow-gen-test-guide/SKILL.md +6 -6
  327. package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/SKILL.md +14 -14
  328. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/SKILL.md +82 -69
  329. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/api-mainflow-rules.md +20 -15
  330. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/codebase-discovery.md +12 -10
  331. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/intermediate-format.md +115 -192
  332. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/mainflow-selection.md +27 -23
  333. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/project-dsl-contract.md +78 -153
  334. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/references/ui-mainflow-rules.md +36 -83
  335. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/compile_dsl.py +497 -556
  336. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/discover_codebases.py +2 -2
  337. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/export_legacy_testagent.py +647 -0
  338. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/normalize_case_payloads.py +10 -3
  339. package/plugins/devflow-quality/skills/workflow-generate-mainflow-cases/scripts/validate_case_payloads.py +165 -275
  340. package/plugins/devflow-quality/skills/workflow-human-acceptance/SKILL.md +61 -0
  341. package/plugins/devflow-quality/skills/workflow-release-closeout/SKILL.md +18 -18
  342. package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +20 -14
  343. package/plugins/devflow-requirements/skills/requirement-feasibility-review/SKILL.md +1 -1
  344. package/plugins/devflow-requirements/skills/workflow-demand-clarify/SKILL.md +10 -10
  345. package/plugins/devflow-requirements/skills/workflow-domain-modeling/SKILL.md +25 -25
  346. package/plugins/devflow-requirements/skills/workflow-requirement-baseline/SKILL.md +49 -49
  347. package/plugins/devflow-requirements/skills/workflow-requirement-baseline/assets/baseline-template.md +22 -22
  348. package/plugins/devflow-requirements/skills/workflow-requirement-baseline/references/antipatterns.md +3 -3
  349. package/plugins/devflow-requirements/skills/workflow-requirement-baseline/references/challenge-review-guide.md +3 -3
  350. package/plugins/devflow-requirements/skills/workflow-requirement-baseline/references/project-context-guide.md +11 -11
  351. package/plugins/devflow-requirements/skills/workflow-requirement-baseline/references/questioning.md +1 -1
  352. package/plugins/devflow-requirements/skills/workflow-requirement-baseline/references/requirement-review-checklist.md +4 -4
  353. package/plugins/devflow-requirements/skills/workflow-requirement-triage/SKILL.md +16 -16
  354. package/plugins/devflow-requirements/skills/workflow-wayfinder/SKILL.md +6 -6
  355. package/src/approval-store.ts +340 -0
  356. package/src/cli.ts +15 -0
  357. package/src/constants.ts +1 -1
  358. package/src/doctor.ts +259 -2
  359. package/src/initialization-store.ts +51 -3
  360. package/src/paths.ts +12 -1
  361. package/src/project-context.ts +60 -15
  362. package/src/project-documents.ts +61 -61
  363. package/src/project-initializer.ts +9 -8
  364. package/src/project-layout.ts +217 -0
  365. package/src/runtime.ts +3 -3
  366. package/templates/commands/start.md +4 -4
  367. package/templates/omp/AGENTS.md +1 -1
  368. package/templates/omp/hook-config.json +13 -7
  369. package/templates/omp/index-policy.json +8 -8
@@ -1,627 +1,568 @@
1
1
  #!/usr/bin/env python3
2
- """Compile AI-friendly case descriptions into strict DSL JSON.
3
-
4
- AI 产出简化的用例描述(locator 用 "type:value" 字符串格式),
5
- 本脚本负责编译成符合系统 DslTarget 严格格式的 DSL JSON。
6
- 这是硬约束层 —— 所有 target key、strategy、assertion 由脚本决定,AI 无法编造。
2
+ """Compile AI-friendly case descriptions into devflow-test-dsl/v1 files.
3
+
4
+ Input: ``.devflow/planning/generated-cases/<feature>/case-descriptions.json``
5
+ (format: ``references/intermediate-format.md``).
6
+ Output: one DSL file per frozen TC at
7
+ ``.devflow/planning/generated-cases/<feature>/TC-xxxx.json``.
8
+
9
+ This script is the hard-constraint layer between AI judgement and the gate:
10
+
11
+ - every case must reference a frozen, confirmed, ``automated`` TC whose
12
+ ``execution_surface`` is ``ui`` or ``api_flow`` (legacy page TCs derive to
13
+ ``ui`` through the single Core implementation ``execution_surface.derive``);
14
+ - ``tc_sha256`` is always computed here with Core ``tc_hash`` (never copied
15
+ from the AI input);
16
+ - locators are compiled to v1 targets, ``testid`` first; ``xpath``, ``near``,
17
+ ``ai_only``, Midscene, prompts, scripts and token injection are rejected;
18
+ - every generated document is validated with Core ``dsl_v1`` (Schema +
19
+ semantics) and ``dsl_coverage`` (``covers`` structural coverage) and nothing
20
+ is written unless every case passes.
21
+
22
+ Output bytes are deterministic (no timestamps), so re-compiling an unchanged
23
+ description keeps ``dsl_sha256`` and therefore an existing execution_plan
24
+ approval valid. The legacy TestAgent import payload lives in
25
+ ``export_legacy_testagent.py`` and is not part of the gate.
7
26
  """
8
-
9
27
  from __future__ import annotations
10
28
 
11
29
  import argparse
12
30
  import json
31
+ import os
13
32
  import re
14
- from copy import deepcopy
15
- from datetime import datetime, timezone
33
+ import sys
16
34
  from pathlib import Path
17
35
  from typing import Any
18
36
 
19
- # ---------------------------------------------------------------------------
20
- # 硬约束常量 — 唯一权威定义
21
- # ---------------------------------------------------------------------------
22
-
23
- VALID_TARGET_KEYS = {"css", "xpath", "label", "placeholder", "role", "name", "near_text", "exact"}
24
- EXACT_LOCATOR_KEYS = {"css", "xpath", "role"} # 精确匹配,必须有 source evidence
25
- SEMANTIC_LOCATOR_KEYS = {"label", "placeholder", "near_text"} # 语义匹配,需检查唯一性
26
37
 
27
- # 高风险 label — 表格行常见操作文本,用 label 定位时几乎一定不唯一
28
- HIGH_RISK_LABELS = {
29
- "编辑", "删除", "查看", "详情", "启用", "禁用", "停用",
30
- "新增", "添加", "修改", "复制", "移动", "导出", "导入", "下载",
31
- "上传", "设置", "配置", "操作", "更多", "取消", "确定", "保存",
32
- "提交", "重置", "刷新", "搜索", "查询", "筛选", "排序",
38
+ def _core_scripts() -> Path:
39
+ candidates = [Path(__file__).resolve().parents[4] / "devflow-core" / "skills" / "workflow-orchestrator" / "scripts"]
40
+ configured = os.environ.get("DEVFLOW_KIT_ROOT")
41
+ if configured:
42
+ candidates.append(Path(configured).expanduser() / "workflow-orchestrator" / "scripts")
43
+ for candidate in candidates:
44
+ if (candidate / "dsl_v1.py").is_file():
45
+ return candidate.resolve()
46
+ raise RuntimeError("Devflow Core scripts (dsl_v1.py) not found")
47
+
48
+
49
+ sys.path.insert(0, str(_core_scripts()))
50
+ from dsl_coverage import check as coverage_check # noqa: E402
51
+ from dsl_v1 import SCHEMA_VERSION, validate as validate_dsl # noqa: E402
52
+ from execution_surface import derive as derive_surface # noqa: E402
53
+ from tc_hash import tc_sha256 # noqa: E402
54
+
55
+
56
+ FEATURE_RE = re.compile(r"^[^/\\\x00-\x1f\x7f]+$")
57
+ TC_ID_RE = re.compile(r"^TC-[0-9]{4,}$")
58
+ GENERATABLE_SURFACES = frozenset({"ui", "api_flow"})
59
+
60
+ UI_ACTIONS = frozenset({"goto", "click", "fill", "select", "check", "uncheck", "wait_for",
61
+ "assert_visible", "assert_text", "assert_url", "screenshot"})
62
+ TARGET_ACTIONS = UI_ACTIONS - {"goto", "assert_url", "screenshot"}
63
+ HTTP_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE")
64
+ API_SHORTHAND = {method.lower(): method for method in HTTP_METHODS}
65
+
66
+ COMMON_KEYS = {"id", "name", "action", "covers", "timeout_ms"}
67
+ UI_KEYS = {
68
+ "goto": {"url"},
69
+ "click": {"locator", "exact"},
70
+ "fill": {"locator", "exact", "value"},
71
+ "select": {"locator", "exact", "value"},
72
+ "check": {"locator", "exact"},
73
+ "uncheck": {"locator", "exact"},
74
+ "wait_for": {"locator", "exact", "state"},
75
+ "assert_visible": {"locator", "exact"},
76
+ "assert_text": {"locator", "exact", "expected", "match", "normalize_whitespace", "case_sensitive"},
77
+ "assert_url": {"expected", "match"},
78
+ "screenshot": {"full_page"},
33
79
  }
80
+ API_KEYS = {"method", "url", "headers", "query", "json", "expect_status", "assertions", "extract", "retry", "idempotent"}
81
+ CASE_KEYS = {"tc_id", "name", "description", "variables", "data", "steps", "source_evidence", "tags"}
82
+ DATA_KEYS = {"namespace", "owner", "accounts", "repeatable", "setup", "cleanup"}
83
+
84
+ # Keys and values of the retired TestAgent/Midscene format. They are rejected
85
+ # with a pointed message instead of being silently dropped.
86
+ FORBIDDEN_KEYS = {
87
+ "strategy": "strategies (structured_first/ai_only/Midscene) do not exist in devflow-test-dsl/v1",
88
+ "prompt": "AI prompts are not executable in the gate; write a deterministic assert_* step",
89
+ "scope": "row/dialog scopes are not supported; use a data-testid or another unique locator",
90
+ "auth": "auth shortcuts are not supported; write explicit data.setup API steps or UI login steps",
91
+ "script": "scripts are not allowed in the DSL",
92
+ "params": "legacy params{} is not supported; put url/json/query/headers directly on the step",
93
+ "extract_vars": "use extract: {name: \"/json/pointer\"}",
94
+ "depends_on": "steps run in order; depends_on is not supported",
95
+ "on_failure": "a failed step always stops the case",
96
+ "driver": "the compiler derives driver from action",
97
+ "target": "write locator: \"testid:...\" (the compiler builds the target)",
98
+ }
99
+ FORBIDDEN_ACTIONS = {
100
+ "verify": "use assert_text / assert_visible / assert_url with a deterministic expectation",
101
+ "act": "ai_only actions are not allowed; use a deterministic UI action",
102
+ "navigate": "use goto",
103
+ "wait": "fixed or load waits are not allowed; use wait_for with a locator and state",
104
+ "inject_token": "token injection scripts are not allowed; log in via UI or data.setup API steps",
105
+ "evaluate_js": "JavaScript is not allowed in the DSL",
106
+ "hover": "hover is not in devflow-test-dsl/v1",
107
+ "assert_url_contains": "use assert_url with match: \"contains\"",
108
+ "assert_any_visible": "use assert_visible on one unique target",
109
+ }
110
+ FORBIDDEN_TEXT_RE = re.compile(r"\b(ai_only|midscene)\b", re.I)
34
111
 
112
+ LOCATOR_TYPES = ("testid", "role", "label", "placeholder", "text", "css")
113
+ REJECTED_LOCATOR_TYPES = {
114
+ "xpath": "XPath is not supported in devflow-test-dsl/v1; use testid/role/label/placeholder/text/css",
115
+ "near": "near_text is not supported; ask for a data-testid or use label/placeholder",
116
+ }
117
+ # Row-repeated action texts: with a non-testid locator these almost always
118
+ # match several elements, which fails under strict mode.
119
+ HIGH_RISK_TEXTS = {
120
+ "编辑", "删除", "查看", "详情", "启用", "禁用", "停用", "新增", "添加", "修改", "复制", "移动", "导出", "导入", "下载",
121
+ "上传", "设置", "配置", "操作", "更多", "取消", "确定", "保存", "提交", "重置", "刷新", "搜索", "查询", "筛选", "排序",
122
+ }
35
123
  MIN_CSS_LENGTH = 10
36
-
37
- DEFAULT_UI_STRATEGY = {"mode": "structured_first", "fallback": "midscene"}
38
- AI_ONLY_STRATEGY = {"mode": "ai_only"}
39
-
40
- # ---------------------------------------------------------------------------
41
- # Locator 字符串解析器
42
- # ---------------------------------------------------------------------------
43
-
44
- LOCATOR_RE = re.compile(
45
- r"^(?P<type>text|placeholder|css|xpath|role|near)"
46
- r":(?P<value>.+)$"
47
- )
48
- ROLE_LOCATOR_RE = re.compile(r"^role:(?P<role>[^:]+):(?P<name>.+)$")
49
- SCOPE_RE = re.compile(r"^row:(?P<row>\d+)$")
50
-
51
-
52
- def parse_locator(raw: str) -> dict[str, str]:
53
- """Parse 'type:value' string into flat target dict.
54
-
55
- Returns {'label': '...'} for text:, {'placeholder': '...'} for placeholder:, etc.
56
- Raises ValueError on unrecognized format.
57
- """
58
- raw = raw.strip()
59
-
60
- # role:button:确定
61
- m = ROLE_LOCATOR_RE.match(raw)
62
- if m:
63
- return {"role": m.group("role"), "name": m.group("name")}
64
-
65
- m = LOCATOR_RE.match(raw)
66
- if not m:
67
- raise ValueError(
68
- f"Invalid locator format: '{raw}'. "
69
- f"Expected: text:<text> | placeholder:<text> | css:<selector> | "
70
- f"xpath:<expr> | role:<role>:<name> | near:<text>"
71
- )
72
-
73
- loc_type = m.group("type")
74
- value = m.group("value").strip()
75
-
76
- if not value:
77
- raise ValueError(f"Locator '{raw}' has empty value.")
78
-
79
- if loc_type == "text":
80
- return {"label": value}
81
- if loc_type == "placeholder":
82
- return {"placeholder": value}
83
- if loc_type == "css":
84
- return {"css": value}
85
- if loc_type == "xpath":
86
- return {"xpath": value}
87
- if loc_type == "near":
88
- return {"near_text": value}
89
-
90
- raise ValueError(f"Unknown locator type: {loc_type}")
91
-
92
-
93
- # ---------------------------------------------------------------------------
94
- # CSS 编造检测 — 零容忍
95
- # ---------------------------------------------------------------------------
96
-
97
- # 明显是 AI 编造的 CSS class 特征
98
124
  HALLUCINATED_CSS_PATTERNS: list[tuple[str, str]] = [
99
- # 包含 % 的伪 Tailwind class(w-100%, h-50% 等)
100
- (r"\b[a-z]+-\d+%", "contains invented utility class with % (e.g. w-100%)"),
101
- # 裸的百分比值
102
- (r"%", "contains '%' — not valid in CSS selector without escaping"),
103
- # 纯表单容器通用类,不指定具体字段
104
- (r"^\.el-form-item(__content)?$", "matches all form items, not a specific field"),
105
- # 明显编造的随机 class 名模式
125
+ (r"%", "contains '%' (invented utility class such as w-100%)"),
126
+ (r"^\.el-form-item(__content)?$", "matches every form item, not a specific field"),
106
127
  (r"\b[a-z]{1,3}-\d{3,}\b", "looks like a made-up class (random letters+numbers)"),
128
+ (r"^(tr|td|li|div|span|button)?:nth-(child|of-type)\(", "position-only selector is not stable"),
107
129
  ]
130
+ GENERIC_FORM_CLASSES = (".el-form-item", ".el-form-item__content", ".el-form-item__label", ".ant-form-item",
131
+ ".ant-form-item-control", ".form-item", ".form-group")
108
132
 
109
- # fill/select 动作在非 placeholder 情况下,CSS 禁止匹配纯容器类
110
- GENERIC_FORM_CLASSES = [
111
- ".el-form-item",
112
- ".el-form-item__content",
113
- ".el-form-item__label",
114
- ".ant-form-item",
115
- ".ant-form-item-control",
116
- ".form-item",
117
- ".form-group",
118
- ]
119
133
 
120
-
121
- def _validate_css_not_hallucinated(css_value: str, step_name: str) -> None:
122
- """Reject CSS selectors that show signs of AI hallucination. Hard gate."""
123
- errors: list[str] = []
124
-
125
- # Check against hallucination patterns
126
- for pattern, description in HALLUCINATED_CSS_PATTERNS:
127
- if re.search(pattern, css_value):
128
- errors.append(
129
- f"CSS selector '{css_value}' rejected: {description}. "
130
- f"Step: {step_name}"
131
- )
132
-
133
- # Check if CSS is purely generic form classes (no specific target)
134
- stripped = css_value.strip()
135
- parts = [p.strip() for p in stripped.split() if p.strip()]
136
- if parts and all(
137
- any(p == g or p.startswith(g) for g in GENERIC_FORM_CLASSES)
138
- for p in parts
139
- ):
140
- errors.append(
141
- f"CSS selector '{css_value}' only matches generic form containers, "
142
- f"not a specific field. Use 'near:<label文本>' instead. "
143
- f"Step: {step_name}"
144
- )
145
-
146
- # Too short = probably made up
147
- if len(css_value) < MIN_CSS_LENGTH:
148
- errors.append(
149
- f"CSS selector '{css_value}' is too generic ({len(css_value)} chars, min {MIN_CSS_LENGTH}). "
150
- f"Step: {step_name}"
151
- )
152
-
153
- if errors:
154
- raise ValueError("CSS HALLUCINATION DETECTED:\n" + "\n".join(f" - {e}" for e in errors))
134
+ class CompileError(ValueError):
135
+ pass
155
136
 
156
137
 
157
138
  # ---------------------------------------------------------------------------
158
- # Fill 动作 locator 硬决策链
139
+ # Suite
159
140
  # ---------------------------------------------------------------------------
160
141
 
161
- FILL_ALLOWED_LOCATORS = {"placeholder", "near"} # fill 只能用这两个
162
-
142
+ def current_feature(root: Path) -> str | None:
143
+ state = root / ".devflow/planning" / "STATE.md"
144
+ if not state.is_file():
145
+ return None
146
+ match = re.search(r"^current_feature:\s*['\"]?([^'\"\n]+)", state.read_text(encoding="utf-8"), re.MULTILINE)
147
+ return match.group(1).strip() if match else None
163
148
 
164
- def _validate_fill_locator(step_name: str, locator_raw: str) -> None:
165
- """Hard decision chain for fill actions: placeholder → near → ai_only. CSS/xpath forbidden."""
166
- if not locator_raw:
167
- raise ValueError(
168
- f"Fill step '{step_name}' has no locator. "
169
- f"Use 'placeholder:<text>', 'near:<label文本>', or change action to 'act' with ai_only."
170
- )
171
149
 
172
- loc_type = locator_raw.split(":", 1)[0].strip()
173
- if loc_type not in FILL_ALLOWED_LOCATORS:
174
- raise ValueError(
175
- f"Fill step '{step_name}' uses locator type '{loc_type}', which is forbidden for fill actions. "
176
- f"Fill hard chain: placeholder → near → ai_only. "
177
- f"Got '{locator_raw}'. "
178
- f"Use 'placeholder:<text>' if the field has a placeholder, "
179
- f"'near:<label文本>' if there's a label next to the field, "
180
- f"or change action to 'act' with ai_only strategy."
181
- )
150
+ def load_suite(path: Path) -> dict[str, Any]:
151
+ try:
152
+ suite = json.loads(path.read_text(encoding="utf-8"))
153
+ except FileNotFoundError as error:
154
+ raise CompileError(f"frozen TC suite not found: {path}") from error
155
+ except json.JSONDecodeError as error:
156
+ raise CompileError(f"frozen TC suite is not valid JSON: {error}") from error
157
+ if not isinstance(suite, dict) or not isinstance(suite.get("nodes"), list):
158
+ raise CompileError("TC suite must be an object with nodes[]")
159
+ if suite.get("frozen") is not True:
160
+ raise CompileError("TC suite is not frozen: DSL can only be generated for a CP-3 frozen suite")
161
+ return suite
162
+
163
+
164
+ def eligible_cases(suite: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], dict[str, str]]:
165
+ """Return ({tc_id: data} generatable, {tc_id: reason} for known but ineligible TCs)."""
166
+ eligible: dict[str, dict[str, Any]] = {}
167
+ rejected: dict[str, str] = {}
168
+ for node in suite["nodes"]:
169
+ if not isinstance(node, dict) or node.get("kind") != "test_case" or not isinstance(node.get("id"), str):
170
+ continue
171
+ tc_id, data = node["id"], node.get("data")
172
+ if not isinstance(data, dict):
173
+ rejected[tc_id] = "has no data"
174
+ elif node.get("status") != "confirmed":
175
+ rejected[tc_id] = f"status is {node.get('status')!r}, not confirmed"
176
+ elif data.get("automation_mode") != "automated":
177
+ rejected[tc_id] = f"automation_mode is {data.get('automation_mode')!r}; only automated TCs get a DSL"
178
+ else:
179
+ surface, legacy = derive_surface(data)
180
+ if surface not in GENERATABLE_SURFACES:
181
+ suffix = " (legacy_unclassified)" if legacy else ""
182
+ rejected[tc_id] = f"execution_surface is {surface!r}{suffix}; DSL is only generated for ui/api_flow"
183
+ else:
184
+ eligible[tc_id] = data
185
+ return eligible, rejected
182
186
 
183
187
 
184
188
  # ---------------------------------------------------------------------------
185
- # Target 编译 — 硬约束规则
189
+ # Locators
186
190
  # ---------------------------------------------------------------------------
187
191
 
188
- def compile_target(
189
- locator_raw: str,
190
- scope: str | None = None,
191
- step_name: str = "",
192
- ) -> dict[str, Any]:
193
- """Compile a locator string + scope into a valid DslTarget dict.
194
-
195
- Rules (in priority order):
196
- 1. text:<unique_button> + page scope → label + exact
197
- 2. text:<common_action> + row:N scope → label + css row selector
198
- 3. text:<text> + dialog scope → label (dialog内默认唯一)
199
- 4. placeholder:<text> → placeholder
200
- 5. css:<selector> → css (validated for specificity)
201
- 6. role:<role>:<name> → role + name
202
- 7. near:<text> → near_text
203
- 8. xpath:<expr> → xpath (last resort)
204
- """
205
- target = parse_locator(locator_raw)
206
- errors: list[str] = []
207
-
208
- # text: → label
209
- if "label" in target:
210
- label_value = target["label"]
211
-
212
- # Determine scope
213
- if scope:
214
- scope_match = SCOPE_RE.match(scope)
215
- if scope_match:
216
- # row scope: label + css row selector
217
- row_n = int(scope_match.group("row"))
218
- target["css"] = (
219
- f".el-table__body tr:nth-child({row_n}) "
220
- f"td:last-child .el-button--small"
221
- )
222
- elif scope == "dialog":
223
- # dialog scope: label within dialog, no extra css needed
224
- pass
225
- # other scopes just use label as-is
226
-
227
- # exact match for page-unique items
228
- if label_value not in HIGH_RISK_LABELS:
229
- target["exact"] = True
230
-
231
- # High-risk label without row scope → warn but don't block
232
- if label_value in HIGH_RISK_LABELS and not (scope and SCOPE_RE.match(scope)):
233
- # Still produce valid target, but record warning
234
- pass
235
-
236
- # css: → hard hallucination check
237
- if "css" in target:
238
- css_value = target["css"]
239
- _validate_css_not_hallucinated(css_value, step_name)
240
-
241
- # role: → ensure name is present (already handled by parse_locator)
242
-
243
- if errors:
244
- raise ValueError("\n".join(errors))
245
-
192
+ def _css_errors(css: str) -> list[str]:
193
+ errors = [f"css '{css}' rejected: {reason}" for pattern, reason in HALLUCINATED_CSS_PATTERNS if re.search(pattern, css)]
194
+ parts = [part for part in css.split() if part]
195
+ if parts and all(any(part.startswith(generic) for generic in GENERIC_FORM_CLASSES) for part in parts):
196
+ errors.append(f"css '{css}' only matches generic form containers, not a specific field")
197
+ if len(css) < MIN_CSS_LENGTH:
198
+ errors.append(f"css '{css}' is too generic ({len(css)} chars, min {MIN_CSS_LENGTH})")
199
+ return errors
200
+
201
+
202
+ def compile_target(raw: Any, exact: Any, label: str, warnings: list[str]) -> dict[str, Any]:
203
+ """'type:value' → one v1 target. testid first; role/label/placeholder/text default to exact."""
204
+ if not isinstance(raw, str) or ":" not in raw:
205
+ raise CompileError(f"{label}.locator must be 'testid:<id>' | 'role:<role>:<name>' | 'label:<text>' | "
206
+ "'placeholder:<text>' | 'text:<text>' | 'css:<selector>'")
207
+ kind, _, value = raw.strip().partition(":")
208
+ kind = kind.strip().lower()
209
+ if kind in REJECTED_LOCATOR_TYPES:
210
+ raise CompileError(f"{label}.locator: {REJECTED_LOCATOR_TYPES[kind]}")
211
+ if kind not in LOCATOR_TYPES:
212
+ raise CompileError(f"{label}.locator type '{kind}' is unknown; use one of {', '.join(LOCATOR_TYPES)}")
213
+ if exact is not None and not isinstance(exact, bool):
214
+ raise CompileError(f"{label}.exact must be a boolean")
215
+ if kind == "testid":
216
+ if exact is not None:
217
+ raise CompileError(f"{label}.exact does not apply to testid locators")
218
+ if not value.strip():
219
+ raise CompileError(f"{label}.locator has an empty value")
220
+ return {"testid": value.strip()}
221
+ if kind == "css":
222
+ if exact is not None:
223
+ raise CompileError(f"{label}.exact does not apply to css locators")
224
+ errors = _css_errors(value.strip())
225
+ if errors:
226
+ raise CompileError(f"{label}: " + "; ".join(errors))
227
+ warnings.append(f"{label} uses a css locator; prefer a data-testid and cite the selector source in source-map.md")
228
+ return {"css": value.strip()}
229
+ if kind == "role":
230
+ role, _, name = value.partition(":")
231
+ if not re.fullmatch(r"[a-z]+", role.strip()) or not name.strip():
232
+ raise CompileError(f"{label}.locator role form is 'role:<aria-role>:<accessible name>'")
233
+ target: dict[str, Any] = {"role": role.strip(), "name": name.strip()}
234
+ text = name.strip()
235
+ else:
236
+ if not value.strip():
237
+ raise CompileError(f"{label}.locator has an empty value")
238
+ target = {kind: value.strip()}
239
+ text = value.strip()
240
+ target["exact"] = True if exact is None else exact
241
+ if text in HIGH_RISK_TEXTS:
242
+ warnings.append(f"{label} locates '{text}' by {kind}; this text usually repeats per row/dialog and fails strict mode — use a data-testid")
243
+ else:
244
+ warnings.append(f"{label} uses {kind} instead of a data-testid")
246
245
  return target
247
246
 
248
247
 
249
248
  # ---------------------------------------------------------------------------
250
- # Strategy 决策 — 硬约束
249
+ # covers
251
250
  # ---------------------------------------------------------------------------
252
251
 
253
- VERIFICATION_ACTIONS = {"verify", "assert_text", "assert_visible", "assert_url_contains", "assert_any_visible"}
252
+ def _indexes(value: str, label: str) -> list[int]:
253
+ match = re.fullmatch(r"(\d+)(?:-(\d+))?", value.strip())
254
+ if not match:
255
+ raise CompileError(f"{label}: '{value}' must be N or N-M")
256
+ start, end = int(match.group(1)), int(match.group(2) or match.group(1))
257
+ if start < 1 or end < start:
258
+ raise CompileError(f"{label}: '{value}' is not a valid 1-based range")
259
+ return list(range(start, end + 1))
254
260
 
255
261
 
256
- def compile_strategy(action: str) -> dict[str, Any]:
257
- """Decide strategy based on action type. Hard rule: verification → ai_only."""
258
- if action in VERIFICATION_ACTIONS:
259
- return deepcopy(AI_ONLY_STRATEGY)
260
- if action == "act":
261
- return deepcopy(AI_ONLY_STRATEGY)
262
- return deepcopy(DEFAULT_UI_STRATEGY)
262
+ def compile_covers(raw: Any, label: str) -> dict[str, Any] | None:
263
+ """Accept {preconditions, steps, oracle} or shorthand 'pre:1' / 'step:2-3' / 'oracle' (string or list)."""
264
+ if raw is None:
265
+ return None
266
+ if isinstance(raw, dict):
267
+ unknown = set(raw) - {"preconditions", "steps", "oracle"}
268
+ if unknown:
269
+ raise CompileError(f"{label}.covers has unknown keys {sorted(unknown)}")
270
+ covers: dict[str, Any] = {}
271
+ for key in ("preconditions", "steps"):
272
+ if key in raw:
273
+ values = raw[key]
274
+ if not isinstance(values, list) or not values or not all(isinstance(item, int) and not isinstance(item, bool) for item in values):
275
+ raise CompileError(f"{label}.covers.{key} must be a non-empty list of 1-based integers")
276
+ covers[key] = sorted(set(values))
277
+ if "oracle" in raw:
278
+ if raw["oracle"] is not True:
279
+ raise CompileError(f"{label}.covers.oracle may only be true")
280
+ covers["oracle"] = True
281
+ if not covers:
282
+ raise CompileError(f"{label}.covers is empty")
283
+ return covers
284
+ items = [raw] if isinstance(raw, str) else raw
285
+ if not isinstance(items, list) or not items or not all(isinstance(item, str) for item in items):
286
+ raise CompileError(f"{label}.covers must be an object, a string, or a list of strings")
287
+ found: dict[str, set[int]] = {"preconditions": set(), "steps": set()}
288
+ oracle = False
289
+ for item in items:
290
+ text = item.strip().lower()
291
+ if text == "oracle":
292
+ oracle = True
293
+ continue
294
+ prefix, _, rest = text.partition(":")
295
+ key = {"pre": "preconditions", "precondition": "preconditions", "preconditions": "preconditions",
296
+ "step": "steps", "steps": "steps"}.get(prefix)
297
+ if key is None or not rest:
298
+ raise CompileError(f"{label}.covers item '{item}' must be 'pre:N', 'step:N', 'step:N-M' or 'oracle'")
299
+ for part in rest.split(","):
300
+ found[key].update(_indexes(part, f"{label}.covers"))
301
+ covers = {key: sorted(values) for key, values in found.items() if values}
302
+ if oracle:
303
+ covers["oracle"] = True
304
+ return covers
263
305
 
264
306
 
265
307
  # ---------------------------------------------------------------------------
266
- # Step 编译
308
+ # Steps
267
309
  # ---------------------------------------------------------------------------
268
310
 
269
- def compile_step(step: dict[str, Any], index: int) -> dict[str, Any]:
270
- """Compile one simplified step into strict DSL step."""
271
- name = str(step.get("name") or f"Step {index}")
272
- action = str(step.get("action") or "").lower().strip()
273
-
274
- compiled: dict[str, Any] = {
275
- "id": f"s{index:02d}",
276
- "name": name,
277
- "depends_on": [],
278
- "timeout_ms": int(step.get("timeout_ms", 30_000)),
279
- "on_failure": step.get("on_failure", "stop"),
280
- }
311
+ def _check_forbidden(step: dict[str, Any], label: str) -> None:
312
+ for key, reason in FORBIDDEN_KEYS.items():
313
+ if key in step:
314
+ raise CompileError(f"{label}.{key} is not allowed: {reason}")
315
+ for key, value in step.items():
316
+ if isinstance(value, str) and FORBIDDEN_TEXT_RE.search(value):
317
+ raise CompileError(f"{label}.{key} mentions ai_only/Midscene, which is not allowed in the gate DSL")
318
+
281
319
 
282
- # --- Navigate → web/goto ---
283
- if action == "navigate":
284
- compiled["driver"] = "web"
285
- compiled["action"] = "goto"
286
- compiled["params"] = {"url": step.get("url", "")}
287
- return compiled
288
-
289
- # --- Wait ---
290
- if action == "wait":
291
- compiled["driver"] = "web"
292
- compiled["action"] = "wait"
293
- compiled["params"] = {"wait_for": step.get("wait_for", "load")}
294
- compiled["timeout_ms"] = int(step.get("timeout_ms", 15_000))
295
- return compiled
296
-
297
- # --- Inject token ---
298
- if action == "inject_token":
299
- compiled["driver"] = "web"
300
- compiled["action"] = "evaluate_js"
301
- compiled["params"] = {"script": step.get("script", "")}
302
- return compiled
303
-
304
- # --- API step ---
305
- if action in {"get", "post", "put", "patch", "delete"}:
306
- compiled["driver"] = "api"
307
- compiled["action"] = action
308
- compiled["params"] = deepcopy(step.get("params", {}))
309
- compiled["assertions"] = _compile_api_assertions(step.get("assertions", []))
310
- compiled["extract_vars"] = deepcopy(step.get("extract_vars", {}))
311
- return compiled
312
-
313
- # --- UI action steps ---
314
- if action in {"click", "fill", "select", "hover"}:
315
- compiled["driver"] = "ui"
316
- compiled["action"] = action
317
- locator_raw = step.get("locator", "")
318
- if locator_raw:
319
- compiled["target"] = compile_target(
320
- locator_raw,
321
- scope=step.get("scope"),
322
- step_name=name,
323
- )
324
- compiled["strategy"] = compile_strategy(action)
325
- if action == "fill":
326
- compiled["params"] = {"value": step.get("value", "")}
327
- _validate_fill_locator(name, locator_raw)
328
- return compiled
329
-
330
- # --- UI verification step ---
331
- if action == "verify":
332
- compiled["driver"] = "ui"
333
- compiled["action"] = "act"
334
- locator_raw = step.get("locator", "")
335
- if locator_raw:
336
- compiled["target"] = compile_target(locator_raw, scope=step.get("scope"), step_name=name)
337
- compiled["strategy"] = deepcopy(AI_ONLY_STRATEGY)
338
- expect = step.get("expect", "")
339
- compiled["prompt"] = _build_verify_prompt(name, expect, step)
340
- compiled["timeout_ms"] = int(step.get("timeout_ms", 15_000))
341
- return compiled
342
-
343
- # --- AI act step (with prompt) ---
344
- if action == "act":
345
- compiled["driver"] = "ui"
346
- compiled["action"] = "act"
347
- locator_raw = step.get("locator", "")
348
- if locator_raw:
349
- compiled["target"] = compile_target(locator_raw, scope=step.get("scope"), step_name=name)
350
- compiled["strategy"] = deepcopy(AI_ONLY_STRATEGY)
351
- compiled["prompt"] = step.get("prompt", "")
352
- compiled["timeout_ms"] = int(step.get("timeout_ms", 15_000))
353
- return compiled
354
-
355
- # Fallback
356
- raise ValueError(f"Unknown action '{action}' in step '{name}'")
357
-
358
-
359
- def _compile_api_assertions(raw_assertions: list[dict[str, Any]]) -> list[dict[str, Any]]:
360
- """Compile simplified API assertions into strict format."""
361
- compiled: list[dict[str, Any]] = []
362
- for a in raw_assertions:
363
- atype = str(a.get("type", "status_code"))
364
- compiled.append({
365
- "type": atype,
366
- "expected": a.get("expected"),
367
- "path": a.get("path"),
368
- })
320
+ def _base(step: dict[str, Any], default_id: str, label: str) -> dict[str, Any]:
321
+ step_id = step.get("id", default_id)
322
+ compiled: dict[str, Any] = {"id": step_id}
369
323
  return compiled
370
324
 
371
325
 
372
- def _build_verify_prompt(step_name: str, expect: str, step: dict[str, Any]) -> str:
373
- """Build AI prompt for verification step."""
374
- if step.get("prompt"):
375
- return step["prompt"]
376
- if expect.startswith("text:"):
377
- text = expect[5:]
378
- return f"检查页面是否出现'{text}'的提示消息,确认操作已成功完成"
379
- if expect == "dialog_visible":
380
- return "检查对话框是否已打开并显示"
381
- if expect == "dialog_closed":
382
- return "检查对话框是否已关闭"
383
- return f"验证:{expect}"
326
+ def _extract(raw: Any, label: str) -> dict[str, Any]:
327
+ if not isinstance(raw, dict):
328
+ raise CompileError(f"{label}.extract must map variable names to JSON Pointers")
329
+ compiled: dict[str, Any] = {}
330
+ for name, spec in raw.items():
331
+ if isinstance(spec, str):
332
+ compiled[name] = {"pointer": spec}
333
+ elif isinstance(spec, dict):
334
+ compiled[name] = dict(spec)
335
+ else:
336
+ raise CompileError(f"{label}.extract.{name} must be a JSON Pointer string or {{pointer, sensitive}}")
337
+ return compiled
384
338
 
385
339
 
386
- # ---------------------------------------------------------------------------
387
- # Auth setup 编译
388
- # ---------------------------------------------------------------------------
340
+ def compile_api_step(step: dict[str, Any], method: str | None, default_id: str, label: str) -> dict[str, Any]:
341
+ allowed = COMMON_KEYS | API_KEYS
342
+ unknown = set(step) - allowed
343
+ if unknown:
344
+ raise CompileError(f"{label} has unsupported keys {sorted(unknown)} for an API request (allowed: {sorted(allowed)})")
345
+ raw_method = method or step.get("method")
346
+ if not isinstance(raw_method, str) or raw_method.upper() not in HTTP_METHODS:
347
+ raise CompileError(f"{label}.method must be one of {', '.join(HTTP_METHODS)}")
348
+ if method and "method" in step and str(step["method"]).upper() != method:
349
+ raise CompileError(f"{label}.method conflicts with action {step.get('action')}")
350
+ compiled = _base(step, default_id, label)
351
+ compiled.update({"driver": "api", "action": "request", "method": raw_method.upper(), "url": step.get("url")})
352
+ for key in ("headers", "query", "json", "expect_status", "assertions", "retry", "idempotent", "timeout_ms"):
353
+ if key in step:
354
+ compiled[key] = step[key]
355
+ if "extract" in step:
356
+ compiled["extract"] = _extract(step["extract"], label)
357
+ covers = compile_covers(step.get("covers"), label)
358
+ if covers:
359
+ compiled["covers"] = covers
360
+ return compiled
389
361
 
390
- def compile_auth_steps(auth: dict[str, Any] | None, base_url: str = "${base_url}") -> list[dict[str, Any]]:
391
- """Compile auth config into API login + web inject steps."""
392
- if not auth:
393
- return []
394
-
395
- method = str(auth.get("method", "")).lower()
396
- steps: list[dict[str, Any]] = []
397
-
398
- if method == "api_login":
399
- login_url = auth.get("login_url", f"{base_url}/auth/login_by_id")
400
- login_body = auth.get("login_body", {})
401
- token_path = auth.get("token_path", "headers.X-Jwt-Token")
402
- app_id = auth.get("app_id")
403
-
404
- steps.append({
405
- "id": "api_auth_login",
406
- "name": "获取JWT令牌",
407
- "driver": "api",
408
- "action": "post",
409
- "params": {"url": login_url, "json": login_body},
410
- "assertions": [
411
- {"type": "status_code", "expected": 200},
412
- {"type": "json_path_exists", "path": token_path},
413
- ],
414
- "extract_vars": {"jwt_token": {"path": token_path, "source": "jsonpath"}},
415
- "depends_on": [],
416
- "timeout_ms": 15_000,
417
- "on_failure": "stop",
418
- })
419
-
420
- if app_id:
421
- steps.append({
422
- "id": "api_app_token",
423
- "name": "获取应用访问Token",
424
- "driver": "api",
425
- "action": "post",
426
- "params": {
427
- "url": auth.get("app_token_url", f"{base_url}/admin/app/access"),
428
- "json": {"id": app_id},
429
- "headers": {"Authorization": "Bearer ${jwt_token}"},
430
- },
431
- "assertions": [{"type": "status_code", "expected": 200}],
432
- "extract_vars": {"token_url": {"path": "body.data.url", "source": "jsonpath"}},
433
- "depends_on": ["api_auth_login"],
434
- "timeout_ms": 15_000,
435
- "on_failure": "stop",
436
- })
437
-
438
- steps.append({
439
- "id": "web_auth_inject",
440
- "name": "Token注入",
441
- "driver": "web",
442
- "action": "goto",
443
- "params": {"url": "${token_url}"},
444
- "depends_on": ["api_app_token"],
445
- "timeout_ms": 30_000,
446
- "on_failure": "stop",
447
- })
448
-
449
- return steps
450
362
 
363
+ def compile_step(step: Any, default_id: str, label: str, warnings: list[str], api_only: bool = False) -> dict[str, Any]:
364
+ if not isinstance(step, dict):
365
+ raise CompileError(f"{label} must be an object")
366
+ _check_forbidden(step, label)
367
+ action = str(step.get("action") or "").strip()
368
+ lowered = action.lower()
369
+ if lowered in FORBIDDEN_ACTIONS:
370
+ raise CompileError(f"{label}.action '{action}' is not allowed: {FORBIDDEN_ACTIONS[lowered]}")
371
+ if lowered == "request" or lowered in API_SHORTHAND:
372
+ return compile_api_step(step, API_SHORTHAND.get(lowered), default_id, label)
373
+ if api_only:
374
+ raise CompileError(f"{label}: data.setup/cleanup may only contain API request steps")
375
+ if lowered not in UI_ACTIONS:
376
+ raise CompileError(f"{label}.action '{action}' is unknown; UI actions: {', '.join(sorted(UI_ACTIONS))}; "
377
+ f"API: request or {'/'.join(API_SHORTHAND)}")
378
+ allowed = COMMON_KEYS | UI_KEYS[lowered]
379
+ unknown = set(step) - allowed
380
+ if unknown:
381
+ raise CompileError(f"{label} has unsupported keys {sorted(unknown)} for action {lowered} (allowed: {sorted(allowed)})")
382
+ compiled = _base(step, default_id, label)
383
+ compiled.update({"driver": "ui", "action": lowered})
384
+ if lowered in TARGET_ACTIONS:
385
+ if "locator" not in step:
386
+ raise CompileError(f"{label}.locator is required for {lowered}")
387
+ compiled["target"] = compile_target(step["locator"], step.get("exact"), label, warnings)
388
+ if lowered == "goto":
389
+ compiled["url"] = step.get("url")
390
+ elif lowered in {"fill", "select"}:
391
+ compiled["value"] = step.get("value")
392
+ elif lowered == "wait_for":
393
+ compiled["state"] = step.get("state", "visible")
394
+ elif lowered == "assert_text":
395
+ compiled["match"] = step.get("match", "exact")
396
+ compiled["expected"] = step.get("expected")
397
+ for key in ("normalize_whitespace", "case_sensitive"):
398
+ if key in step:
399
+ compiled[key] = step[key]
400
+ elif lowered == "assert_url":
401
+ compiled["match"] = step.get("match", "contains")
402
+ compiled["expected"] = step.get("expected")
403
+ elif lowered == "screenshot":
404
+ compiled["name"] = step.get("id", default_id) if isinstance(step.get("id", default_id), str) else default_id
405
+ if "full_page" in step:
406
+ compiled["full_page"] = step["full_page"]
407
+ if "timeout_ms" in step:
408
+ compiled["timeout_ms"] = step["timeout_ms"]
409
+ covers = compile_covers(step.get("covers"), label)
410
+ if covers:
411
+ compiled["covers"] = covers
412
+ return compiled
451
413
 
452
- # ---------------------------------------------------------------------------
453
- # Main compile entry
454
- # ---------------------------------------------------------------------------
455
414
 
456
- def _check_terminology(steps: list[dict[str, Any]]) -> list[str]:
457
- """Check for terminology hallucination in step names vs locator values."""
458
- # Commonly hallucinated term pairs: (AI-invented, source-code-actual)
459
- TERM_PAIRS: list[tuple[str, str]] = [
460
- ("搜索", "查询"),
461
- ("搜索", "筛选"),
462
- ("新增", "添加"),
463
- ("编辑", "修改"),
464
- ("删除", "移除"),
465
- ("保存", "提交"),
466
- ("关闭", "取消"),
467
- ]
468
- warnings: list[str] = []
469
- for step in steps:
470
- name = str(step.get("name", ""))
471
- locator_raw = str(step.get("_locator_raw", ""))
472
- for ai_term, src_term in TERM_PAIRS:
473
- if ai_term in name and src_term in locator_raw:
474
- warnings.append(
475
- f"Step '{name}' uses '{ai_term}' but locator contains '{src_term}'. "
476
- f"Verify the button text in source code — use the exact source text."
477
- )
478
- return warnings
479
-
480
-
481
- def compile_case(case_desc: dict[str, Any]) -> dict[str, Any]:
482
- """Compile one simplified case description into strict DSL."""
483
- name = str(case_desc.get("name") or "Unnamed Case")
484
- tags = list(case_desc.get("tags") or ["ui-mainflow"])
485
- variables = dict(case_desc.get("variables") or {})
486
- base_url = variables.get("base_url", "${base_url}")
487
-
488
- # Compile auth
489
- auth_steps = compile_auth_steps(case_desc.get("auth"), base_url)
490
-
491
- # Compile business steps
492
- raw_steps = list(case_desc.get("steps") or [])
493
- business_steps: list[dict[str, Any]] = []
494
- for i, raw_step in enumerate(raw_steps):
495
- compiled_step = compile_step(raw_step, i + 1)
496
- # Preserve original locator string for terminology checking
497
- compiled_step["_locator_raw"] = raw_step.get("locator", "")
498
- business_steps.append(compiled_step)
499
-
500
- # Terminology check (warning, not error)
501
- term_warnings = _check_terminology(business_steps)
502
- if term_warnings:
503
- for w in term_warnings:
504
- print(f"TERMINOLOGY WARNING: {w}", file=__import__("sys").stderr)
505
-
506
- # Merge: auth steps first, then business steps
507
- all_steps = auth_steps + business_steps
508
-
509
- # Wire dependencies: auth steps are already wired; business steps depend on previous
510
- first_business_idx = len(auth_steps)
511
- for i, step in enumerate(all_steps):
512
- if i >= first_business_idx and i > 0:
513
- prev_id = all_steps[i - 1]["id"]
514
- if not step.get("depends_on"):
515
- step["depends_on"] = [prev_id]
415
+ def compile_variables(raw: Any, label: str) -> dict[str, Any]:
416
+ if raw is None:
417
+ return {}
418
+ if not isinstance(raw, dict):
419
+ raise CompileError(f"{label}.variables must be an object")
420
+ compiled: dict[str, Any] = {}
421
+ for name, value in raw.items():
422
+ if isinstance(value, dict):
423
+ compiled[name] = dict(value)
424
+ elif isinstance(value, (str, int, float, bool)):
425
+ compiled[name] = {"value": value}
426
+ else:
427
+ raise CompileError(f"{label}.variables.{name} must be a scalar, {{value, sensitive}} or {{secret}}")
428
+ return compiled
429
+
516
430
 
431
+ def compile_case(desc: Any, index: int, eligible: dict[str, dict[str, Any]], rejected: dict[str, str]) -> tuple[str, dict[str, Any], list[str]]:
432
+ label = f"cases[{index}]"
433
+ if not isinstance(desc, dict):
434
+ raise CompileError(f"{label} must be an object")
435
+ unknown = set(desc) - CASE_KEYS
436
+ for key in sorted(unknown):
437
+ raise CompileError(f"{label}.{key} is not allowed: {FORBIDDEN_KEYS.get(key, 'unknown case field')}")
438
+ tc_id = desc.get("tc_id")
439
+ if not isinstance(tc_id, str) or not TC_ID_RE.fullmatch(tc_id):
440
+ raise CompileError(f"{label}.tc_id must reference a frozen TC (TC-xxxx)")
441
+ if tc_id not in eligible:
442
+ reason = rejected.get(tc_id, "is not in the frozen TC suite")
443
+ raise CompileError(f"{label}.tc_id {tc_id} {reason}")
444
+ label = f"{label}({tc_id})"
445
+ warnings: list[str] = []
446
+ data_raw = desc.get("data") or {}
447
+ if not isinstance(data_raw, dict):
448
+ raise CompileError(f"{label}.data must be an object")
449
+ unknown_data = set(data_raw) - DATA_KEYS
450
+ if unknown_data:
451
+ raise CompileError(f"{label}.data has unsupported keys {sorted(unknown_data)}")
452
+ data: dict[str, Any] = {"namespace": data_raw.get("namespace", "${execution_id}")}
453
+ for key in ("owner", "accounts", "repeatable"):
454
+ if key in data_raw:
455
+ data[key] = data_raw[key]
456
+ for phase in ("setup", "cleanup"):
457
+ steps = data_raw.get(phase, [])
458
+ if not isinstance(steps, list):
459
+ raise CompileError(f"{label}.data.{phase} must be an array")
460
+ data[phase] = [compile_step(step, f"{phase}-{i:02d}", f"{label}.data.{phase}[{i - 1}]", warnings, api_only=True)
461
+ for i, step in enumerate(steps, start=1)]
462
+ raw_steps = desc.get("steps")
463
+ if not isinstance(raw_steps, list) or not raw_steps:
464
+ raise CompileError(f"{label}.steps must be a non-empty array")
465
+ steps = [compile_step(step, f"s{i:02d}", f"{label}.steps[{i - 1}]", warnings) for i, step in enumerate(raw_steps, start=1)]
466
+ source = eligible[tc_id]
467
+ name = desc.get("name")
517
468
  dsl = {
518
- "name": name,
519
- "description": case_desc.get("description", ""),
520
- "tags": tags,
521
- "variables": variables,
522
- "steps": all_steps,
469
+ "schema_version": SCHEMA_VERSION,
470
+ "tc_id": tc_id,
471
+ "tc_sha256": tc_sha256(source),
472
+ "name": name if isinstance(name, str) and name.strip() else tc_id,
473
+ "variables": compile_variables(desc.get("variables"), label),
474
+ "data": data,
475
+ "steps": steps,
523
476
  }
477
+ errors = [f"{label}: {error}" for error in validate_dsl(dsl)]
478
+ if not errors:
479
+ errors = [f"{label}: coverage: {error}" for error in coverage_check(source, dsl)]
480
+ if errors:
481
+ raise CompileError("\n".join(errors))
482
+ return tc_id, dsl, [f"{label}: {warning}" for warning in warnings]
524
483
 
525
- return {
526
- "project_id": int(case_desc.get("project_id", 1)),
527
- "name": name,
528
- "description": case_desc.get("description", ""),
529
- "tags": tags,
530
- "dsl_json": dsl,
531
- }
532
484
 
485
+ def compile_all(payload: Any, suite: dict[str, Any]) -> tuple[dict[str, dict[str, Any]], list[str], list[str]]:
486
+ """Return ({tc_id: dsl}, warnings, errors). Errors are collected per case."""
487
+ cases = payload.get("cases") if isinstance(payload, dict) else payload
488
+ if not isinstance(cases, list):
489
+ raise CompileError("case descriptions must be {\"cases\": [...]} or an array")
490
+ eligible, rejected = eligible_cases(suite)
491
+ compiled: dict[str, dict[str, Any]] = {}
492
+ warnings: list[str] = []
493
+ errors: list[str] = []
494
+ for index, desc in enumerate(cases):
495
+ try:
496
+ tc_id, dsl, case_warnings = compile_case(desc, index, eligible, rejected)
497
+ except CompileError as error:
498
+ errors.append(str(error))
499
+ continue
500
+ if tc_id in compiled:
501
+ errors.append(f"cases[{index}]: duplicate case for {tc_id}; each frozen TC has exactly one DSL file")
502
+ continue
503
+ compiled[tc_id] = dsl
504
+ warnings.extend(case_warnings)
505
+ return compiled, warnings, errors
533
506
 
534
- def compile_all(payload: dict[str, Any], frozen_mainflow_tc_ids: set[str] | None = None) -> dict[str, Any]:
535
- """Compile all cases in the payload."""
536
- metadata = dict(payload.get("metadata") or {})
537
- descriptions = list(payload.get("cases", []))
538
- tc_ids: list[str] = []
539
- if frozen_mainflow_tc_ids is not None:
540
- for index, description in enumerate(descriptions):
541
- tc_id = description.get("tc_id") if isinstance(description, dict) else None
542
- if not isinstance(tc_id, str) or tc_id not in frozen_mainflow_tc_ids:
543
- raise ValueError(f"cases[{index}].tc_id must reference a frozen api/page TC that is positive")
544
- tc_ids.append(tc_id)
545
- if len(tc_ids) != len(set(tc_ids)):
546
- raise ValueError("each frozen positive api/page TC may generate at most one mainflow case")
547
- cases = [compile_case(c) for c in descriptions]
548
-
549
- return {
550
- "metadata": {
551
- **metadata,
552
- **({"frozen_tc_ids": tc_ids} if frozen_mainflow_tc_ids is not None else {}),
553
- "compiled_at": datetime.now(timezone.utc).isoformat(),
554
- "compiled_by": "compile_dsl.py (hard-constraint layer)",
555
- },
556
- "cases": cases,
557
- }
558
507
 
508
+ def render(dsl: dict[str, Any]) -> bytes:
509
+ return (json.dumps(dsl, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
559
510
 
560
- def frozen_mainflow_tc_ids_from_cwd(cwd: Path) -> set[str] | None:
561
- """Return the frozen API/page suite for new workflows, else preserve legacy compilation."""
562
- state_path = cwd / ".planning" / "STATE.md"
563
- if not state_path.is_file():
564
- return None
565
- match = re.search(r"^current_feature:\s*['\"]?([^'\"\n]+)", state_path.read_text(encoding="utf-8"), re.MULTILINE)
566
- if not match:
567
- return None
568
- suite_path = cwd / ".planning" / "requirements" / match.group(1).strip() / "tests.json"
569
- if not suite_path.is_file():
570
- return None
571
- suite = json.loads(suite_path.read_text(encoding="utf-8"))
572
- if suite.get("frozen") is not True:
573
- raise ValueError("current TC suite exists but is not frozen and fresh")
574
- eligible = {
575
- node.get("id")
576
- for node in suite.get("nodes", [])
577
- if isinstance(node, dict)
578
- and node.get("kind") == "test_case"
579
- and node.get("status") == "confirmed"
580
- and isinstance(node.get("data"), dict)
581
- and node["data"].get("test_type") in {"api", "page"}
582
- # A generated API TC with an error-code oracle is a negative/exception
583
- # test. It belongs to run-acceptance-tests, never to the positive
584
- # mainflow importer even though its broad test_type is "api".
585
- and not (
586
- isinstance(node["data"].get("generated_case_key"), str)
587
- and isinstance(node["data"].get("execution"), dict)
588
- and isinstance(node["data"]["execution"].get("expect"), dict)
589
- and "error_code" in node["data"]["execution"]["expect"]
590
- )
591
- }
592
- return {value for value in eligible if isinstance(value, str)}
593
511
 
512
+ def write_atomic(path: Path, content: bytes) -> None:
513
+ temporary = path.with_name(f".{path.name}.tmp")
514
+ temporary.write_bytes(content)
515
+ temporary.replace(path)
594
516
 
595
- # ---------------------------------------------------------------------------
596
- # CLI
597
- # ---------------------------------------------------------------------------
598
517
 
599
518
  def main() -> int:
600
- parser = argparse.ArgumentParser(
601
- description="Compile AI-friendly case descriptions into strict DSL JSON."
602
- )
603
- parser.add_argument("input", help="Simplified case descriptions JSON")
604
- parser.add_argument("--output", required=True, help="Output path for case-import-payloads.json")
605
- args = parser.parse_args()
606
-
607
- input_path = Path(args.input)
608
- output_path = Path(args.output)
609
-
610
- raw = json.loads(input_path.read_text(encoding="utf-8"))
611
-
612
- # Accept both array and {cases: [...]} format
613
- if isinstance(raw, list):
614
- raw = {"metadata": {}, "cases": raw}
615
-
519
+ # allow_abbrev=False: the removed `--output` must not silently abbreviate to `--output-dir`.
520
+ parser = argparse.ArgumentParser(description="Compile case descriptions into one devflow-test-dsl/v1 file per frozen TC.",
521
+ allow_abbrev=False)
522
+ parser.add_argument("input", help="case-descriptions.json (see references/intermediate-format.md)")
523
+ parser.add_argument("--feature", help="feature name (default: current_feature in .devflow/planning/STATE.md)")
524
+ parser.add_argument("--suite", help="frozen TC suite (default: .devflow/planning/requirements/<feature>/tests.json)")
525
+ parser.add_argument("--output-dir", help="default: .devflow/planning/generated-cases/<feature>")
526
+ args, unknown = parser.parse_known_args()
527
+ if unknown:
528
+ removed = [item for item in unknown if item == "--output" or item.startswith("--output=")]
529
+ hint = (" `--output` was removed: the compiler writes one TC-xxxx.json per frozen TC into a directory; "
530
+ "use --output-dir (default .devflow/planning/generated-cases/<feature>)." if removed else "")
531
+ parser.error(f"unrecognized arguments: {' '.join(unknown)}.{hint}")
532
+
533
+ root = Path.cwd().resolve()
534
+ feature = args.feature or current_feature(root)
616
535
  try:
617
- result = compile_all(raw, frozen_mainflow_tc_ids_from_cwd(Path.cwd()))
618
- except ValueError as exc:
619
- print(f"COMPILE ERROR: {exc}", file=__import__("sys").stderr)
536
+ if not feature or not FEATURE_RE.fullmatch(feature):
537
+ raise CompileError("feature is required (--feature or current_feature in .devflow/planning/STATE.md)")
538
+ suite = load_suite(Path(args.suite) if args.suite else root / ".devflow/planning" / "requirements" / feature / "tests.json")
539
+ try:
540
+ payload = json.loads(Path(args.input).read_text(encoding="utf-8"))
541
+ except (OSError, json.JSONDecodeError) as error:
542
+ raise CompileError(f"cannot read case descriptions: {error}") from error
543
+ compiled, warnings, errors = compile_all(payload, suite)
544
+ except CompileError as error:
545
+ print(f"COMPILE ERROR: {error}", file=sys.stderr)
620
546
  return 1
621
-
622
- output_path.parent.mkdir(parents=True, exist_ok=True)
623
- output_path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
624
- print(json.dumps({"output": str(output_path), "case_count": len(result["cases"])}, ensure_ascii=False))
547
+ for warning in warnings:
548
+ print(f"WARNING: {warning}", file=sys.stderr)
549
+ if errors:
550
+ # All-or-nothing: never leave a partially regenerated execution plan on disk.
551
+ print("COMPILE ERROR: no DSL file was written\n" + "\n".join(f"- {error}" for error in errors), file=sys.stderr)
552
+ return 1
553
+ output_dir = Path(args.output_dir) if args.output_dir else root / ".devflow/planning" / "generated-cases" / feature
554
+ output_dir.mkdir(parents=True, exist_ok=True)
555
+ files: list[dict[str, Any]] = []
556
+ for tc_id in sorted(compiled):
557
+ path = output_dir / f"{tc_id}.json"
558
+ content = render(compiled[tc_id])
559
+ unchanged = path.is_file() and path.read_bytes() == content
560
+ if not unchanged:
561
+ write_atomic(path, content)
562
+ files.append({"tc_id": tc_id, "path": path.relative_to(root).as_posix() if path.is_relative_to(root) else str(path),
563
+ "changed": not unchanged})
564
+ print(json.dumps({"status": "compiled", "feature": feature, "case_count": len(files), "files": files,
565
+ "warnings": len(warnings)}, ensure_ascii=False, indent=2))
625
566
  return 0
626
567
 
627
568