codd-dev 1.30.0__tar.gz → 1.32.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 (201) hide show
  1. {codd_dev-1.30.0 → codd_dev-1.32.0}/PKG-INFO +1 -1
  2. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/__init__.py +1 -1
  3. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/cli.py +378 -26
  4. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/coverage_auditor.py +2 -1
  5. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/builder.py +35 -1
  6. codd_dev-1.32.0/codd/dag/checks/environment_coverage.py +238 -0
  7. codd_dev-1.32.0/codd/dag/coverage_axes.py +177 -0
  8. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/extractor.py +4 -0
  9. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/runner.py +1 -0
  10. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/defaults.yaml +6 -0
  11. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployer.py +1 -0
  12. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/verification/cdp_browser.py +156 -4
  13. codd_dev-1.32.0/codd/deployment/providers/verification/cdp_engines.py +165 -0
  14. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/hitl_session.py +2 -1
  15. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/implementer/__init__.py +3 -0
  16. codd_dev-1.32.0/codd/implementer/typecheck_loop.py +241 -0
  17. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/implementer.py +55 -0
  18. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/knowledge_fetcher.py +2 -1
  19. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/best_practice_augmenter.py +2 -0
  20. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/criteria_expander.py +118 -4
  21. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/impl_step_deriver.py +4 -0
  22. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/templates/best_practice_augment_meta.md +12 -0
  23. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/templates/criteria_expand_meta.md +9 -2
  24. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/templates/impl_step_derive_meta.md +8 -1
  25. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/planner.py +2 -1
  26. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair/verify_runner.py +13 -0
  27. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/codd.yaml.tmpl +14 -7
  28. {codd_dev-1.30.0 → codd_dev-1.32.0}/pyproject.toml +1 -1
  29. codd_dev-1.32.0/tests/integration/standalone_repair_skeleton/README.md +3 -0
  30. codd_dev-1.30.0/codd/deployment/providers/verification/cdp_engines.py +0 -40
  31. {codd_dev-1.30.0 → codd_dev-1.32.0}/.gitignore +0 -0
  32. {codd_dev-1.30.0 → codd_dev-1.32.0}/LICENSE +0 -0
  33. {codd_dev-1.30.0 → codd_dev-1.32.0}/README.md +0 -0
  34. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/__main__.py +0 -0
  35. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/_git_helper.py +0 -0
  36. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/ask_user_question_adapter.py +0 -0
  37. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/assembler.py +0 -0
  38. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/bridge.py +0 -0
  39. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/clustering.py +0 -0
  40. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/coherence_adapters.py +0 -0
  41. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/coherence_engine.py +0 -0
  42. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/config.py +0 -0
  43. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/contracts.py +0 -0
  44. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/coverage_metrics.py +0 -0
  45. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/__init__.py +0 -0
  46. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/checks/__init__.py +0 -0
  47. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/checks/depends_on_consistency.py +0 -0
  48. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/checks/deployment_completeness.py +0 -0
  49. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/checks/edge_validity.py +0 -0
  50. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/checks/implementation_coverage.py +0 -0
  51. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/checks/node_completeness.py +0 -0
  52. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/checks/task_completion.py +0 -0
  53. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/checks/transitive_closure.py +0 -0
  54. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/checks/user_journey_coherence.py +0 -0
  55. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/cli.yaml +0 -0
  56. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/cpp_embedded.yaml +0 -0
  57. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/csharp.yaml +0 -0
  58. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/elixir.yaml +0 -0
  59. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/generic.yaml +0 -0
  60. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/iot.yaml +0 -0
  61. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/java.yaml +0 -0
  62. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/kotlin.yaml +0 -0
  63. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/mobile.yaml +0 -0
  64. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/ruby.yaml +0 -0
  65. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/rust.yaml +0 -0
  66. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/scala.yaml +0 -0
  67. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/swift.yaml +0 -0
  68. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/test_frameworks.yaml +0 -0
  69. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/dag/defaults/web.yaml +0 -0
  70. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deploy_targets/__init__.py +0 -0
  71. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deploy_targets/app_service.py +0 -0
  72. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deploy_targets/base.py +0 -0
  73. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deploy_targets/docker_compose.py +0 -0
  74. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/__init__.py +0 -0
  75. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/checks/__init__.py +0 -0
  76. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/defaults/deploy_targets.yaml +0 -0
  77. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/defaults/runtime_capability_inference.yaml +0 -0
  78. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/defaults/schema_providers.yaml +0 -0
  79. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/defaults/verification_templates.yaml +0 -0
  80. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/extractor.py +0 -0
  81. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/__init__.py +0 -0
  82. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/ai_command.py +0 -0
  83. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/llm_consideration.py +0 -0
  84. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/schema/__init__.py +0 -0
  85. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/schema/prisma.py +0 -0
  86. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/target/__init__.py +0 -0
  87. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/target/docker_compose.py +0 -0
  88. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/verification/__init__.py +0 -0
  89. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/verification/assertion_handlers.py +0 -0
  90. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/verification/cdp_launchers.py +0 -0
  91. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/verification/cdp_wire.py +0 -0
  92. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/verification/curl.py +0 -0
  93. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/verification/form_strategies.py +0 -0
  94. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/verification/means_catalog.py +0 -0
  95. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/deployment/providers/verification/playwright.py +0 -0
  96. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/design_md.py +0 -0
  97. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/drift.py +0 -0
  98. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/e2e_extractor.py +0 -0
  99. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/e2e_generator.py +0 -0
  100. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/e2e_runner.py +0 -0
  101. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/env_refs.py +0 -0
  102. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/extract_ai.py +0 -0
  103. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/extractor.py +0 -0
  104. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/fixer.py +0 -0
  105. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/fixup_drift.py +0 -0
  106. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/fixup_drift_strategies/__init__.py +0 -0
  107. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/fixup_drift_strategies/design_token_drift.py +0 -0
  108. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/fixup_drift_strategies/lexicon_violation.py +0 -0
  109. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/fixup_drift_strategies/url_drift.py +0 -0
  110. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/generator.py +0 -0
  111. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/graph.py +0 -0
  112. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/hooks/__init__.py +0 -0
  113. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/hooks/pre-commit +0 -0
  114. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/hooks/recipes/claude_settings_example.json +0 -0
  115. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/hooks/recipes/codex_hook.sh +0 -0
  116. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/hooks/recipes/git_post_commit.sh +0 -0
  117. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/hooks/recipes/git_pre_commit.sh +0 -0
  118. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/implementer/chunked_runner.py +0 -0
  119. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/inheritance.py +0 -0
  120. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/lexicon.py +0 -0
  121. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/__init__.py +0 -0
  122. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/approval.py +0 -0
  123. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/design_doc_extractor.py +0 -0
  124. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/means_catalog_loader.py +0 -0
  125. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/parser.py +0 -0
  126. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/plan_deriver.py +0 -0
  127. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/prompt_builder.py +0 -0
  128. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/strategy_validator.py +0 -0
  129. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/templates/design_doc_extract_meta.md +0 -0
  130. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/templates/implementation_step_catalog.yaml +0 -0
  131. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/templates/meta_instruction.md +0 -0
  132. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/templates/plan_derive_meta.md +0 -0
  133. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/llm/templates/verification_means_catalog.yaml +0 -0
  134. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/mcp_server.py +0 -0
  135. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/measure.py +0 -0
  136. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/parsing.py +0 -0
  137. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/policy.py +0 -0
  138. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/preflight/__init__.py +0 -0
  139. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/preflight/defaults/cli.yaml +0 -0
  140. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/preflight/defaults/iot.yaml +0 -0
  141. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/preflight/defaults/mobile.yaml +0 -0
  142. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/preflight/defaults/web.yaml +0 -0
  143. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/propagate.py +0 -0
  144. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/propagator.py +0 -0
  145. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/registry.py +0 -0
  146. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair/__init__.py +0 -0
  147. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair/approval_repair.py +0 -0
  148. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair/engine.py +0 -0
  149. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair/git_patcher.py +0 -0
  150. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair/history.py +0 -0
  151. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair/llm_repair_engine.py +0 -0
  152. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair/loop.py +0 -0
  153. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair/schema.py +0 -0
  154. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair/templates/analyze_meta.md +0 -0
  155. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair/templates/propose_meta.md +0 -0
  156. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/repair_slice.py +0 -0
  157. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/require.py +0 -0
  158. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/require_plugins.py +0 -0
  159. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/require_propagate.py +0 -0
  160. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/required_artifacts/defaults/cli.yaml +0 -0
  161. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/required_artifacts/defaults/iot.yaml +0 -0
  162. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/required_artifacts/defaults/mobile.yaml +0 -0
  163. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/required_artifacts/defaults/web.yaml +0 -0
  164. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/required_artifacts_deriver.py +0 -0
  165. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/requirement_completeness/defaults/cli.yaml +0 -0
  166. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/requirement_completeness/defaults/iot.yaml +0 -0
  167. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/requirement_completeness/defaults/mobile.yaml +0 -0
  168. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/requirement_completeness/defaults/web.yaml +0 -0
  169. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/requirement_completeness_auditor.py +0 -0
  170. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/restore.py +0 -0
  171. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/routes_extractor.py +0 -0
  172. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/scanner.py +0 -0
  173. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/schema_refs.py +0 -0
  174. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/screen_flow_validator.py +0 -0
  175. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/screen_transition_extractor.py +0 -0
  176. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/screen_transitions/defaults.yaml +0 -0
  177. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/synth.py +0 -0
  178. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/conventions.yaml.tmpl +0 -0
  179. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
  180. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/doc_links.yaml.tmpl +0 -0
  181. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/extract_ai_prompt_baseline.md +0 -0
  182. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
  183. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
  184. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
  185. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
  186. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/extracted/system-context.md.j2 +0 -0
  187. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/gitignore.tmpl +0 -0
  188. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/lexicon_questions.md +0 -0
  189. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/lexicon_schema.yaml +0 -0
  190. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/templates/overrides.yaml.tmpl +0 -0
  191. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/traceability.py +0 -0
  192. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/validator.py +0 -0
  193. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/watch/__init__.py +0 -0
  194. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/watch/events.py +0 -0
  195. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/watch/propagation_log.py +0 -0
  196. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/watch/propagation_pipeline.py +0 -0
  197. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/watch/test_runner.py +0 -0
  198. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/watch/watcher.py +0 -0
  199. {codd_dev-1.30.0 → codd_dev-1.32.0}/codd/wiring.py +0 -0
  200. {codd_dev-1.30.0 → codd_dev-1.32.0}/docs/cookbook/cdp_browser/README.md +0 -0
  201. {codd_dev-1.30.0 → codd_dev-1.32.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.30.0
3
+ Version: 1.32.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,3 +1,3 @@
1
1
  """CoDD — Coherence-Driven Development."""
2
2
 
3
- __version__ = "1.24.0"
3
+ __version__ = "1.30.0"
@@ -2,9 +2,12 @@
2
2
 
3
3
  from dataclasses import asdict, dataclass, is_dataclass
4
4
  from datetime import datetime, timezone
5
+ import importlib.metadata
5
6
  import json
6
7
  import os
8
+ import re
7
9
  import shutil
10
+ import subprocess
8
11
  from pathlib import Path
9
12
  from typing import Any
10
13
 
@@ -29,9 +32,148 @@ class _CliVerificationResult:
29
32
  failure: Any | None = None
30
33
 
31
34
 
35
+ @dataclass(frozen=True)
36
+ class _VersionCheckResult:
37
+ installed_version: str
38
+ required_spec: str
39
+ satisfied: bool
40
+ strict: bool
41
+ message: str
42
+
43
+
44
+ _SPECIFIER_RE = re.compile(r"^\s*(==|!=|<=|>=|<|>|~=)\s*([A-Za-z0-9.!+_-]+)\s*$")
45
+
46
+
47
+ def _installed_codd_version() -> str:
48
+ try:
49
+ return importlib.metadata.version("codd-dev")
50
+ except importlib.metadata.PackageNotFoundError:
51
+ pass
52
+
53
+ try:
54
+ from codd import __version__
55
+
56
+ if __version__:
57
+ return str(__version__)
58
+ except Exception: # pragma: no cover - best-effort fallback for source trees.
59
+ pass
60
+
61
+ pyproject_path = Path(__file__).resolve().parent.parent / "pyproject.toml"
62
+ try:
63
+ match = re.search(r'(?m)^version\s*=\s*"([^"]+)"', pyproject_path.read_text(encoding="utf-8"))
64
+ except OSError:
65
+ match = None
66
+ return match.group(1) if match else "unknown"
67
+
68
+
69
+ def _evaluate_version_requirement(project_root: Path, *, strict_override: bool = False) -> _VersionCheckResult | None:
70
+ try:
71
+ config = load_project_config(project_root)
72
+ except (FileNotFoundError, ValueError):
73
+ return None
74
+
75
+ required = config.get("codd_required_version")
76
+ if not isinstance(required, str) or not required.strip():
77
+ return None
78
+
79
+ installed = _installed_codd_version()
80
+ required_spec = required.strip()
81
+ satisfied, error = _version_satisfies(installed, required_spec)
82
+ strict = bool(strict_override or config.get("codd_required_version_strict", False))
83
+ if error:
84
+ return _VersionCheckResult(
85
+ installed_version=installed,
86
+ required_spec=required_spec,
87
+ satisfied=False,
88
+ strict=strict,
89
+ message=f"WARN: invalid codd_required_version {required_spec!r}: {error}",
90
+ )
91
+ return _VersionCheckResult(
92
+ installed_version=installed,
93
+ required_spec=required_spec,
94
+ satisfied=satisfied,
95
+ strict=strict,
96
+ message=f"WARN: project requires codd {required_spec}, installed {installed}",
97
+ )
98
+
99
+
100
+ def _warn_if_project_version_mismatch(project_root: Path) -> None:
101
+ result = _evaluate_version_requirement(project_root)
102
+ if result is None or result.satisfied:
103
+ return
104
+ click.echo(result.message, err=True)
105
+ if result.strict:
106
+ raise SystemExit(1)
107
+
108
+
109
+ def _version_satisfies(installed: str, specifier: str) -> tuple[bool, str | None]:
110
+ try:
111
+ from packaging.specifiers import InvalidSpecifier, SpecifierSet
112
+ from packaging.version import InvalidVersion, Version
113
+
114
+ try:
115
+ return Version(installed) in SpecifierSet(specifier), None
116
+ except (InvalidSpecifier, InvalidVersion) as exc:
117
+ return False, str(exc)
118
+ except ImportError:
119
+ return _version_satisfies_fallback(installed, specifier)
120
+
121
+
122
+ def _version_satisfies_fallback(installed: str, specifier: str) -> tuple[bool, str | None]:
123
+ installed_key = _version_key(installed)
124
+ if installed_key is None:
125
+ return False, f"unsupported installed version {installed!r}"
126
+
127
+ for raw_part in specifier.split(","):
128
+ part = raw_part.strip()
129
+ if not part:
130
+ continue
131
+ match = _SPECIFIER_RE.match(part)
132
+ if match is None:
133
+ return False, f"unsupported version specifier {part!r}; install packaging for full PEP 440 support"
134
+ op, expected = match.groups()
135
+ expected_key = _version_key(expected)
136
+ if expected_key is None:
137
+ return False, f"unsupported version {expected!r}"
138
+ if not _compare_version_keys(installed_key, op, expected_key):
139
+ return False, None
140
+ return True, None
141
+
142
+
143
+ def _version_key(version: str) -> tuple[int, ...] | None:
144
+ release = version.split("+", 1)[0].split("-", 1)[0]
145
+ numbers = re.findall(r"\d+", release)
146
+ if not numbers:
147
+ return None
148
+ return tuple(int(item) for item in numbers)
149
+
150
+
151
+ def _compare_version_keys(installed: tuple[int, ...], op: str, expected: tuple[int, ...]) -> bool:
152
+ size = max(len(installed), len(expected))
153
+ left = installed + (0,) * (size - len(installed))
154
+ right = expected + (0,) * (size - len(expected))
155
+ if op == "==":
156
+ return left == right
157
+ if op == "!=":
158
+ return left != right
159
+ if op == ">=":
160
+ return left >= right
161
+ if op == "<=":
162
+ return left <= right
163
+ if op == ">":
164
+ return left > right
165
+ if op == "<":
166
+ return left < right
167
+ if op == "~=":
168
+ upper = (expected[0] + 1, 0) if len(expected) <= 2 else (expected[0], expected[1] + 1, 0)
169
+ upper = upper + (0,) * (size - len(upper))
170
+ return left >= right and left < upper
171
+ return False
172
+
173
+
32
174
  def _run_pro_command(name: str, **kwargs):
33
175
  """Dispatch a Pro-only command when the bridge plugin is installed."""
34
- handler = get_command_handler(name)
176
+ handler = get_command_handler(name)
35
177
  if handler is None:
36
178
  click.echo(PRO_COMMAND_INSTALL_MESSAGE)
37
179
  raise SystemExit(1)
@@ -196,11 +338,40 @@ def _ensure_bootstrap_codd_yaml(
196
338
  return config_path, True
197
339
 
198
340
 
199
- @click.group()
200
- @click.version_option(package_name="codd-dev")
201
- def main():
341
+ @click.group()
342
+ @click.version_option(package_name="codd-dev")
343
+ @click.pass_context
344
+ def main(ctx: click.Context):
202
345
  """CoDD: Coherence-Driven Development."""
203
- pass
346
+ if ctx.resilient_parsing or ctx.invoked_subcommand in {None, "version"}:
347
+ return
348
+ _warn_if_project_version_mismatch(Path.cwd())
349
+
350
+
351
+ @main.command("version")
352
+ @click.option("--check", "check_project", is_flag=True, help="Check installed CoDD against codd.yaml requirement")
353
+ @click.option("--strict", is_flag=True, help="Exit non-zero when the version requirement is not satisfied")
354
+ @click.option("--path", "project_path", default=".", help="Project root directory")
355
+ def version_cmd(check_project: bool, strict: bool, project_path: str) -> None:
356
+ """Print the installed CoDD version."""
357
+ installed = _installed_codd_version()
358
+ click.echo(f"codd {installed}")
359
+ if not check_project:
360
+ return
361
+
362
+ project_root = Path(project_path).resolve()
363
+ result = _evaluate_version_requirement(project_root, strict_override=strict)
364
+ if result is None:
365
+ click.echo("Version check: no codd_required_version configured")
366
+ return
367
+ if result.satisfied:
368
+ click.echo(f"Version check: PASS (requires {result.required_spec})")
369
+ return
370
+
371
+ click.echo(result.message, err=True)
372
+ click.echo(f"Version check: FAIL (requires {result.required_spec})")
373
+ if result.strict:
374
+ raise SystemExit(1)
204
375
 
205
376
 
206
377
  @main.command("preflight")
@@ -539,6 +710,13 @@ def restore(wave: int, path: str, force: bool, ai_cmd: str | None, feedback: str
539
710
  default=False,
540
711
  help="Run CoverageAuditor 3-class requirement gap analysis.",
541
712
  )
713
+ @click.option(
714
+ "--check",
715
+ "check_coverage",
716
+ is_flag=True,
717
+ default=False,
718
+ help="Check requirement-to-implementation coverage without generating requirements.",
719
+ )
542
720
  @click.option(
543
721
  "--completeness-audit",
544
722
  is_flag=True,
@@ -556,6 +734,7 @@ def require(
556
734
  base_ref: str | None,
557
735
  apply_mode: bool,
558
736
  audit: bool,
737
+ check_coverage: bool,
559
738
  completeness_audit: bool,
560
739
  ):
561
740
  """Infer requirements from extracted codebase facts (brownfield).
@@ -582,6 +761,10 @@ def require(
582
761
 
583
762
  _require_codd_dir(project_root)
584
763
 
764
+ if check_coverage:
765
+ _run_require_check(project_root)
766
+ return
767
+
585
768
  if audit:
586
769
  from codd.coverage_auditor import CoverageAuditor
587
770
 
@@ -680,12 +863,39 @@ def require(
680
863
  generated += 1
681
864
  else:
682
865
  skipped += 1
683
-
684
- click.echo(f"Requirements: {generated} generated, {skipped} skipped")
685
-
686
-
687
- @main.command()
688
- @click.option("--diff", default="HEAD", help="Git diff target (default: HEAD, shows uncommitted changes)")
866
+
867
+ click.echo(f"Requirements: {generated} generated, {skipped} skipped")
868
+
869
+
870
+ def _run_require_check(project_root: Path) -> None:
871
+ from codd.dag import runner as dag_runner
872
+
873
+ try:
874
+ results = dag_runner.run_all_checks(project_root, check_names=["implementation_coverage"])
875
+ except (FileNotFoundError, ValueError) as exc:
876
+ click.echo(f"Error: {exc}")
877
+ raise SystemExit(1)
878
+
879
+ failed = []
880
+ for result in results:
881
+ check_name = str(getattr(result, "check_name", "implementation_coverage"))
882
+ message = str(getattr(result, "message", "") or "")
883
+ passed = bool(getattr(result, "passed", False))
884
+ status = "PASS" if passed else "FAIL"
885
+ suffix = f" - {message}" if message else ""
886
+ click.echo(f"{status}: {check_name}{suffix}")
887
+ for violation in getattr(result, "violations", []) or []:
888
+ click.echo(f" - {violation}")
889
+ if not passed:
890
+ failed.append(result)
891
+
892
+ if failed:
893
+ raise SystemExit(1)
894
+ click.echo("Requirement check complete: implementation_coverage PASS")
895
+
896
+
897
+ @main.command()
898
+ @click.option("--diff", default="HEAD", help="Git diff target (default: HEAD, shows uncommitted changes)")
689
899
  @click.option("--path", default=".", help="Project root directory")
690
900
  @click.option("--update", is_flag=True, help="Actually update affected design docs via AI")
691
901
  @click.option("--verify", is_flag=True, help="Auto-apply green band, list amber/gray for HITL review")
@@ -1238,6 +1448,7 @@ def implement_augment_cmd(
1238
1448
  show_default=True,
1239
1449
  help="Seconds before one chunk is interrupted",
1240
1450
  )
1451
+ @click.option("--enable-typecheck-loop", is_flag=True, default=False, help="Run configured typecheck repair loop after implementation")
1241
1452
  def implement_run_cmd(
1242
1453
  task_id: str | None,
1243
1454
  project_path: str,
@@ -1245,12 +1456,21 @@ def implement_run_cmd(
1245
1456
  use_derived_steps: str,
1246
1457
  chunk_size: int | None,
1247
1458
  timeout_per_chunk: int,
1459
+ enable_typecheck_loop: bool,
1248
1460
  ):
1249
1461
  """Run implementation with optional derived step injection."""
1250
- from codd.implementer import implement_tasks
1462
+ from codd.implementer import auto_detect_task, implement_tasks
1251
1463
 
1252
1464
  project_root = Path(project_path).resolve()
1253
1465
  _require_codd_dir(project_root)
1466
+ if task_id is None:
1467
+ try:
1468
+ task_id = auto_detect_task(project_root)
1469
+ except ValueError as exc:
1470
+ click.echo(f"Error: {exc}")
1471
+ raise SystemExit(1)
1472
+ click.echo(f"Auto-detected task: {task_id}")
1473
+
1254
1474
  if chunk_size is not None:
1255
1475
  try:
1256
1476
  result = _run_chunked_implementation(
@@ -1267,6 +1487,16 @@ def implement_run_cmd(
1267
1487
  _echo_chunked_result(project_root, result)
1268
1488
  if result.status != "SUCCESS":
1269
1489
  raise SystemExit(1)
1490
+ try:
1491
+ _run_typecheck_loop_after_implement(
1492
+ project_root=project_root,
1493
+ modified_files=None,
1494
+ ai_cmd=ai_cmd,
1495
+ force_enabled=enable_typecheck_loop,
1496
+ )
1497
+ except (FileNotFoundError, ValueError, CoddCLIError) as exc:
1498
+ click.echo(f"Error: {exc}")
1499
+ raise SystemExit(1)
1270
1500
  return
1271
1501
 
1272
1502
  try:
@@ -1286,6 +1516,16 @@ def implement_run_cmd(
1286
1516
  click.echo(f"{sum(len(result.generated_files) for result in results)} files generated across {len(results) - len(failed)} task(s)")
1287
1517
  if failed:
1288
1518
  raise SystemExit(1)
1519
+ try:
1520
+ _run_typecheck_loop_after_implement(
1521
+ project_root=project_root,
1522
+ modified_files=[generated_file for result in results for generated_file in result.generated_files],
1523
+ ai_cmd=ai_cmd,
1524
+ force_enabled=enable_typecheck_loop,
1525
+ )
1526
+ except (FileNotFoundError, ValueError, CoddCLIError) as exc:
1527
+ click.echo(f"Error: {exc}")
1528
+ raise SystemExit(1)
1289
1529
 
1290
1530
 
1291
1531
  @implement.command("resume")
@@ -1391,9 +1631,71 @@ def _echo_chunked_result(project_root: Path, result) -> None:
1391
1631
  f"Chunked implementation {result.status}: "
1392
1632
  f"{len(result.completed_chunks)}/{result.total_chunks} chunks; history={history}"
1393
1633
  )
1394
-
1395
-
1396
- @main.command()
1634
+
1635
+
1636
+ def _run_typecheck_loop_after_implement(
1637
+ *,
1638
+ project_root: Path,
1639
+ modified_files: list[Path] | None,
1640
+ ai_cmd: str | None,
1641
+ force_enabled: bool,
1642
+ ):
1643
+ config = _load_optional_project_config(project_root)
1644
+ if not force_enabled and not _typecheck_config_enabled(config):
1645
+ return None
1646
+ from codd.implementer import TypecheckRepairLoop
1647
+
1648
+ loop = TypecheckRepairLoop.from_config(config, force_enabled=force_enabled)
1649
+ if not loop.enabled:
1650
+ return None
1651
+
1652
+ result = loop.run_after_implement(
1653
+ project_root,
1654
+ modified_files if modified_files is not None else _git_modified_files(project_root),
1655
+ ai_cmd or _configured_ai_command(config),
1656
+ )
1657
+ click.echo(f"Typecheck loop {result.status}")
1658
+ if result.status == "REPAIR_EXHAUSTED":
1659
+ raise CoddCLIError("typecheck repair loop exhausted")
1660
+ return result
1661
+
1662
+
1663
+ def _configured_ai_command(config: dict[str, Any]) -> str:
1664
+ command = config.get("ai_command")
1665
+ return command if isinstance(command, str) else ""
1666
+
1667
+
1668
+ def _typecheck_config_enabled(config: dict[str, Any]) -> bool:
1669
+ typecheck = config.get("typecheck")
1670
+ return bool(typecheck.get("enabled")) if isinstance(typecheck, dict) else False
1671
+
1672
+
1673
+ def _git_modified_files(project_root: Path) -> list[Path]:
1674
+ try:
1675
+ completed = subprocess.run(
1676
+ ["git", "-C", str(project_root), "status", "--porcelain=v1", "--untracked-files=all"],
1677
+ capture_output=True,
1678
+ text=True,
1679
+ encoding="utf-8",
1680
+ check=False,
1681
+ )
1682
+ except (OSError, ValueError):
1683
+ return []
1684
+ if completed.returncode != 0:
1685
+ return []
1686
+ paths: list[Path] = []
1687
+ for line in completed.stdout.splitlines():
1688
+ if len(line) < 4:
1689
+ continue
1690
+ raw_path = line[3:]
1691
+ if " -> " in raw_path:
1692
+ raw_path = raw_path.rsplit(" -> ", 1)[1]
1693
+ if raw_path:
1694
+ paths.append(project_root / raw_path)
1695
+ return paths
1696
+
1697
+
1698
+ @main.command()
1397
1699
  @click.option("--path", default=".", help="Project root directory")
1398
1700
  @click.option("--output-dir", default=None, help="Output directory for assembled project (default: src/)")
1399
1701
  @click.option(
@@ -1457,7 +1759,7 @@ def verify(
1457
1759
  return
1458
1760
 
1459
1761
  project_root = Path(path).resolve()
1460
- result = _run_verify_once(path=path, sprint=sprint)
1762
+ result = _run_verify_once(path=path, sprint=sprint, prefer_standalone=True)
1461
1763
  if result.passed:
1462
1764
  return
1463
1765
 
@@ -1471,7 +1773,7 @@ def verify(
1471
1773
  repair_config=repair_config,
1472
1774
  max_attempts=max_attempts,
1473
1775
  engine_name=engine_name,
1474
- verify_callable=lambda: _run_verify_once(path=path, sprint=sprint),
1776
+ verify_callable=lambda: _run_verify_once(path=path, sprint=sprint, prefer_standalone=True),
1475
1777
  )
1476
1778
  click.echo(f"Repair outcome: {outcome.status}")
1477
1779
  click.echo(f"Repair history: {_display_path(outcome.history_session_dir, project_root)}")
@@ -3236,12 +3538,14 @@ def _load_optional_project_config(project_root: Path) -> dict[str, Any]:
3236
3538
  return {}
3237
3539
 
3238
3540
 
3239
- def _run_verify_once(path: str, sprint: int | None = None) -> _CliVerificationResult:
3240
- if get_command_handler("verify") is None:
3241
- from codd.repair.verify_runner import run_standalone_verify
3242
-
3243
- result = run_standalone_verify(Path(path).resolve())
3244
- return _cli_result_from_standalone_verify(result)
3541
+ def _run_verify_once(
3542
+ path: str,
3543
+ sprint: int | None = None,
3544
+ *,
3545
+ prefer_standalone: bool = False,
3546
+ ) -> _CliVerificationResult:
3547
+ if prefer_standalone or get_command_handler("verify") is None:
3548
+ return _run_standalone_verify_once(path)
3245
3549
 
3246
3550
  try:
3247
3551
  _run_pro_command("verify", path=path, sprint=sprint)
@@ -3260,6 +3564,23 @@ def _run_verify_once(path: str, sprint: int | None = None) -> _CliVerificationRe
3260
3564
  return _CliVerificationResult(passed=True, exit_code=0, failure=None)
3261
3565
 
3262
3566
 
3567
+ def _run_standalone_verify_once(path: str) -> _CliVerificationResult:
3568
+ from codd.repair.verify_runner import run_standalone_verify
3569
+
3570
+ result = run_standalone_verify(Path(path).resolve())
3571
+ _echo_verification_warnings(result)
3572
+ return _cli_result_from_standalone_verify(result)
3573
+
3574
+
3575
+ def _echo_verification_warnings(result: Any) -> None:
3576
+ for warning in getattr(result, "warnings", []) or []:
3577
+ text = str(warning)
3578
+ if not text:
3579
+ continue
3580
+ prefix = "" if text.upper().startswith("WARNING:") else "WARNING: "
3581
+ click.echo(f"{prefix}{text}")
3582
+
3583
+
3263
3584
  def _cli_result_from_standalone_verify(result: Any) -> _CliVerificationResult:
3264
3585
  return _CliVerificationResult(
3265
3586
  passed=bool(result.passed),
@@ -3769,19 +4090,32 @@ def dag_journeys(project_path: str, output_format: str):
3769
4090
  @dag.command("run-journey")
3770
4091
  @click.argument("journey_name")
3771
4092
  @click.option("--project-path", "--path", default=".", show_default=True, help="Project root directory")
4093
+ @click.option(
4094
+ "--axis",
4095
+ "axis_overrides",
4096
+ multiple=True,
4097
+ metavar="TYPE=VARIANT",
4098
+ help="Runtime axis override. Repeat for multiple axes.",
4099
+ )
3772
4100
  @click.option(
3773
4101
  "--config-section",
3774
4102
  default="cdp_browser",
3775
4103
  show_default=True,
3776
4104
  help="verification.templates section used for browser config",
3777
4105
  )
3778
- def dag_run_journey(journey_name: str, project_path: str, config_section: str):
4106
+ def dag_run_journey(
4107
+ journey_name: str,
4108
+ project_path: str,
4109
+ axis_overrides: tuple[str, ...],
4110
+ config_section: str,
4111
+ ):
3779
4112
  """Run one declared user_journey with the CDP browser template."""
3780
4113
  from codd.dag.builder import build_dag
3781
4114
  from codd.deployment.providers.verification.cdp_browser import CdpBrowser
3782
4115
 
3783
4116
  project_root = Path(project_path).resolve()
3784
4117
  try:
4118
+ parsed_axes = _parse_axis_overrides(axis_overrides)
3785
4119
  config = load_project_config(project_root)
3786
4120
  template_config = _journey_template_config(config, config_section)
3787
4121
  built_dag = build_dag(project_root)
@@ -3795,7 +4129,7 @@ def dag_run_journey(journey_name: str, project_path: str, config_section: str):
3795
4129
  raise SystemExit(2)
3796
4130
 
3797
4131
  command = json.dumps(
3798
- _journey_execution_plan(project_root, journey_record, template_config),
4132
+ _journey_execution_plan(project_root, journey_record, template_config, parsed_axes),
3799
4133
  sort_keys=True,
3800
4134
  )
3801
4135
  result = CdpBrowser(config=template_config).execute(command)
@@ -3816,6 +4150,20 @@ def _journey_template_config(config: dict[str, Any], config_section: str) -> dic
3816
4150
  return dict(section)
3817
4151
 
3818
4152
 
4153
+ def _parse_axis_overrides(axis_overrides: tuple[str, ...]) -> dict[str, str]:
4154
+ parsed: dict[str, str] = {}
4155
+ for raw in axis_overrides:
4156
+ if "=" not in raw:
4157
+ raise ValueError("--axis must use TYPE=VARIANT")
4158
+ axis_type, variant_id = raw.split("=", 1)
4159
+ axis = axis_type.strip()
4160
+ variant = variant_id.strip()
4161
+ if not axis or not variant:
4162
+ raise ValueError("--axis must use non-empty TYPE=VARIANT")
4163
+ parsed[axis] = variant
4164
+ return parsed
4165
+
4166
+
3819
4167
  def _find_dag_journey(
3820
4168
  dag: Any,
3821
4169
  journey_name: str,
@@ -3838,11 +4186,12 @@ def _journey_execution_plan(
3838
4186
  project_root: Path,
3839
4187
  journey_record: dict[str, Any],
3840
4188
  template_config: dict[str, Any],
4189
+ axis_overrides: dict[str, str] | None = None,
3841
4190
  ) -> dict[str, Any]:
3842
4191
  journey = dict(journey_record["journey"])
3843
4192
  journey_name = str(journey.get("name") or "")
3844
4193
  steps = journey.get("steps")
3845
- return {
4194
+ plan = {
3846
4195
  "template": "cdp_browser",
3847
4196
  "test_kind": "e2e",
3848
4197
  "target": _journey_target(journey),
@@ -3853,6 +4202,9 @@ def _journey_execution_plan(
3853
4202
  "design_doc": journey_record["design_doc"],
3854
4203
  "config": template_config,
3855
4204
  }
4205
+ if axis_overrides:
4206
+ plan["axis_overrides"] = dict(axis_overrides)
4207
+ return plan
3856
4208
 
3857
4209
 
3858
4210
  def _journey_target(journey: dict[str, Any]) -> str:
@@ -3,7 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass, field
6
- from datetime import UTC, datetime
6
+ from datetime import datetime, timezone
7
+ UTC = timezone.utc
7
8
  from pathlib import Path
8
9
  import re
9
10
  from typing import Any, Literal
@@ -16,6 +16,7 @@ import yaml
16
16
 
17
17
  from codd.config import load_project_config
18
18
  from codd.dag import DAG, Edge, Node
19
+ from codd.dag.coverage_axes import CoverageAxis, extract_coverage_axes_from_design_doc, extract_coverage_axes_from_lexicon
19
20
  from codd.dag.extractor import extract_design_doc_metadata, extract_imports, scan_capability_evidence
20
21
  from codd.llm.design_doc_extractor import (
21
22
  ExpectedExtraction,
@@ -88,6 +89,7 @@ def build_dag(project_root: Path, settings: dict[str, Any] | None = None) -> DAG
88
89
  _add_design_doc_expected_outcome_edges(dag, design_docs)
89
90
  _add_plan_tasks(dag, root, dag_settings)
90
91
  _add_deployment_graph(dag, root, design_docs, impl_nodes)
92
+ _attach_coverage_axes(dag, root, dag_settings)
91
93
 
92
94
  write_dag_json(dag, root, default_dag_json_path(root))
93
95
  return dag
@@ -151,7 +153,7 @@ def default_dag_mermaid_path(project_root: Path) -> Path:
151
153
  def dag_to_dict(dag: DAG, project_root: Path) -> dict[str, Any]:
152
154
  """Serialize a DAG using the stable `.codd/dag.json` schema."""
153
155
 
154
- return {
156
+ payload = {
155
157
  "version": "1",
156
158
  "built_at": datetime.now(timezone.utc).isoformat(),
157
159
  "project_root": str(Path(project_root).resolve()),
@@ -167,6 +169,12 @@ def dag_to_dict(dag: DAG, project_root: Path) -> dict[str, Any]:
167
169
  "edges": [_edge_to_dict(edge) for edge in sorted(dag.edges, key=lambda item: (item.from_id, item.to_id, item.kind))],
168
170
  "cycles": dag.detect_cycles(),
169
171
  }
172
+ coverage_axes = getattr(dag, "coverage_axes", [])
173
+ if coverage_axes:
174
+ payload["coverage_axes"] = [
175
+ axis.to_dict() if isinstance(axis, CoverageAxis) else axis for axis in coverage_axes
176
+ ]
177
+ return payload
170
178
 
171
179
 
172
180
  def _edge_to_dict(edge: Edge) -> dict[str, Any]:
@@ -656,6 +664,32 @@ def _add_deployment_graph(
656
664
  existing_edges.add(edge_key)
657
665
 
658
666
 
667
+ def _attach_coverage_axes(dag: DAG, project_root: Path, settings: dict[str, Any]) -> None:
668
+ lexicon_path = _project_path(project_root, str(settings.get("lexicon_file", "project_lexicon.yaml")))
669
+ axes = extract_coverage_axes_from_lexicon(lexicon_path)
670
+ for node in sorted(dag.nodes.values(), key=lambda item: item.id):
671
+ if node.kind == "design_doc":
672
+ axes.extend(extract_coverage_axes_from_design_doc(node))
673
+ dag.coverage_axes = _dedupe_coverage_axes(axes)
674
+
675
+
676
+ def _dedupe_coverage_axes(axes: list[CoverageAxis]) -> list[CoverageAxis]:
677
+ deduped: list[CoverageAxis] = []
678
+ seen: set[tuple[str, tuple[str, ...], str, str]] = set()
679
+ for axis in axes:
680
+ key = (
681
+ axis.axis_type,
682
+ tuple(variant.id for variant in axis.variants),
683
+ axis.source,
684
+ axis.owner_section,
685
+ )
686
+ if key in seen:
687
+ continue
688
+ seen.add(key)
689
+ deduped.append(axis)
690
+ return deduped
691
+
692
+
659
693
  def _extract_outputs(section: str) -> list[str]:
660
694
  match = OUTPUTS_RE.search(section)
661
695
  if not match: