codd-dev 1.28.0__tar.gz → 1.30.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 (196) hide show
  1. {codd_dev-1.28.0 → codd_dev-1.30.0}/PKG-INFO +1 -1
  2. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/cli.py +460 -14
  3. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/config.py +5 -2
  4. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/builder.py +28 -1
  5. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/checks/implementation_coverage.py +180 -10
  6. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/extractor.py +57 -18
  7. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/defaults.yaml +6 -0
  8. codd_dev-1.30.0/codd/implementer/__init__.py +31 -0
  9. codd_dev-1.30.0/codd/implementer/chunked_runner.py +498 -0
  10. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/implementer.py +190 -0
  11. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/approval.py +58 -0
  12. codd_dev-1.30.0/codd/llm/best_practice_augmenter.py +158 -0
  13. codd_dev-1.30.0/codd/llm/impl_step_deriver.py +611 -0
  14. codd_dev-1.30.0/codd/llm/templates/best_practice_augment_meta.md +52 -0
  15. codd_dev-1.30.0/codd/llm/templates/impl_step_derive_meta.md +50 -0
  16. codd_dev-1.30.0/codd/llm/templates/implementation_step_catalog.yaml +35 -0
  17. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair/verify_runner.py +13 -0
  18. {codd_dev-1.28.0 → codd_dev-1.30.0}/pyproject.toml +1 -1
  19. {codd_dev-1.28.0 → codd_dev-1.30.0}/.gitignore +0 -0
  20. {codd_dev-1.28.0 → codd_dev-1.30.0}/LICENSE +0 -0
  21. {codd_dev-1.28.0 → codd_dev-1.30.0}/README.md +0 -0
  22. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/__init__.py +0 -0
  23. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/__main__.py +0 -0
  24. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/_git_helper.py +0 -0
  25. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/ask_user_question_adapter.py +0 -0
  26. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/assembler.py +0 -0
  27. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/bridge.py +0 -0
  28. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/clustering.py +0 -0
  29. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/coherence_adapters.py +0 -0
  30. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/coherence_engine.py +0 -0
  31. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/contracts.py +0 -0
  32. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/coverage_auditor.py +0 -0
  33. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/coverage_metrics.py +0 -0
  34. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/__init__.py +0 -0
  35. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/checks/__init__.py +0 -0
  36. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/checks/depends_on_consistency.py +0 -0
  37. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/checks/deployment_completeness.py +0 -0
  38. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/checks/edge_validity.py +0 -0
  39. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/checks/node_completeness.py +0 -0
  40. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/checks/task_completion.py +0 -0
  41. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/checks/transitive_closure.py +0 -0
  42. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/checks/user_journey_coherence.py +0 -0
  43. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/cli.yaml +0 -0
  44. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/cpp_embedded.yaml +0 -0
  45. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/csharp.yaml +0 -0
  46. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/elixir.yaml +0 -0
  47. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/generic.yaml +0 -0
  48. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/iot.yaml +0 -0
  49. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/java.yaml +0 -0
  50. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/kotlin.yaml +0 -0
  51. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/mobile.yaml +0 -0
  52. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/ruby.yaml +0 -0
  53. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/rust.yaml +0 -0
  54. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/scala.yaml +0 -0
  55. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/swift.yaml +0 -0
  56. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/test_frameworks.yaml +0 -0
  57. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/defaults/web.yaml +0 -0
  58. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/dag/runner.py +0 -0
  59. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deploy_targets/__init__.py +0 -0
  60. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deploy_targets/app_service.py +0 -0
  61. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deploy_targets/base.py +0 -0
  62. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deploy_targets/docker_compose.py +0 -0
  63. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployer.py +0 -0
  64. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/__init__.py +0 -0
  65. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/checks/__init__.py +0 -0
  66. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/defaults/deploy_targets.yaml +0 -0
  67. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/defaults/runtime_capability_inference.yaml +0 -0
  68. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/defaults/schema_providers.yaml +0 -0
  69. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/defaults/verification_templates.yaml +0 -0
  70. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/extractor.py +0 -0
  71. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/__init__.py +0 -0
  72. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/ai_command.py +0 -0
  73. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/llm_consideration.py +0 -0
  74. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/schema/__init__.py +0 -0
  75. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/schema/prisma.py +0 -0
  76. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/target/__init__.py +0 -0
  77. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/target/docker_compose.py +0 -0
  78. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/verification/__init__.py +0 -0
  79. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/verification/assertion_handlers.py +0 -0
  80. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/verification/cdp_browser.py +0 -0
  81. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/verification/cdp_engines.py +0 -0
  82. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/verification/cdp_launchers.py +0 -0
  83. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/verification/cdp_wire.py +0 -0
  84. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/verification/curl.py +0 -0
  85. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/verification/form_strategies.py +0 -0
  86. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/verification/means_catalog.py +0 -0
  87. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/deployment/providers/verification/playwright.py +0 -0
  88. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/design_md.py +0 -0
  89. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/drift.py +0 -0
  90. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/e2e_extractor.py +0 -0
  91. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/e2e_generator.py +0 -0
  92. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/e2e_runner.py +0 -0
  93. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/env_refs.py +0 -0
  94. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/extract_ai.py +0 -0
  95. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/extractor.py +0 -0
  96. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/fixer.py +0 -0
  97. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/fixup_drift.py +0 -0
  98. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/fixup_drift_strategies/__init__.py +0 -0
  99. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/fixup_drift_strategies/design_token_drift.py +0 -0
  100. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/fixup_drift_strategies/lexicon_violation.py +0 -0
  101. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/fixup_drift_strategies/url_drift.py +0 -0
  102. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/generator.py +0 -0
  103. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/graph.py +0 -0
  104. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/hitl_session.py +0 -0
  105. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/hooks/__init__.py +0 -0
  106. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/hooks/pre-commit +0 -0
  107. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/hooks/recipes/claude_settings_example.json +0 -0
  108. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/hooks/recipes/codex_hook.sh +0 -0
  109. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/hooks/recipes/git_post_commit.sh +0 -0
  110. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/hooks/recipes/git_pre_commit.sh +0 -0
  111. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/inheritance.py +0 -0
  112. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/knowledge_fetcher.py +0 -0
  113. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/lexicon.py +0 -0
  114. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/__init__.py +0 -0
  115. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/criteria_expander.py +0 -0
  116. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/design_doc_extractor.py +0 -0
  117. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/means_catalog_loader.py +0 -0
  118. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/parser.py +0 -0
  119. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/plan_deriver.py +0 -0
  120. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/prompt_builder.py +0 -0
  121. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/strategy_validator.py +0 -0
  122. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/templates/criteria_expand_meta.md +0 -0
  123. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/templates/design_doc_extract_meta.md +0 -0
  124. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/templates/meta_instruction.md +0 -0
  125. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/templates/plan_derive_meta.md +0 -0
  126. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/llm/templates/verification_means_catalog.yaml +0 -0
  127. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/mcp_server.py +0 -0
  128. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/measure.py +0 -0
  129. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/parsing.py +0 -0
  130. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/planner.py +0 -0
  131. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/policy.py +0 -0
  132. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/preflight/__init__.py +0 -0
  133. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/preflight/defaults/cli.yaml +0 -0
  134. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/preflight/defaults/iot.yaml +0 -0
  135. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/preflight/defaults/mobile.yaml +0 -0
  136. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/preflight/defaults/web.yaml +0 -0
  137. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/propagate.py +0 -0
  138. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/propagator.py +0 -0
  139. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/registry.py +0 -0
  140. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair/__init__.py +0 -0
  141. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair/approval_repair.py +0 -0
  142. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair/engine.py +0 -0
  143. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair/git_patcher.py +0 -0
  144. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair/history.py +0 -0
  145. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair/llm_repair_engine.py +0 -0
  146. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair/loop.py +0 -0
  147. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair/schema.py +0 -0
  148. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair/templates/analyze_meta.md +0 -0
  149. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair/templates/propose_meta.md +0 -0
  150. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/repair_slice.py +0 -0
  151. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/require.py +0 -0
  152. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/require_plugins.py +0 -0
  153. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/require_propagate.py +0 -0
  154. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/required_artifacts/defaults/cli.yaml +0 -0
  155. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/required_artifacts/defaults/iot.yaml +0 -0
  156. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/required_artifacts/defaults/mobile.yaml +0 -0
  157. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/required_artifacts/defaults/web.yaml +0 -0
  158. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/required_artifacts_deriver.py +0 -0
  159. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/requirement_completeness/defaults/cli.yaml +0 -0
  160. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/requirement_completeness/defaults/iot.yaml +0 -0
  161. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/requirement_completeness/defaults/mobile.yaml +0 -0
  162. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/requirement_completeness/defaults/web.yaml +0 -0
  163. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/requirement_completeness_auditor.py +0 -0
  164. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/restore.py +0 -0
  165. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/routes_extractor.py +0 -0
  166. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/scanner.py +0 -0
  167. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/schema_refs.py +0 -0
  168. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/screen_flow_validator.py +0 -0
  169. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/screen_transition_extractor.py +0 -0
  170. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/screen_transitions/defaults.yaml +0 -0
  171. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/synth.py +0 -0
  172. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/codd.yaml.tmpl +0 -0
  173. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/conventions.yaml.tmpl +0 -0
  174. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
  175. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/doc_links.yaml.tmpl +0 -0
  176. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/extract_ai_prompt_baseline.md +0 -0
  177. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
  178. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
  179. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
  180. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
  181. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/extracted/system-context.md.j2 +0 -0
  182. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/gitignore.tmpl +0 -0
  183. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/lexicon_questions.md +0 -0
  184. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/lexicon_schema.yaml +0 -0
  185. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/templates/overrides.yaml.tmpl +0 -0
  186. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/traceability.py +0 -0
  187. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/validator.py +0 -0
  188. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/watch/__init__.py +0 -0
  189. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/watch/events.py +0 -0
  190. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/watch/propagation_log.py +0 -0
  191. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/watch/propagation_pipeline.py +0 -0
  192. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/watch/test_runner.py +0 -0
  193. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/watch/watcher.py +0 -0
  194. {codd_dev-1.28.0 → codd_dev-1.30.0}/codd/wiring.py +0 -0
  195. {codd_dev-1.28.0 → codd_dev-1.30.0}/docs/cookbook/cdp_browser/README.md +0 -0
  196. {codd_dev-1.28.0 → codd_dev-1.30.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.28.0
3
+ Version: 1.30.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
@@ -942,7 +942,7 @@ def propagate_from(project_path: str, files: tuple[str, ...], source: str, edito
942
942
  raise SystemExit(1)
943
943
 
944
944
 
945
- @main.command()
945
+ @main.group(invoke_without_command=True)
946
946
  @click.option("--path", default=".", help="Project root directory")
947
947
  @click.option("--task", default=None, help="Generate only one task by task ID or title match")
948
948
  @click.option("--clean", is_flag=True, default=False, help="Remove existing generated output before re-generating")
@@ -964,16 +964,23 @@ def propagate_from(project_path: str, files: tuple[str, ...], source: str, edito
964
964
  default=None,
965
965
  help="Override AI CLI command (defaults to codd.yaml ai_command or merged CoDD defaults)",
966
966
  )
967
+ @click.option("--use-derived-steps", default=None, help="Inject derived implementation steps: true or false")
968
+ @click.pass_context
967
969
  def implement(
970
+ ctx,
968
971
  path: str,
969
972
  task: str | None,
970
973
  clean: bool,
971
974
  max_tasks: int,
972
975
  wave: int | None,
973
976
  ai_cmd: str | None,
977
+ use_derived_steps: str | None,
974
978
  ):
975
- """Generate implementation code from the implementation plan."""
976
- from codd.implementer import implement_tasks
979
+ """Generate implementation code from the implementation plan."""
980
+ if ctx.invoked_subcommand is not None:
981
+ return
982
+
983
+ from codd.implementer import implement_tasks
977
984
 
978
985
  project_root = Path(path).resolve()
979
986
  codd_dir = _require_codd_dir(project_root)
@@ -982,14 +989,17 @@ def implement(
982
989
  click.echo("Cleaning src/generated/ ...")
983
990
 
984
991
  try:
985
- results = implement_tasks(
986
- project_root,
987
- task=task,
988
- ai_command=ai_cmd,
989
- clean=clean,
990
- max_tasks=max_tasks,
991
- wave=wave,
992
- )
992
+ implement_kwargs = {
993
+ "task": task,
994
+ "ai_command": ai_cmd,
995
+ "clean": clean,
996
+ "max_tasks": max_tasks,
997
+ "wave": wave,
998
+ }
999
+ parsed_use_derived_steps = _optional_bool(use_derived_steps)
1000
+ if parsed_use_derived_steps is not None:
1001
+ implement_kwargs["use_derived_steps"] = parsed_use_derived_steps
1002
+ results = implement_tasks(project_root, **implement_kwargs)
993
1003
  except (FileNotFoundError, ValueError, CoddCLIError) as exc:
994
1004
  click.echo(f"Error: {exc}")
995
1005
  raise SystemExit(1)
@@ -1013,9 +1023,374 @@ def implement(
1013
1023
  f"\nFAILED: {len(failed_tasks)} task(s) produced no files:",
1014
1024
  fg="red", bold=True,
1015
1025
  ))
1016
- for ft in failed_tasks:
1017
- click.echo(click.style(f" ✗ {ft.task_id} ({ft.task_title}): {ft.error}", fg="red"))
1018
- raise SystemExit(1)
1026
+ for ft in failed_tasks:
1027
+ click.echo(click.style(f" ✗ {ft.task_id} ({ft.task_title}): {ft.error}", fg="red"))
1028
+ raise SystemExit(1)
1029
+
1030
+
1031
+ @implement.command("plan")
1032
+ @click.option("--task", "task_id", required=True, help="Implementation task id or title match")
1033
+ @click.option("--design-doc", "design_docs", multiple=True, help="Design document path. May be repeated.")
1034
+ @click.option("--force", is_flag=True, help="Bypass cached implementation steps")
1035
+ @click.option("--dry-run", is_flag=True, help="Print derived steps without writing cache")
1036
+ @click.option("--path", "project_path", default=".", help="Project root directory")
1037
+ @click.option("--provider", default=None, help="Implementation step deriver provider name")
1038
+ @click.option("--ai-cmd", default=None, help="Override AI command")
1039
+ def implement_plan_cmd(
1040
+ task_id: str,
1041
+ design_docs: tuple[str, ...],
1042
+ force: bool,
1043
+ dry_run: bool,
1044
+ project_path: str,
1045
+ provider: str | None,
1046
+ ai_cmd: str | None,
1047
+ ):
1048
+ """Derive implementation steps for one task."""
1049
+ from codd.deployment.providers.ai_command import SubprocessAiCommand
1050
+ from codd.llm.impl_step_deriver import IMPL_STEP_DERIVERS
1051
+
1052
+ project_root = Path(project_path).resolve()
1053
+ _require_codd_dir(project_root)
1054
+ config = _load_optional_project_config(project_root)
1055
+ provider_name = provider or _impl_step_provider(config)
1056
+ deriver_cls = IMPL_STEP_DERIVERS.get(provider_name)
1057
+ if deriver_cls is None:
1058
+ click.echo(f"Error: implementation step deriver provider not found: {provider_name}")
1059
+ raise SystemExit(1)
1060
+
1061
+ try:
1062
+ task_item = _implement_task_for_cli(project_root, config, task_id)
1063
+ nodes = _plan_design_doc_nodes(project_root, design_docs)
1064
+ except (FileNotFoundError, ValueError) as exc:
1065
+ click.echo(f"Error: {exc}")
1066
+ raise SystemExit(1)
1067
+
1068
+ command = ai_cmd or _impl_step_command(config)
1069
+ deriver = deriver_cls(SubprocessAiCommand(command=command, project_root=project_root, config=config))
1070
+ steps = deriver.derive_steps(
1071
+ task_item,
1072
+ nodes,
1073
+ {
1074
+ "project_root": project_root,
1075
+ "force": force,
1076
+ "dry_run": dry_run,
1077
+ "write_cache": not dry_run,
1078
+ "config": config,
1079
+ "project_context": {"project": config.get("project", {})},
1080
+ },
1081
+ )
1082
+ if dry_run:
1083
+ click.echo(yaml.safe_dump([step.to_dict() for step in steps], sort_keys=False, allow_unicode=True), nl=False)
1084
+ return
1085
+ click.echo(f"Derived implementation steps: {len(steps)}")
1086
+
1087
+
1088
+ @implement.command("steps")
1089
+ @click.option("--task", "task_id", required=True, help="Implementation task id")
1090
+ @click.option("--approve", is_flag=True, help="Approve one or more derived steps")
1091
+ @click.option("--step", "step_id", default=None, help="Step id for --approve")
1092
+ @click.option("--all", "approve_all", is_flag=True, help="Approve all pending steps")
1093
+ @click.option("--show-only", is_flag=True, help="Only show cached steps")
1094
+ @click.option("--show-layer-breakdown", is_flag=True, help="Show explicit and inferred step groups")
1095
+ @click.option("--path", "project_path", default=".", help="Project root directory")
1096
+ def implement_steps_cmd(
1097
+ task_id: str,
1098
+ approve: bool,
1099
+ step_id: str | None,
1100
+ approve_all: bool,
1101
+ show_only: bool,
1102
+ show_layer_breakdown: bool,
1103
+ project_path: str,
1104
+ ):
1105
+ """Show or approve derived implementation steps."""
1106
+ from codd.llm.impl_step_deriver import approve_cached_impl_steps, impl_step_cache_path, read_impl_step_cache
1107
+
1108
+ project_root = Path(project_path).resolve()
1109
+ cache_path = impl_step_cache_path(task_id, {"project_root": project_root})
1110
+ if approve:
1111
+ if not approve_all and not step_id:
1112
+ click.echo("Error: --approve requires --step or --all")
1113
+ raise SystemExit(2)
1114
+ try:
1115
+ changed = approve_cached_impl_steps(cache_path, step_id=step_id, approve_all=approve_all)
1116
+ except (FileNotFoundError, ValueError) as exc:
1117
+ click.echo(f"Error: {exc}")
1118
+ raise SystemExit(1)
1119
+ click.echo(f"Approved implementation steps: {changed}")
1120
+ if not show_only:
1121
+ return
1122
+
1123
+ record = read_impl_step_cache(cache_path)
1124
+ if record is None:
1125
+ click.echo("No derived implementation steps found")
1126
+ return
1127
+ if show_layer_breakdown:
1128
+ _echo_impl_step_layer_breakdown(record)
1129
+ return
1130
+ for step in record.steps:
1131
+ layer = "layer2" if step.inferred else "layer1"
1132
+ status = "approved" if step.approved else "pending"
1133
+ click.echo(f"{step.id}\t{status}\t{layer}\t{step.kind}\t{step.source_design_section}")
1134
+
1135
+
1136
+ def _echo_impl_step_layer_breakdown(record: Any) -> None:
1137
+ layer_1 = [step for step in record.steps if not step.inferred]
1138
+ layer_2 = [step for step in record.steps if step.inferred]
1139
+ click.echo(f"[Layer 1 - Explicit, from design] (count={len(layer_1)})")
1140
+ for step in layer_1:
1141
+ click.echo(f" - {step.kind}: {step.id} (rationale: {step.rationale})")
1142
+
1143
+ avg_confidence = sum(float(step.confidence) for step in layer_2) / len(layer_2) if layer_2 else 0.0
1144
+ click.echo("")
1145
+ click.echo(f"[Layer 2 - Best Practice Augment] (count={len(layer_2)}, avg_confidence={avg_confidence:.2f})")
1146
+ for step in layer_2:
1147
+ category = step.best_practice_category or "uncategorized"
1148
+ click.echo(
1149
+ f" - {step.kind}: {step.id} "
1150
+ f"(confidence={step.confidence:.2f}, category={category}, rationale: {step.rationale})"
1151
+ )
1152
+
1153
+
1154
+ @implement.command("augment")
1155
+ @click.option("--task", "task_id", required=True, help="Implementation task id or title match")
1156
+ @click.option("--design-doc", "design_docs", multiple=True, help="Design document path. May be repeated.")
1157
+ @click.option("--path", "project_path", default=".", help="Project root directory")
1158
+ @click.option("--provider", default=None, help="Best practice augmenter provider name")
1159
+ @click.option("--ai-cmd", default=None, help="Override AI command")
1160
+ def implement_augment_cmd(
1161
+ task_id: str,
1162
+ design_docs: tuple[str, ...],
1163
+ project_path: str,
1164
+ provider: str | None,
1165
+ ai_cmd: str | None,
1166
+ ):
1167
+ """Suggest inferred implementation steps and merge them into the task cache."""
1168
+ from codd.deployment.providers.ai_command import SubprocessAiCommand
1169
+ from codd.llm.best_practice_augmenter import BEST_PRACTICE_AUGMENTERS
1170
+ from codd.llm.impl_step_deriver import (
1171
+ ImplStepCacheRecord,
1172
+ impl_step_cache_path,
1173
+ merge_impl_steps,
1174
+ read_impl_step_cache,
1175
+ utc_timestamp,
1176
+ write_impl_step_cache,
1177
+ )
1178
+
1179
+ project_root = Path(project_path).resolve()
1180
+ _require_codd_dir(project_root)
1181
+ config = _load_optional_project_config(project_root)
1182
+ cache_path = impl_step_cache_path(task_id, {"project_root": project_root})
1183
+ record = read_impl_step_cache(cache_path)
1184
+ if record is None:
1185
+ click.echo("Error: derive Layer 1 steps before augmenting")
1186
+ raise SystemExit(1)
1187
+
1188
+ provider_name = provider or _best_practice_provider(config)
1189
+ augmenter_cls = BEST_PRACTICE_AUGMENTERS.get(provider_name)
1190
+ if augmenter_cls is None:
1191
+ click.echo(f"Error: best practice augmenter provider not found: {provider_name}")
1192
+ raise SystemExit(1)
1193
+
1194
+ try:
1195
+ task_item = _implement_task_for_cli(project_root, config, task_id)
1196
+ docs = design_docs or tuple(record.design_docs)
1197
+ nodes = _plan_design_doc_nodes(project_root, docs)
1198
+ except (FileNotFoundError, ValueError) as exc:
1199
+ click.echo(f"Error: {exc}")
1200
+ raise SystemExit(1)
1201
+
1202
+ command = ai_cmd or _best_practice_command(config)
1203
+ augmenter = augmenter_cls(SubprocessAiCommand(command=command, project_root=project_root, config=config))
1204
+ explicit = [step for step in record.steps if not step.inferred]
1205
+ implicit = augmenter.suggest_implicit_steps(
1206
+ task_item,
1207
+ nodes,
1208
+ explicit,
1209
+ {"project_root": project_root, "config": config, "project_context": {"project": config.get("project", {})}},
1210
+ )
1211
+ merged = merge_impl_steps(explicit, implicit)
1212
+ write_impl_step_cache(
1213
+ cache_path,
1214
+ ImplStepCacheRecord(
1215
+ provider_id=record.provider_id,
1216
+ cache_key=f"{record.cache_key}:augmented",
1217
+ task_id=record.task_id,
1218
+ design_doc_sha=record.design_doc_sha,
1219
+ prompt_template_sha=record.prompt_template_sha,
1220
+ generated_at=utc_timestamp(),
1221
+ design_docs=record.design_docs,
1222
+ steps=merged,
1223
+ ),
1224
+ )
1225
+ click.echo(f"Augmented implementation steps: {len(implicit)}")
1226
+
1227
+
1228
+ @implement.command("run")
1229
+ @click.option("--task", "task_id", default=None, help="Generate only one task by task ID or title match")
1230
+ @click.option("--path", "project_path", default=".", help="Project root directory")
1231
+ @click.option("--ai-cmd", default=None, help="Override AI CLI command")
1232
+ @click.option("--use-derived-steps", default="true", help="Inject derived implementation steps: true or false")
1233
+ @click.option("--chunk-size", default=None, type=click.IntRange(min=1), help="Run derived steps in chunks of this size")
1234
+ @click.option(
1235
+ "--timeout-per-chunk",
1236
+ default=600,
1237
+ type=click.IntRange(min=1),
1238
+ show_default=True,
1239
+ help="Seconds before one chunk is interrupted",
1240
+ )
1241
+ def implement_run_cmd(
1242
+ task_id: str | None,
1243
+ project_path: str,
1244
+ ai_cmd: str | None,
1245
+ use_derived_steps: str,
1246
+ chunk_size: int | None,
1247
+ timeout_per_chunk: int,
1248
+ ):
1249
+ """Run implementation with optional derived step injection."""
1250
+ from codd.implementer import implement_tasks
1251
+
1252
+ project_root = Path(project_path).resolve()
1253
+ _require_codd_dir(project_root)
1254
+ if chunk_size is not None:
1255
+ try:
1256
+ result = _run_chunked_implementation(
1257
+ project_root=project_root,
1258
+ task_id=task_id,
1259
+ ai_cmd=ai_cmd,
1260
+ chunk_size=chunk_size,
1261
+ timeout_per_chunk=timeout_per_chunk,
1262
+ history=None,
1263
+ )
1264
+ except (FileNotFoundError, ValueError, CoddCLIError) as exc:
1265
+ click.echo(f"Error: {exc}")
1266
+ raise SystemExit(1)
1267
+ _echo_chunked_result(project_root, result)
1268
+ if result.status != "SUCCESS":
1269
+ raise SystemExit(1)
1270
+ return
1271
+
1272
+ try:
1273
+ results = implement_tasks(
1274
+ project_root,
1275
+ task=task_id,
1276
+ ai_command=ai_cmd,
1277
+ use_derived_steps=_optional_bool(use_derived_steps),
1278
+ )
1279
+ except (FileNotFoundError, ValueError, CoddCLIError) as exc:
1280
+ click.echo(f"Error: {exc}")
1281
+ raise SystemExit(1)
1282
+ failed = [result for result in results if result.error]
1283
+ for result in results:
1284
+ for generated_file in result.generated_files:
1285
+ click.echo(f"Generated: {generated_file.relative_to(project_root)} ({result.task_id})")
1286
+ click.echo(f"{sum(len(result.generated_files) for result in results)} files generated across {len(results) - len(failed)} task(s)")
1287
+ if failed:
1288
+ raise SystemExit(1)
1289
+
1290
+
1291
+ @implement.command("resume")
1292
+ @click.option("--task", "task_id", required=True, help="Implementation task id or title match")
1293
+ @click.option("--history", required=True, help="History id or path from a previous chunked run")
1294
+ @click.option("--path", "project_path", default=".", help="Project root directory")
1295
+ @click.option("--ai-cmd", default=None, help="Override AI CLI command")
1296
+ @click.option("--chunk-size", default=5, type=click.IntRange(min=1), show_default=True, help="Chunk size")
1297
+ @click.option(
1298
+ "--timeout-per-chunk",
1299
+ default=600,
1300
+ type=click.IntRange(min=1),
1301
+ show_default=True,
1302
+ help="Seconds before one chunk is interrupted",
1303
+ )
1304
+ def implement_resume_cmd(
1305
+ task_id: str,
1306
+ history: str,
1307
+ project_path: str,
1308
+ ai_cmd: str | None,
1309
+ chunk_size: int,
1310
+ timeout_per_chunk: int,
1311
+ ):
1312
+ """Resume a chunked implementation run."""
1313
+ project_root = Path(project_path).resolve()
1314
+ _require_codd_dir(project_root)
1315
+ try:
1316
+ result = _run_chunked_implementation(
1317
+ project_root=project_root,
1318
+ task_id=task_id,
1319
+ ai_cmd=ai_cmd,
1320
+ chunk_size=chunk_size,
1321
+ timeout_per_chunk=timeout_per_chunk,
1322
+ history=history,
1323
+ )
1324
+ except (FileNotFoundError, ValueError, CoddCLIError) as exc:
1325
+ click.echo(f"Error: {exc}")
1326
+ raise SystemExit(1)
1327
+ _echo_chunked_result(project_root, result)
1328
+ if result.status != "SUCCESS":
1329
+ raise SystemExit(1)
1330
+
1331
+
1332
+ def _run_chunked_implementation(
1333
+ *,
1334
+ project_root: Path,
1335
+ task_id: str | None,
1336
+ ai_cmd: str | None,
1337
+ chunk_size: int,
1338
+ timeout_per_chunk: int,
1339
+ history: str | None,
1340
+ ):
1341
+ if not task_id:
1342
+ raise ValueError("--task is required when chunked execution is enabled")
1343
+
1344
+ import codd.generator as generator_module
1345
+ from codd.implementer.chunked_runner import ChunkedRunner
1346
+
1347
+ config = _load_optional_project_config(project_root)
1348
+ task_item, steps = _chunked_task_and_steps(project_root, config, task_id)
1349
+ resolved_ai_command = generator_module._resolve_ai_command(config, ai_cmd, command_name="implement")
1350
+
1351
+ def progress(current: int, total: int) -> None:
1352
+ click.echo(f"Chunk {current}/{total} complete")
1353
+
1354
+ runner = ChunkedRunner(
1355
+ chunk_size=chunk_size,
1356
+ timeout_per_chunk=timeout_per_chunk,
1357
+ progress_callback=progress,
1358
+ )
1359
+ if history is None:
1360
+ return runner.run_steps(task_item, steps, resolved_ai_command, project_root)
1361
+ return runner.resume_steps(task_item, steps, resolved_ai_command, project_root, history)
1362
+
1363
+
1364
+ def _chunked_task_and_steps(project_root: Path, config: dict[str, Any], task_id: str):
1365
+ from codd.implementer import _filter_layer1_impl_steps, _filter_layer2_impl_steps
1366
+ from codd.llm.impl_step_deriver import impl_step_cache_path, read_impl_step_cache
1367
+
1368
+ task_item = _implement_task_for_cli(project_root, config, task_id)
1369
+ context = {"project_root": project_root}
1370
+ cache_path = impl_step_cache_path(task_item, context)
1371
+ record = read_impl_step_cache(cache_path)
1372
+ if record is None:
1373
+ record = read_impl_step_cache(impl_step_cache_path(task_id, context))
1374
+ if record is None or not record.steps:
1375
+ raise ValueError("no derived implementation steps found; run 'codd implement plan' first")
1376
+
1377
+ explicit = _filter_layer1_impl_steps([step for step in record.steps if not step.inferred], config)
1378
+ implicit = _filter_layer2_impl_steps([step for step in record.steps if step.inferred], config)
1379
+ steps = [*explicit, *implicit]
1380
+ if not steps:
1381
+ raise ValueError("no approved implementation steps found for chunked execution")
1382
+ return task_item, steps
1383
+
1384
+
1385
+ def _echo_chunked_result(project_root: Path, result) -> None:
1386
+ try:
1387
+ history = result.history_path.relative_to(project_root)
1388
+ except ValueError:
1389
+ history = result.history_path
1390
+ click.echo(
1391
+ f"Chunked implementation {result.status}: "
1392
+ f"{len(result.completed_chunks)}/{result.total_chunks} chunks; history={history}"
1393
+ )
1019
1394
 
1020
1395
 
1021
1396
  @main.command()
@@ -2425,6 +2800,63 @@ def _plan_derive_provider(config: dict[str, Any]) -> str:
2425
2800
  return value if isinstance(value, str) and value else "subprocess_ai_command"
2426
2801
 
2427
2802
 
2803
+ def _impl_step_command(config: dict[str, Any]) -> str | None:
2804
+ value = _nested_config_value(config, ("ai_commands", "impl_step_derive"))
2805
+ if isinstance(value, str):
2806
+ return value
2807
+ if isinstance(value, dict):
2808
+ command = value.get("command")
2809
+ return command if isinstance(command, str) else None
2810
+ return None
2811
+
2812
+
2813
+ def _impl_step_provider(config: dict[str, Any]) -> str:
2814
+ value = _nested_config_value(config, ("ai_commands", "impl_step_derive"))
2815
+ if isinstance(value, dict) and isinstance(value.get("provider"), str):
2816
+ return value["provider"]
2817
+ value = _nested_config_value(config, ("ai_commands", "impl_step_deriver_provider"))
2818
+ return value if isinstance(value, str) and value else "subprocess_ai_command"
2819
+
2820
+
2821
+ def _best_practice_command(config: dict[str, Any]) -> str | None:
2822
+ value = _nested_config_value(config, ("ai_commands", "best_practice_augment"))
2823
+ if isinstance(value, str):
2824
+ return value
2825
+ if isinstance(value, dict):
2826
+ command = value.get("command")
2827
+ return command if isinstance(command, str) else None
2828
+ return None
2829
+
2830
+
2831
+ def _best_practice_provider(config: dict[str, Any]) -> str:
2832
+ value = _nested_config_value(config, ("ai_commands", "best_practice_augment"))
2833
+ if isinstance(value, dict) and isinstance(value.get("provider"), str):
2834
+ return value["provider"]
2835
+ value = _nested_config_value(config, ("ai_commands", "best_practice_augmenter_provider"))
2836
+ return value if isinstance(value, str) and value else "subprocess_ai_command"
2837
+
2838
+
2839
+ def _optional_bool(value: str | bool | None) -> bool | None:
2840
+ if value is None or isinstance(value, bool):
2841
+ return value
2842
+ text = str(value).strip().casefold()
2843
+ if text in {"1", "true", "yes", "on"}:
2844
+ return True
2845
+ if text in {"0", "false", "no", "off"}:
2846
+ return False
2847
+ raise click.BadParameter("expected true or false")
2848
+
2849
+
2850
+ def _implement_task_for_cli(project_root: Path, config: dict[str, Any], task_id: str):
2851
+ from codd.implementer import _extract_all_tasks, _filter_tasks, _load_implementation_plan
2852
+
2853
+ plan = _load_implementation_plan(project_root, config)
2854
+ matches = _filter_tasks(_extract_all_tasks(plan), task_id)
2855
+ if not matches:
2856
+ raise ValueError(f"no implementation task matched {task_id!r}")
2857
+ return matches[0]
2858
+
2859
+
2428
2860
  def _nested_config_value(config: dict[str, Any], path: tuple[str, ...]) -> Any:
2429
2861
  value: Any = config
2430
2862
  for key in path:
@@ -2805,6 +3237,12 @@ def _load_optional_project_config(project_root: Path) -> dict[str, Any]:
2805
3237
 
2806
3238
 
2807
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)
3245
+
2808
3246
  try:
2809
3247
  _run_pro_command("verify", path=path, sprint=sprint)
2810
3248
  except SystemExit as exc:
@@ -2822,6 +3260,14 @@ def _run_verify_once(path: str, sprint: int | None = None) -> _CliVerificationRe
2822
3260
  return _CliVerificationResult(passed=True, exit_code=0, failure=None)
2823
3261
 
2824
3262
 
3263
+ def _cli_result_from_standalone_verify(result: Any) -> _CliVerificationResult:
3264
+ return _CliVerificationResult(
3265
+ passed=bool(result.passed),
3266
+ exit_code=0 if result.passed else 1,
3267
+ failure=getattr(result, "failure", None),
3268
+ )
3269
+
3270
+
2825
3271
  def _system_exit_code(exc: SystemExit) -> int:
2826
3272
  code = exc.code
2827
3273
  if code is None:
@@ -57,16 +57,19 @@ def _read_yaml_mapping(path: Path) -> dict[str, Any]:
57
57
  return payload
58
58
 
59
59
 
60
- def _deep_merge(defaults: Any, project: Any) -> Any:
60
+ def _deep_merge(defaults: Any, project: Any, path: tuple[str, ...] = ()) -> Any:
61
61
  if isinstance(defaults, dict) and isinstance(project, dict):
62
62
  merged = deepcopy(defaults)
63
63
  for key, value in project.items():
64
64
  if key in merged:
65
- merged[key] = _deep_merge(merged[key], value)
65
+ merged[key] = _deep_merge(merged[key], value, (*path, str(key)))
66
66
  else:
67
67
  merged[key] = deepcopy(value)
68
68
  return merged
69
69
 
70
+ if path == ("coherence", "path_prefix_tolerant"):
71
+ return deepcopy(project)
72
+
70
73
  if isinstance(defaults, list) and isinstance(project, list):
71
74
  return _merge_lists(defaults, project)
72
75
 
@@ -124,6 +124,7 @@ def load_dag_settings(project_root: Path, settings: dict[str, Any] | None = None
124
124
  _apply_scan_patterns(merged, project_config)
125
125
  _apply_scan_patterns(merged, requested_settings)
126
126
  merged["coherence"] = _coherence_settings(project_config, requested_settings)
127
+ merged["extraction"] = _extraction_settings(project_config, requested_settings)
127
128
  merged.setdefault("design_doc_patterns", [])
128
129
  merged.setdefault("impl_file_patterns", [])
129
130
  merged.setdefault("test_file_patterns", [])
@@ -210,11 +211,12 @@ def render_mermaid(dag: DAG) -> str:
210
211
  def _add_design_docs(dag: DAG, project_root: Path, settings: dict[str, Any]) -> dict[str, dict[str, Any]]:
211
212
  design_docs: dict[str, dict[str, Any]] = {}
212
213
  aliases: dict[str, str] = {}
214
+ frontmatter_alias = _frontmatter_alias_settings(settings)
213
215
  for md_path in _glob_project_paths(project_root, settings.get("design_doc_patterns", [])):
214
216
  if not md_path.is_file():
215
217
  continue
216
218
  node_id = _relative_id(md_path, project_root)
217
- metadata = extract_design_doc_metadata(md_path)
219
+ metadata = extract_design_doc_metadata(md_path, frontmatter_alias=frontmatter_alias)
218
220
  attributes = metadata.get("attributes") or {}
219
221
  _validate_design_doc_journey_attributes(node_id, attributes)
220
222
  _add_node_once(
@@ -1180,6 +1182,31 @@ def _coherence_settings(*configs: dict[str, Any]) -> dict[str, Any]:
1180
1182
  return coherence
1181
1183
 
1182
1184
 
1185
+ def _extraction_settings(*configs: dict[str, Any]) -> dict[str, Any]:
1186
+ extraction: dict[str, Any] = {"frontmatter_alias": {}}
1187
+ for config in configs:
1188
+ section = config.get("extraction", {})
1189
+ if isinstance(section, dict):
1190
+ extraction = _deep_merge(extraction, section)
1191
+ if not isinstance(extraction.get("frontmatter_alias"), dict):
1192
+ extraction["frontmatter_alias"] = {}
1193
+ return extraction
1194
+
1195
+
1196
+ def _frontmatter_alias_settings(settings: dict[str, Any]) -> dict[str, str]:
1197
+ extraction = settings.get("extraction", {})
1198
+ if not isinstance(extraction, dict):
1199
+ return {}
1200
+ aliases = extraction.get("frontmatter_alias", {})
1201
+ if not isinstance(aliases, dict):
1202
+ return {}
1203
+ return {
1204
+ str(alias_key).strip(): str(canonical_key).strip()
1205
+ for alias_key, canonical_key in aliases.items()
1206
+ if str(alias_key).strip() and str(canonical_key).strip()
1207
+ }
1208
+
1209
+
1183
1210
  def _capability_patterns(settings: dict[str, Any]) -> dict[str, Any]:
1184
1211
  coherence = settings.get("coherence", {})
1185
1212
  if not isinstance(coherence, dict):