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,874 @@
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 pydantic import BaseModel
11
+
12
+ from ..devflow_state import publish_devflow_state
13
+ from ..stores.execution_store import ExecutionStore
14
+ from ..vendor.datalumina_genai.core.nodes.agent import AgentConfig, AgentNode
15
+ from ..vendor.datalumina_genai.core.nodes.base import Node
16
+ from ..vendor.datalumina_genai.core.schema import NodeConfig, WorkflowSchema
17
+ from ..vendor.datalumina_genai.core.task import TaskContext
18
+ from ..vendor.datalumina_genai.core.workflow import Workflow
19
+ from . import agentic as ui_grounding_agentic
20
+ from .models import (
21
+ CodeDesignGeneratedReference,
22
+ CodeDesignInventoryArtifact,
23
+ GroundedStoryReferencePatchArtifact,
24
+ UIGapReportArtifact,
25
+ UIGenerationManifestArtifact,
26
+ UIGroundingContextArtifact,
27
+ UIGroundingDagSummary,
28
+ UIGroundingReportArtifact,
29
+ UIGroundingSurfaceResult,
30
+ UIFlowMapArtifact,
31
+ UIReferenceInventoryArtifact,
32
+ UIReferenceInventoryItem,
33
+ UIScreenRelationshipsArtifact,
34
+ UIStructureContractArtifact,
35
+ UIStructureScreenItem,
36
+ UISurfaceDerivationArtifact,
37
+ DerivedUISurface,
38
+ )
39
+ from .pencil_bridge import run_pencil_preflight
40
+
41
+ DAG_ID = "ui_grounding_dag"
42
+ _CURRENT_STORE: ExecutionStore | None = None
43
+ _CURRENT_RUN_ID: str | None = None
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class UIGroundingDagResult:
48
+ exit_code: int
49
+ run_id: str
50
+ pipeline_dir: Path
51
+ message: str
52
+ outcome: dict[str, Any]
53
+
54
+
55
+ class UIGroundingDagEvent(BaseModel):
56
+ repo_root: str
57
+ project_id: str
58
+ idea_id: str
59
+ idea_path: str | None = None
60
+ idea_inline: dict[str, Any] | None = None
61
+ story_paths: list[str] = []
62
+ design_paths: list[str] = []
63
+ pipeline_key: str
64
+ allow_code_design_enrichment: bool = False
65
+ pencil_app_path: str | None = None
66
+
67
+
68
+ def _store_run() -> tuple[ExecutionStore, str]:
69
+ if _CURRENT_STORE is None or _CURRENT_RUN_ID is None:
70
+ raise RuntimeError("ui grounding dag missing runtime store/run_id")
71
+ return _CURRENT_STORE, _CURRENT_RUN_ID
72
+
73
+
74
+ def _stable_id(prefix: str, payload: Any, *, size: int = 12) -> str:
75
+ raw = json.dumps(payload, sort_keys=True).encode("utf-8")
76
+ return f"{prefix}{hashlib.sha256(raw).hexdigest()[:size]}"
77
+
78
+
79
+ def _write_json(path: Path, payload: dict[str, Any]) -> None:
80
+ path.parent.mkdir(parents=True, exist_ok=True)
81
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
82
+
83
+
84
+ def _dfs_running(*, project_id: str, run_id: str, summary: str, idea_id: str) -> None:
85
+ publish_devflow_state(
86
+ project_id=project_id,
87
+ run_id=run_id,
88
+ current_state="running",
89
+ current_status="processing",
90
+ run_summary=summary,
91
+ display="project",
92
+ display_path=f"idea:{idea_id}",
93
+ )
94
+
95
+
96
+ def _pipeline_root(repo_root: Path, *, idea_id: str, pipeline_key: str) -> Path:
97
+ return repo_root / ".devflow" / "ideas" / idea_id / "pipelines" / DAG_ID / pipeline_key
98
+
99
+
100
+ def _load_json(path: Path) -> dict[str, Any]:
101
+ return json.loads(path.read_text(encoding="utf-8"))
102
+
103
+
104
+ def _normalize_text(text: str) -> str:
105
+ return re.sub(r"[^a-z0-9\s]+", " ", text.lower())
106
+
107
+
108
+ def _tokens(text: str) -> list[str]:
109
+ stop = {"the", "and", "with", "from", "that", "this", "into", "for", "your", "their", "then", "when", "must", "should"}
110
+ out: list[str] = []
111
+ for tok in _normalize_text(text).split():
112
+ if len(tok) >= 4 and tok not in stop and tok not in out:
113
+ out.append(tok)
114
+ return out
115
+
116
+
117
+ def _sentence_chunks(text: str) -> list[str]:
118
+ text = re.sub(r"\s+", " ", text).strip()
119
+ return [part.strip() for part in re.split(r"(?<=[.!?])\s+", text) if part.strip()]
120
+
121
+
122
+ def _surface_name(chunk: str) -> str:
123
+ lowered = chunk.lower()
124
+ mapping = [
125
+ ("Manager dashboard", ["manager dashboard", "dashboard", "reporting", "score trends"]),
126
+ ("Need Help flow", ["need help", "crisis", "help request"]),
127
+ ("Vibe Check flow", ["vibe check", "mood"]),
128
+ ("Participant survey", ["participant", "survey", "question", "form"]),
129
+ ("Quote approval screen", ["quote approval", "approve quote", "secure link"]),
130
+ ]
131
+ for title, needles in mapping:
132
+ if any(n in lowered for n in needles):
133
+ return title
134
+ words = chunk.split()
135
+ return " ".join(words[:5]).strip().capitalize() or "UI surface"
136
+
137
+
138
+ def _surface_actor(chunk: str) -> str:
139
+ lowered = chunk.lower()
140
+ if "manager" in lowered:
141
+ return "manager"
142
+ if "participant" in lowered or "customer" in lowered:
143
+ return "participant" if "participant" in lowered else "customer"
144
+ if "admin" in lowered:
145
+ return "admin"
146
+ return "user"
147
+
148
+
149
+
150
+
151
+ def _classify_reference_type(path: Path) -> str:
152
+ lowered = path.as_posix().lower()
153
+ if path.suffix == ".pen":
154
+ return "pencil_artifact"
155
+ if any(part in lowered for part in ["screenshot", "mock", "wireframe"]):
156
+ return "screenshot_spec" if "screenshot" in lowered else "wireframe_package"
157
+ if any(part in lowered for part in ["design", "spec", "figma", "notes"]):
158
+ return "design_doc"
159
+ return "implemented_ui"
160
+
161
+
162
+ def _candidate_ui_files(repo_root: Path) -> list[Path]:
163
+ roots = [repo_root / "app", repo_root / "src", repo_root / "components", repo_root / "pages", repo_root / "ai_docs"]
164
+ out: list[Path] = []
165
+ for root in roots:
166
+ if not root.exists():
167
+ continue
168
+ for path in root.rglob("*"):
169
+ if not path.is_file():
170
+ continue
171
+ if path.suffix.lower() not in {".tsx", ".ts", ".jsx", ".js", ".md", ".mdx", ".pen", ".json", ".png", ".jpg", ".jpeg", ".webp"}:
172
+ continue
173
+ rel = path.relative_to(repo_root).as_posix().lower()
174
+ if any(marker in rel for marker in ["dashboard", "screen", "page", "survey", "participant", "manager", "vibe", "help", "quote", "design", "wireframe", "mock"]):
175
+ out.append(path)
176
+ return sorted(out)[:80]
177
+
178
+
179
+ def _labels_for_path(path: Path) -> list[str]:
180
+ stem = path.stem.replace("_", " ").replace("-", " ")
181
+ parent = path.parent.name.replace("_", " ").replace("-", " ")
182
+ return [item.strip() for item in [stem, parent] if item.strip()]
183
+
184
+
185
+ def _materialize_report(derivation: UISurfaceDerivationArtifact, inventory: UIReferenceInventoryArtifact, *, prefer_generated: bool = False) -> tuple[UIGroundingReportArtifact, UIGapReportArtifact | None]:
186
+ by_id = {item.reference_id: item for item in inventory.references}
187
+ ref_index = [(item, set(_tokens(" ".join([*item.surface_labels, item.path])))) for item in inventory.references]
188
+ rows: list[UIGroundingSurfaceResult] = []
189
+ missing: list[str] = []
190
+ assumptions: list[str] = []
191
+ generated_hits = 0
192
+ for surface in derivation.surfaces:
193
+ if not surface.ui_bearing:
194
+ rows.append(UIGroundingSurfaceResult(
195
+ surface_id=surface.surface_id,
196
+ name=surface.name,
197
+ classification="non_ui_or_backend_only",
198
+ next_required_action="No UI grounding required for this slice.",
199
+ ))
200
+ continue
201
+ stoks = set(_tokens(f"{surface.name} {' '.join(surface.jobs)}"))
202
+ matches = [item for item, toks in ref_index if stoks and len(stoks & toks) >= 2]
203
+ if matches:
204
+ top_matches = matches[:3]
205
+ if prefer_generated and any(m.reference_type == "pencil_artifact" for m in top_matches):
206
+ classification = "grounded_with_generated_artifacts"
207
+ generated_hits += 1
208
+ next_action = "Attach strongest code-derived design anchors to grounded story/idea artifacts."
209
+ else:
210
+ classification = "grounded_existing"
211
+ next_action = "Attach existing UI anchors to grounded story/idea artifacts."
212
+ rows.append(UIGroundingSurfaceResult(
213
+ surface_id=surface.surface_id,
214
+ name=surface.name,
215
+ classification=classification,
216
+ matched_reference_ids=[m.reference_id for m in top_matches],
217
+ supporting_paths=[m.path for m in top_matches],
218
+ next_required_action=next_action,
219
+ ))
220
+ else:
221
+ missing.append(surface.name)
222
+ assumptions.append(f"No existing UI reference was found for {surface.name}; DFUS should not improvise this surface.")
223
+ rows.append(UIGroundingSurfaceResult(
224
+ surface_id=surface.surface_id,
225
+ name=surface.name,
226
+ classification="missing_reference",
227
+ unresolved_questions=[f"Which approved screen/spec should ground {surface.name}?"],
228
+ assumptions=[assumptions[-1]],
229
+ next_required_action="Create a UI gap artifact and route to design review or wireframe generation.",
230
+ ))
231
+ if missing:
232
+ status = "blocked_missing_ui_grounding"
233
+ gap = UIGapReportArtifact(
234
+ project_id=derivation.project_id,
235
+ idea_id=derivation.idea_id,
236
+ missing_surfaces=missing,
237
+ reasons=[f"No matching existing UI/design reference for {name}." for name in missing],
238
+ recommendation="Needs UI review before DFUS or implementation proceeds for the affected slices.",
239
+ blocked=True,
240
+ )
241
+ summary = f"UI grounding blocked: {len(missing)} required surface(s) are missing durable UI references."
242
+ next_actions = [gap.recommendation]
243
+ else:
244
+ status = "grounded_with_generated_artifacts" if generated_hits else "grounded_ready"
245
+ gap = None
246
+ summary = "All derived UI-bearing surfaces were matched to existing UI/design references."
247
+ if generated_hits:
248
+ summary = f"All derived UI-bearing surfaces were grounded after code-to-design enrichment; {generated_hits} surface(s) now use generated design anchors."
249
+ next_actions = ["Patch the idea/provisional story artifacts with explicit UI anchors."]
250
+ return UIGroundingReportArtifact(
251
+ project_id=derivation.project_id,
252
+ idea_id=derivation.idea_id,
253
+ status=status,
254
+ surfaces=rows,
255
+ summary=summary,
256
+ assumptions=assumptions,
257
+ next_actions=next_actions,
258
+ ), gap
259
+
260
+
261
+ def _grounding_report_from_inventory(derivation: UISurfaceDerivationArtifact, inventory: UIReferenceInventoryArtifact, *, prefer_generated: bool) -> tuple[UIGroundingReportArtifact, UIGapReportArtifact | None]:
262
+ return _materialize_report(derivation, inventory, prefer_generated=prefer_generated)
263
+
264
+
265
+ def _slug(text: str) -> str:
266
+ pieces = [part for part in re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_").split("_") if part]
267
+ return "_".join(pieces[:8]) or "screen"
268
+
269
+
270
+ def _surface_class(surface: DerivedUISurface) -> str:
271
+ lowered = f"{surface.name} {' '.join(surface.jobs)}".lower()
272
+ if any(token in lowered for token in ["secure link", "approve quote", "quote approval", "payment", "checkout"]):
273
+ return "customer_secure_link"
274
+ if any(token in lowered for token in ["portal", "account area"]):
275
+ return "customer_portal"
276
+ if any(token in lowered for token in ["technician", "field", "crew", "job site", "tablet"]):
277
+ return "field_tablet"
278
+ if any(token in lowered for token in ["manager", "admin", "dashboard", "reporting", "back office"]):
279
+ return "admin_web"
280
+ if any(token in lowered for token in ["support", "need help", "csr"]):
281
+ return "internal_support_tool"
282
+ if any(token in lowered for token in ["participant", "survey", "form", "vibe check", "page", "flow"]):
283
+ return "general_web"
284
+ if not surface.ui_bearing:
285
+ return "non_ui_or_backend_only"
286
+ return "general_web"
287
+
288
+
289
+ def _surface_platform(surface_class: str) -> str:
290
+ if surface_class == "field_tablet":
291
+ return "tablet"
292
+ if surface_class in {"customer_secure_link", "customer_portal"}:
293
+ return "web_mobile_responsive"
294
+ if surface_class == "non_ui_or_backend_only":
295
+ return "not_applicable"
296
+ return "web_desktop"
297
+
298
+
299
+ def _target_audience(surface_class: str) -> str:
300
+ if surface_class == "non_ui_or_backend_only":
301
+ return "internal_exploration"
302
+ return "customer_review"
303
+
304
+
305
+ def _target_fidelity(surface_class: str, audience: str) -> str:
306
+ if audience == "customer_review" and surface_class in {"customer_secure_link", "customer_portal", "admin_web", "field_tablet", "general_web"}:
307
+ return "medium"
308
+ return "low"
309
+
310
+
311
+ def _journey_stage(surface: DerivedUISurface) -> str:
312
+ lowered = f"{surface.name} {' '.join(surface.jobs)}".lower()
313
+ if "approve" in lowered or "approval" in lowered:
314
+ return "review_and_approve"
315
+ if "dashboard" in lowered or "report" in lowered:
316
+ return "monitor_and_review"
317
+ if "survey" in lowered or "form" in lowered or "check" in lowered:
318
+ return "capture_input"
319
+ if "need help" in lowered or "support" in lowered:
320
+ return "request_support"
321
+ return "primary_flow"
322
+
323
+
324
+ def _shared_shell_id(surface_class: str) -> str | None:
325
+ return {
326
+ "admin_web": "admin_shell",
327
+ "customer_secure_link": "secure_link_shell",
328
+ "customer_portal": "customer_portal_shell",
329
+ "field_tablet": "field_ops_shell",
330
+ "internal_support_tool": "support_shell",
331
+ "general_web": "default_web_shell",
332
+ }.get(surface_class)
333
+
334
+
335
+ def _build_ui_structure_contract(*, context: UIGroundingContextArtifact, derivation: UISurfaceDerivationArtifact, report: UIGroundingReportArtifact) -> tuple[UIStructureContractArtifact, UIFlowMapArtifact, UIScreenRelationshipsArtifact, UIGenerationManifestArtifact]:
336
+ by_surface = {surface.surface_id: surface for surface in derivation.surfaces}
337
+ screens: list[UIStructureScreenItem] = []
338
+ flows: dict[str, list[str]] = {}
339
+ relationships: list[dict[str, Any]] = []
340
+ unresolved: list[str] = []
341
+
342
+ for row in report.surfaces:
343
+ surface = by_surface.get(row.surface_id)
344
+ if surface is None:
345
+ continue
346
+ surface_class = _surface_class(surface)
347
+ audience = _target_audience(surface_class)
348
+ fidelity = _target_fidelity(surface_class, audience)
349
+ flow_id = f"flow_{_journey_stage(surface)}"
350
+ screen_id = _slug(surface.name)
351
+ confidence = surface.confidence
352
+ confidence_reason = "Grounded to approved references." if row.classification in {"grounded_existing", "grounded_with_generated_artifacts"} else "Still missing approved grounding references."
353
+ assumption_tags = [
354
+ "assume_customer_facing_review" if audience == "customer_review" else "assume_internal_exploration",
355
+ "assume_medium_fidelity_minimum" if fidelity == "medium" else "assume_low_fidelity_allowed",
356
+ ]
357
+ notes = []
358
+ if fidelity == "medium":
359
+ notes.append("Use realistic labels, hierarchy, and CTA placement rather than skeletal placeholder boxes.")
360
+ if row.classification == "grounded_with_generated_artifacts":
361
+ notes.append("Generated design anchors should still receive review before implementation handoff.")
362
+ grounding_refs = []
363
+ for path in row.supporting_paths:
364
+ grounding_refs.append(path if path.startswith("pencil:") else f"ui:file:{path}")
365
+ screen = UIStructureScreenItem(
366
+ surface_id=row.surface_id,
367
+ screen_id=screen_id,
368
+ name=surface.name,
369
+ surface_class=surface_class,
370
+ platform=_surface_platform(surface_class),
371
+ actor=surface.actor,
372
+ journey_stage=_journey_stage(surface),
373
+ required=surface.ui_bearing,
374
+ source_refs=surface.source_refs,
375
+ grounding_refs=grounding_refs,
376
+ target_fidelity=fidelity,
377
+ target_audience=audience,
378
+ confidence=confidence,
379
+ confidence_reason=confidence_reason,
380
+ open_questions=list(row.unresolved_questions),
381
+ assumption_tags=assumption_tags,
382
+ notes=notes,
383
+ flow_id=flow_id,
384
+ shell_id=_shared_shell_id(surface_class),
385
+ )
386
+ screens.append(screen)
387
+ flows.setdefault(flow_id, []).append(screen_id)
388
+ if row.classification == "missing_reference":
389
+ unresolved.extend(row.unresolved_questions)
390
+ if screen.shell_id:
391
+ relationships.append({"type": "shared_shell", "from": screen_id, "to": screen.shell_id})
392
+
393
+ ordered_flow_ids = sorted(flows)
394
+ screen_index = {screen.screen_id: screen for screen in screens}
395
+ for flow_id, ids in flows.items():
396
+ for current, nxt in zip(ids, ids[1:]):
397
+ screen_index[current].dependency_screen_ids.append(nxt)
398
+ relationships.append({"type": "flow_transition", "from": current, "to": nxt, "flow_id": flow_id})
399
+
400
+ status = "non_ui_only" if not any(screen.required for screen in screens) else ("needs_ui_review" if report.status == "blocked_missing_ui_grounding" else "ready_for_generation")
401
+ shared_shells = [
402
+ {"shell_id": shell_id, "screen_ids": [screen.screen_id for screen in screens if screen.shell_id == shell_id]}
403
+ for shell_id in sorted({screen.shell_id for screen in screens if screen.shell_id})
404
+ ]
405
+ structure = UIStructureContractArtifact(
406
+ project_id=context.project_id,
407
+ idea_id=context.idea_id,
408
+ status=status,
409
+ default_minimum_fidelity="medium",
410
+ primary_audience="customer_review",
411
+ screens=screens,
412
+ flow_order=ordered_flow_ids,
413
+ shared_shells=shared_shells,
414
+ structural_notes=[
415
+ "Planning/discovery decides what surfaces exist; structure contract decides how the approved screen set coheres before generation.",
416
+ "Customer-facing and stakeholder-review surfaces default to medium fidelity minimum.",
417
+ ],
418
+ unresolved_questions=sorted(dict.fromkeys(unresolved)),
419
+ )
420
+ flow_map = UIFlowMapArtifact(
421
+ project_id=context.project_id,
422
+ idea_id=context.idea_id,
423
+ flows=[{"flow_id": flow_id, "screen_ids": ids} for flow_id, ids in flows.items()],
424
+ )
425
+ screen_relationships = UIScreenRelationshipsArtifact(
426
+ project_id=context.project_id,
427
+ idea_id=context.idea_id,
428
+ relationships=relationships,
429
+ )
430
+ generation_manifest = UIGenerationManifestArtifact(
431
+ project_id=context.project_id,
432
+ idea_id=context.idea_id,
433
+ primary_audience="customer_review",
434
+ default_minimum_fidelity="medium",
435
+ review_packet_outputs=[
436
+ "wireframe_index.md",
437
+ "customer_review_packet.json",
438
+ "screen_comment_matrix.json",
439
+ "screen_inventory_snapshot.json",
440
+ ],
441
+ screen_generation_items=[
442
+ {
443
+ "screen_id": screen.screen_id,
444
+ "surface_id": screen.surface_id,
445
+ "target_fidelity": screen.target_fidelity,
446
+ "target_audience": screen.target_audience,
447
+ "grounding_refs": screen.grounding_refs,
448
+ "flow_id": screen.flow_id,
449
+ }
450
+ for screen in screens if screen.required
451
+ ],
452
+ )
453
+ return structure, flow_map, screen_relationships, generation_manifest
454
+
455
+
456
+ def _write_report_and_gap(pipeline_root: Path, artifact: UIGroundingReportArtifact, gap: UIGapReportArtifact | None) -> None:
457
+ report_path = pipeline_root / "ui_grounding_report.json"
458
+ _write_json(report_path, artifact.model_dump())
459
+ gap_path = pipeline_root / "ui_gap_report.json"
460
+ if gap is not None:
461
+ _write_json(gap_path, gap.model_dump())
462
+ elif gap_path.exists():
463
+ gap_path.unlink()
464
+
465
+
466
+ class LoadUIGroundingContextNode(Node):
467
+ async def process(self, task_context: TaskContext) -> TaskContext:
468
+ event = task_context.event
469
+ _dfs_running(project_id=event.project_id, run_id=_store_run()[1], summary="Reviewing existing UI", idea_id=event.idea_id)
470
+ repo_root = Path(event.repo_root)
471
+ idea_payload = dict(event.idea_inline or {})
472
+ idea_path = None
473
+ if event.idea_path:
474
+ idea_path = repo_root / event.idea_path if not Path(event.idea_path).is_absolute() else Path(event.idea_path)
475
+ idea_payload = _load_json(idea_path)
476
+ story_paths: list[str] = []
477
+ story_summaries: list[str] = []
478
+ for raw in event.story_paths:
479
+ path = repo_root / raw if not Path(raw).is_absolute() else Path(raw)
480
+ story_paths.append(str(path))
481
+ try:
482
+ payload = _load_json(path)
483
+ story_summaries.append(str(payload.get("title") or payload.get("user_value_statement") or path.name))
484
+ except Exception:
485
+ story_summaries.append(path.name)
486
+ design_paths = [str((repo_root / p if not Path(p).is_absolute() else Path(p))) for p in event.design_paths]
487
+ context = UIGroundingContextArtifact(
488
+ project_id=event.project_id,
489
+ idea_id=event.idea_id,
490
+ idea_title=str(idea_payload.get("title") or event.idea_id),
491
+ idea_summary=str(idea_payload.get("summary") or idea_payload.get("goal") or idea_payload.get("problem") or idea_payload),
492
+ idea_path=str(idea_path) if idea_path else None,
493
+ story_paths=story_paths,
494
+ story_summaries=story_summaries,
495
+ design_paths=design_paths,
496
+ repo_root=str(repo_root),
497
+ upstream_refs=[event.idea_path] if event.idea_path else [f"idea:{event.idea_id}"],
498
+ )
499
+ pipeline_root = _pipeline_root(repo_root, idea_id=event.idea_id, pipeline_key=event.pipeline_key)
500
+ _write_json(pipeline_root / "ui_grounding_context.json", context.model_dump())
501
+ task_context.metadata["pipeline_root"] = str(pipeline_root)
502
+ task_context.metadata["ui_grounding_context"] = context
503
+ task_context.metadata["allow_code_design_enrichment"] = bool(getattr(event, "allow_code_design_enrichment", False))
504
+ task_context.metadata["pencil_app_path"] = getattr(event, "pencil_app_path", None)
505
+ self.save_output(context)
506
+ return task_context
507
+
508
+
509
+ class DeriveUISurfacesFromIdeaNode(AgentNode):
510
+ def get_agent_config(self) -> AgentConfig:
511
+ return AgentConfig(instructions="Derive the bounded set of UI-bearing surfaces implied by the approved idea/stories.", output_type=UISurfaceDerivationArtifact)
512
+
513
+ async def process(self, task_context: TaskContext) -> TaskContext:
514
+ _dfs_running(project_id=task_context.event.project_id, run_id=_store_run()[1], summary="Mapping screens and flows", idea_id=task_context.event.idea_id)
515
+ context = task_context.metadata["ui_grounding_context"]
516
+ repo_root = Path(task_context.event.repo_root)
517
+ pipeline_root = Path(task_context.metadata["pipeline_root"])
518
+ artifact, envelope = ui_grounding_agentic.run_ui_grounding_agent_step(
519
+ repo_root=repo_root,
520
+ stage_name="derive_ui_surfaces",
521
+ output_model=UISurfaceDerivationArtifact,
522
+ context_payload=context.model_dump(),
523
+ guidance=["Return only surfaces that are explicitly or strongly implied by the approved idea/story inputs.", "Mark non-UI slices explicitly rather than pretending they need screens."],
524
+ timeout_seconds=1800,
525
+ )
526
+ ui_grounding_agentic.persist_agent_run(pipeline_root=pipeline_root, node_id="derive_ui_surfaces", envelope=envelope)
527
+ artifact = UISurfaceDerivationArtifact.model_validate(artifact.model_dump())
528
+ _write_json(pipeline_root / "ui_surface_derivation.json", artifact.model_dump())
529
+ task_context.metadata["ui_surface_derivation"] = artifact
530
+ self.save_output(artifact)
531
+ return task_context
532
+
533
+
534
+ class InventoryExistingUIReferencesNode(Node):
535
+ async def process(self, task_context: TaskContext) -> TaskContext:
536
+ context = task_context.metadata["ui_grounding_context"]
537
+ repo_root = Path(task_context.event.repo_root)
538
+ refs: list[UIReferenceInventoryItem] = []
539
+ seen: set[str] = set()
540
+ for path in [*(_candidate_ui_files(repo_root)), *(Path(p) for p in context.design_paths if Path(p).exists())]:
541
+ rel = path.relative_to(repo_root).as_posix() if path.is_relative_to(repo_root) else str(path)
542
+ if rel in seen:
543
+ continue
544
+ seen.add(rel)
545
+ refs.append(UIReferenceInventoryItem(
546
+ reference_id=_stable_id("uiref_", {"path": rel}),
547
+ reference_type=_classify_reference_type(path),
548
+ path=rel,
549
+ surface_labels=_labels_for_path(path),
550
+ provenance=["repo_scan" if str(path).startswith(str(repo_root)) else "design_input"],
551
+ approval_status="approved" if path.suffix == ".pen" else "unknown",
552
+ ))
553
+ artifact = UIReferenceInventoryArtifact(
554
+ project_id=context.project_id,
555
+ idea_id=context.idea_id,
556
+ references=refs,
557
+ inventory_notes=[f"Inventoried {len(refs)} UI/design reference candidate(s)."],
558
+ )
559
+ _write_json(Path(task_context.metadata["pipeline_root"]) / "ui_reference_inventory.json", artifact.model_dump())
560
+ task_context.metadata["ui_reference_inventory"] = artifact
561
+ self.save_output(artifact)
562
+ return task_context
563
+
564
+
565
+ class EnsurePencilAvailableNode(Node):
566
+ async def process(self, task_context: TaskContext) -> TaskContext:
567
+ _dfs_running(project_id=task_context.event.project_id, run_id=_store_run()[1], summary="Preparing UI generation", idea_id=task_context.event.idea_id)
568
+ pipeline_root = Path(task_context.metadata["pipeline_root"])
569
+ allow = bool(task_context.metadata.get("allow_code_design_enrichment"))
570
+ artifact_path = pipeline_root / "pencil_preflight.json"
571
+ if not allow:
572
+ payload = {
573
+ "ok": False,
574
+ "status": "not_requested",
575
+ "repo_root": str(task_context.event.repo_root),
576
+ "artifact_path": str(artifact_path),
577
+ "errors": [],
578
+ }
579
+ _write_json(artifact_path, payload)
580
+ task_context.metadata["pencil_preflight"] = payload
581
+ self.save_output(payload)
582
+ return task_context
583
+ result = run_pencil_preflight(
584
+ repo_root=Path(task_context.event.repo_root),
585
+ store=None,
586
+ app_path=Path(task_context.metadata["pencil_app_path"]) if task_context.metadata.get("pencil_app_path") else None,
587
+ artifact_path=artifact_path,
588
+ )
589
+ task_context.metadata["pencil_preflight"] = result.artifact.model_dump()
590
+ self.save_output(result.artifact)
591
+ return task_context
592
+
593
+
594
+ class MatchUISurfacesToReferencesNode(AgentNode):
595
+ def get_agent_config(self) -> AgentConfig:
596
+ return AgentConfig(instructions="Match derived UI surfaces to the best existing UI/design references.", output_type=UIGroundingReportArtifact)
597
+
598
+ async def process(self, task_context: TaskContext) -> TaskContext:
599
+ _dfs_running(project_id=task_context.event.project_id, run_id=_store_run()[1], summary="Matching UI references", idea_id=task_context.event.idea_id)
600
+ repo_root = Path(task_context.event.repo_root)
601
+ pipeline_root = Path(task_context.metadata["pipeline_root"])
602
+ derivation = task_context.metadata["ui_surface_derivation"]
603
+ inventory = task_context.metadata["ui_reference_inventory"]
604
+ deterministic_report, gap = _grounding_report_from_inventory(derivation, inventory, prefer_generated=False)
605
+ artifact, envelope = ui_grounding_agentic.run_ui_grounding_agent_step(
606
+ repo_root=repo_root,
607
+ stage_name="match_ui_surfaces",
608
+ output_model=UIGroundingReportArtifact,
609
+ context_payload={"ui_surface_derivation": derivation.model_dump(), "ui_reference_inventory": inventory.model_dump(), "deterministic_hints": deterministic_report.model_dump()},
610
+ guidance=["Classify each derived surface as grounded_existing, grounded_partial, missing_reference, or non_ui_or_backend_only.", "If a required UI surface has no durable reference, do not mark the slice grounded."],
611
+ timeout_seconds=1800,
612
+ )
613
+ ui_grounding_agentic.persist_agent_run(pipeline_root=pipeline_root, node_id="match_ui_surfaces", envelope=envelope)
614
+ artifact = UIGroundingReportArtifact.model_validate(artifact.model_dump())
615
+ _write_report_and_gap(pipeline_root, artifact, gap)
616
+ task_context.metadata["ui_grounding_report_first_pass"] = artifact
617
+ task_context.metadata["ui_gap_report"] = gap
618
+ task_context.metadata["ui_grounding_report"] = artifact
619
+ self.save_output(artifact)
620
+ return task_context
621
+
622
+
623
+ class EnrichCodeDesignInventoryNode(Node):
624
+ async def process(self, task_context: TaskContext) -> TaskContext:
625
+ pipeline_root = Path(task_context.metadata["pipeline_root"])
626
+ report: UIGroundingReportArtifact = task_context.metadata["ui_grounding_report_first_pass"]
627
+ inventory: UIReferenceInventoryArtifact = task_context.metadata["ui_reference_inventory"]
628
+ preflight = dict(task_context.metadata.get("pencil_preflight") or {})
629
+ missing_surface_ids = [row.surface_id for row in report.surfaces if row.classification in {"missing_reference", "grounded_partial"}]
630
+ if not bool(task_context.metadata.get("allow_code_design_enrichment")):
631
+ artifact = CodeDesignInventoryArtifact(project_id=report.project_id, idea_id=report.idea_id, status="not_requested", notes=["Code-to-design enrichment not requested for this run."])
632
+ elif preflight.get("status") != "ready":
633
+ artifact = CodeDesignInventoryArtifact(project_id=report.project_id, idea_id=report.idea_id, status="skipped_preflight_not_ready", target_surface_ids=missing_surface_ids, notes=[f"Pencil preflight status {preflight.get('status', 'unknown')} prevented enrichment."])
634
+ elif not missing_surface_ids:
635
+ artifact = CodeDesignInventoryArtifact(project_id=report.project_id, idea_id=report.idea_id, status="skipped_no_missing_surfaces", notes=["First-pass matching already grounded all required UI surfaces."])
636
+ else:
637
+ derivation: UISurfaceDerivationArtifact = task_context.metadata["ui_surface_derivation"]
638
+ generated: list[CodeDesignGeneratedReference] = []
639
+ for surface in derivation.surfaces:
640
+ if surface.surface_id not in missing_surface_ids:
641
+ continue
642
+ stoks = set(_tokens(f"{surface.name} {' '.join(surface.jobs)}"))
643
+ candidates = [
644
+ ref for ref in inventory.references
645
+ if ref.reference_type == "implemented_ui" and stoks and len(stoks & set(_tokens(' '.join([*ref.surface_labels, ref.path])))) >= 1
646
+ ]
647
+ if not candidates:
648
+ continue
649
+ source_paths = [ref.path for ref in candidates[:3]]
650
+ generated_anchor = f"pencil:code_design_inventory.json#{surface.surface_id}"
651
+ normalized = UIReferenceInventoryItem(
652
+ reference_id=_stable_id("pencilref_", {"surface_id": surface.surface_id, "source_paths": source_paths}),
653
+ reference_type="pencil_artifact",
654
+ path=generated_anchor,
655
+ surface_labels=[surface.name, *surface.jobs[:1]],
656
+ provenance=[*source_paths, "code_to_design"],
657
+ approval_status="generated_from_code",
658
+ )
659
+ generated.append(CodeDesignGeneratedReference(
660
+ surface_id=surface.surface_id,
661
+ source_paths=source_paths,
662
+ generated_reference_id=normalized.reference_id,
663
+ generated_anchor=generated_anchor,
664
+ generation_kind="full_screen",
665
+ review_needed=False,
666
+ normalized_reference=normalized,
667
+ ))
668
+ if generated:
669
+ artifact = CodeDesignInventoryArtifact(
670
+ project_id=report.project_id,
671
+ idea_id=report.idea_id,
672
+ status="generated",
673
+ target_surface_ids=missing_surface_ids,
674
+ generated_references=generated,
675
+ notes=[f"Generated {len(generated)} code-derived design reference(s) from implemented UI evidence."],
676
+ )
677
+ else:
678
+ artifact = CodeDesignInventoryArtifact(
679
+ project_id=report.project_id,
680
+ idea_id=report.idea_id,
681
+ status="skipped_no_relevant_implemented_ui",
682
+ target_surface_ids=missing_surface_ids,
683
+ notes=["No relevant implemented_ui references were available for code-to-design enrichment."],
684
+ )
685
+ _write_json(pipeline_root / "code_design_inventory.json", artifact.model_dump())
686
+ task_context.metadata["code_design_inventory"] = artifact
687
+ self.save_output(artifact)
688
+ return task_context
689
+
690
+
691
+ class RerunMatchingAfterCodeDesignNode(Node):
692
+ async def process(self, task_context: TaskContext) -> TaskContext:
693
+ pipeline_root = Path(task_context.metadata["pipeline_root"])
694
+ derivation: UISurfaceDerivationArtifact = task_context.metadata["ui_surface_derivation"]
695
+ inventory: UIReferenceInventoryArtifact = task_context.metadata["ui_reference_inventory"]
696
+ first_report: UIGroundingReportArtifact = task_context.metadata["ui_grounding_report_first_pass"]
697
+ code_design: CodeDesignInventoryArtifact = task_context.metadata["code_design_inventory"]
698
+ if code_design.status != "generated":
699
+ artifact = first_report
700
+ gap = task_context.metadata.get("ui_gap_report")
701
+ effective_inventory = inventory
702
+ else:
703
+ effective_inventory = UIReferenceInventoryArtifact(
704
+ project_id=inventory.project_id,
705
+ idea_id=inventory.idea_id,
706
+ references=[*inventory.references, *(item.normalized_reference for item in code_design.generated_references)],
707
+ inventory_notes=[*inventory.inventory_notes, f"Merged {len(code_design.generated_references)} code-derived design reference(s) for second-pass matching."],
708
+ )
709
+ artifact, gap = _materialize_report(derivation, effective_inventory, prefer_generated=True)
710
+ _write_json(pipeline_root / "effective_ui_reference_inventory.json", effective_inventory.model_dump())
711
+ _write_report_and_gap(pipeline_root, artifact, gap)
712
+ task_context.metadata["effective_ui_reference_inventory"] = effective_inventory
713
+ task_context.metadata["ui_grounding_report"] = artifact
714
+ task_context.metadata["ui_gap_report"] = gap
715
+ self.save_output(artifact)
716
+ return task_context
717
+
718
+
719
+ class IntegrateUIScreenStructureNode(Node):
720
+ async def process(self, task_context: TaskContext) -> TaskContext:
721
+ pipeline_root = Path(task_context.metadata["pipeline_root"])
722
+ context: UIGroundingContextArtifact = task_context.metadata["ui_grounding_context"]
723
+ derivation: UISurfaceDerivationArtifact = task_context.metadata["ui_surface_derivation"]
724
+ report: UIGroundingReportArtifact = task_context.metadata["ui_grounding_report"]
725
+ structure, flow_map, relationships, manifest = _build_ui_structure_contract(
726
+ context=context,
727
+ derivation=derivation,
728
+ report=report,
729
+ )
730
+ _write_json(pipeline_root / "ui_structure_contract.json", structure.model_dump())
731
+ _write_json(pipeline_root / "ui_flow_map.json", flow_map.model_dump())
732
+ _write_json(pipeline_root / "ui_screen_relationships.json", relationships.model_dump())
733
+ _write_json(pipeline_root / "ui_generation_manifest.json", manifest.model_dump())
734
+ task_context.metadata["ui_structure_contract"] = structure
735
+ task_context.metadata["ui_flow_map"] = flow_map
736
+ task_context.metadata["ui_screen_relationships"] = relationships
737
+ task_context.metadata["ui_generation_manifest"] = manifest
738
+ self.save_output(structure)
739
+ return task_context
740
+
741
+
742
+ class PatchStoryAndIdeaUIReferencesNode(Node):
743
+ async def process(self, task_context: TaskContext) -> TaskContext:
744
+ report: UIGroundingReportArtifact = task_context.metadata["ui_grounding_report"]
745
+ effective_inventory: UIReferenceInventoryArtifact = task_context.metadata.get("effective_ui_reference_inventory") or task_context.metadata["ui_reference_inventory"]
746
+ inventory_by_id = {item.reference_id: item for item in effective_inventory.references}
747
+ anchors: list[dict[str, Any]] = []
748
+ evidence: list[str] = []
749
+ anchor_kinds: set[str] = set()
750
+ for row in report.surfaces:
751
+ for ref_id, path in zip(row.matched_reference_ids, row.supporting_paths):
752
+ item = inventory_by_id.get(ref_id)
753
+ if item is not None and item.reference_type == "pencil_artifact":
754
+ anchor = path
755
+ anchor_type = "pencil"
756
+ else:
757
+ anchor = f"ui:file:{path}"
758
+ anchor_type = "ui:file"
759
+ anchors.append({"surface_id": row.surface_id, "surface": row.name, "anchor": anchor, "anchor_type": anchor_type, "reference_id": ref_id})
760
+ evidence.append(ref_id)
761
+ anchor_kinds.add(anchor_type)
762
+ if anchor_kinds == {"pencil"}:
763
+ origin = "generated_ui"
764
+ elif "pencil" in anchor_kinds and "ui:file" in anchor_kinds:
765
+ origin = "mixed"
766
+ else:
767
+ origin = "existing_ui"
768
+ patch = GroundedStoryReferencePatchArtifact(
769
+ project_id=report.project_id,
770
+ idea_id=report.idea_id,
771
+ target_refs=[task_context.event.idea_path or f"idea:{report.idea_id}", *task_context.event.story_paths],
772
+ attached_ui_anchors=anchors,
773
+ supporting_evidence_refs=sorted(set(evidence)),
774
+ origin=origin,
775
+ )
776
+ _write_json(Path(task_context.metadata["pipeline_root"]) / "grounded_story_reference_patch.json", patch.model_dump())
777
+ task_context.metadata["grounded_story_reference_patch"] = patch
778
+ self.save_output(patch)
779
+ return task_context
780
+
781
+
782
+ class FinalizeUIGroundingOutcomeNode(Node):
783
+ async def process(self, task_context: TaskContext) -> TaskContext:
784
+ report: UIGroundingReportArtifact = task_context.metadata["ui_grounding_report"]
785
+ patch: GroundedStoryReferencePatchArtifact = task_context.metadata["grounded_story_reference_patch"]
786
+ outcome = {
787
+ "project_id": report.project_id,
788
+ "idea_id": report.idea_id,
789
+ "status": report.status,
790
+ "surface_count": len(report.surfaces),
791
+ "grounded_surface_count": sum(1 for row in report.surfaces if row.classification in {"grounded_existing", "grounded_with_generated_artifacts"}),
792
+ "missing_surface_count": sum(1 for row in report.surfaces if row.classification == "missing_reference"),
793
+ "anchor_count": len(patch.attached_ui_anchors),
794
+ }
795
+ exit_code = 0 if report.status in {"grounded_ready", "grounded_with_generated_artifacts", "pass_with_assumptions"} else 2
796
+ summary = UIGroundingDagSummary(
797
+ exit_code=exit_code,
798
+ run_id=_store_run()[1],
799
+ pipeline_dir=str(task_context.metadata["pipeline_root"]),
800
+ message="ui grounding run complete",
801
+ outcome=outcome,
802
+ )
803
+ _write_json(Path(task_context.metadata["pipeline_root"]) / "summary.json", summary.model_dump())
804
+ task_context.metadata["message"] = json.dumps({**outcome, "run_id": summary.run_id, "pipeline_dir": summary.pipeline_dir}, sort_keys=True) + "\n"
805
+ task_context.metadata["outcome"] = outcome
806
+ task_context.metadata["exit_code"] = exit_code
807
+ self.save_output(summary)
808
+ return task_context
809
+
810
+
811
+ class UIGroundingWorkflow(Workflow):
812
+ workflow_schema = WorkflowSchema(
813
+ description="UI grounding DAG Phase 2 (load context -> derive surfaces -> inventory refs -> Pencil gate -> first pass -> code-design enrichment -> second pass -> integrate screen structure -> patch -> finalize)",
814
+ event_schema=UIGroundingDagEvent,
815
+ start=LoadUIGroundingContextNode,
816
+ nodes=[
817
+ NodeConfig(node=LoadUIGroundingContextNode, connections=[DeriveUISurfacesFromIdeaNode]),
818
+ NodeConfig(node=DeriveUISurfacesFromIdeaNode, connections=[InventoryExistingUIReferencesNode]),
819
+ NodeConfig(node=InventoryExistingUIReferencesNode, connections=[EnsurePencilAvailableNode]),
820
+ NodeConfig(node=EnsurePencilAvailableNode, connections=[MatchUISurfacesToReferencesNode]),
821
+ NodeConfig(node=MatchUISurfacesToReferencesNode, connections=[EnrichCodeDesignInventoryNode]),
822
+ NodeConfig(node=EnrichCodeDesignInventoryNode, connections=[RerunMatchingAfterCodeDesignNode]),
823
+ NodeConfig(node=RerunMatchingAfterCodeDesignNode, connections=[IntegrateUIScreenStructureNode]),
824
+ NodeConfig(node=IntegrateUIScreenStructureNode, connections=[PatchStoryAndIdeaUIReferencesNode]),
825
+ NodeConfig(node=PatchStoryAndIdeaUIReferencesNode, connections=[FinalizeUIGroundingOutcomeNode]),
826
+ NodeConfig(node=FinalizeUIGroundingOutcomeNode, connections=[]),
827
+ ],
828
+ )
829
+
830
+
831
+ def build_pipeline_key(*, repo_root: Path, project_id: str, idea_id: str, idea_payload: dict[str, Any], story_paths: list[str], design_paths: list[str], allow_code_design_enrichment: bool = False) -> str:
832
+ return _stable_id("run_", {"repo_root": str(repo_root), "project_id": project_id, "idea_id": idea_id, "idea_payload": idea_payload, "story_paths": story_paths, "design_paths": design_paths, "allow_code_design_enrichment": allow_code_design_enrichment})
833
+
834
+
835
+ def run_ui_grounding_dag(*, repo_root: Path, store: ExecutionStore, project_id: str, idea_id: str, idea_path: Path | None = None, idea_inline: dict[str, Any] | None = None, story_paths: list[Path] | None = None, design_paths: list[Path] | None = None, allow_code_design_enrichment: bool = False, pencil_app_path: Path | None = None) -> UIGroundingDagResult:
836
+ payload = dict(idea_inline or {})
837
+ if idea_path is not None:
838
+ payload = _load_json(idea_path)
839
+ pipeline_key = build_pipeline_key(
840
+ repo_root=repo_root,
841
+ project_id=project_id,
842
+ idea_id=idea_id,
843
+ idea_payload=payload,
844
+ story_paths=[str(p) for p in (story_paths or [])],
845
+ design_paths=[str(p) for p in (design_paths or [])],
846
+ allow_code_design_enrichment=allow_code_design_enrichment,
847
+ )
848
+ pipeline_dir = _pipeline_root(repo_root, idea_id=idea_id, pipeline_key=pipeline_key)
849
+ pipeline_dir.mkdir(parents=True, exist_ok=True)
850
+ run_id = store.create_run(dag_id=DAG_ID, dag_version="v1_phase1", root_correlation_id=f"corr_{pipeline_key}", config={"project_id": project_id, "idea_id": idea_id, "pipeline_key": pipeline_key, "allow_code_design_enrichment": allow_code_design_enrichment})
851
+ store.mark_run_started(run_id=run_id)
852
+ wf = UIGroundingWorkflow()
853
+ global _CURRENT_STORE, _CURRENT_RUN_ID
854
+ _CURRENT_STORE = store
855
+ _CURRENT_RUN_ID = run_id
856
+ try:
857
+ ctx = wf.run({
858
+ "repo_root": str(repo_root),
859
+ "project_id": project_id,
860
+ "idea_id": idea_id,
861
+ "idea_path": str(idea_path.relative_to(repo_root)) if idea_path is not None and idea_path.is_relative_to(repo_root) else (str(idea_path) if idea_path else None),
862
+ "idea_inline": payload if idea_path is None else None,
863
+ "story_paths": [str(p.relative_to(repo_root)) if p.is_relative_to(repo_root) else str(p) for p in (story_paths or [])],
864
+ "design_paths": [str(p.relative_to(repo_root)) if p.is_relative_to(repo_root) else str(p) for p in (design_paths or [])],
865
+ "pipeline_key": pipeline_key,
866
+ "allow_code_design_enrichment": allow_code_design_enrichment,
867
+ "pencil_app_path": str(pencil_app_path) if pencil_app_path is not None else None,
868
+ })
869
+ finally:
870
+ _CURRENT_STORE = None
871
+ _CURRENT_RUN_ID = None
872
+ exit_code = int(ctx.metadata.get("exit_code") or 0)
873
+ store.mark_run_finished(run_id=run_id, status="succeeded" if exit_code == 0 else "failed")
874
+ return UIGroundingDagResult(exit_code=exit_code, run_id=run_id, pipeline_dir=Path(str(ctx.metadata.get("pipeline_root") or pipeline_dir)), message=str(ctx.metadata.get("message") or ""), outcome=dict(ctx.metadata.get("outcome") or {}))