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
File without changes
@@ -0,0 +1,700 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import hashlib
5
+ import os
6
+ import re
7
+ import sqlite3
8
+ import subprocess
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ _CREATE_DATE_RE = re.compile(r"Create Date:\s*([^\n]+)")
15
+ _SQLITE_URL_RE = re.compile(r"sqlite:///(?P<path>[^\s\"']+)")
16
+ _ALEMBIC_LAYOUT_TOKEN_RE = re.compile(r"(^|[_-])(alembic|migrations?)([_-]|$)", re.IGNORECASE)
17
+ _UNSUPPORTED_ASSIGNMENT = object()
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class AlembicScript:
22
+ path: Path
23
+ revision: str
24
+ down_revisions: tuple[str, ...]
25
+ create_date: str
26
+
27
+
28
+ @dataclass
29
+ class AlembicProject:
30
+ versions_dir: Path
31
+ scripts: list[AlembicScript]
32
+
33
+
34
+ @dataclass
35
+ class AlembicPreflightResult:
36
+ detected: bool = False
37
+ ok: bool = True
38
+ repairs: list[dict[str, Any]] = field(default_factory=list)
39
+ validation_errors: list[dict[str, Any]] = field(default_factory=list)
40
+ migration_run: dict[str, Any] | None = None
41
+ check_run: dict[str, Any] | None = None
42
+ canonical_heads: list[str] = field(default_factory=list)
43
+
44
+ def to_metadata(self) -> dict[str, Any]:
45
+ return {
46
+ "detected": self.detected,
47
+ "ok": self.ok,
48
+ "repairs": self.repairs,
49
+ "validation_errors": self.validation_errors,
50
+ "migration_run": self.migration_run,
51
+ "check_run": self.check_run,
52
+ "canonical_heads": self.canonical_heads,
53
+ }
54
+
55
+
56
+ def run_alembic_preflight(*, repo_root: Path, migration_command: str, check_command: str) -> AlembicPreflightResult:
57
+ result = AlembicPreflightResult()
58
+ projects = _discover_projects(repo_root)
59
+ result.detected = bool(projects)
60
+
61
+ if projects:
62
+ for _ in range(3):
63
+ issues = _collect_repo_issues(projects)
64
+ if not issues:
65
+ break
66
+ changed = False
67
+ for issue in issues:
68
+ changed = _apply_issue_fix(repo_root=repo_root, issue=issue, repairs=result.repairs) or changed
69
+ projects = _discover_projects(repo_root)
70
+ if not changed:
71
+ break
72
+ final_issues = _collect_repo_issues(projects)
73
+ if final_issues:
74
+ result.ok = False
75
+ result.validation_errors.extend(_issue_to_message(issue) for issue in final_issues)
76
+
77
+ db_repairs, db_errors = _repair_local_db_drift(repo_root=repo_root, projects=projects)
78
+ result.repairs.extend(db_repairs)
79
+ result.validation_errors.extend(db_errors)
80
+ if db_errors:
81
+ result.ok = False
82
+ result.canonical_heads = [head for project in projects for head in _head_revisions(project.scripts)]
83
+
84
+ if projects and check_command:
85
+ pre_migration_check = _run_shell(repo_root=repo_root, command=check_command, timeout=120)
86
+ if pre_migration_check.returncode != 0:
87
+ live_repair = _repair_live_current_overlap_drift(
88
+ repo_root=repo_root,
89
+ projects=projects,
90
+ check_command=check_command,
91
+ current_run=pre_migration_check,
92
+ )
93
+ if live_repair is not None:
94
+ repair_run, repair_record = live_repair
95
+ result.repairs.append(repair_record)
96
+ if repair_run.returncode == 0:
97
+ projects = _discover_projects(repo_root)
98
+
99
+ migration_run = _run_shell(repo_root=repo_root, command=migration_command, timeout=120)
100
+ if migration_run.returncode != 0 and projects:
101
+ live_repair = _repair_live_overlap_drift(
102
+ repo_root=repo_root,
103
+ projects=projects,
104
+ stamp_source_command=check_command,
105
+ overlap_run=migration_run,
106
+ repair_kind="migration_overlap_drift",
107
+ )
108
+ if live_repair is not None:
109
+ repair_run, repair_record = live_repair
110
+ result.repairs.append(repair_record)
111
+ if repair_run.returncode == 0:
112
+ projects = _discover_projects(repo_root)
113
+ migration_run = _run_shell(repo_root=repo_root, command=migration_command, timeout=120)
114
+ result.migration_run = _cp_to_meta(migration_run)
115
+ if migration_run.returncode != 0:
116
+ result.ok = False
117
+ result.validation_errors.append(
118
+ {
119
+ "kind": "migration_failed",
120
+ "message": f"migration command failed (rc={migration_run.returncode}): {(migration_run.stderr or migration_run.stdout)[:500]}",
121
+ }
122
+ )
123
+ return result
124
+
125
+ check_run = _run_shell(repo_root=repo_root, command=check_command, timeout=120)
126
+ result.check_run = _cp_to_meta(check_run)
127
+ if check_run.returncode != 0 and projects:
128
+ repaired = False
129
+ live_repair = _repair_live_current_overlap_drift(
130
+ repo_root=repo_root,
131
+ projects=projects,
132
+ check_command=check_command,
133
+ current_run=check_run,
134
+ )
135
+ if live_repair is not None:
136
+ repair_run, repair_record = live_repair
137
+ result.repairs.append(repair_record)
138
+ repaired = repair_run.returncode == 0
139
+ check_run = _run_shell(repo_root=repo_root, command=check_command, timeout=120)
140
+ result.check_run = _cp_to_meta(check_run)
141
+ issues = _collect_repo_issues(projects)
142
+ for issue in issues:
143
+ repaired = _apply_issue_fix(repo_root=repo_root, issue=issue, repairs=result.repairs) or repaired
144
+ if repaired:
145
+ projects = _discover_projects(repo_root)
146
+ db_repairs, db_errors = _repair_local_db_drift(repo_root=repo_root, projects=projects)
147
+ result.repairs.extend(db_repairs)
148
+ result.validation_errors.extend(db_errors)
149
+ check_run = _run_shell(repo_root=repo_root, command=check_command, timeout=120)
150
+ result.check_run = _cp_to_meta(check_run)
151
+
152
+ if check_run.returncode != 0:
153
+ result.ok = False
154
+ result.validation_errors.append(
155
+ {
156
+ "kind": "migration_check_failed",
157
+ "message": f"migration check failed (rc={check_run.returncode}): {(check_run.stderr or check_run.stdout)[:500]}",
158
+ }
159
+ )
160
+
161
+ if projects:
162
+ projects = _discover_projects(repo_root)
163
+ final_issues = _collect_repo_issues(projects)
164
+ if final_issues:
165
+ result.ok = False
166
+ result.validation_errors.extend(_issue_to_message(issue) for issue in final_issues)
167
+ result.canonical_heads = [head for project in projects for head in _head_revisions(project.scripts)]
168
+ post_db_repairs, post_db_errors = _repair_local_db_drift(repo_root=repo_root, projects=projects, validate_only=True)
169
+ result.validation_errors.extend(post_db_errors)
170
+ if post_db_errors:
171
+ result.ok = False
172
+
173
+ return result
174
+
175
+
176
+ def _discover_projects(repo_root: Path) -> list[AlembicProject]:
177
+ projects: list[AlembicProject] = []
178
+ seen: set[Path] = set()
179
+ for versions_dir in repo_root.glob("**/versions"):
180
+ if versions_dir in seen or not versions_dir.is_dir() or not _looks_like_alembic_versions_dir(versions_dir):
181
+ continue
182
+ scripts = [_parse_script(path) for path in sorted(versions_dir.glob("*.py")) if path.is_file() and path.name != "__init__.py"]
183
+ scripts = [script for script in scripts if script is not None]
184
+ if scripts:
185
+ projects.append(AlembicProject(versions_dir=versions_dir, scripts=_sort_scripts(repo_root, scripts)))
186
+ seen.add(versions_dir)
187
+ return projects
188
+
189
+
190
+ def _looks_like_alembic_versions_dir(versions_dir: Path) -> bool:
191
+ parent = versions_dir.parent
192
+ if (parent / "env.py").is_file() or (parent / "script.py.mako").is_file():
193
+ return True
194
+ return bool(_ALEMBIC_LAYOUT_TOKEN_RE.search(parent.name))
195
+
196
+
197
+ def _parse_script(path: Path) -> AlembicScript | None:
198
+ raw = path.read_text(encoding="utf-8")
199
+ module = ast.parse(raw, filename=str(path))
200
+ revision: str | None = None
201
+ down_revisions: tuple[str, ...] = ()
202
+ for node in module.body:
203
+ for name, value_node in _iter_named_assignments(node):
204
+ value = _literal_assignment_value(value_node)
205
+ if name == "revision":
206
+ if isinstance(value, str) and value:
207
+ revision = value
208
+ if name == "down_revision":
209
+ if isinstance(value, str) and value:
210
+ down_revisions = (value,)
211
+ elif isinstance(value, (tuple, list)):
212
+ down_revisions = tuple(str(item) for item in value if item)
213
+ else:
214
+ down_revisions = ()
215
+ if not revision:
216
+ return None
217
+ create_date_match = _CREATE_DATE_RE.search(raw)
218
+ create_date = create_date_match.group(1).strip() if create_date_match else ""
219
+ return AlembicScript(path=path, revision=revision, down_revisions=down_revisions, create_date=create_date)
220
+
221
+
222
+ def _iter_named_assignments(node: ast.stmt) -> list[tuple[str, ast.AST]]:
223
+ if isinstance(node, ast.Assign):
224
+ pairs: list[tuple[str, ast.AST]] = []
225
+ for target in node.targets:
226
+ if isinstance(target, ast.Name):
227
+ pairs.append((target.id, node.value))
228
+ return pairs
229
+ if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.value is not None:
230
+ return [(node.target.id, node.value)]
231
+ return []
232
+
233
+
234
+ def _literal_assignment_value(node: ast.AST) -> Any:
235
+ try:
236
+ return ast.literal_eval(node)
237
+ except (ValueError, TypeError, SyntaxError):
238
+ return _UNSUPPORTED_ASSIGNMENT
239
+
240
+
241
+ def _sort_scripts(repo_root: Path, scripts: list[AlembicScript]) -> list[AlembicScript]:
242
+ return sorted(scripts, key=lambda script: (script.create_date or "", os.fspath(script.path.relative_to(repo_root))))
243
+
244
+
245
+ def _collect_repo_issues(projects: list[AlembicProject]) -> list[dict[str, Any]]:
246
+ issues: list[dict[str, Any]] = []
247
+ for project in projects:
248
+ scripts = project.scripts
249
+ by_revision: dict[str, list[AlembicScript]] = {}
250
+ for script in scripts:
251
+ by_revision.setdefault(script.revision, []).append(script)
252
+ for revision, duplicates in by_revision.items():
253
+ if len(duplicates) > 1:
254
+ duplicates = sorted(duplicates, key=lambda script: os.fspath(script.path))
255
+ for duplicate in duplicates[1:]:
256
+ issues.append(
257
+ {
258
+ "kind": "duplicate_revision_id",
259
+ "project": project,
260
+ "script": duplicate,
261
+ "canonical": duplicates[0],
262
+ "revision": revision,
263
+ }
264
+ )
265
+
266
+ revisions = {script.revision for script in scripts}
267
+ ordered = list(scripts)
268
+ heads = {head.revision for head in _head_scripts(scripts)}
269
+ for index, script in enumerate(ordered):
270
+ missing_parents = [parent for parent in script.down_revisions if parent not in revisions]
271
+ if missing_parents:
272
+ inferred_parent = ordered[index - 1].revision if index > 0 else None
273
+ if inferred_parent:
274
+ issues.append(
275
+ {
276
+ "kind": "broken_parent_reference",
277
+ "project": project,
278
+ "script": script,
279
+ "missing_parents": missing_parents,
280
+ "inferred_parent": inferred_parent,
281
+ }
282
+ )
283
+ continue
284
+ if not script.down_revisions and index > 0:
285
+ issues.append(
286
+ {
287
+ "kind": "missing_down_revision",
288
+ "project": project,
289
+ "script": script,
290
+ "inferred_parent": ordered[index - 1].revision,
291
+ }
292
+ )
293
+ continue
294
+ if index > 0 and script.down_revisions:
295
+ expected_parent = ordered[index - 1].revision
296
+ current_parent = script.down_revisions[0]
297
+ if current_parent != expected_parent:
298
+ issues.append(
299
+ {
300
+ "kind": "stale_autogen_branch",
301
+ "project": project,
302
+ "script": script,
303
+ "current_parent": current_parent,
304
+ "inferred_parent": expected_parent,
305
+ }
306
+ )
307
+
308
+ ordered_heads = _head_scripts(scripts)
309
+ if len(ordered_heads) > 1:
310
+ chain_parent = ordered_heads[0]
311
+ for head in ordered_heads[1:]:
312
+ issues.append(
313
+ {
314
+ "kind": "multiple_heads",
315
+ "project": project,
316
+ "script": head,
317
+ "canonical_parent": chain_parent,
318
+ }
319
+ )
320
+ chain_parent = head
321
+ return issues
322
+
323
+
324
+ def _head_scripts(scripts: list[AlembicScript]) -> list[AlembicScript]:
325
+ referenced: set[str] = set()
326
+ for script in scripts:
327
+ referenced.update(script.down_revisions)
328
+ heads = [script for script in scripts if script.revision not in referenced]
329
+ return sorted(heads, key=lambda script: (script.create_date or "", os.fspath(script.path)))
330
+
331
+
332
+ def _head_revisions(scripts: list[AlembicScript]) -> list[str]:
333
+ return [script.revision for script in _head_scripts(scripts)]
334
+
335
+
336
+ def _apply_issue_fix(*, repo_root: Path, issue: dict[str, Any], repairs: list[dict[str, Any]]) -> bool:
337
+ kind = str(issue["kind"])
338
+ script = issue.get("script")
339
+ if not isinstance(script, AlembicScript):
340
+ return False
341
+ if kind == "duplicate_revision_id":
342
+ new_revision = _deterministic_revision(repo_root=repo_root, script=script)
343
+ if new_revision == script.revision:
344
+ return False
345
+ _rewrite_script_assignments(script.path, revision=new_revision)
346
+ repairs.append(
347
+ {
348
+ "kind": kind,
349
+ "path": os.fspath(script.path.relative_to(repo_root)),
350
+ "old_revision": script.revision,
351
+ "new_revision": new_revision,
352
+ }
353
+ )
354
+ return True
355
+ if kind in {"broken_parent_reference", "missing_down_revision", "multiple_heads", "stale_autogen_branch"}:
356
+ parent = issue.get("inferred_parent")
357
+ if kind == "multiple_heads":
358
+ canonical_parent = issue.get("canonical_parent")
359
+ if isinstance(canonical_parent, AlembicScript):
360
+ parent = canonical_parent.revision
361
+ if not isinstance(parent, str) or not parent:
362
+ return False
363
+ _rewrite_script_assignments(script.path, down_revision=(parent,))
364
+ repairs.append(
365
+ {
366
+ "kind": kind,
367
+ "path": os.fspath(script.path.relative_to(repo_root)),
368
+ "new_down_revision": parent,
369
+ }
370
+ )
371
+ return True
372
+ return False
373
+
374
+
375
+ def _rewrite_script_assignments(path: Path, *, revision: str | None = None, down_revision: tuple[str, ...] | None = None) -> None:
376
+ raw = path.read_text(encoding="utf-8")
377
+ if revision is not None:
378
+ raw = _replace_assignment(raw, name="revision", value_literal=repr(revision))
379
+ if down_revision is not None:
380
+ if len(down_revision) == 0:
381
+ replacement = "None"
382
+ elif len(down_revision) == 1:
383
+ replacement = repr(down_revision[0])
384
+ else:
385
+ replacement = repr(tuple(down_revision))
386
+ raw = _replace_assignment(raw, name="down_revision", value_literal=replacement)
387
+ path.write_text(raw, encoding="utf-8")
388
+
389
+
390
+ def _replace_assignment(raw: str, *, name: str, value_literal: str) -> str:
391
+ module = ast.parse(raw)
392
+ assignment = _find_assignment_node(module, name=name)
393
+ if assignment is None:
394
+ return raw
395
+
396
+ start = _line_col_to_offset(raw, lineno=assignment.lineno, col_offset=assignment.col_offset)
397
+ end_lineno = getattr(assignment, "end_lineno", assignment.lineno)
398
+ end_col_offset = getattr(assignment, "end_col_offset", assignment.col_offset)
399
+ end = _line_col_to_offset(raw, lineno=end_lineno, col_offset=end_col_offset)
400
+ indent = " " * assignment.col_offset
401
+ lhs = _assignment_lhs(raw, assignment=assignment, name=name, indent=indent)
402
+ return f"{raw[:start]}{lhs} = {value_literal}{raw[end:]}"
403
+
404
+
405
+ def _find_assignment_node(module: ast.Module, *, name: str) -> ast.Assign | ast.AnnAssign | None:
406
+ for statement in module.body:
407
+ if isinstance(statement, ast.Assign):
408
+ if len(statement.targets) != 1:
409
+ continue
410
+ target = statement.targets[0]
411
+ if isinstance(target, ast.Name) and target.id == name:
412
+ return statement
413
+ if isinstance(statement, ast.AnnAssign):
414
+ target = statement.target
415
+ if isinstance(target, ast.Name) and target.id == name:
416
+ return statement
417
+ return None
418
+
419
+
420
+ def _assignment_lhs(raw: str, *, assignment: ast.Assign | ast.AnnAssign, name: str, indent: str) -> str:
421
+ if isinstance(assignment, ast.Assign):
422
+ return f"{indent}{name}"
423
+
424
+ annotation = ast.get_source_segment(raw, assignment.annotation)
425
+ if annotation is None:
426
+ return f"{indent}{name}"
427
+ return f"{indent}{name}: {annotation}"
428
+
429
+
430
+ def _line_col_to_offset(raw: str, *, lineno: int, col_offset: int) -> int:
431
+ lines = raw.splitlines(keepends=True)
432
+ return sum(len(line) for line in lines[: lineno - 1]) + col_offset
433
+
434
+
435
+ def _deterministic_revision(*, repo_root: Path, script: AlembicScript) -> str:
436
+ digest = hashlib.sha1(os.fspath(script.path.relative_to(repo_root)).encode("utf-8")).hexdigest()
437
+ return f"{digest[:12]}"
438
+
439
+
440
+ def _repair_local_db_drift(
441
+ *,
442
+ repo_root: Path,
443
+ projects: list[AlembicProject],
444
+ validate_only: bool = False,
445
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
446
+ repairs: list[dict[str, Any]] = []
447
+ errors: list[dict[str, Any]] = []
448
+ db_path = _find_local_sqlite_db(repo_root)
449
+ if db_path is None or not projects:
450
+ return repairs, errors
451
+ if not db_path.exists():
452
+ return repairs, errors
453
+ canonical_heads = [head for project in projects for head in _head_revisions(project.scripts)]
454
+ if len(canonical_heads) != 1:
455
+ return repairs, errors
456
+ canonical_head = canonical_heads[0]
457
+ repo_revisions = {script.revision for project in projects for script in project.scripts}
458
+ with sqlite3.connect(db_path) as conn:
459
+ existing_table = conn.execute(
460
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='alembic_version'"
461
+ ).fetchone()
462
+ if existing_table is None:
463
+ return repairs, errors
464
+ rows = conn.execute("SELECT version_num FROM alembic_version").fetchall()
465
+ current_versions = [str(row[0]) for row in rows if row and row[0] is not None]
466
+ if not current_versions:
467
+ if not validate_only:
468
+ conn.execute("INSERT INTO alembic_version(version_num) VALUES (?)", (canonical_head,))
469
+ conn.commit()
470
+ repairs.append(
471
+ {
472
+ "kind": "db_revision_drift",
473
+ "db_path": os.fspath(db_path.relative_to(repo_root)),
474
+ "new_version": canonical_head,
475
+ }
476
+ )
477
+ return repairs, errors
478
+ out_of_graph = [revision for revision in current_versions if revision not in repo_revisions]
479
+ needs_reset = bool(out_of_graph) or current_versions != [canonical_head]
480
+ if needs_reset and not validate_only:
481
+ conn.execute("DELETE FROM alembic_version")
482
+ conn.execute("INSERT INTO alembic_version(version_num) VALUES (?)", (canonical_head,))
483
+ conn.commit()
484
+ repairs.append(
485
+ {
486
+ "kind": "db_revision_drift",
487
+ "db_path": os.fspath(db_path.relative_to(repo_root)),
488
+ "old_versions": current_versions,
489
+ "new_version": canonical_head,
490
+ }
491
+ )
492
+ current_versions = [canonical_head]
493
+ if current_versions != [canonical_head]:
494
+ errors.append(
495
+ {
496
+ "kind": "db_revision_not_at_head",
497
+ "message": f"local alembic_version is {current_versions!r}, expected {[canonical_head]!r}",
498
+ }
499
+ )
500
+ return repairs, errors
501
+
502
+
503
+ def _repair_live_current_overlap_drift(
504
+ *,
505
+ repo_root: Path,
506
+ projects: list[AlembicProject],
507
+ check_command: str,
508
+ current_run: subprocess.CompletedProcess[str],
509
+ ) -> tuple[subprocess.CompletedProcess[str], dict[str, Any]] | None:
510
+ return _repair_live_overlap_drift(
511
+ repo_root=repo_root,
512
+ projects=projects,
513
+ stamp_source_command=check_command,
514
+ overlap_run=current_run,
515
+ repair_kind="live_current_overlap_drift",
516
+ )
517
+
518
+
519
+ def _repair_live_overlap_drift(
520
+ *,
521
+ repo_root: Path,
522
+ projects: list[AlembicProject],
523
+ stamp_source_command: str,
524
+ overlap_run: subprocess.CompletedProcess[str],
525
+ repair_kind: str,
526
+ ) -> tuple[subprocess.CompletedProcess[str], dict[str, Any]] | None:
527
+ scripts = [script for project in projects for script in project.scripts]
528
+ normalized_revisions = _normalized_overlap_revisions(scripts=scripts, overlap_run=overlap_run)
529
+ if normalized_revisions is None:
530
+ return None
531
+
532
+ current_revisions, normalized = normalized_revisions
533
+ stamp_command = _rewrite_alembic_current_to_stamp(check_command=stamp_source_command, target_revisions=normalized)
534
+ if stamp_command is None:
535
+ return None
536
+
537
+ repair_run = _run_shell(repo_root=repo_root, command=stamp_command, timeout=120)
538
+ repair_record = {
539
+ "kind": repair_kind,
540
+ "old_versions": current_revisions,
541
+ "new_versions": normalized,
542
+ "repair_command": stamp_command,
543
+ "repair_run": _cp_to_meta(repair_run),
544
+ }
545
+ return repair_run, repair_record
546
+
547
+
548
+ def _normalized_overlap_revisions(
549
+ *,
550
+ scripts: list[AlembicScript],
551
+ overlap_run: subprocess.CompletedProcess[str],
552
+ ) -> tuple[list[str], list[str]] | None:
553
+ current_revisions = _extract_live_current_revisions(scripts=scripts, current_run=overlap_run)
554
+ if len(current_revisions) < 2:
555
+ return None
556
+
557
+ ancestry = _build_ancestor_map(scripts)
558
+ normalized = _maximal_descendant_revisions(current_revisions, ancestry=ancestry)
559
+ if normalized == current_revisions:
560
+ return None
561
+ removed = [revision for revision in current_revisions if revision not in normalized]
562
+ if not removed:
563
+ return None
564
+ if not all(any(removed_revision in ancestry.get(kept_revision, set()) for kept_revision in normalized) for removed_revision in removed):
565
+ return None
566
+ return current_revisions, normalized
567
+
568
+
569
+ def _extract_live_current_revisions(
570
+ *,
571
+ scripts: list[AlembicScript],
572
+ current_run: subprocess.CompletedProcess[str],
573
+ ) -> list[str]:
574
+ repo_revisions = {script.revision for script in scripts}
575
+ combined = "\n".join(part for part in (current_run.stdout, current_run.stderr) if part)
576
+ revisions = _ordered_known_revision_matches(combined, known_revisions=repo_revisions)
577
+ if revisions:
578
+ return revisions
579
+
580
+ overlap_match = re.search(
581
+ r"Requested revision\s+([A-Za-z0-9_]+)\s+overlaps with other requested revisions\s+([A-Za-z0-9_,\s]+)",
582
+ combined,
583
+ )
584
+ if overlap_match is None:
585
+ return []
586
+ overlap_revisions = _ordered_known_revision_matches(
587
+ " ".join(overlap_match.groups()),
588
+ known_revisions=repo_revisions,
589
+ )
590
+ return overlap_revisions
591
+
592
+
593
+ def _ordered_known_revision_matches(raw: str, *, known_revisions: set[str]) -> list[str]:
594
+ matches: list[tuple[int, str]] = []
595
+ for revision in known_revisions:
596
+ for match in re.finditer(rf"(?<![A-Za-z0-9_]){re.escape(revision)}(?![A-Za-z0-9_])", raw):
597
+ matches.append((match.start(), revision))
598
+ matches.sort()
599
+ ordered: list[str] = []
600
+ seen: set[str] = set()
601
+ for _, revision in matches:
602
+ if revision in seen:
603
+ continue
604
+ seen.add(revision)
605
+ ordered.append(revision)
606
+ return ordered
607
+
608
+
609
+ def _build_ancestor_map(scripts: list[AlembicScript]) -> dict[str, set[str]]:
610
+ parents = {script.revision: set(script.down_revisions) for script in scripts}
611
+ ancestors: dict[str, set[str]] = {}
612
+
613
+ def collect(revision: str) -> set[str]:
614
+ if revision in ancestors:
615
+ return ancestors[revision]
616
+ collected: set[str] = set()
617
+ for parent in parents.get(revision, set()):
618
+ collected.add(parent)
619
+ collected.update(collect(parent))
620
+ ancestors[revision] = collected
621
+ return collected
622
+
623
+ for script in scripts:
624
+ collect(script.revision)
625
+ return ancestors
626
+
627
+
628
+ def _maximal_descendant_revisions(revisions: list[str], *, ancestry: dict[str, set[str]]) -> list[str]:
629
+ normalized: list[str] = []
630
+ for revision in revisions:
631
+ if any(revision in ancestry.get(other, set()) for other in revisions if other != revision):
632
+ continue
633
+ normalized.append(revision)
634
+ return normalized
635
+
636
+
637
+ def _rewrite_alembic_current_to_stamp(*, check_command: str, target_revisions: list[str]) -> str | None:
638
+ revisions_literal = " ".join(target_revisions)
639
+ patterns = [
640
+ (r"\balembic\s+current\b", f"alembic stamp {revisions_literal}"),
641
+ (r"\bpython(?:3)?\s+-m\s+alembic\s+current\b", f"python -m alembic stamp {revisions_literal}"),
642
+ ]
643
+ for pattern, replacement in patterns:
644
+ rewritten, count = re.subn(pattern, replacement, check_command, count=1)
645
+ if count:
646
+ return rewritten
647
+ return None
648
+
649
+
650
+ def _find_local_sqlite_db(repo_root: Path) -> Path | None:
651
+ env_candidates = [repo_root / ".env", repo_root / ".env.local", repo_root / ".env.dev", repo_root / ".env.development"]
652
+ for candidate in env_candidates:
653
+ if not candidate.exists():
654
+ continue
655
+ match = _SQLITE_URL_RE.search(candidate.read_text(encoding="utf-8"))
656
+ if match:
657
+ return _sqlite_url_to_path(repo_root=repo_root, value=match.group("path"))
658
+ return None
659
+
660
+
661
+ def _sqlite_url_to_path(*, repo_root: Path, value: str) -> Path:
662
+ if value.startswith("/"):
663
+ return Path(value)
664
+ return (repo_root / value).resolve()
665
+
666
+
667
+ def _run_shell(*, repo_root: Path, command: str, timeout: int) -> subprocess.CompletedProcess[str]:
668
+ return subprocess.run(
669
+ ["/bin/sh", "-lc", command],
670
+ cwd=str(repo_root),
671
+ capture_output=True,
672
+ text=True,
673
+ check=False,
674
+ timeout=timeout,
675
+ )
676
+
677
+
678
+ def _cp_to_meta(cp: subprocess.CompletedProcess[str]) -> dict[str, Any]:
679
+ return {
680
+ "returncode": cp.returncode,
681
+ "stdout": cp.stdout[-1000:],
682
+ "stderr": cp.stderr[-1000:],
683
+ }
684
+
685
+
686
+ def _issue_to_message(issue: dict[str, Any]) -> dict[str, Any]:
687
+ kind = str(issue["kind"])
688
+ script = issue.get("script")
689
+ rel_path = os.fspath(script.path) if isinstance(script, AlembicScript) else "unknown"
690
+ if kind == "duplicate_revision_id":
691
+ return {"kind": kind, "message": f"duplicate revision id remains in {rel_path}"}
692
+ if kind == "multiple_heads":
693
+ return {"kind": kind, "message": f"multiple alembic heads remain after normalization near {rel_path}"}
694
+ if kind == "broken_parent_reference":
695
+ return {"kind": kind, "message": f"broken parent reference remains in {rel_path}"}
696
+ if kind == "missing_down_revision":
697
+ return {"kind": kind, "message": f"missing down_revision remains in {rel_path}"}
698
+ if kind == "stale_autogen_branch":
699
+ return {"kind": kind, "message": f"stale autogen branch remains in {rel_path}"}
700
+ return {"kind": kind, "message": f"unresolved alembic issue in {rel_path}"}