codd-dev 1.24.0__tar.gz → 1.26.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 (181) hide show
  1. {codd_dev-1.24.0 → codd_dev-1.26.0}/PKG-INFO +2 -1
  2. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/cli.py +300 -0
  3. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/__init__.py +1 -1
  4. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/builder.py +365 -20
  5. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/checks/__init__.py +35 -0
  6. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/checks/deployment_completeness.py +3 -2
  7. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/checks/node_completeness.py +2 -0
  8. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/checks/task_completion.py +3 -1
  9. codd_dev-1.26.0/codd/dag/checks/user_journey_coherence.py +728 -0
  10. codd_dev-1.26.0/codd/dag/defaults/cpp_embedded.yaml +2 -0
  11. codd_dev-1.26.0/codd/dag/defaults/csharp.yaml +2 -0
  12. codd_dev-1.26.0/codd/dag/defaults/elixir.yaml +2 -0
  13. codd_dev-1.26.0/codd/dag/defaults/generic.yaml +5 -0
  14. codd_dev-1.26.0/codd/dag/defaults/java.yaml +2 -0
  15. codd_dev-1.26.0/codd/dag/defaults/kotlin.yaml +2 -0
  16. codd_dev-1.26.0/codd/dag/defaults/ruby.yaml +2 -0
  17. codd_dev-1.26.0/codd/dag/defaults/rust.yaml +2 -0
  18. codd_dev-1.26.0/codd/dag/defaults/scala.yaml +2 -0
  19. codd_dev-1.26.0/codd/dag/defaults/swift.yaml +2 -0
  20. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/defaults/web.yaml +2 -0
  21. codd_dev-1.26.0/codd/dag/extractor.py +264 -0
  22. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/runner.py +1 -0
  23. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/defaults.yaml +2 -0
  24. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployer.py +146 -1
  25. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/__init__.py +1 -0
  26. codd_dev-1.26.0/codd/deployment/defaults/runtime_capability_inference.yaml +12 -0
  27. codd_dev-1.26.0/codd/deployment/defaults/verification_means_catalog.yaml +6 -0
  28. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/extractor.py +228 -5
  29. codd_dev-1.26.0/codd/deployment/providers/ai_command.py +186 -0
  30. codd_dev-1.26.0/codd/deployment/providers/llm_consideration.py +338 -0
  31. codd_dev-1.26.0/codd/deployment/providers/verification/__init__.py +21 -0
  32. codd_dev-1.26.0/codd/deployment/providers/verification/assertion_handlers.py +244 -0
  33. codd_dev-1.26.0/codd/deployment/providers/verification/cdp_browser.py +350 -0
  34. codd_dev-1.26.0/codd/deployment/providers/verification/cdp_engines.py +40 -0
  35. codd_dev-1.26.0/codd/deployment/providers/verification/cdp_launchers.py +44 -0
  36. codd_dev-1.26.0/codd/deployment/providers/verification/cdp_wire.py +130 -0
  37. codd_dev-1.26.0/codd/deployment/providers/verification/form_strategies.py +44 -0
  38. codd_dev-1.26.0/codd/deployment/providers/verification/means_catalog.py +102 -0
  39. codd_dev-1.26.0/codd/llm/approval.py +324 -0
  40. codd_dev-1.26.0/codd/llm/means_catalog_loader.py +122 -0
  41. codd_dev-1.26.0/codd/llm/parser.py +119 -0
  42. codd_dev-1.26.0/codd/llm/prompt_builder.py +57 -0
  43. codd_dev-1.26.0/codd/llm/strategy_validator.py +37 -0
  44. codd_dev-1.26.0/codd/llm/templates/meta_instruction.md +37 -0
  45. codd_dev-1.26.0/codd/llm/templates/verification_means_catalog.yaml +6 -0
  46. codd_dev-1.26.0/codd/repair/__init__.py +5 -0
  47. codd_dev-1.26.0/codd/repair/engine.py +90 -0
  48. codd_dev-1.26.0/codd/repair/git_patcher.py +120 -0
  49. codd_dev-1.26.0/codd/repair/history.py +124 -0
  50. codd_dev-1.26.0/codd/repair/llm_repair_engine.py +305 -0
  51. codd_dev-1.26.0/codd/repair/schema.py +85 -0
  52. codd_dev-1.26.0/codd/repair/templates/analyze_meta.md +28 -0
  53. codd_dev-1.26.0/codd/repair/templates/propose_meta.md +34 -0
  54. codd_dev-1.26.0/codd/templates/extract_ai_prompt_baseline.md +418 -0
  55. codd_dev-1.26.0/codd/templates/lexicon_questions.md +151 -0
  56. codd_dev-1.26.0/docs/cookbook/cdp_browser/README.md +88 -0
  57. {codd_dev-1.24.0 → codd_dev-1.26.0}/pyproject.toml +3 -1
  58. codd_dev-1.24.0/codd/dag/extractor.py +0 -76
  59. {codd_dev-1.24.0 → codd_dev-1.26.0}/.gitignore +0 -0
  60. {codd_dev-1.24.0 → codd_dev-1.26.0}/LICENSE +0 -0
  61. {codd_dev-1.24.0 → codd_dev-1.26.0}/README.md +0 -0
  62. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/__init__.py +0 -0
  63. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/__main__.py +0 -0
  64. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/_git_helper.py +0 -0
  65. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/ask_user_question_adapter.py +0 -0
  66. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/assembler.py +0 -0
  67. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/bridge.py +0 -0
  68. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/clustering.py +0 -0
  69. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/coherence_adapters.py +0 -0
  70. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/coherence_engine.py +0 -0
  71. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/config.py +0 -0
  72. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/contracts.py +0 -0
  73. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/coverage_auditor.py +0 -0
  74. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/coverage_metrics.py +0 -0
  75. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/checks/depends_on_consistency.py +0 -0
  76. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/checks/edge_validity.py +0 -0
  77. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/checks/transitive_closure.py +0 -0
  78. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/defaults/cli.yaml +0 -0
  79. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/defaults/iot.yaml +0 -0
  80. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/defaults/mobile.yaml +0 -0
  81. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/dag/defaults/test_frameworks.yaml +0 -0
  82. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deploy_targets/__init__.py +0 -0
  83. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deploy_targets/app_service.py +0 -0
  84. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deploy_targets/base.py +0 -0
  85. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deploy_targets/docker_compose.py +0 -0
  86. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/checks/__init__.py +0 -0
  87. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/defaults/deploy_targets.yaml +0 -0
  88. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/defaults/schema_providers.yaml +0 -0
  89. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/defaults/verification_templates.yaml +0 -0
  90. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/providers/__init__.py +0 -0
  91. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/providers/schema/__init__.py +0 -0
  92. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/providers/schema/prisma.py +0 -0
  93. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/providers/target/__init__.py +0 -0
  94. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/providers/target/docker_compose.py +0 -0
  95. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/providers/verification/curl.py +0 -0
  96. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/deployment/providers/verification/playwright.py +0 -0
  97. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/design_md.py +0 -0
  98. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/drift.py +0 -0
  99. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/e2e_extractor.py +0 -0
  100. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/e2e_generator.py +0 -0
  101. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/e2e_runner.py +0 -0
  102. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/env_refs.py +0 -0
  103. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/extract_ai.py +0 -0
  104. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/extractor.py +0 -0
  105. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/fixer.py +0 -0
  106. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/fixup_drift.py +0 -0
  107. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/fixup_drift_strategies/__init__.py +0 -0
  108. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/fixup_drift_strategies/design_token_drift.py +0 -0
  109. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/fixup_drift_strategies/lexicon_violation.py +0 -0
  110. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/fixup_drift_strategies/url_drift.py +0 -0
  111. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/generator.py +0 -0
  112. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/graph.py +0 -0
  113. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/hitl_session.py +0 -0
  114. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/hooks/__init__.py +0 -0
  115. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/hooks/pre-commit +0 -0
  116. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/hooks/recipes/claude_settings_example.json +0 -0
  117. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/hooks/recipes/codex_hook.sh +0 -0
  118. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/hooks/recipes/git_post_commit.sh +0 -0
  119. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/hooks/recipes/git_pre_commit.sh +0 -0
  120. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/implementer.py +0 -0
  121. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/inheritance.py +0 -0
  122. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/knowledge_fetcher.py +0 -0
  123. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/lexicon.py +0 -0
  124. {codd_dev-1.24.0/codd/deployment/providers/verification → codd_dev-1.26.0/codd/llm}/__init__.py +0 -0
  125. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/mcp_server.py +0 -0
  126. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/measure.py +0 -0
  127. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/parsing.py +0 -0
  128. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/planner.py +0 -0
  129. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/policy.py +0 -0
  130. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/preflight/__init__.py +0 -0
  131. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/preflight/defaults/cli.yaml +0 -0
  132. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/preflight/defaults/iot.yaml +0 -0
  133. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/preflight/defaults/mobile.yaml +0 -0
  134. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/preflight/defaults/web.yaml +0 -0
  135. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/propagate.py +0 -0
  136. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/propagator.py +0 -0
  137. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/registry.py +0 -0
  138. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/repair_slice.py +0 -0
  139. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/require.py +0 -0
  140. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/require_plugins.py +0 -0
  141. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/require_propagate.py +0 -0
  142. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/required_artifacts/defaults/cli.yaml +0 -0
  143. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/required_artifacts/defaults/iot.yaml +0 -0
  144. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/required_artifacts/defaults/mobile.yaml +0 -0
  145. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/required_artifacts/defaults/web.yaml +0 -0
  146. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/required_artifacts_deriver.py +0 -0
  147. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/requirement_completeness/defaults/cli.yaml +0 -0
  148. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/requirement_completeness/defaults/iot.yaml +0 -0
  149. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/requirement_completeness/defaults/mobile.yaml +0 -0
  150. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/requirement_completeness/defaults/web.yaml +0 -0
  151. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/requirement_completeness_auditor.py +0 -0
  152. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/restore.py +0 -0
  153. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/routes_extractor.py +0 -0
  154. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/scanner.py +0 -0
  155. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/schema_refs.py +0 -0
  156. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/screen_flow_validator.py +0 -0
  157. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/screen_transition_extractor.py +0 -0
  158. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/screen_transitions/defaults.yaml +0 -0
  159. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/synth.py +0 -0
  160. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/codd.yaml.tmpl +0 -0
  161. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/conventions.yaml.tmpl +0 -0
  162. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
  163. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/doc_links.yaml.tmpl +0 -0
  164. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
  165. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
  166. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
  167. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
  168. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/extracted/system-context.md.j2 +0 -0
  169. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/gitignore.tmpl +0 -0
  170. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/lexicon_schema.yaml +0 -0
  171. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/templates/overrides.yaml.tmpl +0 -0
  172. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/traceability.py +0 -0
  173. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/validator.py +0 -0
  174. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/watch/__init__.py +0 -0
  175. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/watch/events.py +0 -0
  176. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/watch/propagation_log.py +0 -0
  177. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/watch/propagation_pipeline.py +0 -0
  178. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/watch/test_runner.py +0 -0
  179. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/watch/watcher.py +0 -0
  180. {codd_dev-1.24.0 → codd_dev-1.26.0}/codd/wiring.py +0 -0
  181. {codd_dev-1.24.0 → codd_dev-1.26.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.24.0
3
+ Version: 1.26.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
@@ -20,6 +20,7 @@ Requires-Dist: jinja2>=3.1.0
20
20
  Requires-Dist: pyyaml>=6.0
21
21
  Requires-Dist: tomli>=2.0.1; python_version < '3.11'
22
22
  Requires-Dist: watchdog>=4.0.0
23
+ Requires-Dist: websocket-client>=1.8.0
23
24
  Provides-Extra: ai
24
25
  Provides-Extra: api-parsers
25
26
  Requires-Dist: graphql-core>=3.2.0; extra == 'api-parsers'
@@ -2283,6 +2283,119 @@ def test_cmd(project_path: str, related: tuple[str, ...], dry_run: bool):
2283
2283
  raise SystemExit(1)
2284
2284
 
2285
2285
 
2286
+ @main.group()
2287
+ def llm():
2288
+ """Manage LLM-derived considerations."""
2289
+ pass
2290
+
2291
+
2292
+ @llm.command("derive")
2293
+ @click.argument("design_doc", type=click.Path(exists=True, dir_okay=False, path_type=Path))
2294
+ @click.option("--path", "project_path", default=".", help="Project root directory")
2295
+ @click.option("--ai-cmd", default=None, help="Override AI CLI command")
2296
+ @click.option("--model", default=None, help="Override AI model")
2297
+ @click.option("--force", is_flag=True, help="Bypass cached derived considerations")
2298
+ def llm_derive(design_doc: Path, project_path: str, ai_cmd: str | None, model: str | None, force: bool):
2299
+ """Derive considerations for a design document."""
2300
+ from codd.deployment.providers.ai_command import SubprocessAiCommand
2301
+ from codd.deployment.providers.llm_consideration import LlmConsiderationProvider
2302
+ from codd.llm.approval import notify_pending_considerations
2303
+
2304
+ project_root = Path(project_path).resolve()
2305
+ config = _load_optional_project_config(project_root)
2306
+ provider = LlmConsiderationProvider(
2307
+ SubprocessAiCommand(command=ai_cmd, project_root=project_root, config=config),
2308
+ project_root=project_root,
2309
+ cache_dir=project_root / ".codd" / "consideration_cache",
2310
+ model=model,
2311
+ use_cache=not force,
2312
+ )
2313
+ result = provider.provide(design_doc.read_text(encoding="utf-8"), {"model": model} if model else {})
2314
+ notify_pending_considerations(result.considerations, config)
2315
+ click.echo(f"Derived considerations: {len(result.considerations)}")
2316
+
2317
+
2318
+ @llm.command("approve")
2319
+ @click.argument("consideration_id", required=False)
2320
+ @click.option("--all", "approve_all", is_flag=True, help="Approve all pending considerations")
2321
+ @click.option("--path", "project_path", default=".", help="Project root directory")
2322
+ def llm_approve(consideration_id: str | None, approve_all: bool, project_path: str):
2323
+ """Approve one or all pending considerations."""
2324
+ from codd.llm.approval import ApprovalCache, consideration_status, load_cached_considerations
2325
+
2326
+ project_root = Path(project_path).resolve()
2327
+ considerations = load_cached_considerations(project_root)
2328
+ by_id = {consideration.id: consideration for consideration in considerations}
2329
+
2330
+ if approve_all:
2331
+ targets = [
2332
+ consideration
2333
+ for consideration in considerations
2334
+ if consideration_status(consideration, project_root) == "pending"
2335
+ ]
2336
+ else:
2337
+ if not consideration_id:
2338
+ click.echo("Error: consideration_id or --all is required")
2339
+ raise SystemExit(2)
2340
+ if consideration_id not in by_id:
2341
+ click.echo(f"Error: consideration not found: {consideration_id}")
2342
+ raise SystemExit(1)
2343
+ targets = [by_id[consideration_id]]
2344
+
2345
+ for consideration in targets:
2346
+ ApprovalCache.save(consideration.id, "approved", project_root)
2347
+ click.echo(f"Approved considerations: {len(targets)}")
2348
+
2349
+
2350
+ @llm.command("skip")
2351
+ @click.argument("consideration_id")
2352
+ @click.option("--path", "project_path", default=".", help="Project root directory")
2353
+ def llm_skip(consideration_id: str, project_path: str):
2354
+ """Skip one consideration."""
2355
+ from codd.llm.approval import ApprovalCache, load_cached_considerations
2356
+
2357
+ project_root = Path(project_path).resolve()
2358
+ known = {consideration.id for consideration in load_cached_considerations(project_root)}
2359
+ if consideration_id not in known:
2360
+ click.echo(f"Error: consideration not found: {consideration_id}")
2361
+ raise SystemExit(1)
2362
+ ApprovalCache.save(consideration_id, "skipped", project_root)
2363
+ click.echo(f"Skipped consideration: {consideration_id}")
2364
+
2365
+
2366
+ @llm.command("list")
2367
+ @click.option("--path", "project_path", default=".", help="Project root directory")
2368
+ @click.option("--format", "output_format", default="text", type=click.Choice(["text", "json"]))
2369
+ def llm_list(project_path: str, output_format: str):
2370
+ """List generated considerations with approval status."""
2371
+ from codd.llm.approval import consideration_status, consideration_to_dict, load_cached_considerations
2372
+
2373
+ project_root = Path(project_path).resolve()
2374
+ rows = []
2375
+ for consideration in sorted(load_cached_considerations(project_root), key=lambda item: item.id):
2376
+ row = consideration_to_dict(consideration)
2377
+ row["status"] = consideration_status(consideration, project_root)
2378
+ rows.append(row)
2379
+
2380
+ if output_format == "json":
2381
+ click.echo(json.dumps(rows, ensure_ascii=False, indent=2))
2382
+ return
2383
+
2384
+ if not rows:
2385
+ click.echo("No considerations found")
2386
+ return
2387
+ for row in rows:
2388
+ description = str(row.get("description") or "")
2389
+ click.echo(f"{row['id']}\t{row['status']}\t{description}")
2390
+
2391
+
2392
+ def _load_optional_project_config(project_root: Path) -> dict[str, Any]:
2393
+ try:
2394
+ return load_project_config(project_root)
2395
+ except (FileNotFoundError, ValueError):
2396
+ return {}
2397
+
2398
+
2286
2399
  @main.group()
2287
2400
  def dag():
2288
2401
  """DAG Completeness Gate commands."""
@@ -2409,6 +2522,192 @@ def dag_visualize(project_path: str):
2409
2522
  click.echo(render_mermaid(built_dag), nl=False)
2410
2523
 
2411
2524
 
2525
+ @dag.command("journeys")
2526
+ @click.option("--project-path", "--path", default=".", show_default=True, help="Project root directory")
2527
+ @click.option(
2528
+ "--format",
2529
+ "output_format",
2530
+ default="text",
2531
+ type=click.Choice(["text", "json"]),
2532
+ help="Output format",
2533
+ )
2534
+ def dag_journeys(project_path: str, output_format: str):
2535
+ """List user_journeys declared on design_doc DAG nodes."""
2536
+ from codd.dag.builder import build_dag
2537
+
2538
+ project_root = Path(project_path).resolve()
2539
+ try:
2540
+ built_dag = build_dag(project_root)
2541
+ except (FileNotFoundError, ValueError) as exc:
2542
+ click.echo(f"Error: {exc}")
2543
+ raise SystemExit(1)
2544
+
2545
+ journeys = _collect_dag_journeys(built_dag, project_root, _load_optional_project_config(project_root))
2546
+ if output_format == "json":
2547
+ click.echo(json.dumps(journeys, ensure_ascii=False, indent=2))
2548
+ return
2549
+
2550
+ current_doc: str | None = None
2551
+ for journey in journeys:
2552
+ design_doc = journey["design_doc"]
2553
+ if design_doc != current_doc:
2554
+ if current_doc is not None:
2555
+ click.echo()
2556
+ click.echo(design_doc)
2557
+ current_doc = design_doc
2558
+ required = journey["required_capabilities"]
2559
+ requires = ", ".join(required) if required else "-"
2560
+ click.echo(f" {journey['name']} [{journey['criticality']}] requires: {requires}")
2561
+
2562
+
2563
+ @dag.command("run-journey")
2564
+ @click.argument("journey_name")
2565
+ @click.option("--project-path", "--path", default=".", show_default=True, help="Project root directory")
2566
+ @click.option(
2567
+ "--config-section",
2568
+ default="cdp_browser",
2569
+ show_default=True,
2570
+ help="verification.templates section used for browser config",
2571
+ )
2572
+ def dag_run_journey(journey_name: str, project_path: str, config_section: str):
2573
+ """Run one declared user_journey with the CDP browser template."""
2574
+ from codd.dag.builder import build_dag
2575
+ from codd.deployment.providers.verification.cdp_browser import CdpBrowser
2576
+
2577
+ project_root = Path(project_path).resolve()
2578
+ try:
2579
+ config = load_project_config(project_root)
2580
+ template_config = _journey_template_config(config, config_section)
2581
+ built_dag = build_dag(project_root)
2582
+ except (FileNotFoundError, ValueError) as exc:
2583
+ click.echo(f"Error: {exc}")
2584
+ raise SystemExit(2)
2585
+
2586
+ journey_record = _find_dag_journey(built_dag, journey_name, project_root, config)
2587
+ if journey_record is None:
2588
+ click.echo(f"Error: user_journey not found: {journey_name}")
2589
+ raise SystemExit(2)
2590
+
2591
+ command = json.dumps(
2592
+ _journey_execution_plan(project_root, journey_record, template_config),
2593
+ sort_keys=True,
2594
+ )
2595
+ result = CdpBrowser(config=template_config).execute(command)
2596
+ if result.output:
2597
+ click.echo(result.output)
2598
+ raise SystemExit(0 if result.passed else 1)
2599
+
2600
+
2601
+ def _journey_template_config(config: dict[str, Any], config_section: str) -> dict[str, Any]:
2602
+ verification = config.get("verification")
2603
+ templates = verification.get("templates") if isinstance(verification, dict) else None
2604
+ if not isinstance(templates, dict) or config_section not in templates:
2605
+ raise ValueError(f"verification.templates.{config_section} config not found")
2606
+
2607
+ section = templates[config_section]
2608
+ if not isinstance(section, dict):
2609
+ raise ValueError(f"verification.templates.{config_section} must be a mapping")
2610
+ return dict(section)
2611
+
2612
+
2613
+ def _find_dag_journey(
2614
+ dag: Any,
2615
+ journey_name: str,
2616
+ project_root: Path | None = None,
2617
+ settings: dict[str, Any] | None = None,
2618
+ ) -> dict[str, Any] | None:
2619
+ for node in sorted(dag.nodes.values(), key=lambda item: item.id):
2620
+ if node.kind != "design_doc":
2621
+ continue
2622
+ for journey in _node_user_journeys(node, project_root, settings):
2623
+ if str(journey.get("name") or "") == journey_name:
2624
+ return {
2625
+ "design_doc": node.path or node.id,
2626
+ "journey": dict(journey),
2627
+ }
2628
+ return None
2629
+
2630
+
2631
+ def _journey_execution_plan(
2632
+ project_root: Path,
2633
+ journey_record: dict[str, Any],
2634
+ template_config: dict[str, Any],
2635
+ ) -> dict[str, Any]:
2636
+ journey = dict(journey_record["journey"])
2637
+ journey_name = str(journey.get("name") or "")
2638
+ steps = journey.get("steps")
2639
+ return {
2640
+ "template": "cdp_browser",
2641
+ "test_kind": "e2e",
2642
+ "target": _journey_target(journey),
2643
+ "identifier": f"journey:{journey_name}",
2644
+ "journey": journey_name,
2645
+ "steps": steps if isinstance(steps, list) else [],
2646
+ "project_root": str(project_root),
2647
+ "design_doc": journey_record["design_doc"],
2648
+ "config": template_config,
2649
+ }
2650
+
2651
+
2652
+ def _journey_target(journey: dict[str, Any]) -> str:
2653
+ steps = journey.get("steps")
2654
+ if isinstance(steps, list):
2655
+ for step in steps:
2656
+ if not isinstance(step, dict):
2657
+ continue
2658
+ if step.get("action") == "navigate":
2659
+ target = step.get("target") or step.get("url")
2660
+ if target:
2661
+ return str(target)
2662
+
2663
+ target = journey.get("target") or journey.get("url")
2664
+ return str(target or "")
2665
+
2666
+
2667
+ def _collect_dag_journeys(
2668
+ dag: Any,
2669
+ project_root: Path | None = None,
2670
+ settings: dict[str, Any] | None = None,
2671
+ ) -> list[dict[str, Any]]:
2672
+ journeys: list[dict[str, Any]] = []
2673
+ for node in sorted(dag.nodes.values(), key=lambda item: item.id):
2674
+ if node.kind != "design_doc":
2675
+ continue
2676
+ for journey in _node_user_journeys(node, project_root, settings):
2677
+ journeys.append(
2678
+ {
2679
+ "design_doc": node.path or node.id,
2680
+ "name": str(journey.get("name") or ""),
2681
+ "criticality": str(journey.get("criticality") or ""),
2682
+ "required_capabilities": _string_list(journey.get("required_capabilities")),
2683
+ }
2684
+ )
2685
+ return journeys
2686
+
2687
+
2688
+ def _node_user_journeys(
2689
+ node: Any,
2690
+ project_root: Path | None = None,
2691
+ settings: dict[str, Any] | None = None,
2692
+ ) -> list[dict[str, Any]]:
2693
+ if project_root is not None:
2694
+ from codd.dag.checks.user_journey_coherence import UserJourneyCoherenceCheck
2695
+
2696
+ return UserJourneyCoherenceCheck(project_root=project_root, settings=settings or {})._journey_entries(node)
2697
+
2698
+ attributes = getattr(node, "attributes", {}) or {}
2699
+ value = attributes.get("user_journeys")
2700
+ if not isinstance(value, list):
2701
+ return []
2702
+ return [journey for journey in value if isinstance(journey, dict)]
2703
+
2704
+
2705
+ def _string_list(value: Any) -> list[str]:
2706
+ if not isinstance(value, list):
2707
+ return []
2708
+ return [str(item) for item in value if isinstance(item, str) and item]
2709
+
2710
+
2412
2711
  def _dag_result_to_dict(result: Any) -> dict[str, Any]:
2413
2712
  if is_dataclass(result):
2414
2713
  return asdict(result)
@@ -2447,6 +2746,7 @@ def _dag_result_has_findings(result: Any) -> bool:
2447
2746
  def _dag_result_details(result: Any) -> list[str]:
2448
2747
  details: list[str] = []
2449
2748
  for key in (
2749
+ "message",
2450
2750
  "missing_impl_files",
2451
2751
  "orphan_edges",
2452
2752
  "dangling_refs",
@@ -19,7 +19,7 @@ class Edge:
19
19
  from_id: str
20
20
  to_id: str
21
21
  kind: str
22
- attributes: dict[str, Any] = field(default_factory=dict)
22
+ attributes: dict[str, Any] | None = None
23
23
 
24
24
 
25
25
  class DAG: