codd-dev 1.26.0__tar.gz → 1.27.0__tar.gz

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 (184) hide show
  1. {codd_dev-1.26.0 → codd_dev-1.27.0}/PKG-INFO +1 -1
  2. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/cli.py +402 -5
  3. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/__init__.py +8 -0
  4. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/builder.py +14 -0
  5. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/means_catalog.py +1 -1
  6. codd_dev-1.27.0/codd/repair/__init__.py +15 -0
  7. codd_dev-1.27.0/codd/repair/approval_repair.py +127 -0
  8. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/history.py +3 -2
  9. codd_dev-1.27.0/codd/repair/loop.py +298 -0
  10. codd_dev-1.27.0/codd/repair/verify_runner.py +355 -0
  11. {codd_dev-1.26.0 → codd_dev-1.27.0}/pyproject.toml +1 -1
  12. codd_dev-1.26.0/codd/llm/templates/verification_means_catalog.yaml +0 -6
  13. codd_dev-1.26.0/codd/repair/__init__.py +0 -5
  14. {codd_dev-1.26.0 → codd_dev-1.27.0}/.gitignore +0 -0
  15. {codd_dev-1.26.0 → codd_dev-1.27.0}/LICENSE +0 -0
  16. {codd_dev-1.26.0 → codd_dev-1.27.0}/README.md +0 -0
  17. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/__init__.py +0 -0
  18. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/__main__.py +0 -0
  19. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/_git_helper.py +0 -0
  20. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/ask_user_question_adapter.py +0 -0
  21. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/assembler.py +0 -0
  22. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/bridge.py +0 -0
  23. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/clustering.py +0 -0
  24. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/coherence_adapters.py +0 -0
  25. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/coherence_engine.py +0 -0
  26. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/config.py +0 -0
  27. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/contracts.py +0 -0
  28. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/coverage_auditor.py +0 -0
  29. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/coverage_metrics.py +0 -0
  30. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/__init__.py +0 -0
  31. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/depends_on_consistency.py +0 -0
  32. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/deployment_completeness.py +0 -0
  33. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/edge_validity.py +0 -0
  34. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/node_completeness.py +0 -0
  35. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/task_completion.py +0 -0
  36. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/transitive_closure.py +0 -0
  37. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/checks/user_journey_coherence.py +0 -0
  38. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/cli.yaml +0 -0
  39. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/cpp_embedded.yaml +0 -0
  40. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/csharp.yaml +0 -0
  41. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/elixir.yaml +0 -0
  42. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/generic.yaml +0 -0
  43. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/iot.yaml +0 -0
  44. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/java.yaml +0 -0
  45. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/kotlin.yaml +0 -0
  46. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/mobile.yaml +0 -0
  47. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/ruby.yaml +0 -0
  48. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/rust.yaml +0 -0
  49. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/scala.yaml +0 -0
  50. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/swift.yaml +0 -0
  51. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/test_frameworks.yaml +0 -0
  52. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/defaults/web.yaml +0 -0
  53. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/extractor.py +0 -0
  54. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/dag/runner.py +0 -0
  55. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/defaults.yaml +0 -0
  56. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deploy_targets/__init__.py +0 -0
  57. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deploy_targets/app_service.py +0 -0
  58. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deploy_targets/base.py +0 -0
  59. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deploy_targets/docker_compose.py +0 -0
  60. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployer.py +0 -0
  61. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/__init__.py +0 -0
  62. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/checks/__init__.py +0 -0
  63. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/defaults/deploy_targets.yaml +0 -0
  64. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/defaults/runtime_capability_inference.yaml +0 -0
  65. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/defaults/schema_providers.yaml +0 -0
  66. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/defaults/verification_templates.yaml +0 -0
  67. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/extractor.py +0 -0
  68. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/__init__.py +0 -0
  69. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/ai_command.py +0 -0
  70. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/llm_consideration.py +0 -0
  71. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/schema/__init__.py +0 -0
  72. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/schema/prisma.py +0 -0
  73. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/target/__init__.py +0 -0
  74. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/target/docker_compose.py +0 -0
  75. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/__init__.py +0 -0
  76. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/assertion_handlers.py +0 -0
  77. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/cdp_browser.py +0 -0
  78. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/cdp_engines.py +0 -0
  79. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/cdp_launchers.py +0 -0
  80. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/cdp_wire.py +0 -0
  81. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/curl.py +0 -0
  82. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/form_strategies.py +0 -0
  83. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/deployment/providers/verification/playwright.py +0 -0
  84. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/design_md.py +0 -0
  85. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/drift.py +0 -0
  86. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/e2e_extractor.py +0 -0
  87. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/e2e_generator.py +0 -0
  88. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/e2e_runner.py +0 -0
  89. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/env_refs.py +0 -0
  90. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/extract_ai.py +0 -0
  91. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/extractor.py +0 -0
  92. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixer.py +0 -0
  93. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixup_drift.py +0 -0
  94. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixup_drift_strategies/__init__.py +0 -0
  95. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixup_drift_strategies/design_token_drift.py +0 -0
  96. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixup_drift_strategies/lexicon_violation.py +0 -0
  97. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/fixup_drift_strategies/url_drift.py +0 -0
  98. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/generator.py +0 -0
  99. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/graph.py +0 -0
  100. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hitl_session.py +0 -0
  101. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/__init__.py +0 -0
  102. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/pre-commit +0 -0
  103. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/recipes/claude_settings_example.json +0 -0
  104. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/recipes/codex_hook.sh +0 -0
  105. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/recipes/git_post_commit.sh +0 -0
  106. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/hooks/recipes/git_pre_commit.sh +0 -0
  107. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/implementer.py +0 -0
  108. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/inheritance.py +0 -0
  109. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/knowledge_fetcher.py +0 -0
  110. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/lexicon.py +0 -0
  111. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/__init__.py +0 -0
  112. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/approval.py +0 -0
  113. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/means_catalog_loader.py +0 -0
  114. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/parser.py +0 -0
  115. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/prompt_builder.py +0 -0
  116. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/strategy_validator.py +0 -0
  117. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/llm/templates/meta_instruction.md +0 -0
  118. {codd_dev-1.26.0/codd/deployment/defaults → codd_dev-1.27.0/codd/llm/templates}/verification_means_catalog.yaml +0 -0
  119. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/mcp_server.py +0 -0
  120. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/measure.py +0 -0
  121. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/parsing.py +0 -0
  122. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/planner.py +0 -0
  123. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/policy.py +0 -0
  124. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/preflight/__init__.py +0 -0
  125. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/preflight/defaults/cli.yaml +0 -0
  126. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/preflight/defaults/iot.yaml +0 -0
  127. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/preflight/defaults/mobile.yaml +0 -0
  128. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/preflight/defaults/web.yaml +0 -0
  129. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/propagate.py +0 -0
  130. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/propagator.py +0 -0
  131. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/registry.py +0 -0
  132. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/engine.py +0 -0
  133. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/git_patcher.py +0 -0
  134. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/llm_repair_engine.py +0 -0
  135. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/schema.py +0 -0
  136. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/templates/analyze_meta.md +0 -0
  137. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair/templates/propose_meta.md +0 -0
  138. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/repair_slice.py +0 -0
  139. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/require.py +0 -0
  140. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/require_plugins.py +0 -0
  141. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/require_propagate.py +0 -0
  142. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/required_artifacts/defaults/cli.yaml +0 -0
  143. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/required_artifacts/defaults/iot.yaml +0 -0
  144. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/required_artifacts/defaults/mobile.yaml +0 -0
  145. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/required_artifacts/defaults/web.yaml +0 -0
  146. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/required_artifacts_deriver.py +0 -0
  147. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/requirement_completeness/defaults/cli.yaml +0 -0
  148. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/requirement_completeness/defaults/iot.yaml +0 -0
  149. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/requirement_completeness/defaults/mobile.yaml +0 -0
  150. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/requirement_completeness/defaults/web.yaml +0 -0
  151. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/requirement_completeness_auditor.py +0 -0
  152. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/restore.py +0 -0
  153. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/routes_extractor.py +0 -0
  154. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/scanner.py +0 -0
  155. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/schema_refs.py +0 -0
  156. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/screen_flow_validator.py +0 -0
  157. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/screen_transition_extractor.py +0 -0
  158. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/screen_transitions/defaults.yaml +0 -0
  159. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/synth.py +0 -0
  160. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/codd.yaml.tmpl +0 -0
  161. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/conventions.yaml.tmpl +0 -0
  162. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
  163. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/doc_links.yaml.tmpl +0 -0
  164. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extract_ai_prompt_baseline.md +0 -0
  165. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
  166. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
  167. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
  168. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
  169. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/extracted/system-context.md.j2 +0 -0
  170. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/gitignore.tmpl +0 -0
  171. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/lexicon_questions.md +0 -0
  172. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/lexicon_schema.yaml +0 -0
  173. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/templates/overrides.yaml.tmpl +0 -0
  174. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/traceability.py +0 -0
  175. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/validator.py +0 -0
  176. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/__init__.py +0 -0
  177. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/events.py +0 -0
  178. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/propagation_log.py +0 -0
  179. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/propagation_pipeline.py +0 -0
  180. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/test_runner.py +0 -0
  181. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/watch/watcher.py +0 -0
  182. {codd_dev-1.26.0 → codd_dev-1.27.0}/codd/wiring.py +0 -0
  183. {codd_dev-1.26.0 → codd_dev-1.27.0}/docs/cookbook/cdp_browser/README.md +0 -0
  184. {codd_dev-1.26.0 → codd_dev-1.27.0}/docs/requirements/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codd-dev
3
- Version: 1.26.0
3
+ Version: 1.27.0
4
4
  Summary: CoDD: Coherence-Driven Development — cross-artifact change impact analysis
5
5
  Project-URL: Homepage, https://github.com/yohey-w/codd-dev
6
6
  Project-URL: Repository, https://github.com/yohey-w/codd-dev
@@ -1,6 +1,7 @@
1
1
  """CoDD CLI — codd init / scan / impact / require / plan."""
2
2
 
3
- from dataclasses import asdict, is_dataclass
3
+ from dataclasses import asdict, dataclass, is_dataclass
4
+ from datetime import datetime, timezone
4
5
  import json
5
6
  import os
6
7
  import shutil
@@ -21,8 +22,15 @@ class CoddCLIError(RuntimeError):
21
22
  """Error raised for CLI-facing validation failures."""
22
23
 
23
24
 
24
- def _run_pro_command(name: str, **kwargs):
25
- """Dispatch a Pro-only command when the bridge plugin is installed."""
25
+ @dataclass
26
+ class _CliVerificationResult:
27
+ passed: bool
28
+ exit_code: int
29
+ failure: Any | None = None
30
+
31
+
32
+ def _run_pro_command(name: str, **kwargs):
33
+ """Dispatch a Pro-only command when the bridge plugin is installed."""
26
34
  handler = get_command_handler(name)
27
35
  if handler is None:
28
36
  click.echo(PRO_COMMAND_INSTALL_MESSAGE)
@@ -1046,7 +1054,20 @@ def assemble(path: str, output_dir: str | None, ai_cmd: str | None):
1046
1054
  default=False,
1047
1055
  help="Run npx @google/design.md lint on DESIGN.md (skip if npx unavailable).",
1048
1056
  )
1049
- def verify(path: str, sprint: int | None, e2e: bool, deploy: bool, base_url: str | None, design_md: bool) -> None:
1057
+ @click.option("--auto-repair", is_flag=True, default=False, help="Run RepairLoop when verification fails")
1058
+ @click.option("--max-attempts", default=None, type=click.IntRange(min=1), help="Maximum repair attempts")
1059
+ @click.option("--engine", "engine_name", default=None, help="Repair engine name")
1060
+ def verify(
1061
+ path: str,
1062
+ sprint: int | None,
1063
+ e2e: bool,
1064
+ deploy: bool,
1065
+ base_url: str | None,
1066
+ design_md: bool,
1067
+ auto_repair: bool,
1068
+ max_attempts: int | None,
1069
+ engine_name: str | None,
1070
+ ) -> None:
1050
1071
  """Run build + test verification and trace failures to design documents."""
1051
1072
  if design_md:
1052
1073
  _run_design_md_lint(Path(path).resolve())
@@ -1055,7 +1076,31 @@ def verify(path: str, sprint: int | None, e2e: bool, deploy: bool, base_url: str
1055
1076
  from codd.e2e_runner import run_e2e
1056
1077
  run_e2e(path=path, deploy=deploy, base_url=base_url)
1057
1078
  return
1058
- _run_pro_command("verify", path=path, sprint=sprint)
1079
+
1080
+ if not auto_repair:
1081
+ _run_pro_command("verify", path=path, sprint=sprint)
1082
+ return
1083
+
1084
+ project_root = Path(path).resolve()
1085
+ result = _run_verify_once(path=path, sprint=sprint)
1086
+ if result.passed:
1087
+ return
1088
+
1089
+ repair_config = _load_required_repair_config(project_root)
1090
+ if repair_config is None:
1091
+ raise SystemExit(1)
1092
+
1093
+ outcome = _run_repair_loop(
1094
+ project_root,
1095
+ result.failure,
1096
+ repair_config=repair_config,
1097
+ max_attempts=max_attempts,
1098
+ engine_name=engine_name,
1099
+ verify_callable=lambda: _run_verify_once(path=path, sprint=sprint),
1100
+ )
1101
+ click.echo(f"Repair outcome: {outcome.status}")
1102
+ click.echo(f"Repair history: {_display_path(outcome.history_session_dir, project_root)}")
1103
+ raise SystemExit(_repair_exit_code(outcome.status))
1059
1104
 
1060
1105
 
1061
1106
  def _run_e2e_generate(path: str, base_url: str, output: str | None, framework: str, mode: str = "scenarios") -> None:
@@ -2396,6 +2441,358 @@ def _load_optional_project_config(project_root: Path) -> dict[str, Any]:
2396
2441
  return {}
2397
2442
 
2398
2443
 
2444
+ def _run_verify_once(path: str, sprint: int | None = None) -> _CliVerificationResult:
2445
+ try:
2446
+ _run_pro_command("verify", path=path, sprint=sprint)
2447
+ except SystemExit as exc:
2448
+ exit_code = _system_exit_code(exc)
2449
+ passed = exit_code == 0
2450
+ failure = None
2451
+ if not passed:
2452
+ failure = _verification_failure_report(
2453
+ "verify",
2454
+ [],
2455
+ [f"codd verify exited with code {exit_code}"],
2456
+ {},
2457
+ )
2458
+ return _CliVerificationResult(passed=passed, exit_code=exit_code, failure=failure)
2459
+ return _CliVerificationResult(passed=True, exit_code=0, failure=None)
2460
+
2461
+
2462
+ def _system_exit_code(exc: SystemExit) -> int:
2463
+ code = exc.code
2464
+ if code is None:
2465
+ return 0
2466
+ try:
2467
+ return int(code)
2468
+ except (TypeError, ValueError):
2469
+ return 1
2470
+
2471
+
2472
+ def _load_required_repair_config(project_root: Path) -> dict[str, Any] | None:
2473
+ try:
2474
+ config = load_project_config(project_root)
2475
+ except (FileNotFoundError, ValueError) as exc:
2476
+ click.echo(f"WARN: codd.yaml repair config is required for repair: {exc}")
2477
+ return None
2478
+
2479
+ repair = config.get("repair")
2480
+ if not isinstance(repair, dict) or not repair:
2481
+ click.echo("WARN: codd.yaml [repair] section is required for repair.")
2482
+ return None
2483
+ return config
2484
+
2485
+
2486
+ def _run_repair_loop(
2487
+ project_root: Path,
2488
+ failure: Any,
2489
+ *,
2490
+ repair_config: dict[str, Any],
2491
+ max_attempts: int | None,
2492
+ engine_name: str | None,
2493
+ verify_callable,
2494
+ ):
2495
+ from codd.dag import DAG
2496
+ from codd.dag.builder import build_dag
2497
+ from codd.repair import RepairLoop, RepairLoopConfig
2498
+
2499
+ try:
2500
+ dag = build_dag(project_root)
2501
+ except (FileNotFoundError, ValueError):
2502
+ dag = DAG()
2503
+
2504
+ resolved_failure = failure if failure is not None else _verification_failure_report("verify", [], [], {})
2505
+ if getattr(resolved_failure, "dag_snapshot", None) in ({}, None):
2506
+ resolved_failure.dag_snapshot = _dag_snapshot(dag, project_root)
2507
+
2508
+ repair = repair_config.get("repair") if isinstance(repair_config.get("repair"), dict) else {}
2509
+ config = RepairLoopConfig(
2510
+ max_attempts=_repair_max_attempts(repair, max_attempts),
2511
+ approval_mode=str(repair.get("approval_mode") or "required"), # type: ignore[arg-type]
2512
+ history_dir=Path(str(repair.get("history_dir") or ".codd/repair_history")),
2513
+ engine_name=str(engine_name or repair.get("engine_name") or repair.get("engine") or "llm"),
2514
+ )
2515
+ return RepairLoop(config, project_root).run(resolved_failure, dag, verify_callable=verify_callable)
2516
+
2517
+
2518
+ def _repair_max_attempts(repair: dict[str, Any], max_attempts: int | None) -> int:
2519
+ raw = max_attempts if max_attempts is not None else repair.get("max_attempts", 3)
2520
+ try:
2521
+ return max(1, int(raw))
2522
+ except (TypeError, ValueError):
2523
+ return 3
2524
+
2525
+
2526
+ def _dag_snapshot(dag: Any, project_root: Path) -> dict[str, Any]:
2527
+ try:
2528
+ from codd.dag.builder import dag_to_dict
2529
+
2530
+ return dag_to_dict(dag, project_root)
2531
+ except Exception: # noqa: BLE001 - repair reports should survive DAG serialization failures.
2532
+ return {}
2533
+
2534
+
2535
+ def _verification_failure_report(
2536
+ check_name: str,
2537
+ failed_nodes: list[str],
2538
+ error_messages: list[str],
2539
+ dag_snapshot: dict[str, Any],
2540
+ ):
2541
+ from codd.repair.schema import VerificationFailureReport
2542
+
2543
+ return VerificationFailureReport(
2544
+ check_name=check_name,
2545
+ failed_nodes=failed_nodes,
2546
+ error_messages=error_messages,
2547
+ dag_snapshot=dag_snapshot,
2548
+ timestamp=_utc_timestamp(),
2549
+ )
2550
+
2551
+
2552
+ def _utc_timestamp() -> str:
2553
+ return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
2554
+
2555
+
2556
+ def _repair_exit_code(status: str) -> int:
2557
+ return {
2558
+ "REPAIR_SUCCESS": 0,
2559
+ "REPAIR_REJECTED_BY_HITL": 1,
2560
+ "REPAIR_EXHAUSTED": 2,
2561
+ "REPAIR_FAILED": 3,
2562
+ }.get(str(status), 3)
2563
+
2564
+
2565
+ def _load_failure_report(path: Path) -> Any:
2566
+ payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
2567
+ if not isinstance(payload, dict):
2568
+ raise ValueError("failure report must contain a YAML mapping")
2569
+ return _verification_failure_report(
2570
+ str(payload.get("check_name") or payload.get("name") or "verify"),
2571
+ _repair_string_list(payload.get("failed_nodes") or payload.get("nodes")),
2572
+ _repair_string_list(payload.get("error_messages") or payload.get("errors") or payload.get("messages")),
2573
+ payload.get("dag_snapshot") if isinstance(payload.get("dag_snapshot"), dict) else {},
2574
+ )
2575
+
2576
+
2577
+ def _repair_string_list(value: Any) -> list[str]:
2578
+ if isinstance(value, list):
2579
+ return [str(item) for item in value if item is not None]
2580
+ if value is None:
2581
+ return []
2582
+ return [str(value)]
2583
+
2584
+
2585
+ def _repair_history_dir(project_root: Path, config: dict[str, Any] | None = None) -> Path:
2586
+ repair = config.get("repair") if isinstance(config, dict) and isinstance(config.get("repair"), dict) else {}
2587
+ history_dir = Path(str(repair.get("history_dir") or ".codd/repair_history"))
2588
+ if history_dir.is_absolute():
2589
+ return history_dir
2590
+ return project_root / history_dir
2591
+
2592
+
2593
+ def _session_attempt_dirs(session_dir: Path) -> list[Path]:
2594
+ return sorted(
2595
+ [path for path in session_dir.glob("attempt_*") if path.is_dir()],
2596
+ key=lambda path: _attempt_number(path.name),
2597
+ )
2598
+
2599
+
2600
+ def _attempt_number(name: str) -> int:
2601
+ try:
2602
+ return int(name.removeprefix("attempt_"))
2603
+ except ValueError:
2604
+ return 10**9
2605
+
2606
+
2607
+ def _read_repair_yaml(path: Path) -> Any:
2608
+ if not path.exists():
2609
+ return None
2610
+ return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
2611
+
2612
+
2613
+ def _repair_session_summary(session_dir: Path) -> dict[str, Any]:
2614
+ final_status = _read_repair_yaml(session_dir / "final_status.yaml") or {}
2615
+ attempts = _session_attempt_dirs(session_dir)
2616
+ return {
2617
+ "history_id": session_dir.name,
2618
+ "timestamp": final_status.get("timestamp") or session_dir.name,
2619
+ "status": final_status.get("outcome") or final_status.get("status") or "IN_PROGRESS",
2620
+ "attempts": len(attempts),
2621
+ "path": str(session_dir),
2622
+ }
2623
+
2624
+
2625
+ def _session_matches_design_doc(session_dir: Path, design_doc: str | None) -> bool:
2626
+ if not design_doc:
2627
+ return True
2628
+ needle = str(design_doc)
2629
+ for path in session_dir.glob("**/*.yaml"):
2630
+ try:
2631
+ if needle in path.read_text(encoding="utf-8"):
2632
+ return True
2633
+ except OSError:
2634
+ continue
2635
+ return False
2636
+
2637
+
2638
+ def _resolve_history_session(project_root: Path, history_id: str, config: dict[str, Any] | None = None) -> Path:
2639
+ raw = Path(history_id).expanduser()
2640
+ if raw.is_absolute() and raw.is_dir():
2641
+ return raw
2642
+ if raw.is_dir():
2643
+ return raw.resolve()
2644
+ session_dir = _repair_history_dir(project_root, config) / history_id
2645
+ if session_dir.is_dir():
2646
+ return session_dir
2647
+ raise FileNotFoundError(f"repair history session not found: {history_id}")
2648
+
2649
+
2650
+ def _load_repair_proposal(path: Path) -> Any:
2651
+ from codd.repair.schema import RepairProposal
2652
+
2653
+ payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
2654
+ if not isinstance(payload, dict):
2655
+ raise ValueError("repair proposal must contain a YAML mapping")
2656
+ return RepairProposal(**payload)
2657
+
2658
+
2659
+ @main.group(invoke_without_command=True)
2660
+ @click.option("--from-report", "from_report", type=click.Path(exists=True, dir_okay=False, path_type=Path))
2661
+ @click.option("--path", "project_path", default=".", help="Project root directory")
2662
+ @click.option("--max-attempts", default=None, type=click.IntRange(min=1), help="Maximum repair attempts")
2663
+ @click.option("--engine", "engine_name", default=None, help="Repair engine name")
2664
+ @click.pass_context
2665
+ def repair(ctx, from_report: Path | None, project_path: str, max_attempts: int | None, engine_name: str | None):
2666
+ """Run and inspect repair sessions."""
2667
+ if ctx.invoked_subcommand is not None:
2668
+ return
2669
+ if from_report is None:
2670
+ click.echo(ctx.get_help())
2671
+ return
2672
+
2673
+ project_root = Path(project_path).resolve()
2674
+ repair_config = _load_required_repair_config(project_root)
2675
+ if repair_config is None:
2676
+ raise SystemExit(1)
2677
+
2678
+ try:
2679
+ failure = _load_failure_report(from_report)
2680
+ outcome = _run_repair_loop(
2681
+ project_root,
2682
+ failure,
2683
+ repair_config=repair_config,
2684
+ max_attempts=max_attempts,
2685
+ engine_name=engine_name,
2686
+ verify_callable=lambda: _run_verify_once(path=str(project_root), sprint=None),
2687
+ )
2688
+ except (FileNotFoundError, ValueError) as exc:
2689
+ click.echo(f"Error: {exc}")
2690
+ raise SystemExit(1)
2691
+
2692
+ click.echo(f"Repair outcome: {outcome.status}")
2693
+ click.echo(f"Repair history: {_display_path(outcome.history_session_dir, project_root)}")
2694
+ raise SystemExit(_repair_exit_code(outcome.status))
2695
+
2696
+
2697
+ @repair.command("history")
2698
+ @click.option("--path", "project_path", default=".", help="Project root directory")
2699
+ @click.option("--last", "last", default=10, type=click.IntRange(min=1), show_default=True, help="Number of sessions")
2700
+ @click.option("--design-doc", "design_doc", default=None, help="Filter sessions containing a design doc path")
2701
+ def repair_history(project_path: str, last: int, design_doc: str | None):
2702
+ """List repair history sessions."""
2703
+ from codd.repair.history import RepairHistory
2704
+
2705
+ project_root = Path(project_path).resolve()
2706
+ config = _load_optional_project_config(project_root)
2707
+ sessions = [
2708
+ session
2709
+ for session in RepairHistory().list_sessions(_repair_history_dir(project_root, config))
2710
+ if _session_matches_design_doc(session, design_doc)
2711
+ ][:last]
2712
+
2713
+ if not sessions:
2714
+ click.echo("No repair history found.")
2715
+ return
2716
+
2717
+ for session_dir in sessions:
2718
+ summary = _repair_session_summary(session_dir)
2719
+ click.echo(
2720
+ f"{summary['history_id']}\t{summary['status']}\t"
2721
+ f"attempts={summary['attempts']}\t{summary['timestamp']}"
2722
+ )
2723
+
2724
+
2725
+ @repair.command("approve")
2726
+ @click.argument("history_id")
2727
+ @click.option("--attempt", "attempt", default=None, type=click.IntRange(min=0), help="Attempt number")
2728
+ @click.option("--path", "project_path", default=".", help="Project root directory")
2729
+ def repair_approve(history_id: str, attempt: int | None, project_path: str):
2730
+ """Approve a repair proposal in history."""
2731
+ from codd.repair.approval_repair import approve_repair_proposal
2732
+
2733
+ project_root = Path(project_path).resolve()
2734
+ config = _load_optional_project_config(project_root)
2735
+ try:
2736
+ session_dir = _resolve_history_session(project_root, history_id, config)
2737
+ attempt_dirs = _session_attempt_dirs(session_dir)
2738
+ if not attempt_dirs:
2739
+ raise FileNotFoundError("repair attempt not found")
2740
+ attempt_dir = session_dir / f"attempt_{attempt}" if attempt is not None else attempt_dirs[-1]
2741
+ proposal = _load_repair_proposal(attempt_dir / "repair_proposal.yaml")
2742
+ except (FileNotFoundError, ValueError) as exc:
2743
+ click.echo(f"Error: {exc}")
2744
+ raise SystemExit(1)
2745
+
2746
+ approval_config = dict(config)
2747
+ repair_config = dict(approval_config.get("repair") if isinstance(approval_config.get("repair"), dict) else {})
2748
+ repair_config["approval_decision"] = "approved"
2749
+ approval_config["repair"] = repair_config
2750
+ approved = approve_repair_proposal(
2751
+ proposal,
2752
+ approval_mode="required",
2753
+ codd_yaml=approval_config,
2754
+ notify_callable=click.echo,
2755
+ )
2756
+ if not approved:
2757
+ click.echo("Repair proposal not approved.")
2758
+ raise SystemExit(1)
2759
+
2760
+ (attempt_dir / "approval.yaml").write_text(
2761
+ yaml.safe_dump({"status": "approved", "timestamp": _utc_timestamp()}, sort_keys=False),
2762
+ encoding="utf-8",
2763
+ )
2764
+ click.echo(f"Approved repair proposal: {session_dir.name} attempt={attempt_dir.name.removeprefix('attempt_')}")
2765
+
2766
+
2767
+ @repair.command("status")
2768
+ @click.argument("history_id", required=False)
2769
+ @click.option("--path", "project_path", default=".", help="Project root directory")
2770
+ def repair_status(history_id: str | None, project_path: str):
2771
+ """Show repair session status."""
2772
+ from codd.repair.history import RepairHistory
2773
+
2774
+ project_root = Path(project_path).resolve()
2775
+ config = _load_optional_project_config(project_root)
2776
+ try:
2777
+ if history_id is None:
2778
+ sessions = RepairHistory().list_sessions(_repair_history_dir(project_root, config))
2779
+ if not sessions:
2780
+ click.echo("No repair history found.")
2781
+ raise SystemExit(1)
2782
+ session_dir = sessions[0]
2783
+ else:
2784
+ session_dir = _resolve_history_session(project_root, history_id, config)
2785
+ except FileNotFoundError as exc:
2786
+ click.echo(f"Error: {exc}")
2787
+ raise SystemExit(1)
2788
+
2789
+ summary = _repair_session_summary(session_dir)
2790
+ click.echo(f"history_id: {summary['history_id']}")
2791
+ click.echo(f"status: {summary['status']}")
2792
+ click.echo(f"attempts: {summary['attempts']}")
2793
+ click.echo(f"timestamp: {summary['timestamp']}")
2794
+
2795
+
2399
2796
  @main.group()
2400
2797
  def dag():
2401
2798
  """DAG Completeness Gate commands."""
@@ -6,6 +6,14 @@ from dataclasses import dataclass, field
6
6
  from typing import Any, Optional
7
7
 
8
8
 
9
+ def reset_dag_cache(project_root=None) -> None:
10
+ """Clear DAG builder cache state for a fresh verification attempt."""
11
+
12
+ from codd.dag.builder import reset_dag_cache as _reset_dag_cache
13
+
14
+ _reset_dag_cache(project_root)
15
+
16
+
9
17
  @dataclass
10
18
  class Node:
11
19
  id: str
@@ -30,6 +30,7 @@ from codd.deployment.extractor import (
30
30
  LOGGER = logging.getLogger(__name__)
31
31
  DEFAULTS_DIR = Path(__file__).parent / "defaults"
32
32
  DEFAULT_PROJECT_TYPE = "generic"
33
+ _DAG_BUILD_CACHE: dict[str, Any] = {}
33
34
  LEGACY_IMPLEMENTATION_SUFFIXES = (".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".java")
34
35
  LEGACY_TEST_SUFFIXES = (".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".bats")
35
36
  PLAN_HEADER_RE = re.compile(r"^#{2,6}\s+([A-Za-z0-9]+(?:[-_.][A-Za-z0-9]+)*)(?:\s+(.+))?$", re.MULTILINE)
@@ -85,6 +86,19 @@ def build_dag(project_root: Path, settings: dict[str, Any] | None = None) -> DAG
85
86
  return dag
86
87
 
87
88
 
89
+ def reset_dag_cache(project_root: Path | None = None) -> None:
90
+ """Clear in-process DAG builder cache state.
91
+
92
+ The builder currently rebuilds eagerly, but repair verification calls this
93
+ public hook per attempt so future memoization cannot leak stale DAG state.
94
+ """
95
+
96
+ if project_root is None:
97
+ _DAG_BUILD_CACHE.clear()
98
+ return
99
+ _DAG_BUILD_CACHE.pop(str(Path(project_root).resolve()), None)
100
+
101
+
88
102
  def load_dag_settings(project_root: Path, settings: dict[str, Any] | None = None) -> dict[str, Any]:
89
103
  """Load project-type defaults and apply ``codd.yaml dag:`` overrides."""
90
104
 
@@ -14,7 +14,7 @@ class VerificationMeansCatalog:
14
14
  """Load verification means from project override or bundled defaults."""
15
15
 
16
16
  DEFAULT_CATALOG_PATH: ClassVar[str] = str(
17
- Path(__file__).parents[2] / "defaults" / "verification_means_catalog.yaml"
17
+ Path(__file__).parents[3] / "llm" / "templates" / "verification_means_catalog.yaml"
18
18
  )
19
19
 
20
20
  @classmethod
@@ -0,0 +1,15 @@
1
+ """Repair engine package."""
2
+
3
+ from codd.repair.llm_repair_engine import LlmRepairEngine, RepairFailed
4
+ from codd.repair.loop import RepairLoop, RepairLoopConfig, RepairLoopOutcome
5
+ from codd.repair.verify_runner import VerificationResult, VerifyRunner
6
+
7
+ __all__ = [
8
+ "LlmRepairEngine",
9
+ "RepairFailed",
10
+ "RepairLoop",
11
+ "RepairLoopConfig",
12
+ "RepairLoopOutcome",
13
+ "VerificationResult",
14
+ "VerifyRunner",
15
+ ]
@@ -0,0 +1,127 @@
1
+ """Approval helpers for repair proposals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ import warnings
8
+ from typing import Any, Callable, Literal, Mapping
9
+
10
+ from codd.repair.schema import RepairProposal
11
+
12
+
13
+ RepairApprovalMode = Literal["required", "auto", "per_attempt"]
14
+ APPROVAL_MODES: set[str] = {"required", "auto", "per_attempt"}
15
+ DEFAULT_MAX_FILES_PER_AUTO_PROPOSAL = 5
16
+
17
+ _APPROVE_VALUES = {"1", "approve", "approved", "true", "y", "yes"}
18
+ _REJECT_VALUES = {"0", "false", "n", "no", "reject", "rejected", "skip", "skipped"}
19
+
20
+
21
+ class RepairApprovalError(RuntimeError):
22
+ """Raised when a repair proposal cannot pass the approval policy."""
23
+
24
+
25
+ def approve_repair_proposal(
26
+ proposal: RepairProposal,
27
+ *,
28
+ approval_mode: RepairApprovalMode,
29
+ codd_yaml: dict,
30
+ notify_callable: Callable[[str], None] | None = None,
31
+ ) -> bool:
32
+ """Return whether a repair proposal may be applied."""
33
+
34
+ mode = _approval_mode(approval_mode)
35
+ repair_config = _repair_config(codd_yaml)
36
+
37
+ if mode == "auto":
38
+ allow_auto = _mapping(repair_config.get("allow_auto"))
39
+ if not bool(allow_auto.get("require_explicit_optin")):
40
+ raise RepairApprovalError(
41
+ "auto repair approval requires repair.allow_auto.require_explicit_optin=true"
42
+ )
43
+
44
+ max_files = _max_files_per_auto_proposal(allow_auto)
45
+ if len(proposal.patches) <= max_files:
46
+ return True
47
+
48
+ warnings.warn(
49
+ "repair proposal exceeds repair.allow_auto.max_files_per_proposal; "
50
+ "escalating to required approval",
51
+ RuntimeWarning,
52
+ stacklevel=2,
53
+ )
54
+ mode = "required"
55
+
56
+ message = _approval_message(proposal, mode)
57
+ if notify_callable is not None:
58
+ notify_callable(message)
59
+ else:
60
+ print(message)
61
+
62
+ decision = _configured_decision(repair_config) or os.environ.get("CODD_REPAIR_APPROVAL")
63
+ if decision is not None:
64
+ return _decision_to_bool(decision)
65
+
66
+ if sys.stdin.isatty():
67
+ try:
68
+ return _decision_to_bool(input("Approve repair proposal? [y/N]: "))
69
+ except EOFError:
70
+ return False
71
+
72
+ return False
73
+
74
+
75
+ def _approval_mode(value: str) -> RepairApprovalMode:
76
+ text = str(value or "required")
77
+ return text if text in APPROVAL_MODES else "required" # type: ignore[return-value]
78
+
79
+
80
+ def _repair_config(codd_yaml: Mapping[str, Any] | None) -> Mapping[str, Any]:
81
+ return _mapping(codd_yaml.get("repair") if isinstance(codd_yaml, Mapping) else None)
82
+
83
+
84
+ def _mapping(value: Any) -> Mapping[str, Any]:
85
+ return value if isinstance(value, Mapping) else {}
86
+
87
+
88
+ def _max_files_per_auto_proposal(allow_auto: Mapping[str, Any]) -> int:
89
+ raw = allow_auto.get("max_files_per_proposal", DEFAULT_MAX_FILES_PER_AUTO_PROPOSAL)
90
+ try:
91
+ return max(0, int(raw))
92
+ except (TypeError, ValueError):
93
+ return DEFAULT_MAX_FILES_PER_AUTO_PROPOSAL
94
+
95
+
96
+ def _configured_decision(repair_config: Mapping[str, Any]) -> Any:
97
+ for key in ("approval_decision", "approval_response"):
98
+ value = repair_config.get(key)
99
+ if value is not None:
100
+ return value
101
+ return None
102
+
103
+
104
+ def _decision_to_bool(value: Any) -> bool:
105
+ text = str(value or "").strip().lower()
106
+ if text in _APPROVE_VALUES:
107
+ return True
108
+ if text in _REJECT_VALUES:
109
+ return False
110
+ raise RepairApprovalError(f"unknown repair approval decision: {value}")
111
+
112
+
113
+ def _approval_message(proposal: RepairProposal, mode: RepairApprovalMode) -> str:
114
+ files = ", ".join(patch.file_path for patch in proposal.patches) or "none"
115
+ return (
116
+ "CoDD repair approval required "
117
+ f"(mode={mode}, patches={len(proposal.patches)}, files={files})"
118
+ )
119
+
120
+
121
+ __all__ = [
122
+ "APPROVAL_MODES",
123
+ "DEFAULT_MAX_FILES_PER_AUTO_PROPOSAL",
124
+ "RepairApprovalError",
125
+ "RepairApprovalMode",
126
+ "approve_repair_proposal",
127
+ ]
@@ -56,8 +56,9 @@ class RepairHistory:
56
56
  def finalize(self, session_dir: Path, outcome: str) -> None:
57
57
  """Write the final repair session outcome."""
58
58
 
59
- if outcome not in {"REPAIR_SUCCESS", "REPAIR_EXHAUSTED", "REPAIR_FAILED"}:
60
- raise ValueError("outcome must be REPAIR_SUCCESS, REPAIR_EXHAUSTED, or REPAIR_FAILED")
59
+ allowed = {"REPAIR_SUCCESS", "REPAIR_EXHAUSTED", "REPAIR_REJECTED_BY_HITL", "REPAIR_FAILED"}
60
+ if outcome not in allowed:
61
+ raise ValueError(f"outcome must be one of {sorted(allowed)}")
61
62
  _write_yaml(
62
63
  Path(session_dir) / "final_status.yaml",
63
64
  {"outcome": outcome, "timestamp": _timestamp()},