codd-dev 1.22.0__tar.gz → 1.24.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 (147) hide show
  1. {codd_dev-1.22.0 → codd_dev-1.24.0}/PKG-INFO +32 -5
  2. {codd_dev-1.22.0 → codd_dev-1.24.0}/README.md +30 -4
  3. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/__init__.py +1 -1
  4. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/cli.py +118 -18
  5. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/coherence_engine.py +0 -1
  6. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/coverage_metrics.py +2 -18
  7. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/__init__.py +1 -0
  8. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/builder.py +310 -14
  9. codd_dev-1.24.0/codd/dag/checks/deployment_completeness.py +615 -0
  10. codd_dev-1.24.0/codd/dag/defaults/test_frameworks.yaml +7 -0
  11. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/defaults/web.yaml +10 -0
  12. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/runner.py +1 -0
  13. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/deployer.py +190 -118
  14. codd_dev-1.24.0/codd/deployment/__init__.py +67 -0
  15. codd_dev-1.24.0/codd/deployment/checks/__init__.py +21 -0
  16. codd_dev-1.24.0/codd/deployment/defaults/deploy_targets.yaml +5 -0
  17. codd_dev-1.24.0/codd/deployment/defaults/schema_providers.yaml +6 -0
  18. codd_dev-1.24.0/codd/deployment/defaults/verification_templates.yaml +7 -0
  19. codd_dev-1.24.0/codd/deployment/extractor.py +679 -0
  20. codd_dev-1.24.0/codd/deployment/providers/__init__.py +105 -0
  21. codd_dev-1.24.0/codd/deployment/providers/schema/__init__.py +0 -0
  22. codd_dev-1.24.0/codd/deployment/providers/schema/prisma.py +195 -0
  23. codd_dev-1.24.0/codd/deployment/providers/target/__init__.py +7 -0
  24. codd_dev-1.24.0/codd/deployment/providers/target/docker_compose.py +197 -0
  25. codd_dev-1.24.0/codd/deployment/providers/verification/__init__.py +1 -0
  26. codd_dev-1.24.0/codd/deployment/providers/verification/curl.py +92 -0
  27. codd_dev-1.24.0/codd/deployment/providers/verification/playwright.py +91 -0
  28. codd_dev-1.24.0/codd/hooks/recipes/claude_settings_example.json +15 -0
  29. codd_dev-1.24.0/codd/hooks/recipes/codex_hook.sh +31 -0
  30. codd_dev-1.24.0/codd/hooks/recipes/git_post_commit.sh +15 -0
  31. codd_dev-1.24.0/codd/hooks/recipes/git_pre_commit.sh +15 -0
  32. codd_dev-1.24.0/codd/watch/__init__.py +1 -0
  33. codd_dev-1.24.0/codd/watch/events.py +43 -0
  34. codd_dev-1.24.0/codd/watch/propagation_log.py +40 -0
  35. codd_dev-1.24.0/codd/watch/propagation_pipeline.py +233 -0
  36. codd_dev-1.24.0/codd/watch/test_runner.py +187 -0
  37. codd_dev-1.24.0/codd/watch/watcher.py +112 -0
  38. {codd_dev-1.22.0 → codd_dev-1.24.0}/pyproject.toml +4 -1
  39. codd_dev-1.22.0/codd/drift_linkers/__init__.py +0 -46
  40. codd_dev-1.22.0/codd/drift_linkers/api.py +0 -484
  41. codd_dev-1.22.0/codd/drift_linkers/defaults/cli.yaml +0 -1
  42. codd_dev-1.22.0/codd/drift_linkers/defaults/iot.yaml +0 -1
  43. codd_dev-1.22.0/codd/drift_linkers/defaults/mobile.yaml +0 -2
  44. codd_dev-1.22.0/codd/drift_linkers/defaults/web.yaml +0 -8
  45. codd_dev-1.22.0/codd/drift_linkers/schema.py +0 -262
  46. codd_dev-1.22.0/codd/drift_linkers/screen_flow.py +0 -171
  47. {codd_dev-1.22.0 → codd_dev-1.24.0}/.gitignore +0 -0
  48. {codd_dev-1.22.0 → codd_dev-1.24.0}/LICENSE +0 -0
  49. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/__main__.py +0 -0
  50. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/_git_helper.py +0 -0
  51. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/ask_user_question_adapter.py +0 -0
  52. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/assembler.py +0 -0
  53. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/bridge.py +0 -0
  54. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/clustering.py +0 -0
  55. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/coherence_adapters.py +0 -0
  56. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/config.py +0 -0
  57. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/contracts.py +0 -0
  58. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/coverage_auditor.py +0 -0
  59. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/checks/__init__.py +0 -0
  60. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/checks/depends_on_consistency.py +0 -0
  61. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/checks/edge_validity.py +0 -0
  62. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/checks/node_completeness.py +0 -0
  63. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/checks/task_completion.py +0 -0
  64. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/checks/transitive_closure.py +0 -0
  65. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/defaults/cli.yaml +0 -0
  66. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/defaults/iot.yaml +0 -0
  67. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/defaults/mobile.yaml +0 -0
  68. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/dag/extractor.py +0 -0
  69. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/defaults.yaml +0 -0
  70. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/deploy_targets/__init__.py +0 -0
  71. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/deploy_targets/app_service.py +0 -0
  72. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/deploy_targets/base.py +0 -0
  73. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/deploy_targets/docker_compose.py +0 -0
  74. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/design_md.py +0 -0
  75. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/drift.py +0 -0
  76. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/e2e_extractor.py +0 -0
  77. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/e2e_generator.py +0 -0
  78. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/e2e_runner.py +0 -0
  79. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/env_refs.py +0 -0
  80. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/extract_ai.py +0 -0
  81. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/extractor.py +0 -0
  82. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/fixer.py +0 -0
  83. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/fixup_drift.py +0 -0
  84. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/fixup_drift_strategies/__init__.py +0 -0
  85. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/fixup_drift_strategies/design_token_drift.py +0 -0
  86. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/fixup_drift_strategies/lexicon_violation.py +0 -0
  87. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/fixup_drift_strategies/url_drift.py +0 -0
  88. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/generator.py +0 -0
  89. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/graph.py +0 -0
  90. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/hitl_session.py +0 -0
  91. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/hooks/__init__.py +0 -0
  92. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/hooks/pre-commit +0 -0
  93. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/implementer.py +0 -0
  94. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/inheritance.py +0 -0
  95. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/knowledge_fetcher.py +0 -0
  96. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/lexicon.py +0 -0
  97. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/mcp_server.py +0 -0
  98. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/measure.py +0 -0
  99. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/parsing.py +0 -0
  100. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/planner.py +0 -0
  101. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/policy.py +0 -0
  102. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/preflight/__init__.py +0 -0
  103. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/preflight/defaults/cli.yaml +0 -0
  104. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/preflight/defaults/iot.yaml +0 -0
  105. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/preflight/defaults/mobile.yaml +0 -0
  106. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/preflight/defaults/web.yaml +0 -0
  107. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/propagate.py +0 -0
  108. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/propagator.py +0 -0
  109. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/registry.py +0 -0
  110. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/repair_slice.py +0 -0
  111. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/require.py +0 -0
  112. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/require_plugins.py +0 -0
  113. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/require_propagate.py +0 -0
  114. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/required_artifacts/defaults/cli.yaml +0 -0
  115. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/required_artifacts/defaults/iot.yaml +0 -0
  116. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/required_artifacts/defaults/mobile.yaml +0 -0
  117. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/required_artifacts/defaults/web.yaml +0 -0
  118. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/required_artifacts_deriver.py +0 -0
  119. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/requirement_completeness/defaults/cli.yaml +0 -0
  120. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/requirement_completeness/defaults/iot.yaml +0 -0
  121. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/requirement_completeness/defaults/mobile.yaml +0 -0
  122. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/requirement_completeness/defaults/web.yaml +0 -0
  123. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/requirement_completeness_auditor.py +0 -0
  124. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/restore.py +0 -0
  125. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/routes_extractor.py +0 -0
  126. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/scanner.py +0 -0
  127. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/schema_refs.py +0 -0
  128. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/screen_flow_validator.py +0 -0
  129. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/screen_transition_extractor.py +0 -0
  130. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/screen_transitions/defaults.yaml +0 -0
  131. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/synth.py +0 -0
  132. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/codd.yaml.tmpl +0 -0
  133. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/conventions.yaml.tmpl +0 -0
  134. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
  135. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/doc_links.yaml.tmpl +0 -0
  136. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
  137. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
  138. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
  139. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
  140. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/extracted/system-context.md.j2 +0 -0
  141. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/gitignore.tmpl +0 -0
  142. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/lexicon_schema.yaml +0 -0
  143. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/templates/overrides.yaml.tmpl +0 -0
  144. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/traceability.py +0 -0
  145. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/validator.py +0 -0
  146. {codd_dev-1.22.0 → codd_dev-1.24.0}/codd/wiring.py +0 -0
  147. {codd_dev-1.22.0 → codd_dev-1.24.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.22.0
3
+ Version: 1.24.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
@@ -19,6 +19,7 @@ Requires-Dist: click>=8.0
19
19
  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
+ Requires-Dist: watchdog>=4.0.0
22
23
  Provides-Extra: ai
23
24
  Provides-Extra: api-parsers
24
25
  Requires-Dist: graphql-core>=3.2.0; extra == 'api-parsers'
@@ -950,23 +951,49 @@ You: yes
950
951
 
951
952
  ### Hook Integration — Set It Once, Never Think Again
952
953
 
953
- Add this hook and **you never run `codd scan` manually again.** Every file edit triggers it automatically — the dependency graph is always current, always accurate, zero mental overhead:
954
+ CoDD ships copyable hook recipes in `codd/hooks/recipes/` so Claude, Codex, and Git can all trigger the same change-driven propagation path:
955
+ `codd propagate-from --files <changed-file>`.
956
+
957
+ #### Claude PostToolUse
958
+
959
+ Use `codd/hooks/recipes/claude_settings_example.json` as the starting point for `.claude/settings.json`. It listens for Edit / Write / MultiEdit and extracts changed files from `TOOL_INPUT` before calling `propagate-from`.
954
960
 
955
961
  ```json
956
962
  {
957
963
  "hooks": {
958
964
  "PostToolUse": [{
959
- "matcher": "Edit|Write",
965
+ "matcher": "Edit|Write|MultiEdit",
960
966
  "hooks": [{
961
967
  "type": "command",
962
- "command": "codd scan --path ."
968
+ "command": "python -m codd propagate-from --files <changed-file> --source editor_hook --editor claude"
963
969
  }]
964
970
  }]
965
971
  }
966
972
  }
967
973
  ```
968
974
 
969
- With hooks active, your entire workflow becomes: **edit files normally, then run `/codd-impact` when you want to know what's affected.** That's it. The graph maintenance is invisible.
975
+ #### Codex Post-Edit
976
+
977
+ Use `codd/hooks/recipes/codex_hook.sh` when your Codex wrapper can pass edited files through `CODEX_EDITED_FILES`:
978
+
979
+ ```bash
980
+ export CODEX_EDITED_FILES="src/app.ts,docs/design/api.md"
981
+ bash codd/hooks/recipes/codex_hook.sh
982
+ ```
983
+
984
+ #### Git Hooks
985
+
986
+ Use the git recipes as a final catch for manual edits and non-editor workflows:
987
+
988
+ ```bash
989
+ cp codd/hooks/recipes/git_pre_commit.sh .git/hooks/pre-commit
990
+ cp codd/hooks/recipes/git_post_commit.sh .git/hooks/post-commit
991
+ chmod +x .git/hooks/pre-commit .git/hooks/post-commit
992
+ ```
993
+
994
+ The pre-commit hook runs `propagate-from` in `--dry-run` mode against staged files. The post-commit hook runs propagation against the committed file set.
995
+
996
+ With hooks active, your entire workflow becomes: **edit files normally, and CoDD propagates the change from the files that actually changed.** The graph maintenance is invisible.
970
997
 
971
998
  ### Available Skills
972
999
 
@@ -912,23 +912,49 @@ You: yes
912
912
 
913
913
  ### Hook Integration — Set It Once, Never Think Again
914
914
 
915
- Add this hook and **you never run `codd scan` manually again.** Every file edit triggers it automatically — the dependency graph is always current, always accurate, zero mental overhead:
915
+ CoDD ships copyable hook recipes in `codd/hooks/recipes/` so Claude, Codex, and Git can all trigger the same change-driven propagation path:
916
+ `codd propagate-from --files <changed-file>`.
917
+
918
+ #### Claude PostToolUse
919
+
920
+ Use `codd/hooks/recipes/claude_settings_example.json` as the starting point for `.claude/settings.json`. It listens for Edit / Write / MultiEdit and extracts changed files from `TOOL_INPUT` before calling `propagate-from`.
916
921
 
917
922
  ```json
918
923
  {
919
924
  "hooks": {
920
925
  "PostToolUse": [{
921
- "matcher": "Edit|Write",
926
+ "matcher": "Edit|Write|MultiEdit",
922
927
  "hooks": [{
923
928
  "type": "command",
924
- "command": "codd scan --path ."
929
+ "command": "python -m codd propagate-from --files <changed-file> --source editor_hook --editor claude"
925
930
  }]
926
931
  }]
927
932
  }
928
933
  }
929
934
  ```
930
935
 
931
- With hooks active, your entire workflow becomes: **edit files normally, then run `/codd-impact` when you want to know what's affected.** That's it. The graph maintenance is invisible.
936
+ #### Codex Post-Edit
937
+
938
+ Use `codd/hooks/recipes/codex_hook.sh` when your Codex wrapper can pass edited files through `CODEX_EDITED_FILES`:
939
+
940
+ ```bash
941
+ export CODEX_EDITED_FILES="src/app.ts,docs/design/api.md"
942
+ bash codd/hooks/recipes/codex_hook.sh
943
+ ```
944
+
945
+ #### Git Hooks
946
+
947
+ Use the git recipes as a final catch for manual edits and non-editor workflows:
948
+
949
+ ```bash
950
+ cp codd/hooks/recipes/git_pre_commit.sh .git/hooks/pre-commit
951
+ cp codd/hooks/recipes/git_post_commit.sh .git/hooks/post-commit
952
+ chmod +x .git/hooks/pre-commit .git/hooks/post-commit
953
+ ```
954
+
955
+ The pre-commit hook runs `propagate-from` in `--dry-run` mode against staged files. The post-commit hook runs propagation against the committed file set.
956
+
957
+ With hooks active, your entire workflow becomes: **edit files normally, and CoDD propagates the change from the files that actually changed.** The graph maintenance is invisible.
932
958
 
933
959
  ### Available Skills
934
960
 
@@ -1,3 +1,3 @@
1
1
  """CoDD — Coherence-Driven Development."""
2
2
 
3
- __version__ = "1.22.0"
3
+ __version__ = "1.24.0"
@@ -344,23 +344,65 @@ def scan(path: str):
344
344
  run_scan(project_root, codd_dir)
345
345
 
346
346
 
347
- @main.command()
348
- @click.option("--diff", default="HEAD", help="Git diff target (default: HEAD, shows uncommitted changes)")
349
- @click.option("--path", default=".", help="Project root directory")
350
- @click.option("--output", default=None, help="Output file (default: stdout)")
351
- def impact(diff: str, path: str, output: str):
347
+ @main.command()
348
+ @click.option("--diff", default="HEAD", help="Git diff target (default: HEAD, shows uncommitted changes)")
349
+ @click.option("--path", default=".", help="Project root directory")
350
+ @click.option("--output", default=None, help="Output file (default: stdout)")
351
+ def impact(diff: str, path: str, output: str):
352
352
  """Analyze change impact from git diff."""
353
353
  from codd.propagate import run_impact
354
354
  project_root = Path(path).resolve()
355
355
  codd_dir = _require_codd_dir(project_root)
356
-
357
- run_impact(project_root, codd_dir, diff, output)
358
-
359
-
360
- @main.command()
361
- @click.option("--wave", required=True, type=click.IntRange(min=1), help="Wave number to generate")
362
- @click.option("--path", default=".", help="Project root directory")
363
- @click.option("--force", is_flag=True, help="Overwrite existing files")
356
+
357
+ run_impact(project_root, codd_dir, diff, output)
358
+
359
+
360
+ @main.command("watch")
361
+ @click.option("--project-path", "--path", default=".", show_default=True, help="Project root directory")
362
+ @click.option("--debounce", default=500, show_default=True, type=int, help="Debounce interval in milliseconds")
363
+ @click.option("--background", is_flag=True, default=False, help="Run watcher in background mode")
364
+ @click.option("--status", is_flag=True, default=False, help="Show watcher status")
365
+ def watch_cmd(project_path: str, debounce: int, background: bool, status: bool) -> None:
366
+ """Watch for file changes and emit CDAP file-change events."""
367
+ project_root = Path(project_path).resolve()
368
+ pid_file = project_root / ".codd" / "watch.pid"
369
+
370
+ if status:
371
+ if pid_file.exists():
372
+ click.echo(f"Watcher running (PID: {pid_file.read_text(encoding='utf-8').strip()})")
373
+ else:
374
+ click.echo("Watcher not running")
375
+ return
376
+
377
+ from codd.watch.events import FileChangeEvent
378
+ from codd.watch.watcher import start_watch
379
+
380
+ if not project_root.exists():
381
+ click.echo(f"Error: Project path does not exist: {project_root}")
382
+ raise SystemExit(1)
383
+ if not project_root.is_dir():
384
+ click.echo(f"Error: Project path is not a directory: {project_root}")
385
+ raise SystemExit(1)
386
+
387
+ pid_file.parent.mkdir(parents=True, exist_ok=True)
388
+ pid_file.write_text(f"{os.getpid()}\n", encoding="utf-8")
389
+
390
+ def on_change(event: FileChangeEvent) -> None:
391
+ preview = ", ".join(event.files[:3])
392
+ suffix = "" if len(event.files) <= 3 else f", ... {len(event.files) - 3} more"
393
+ click.echo(f"[watch] {len(event.files)} file(s) changed: {preview}{suffix}")
394
+
395
+ click.echo(f"Watching {project_root} (debounce={debounce}ms)")
396
+ observer = start_watch(project_root, on_change, debounce_ms=debounce, background=background)
397
+ if background:
398
+ click.echo(f"Watcher running in background mode (PID: {os.getpid()})")
399
+ observer.join(timeout=0)
400
+
401
+
402
+ @main.command()
403
+ @click.option("--wave", required=True, type=click.IntRange(min=1), help="Wave number to generate")
404
+ @click.option("--path", default=".", help="Project root directory")
405
+ @click.option("--force", is_flag=True, help="Overwrite existing files")
364
406
  @click.option(
365
407
  "--ai-cmd",
366
408
  default=None,
@@ -852,11 +894,47 @@ def propagate(diff: str, path: str, update: bool, verify: bool, do_commit: bool,
852
894
  click.echo(f" [{status}] {doc.path} ({doc.node_id})")
853
895
  click.echo(f" modules: {', '.join(doc.matched_modules)}")
854
896
 
855
- if not update and result.affected_docs:
856
- click.echo(f"\nRun with --update to update these docs via AI.")
857
-
858
-
859
- @main.command()
897
+ if not update and result.affected_docs:
898
+ click.echo(f"\nRun with --update to update these docs via AI.")
899
+
900
+
901
+ @main.command("propagate-from")
902
+ @click.option("--project-path", default=".", show_default=True, help="Project root directory")
903
+ @click.option("--files", multiple=True, required=True, help="Changed file path. Can be repeated.")
904
+ @click.option(
905
+ "--source",
906
+ default="manual",
907
+ show_default=True,
908
+ type=click.Choice(["watch", "git_hook", "editor_hook", "manual"]),
909
+ help="Change source that triggered propagation.",
910
+ )
911
+ @click.option(
912
+ "--editor",
913
+ default=None,
914
+ type=click.Choice(["claude", "codex", "manual"]),
915
+ help="Editor that produced the change, when known.",
916
+ )
917
+ @click.option("--dry-run", is_flag=True, default=False, help="Compute impact without propagate/fix/log writes.")
918
+ def propagate_from(project_path: str, files: tuple[str, ...], source: str, editor: str | None, dry_run: bool):
919
+ """Run the CDAP propagation pipeline from changed files."""
920
+ from codd.watch.events import FileChangeEvent
921
+ from codd.watch.propagation_pipeline import run_propagation_pipeline
922
+
923
+ project_root = Path(project_path).resolve()
924
+ event = FileChangeEvent(files=list(files), source=source, editor=editor)
925
+ result = run_propagation_pipeline(project_root, list(files), dry_run=dry_run, event=event)
926
+
927
+ click.echo(f"Impacted nodes: {len(result.impacted_nodes)}")
928
+ click.echo(f"Propagated: {result.propagated_count}")
929
+ click.echo(f"Fixed: {result.fixed_count}")
930
+ if result.errors:
931
+ click.echo(f"Errors: {result.errors}", err=True)
932
+
933
+ if not result.success:
934
+ raise SystemExit(1)
935
+
936
+
937
+ @main.command()
860
938
  @click.option("--path", default=".", help="Project root directory")
861
939
  @click.option("--task", default=None, help="Generate only one task by task ID or title match")
862
940
  @click.option("--clean", is_flag=True, default=False, help="Remove existing generated output before re-generating")
@@ -2183,6 +2261,28 @@ def mcp_server(project: str):
2183
2261
  run_stdio(project_root)
2184
2262
 
2185
2263
 
2264
+ @main.command("test")
2265
+ @click.option("--project-path", "--path", default=".", show_default=True, help="Project root directory")
2266
+ @click.option("--related", multiple=True, help="Run only tests related to these files")
2267
+ @click.option("--dry-run", is_flag=True, default=False, help="Print the related test command without running it")
2268
+ def test_cmd(project_path: str, related: tuple[str, ...], dry_run: bool):
2269
+ """Run tests. Use --related <file> to run only related tests."""
2270
+ from codd.watch.test_runner import run_related_tests
2271
+
2272
+ project_root = Path(project_path).resolve()
2273
+ if not related:
2274
+ click.echo("Use --related <file> to specify files. Full test run not supported via this command.")
2275
+ return
2276
+
2277
+ result = run_related_tests(project_root, list(related), dry_run=dry_run)
2278
+ click.echo(f"Related tests: {result['related']}")
2279
+ if result.get("cmd"):
2280
+ click.echo(f"Command: {result['cmd']}")
2281
+ click.echo(f"Status: {result['status']}")
2282
+ if result.get("exit_code") not in (None, 0):
2283
+ raise SystemExit(1)
2284
+
2285
+
2186
2286
  @main.group()
2187
2287
  def dag():
2188
2288
  """DAG Completeness Gate commands."""
@@ -80,7 +80,6 @@ def set_coherence_bus(bus: EventBus | None) -> None:
80
80
  """Set the opt-in coherence bus on detectors that publish DriftEvents."""
81
81
  for module_name in (
82
82
  "codd.drift",
83
- "codd.drift_linkers.api",
84
83
  "codd.deployer",
85
84
  "codd.hitl_session",
86
85
  "codd.validator",
@@ -134,7 +134,7 @@ def compute_screen_flow_coverage(
134
134
  config: dict[str, Any],
135
135
  threshold: float = 100.0,
136
136
  ) -> CoverageResult:
137
- """Measure screen-flow drift as a coverage gate metric."""
137
+ """Measure screen-flow route drift as a coverage gate metric."""
138
138
 
139
139
  try:
140
140
  from codd.cli import CoddCLIError
@@ -153,25 +153,9 @@ def compute_screen_flow_coverage(
153
153
  details=[f"error: {exc}"],
154
154
  )
155
155
 
156
- design_drift_details: list[str] = []
157
- design_drift_count = 0
158
- try:
159
- from codd.drift_linkers.screen_flow import ScreenFlowGate
160
-
161
- gate_result = ScreenFlowGate(
162
- project_root=project_root,
163
- settings={**config, "apply": True},
164
- ).run()
165
- if not gate_result.skipped:
166
- design_drift_count = gate_result.drift_count
167
- design_drift_details = gate_result.details
168
- except Exception as exc: # pragma: no cover - defensive gate behavior
169
- return _exception_result("screen_flow_coverage", threshold, exc)
170
-
171
- drift_count = len(drifts) + design_drift_count
156
+ drift_count = len(drifts)
172
157
  pct = 100.0 if drift_count == 0 else max(0.0, 100.0 - drift_count * 10.0)
173
158
  details = [f"drift_count: {drift_count}"]
174
- details.extend(design_drift_details)
175
159
  return CoverageResult(
176
160
  metric="screen_flow_coverage",
177
161
  total=1,
@@ -19,6 +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
23
 
23
24
 
24
25
  class DAG: