devflow-engine 1.0.0__py3-none-any.whl

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 (393) hide show
  1. devflow_engine/__init__.py +3 -0
  2. devflow_engine/agentic_prompts.py +100 -0
  3. devflow_engine/agentic_runtime.py +398 -0
  4. devflow_engine/api_key_flow_harness.py +539 -0
  5. devflow_engine/api_keys.py +357 -0
  6. devflow_engine/bootstrap/__init__.py +2 -0
  7. devflow_engine/bootstrap/provision_from_template.py +84 -0
  8. devflow_engine/cli/__init__.py +0 -0
  9. devflow_engine/cli/app.py +7270 -0
  10. devflow_engine/core/__init__.py +0 -0
  11. devflow_engine/core/config.py +86 -0
  12. devflow_engine/core/logging.py +29 -0
  13. devflow_engine/core/paths.py +45 -0
  14. devflow_engine/core/toml_kv.py +33 -0
  15. devflow_engine/devflow_event_worker.py +1292 -0
  16. devflow_engine/devflow_state.py +201 -0
  17. devflow_engine/devin2/__init__.py +9 -0
  18. devflow_engine/devin2/agent_definition.py +120 -0
  19. devflow_engine/devin2/pi_runner.py +204 -0
  20. devflow_engine/devin_orchestration.py +69 -0
  21. devflow_engine/docs/prompts/anti-patterns.md +42 -0
  22. devflow_engine/docs/prompts/devin-agent-prompt.md +55 -0
  23. devflow_engine/docs/prompts/devin2-agent-prompt.md +81 -0
  24. devflow_engine/docs/prompts/examples/devin-vapi-clone-reference-exchange.json +85 -0
  25. devflow_engine/doctor/__init__.py +2 -0
  26. devflow_engine/doctor/triage.py +140 -0
  27. devflow_engine/error/__init__.py +0 -0
  28. devflow_engine/error/remediation.py +21 -0
  29. devflow_engine/errors/error_solver_dag.py +522 -0
  30. devflow_engine/errors/runtime_observability.py +67 -0
  31. devflow_engine/idea/__init__.py +4 -0
  32. devflow_engine/idea/actors.py +481 -0
  33. devflow_engine/idea/agentic.py +465 -0
  34. devflow_engine/idea/analyze.py +93 -0
  35. devflow_engine/idea/devin_chat_dag.py +1 -0
  36. devflow_engine/idea/diff.py +99 -0
  37. devflow_engine/idea/drafts.py +446 -0
  38. devflow_engine/idea/idea_creation_dag.py +643 -0
  39. devflow_engine/idea/ideation_enrichment.py +355 -0
  40. devflow_engine/idea/ideation_enrichment_worker.py +19 -0
  41. devflow_engine/idea/paths.py +28 -0
  42. devflow_engine/idea/promote.py +53 -0
  43. devflow_engine/idea/redaction.py +27 -0
  44. devflow_engine/idea/repo_tools.py +1277 -0
  45. devflow_engine/idea/response_mode.py +30 -0
  46. devflow_engine/idea/story_pipeline.py +1585 -0
  47. devflow_engine/idea/sufficiency.py +376 -0
  48. devflow_engine/idea/traditional_stories.py +1257 -0
  49. devflow_engine/implementation/__init__.py +0 -0
  50. devflow_engine/implementation/alembic_preflight.py +700 -0
  51. devflow_engine/implementation/dag.py +8450 -0
  52. devflow_engine/implementation/green_gate.py +93 -0
  53. devflow_engine/implementation/prompts.py +108 -0
  54. devflow_engine/implementation/test_runtime.py +623 -0
  55. devflow_engine/integration/__init__.py +19 -0
  56. devflow_engine/integration/agentic.py +66 -0
  57. devflow_engine/integration/dag.py +3539 -0
  58. devflow_engine/integration/prompts.py +114 -0
  59. devflow_engine/integration/supabase_schema.sql +31 -0
  60. devflow_engine/integration/supabase_sync.py +177 -0
  61. devflow_engine/llm/__init__.py +1 -0
  62. devflow_engine/llm/cli_one_shot.py +84 -0
  63. devflow_engine/llm/cli_stream.py +371 -0
  64. devflow_engine/llm/execution_context.py +26 -0
  65. devflow_engine/llm/invoke.py +1322 -0
  66. devflow_engine/llm/provider_api.py +304 -0
  67. devflow_engine/llm/repo_knowledge.py +588 -0
  68. devflow_engine/llm_primitives.py +315 -0
  69. devflow_engine/orchestration.py +62 -0
  70. devflow_engine/planning/__init__.py +0 -0
  71. devflow_engine/planning/analyze_repo.py +92 -0
  72. devflow_engine/planning/render_drafts.py +133 -0
  73. devflow_engine/playground/__init__.py +0 -0
  74. devflow_engine/playground/hooks.py +26 -0
  75. devflow_engine/playwright_workflow/__init__.py +5 -0
  76. devflow_engine/playwright_workflow/dag.py +1317 -0
  77. devflow_engine/process/__init__.py +5 -0
  78. devflow_engine/process/dag.py +59 -0
  79. devflow_engine/project_registration/__init__.py +3 -0
  80. devflow_engine/project_registration/dag.py +1581 -0
  81. devflow_engine/project_registry.py +109 -0
  82. devflow_engine/prompts/devin/generic/prompt.md +6 -0
  83. devflow_engine/prompts/devin/ideation/prompt.md +263 -0
  84. devflow_engine/prompts/devin/ideation/scenarios.md +5 -0
  85. devflow_engine/prompts/devin/ideation_loop/prompt.md +6 -0
  86. devflow_engine/prompts/devin/insight/prompt.md +11 -0
  87. devflow_engine/prompts/devin/insight/scenarios.md +5 -0
  88. devflow_engine/prompts/devin/intake/prompt.md +15 -0
  89. devflow_engine/prompts/devin/iterate/prompt.md +12 -0
  90. devflow_engine/prompts/devin/shared/eval_doctrine.md +9 -0
  91. devflow_engine/prompts/devin/shared/principles.md +246 -0
  92. devflow_engine/prompts/devin_eval/assessment/prompt.md +18 -0
  93. devflow_engine/prompts/idea/api_ideation_agent/prompt.md +8 -0
  94. devflow_engine/prompts/idea/api_insight_agent/prompt.md +8 -0
  95. devflow_engine/prompts/idea/response_doctrine/prompt.md +18 -0
  96. devflow_engine/prompts/implementation/dependency_assessment/prompt.md +12 -0
  97. devflow_engine/prompts/implementation/green/green/prompt.md +11 -0
  98. devflow_engine/prompts/implementation/green/node_config/prompt.md +3 -0
  99. devflow_engine/prompts/implementation/green_review/outcome_review/prompt.md +5 -0
  100. devflow_engine/prompts/implementation/green_review/prior_run_review/prompt.md +5 -0
  101. devflow_engine/prompts/implementation/red/prompt.md +27 -0
  102. devflow_engine/prompts/implementation/redreview/prompt.md +23 -0
  103. devflow_engine/prompts/implementation/redreview_repair/prompt.md +16 -0
  104. devflow_engine/prompts/implementation/setupdoc/prompt.md +10 -0
  105. devflow_engine/prompts/implementation/story_planning/prompt.md +13 -0
  106. devflow_engine/prompts/implementation/test_design/prompt.md +27 -0
  107. devflow_engine/prompts/integration/README.md +185 -0
  108. devflow_engine/prompts/integration/green/example.md +67 -0
  109. devflow_engine/prompts/integration/green/green/prompt.md +10 -0
  110. devflow_engine/prompts/integration/green/node_config/prompt.md +42 -0
  111. devflow_engine/prompts/integration/green/past_prompts/20260417T212300/green/prompt.md +15 -0
  112. devflow_engine/prompts/integration/green/past_prompts/20260417T212300/node_config/prompt.md +42 -0
  113. devflow_engine/prompts/integration/green_enrich/example.md +79 -0
  114. devflow_engine/prompts/integration/green_enrich/green_enrich/prompt.md +9 -0
  115. devflow_engine/prompts/integration/green_enrich/node_config/prompt.md +41 -0
  116. devflow_engine/prompts/integration/green_enrich/past_prompts/20260417T212300/green_enrich/prompt.md +14 -0
  117. devflow_engine/prompts/integration/green_enrich/past_prompts/20260417T212300/node_config/prompt.md +41 -0
  118. devflow_engine/prompts/integration/red/code_repair/prompt.md +12 -0
  119. devflow_engine/prompts/integration/red/example.md +152 -0
  120. devflow_engine/prompts/integration/red/node_config/prompt.md +86 -0
  121. devflow_engine/prompts/integration/red/past_prompts/20260417T212300/code_repair/prompt.md +19 -0
  122. devflow_engine/prompts/integration/red/past_prompts/20260417T212300/node_config/prompt.md +84 -0
  123. devflow_engine/prompts/integration/red/past_prompts/20260417T212300/red/prompt.md +16 -0
  124. devflow_engine/prompts/integration/red/past_prompts/20260417T212300/red_repair/prompt.md +15 -0
  125. devflow_engine/prompts/integration/red/past_prompts/20260417T215032/code_repair/prompt.md +10 -0
  126. devflow_engine/prompts/integration/red/past_prompts/20260417T215032/node_config/prompt.md +84 -0
  127. devflow_engine/prompts/integration/red/past_prompts/20260417T215032/red_repair/prompt.md +11 -0
  128. devflow_engine/prompts/integration/red/red/prompt.md +11 -0
  129. devflow_engine/prompts/integration/red/red_repair/prompt.md +12 -0
  130. devflow_engine/prompts/integration/red_review/example.md +71 -0
  131. devflow_engine/prompts/integration/red_review/node_config/prompt.md +41 -0
  132. devflow_engine/prompts/integration/red_review/past_prompts/20260417T212300/node_config/prompt.md +41 -0
  133. devflow_engine/prompts/integration/red_review/past_prompts/20260417T212300/red_review/prompt.md +15 -0
  134. devflow_engine/prompts/integration/red_review/red_review/prompt.md +9 -0
  135. devflow_engine/prompts/integration/resolve/example.md +111 -0
  136. devflow_engine/prompts/integration/resolve/node_config/prompt.md +64 -0
  137. devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/node_config/prompt.md +64 -0
  138. devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/resolve_implicated_users/prompt.md +15 -0
  139. devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/resolve_side_effects/prompt.md +15 -0
  140. devflow_engine/prompts/integration/resolve/resolve_implicated_users/prompt.md +10 -0
  141. devflow_engine/prompts/integration/resolve/resolve_side_effects/prompt.md +10 -0
  142. devflow_engine/prompts/integration/validate/build_idea_acceptance_coverage/prompt.md +12 -0
  143. devflow_engine/prompts/integration/validate/code_repair/prompt.md +13 -0
  144. devflow_engine/prompts/integration/validate/example.md +143 -0
  145. devflow_engine/prompts/integration/validate/node_config/prompt.md +87 -0
  146. devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/code_repair/prompt.md +19 -0
  147. devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/node_config/prompt.md +67 -0
  148. devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/validate_enrich_gate/prompt.md +17 -0
  149. devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/validate_repair/prompt.md +16 -0
  150. devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/code_repair/prompt.md +10 -0
  151. devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/node_config/prompt.md +67 -0
  152. devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/validate_repair/prompt.md +9 -0
  153. devflow_engine/prompts/integration/validate/validate_enrich_gate/prompt.md +10 -0
  154. devflow_engine/prompts/integration/validate/validate_repair/prompt.md +20 -0
  155. devflow_engine/prompts/integration/write_workflows/example.md +100 -0
  156. devflow_engine/prompts/integration/write_workflows/node_config/prompt.md +44 -0
  157. devflow_engine/prompts/integration/write_workflows/past_prompts/20260417T212300/node_config/prompt.md +44 -0
  158. devflow_engine/prompts/integration/write_workflows/past_prompts/20260417T212300/write_workflows/prompt.md +17 -0
  159. devflow_engine/prompts/integration/write_workflows/write_workflows/prompt.md +11 -0
  160. devflow_engine/prompts/iterate/README.md +7 -0
  161. devflow_engine/prompts/iterate/coder/prompt.md +11 -0
  162. devflow_engine/prompts/iterate/framer/prompt.md +11 -0
  163. devflow_engine/prompts/iterate/iterator/prompt.md +13 -0
  164. devflow_engine/prompts/iterate/observer/prompt.md +11 -0
  165. devflow_engine/prompts/recovery/diagnosis/prompt.md +7 -0
  166. devflow_engine/prompts/recovery/execution/prompt.md +8 -0
  167. devflow_engine/prompts/recovery/execution_verification/prompt.md +7 -0
  168. devflow_engine/prompts/recovery/failure_investigation/prompt.md +10 -0
  169. devflow_engine/prompts/recovery/preflight_health_repo_repair/prompt.md +8 -0
  170. devflow_engine/prompts/recovery/remediation_execution/prompt.md +11 -0
  171. devflow_engine/prompts/recovery/root_cause_investigation/prompt.md +12 -0
  172. devflow_engine/prompts/scope_idea/doctrine/prompt.md +7 -0
  173. devflow_engine/prompts/source_doc_eval/document/prompt.md +6 -0
  174. devflow_engine/prompts/source_doc_eval/targeted_mutation/prompt.md +9 -0
  175. devflow_engine/prompts/source_doc_mutation/domain_entities/prompt.md +6 -0
  176. devflow_engine/prompts/source_doc_mutation/product_brief/prompt.md +6 -0
  177. devflow_engine/prompts/source_doc_mutation/project_doc_coherence/prompt.md +7 -0
  178. devflow_engine/prompts/source_doc_mutation/project_doc_render/prompt.md +9 -0
  179. devflow_engine/prompts/source_doc_mutation/source_doc_coherence/prompt.md +5 -0
  180. devflow_engine/prompts/source_doc_mutation/source_doc_enrichment_coherence/prompt.md +6 -0
  181. devflow_engine/prompts/source_doc_mutation/user_workflows/prompt.md +6 -0
  182. devflow_engine/prompts/source_scope/doctrine/prompt.md +10 -0
  183. devflow_engine/prompts/ui_grounding/doctrine/prompt.md +7 -0
  184. devflow_engine/recovery/__init__.py +3 -0
  185. devflow_engine/recovery/dag.py +2609 -0
  186. devflow_engine/recovery/models.py +220 -0
  187. devflow_engine/refactor.py +93 -0
  188. devflow_engine/registry/__init__.py +1 -0
  189. devflow_engine/registry/cards.py +238 -0
  190. devflow_engine/registry/domain_normalize.py +60 -0
  191. devflow_engine/registry/effects.py +65 -0
  192. devflow_engine/registry/enforce_report.py +150 -0
  193. devflow_engine/registry/module_cards_classify.py +164 -0
  194. devflow_engine/registry/module_cards_draft.py +184 -0
  195. devflow_engine/registry/module_cards_gate.py +59 -0
  196. devflow_engine/registry/packages.py +347 -0
  197. devflow_engine/registry/pathways.py +323 -0
  198. devflow_engine/review/__init__.py +11 -0
  199. devflow_engine/review/dag.py +588 -0
  200. devflow_engine/review/review_story.py +67 -0
  201. devflow_engine/scope_idea/__init__.py +3 -0
  202. devflow_engine/scope_idea/agentic.py +39 -0
  203. devflow_engine/scope_idea/dag.py +1069 -0
  204. devflow_engine/scope_idea/models.py +175 -0
  205. devflow_engine/skills/builtins/devflow/queue_failure_investigation/SKILL.md +112 -0
  206. devflow_engine/skills/builtins/devflow/queue_idea_to_story/SKILL.md +120 -0
  207. devflow_engine/skills/builtins/devflow/queue_integration/SKILL.md +105 -0
  208. devflow_engine/skills/builtins/devflow/queue_recovery/SKILL.md +108 -0
  209. devflow_engine/skills/builtins/devflow/queue_runtime_core/SKILL.md +155 -0
  210. devflow_engine/skills/builtins/devflow/queue_story_implementation/SKILL.md +122 -0
  211. devflow_engine/skills/builtins/devin/idea_to_story_handoff/SKILL.md +120 -0
  212. devflow_engine/skills/builtins/devin/ideation/SKILL.md +168 -0
  213. devflow_engine/skills/builtins/devin/ideation/state-and-phrasing-reference.md +18 -0
  214. devflow_engine/skills/builtins/devin/insight/SKILL.md +22 -0
  215. devflow_engine/skills/registry.example.yaml +42 -0
  216. devflow_engine/source_doc_assumptions.py +291 -0
  217. devflow_engine/source_doc_mutation_dag.py +1606 -0
  218. devflow_engine/source_doc_mutation_eval.py +417 -0
  219. devflow_engine/source_doc_mutation_worker.py +25 -0
  220. devflow_engine/source_docs_schema.py +207 -0
  221. devflow_engine/source_docs_updater.py +309 -0
  222. devflow_engine/source_scope/__init__.py +15 -0
  223. devflow_engine/source_scope/agentic.py +45 -0
  224. devflow_engine/source_scope/dag.py +1626 -0
  225. devflow_engine/source_scope/models.py +177 -0
  226. devflow_engine/stores/__init__.py +0 -0
  227. devflow_engine/stores/execution_store.py +3534 -0
  228. devflow_engine/story/__init__.py +0 -0
  229. devflow_engine/story/contracts.py +160 -0
  230. devflow_engine/story/discovery.py +47 -0
  231. devflow_engine/story/evidence.py +118 -0
  232. devflow_engine/story/hashing.py +27 -0
  233. devflow_engine/story/implemented_queue_purge.py +148 -0
  234. devflow_engine/story/indexer.py +105 -0
  235. devflow_engine/story/io.py +20 -0
  236. devflow_engine/story/markdown_contracts.py +298 -0
  237. devflow_engine/story/reconciliation.py +408 -0
  238. devflow_engine/story/validate_stories.py +149 -0
  239. devflow_engine/story/validate_tests_story.py +512 -0
  240. devflow_engine/story/validation.py +133 -0
  241. devflow_engine/ui_grounding/__init__.py +11 -0
  242. devflow_engine/ui_grounding/agentic.py +31 -0
  243. devflow_engine/ui_grounding/dag.py +874 -0
  244. devflow_engine/ui_grounding/models.py +224 -0
  245. devflow_engine/ui_grounding/pencil_bridge.py +247 -0
  246. devflow_engine/vendor/__init__.py +0 -0
  247. devflow_engine/vendor/datalumina_genai/__init__.py +11 -0
  248. devflow_engine/vendor/datalumina_genai/core/__init__.py +0 -0
  249. devflow_engine/vendor/datalumina_genai/core/exceptions.py +9 -0
  250. devflow_engine/vendor/datalumina_genai/core/nodes/__init__.py +0 -0
  251. devflow_engine/vendor/datalumina_genai/core/nodes/agent.py +48 -0
  252. devflow_engine/vendor/datalumina_genai/core/nodes/agent_streaming_node.py +26 -0
  253. devflow_engine/vendor/datalumina_genai/core/nodes/base.py +89 -0
  254. devflow_engine/vendor/datalumina_genai/core/nodes/concurrent.py +30 -0
  255. devflow_engine/vendor/datalumina_genai/core/nodes/router.py +69 -0
  256. devflow_engine/vendor/datalumina_genai/core/schema.py +72 -0
  257. devflow_engine/vendor/datalumina_genai/core/task.py +52 -0
  258. devflow_engine/vendor/datalumina_genai/core/validate.py +139 -0
  259. devflow_engine/vendor/datalumina_genai/core/workflow.py +200 -0
  260. devflow_engine/worker.py +1086 -0
  261. devflow_engine/worker_guard.py +233 -0
  262. devflow_engine-1.0.0.dist-info/METADATA +235 -0
  263. devflow_engine-1.0.0.dist-info/RECORD +393 -0
  264. devflow_engine-1.0.0.dist-info/WHEEL +4 -0
  265. devflow_engine-1.0.0.dist-info/entry_points.txt +3 -0
  266. devin/__init__.py +6 -0
  267. devin/dag.py +58 -0
  268. devin/dag_two_arm.py +138 -0
  269. devin/devin_chat_scenario_catalog.json +588 -0
  270. devin/devin_eval.py +677 -0
  271. devin/nodes/__init__.py +0 -0
  272. devin/nodes/ideation/__init__.py +0 -0
  273. devin/nodes/ideation/node.py +195 -0
  274. devin/nodes/ideation/playground.py +267 -0
  275. devin/nodes/ideation/prompt.md +65 -0
  276. devin/nodes/ideation/scenarios/continue_refinement.py +13 -0
  277. devin/nodes/ideation/scenarios/continue_refinement_evals.py +18 -0
  278. devin/nodes/ideation/scenarios/idea_fits_existing_patterns.py +17 -0
  279. devin/nodes/ideation/scenarios/idea_fits_existing_patterns_evals.py +16 -0
  280. devin/nodes/ideation/scenarios/large_idea_split.py +4 -0
  281. devin/nodes/ideation/scenarios/large_idea_split_evals.py +17 -0
  282. devin/nodes/ideation/scenarios/source_documentation_added.py +4 -0
  283. devin/nodes/ideation/scenarios/source_documentation_added_evals.py +16 -0
  284. devin/nodes/ideation/scenarios/user_says_create_it.py +30 -0
  285. devin/nodes/ideation/scenarios/user_says_create_it_evals.py +23 -0
  286. devin/nodes/ideation/scenarios/vague_idea.py +16 -0
  287. devin/nodes/ideation/scenarios/vague_idea_evals.py +47 -0
  288. devin/nodes/ideation/tools.json +312 -0
  289. devin/nodes/insight/__init__.py +0 -0
  290. devin/nodes/insight/node.py +49 -0
  291. devin/nodes/insight/playground.py +154 -0
  292. devin/nodes/insight/prompt.md +61 -0
  293. devin/nodes/insight/scenarios/architecture_pattern_query.py +15 -0
  294. devin/nodes/insight/scenarios/architecture_pattern_query_evals.py +25 -0
  295. devin/nodes/insight/scenarios/codebase_exploration.py +15 -0
  296. devin/nodes/insight/scenarios/codebase_exploration_evals.py +23 -0
  297. devin/nodes/insight/scenarios/devin_ideation_routing.py +19 -0
  298. devin/nodes/insight/scenarios/devin_ideation_routing_evals.py +39 -0
  299. devin/nodes/insight/scenarios/devin_insight_routing.py +20 -0
  300. devin/nodes/insight/scenarios/devin_insight_routing_evals.py +40 -0
  301. devin/nodes/insight/scenarios/operational_debugging.py +15 -0
  302. devin/nodes/insight/scenarios/operational_debugging_evals.py +23 -0
  303. devin/nodes/insight/scenarios/operational_question.py +9 -0
  304. devin/nodes/insight/scenarios/operational_question_evals.py +8 -0
  305. devin/nodes/insight/scenarios/queue_status.py +15 -0
  306. devin/nodes/insight/scenarios/queue_status_evals.py +23 -0
  307. devin/nodes/insight/scenarios/source_doc_explanation.py +14 -0
  308. devin/nodes/insight/scenarios/source_doc_explanation_evals.py +21 -0
  309. devin/nodes/insight/scenarios/worker_state_check.py +15 -0
  310. devin/nodes/insight/scenarios/worker_state_check_evals.py +22 -0
  311. devin/nodes/insight/tools.json +126 -0
  312. devin/nodes/intake/__init__.py +0 -0
  313. devin/nodes/intake/node.py +27 -0
  314. devin/nodes/intake/playground.py +47 -0
  315. devin/nodes/intake/prompt.md +12 -0
  316. devin/nodes/intake/scenarios/ideation_routing.py +4 -0
  317. devin/nodes/intake/scenarios/ideation_routing_evals.py +5 -0
  318. devin/nodes/intake/scenarios/insight_routing.py +4 -0
  319. devin/nodes/intake/scenarios/insight_routing_evals.py +5 -0
  320. devin/nodes/iterate/README.md +44 -0
  321. devin/nodes/iterate/__init__.py +1 -0
  322. devin/nodes/iterate/_archived_design_stages/01-objectives-requirements.md +112 -0
  323. devin/nodes/iterate/_archived_design_stages/02-evals.md +131 -0
  324. devin/nodes/iterate/_archived_design_stages/03-tools-and-boundaries.md +110 -0
  325. devin/nodes/iterate/_archived_design_stages/04-harness-and-playground.md +32 -0
  326. devin/nodes/iterate/_archived_design_stages/05-prompt-deferred.md +11 -0
  327. devin/nodes/iterate/_archived_design_stages/coder_agent_design/01-objectives-requirements.md +20 -0
  328. devin/nodes/iterate/_archived_design_stages/coder_agent_design/02-evals.md +8 -0
  329. devin/nodes/iterate/_archived_design_stages/coder_agent_design/03-tools-and-boundaries.md +14 -0
  330. devin/nodes/iterate/_archived_design_stages/coder_agent_design/04-harness-and-playground.md +12 -0
  331. devin/nodes/iterate/_archived_design_stages/framer_agent_design/01-objectives-requirements.md +20 -0
  332. devin/nodes/iterate/_archived_design_stages/framer_agent_design/02-evals.md +8 -0
  333. devin/nodes/iterate/_archived_design_stages/framer_agent_design/03-tools-and-boundaries.md +13 -0
  334. devin/nodes/iterate/_archived_design_stages/framer_agent_design/04-harness-and-playground.md +12 -0
  335. devin/nodes/iterate/_archived_design_stages/iterator_agent_design/01-objectives-requirements.md +25 -0
  336. devin/nodes/iterate/_archived_design_stages/iterator_agent_design/02-evals.md +9 -0
  337. devin/nodes/iterate/_archived_design_stages/iterator_agent_design/03-tools-and-boundaries.md +14 -0
  338. devin/nodes/iterate/_archived_design_stages/iterator_agent_design/04-harness-and-playground.md +12 -0
  339. devin/nodes/iterate/_archived_design_stages/observer_agent_design/01-objectives-requirements.md +20 -0
  340. devin/nodes/iterate/_archived_design_stages/observer_agent_design/02-evals.md +8 -0
  341. devin/nodes/iterate/_archived_design_stages/observer_agent_design/03-tools-and-boundaries.md +14 -0
  342. devin/nodes/iterate/_archived_design_stages/observer_agent_design/04-harness-and-playground.md +13 -0
  343. devin/nodes/iterate/agent-roles.md +89 -0
  344. devin/nodes/iterate/agents/README.md +10 -0
  345. devin/nodes/iterate/artifacts.md +504 -0
  346. devin/nodes/iterate/contract.md +100 -0
  347. devin/nodes/iterate/eval-plan.md +74 -0
  348. devin/nodes/iterate/node.py +100 -0
  349. devin/nodes/iterate/pipeline/README.md +13 -0
  350. devin/nodes/iterate/playground-contract.md +76 -0
  351. devin/nodes/iterate/prompt.md +11 -0
  352. devin/nodes/iterate/scenarios/README.md +38 -0
  353. devin/nodes/iterate/scenarios/artifact-and-loop-scenarios.md +101 -0
  354. devin/nodes/iterate/scenarios/coder_artifact_alignment.py +32 -0
  355. devin/nodes/iterate/scenarios/coder_artifact_alignment_evals.py +45 -0
  356. devin/nodes/iterate/scenarios/coder_bounded_fix.py +27 -0
  357. devin/nodes/iterate/scenarios/coder_bounded_fix_evals.py +45 -0
  358. devin/nodes/iterate/scenarios/devin_iterate_routing.py +21 -0
  359. devin/nodes/iterate/scenarios/devin_iterate_routing_evals.py +36 -0
  360. devin/nodes/iterate/scenarios/framer_scope_boundary.py +25 -0
  361. devin/nodes/iterate/scenarios/framer_scope_boundary_evals.py +57 -0
  362. devin/nodes/iterate/scenarios/framer_task_framing.py +25 -0
  363. devin/nodes/iterate/scenarios/framer_task_framing_evals.py +58 -0
  364. devin/nodes/iterate/scenarios/iterate_error_fix.py +21 -0
  365. devin/nodes/iterate/scenarios/iterate_error_fix_evals.py +39 -0
  366. devin/nodes/iterate/scenarios/iterate_quick_change.py +21 -0
  367. devin/nodes/iterate/scenarios/iterate_quick_change_evals.py +35 -0
  368. devin/nodes/iterate/scenarios/iterate_to_idea_promotion.py +23 -0
  369. devin/nodes/iterate/scenarios/iterate_to_idea_promotion_evals.py +53 -0
  370. devin/nodes/iterate/scenarios/iterate_to_insight_reroute.py +23 -0
  371. devin/nodes/iterate/scenarios/iterate_to_insight_reroute_evals.py +53 -0
  372. devin/nodes/iterate/scenarios/observer_evidence_seam.py +28 -0
  373. devin/nodes/iterate/scenarios/observer_evidence_seam_evals.py +55 -0
  374. devin/nodes/iterate/scenarios/observer_repro_creation.py +28 -0
  375. devin/nodes/iterate/scenarios/observer_repro_creation_evals.py +45 -0
  376. devin/nodes/iterate/scenarios/routing-matrix.md +45 -0
  377. devin/nodes/shared/__init__.py +0 -0
  378. devin/nodes/shared/filemaker_expert.md +80 -0
  379. devin/nodes/shared/filemaker_expert.py +354 -0
  380. devin/nodes/shared/filemaker_expert_eval/runner.py +176 -0
  381. devin/nodes/shared/filemaker_expert_eval/scenarios.json +65 -0
  382. devin/nodes/shared/goldilocks_advisor_eval/runner.py +214 -0
  383. devin/nodes/shared/goldilocks_advisor_eval/scenarios.json +58 -0
  384. devin/nodes/shared/helpers.py +156 -0
  385. devin/nodes/shared/idea_compliance_advisor_eval/runner.py +252 -0
  386. devin/nodes/shared/idea_compliance_advisor_eval/scenarios.json +75 -0
  387. devin/nodes/shared/models.py +44 -0
  388. devin/nodes/shared/post.py +40 -0
  389. devin/nodes/shared/router.py +107 -0
  390. devin/nodes/shared/tools.py +191 -0
  391. devin/shared/devin-chat-rubric.md +237 -0
  392. devin/shared/devin-chat-scenario-suite.md +90 -0
  393. devin/shared/eval_doctrine.md +9 -0
@@ -0,0 +1,1317 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from devflow_engine.stores.execution_store import ExecutionStore
11
+
12
+ DAG_ID = "post_integration_playwright_workflow"
13
+ DAG_VERSION = "0.1"
14
+ UI_SOURCE_SUFFIXES = {".tsx", ".jsx", ".vue", ".svelte", ".html"}
15
+ BUSINESS_CONTROL_CATEGORIES = {
16
+ "navigation",
17
+ "filter-state",
18
+ "mutation",
19
+ "workflow-step",
20
+ "role-gated",
21
+ "status-gated",
22
+ "seeded-content",
23
+ "external-action",
24
+ }
25
+ EXCLUDED_DIR_PARTS = {
26
+ ".devflow",
27
+ ".git",
28
+ ".next",
29
+ "build",
30
+ "coverage",
31
+ "dist",
32
+ "node_modules",
33
+ "playwright-report",
34
+ "test-results",
35
+ }
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class PostIntegrationPlaywrightDagResult:
40
+ exit_code: int
41
+ message: str
42
+ run_id: str
43
+ pipeline_dir: Path
44
+ artifacts: dict[str, str]
45
+
46
+
47
+ def run_post_integration_playwright_dag(
48
+ *,
49
+ repo_root: Path,
50
+ store: ExecutionStore,
51
+ project_id: str,
52
+ trigger_run_id: str | None = None,
53
+ ) -> PostIntegrationPlaywrightDagResult:
54
+ """Create post-integration Playwright planning artifacts from the implemented UI."""
55
+ repo_root = repo_root.resolve()
56
+ run = store.start_run(
57
+ kind="worker.post_integration_playwright",
58
+ repo_root=repo_root,
59
+ args={"project_id": project_id, "trigger_run_id": trigger_run_id, "dag_id": DAG_ID, "dag_version": DAG_VERSION},
60
+ )
61
+ pipeline_dir = repo_root / ".devflow" / "post_integration_playwright" / f"run_{run.run_id}"
62
+ current_dir = repo_root / ".devflow" / "post_integration_playwright" / "current"
63
+ pipeline_dir.mkdir(parents=True, exist_ok=True)
64
+ current_dir.mkdir(parents=True, exist_ok=True)
65
+
66
+ try:
67
+ guidance = _find_playwright_guidance(repo_root)
68
+ inventory = _merge_persisted_inventory(
69
+ current_inventory=_catalogue_interactables(repo_root=repo_root),
70
+ prior_inventory_path=current_dir / "ui_interactable_inventory.json",
71
+ )
72
+ filtered = _filter_business_logic_interactables(inventory)
73
+ workflows = _trace_workflows(filtered)
74
+ ledger = _build_ui_element_ledger(inventory=inventory, filtered=filtered, workflows=workflows)
75
+ verifier = _build_coverage_verifier(ledger=ledger)
76
+ generation_plan = _build_playwright_generation_plan(
77
+ repo_root=repo_root,
78
+ guidance_path=guidance,
79
+ inventory=inventory,
80
+ filtered=filtered,
81
+ workflows=workflows,
82
+ ledger=ledger,
83
+ verifier=verifier,
84
+ prior_preview_manifest_path=current_dir / "workflow_preview_manifest.json",
85
+ )
86
+ generated_files = _write_playwright_expectation_stubs(pipeline_dir=pipeline_dir, workflows=workflows)
87
+ generated_preview_files = _write_playwright_preview_specs(
88
+ pipeline_dir=pipeline_dir,
89
+ workflows=workflows,
90
+ preview_manifest=generation_plan["preview_video_driver"]["manifest"],
91
+ )
92
+
93
+ artifacts = {
94
+ "ui_interactable_inventory": _write_json(pipeline_dir / "ui_interactable_inventory.json", inventory),
95
+ "business_logic_interactable_filter": _write_json(
96
+ pipeline_dir / "business_logic_interactable_filter.json",
97
+ filtered,
98
+ ),
99
+ "ui_workflow_trace": _write_json(pipeline_dir / "ui_workflow_trace.json", workflows),
100
+ "ui_element_ledger": _write_json(pipeline_dir / "ui_element_ledger.json", ledger),
101
+ "playwright_coverage_verifier": _write_json(
102
+ pipeline_dir / "playwright_coverage_verifier.json",
103
+ verifier,
104
+ ),
105
+ "playwright_generation_plan": _write_json(
106
+ pipeline_dir / "playwright_generation_plan.json",
107
+ generation_plan,
108
+ ),
109
+ "generated_playwright_expectations": _write_json(
110
+ pipeline_dir / "generated_playwright_expectations.json",
111
+ {"files": generated_files},
112
+ ),
113
+ "generated_playwright_previews": _write_json(
114
+ pipeline_dir / "generated_playwright_previews.json",
115
+ {"files": generated_preview_files},
116
+ ),
117
+ "workflow_preview_manifest": _write_json(
118
+ pipeline_dir / "workflow_preview_manifest.json",
119
+ generation_plan["preview_video_driver"]["manifest"],
120
+ ),
121
+ }
122
+ _write_current_artifacts(
123
+ current_dir=current_dir,
124
+ inventory=inventory,
125
+ filtered=filtered,
126
+ workflows=workflows,
127
+ ledger=ledger,
128
+ verifier=verifier,
129
+ generation_plan=generation_plan,
130
+ generated_files=generated_files,
131
+ generated_preview_files=generated_preview_files,
132
+ )
133
+ store.mark_run_finished(run_id=run.run_id, status="succeeded")
134
+ return PostIntegrationPlaywrightDagResult(
135
+ exit_code=0,
136
+ message="post-integration Playwright workflow artifacts created",
137
+ run_id=run.run_id,
138
+ pipeline_dir=pipeline_dir,
139
+ artifacts=artifacts,
140
+ )
141
+ except Exception as exc:
142
+ error_path = pipeline_dir / "error.json"
143
+ error_path.write_text(json.dumps({"error": str(exc)}, indent=2, sort_keys=True) + "\n", encoding="utf-8")
144
+ store.mark_run_finished(run_id=run.run_id, status="failed")
145
+ return PostIntegrationPlaywrightDagResult(
146
+ exit_code=1,
147
+ message=str(exc),
148
+ run_id=run.run_id,
149
+ pipeline_dir=pipeline_dir,
150
+ artifacts={"error": str(error_path)},
151
+ )
152
+
153
+
154
+ def _write_json(path: Path, payload: Any) -> str:
155
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
156
+ return str(path)
157
+
158
+
159
+ def _write_current_artifacts(
160
+ *,
161
+ current_dir: Path,
162
+ inventory: dict[str, Any],
163
+ filtered: dict[str, Any],
164
+ workflows: dict[str, Any],
165
+ ledger: dict[str, Any],
166
+ verifier: dict[str, Any],
167
+ generation_plan: dict[str, Any],
168
+ generated_files: list[dict[str, str]],
169
+ generated_preview_files: list[dict[str, str]],
170
+ ) -> None:
171
+ _write_json(current_dir / "ui_interactable_inventory.json", inventory)
172
+ _write_json(current_dir / "business_logic_interactable_filter.json", filtered)
173
+ _write_json(current_dir / "ui_workflow_trace.json", workflows)
174
+ _write_json(current_dir / "ui_element_ledger.json", ledger)
175
+ _write_json(current_dir / "playwright_coverage_verifier.json", verifier)
176
+ _write_json(current_dir / "playwright_generation_plan.json", generation_plan)
177
+ _write_json(current_dir / "generated_playwright_expectations.json", {"files": generated_files})
178
+ _write_json(current_dir / "generated_playwright_previews.json", {"files": generated_preview_files})
179
+ _write_json(current_dir / "workflow_preview_manifest.json", generation_plan["preview_video_driver"]["manifest"])
180
+
181
+
182
+ def _source_files(repo_root: Path) -> list[Path]:
183
+ files: list[Path] = []
184
+ for path in repo_root.rglob("*"):
185
+ if not path.is_file() or path.suffix not in UI_SOURCE_SUFFIXES:
186
+ continue
187
+ if any(part in EXCLUDED_DIR_PARTS for part in path.relative_to(repo_root).parts):
188
+ continue
189
+ files.append(path)
190
+ return sorted(files)
191
+
192
+
193
+ def _catalogue_interactables(*, repo_root: Path) -> dict[str, Any]:
194
+ interactables: list[dict[str, Any]] = []
195
+ for path in _source_files(repo_root):
196
+ text = path.read_text(encoding="utf-8", errors="ignore")
197
+ relative = path.relative_to(repo_root).as_posix()
198
+ seen_signatures: dict[str, int] = {}
199
+ for match in _find_interactable_tags(text):
200
+ signature = _interactable_signature(relative=relative, item=match)
201
+ seen_signatures[signature] = seen_signatures.get(signature, 0) + 1
202
+ item_id = _stable_id(signature, str(seen_signatures[signature]))
203
+ interactables.append(
204
+ {
205
+ "id": item_id,
206
+ "path": relative,
207
+ "identity_signature": signature,
208
+ "logical_signature": _interactable_logical_signature(item=match),
209
+ **match,
210
+ }
211
+ )
212
+
213
+ return {
214
+ "kind": "ui_interactable_inventory.v1",
215
+ "process_step": 1,
216
+ "source_files_scanned": len(_source_files(repo_root)),
217
+ "interactables": interactables,
218
+ "summary": {"total": len(interactables)},
219
+ }
220
+
221
+
222
+ def _merge_persisted_inventory(*, current_inventory: dict[str, Any], prior_inventory_path: Path) -> dict[str, Any]:
223
+ current_items = list(current_inventory.get("interactables") or [])
224
+ prior_items: dict[str, dict[str, Any]] = {}
225
+ prior_missing: list[dict[str, Any]] = []
226
+ if prior_inventory_path.exists():
227
+ try:
228
+ prior_payload = json.loads(prior_inventory_path.read_text(encoding="utf-8"))
229
+ except Exception:
230
+ prior_payload = {}
231
+ if isinstance(prior_payload, dict):
232
+ prior_items = {
233
+ str(item.get("id")): dict(item)
234
+ for item in prior_payload.get("interactables", [])
235
+ if isinstance(item, dict) and item.get("id")
236
+ }
237
+ prior_missing = [
238
+ dict(item)
239
+ for item in prior_payload.get("missing_interactables", [])
240
+ if isinstance(item, dict) and item.get("id")
241
+ ]
242
+
243
+ moved: list[dict[str, str]] = []
244
+ claimed_prior_ids: set[str] = set()
245
+ prior_by_logical: dict[str, list[dict[str, Any]]] = {}
246
+ for prior in prior_items.values():
247
+ logical_signature = str(prior.get("logical_signature") or "")
248
+ if logical_signature:
249
+ prior_by_logical.setdefault(logical_signature, []).append(prior)
250
+
251
+ normalized_current_items: list[dict[str, Any]] = []
252
+ for current in current_items:
253
+ current_item = dict(current)
254
+ current_id = str(current_item.get("id") or "")
255
+ if current_id not in prior_items:
256
+ prior_match = _match_moved_prior_item(
257
+ current_item=current_item,
258
+ prior_candidates=prior_by_logical.get(str(current_item.get("logical_signature") or ""), []),
259
+ claimed_prior_ids=claimed_prior_ids,
260
+ )
261
+ if prior_match is not None:
262
+ prior_id = str(prior_match["id"])
263
+ current_item["id"] = prior_id
264
+ current_item["previous_path"] = str(prior_match.get("path") or "")
265
+ current_item["movement_status"] = "moved"
266
+ claimed_prior_ids.add(prior_id)
267
+ moved.append(
268
+ {
269
+ "id": prior_id,
270
+ "from_path": str(prior_match.get("path") or ""),
271
+ "to_path": str(current_item.get("path") or ""),
272
+ }
273
+ )
274
+ normalized_current_items.append(current_item)
275
+
276
+ current_items = normalized_current_items
277
+ current_ids = {str(item.get("id")) for item in current_items}
278
+ prior_ids = set(prior_items)
279
+ added_ids = sorted(current_ids.difference(prior_ids))
280
+ removed_ids = sorted(prior_ids.difference(current_ids))
281
+ moved_ids = {item["id"] for item in moved}
282
+ unchanged_ids = sorted(current_ids.intersection(prior_ids).difference(moved_ids))
283
+
284
+ active_items = [{**item, "status": "active"} for item in current_items]
285
+ missing_by_id = {str(item["id"]): {**item, "status": "missing"} for item in prior_missing}
286
+ for removed_id in removed_ids:
287
+ missing_by_id[removed_id] = {**prior_items[removed_id], "status": "missing"}
288
+
289
+ return {
290
+ **current_inventory,
291
+ "interactables": active_items,
292
+ "missing_interactables": sorted(missing_by_id.values(), key=lambda item: str(item.get("id"))),
293
+ "changes": {
294
+ "added_interactable_ids": added_ids,
295
+ "removed_interactable_ids": removed_ids,
296
+ "moved_interactable_ids": sorted(moved_ids),
297
+ "moved_interactables": moved,
298
+ "unchanged_interactable_ids": unchanged_ids,
299
+ },
300
+ "summary": {
301
+ "total": len(active_items),
302
+ "active": len(active_items),
303
+ "missing": len(missing_by_id),
304
+ "added": len(added_ids),
305
+ "removed": len(removed_ids),
306
+ "moved": len(moved),
307
+ "unchanged": len(unchanged_ids),
308
+ },
309
+ }
310
+
311
+
312
+ def _match_moved_prior_item(
313
+ *,
314
+ current_item: dict[str, Any],
315
+ prior_candidates: list[dict[str, Any]],
316
+ claimed_prior_ids: set[str],
317
+ ) -> dict[str, Any] | None:
318
+ current_path = str(current_item.get("path") or "")
319
+ for prior in prior_candidates:
320
+ prior_id = str(prior.get("id") or "")
321
+ if prior_id in claimed_prior_ids:
322
+ continue
323
+ if str(prior.get("path") or "") != current_path:
324
+ return prior
325
+ return None
326
+
327
+
328
+ def _interactable_signature(*, relative: str, item: dict[str, Any]) -> str:
329
+ payload = json.loads(_interactable_logical_signature(item=item))
330
+ payload["path"] = relative
331
+ return json.dumps(payload, sort_keys=True)
332
+
333
+
334
+ def _interactable_logical_signature(*, item: dict[str, Any]) -> str:
335
+ attrs = item.get("attributes") if isinstance(item.get("attributes"), dict) else {}
336
+ signature_attrs = {
337
+ key: attrs.get(key)
338
+ for key in ("aria-label", "data-testid", "name", "id", "href", "to", "type", "placeholder", "value")
339
+ if attrs.get(key)
340
+ }
341
+ return json.dumps(
342
+ {
343
+ "tag": item.get("tag"),
344
+ "label": item.get("label"),
345
+ "attributes": signature_attrs,
346
+ },
347
+ sort_keys=True,
348
+ )
349
+
350
+
351
+ def _find_interactable_tags(text: str) -> list[dict[str, Any]]:
352
+ pattern = re.compile(
353
+ r"<(?P<tag>button|a|input|select|textarea|form)\b(?P<attrs>[^>]*)>|role=[\"'](?P<role>button|link|checkbox|switch|tab|menuitem)[\"']",
354
+ re.IGNORECASE | re.MULTILINE,
355
+ )
356
+ items: list[dict[str, Any]] = []
357
+ for match in pattern.finditer(text):
358
+ attrs = match.group("attrs") or ""
359
+ tag = (match.group("tag") or match.group("role") or "role").lower()
360
+ line = text.count("\n", 0, match.start()) + 1
361
+ attributes = _parse_attributes(attrs)
362
+ raw_excerpt = text[match.start() : min(match.end() + 120, len(text))]
363
+ label = _derive_label(attributes=attributes, raw=raw_excerpt, tag=tag)
364
+ logic_signals = _logic_signals(tag=tag, attributes=attributes)
365
+ items.append(
366
+ {
367
+ "tag": tag,
368
+ "line": line,
369
+ "label": label,
370
+ "attributes": attributes,
371
+ "logic_signals": logic_signals,
372
+ "raw_excerpt": " ".join(text[match.start() : min(match.end() + 120, len(text))].split())[:240],
373
+ }
374
+ )
375
+ return items
376
+
377
+
378
+ def _parse_attributes(attrs: str) -> dict[str, str]:
379
+ parsed: dict[str, str] = {}
380
+ for name, value in re.findall(r"([:@A-Za-z0-9_\-\.]+)(?:=\{?['\"]?([^'\"\}\s>]+)['\"]?\}?)?", attrs):
381
+ parsed[name] = value or "true"
382
+ return parsed
383
+
384
+
385
+ def _derive_label(*, attributes: dict[str, str], raw: str, tag: str) -> str:
386
+ for key in ("aria-label", "data-testid", "name", "id", "placeholder", "href", "to", "value"):
387
+ value = attributes.get(key)
388
+ if value and value != "true":
389
+ return value
390
+ text_match = re.search(r">([^<>{]{1,80})<", raw)
391
+ if text_match:
392
+ return " ".join(text_match.group(1).split())
393
+ return tag
394
+
395
+
396
+ def _logic_signals(*, tag: str, attributes: dict[str, str]) -> list[str]:
397
+ signals: list[str] = []
398
+ for key in attributes:
399
+ lowered = key.lower()
400
+ if (
401
+ lowered.startswith("on")
402
+ or lowered in {"action", "href", "to", "name", "formaction", "type", "checked", "value"}
403
+ or lowered.startswith("data-testid")
404
+ ):
405
+ signals.append(key)
406
+ if tag in {"form", "select", "textarea"}:
407
+ signals.append(f"tag:{tag}")
408
+ return sorted(set(signals))
409
+
410
+
411
+ def _filter_business_logic_interactables(inventory: dict[str, Any]) -> dict[str, Any]:
412
+ kept: list[dict[str, Any]] = []
413
+ dropped: list[dict[str, Any]] = []
414
+ for item in inventory.get("interactables", []):
415
+ decision = _business_logic_decision(item)
416
+ output = {**item, "decision_reason": decision["reason"]}
417
+ if decision["keep"]:
418
+ kept.append(output)
419
+ else:
420
+ dropped.append(output)
421
+ return {
422
+ "kind": "business_logic_interactable_filter.v1",
423
+ "process_step": 2,
424
+ "kept": kept,
425
+ "dropped": dropped,
426
+ "summary": {"kept": len(kept), "dropped": len(dropped)},
427
+ }
428
+
429
+
430
+ def _business_logic_decision(item: dict[str, Any]) -> dict[str, Any]:
431
+ attrs = item.get("attributes") if isinstance(item.get("attributes"), dict) else {}
432
+ signals = set(item.get("logic_signals") or [])
433
+ tag = str(item.get("tag") or "")
434
+ input_type = str(attrs.get("type") or "").lower()
435
+ if attrs.get("disabled") == "true" or attrs.get("aria-disabled") == "true":
436
+ return {"keep": False, "reason": "disabled interactable does not currently affect behavior"}
437
+ if tag == "a" and attrs.get("href") not in {None, "", "#"}:
438
+ return {"keep": True, "reason": "navigation changes application state or workflow route"}
439
+ if tag == "button" and (
440
+ input_type == "submit"
441
+ or any(str(signal).lower().startswith("on") for signal in signals)
442
+ or {"formaction", "action", "href", "to"}.intersection(signals)
443
+ ):
444
+ return {"keep": True, "reason": "button submits, routes, or has an event handler"}
445
+ if tag == "input" and input_type in {"hidden", "button", "reset", "image"}:
446
+ return {"keep": False, "reason": f"input type {input_type} is not a user business input"}
447
+ if tag in {"input", "select", "textarea", "form"} and (signals or attrs.get("name") or attrs.get("data-testid")):
448
+ return {"keep": True, "reason": "form control participates in submitted or persisted user state"}
449
+ if signals:
450
+ return {"keep": True, "reason": "event or routing attribute indicates business behavior"}
451
+ return {"keep": False, "reason": "no business logic signal detected"}
452
+
453
+
454
+ def _trace_workflows(filtered: dict[str, Any]) -> dict[str, Any]:
455
+ grouped: dict[str, list[dict[str, Any]]] = {}
456
+ for item in filtered.get("kept", []):
457
+ grouped.setdefault(_workflow_key(item), []).append(item)
458
+
459
+ workflows: list[dict[str, Any]] = []
460
+ covered: set[str] = set()
461
+ for key, items in sorted(grouped.items()):
462
+ workflow_id = _stable_id("workflow", key)
463
+ item_ids = [str(item["id"]) for item in items]
464
+ covered.update(item_ids)
465
+ workflows.append(
466
+ {
467
+ "workflow_id": workflow_id,
468
+ "basis": "feature" if len(items) > 1 else "user",
469
+ "name": key.replace("/", " ").replace("-", " ").replace("_", " ").strip() or "ui workflow",
470
+ "source_paths": sorted({str(item["path"]) for item in items}),
471
+ "covered_interactable_ids": item_ids,
472
+ "seed_data_requirements": [
473
+ "Create baseline authenticated/authorized state when the UI route requires it.",
474
+ "Create each needed record, permission, or status inside the test before assertions.",
475
+ ],
476
+ "expectation_contract": [
477
+ "Assert the initial UI state before interacting.",
478
+ "Interact through accessible roles, labels, or stable test ids.",
479
+ "Assert the business effect in UI state and persisted/API-visible state when available.",
480
+ ],
481
+ }
482
+ )
483
+
484
+ return {
485
+ "kind": "ui_workflow_trace.v1",
486
+ "process_step": 3,
487
+ "workflows": workflows,
488
+ "coverage": {
489
+ "kept_interactable_ids": [str(item["id"]) for item in filtered.get("kept", [])],
490
+ "covered_interactable_ids": sorted(covered),
491
+ "uncovered_interactable_ids": sorted(
492
+ {str(item["id"]) for item in filtered.get("kept", [])}.difference(covered)
493
+ ),
494
+ },
495
+ }
496
+
497
+
498
+ def _build_ui_element_ledger(
499
+ *,
500
+ inventory: dict[str, Any],
501
+ filtered: dict[str, Any],
502
+ workflows: dict[str, Any],
503
+ ) -> dict[str, Any]:
504
+ kept_ids = {str(item.get("id")) for item in filtered.get("kept", [])}
505
+ workflow_by_interactable = _workflow_by_interactable(workflows)
506
+ rows: list[dict[str, Any]] = []
507
+ all_items = [*list(inventory.get("interactables", [])), *list(inventory.get("missing_interactables", []))]
508
+ for item in all_items:
509
+ item_id = str(item.get("id") or "")
510
+ category = _classify_ui_element(item=item, kept=item_id in kept_ids)
511
+ source_path = str(item.get("path") or "")
512
+ selector = _preferred_selector(item)
513
+ expected_behavior_source, confidence, assumption = _expected_behavior_source(category=category, item=item)
514
+ required_proof = _required_proof_for_category(category)
515
+ workflow_id = workflow_by_interactable.get(item_id)
516
+ priority = _priority_for_category(category)
517
+ coverage_status = _coverage_status(
518
+ category=category,
519
+ workflow_id=workflow_id,
520
+ item=item,
521
+ )
522
+ test_id = f"PW-{item_id}" if coverage_status == "covered" and category in BUSINESS_CONTROL_CATEGORIES else None
523
+ rows.append(
524
+ {
525
+ "id": item_id,
526
+ "route": _route_for_item(item),
527
+ "source": source_path,
528
+ "sourceCitation": f"{source_path}:{item.get('line')}",
529
+ "element": str(item.get("label") or item.get("tag") or ""),
530
+ "selector": selector,
531
+ "role": _role_for_item(item),
532
+ "href": _item_attrs(item).get("href") or _item_attrs(item).get("to"),
533
+ "handler": _handler_for_item(item),
534
+ "apiBoundary": _api_boundary_for_item(item),
535
+ "stateTouched": _state_touched_for_item(item),
536
+ "conditionalRule": _conditional_rule_for_item(item),
537
+ "dataSource": _data_source_for_item(item),
538
+ "currentBehavior": _current_behavior_for_item(category=category, item=item),
539
+ "expectedBehavior": _expected_behavior_for_item(category=category, item=item),
540
+ "expectedBehaviorSource": expected_behavior_source,
541
+ "confidence": confidence,
542
+ "assumption": assumption,
543
+ "classification": category,
544
+ "priority": priority,
545
+ "requiredProof": required_proof,
546
+ "testId": test_id,
547
+ "coverageStatus": coverage_status,
548
+ "workflowId": workflow_id,
549
+ "evidence": _ledger_evidence(
550
+ coverage_status=coverage_status,
551
+ workflow_id=workflow_id,
552
+ source_path=source_path,
553
+ ),
554
+ }
555
+ )
556
+
557
+ return {
558
+ "kind": "ui_element_ledger.v1",
559
+ "process_step": 4,
560
+ "rows": rows,
561
+ "summary": {
562
+ "total": len(rows),
563
+ "business_controls": sum(1 for row in rows if row["classification"] in BUSINESS_CONTROL_CATEGORIES),
564
+ "high_medium_business_controls": sum(
565
+ 1
566
+ for row in rows
567
+ if row["classification"] in BUSINESS_CONTROL_CATEGORIES and row["priority"] in {"HIGH", "MEDIUM"}
568
+ ),
569
+ "unknown": sum(1 for row in rows if row["classification"] == "unknown"),
570
+ "covered": sum(1 for row in rows if row["coverageStatus"] == "covered"),
571
+ "uncovered": sum(1 for row in rows if row["coverageStatus"] == "uncovered"),
572
+ },
573
+ }
574
+
575
+
576
+ def _build_coverage_verifier(*, ledger: dict[str, Any]) -> dict[str, Any]:
577
+ rows = [row for row in ledger.get("rows", []) if isinstance(row, dict)]
578
+
579
+ def count_where(predicate: Any) -> int:
580
+ return sum(1 for row in rows if predicate(row))
581
+
582
+ failure_conditions = {
583
+ "unknown_count": count_where(lambda row: row.get("classification") == "unknown"),
584
+ "business_control_without_requiredProof": count_where(
585
+ lambda row: _is_business_row(row) and not row.get("requiredProof")
586
+ ),
587
+ "business_control_without_expectedBehaviorSource": count_where(
588
+ lambda row: _is_business_row(row) and not row.get("expectedBehaviorSource")
589
+ ),
590
+ "best_practice_assumption_without_assumption_note": count_where(
591
+ lambda row: row.get("expectedBehaviorSource") == "best-practice-assumption" and not row.get("assumption")
592
+ ),
593
+ "low_confidence_expected_behavior_not_surfaced": count_where(
594
+ lambda row: row.get("confidence") == "low" and not row.get("assumption")
595
+ ),
596
+ "business_control_without_test_or_disabled_note": count_where(
597
+ lambda row: _is_business_row(row)
598
+ and row.get("priority") in {"HIGH", "MEDIUM"}
599
+ and row.get("coverageStatus") not in {"covered", "intentionally-disabled"}
600
+ ),
601
+ "covered_row_without_test_id": count_where(
602
+ lambda row: _is_business_row(row) and row.get("coverageStatus") == "covered" and not row.get("testId")
603
+ ),
604
+ "seeded_content_allows_empty_state": count_where(
605
+ lambda row: row.get("classification") == "seeded-content"
606
+ and "fail when required seeded data is missing" not in str(row.get("requiredProof") or "")
607
+ ),
608
+ "status_gated_without_matrix": count_where(
609
+ lambda row: row.get("classification") == "status-gated"
610
+ and "show/hide action matrix" not in str(row.get("requiredProof") or "")
611
+ ),
612
+ "filter_state_without_reset_assertion": count_where(
613
+ lambda row: row.get("classification") == "filter-state"
614
+ and "reset" not in str(row.get("requiredProof") or "").lower()
615
+ ),
616
+ "mutation_without_state_or_request_assertion": count_where(
617
+ lambda row: row.get("classification") == "mutation"
618
+ and "durable state" not in str(row.get("requiredProof") or "")
619
+ ),
620
+ "role_gated_without_negative_assertion": count_where(
621
+ lambda row: row.get("classification") == "role-gated"
622
+ and "disallowed role" not in str(row.get("requiredProof") or "")
623
+ ),
624
+ "external_action_without_observable_side_effect": count_where(
625
+ lambda row: row.get("classification") == "external-action"
626
+ and "observable side effect" not in str(row.get("requiredProof") or "")
627
+ ),
628
+ }
629
+ unresolved = {key: value for key, value in failure_conditions.items() if value > 0}
630
+ return {
631
+ "kind": "playwright_coverage_verifier.v1",
632
+ "process_step": 5,
633
+ "passed": not unresolved,
634
+ "failure_conditions": failure_conditions,
635
+ "unresolved_failure_conditions": unresolved,
636
+ "stopping_rule": (
637
+ "Post-integration Playwright coverage is incomplete while any verifier failure condition is non-zero "
638
+ "unless each remaining row is explicitly promoted to a source-cited TODO with a test plan."
639
+ ),
640
+ }
641
+
642
+
643
+ def _workflow_by_interactable(workflows: dict[str, Any]) -> dict[str, str]:
644
+ mapping: dict[str, str] = {}
645
+ for workflow in workflows.get("workflows", []):
646
+ workflow_id = str(workflow.get("workflow_id") or "")
647
+ for item_id in workflow.get("covered_interactable_ids", []):
648
+ mapping[str(item_id)] = workflow_id
649
+ return mapping
650
+
651
+
652
+ def _is_business_row(row: dict[str, Any]) -> bool:
653
+ return str(row.get("classification") or "") in BUSINESS_CONTROL_CATEGORIES
654
+
655
+
656
+ def _item_attrs(item: dict[str, Any]) -> dict[str, str]:
657
+ attrs = item.get("attributes")
658
+ return attrs if isinstance(attrs, dict) else {}
659
+
660
+
661
+ def _classify_ui_element(*, item: dict[str, Any], kept: bool) -> str:
662
+ attrs = _item_attrs(item)
663
+ tag = str(item.get("tag") or "").lower()
664
+ raw = str(item.get("raw_excerpt") or "").lower()
665
+ signals = {str(signal).lower() for signal in item.get("logic_signals") or []}
666
+ if attrs.get("disabled") == "true" or attrs.get("aria-disabled") == "true":
667
+ return "display-only"
668
+ if attrs.get("href") or attrs.get("to"):
669
+ return "navigation"
670
+ if tag in {"select"} or any(token in raw for token in ("filter", "sort", "search", "combobox")):
671
+ return "filter-state"
672
+ if tag == "form" or attrs.get("type") == "submit" or attrs.get("action") or attrs.get("formaction"):
673
+ return "mutation"
674
+ if any(token in raw for token in ("dialog", "modal", "drawer", "wizard", "menu", "dropdown")):
675
+ return "workflow-step"
676
+ if any(token in raw for token in ("role", "permission", "admin", "manager", "viewer")):
677
+ return "role-gated"
678
+ if any(token in raw for token in ("status", "draft", "scheduled", "active", "completed", "closed")):
679
+ return "status-gated"
680
+ if any(token in raw for token in ("download", "copy", "clipboard", "upload", "email", "calendar", "qr")):
681
+ return "external-action"
682
+ if any(token in raw for token in ("seed", "record", "team", "user", "report", "analysis", "score")):
683
+ return "seeded-content"
684
+ if kept:
685
+ return "workflow-step" if tag in {"button", "input", "textarea"} or signals else "navigation"
686
+ return "display-only"
687
+
688
+
689
+ def _priority_for_category(category: str) -> str:
690
+ if category in {"mutation", "workflow-step", "role-gated", "status-gated", "filter-state"}:
691
+ return "HIGH"
692
+ if category in {"navigation", "seeded-content", "external-action"}:
693
+ return "MEDIUM"
694
+ if category == "display-only":
695
+ return "SKIP"
696
+ return "LOW"
697
+
698
+
699
+ def _required_proof_for_category(category: str) -> str:
700
+ proof = {
701
+ "display-only": "Render check only when the content is review-critical or visually fragile.",
702
+ "navigation": "Click reaches the correct route/surface and preserves required org/group/entity context.",
703
+ "filter-state": "Options render from real data; selecting changes downstream records; reset restores scope.",
704
+ "mutation": "Trigger causes durable state change, API request, or visible confirmed result.",
705
+ "workflow-step": (
706
+ "Surface opens, validates required fields, advances/cancels correctly, and asserts final state."
707
+ ),
708
+ "role-gated": "Allowed role sees/uses the control; disallowed role cannot see or use it before mutation.",
709
+ "status-gated": "Every relevant status has a show/hide action matrix with positive and negative assertions.",
710
+ "seeded-content": "Real seeded data renders and tests fail when required seeded data is missing.",
711
+ "external-action": "Download/copy/upload/email/provider action has an observable side effect.",
712
+ "unknown": "Investigate source until the element can be classified or source-cited as disabled/TODO.",
713
+ }
714
+ return proof.get(category, proof["unknown"])
715
+
716
+
717
+ def _expected_behavior_source(*, category: str, item: dict[str, Any]) -> tuple[str, str, str | None]:
718
+ if category == "display-only":
719
+ return "class-2-code-implementation", "high", None
720
+ if category in BUSINESS_CONTROL_CATEGORIES:
721
+ return "class-2-code-implementation", "medium", None
722
+ return (
723
+ "best-practice-assumption",
724
+ "low",
725
+ "Classification could not be resolved from source; inspect source docs, implementation, and adjacent patterns.",
726
+ )
727
+
728
+
729
+ def _preferred_selector(item: dict[str, Any]) -> str:
730
+ attrs = _item_attrs(item)
731
+ if attrs.get("data-testid"):
732
+ return f"[data-testid=\"{attrs['data-testid']}\"]"
733
+ label = str(attrs.get("aria-label") or item.get("label") or "").strip()
734
+ role = _role_for_item(item)
735
+ if label and role:
736
+ return f"page.getByRole('{role}', {{ name: /{_regex_safe(label)}/i }})"
737
+ if label:
738
+ return f"page.getByLabel(/{_regex_safe(label)}/i)"
739
+ return f"{item.get('tag') or 'element'} at {item.get('path')}:{item.get('line')}"
740
+
741
+
742
+ def _role_for_item(item: dict[str, Any]) -> str | None:
743
+ tag = str(item.get("tag") or "").lower()
744
+ attrs = _item_attrs(item)
745
+ if attrs.get("role"):
746
+ return attrs["role"]
747
+ if tag == "button":
748
+ return "button"
749
+ if tag == "a":
750
+ return "link"
751
+ if tag == "select":
752
+ return "combobox"
753
+ if tag in {"input", "textarea"}:
754
+ return "textbox"
755
+ return None
756
+
757
+
758
+ def _handler_for_item(item: dict[str, Any]) -> str | None:
759
+ attrs = _item_attrs(item)
760
+ handlers = [key for key in sorted(attrs) if key.lower().startswith("on") or key in {"action", "formaction"}]
761
+ return ",".join(handlers) if handlers else None
762
+
763
+
764
+ def _api_boundary_for_item(item: dict[str, Any]) -> str | None:
765
+ attrs = _item_attrs(item)
766
+ return attrs.get("action") or attrs.get("formaction") or None
767
+
768
+
769
+ def _state_touched_for_item(item: dict[str, Any]) -> list[str]:
770
+ attrs = _item_attrs(item)
771
+ return [value for key, value in sorted(attrs.items()) if key in {"name", "value", "checked"} and value]
772
+
773
+
774
+ def _conditional_rule_for_item(item: dict[str, Any]) -> str | None:
775
+ raw = str(item.get("raw_excerpt") or "")
776
+ for token in ("disabled", "aria-disabled", "role", "permission", "status"):
777
+ if token in raw:
778
+ return f"source excerpt contains {token}"
779
+ return None
780
+
781
+
782
+ def _data_source_for_item(item: dict[str, Any]) -> str | None:
783
+ attrs = _item_attrs(item)
784
+ return attrs.get("name") or attrs.get("href") or attrs.get("to") or None
785
+
786
+
787
+ def _current_behavior_for_item(*, category: str, item: dict[str, Any]) -> str:
788
+ label = str(item.get("label") or item.get("tag") or "element")
789
+ return f"{label} is present in source and classified as {category} from its tag/attributes."
790
+
791
+
792
+ def _expected_behavior_for_item(*, category: str, item: dict[str, Any]) -> str:
793
+ label = str(item.get("label") or item.get("tag") or "element")
794
+ return f"{label} should satisfy the {category} proof requirement: {_required_proof_for_category(category)}"
795
+
796
+
797
+ def _coverage_status(*, category: str, workflow_id: str | None, item: dict[str, Any]) -> str:
798
+ attrs = _item_attrs(item)
799
+ if item.get("status") == "missing":
800
+ return "uncovered"
801
+ if attrs.get("disabled") == "true" or attrs.get("aria-disabled") == "true":
802
+ return "intentionally-disabled"
803
+ if category == "display-only":
804
+ return "covered"
805
+ if workflow_id:
806
+ return "covered"
807
+ return "uncovered"
808
+
809
+
810
+ def _ledger_evidence(*, coverage_status: str, workflow_id: str | None, source_path: str) -> str:
811
+ if coverage_status == "covered" and workflow_id:
812
+ return f"Mapped to generated workflow {workflow_id}; replace scaffold with calibrated Playwright proof."
813
+ if coverage_status == "intentionally-disabled":
814
+ return f"Source marks control disabled in {source_path}; keep source-cited disabled note."
815
+ return f"Source-cited from {source_path}; needs workflow/test mapping or source-cited TODO."
816
+
817
+
818
+ def _route_for_item(item: dict[str, Any]) -> str:
819
+ path = str(item.get("path") or "")
820
+ if path.startswith("app/"):
821
+ route = path.removeprefix("app/")
822
+ for suffix in ("/page.tsx", "/page.jsx", "/page.vue", "/page.svelte", "/page.html"):
823
+ route = route.removesuffix(suffix)
824
+ return "/" + route.strip("/")
825
+ return _workflow_key(item)
826
+
827
+
828
+ def _regex_safe(value: str) -> str:
829
+ return re.escape(value.strip("/") or value)
830
+
831
+
832
+ def _workflow_key(item: dict[str, Any]) -> str:
833
+ path = str(item.get("path") or "")
834
+ parts = Path(path).parts
835
+ for marker in ("app", "pages", "routes", "src"):
836
+ if marker in parts:
837
+ idx = parts.index(marker)
838
+ return "/".join(parts[idx + 1 : -1] or parts[idx + 1 :] or [path])
839
+ return str(Path(path).parent)
840
+
841
+
842
+ def _build_playwright_generation_plan(
843
+ *,
844
+ repo_root: Path,
845
+ guidance_path: Path | None,
846
+ inventory: dict[str, Any],
847
+ filtered: dict[str, Any],
848
+ workflows: dict[str, Any],
849
+ ledger: dict[str, Any],
850
+ verifier: dict[str, Any],
851
+ prior_preview_manifest_path: Path,
852
+ ) -> dict[str, Any]:
853
+ rewrite_driver = _build_playwright_rewrite_driver(inventory=inventory, workflows=workflows)
854
+ preview_video_driver = _build_preview_video_driver(
855
+ repo_root=repo_root,
856
+ workflows=workflows,
857
+ rewrite_driver=rewrite_driver,
858
+ prior_preview_manifest_path=prior_preview_manifest_path,
859
+ )
860
+ return {
861
+ "kind": "playwright_generation_plan.v1",
862
+ "process_step": 4,
863
+ "playwright_guidance": {
864
+ "path": str(guidance_path.relative_to(repo_root)) if guidance_path else None,
865
+ "status": "found" if guidance_path else "missing",
866
+ "lookup_names": ["how to playwright right.md", "HOW_TO_PLAYWRIGHT_RIGHT.md"],
867
+ },
868
+ "inputs": {
869
+ "inventory_total": inventory.get("summary", {}).get("total", 0),
870
+ "business_logic_interactables": filtered.get("summary", {}).get("kept", 0),
871
+ "workflow_count": len(workflows.get("workflows", [])),
872
+ "ledger_rows": ledger.get("summary", {}).get("total", 0),
873
+ "verifier_passed": verifier.get("passed"),
874
+ },
875
+ "rewrite_driver": rewrite_driver,
876
+ "coverage_verifier": verifier,
877
+ "ledger_summary": ledger.get("summary", {}),
878
+ "flow_doc_contract": _flow_doc_contract(),
879
+ "proof_calibration_plan": _proof_calibration_plan(),
880
+ "deep_business_control_pass": _deep_business_control_pass(ledger=ledger),
881
+ "preview_video_driver": preview_video_driver,
882
+ "test_authoring_rules": [
883
+ (
884
+ "Every visible UI element must be represented in ui_element_ledger before Playwright tests are "
885
+ "claimed complete."
886
+ ),
887
+ "Every HIGH/MEDIUM business control must map to a Playwright test id or source-cited disabled/TODO note.",
888
+ "Seeded/demo workflows must fail when required seed data is missing; empty states are explicit tests only.",
889
+ "Write expectations against business outcomes, not page load, clickability, labels, or mock behavior.",
890
+ "For filters, prove options, scoped data change, reset/unselect, and restored downstream state.",
891
+ "For mutations, assert durable state, request/API boundary, or visible confirmed result.",
892
+ "For role/status gates, assert both allowed and forbidden behavior.",
893
+ (
894
+ "Classify calibration failures as test-bug, implementation-gap, seed-fixture-gap, "
895
+ "route-config-gap, or environment-gap."
896
+ ),
897
+ "Generate workflow videos only after calibrated E2E proof and no unresolved HIGH/MEDIUM ledger gaps.",
898
+ "Prefer accessibility locators, then labels, then stable data-testid locators.",
899
+ ],
900
+ "output_target": ".devflow/post_integration_playwright/<run_id>/generated_tests",
901
+ }
902
+
903
+
904
+ def _flow_doc_contract() -> dict[str, Any]:
905
+ return {
906
+ "required_sections": [
907
+ "Mermaid Diagram",
908
+ "Context",
909
+ "Key Data Shapes",
910
+ "API Summary Table",
911
+ "State Transitions",
912
+ "Error Handling",
913
+ "Role Rules",
914
+ "Test Assertions",
915
+ "Coverage Matrix",
916
+ ],
917
+ "coverage_matrix_columns": [
918
+ "Control",
919
+ "Category",
920
+ "Preconditions",
921
+ "Expected browser behavior",
922
+ "Test file",
923
+ "Test ID",
924
+ ],
925
+ "source_of_truth": "Code is source of truth; update docs when docs and shipped code disagree.",
926
+ }
927
+
928
+
929
+ def _proof_calibration_plan() -> dict[str, Any]:
930
+ return {
931
+ "status": "required_after_verifier_zero",
932
+ "failure_classes": {
933
+ "test-bug": "Wrong/brittle locator or assertion; fix test and rerun.",
934
+ "implementation-gap": "Expected behavior is missing or broken; keep failing proof and document gap.",
935
+ "seed-fixture-gap": "Required seeded/demo data is absent or malformed; document fixture requirement.",
936
+ "route-config-gap": "Route/control expected by source/docs is not deployed or configured.",
937
+ "environment-gap": "Auth/service/browser/provider setup blocks execution; document rerun command/env.",
938
+ },
939
+ "completion_rule": (
940
+ "All test-bug failures are fixed and rerun; remaining failures are classified as product, seed, route, "
941
+ "or environment gaps without weakening assertions."
942
+ ),
943
+ }
944
+
945
+
946
+ def _deep_business_control_pass(*, ledger: dict[str, Any]) -> dict[str, Any]:
947
+ rows = [
948
+ row
949
+ for row in ledger.get("rows", [])
950
+ if isinstance(row, dict)
951
+ and row.get("classification") in BUSINESS_CONTROL_CATEGORIES
952
+ and row.get("priority") in {"HIGH", "MEDIUM"}
953
+ ]
954
+ return {
955
+ "status": "required",
956
+ "row_count": len(rows),
957
+ "ledger_row_ids": [str(row.get("id")) for row in rows],
958
+ "requirements_by_category": {
959
+ "workflow-step": (
960
+ "Open surface, assert title/content, required fields, submit/cancel/back, final visible state."
961
+ ),
962
+ "filter-state": "Options/data render, selection changes records, reset restores scope.",
963
+ "mutation": "Observe request/API boundary or durable state plus success/error UI.",
964
+ "role-gated": "Allowed role sees/uses; disallowed role does not see or is blocked before mutation.",
965
+ "status-gated": "Assert show/hide matrix for each relevant status.",
966
+ "navigation": "Reach expected route and preserve required context.",
967
+ "external-action": "Observe download/copy/upload/email/provider side effect.",
968
+ "seeded-content": "Real seeded data renders; missing seed data fails early.",
969
+ },
970
+ }
971
+
972
+
973
+ def _build_preview_video_driver(
974
+ *,
975
+ repo_root: Path,
976
+ workflows: dict[str, Any],
977
+ rewrite_driver: dict[str, Any],
978
+ prior_preview_manifest_path: Path,
979
+ ) -> dict[str, Any]:
980
+ e2e_status = _read_e2e_status(repo_root=repo_root)
981
+ manifest = _build_preview_manifest(
982
+ repo_root=repo_root,
983
+ workflows=workflows,
984
+ rewrite_driver=rewrite_driver,
985
+ prior_preview_manifest_path=prior_preview_manifest_path,
986
+ e2e_status=e2e_status,
987
+ )
988
+ return {
989
+ "status": "ready_to_record" if e2e_status["status"] == "passed" else "waiting_for_e2e_pass",
990
+ "e2e_gate": e2e_status,
991
+ "policy": {
992
+ "record_when": "all_e2e_pass",
993
+ "preserve_existing_video_when": "workflow_fingerprint_unchanged_and_video_exists",
994
+ "refresh_when": [
995
+ "new_workflow",
996
+ "impacted_by_rewrite_driver",
997
+ "missing_video",
998
+ "workflow_fingerprint_changed",
999
+ ],
1000
+ },
1001
+ "manifest": manifest,
1002
+ "record_command": (
1003
+ "npx playwright test .devflow/post_integration_playwright/current/generated_preview_specs "
1004
+ "--config=playwright.config.ts"
1005
+ ),
1006
+ }
1007
+
1008
+
1009
+ def _read_e2e_status(*, repo_root: Path) -> dict[str, Any]:
1010
+ status_path = repo_root / ".devflow" / "post_integration_playwright" / "current" / "e2e_status.json"
1011
+ if status_path.exists():
1012
+ try:
1013
+ payload = json.loads(status_path.read_text(encoding="utf-8"))
1014
+ except Exception:
1015
+ payload = {}
1016
+ if isinstance(payload, dict) and payload.get("status"):
1017
+ return {
1018
+ "status": str(payload.get("status")),
1019
+ "source": str(status_path.relative_to(repo_root)),
1020
+ "run_id": payload.get("run_id"),
1021
+ }
1022
+ return {"status": "unknown", "source": None, "run_id": None}
1023
+
1024
+
1025
+ def _build_preview_manifest(
1026
+ *,
1027
+ repo_root: Path,
1028
+ workflows: dict[str, Any],
1029
+ rewrite_driver: dict[str, Any],
1030
+ prior_preview_manifest_path: Path,
1031
+ e2e_status: dict[str, Any],
1032
+ ) -> dict[str, Any]:
1033
+ prior_by_workflow = _read_prior_preview_records(prior_preview_manifest_path)
1034
+ impacted = set(str(workflow_id) for workflow_id in rewrite_driver.get("impacted_workflow_ids", []))
1035
+ records: list[dict[str, Any]] = []
1036
+ stale: list[str] = []
1037
+ fresh: list[str] = []
1038
+ waiting: list[str] = []
1039
+ for workflow in workflows.get("workflows", []):
1040
+ workflow_id = str(workflow.get("workflow_id"))
1041
+ fingerprint = _workflow_fingerprint(workflow)
1042
+ prior = prior_by_workflow.get(workflow_id, {})
1043
+ prior_video_path = str(prior.get("video_path") or "")
1044
+ prior_video_exists = bool(prior_video_path) and (repo_root / prior_video_path).exists()
1045
+ unchanged = prior.get("workflow_fingerprint") == fingerprint
1046
+ needs_refresh = workflow_id in impacted or not prior_video_exists or not unchanged
1047
+ if not needs_refresh:
1048
+ status = "fresh"
1049
+ fresh.append(workflow_id)
1050
+ video_path = prior_video_path
1051
+ elif e2e_status.get("status") == "passed":
1052
+ status = "needs_recording"
1053
+ stale.append(workflow_id)
1054
+ video_path = _preview_video_path(workflow_id)
1055
+ else:
1056
+ status = "waiting_for_e2e_pass"
1057
+ waiting.append(workflow_id)
1058
+ video_path = prior_video_path or _preview_video_path(workflow_id)
1059
+ records.append(
1060
+ {
1061
+ "workflow_id": workflow_id,
1062
+ "workflow_fingerprint": fingerprint,
1063
+ "status": status,
1064
+ "video_path": video_path,
1065
+ "preview_spec_path": (
1066
+ ".devflow/post_integration_playwright/current/generated_preview_specs/"
1067
+ f"{workflow_id}.preview.spec.ts"
1068
+ ),
1069
+ "covered_interactable_ids": list(workflow.get("covered_interactable_ids", [])),
1070
+ "source_paths": list(workflow.get("source_paths", [])),
1071
+ "refresh_reasons": _preview_refresh_reasons(
1072
+ workflow_id=workflow_id,
1073
+ impacted=impacted,
1074
+ prior_video_exists=prior_video_exists,
1075
+ unchanged=unchanged,
1076
+ ),
1077
+ }
1078
+ )
1079
+ prior_ids = set(prior_by_workflow)
1080
+ active_ids = {str(workflow.get("workflow_id")) for workflow in workflows.get("workflows", [])}
1081
+ retired_ids = sorted(prior_ids.difference(active_ids))
1082
+ return {
1083
+ "kind": "workflow_preview_manifest.v1",
1084
+ "e2e_gate_status": e2e_status.get("status"),
1085
+ "records": records,
1086
+ "summary": {
1087
+ "total": len(records),
1088
+ "fresh": len(fresh),
1089
+ "needs_recording": len(stale),
1090
+ "waiting_for_e2e_pass": len(waiting),
1091
+ "retired": len(retired_ids),
1092
+ },
1093
+ "retired_workflow_ids": retired_ids,
1094
+ }
1095
+
1096
+
1097
+ def _read_prior_preview_records(path: Path) -> dict[str, dict[str, Any]]:
1098
+ if not path.exists():
1099
+ return {}
1100
+ try:
1101
+ payload = json.loads(path.read_text(encoding="utf-8"))
1102
+ except Exception:
1103
+ return {}
1104
+ if not isinstance(payload, dict):
1105
+ return {}
1106
+ return {
1107
+ str(record.get("workflow_id")): dict(record)
1108
+ for record in payload.get("records", [])
1109
+ if isinstance(record, dict) and record.get("workflow_id")
1110
+ }
1111
+
1112
+
1113
+ def _workflow_fingerprint(workflow: dict[str, Any]) -> str:
1114
+ return _stable_id(
1115
+ json.dumps(
1116
+ {
1117
+ "workflow_id": workflow.get("workflow_id"),
1118
+ "source_paths": workflow.get("source_paths", []),
1119
+ "covered_interactable_ids": workflow.get("covered_interactable_ids", []),
1120
+ "seed_data_requirements": workflow.get("seed_data_requirements", []),
1121
+ "expectation_contract": workflow.get("expectation_contract", []),
1122
+ },
1123
+ sort_keys=True,
1124
+ )
1125
+ )
1126
+
1127
+
1128
+ def _preview_video_path(workflow_id: str) -> str:
1129
+ return f".devflow/post_integration_playwright/current/videos/{workflow_id}.webm"
1130
+
1131
+
1132
+ def _preview_refresh_reasons(
1133
+ *,
1134
+ workflow_id: str,
1135
+ impacted: set[str],
1136
+ prior_video_exists: bool,
1137
+ unchanged: bool,
1138
+ ) -> list[str]:
1139
+ reasons: list[str] = []
1140
+ if workflow_id in impacted:
1141
+ reasons.append("impacted_by_rewrite_driver")
1142
+ if not prior_video_exists:
1143
+ reasons.append("missing_video")
1144
+ if not unchanged:
1145
+ reasons.append("workflow_fingerprint_changed")
1146
+ return reasons
1147
+
1148
+
1149
+ def _build_playwright_rewrite_driver(*, inventory: dict[str, Any], workflows: dict[str, Any]) -> dict[str, Any]:
1150
+ changes = inventory.get("changes") if isinstance(inventory.get("changes"), dict) else {}
1151
+ added_ids = list(changes.get("added_interactable_ids") or [])
1152
+ removed_ids = list(changes.get("removed_interactable_ids") or [])
1153
+ moved_items = list(changes.get("moved_interactables") or [])
1154
+ moved_ids = [str(item.get("id")) for item in moved_items if isinstance(item, dict)]
1155
+ impacted_workflows = _impacted_workflows(
1156
+ changed_ids=set(added_ids + removed_ids + moved_ids),
1157
+ workflows=workflows,
1158
+ )
1159
+ return {
1160
+ "status": "rewrite_required" if added_ids or removed_ids or moved_items else "no_rewrite_needed",
1161
+ "added_interactable_ids": added_ids,
1162
+ "removed_interactable_ids": removed_ids,
1163
+ "moved_interactables": moved_items,
1164
+ "impacted_workflow_ids": impacted_workflows,
1165
+ "rewrite_actions": _rewrite_actions(
1166
+ added_ids=added_ids,
1167
+ removed_ids=removed_ids,
1168
+ moved_items=moved_items,
1169
+ impacted_workflow_ids=impacted_workflows,
1170
+ ),
1171
+ }
1172
+
1173
+
1174
+ def _impacted_workflows(*, changed_ids: set[str], workflows: dict[str, Any]) -> list[str]:
1175
+ impacted: list[str] = []
1176
+ for workflow in workflows.get("workflows", []):
1177
+ covered = {str(item_id) for item_id in workflow.get("covered_interactable_ids", [])}
1178
+ if changed_ids.intersection(covered):
1179
+ impacted.append(str(workflow.get("workflow_id")))
1180
+ return sorted(set(impacted))
1181
+
1182
+
1183
+ def _rewrite_actions(
1184
+ *,
1185
+ added_ids: list[str],
1186
+ removed_ids: list[str],
1187
+ moved_items: list[Any],
1188
+ impacted_workflow_ids: list[str],
1189
+ ) -> list[dict[str, Any]]:
1190
+ actions: list[dict[str, Any]] = []
1191
+ if added_ids:
1192
+ actions.append(
1193
+ {
1194
+ "action": "add_expectations",
1195
+ "interactable_ids": added_ids,
1196
+ "workflow_ids": impacted_workflow_ids,
1197
+ "instruction": "Add workflow coverage and assertions for newly active business interactables.",
1198
+ }
1199
+ )
1200
+ if removed_ids:
1201
+ actions.append(
1202
+ {
1203
+ "action": "remove_or_replace_expectations",
1204
+ "interactable_ids": removed_ids,
1205
+ "workflow_ids": impacted_workflow_ids,
1206
+ "instruction": "Remove stale locators or replace them with active workflow-preserving interactables.",
1207
+ }
1208
+ )
1209
+ if moved_items:
1210
+ actions.append(
1211
+ {
1212
+ "action": "rewrite_moved_locators",
1213
+ "moved_interactables": moved_items,
1214
+ "workflow_ids": impacted_workflow_ids,
1215
+ "instruction": "Update route, fixture setup, and locator scope for interactables that moved surfaces.",
1216
+ }
1217
+ )
1218
+ return actions
1219
+
1220
+
1221
+ def _write_playwright_expectation_stubs(*, pipeline_dir: Path, workflows: dict[str, Any]) -> list[dict[str, str]]:
1222
+ generated_dir = pipeline_dir / "generated_tests"
1223
+ generated_dir.mkdir(parents=True, exist_ok=True)
1224
+ files: list[dict[str, str]] = []
1225
+ for workflow in workflows.get("workflows", []):
1226
+ workflow_id = str(workflow["workflow_id"])
1227
+ test_path = generated_dir / f"{workflow_id}.spec.ts"
1228
+ test_path.write_text(_playwright_stub(workflow), encoding="utf-8")
1229
+ files.append({"workflow_id": workflow_id, "path": str(test_path)})
1230
+ return files
1231
+
1232
+
1233
+ def _write_playwright_preview_specs(
1234
+ *,
1235
+ pipeline_dir: Path,
1236
+ workflows: dict[str, Any],
1237
+ preview_manifest: dict[str, Any],
1238
+ ) -> list[dict[str, str]]:
1239
+ generated_dir = pipeline_dir / "generated_preview_specs"
1240
+ generated_dir.mkdir(parents=True, exist_ok=True)
1241
+ records_by_workflow = {
1242
+ str(record.get("workflow_id")): record
1243
+ for record in preview_manifest.get("records", [])
1244
+ if isinstance(record, dict) and record.get("workflow_id")
1245
+ }
1246
+ files: list[dict[str, str]] = []
1247
+ for workflow in workflows.get("workflows", []):
1248
+ workflow_id = str(workflow["workflow_id"])
1249
+ record = records_by_workflow.get(workflow_id, {})
1250
+ if record.get("status") == "fresh":
1251
+ continue
1252
+ test_path = generated_dir / f"{workflow_id}.preview.spec.ts"
1253
+ test_path.write_text(_playwright_preview_stub(workflow=workflow, manifest_record=record), encoding="utf-8")
1254
+ files.append({"workflow_id": workflow_id, "path": str(test_path), "status": str(record.get("status") or "")})
1255
+ return files
1256
+
1257
+
1258
+ def _playwright_stub(workflow: dict[str, Any]) -> str:
1259
+ name = str(workflow.get("name") or "ui workflow")
1260
+ ids = ", ".join(str(item_id) for item_id in workflow.get("covered_interactable_ids", []))
1261
+ return (
1262
+ "import { test, expect } from '@playwright/test';\n\n"
1263
+ f"test.describe('{_ts_string(name)}', () => {{\n"
1264
+ f" test('covers business interactables: {_ts_string(ids)}', async ({{ page }}) => {{\n"
1265
+ " // Replace this scaffold with calibrated proof from ui_element_ledger.json.\n"
1266
+ " // Seed required business data first and fail early if expected seed records are missing.\n"
1267
+ " // Assert business state changes, negative role/status behavior, and reset/cancel paths as required.\n"
1268
+ " await page.goto('/');\n"
1269
+ " await expect(page).toHaveURL(/.*/);\n"
1270
+ " });\n"
1271
+ "});\n"
1272
+ )
1273
+
1274
+
1275
+ def _playwright_preview_stub(*, workflow: dict[str, Any], manifest_record: dict[str, Any]) -> str:
1276
+ name = str(workflow.get("name") or "ui workflow")
1277
+ workflow_id = str(workflow.get("workflow_id") or "")
1278
+ video_path = str(manifest_record.get("video_path") or _preview_video_path(workflow_id))
1279
+ ids = ", ".join(str(item_id) for item_id in workflow.get("covered_interactable_ids", []))
1280
+ return (
1281
+ "import { test, expect } from '@playwright/test';\n\n"
1282
+ "test.use({ video: 'on' });\n\n"
1283
+ f"test.describe('{_ts_string(name)} preview', () => {{\n"
1284
+ f" test('records workflow preview for {_ts_string(workflow_id)}', async ({{ page }}, testInfo) => {{\n"
1285
+ " test.skip(process.env.DEVFLOW_E2E_PASSED !== '1', 'Preview videos record only after E2E passes.');\n"
1286
+ f" testInfo.annotations.push({{ type: 'devflow-video-path', description: '{_ts_string(video_path)}' }});\n"
1287
+ f" testInfo.annotations.push({{ type: 'devflow-interactables', description: '{_ts_string(ids)}' }});\n"
1288
+ " // Record only after the expectation spec has calibrated real business-control proof.\n"
1289
+ " // Do not use video as source evidence or as a substitute for assertions.\n"
1290
+ " await page.goto('/');\n"
1291
+ " await expect(page).toHaveURL(/.*/);\n"
1292
+ " });\n"
1293
+ "});\n"
1294
+ )
1295
+
1296
+
1297
+ def _ts_string(value: str) -> str:
1298
+ return value.replace("\\", "\\\\").replace("'", "\\'")
1299
+
1300
+
1301
+ def _find_playwright_guidance(repo_root: Path) -> Path | None:
1302
+ exact_names = {"how to playwright right.md", "HOW_TO_PLAYWRIGHT_RIGHT.md"}
1303
+ candidates: list[Path] = []
1304
+ for path in repo_root.rglob("*.md"):
1305
+ if any(part in EXCLUDED_DIR_PARTS for part in path.relative_to(repo_root).parts):
1306
+ continue
1307
+ if path.name in exact_names:
1308
+ return path
1309
+ lower = path.name.lower()
1310
+ if "playwright" in lower and "right" in lower:
1311
+ candidates.append(path)
1312
+ return sorted(candidates)[0] if candidates else None
1313
+
1314
+
1315
+ def _stable_id(*parts: str) -> str:
1316
+ raw = ":".join(parts)
1317
+ return hashlib.sha1(raw.encode("utf-8")).hexdigest()[:12]