codd-dev 1.5.0__tar.gz → 1.6.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 (114) hide show
  1. {codd_dev-1.5.0 → codd_dev-1.6.0}/PKG-INFO +5 -1
  2. {codd_dev-1.5.0 → codd_dev-1.6.0}/README.md +3 -0
  3. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/__init__.py +1 -1
  4. codd_dev-1.6.0/codd/bridge.py +83 -0
  5. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/cli.py +140 -137
  6. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/extractor.py +11 -3
  7. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/mcp_server.py +17 -26
  8. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/measure.py +18 -13
  9. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/policy.py +14 -1
  10. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/require_plugins.py +10 -60
  11. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/templates/codd.yaml.tmpl +2 -2
  12. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/validator.py +46 -2
  13. {codd_dev-1.5.0 → codd_dev-1.6.0}/pyproject.toml +10 -9
  14. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/build-1.4.2.dist-info/licenses/LICENSE +0 -20
  15. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/certifi-2026.2.25.dist-info/licenses/LICENSE +0 -20
  16. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/cffi-2.0.0.dist-info/licenses/LICENSE +0 -23
  17. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/charset_normalizer-3.4.7.dist-info/licenses/LICENSE +0 -21
  18. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/cryptography-46.0.6.dist-info/licenses/LICENSE +0 -3
  19. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/id-1.6.1.dist-info/licenses/LICENSE +0 -202
  20. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/iniconfig-2.3.0.dist-info/licenses/LICENSE +0 -21
  21. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/jaraco.classes-3.4.0.dist-info/LICENSE +0 -17
  22. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/jaraco_context-6.1.2.dist-info/licenses/LICENSE +0 -18
  23. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/jaraco_functools-4.4.0.dist-info/licenses/LICENSE +0 -18
  24. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/jeepney-0.9.0.dist-info/licenses/LICENSE +0 -21
  25. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/keyring-25.7.0.dist-info/licenses/LICENSE +0 -18
  26. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/markdown_it_py-4.0.0.dist-info/licenses/LICENSE +0 -21
  27. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/mdurl-0.1.2.dist-info/LICENSE +0 -46
  28. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/more_itertools-11.0.1.dist-info/licenses/LICENSE +0 -19
  29. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/nh3-0.3.4.dist-info/licenses/LICENSE +0 -21
  30. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/packaging-26.0.dist-info/licenses/LICENSE +0 -3
  31. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/certifi/LICENSE +0 -20
  32. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/distro/LICENSE +0 -202
  33. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/packaging/LICENSE +0 -3
  34. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/pkg_resources/LICENSE +0 -17
  35. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/platformdirs/LICENSE +0 -21
  36. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/pygments/LICENSE +0 -25
  37. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/LICENSE +0 -21
  38. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/requests/LICENSE +0 -175
  39. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/resolvelib/LICENSE +0 -13
  40. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/rich/LICENSE +0 -19
  41. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/tomli/LICENSE +0 -21
  42. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/tomli_w/LICENSE +0 -21
  43. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip/_vendor/truststore/LICENSE +0 -21
  44. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/certifi/LICENSE +0 -20
  45. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/distro/LICENSE +0 -202
  46. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/packaging/LICENSE +0 -3
  47. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/pkg_resources/LICENSE +0 -17
  48. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/platformdirs/LICENSE +0 -21
  49. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/pygments/LICENSE +0 -25
  50. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/pyproject_hooks/LICENSE +0 -21
  51. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/requests/LICENSE +0 -175
  52. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/resolvelib/LICENSE +0 -13
  53. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/rich/LICENSE +0 -19
  54. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/tomli/LICENSE +0 -21
  55. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/tomli_w/LICENSE +0 -21
  56. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pip-26.0.1.dist-info/licenses/src/pip/_vendor/truststore/LICENSE +0 -21
  57. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pluggy-1.6.0.dist-info/licenses/LICENSE +0 -21
  58. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pycparser-3.0.dist-info/licenses/LICENSE +0 -27
  59. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pygments-2.20.0.dist-info/licenses/LICENSE +0 -25
  60. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pyproject_hooks-1.2.0.dist-info/LICENSE +0 -21
  61. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pytest-9.0.2.dist-info/licenses/LICENSE +0 -21
  62. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/pyyaml-6.0.3.dist-info/licenses/LICENSE +0 -20
  63. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/readme_renderer-44.0.dist-info/LICENSE +0 -174
  64. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/requests-2.33.1.dist-info/licenses/LICENSE +0 -175
  65. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/requests_toolbelt-1.0.0.dist-info/LICENSE +0 -13
  66. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/rfc3986-2.0.0.dist-info/LICENSE +0 -13
  67. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/rich-14.3.3.dist-info/licenses/LICENSE +0 -19
  68. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/secretstorage-3.5.0.dist-info/licenses/LICENSE +0 -25
  69. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/tree_sitter-0.25.2.dist-info/licenses/LICENSE +0 -21
  70. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/tree_sitter_java-0.23.5.dist-info/LICENSE +0 -21
  71. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/tree_sitter_python-0.25.0.dist-info/licenses/LICENSE +0 -21
  72. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/tree_sitter_typescript-0.23.2.dist-info/LICENSE +0 -21
  73. codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/twine-6.2.0.dist-info/licenses/LICENSE +0 -174
  74. codd_dev-1.5.0/LICENSE +0 -21
  75. codd_dev-1.5.0/codd/audit.py +0 -354
  76. codd_dev-1.5.0/codd/reviewer.py +0 -342
  77. codd_dev-1.5.0/codd/risk.py +0 -100
  78. codd_dev-1.5.0/codd/verifier.py +0 -679
  79. {codd_dev-1.5.0 → codd_dev-1.6.0}/.gitignore +0 -0
  80. {codd_dev-1.5.0/.venv-scanonly-209d2/lib/python3.12/site-packages/codd_dev-0.2.0a5.dist-info/licenses → codd_dev-1.6.0}/LICENSE +0 -0
  81. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/assembler.py +0 -0
  82. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/clustering.py +0 -0
  83. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/config.py +0 -0
  84. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/contracts.py +0 -0
  85. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/defaults.yaml +0 -0
  86. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/env_refs.py +0 -0
  87. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/extract_ai.py +0 -0
  88. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/generator.py +0 -0
  89. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/graph.py +0 -0
  90. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/hooks/__init__.py +0 -0
  91. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/hooks/pre-commit +0 -0
  92. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/implementer.py +0 -0
  93. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/inheritance.py +0 -0
  94. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/parsing.py +0 -0
  95. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/planner.py +0 -0
  96. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/propagate.py +0 -0
  97. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/propagator.py +0 -0
  98. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/require.py +0 -0
  99. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/restore.py +0 -0
  100. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/scanner.py +0 -0
  101. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/schema_refs.py +0 -0
  102. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/synth.py +0 -0
  103. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/templates/conventions.yaml.tmpl +0 -0
  104. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
  105. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/templates/doc_links.yaml.tmpl +0 -0
  106. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
  107. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
  108. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
  109. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
  110. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/templates/extracted/system-context.md.j2 +0 -0
  111. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/templates/gitignore.tmpl +0 -0
  112. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/templates/overrides.yaml.tmpl +0 -0
  113. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/traceability.py +0 -0
  114. {codd_dev-1.5.0 → codd_dev-1.6.0}/codd/wiring.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codd-dev
3
- Version: 1.5.0
3
+ Version: 1.6.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
+ Provides-Extra: ai
22
23
  Provides-Extra: api-parsers
23
24
  Requires-Dist: graphql-core>=3.2.0; extra == 'api-parsers'
24
25
  Provides-Extra: infra
@@ -262,6 +263,8 @@ All other commands (`scan`, `impact`, `generate`, etc.) automatically discover w
262
263
 
263
264
  Already have a codebase? CoDD provides a full brownfield workflow — from code extraction to design doc reconstruction.
264
265
 
266
+ Full walkthrough: [Harness as Code — A Guide to CoDD #2 Brownfield](https://zenn.dev/shio_shoppaize/articles/shogun-codd-brownfield?locale=en)
267
+
265
268
  ### AI-Powered Extraction (--ai)
266
269
 
267
270
  > **Note on presets**: `codd extract --ai` ships with a **baseline** extraction prompt. The extraction quality in published benchmarks (F1 0.953+) was achieved with a tuned preset and internal evaluation dataset — not the public baseline. The baseline uses the same workflow and output format, but results will vary depending on your codebase and prompt. Use `--prompt-file` to supply your own tuned prompt.
@@ -652,6 +655,7 @@ If CoDD can't manage itself, it shouldn't manage your project.
652
655
  - [dev.to: Harness as Code — Treating AI Workflows Like Infrastructure](https://dev.to/yohey-w/harness-as-code-treating-ai-workflows-like-infrastructure-27ni)
653
656
  - [dev.to: What Happens After "Spec First"](https://dev.to/yohey-w/codd-coherence-driven-development-what-happens-after-spec-first-514f)
654
657
  - [Zenn: Harness as Code — A Guide to CoDD #1 spec → design → code](https://zenn.dev/shio_shoppaize/articles/codd-greenfield-guide?locale=en)
658
+ - [Zenn: Harness as Code — A Guide to CoDD #2 Brownfield](https://zenn.dev/shio_shoppaize/articles/shogun-codd-brownfield?locale=en)
655
659
  - [Zenn: CoDD deep-dive](https://zenn.dev/shio_shoppaize/articles/shogun-codd-coherence?locale=en)
656
660
 
657
661
  ## License
@@ -225,6 +225,8 @@ All other commands (`scan`, `impact`, `generate`, etc.) automatically discover w
225
225
 
226
226
  Already have a codebase? CoDD provides a full brownfield workflow — from code extraction to design doc reconstruction.
227
227
 
228
+ Full walkthrough: [Harness as Code — A Guide to CoDD #2 Brownfield](https://zenn.dev/shio_shoppaize/articles/shogun-codd-brownfield?locale=en)
229
+
228
230
  ### AI-Powered Extraction (--ai)
229
231
 
230
232
  > **Note on presets**: `codd extract --ai` ships with a **baseline** extraction prompt. The extraction quality in published benchmarks (F1 0.953+) was achieved with a tuned preset and internal evaluation dataset — not the public baseline. The baseline uses the same workflow and output format, but results will vary depending on your codebase and prompt. Use `--prompt-file` to supply your own tuned prompt.
@@ -615,6 +617,7 @@ If CoDD can't manage itself, it shouldn't manage your project.
615
617
  - [dev.to: Harness as Code — Treating AI Workflows Like Infrastructure](https://dev.to/yohey-w/harness-as-code-treating-ai-workflows-like-infrastructure-27ni)
616
618
  - [dev.to: What Happens After "Spec First"](https://dev.to/yohey-w/codd-coherence-driven-development-what-happens-after-spec-first-514f)
617
619
  - [Zenn: Harness as Code — A Guide to CoDD #1 spec → design → code](https://zenn.dev/shio_shoppaize/articles/codd-greenfield-guide?locale=en)
620
+ - [Zenn: Harness as Code — A Guide to CoDD #2 Brownfield](https://zenn.dev/shio_shoppaize/articles/shogun-codd-brownfield?locale=en)
618
621
  - [Zenn: CoDD deep-dive](https://zenn.dev/shio_shoppaize/articles/shogun-codd-coherence?locale=en)
619
622
 
620
623
  ## License
@@ -1,3 +1,3 @@
1
1
  """CoDD — Coherence-Driven Development."""
2
2
 
3
- __version__ = "1.3.0"
3
+ __version__ = "1.6.0"
@@ -0,0 +1,83 @@
1
+ """Bridge helpers for optional codd-pro extensions and plugin registration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from importlib.metadata import entry_points
7
+ from typing import Any, Callable
8
+
9
+
10
+ PLUGIN_GROUP = "codd.plugins"
11
+ PRO_COMMAND_INSTALL_MESSAGE = (
12
+ "このコマンドは codd-pro に移動しました。"
13
+ "pip install codd-pro でインストールできます。"
14
+ )
15
+
16
+
17
+ @dataclass
18
+ class BridgeRegistry:
19
+ """Mutable registry populated by entry-point plugins."""
20
+
21
+ require_plugin: Any | None = None
22
+ validator_handler: Callable[..., Any] | None = None
23
+ policy_handler: Callable[..., Any] | None = None
24
+ risk_builder: Callable[..., Any] | None = None
25
+ command_handlers: dict[str, Callable[..., Any]] = field(default_factory=dict)
26
+ mcp_tools: dict[str, dict[str, Any]] = field(default_factory=dict)
27
+ mcp_handlers: dict[str, Callable[..., Any]] = field(default_factory=dict)
28
+
29
+ def register_require_plugin(self, plugin: Any) -> None:
30
+ self.require_plugin = plugin
31
+
32
+ def register_validator(self, handler: Callable[..., Any]) -> None:
33
+ self.validator_handler = handler
34
+
35
+ def register_policy(self, handler: Callable[..., Any]) -> None:
36
+ self.policy_handler = handler
37
+
38
+ def register_risk_builder(self, handler: Callable[..., Any]) -> None:
39
+ self.risk_builder = handler
40
+
41
+ def register_command(self, name: str, handler: Callable[..., Any]) -> None:
42
+ self.command_handlers[name] = handler
43
+
44
+ def register_mcp_tool(self, tool: dict[str, Any], handler: Callable[..., Any]) -> None:
45
+ name = str(tool.get("name") or "")
46
+ if not name:
47
+ raise ValueError("MCP tool registration requires a non-empty tool name")
48
+ self.mcp_tools[name] = tool
49
+ self.mcp_handlers[name] = handler
50
+
51
+
52
+ def _iter_plugin_entry_points():
53
+ try:
54
+ return tuple(entry_points(group=PLUGIN_GROUP))
55
+ except TypeError:
56
+ return tuple(entry_points().select(group=PLUGIN_GROUP))
57
+
58
+
59
+ def load_bridge_registry() -> BridgeRegistry:
60
+ """Load all registered bridge plugins, ignoring broken extensions."""
61
+ registry = BridgeRegistry()
62
+
63
+ for plugin_entry in _iter_plugin_entry_points():
64
+ try:
65
+ plugin = plugin_entry.load()
66
+ except Exception:
67
+ continue
68
+
69
+ register = getattr(plugin, "register", plugin)
70
+ if not callable(register):
71
+ continue
72
+
73
+ try:
74
+ register(registry)
75
+ except Exception:
76
+ continue
77
+
78
+ return registry
79
+
80
+
81
+ def get_command_handler(name: str) -> Callable[..., Any] | None:
82
+ """Return the registered handler for a Pro-only CLI command."""
83
+ return load_bridge_registry().command_handlers.get(name)
@@ -4,11 +4,24 @@ import click
4
4
  import json
5
5
  import os
6
6
  import shutil
7
- from pathlib import Path
8
-
9
- from codd.config import find_codd_dir
10
-
11
- TEMPLATES_DIR = Path(__file__).parent / "templates"
7
+ from pathlib import Path
8
+
9
+ from codd.bridge import PRO_COMMAND_INSTALL_MESSAGE, get_command_handler
10
+ from codd.config import find_codd_dir
11
+
12
+ TEMPLATES_DIR = Path(__file__).parent / "templates"
13
+
14
+
15
+ def _run_pro_command(name: str, **kwargs):
16
+ """Dispatch a Pro-only command when the bridge plugin is installed."""
17
+ handler = get_command_handler(name)
18
+ if handler is None:
19
+ click.echo(PRO_COMMAND_INSTALL_MESSAGE)
20
+ raise SystemExit(1)
21
+
22
+ result = handler(**kwargs)
23
+ if type(result) is int:
24
+ raise SystemExit(result)
12
25
 
13
26
 
14
27
  def _require_codd_dir(project_root: Path) -> Path:
@@ -20,6 +33,64 @@ def _require_codd_dir(project_root: Path) -> Path:
20
33
  return codd_dir
21
34
 
22
35
 
36
+ def _resolve_bootstrap_codd_dir(project_root: Path) -> Path:
37
+ """Choose a config dir for brownfield bootstrap without clobbering source code."""
38
+ existing = find_codd_dir(project_root)
39
+ if existing is not None:
40
+ return existing
41
+
42
+ hidden_dir = project_root / ".codd"
43
+ default_dir = project_root / "codd"
44
+ if hidden_dir.exists():
45
+ return hidden_dir
46
+ if default_dir.exists():
47
+ # Avoid writing config into projects whose source package is already named codd/.
48
+ return hidden_dir
49
+ return default_dir
50
+
51
+
52
+ def _format_yaml_list(items: list[str], *, indent: int = 4) -> str:
53
+ """Render a YAML list block for the simple template engine."""
54
+ if not items:
55
+ return " " * indent + "[]"
56
+ return "\n".join(f'{" " * indent}- "{item}"' for item in items)
57
+
58
+
59
+ def _display_path(path: Path, project_root: Path) -> str:
60
+ try:
61
+ return path.relative_to(project_root).as_posix()
62
+ except ValueError:
63
+ return path.as_posix()
64
+
65
+
66
+ def _ensure_bootstrap_codd_yaml(
67
+ project_root: Path,
68
+ *,
69
+ codd_dir: Path | None = None,
70
+ language: str,
71
+ source_dirs: list[str],
72
+ ) -> tuple[Path, bool]:
73
+ """Create a minimal codd.yaml after brownfield extract when none exists."""
74
+ codd_dir = codd_dir or _resolve_bootstrap_codd_dir(project_root)
75
+ config_path = codd_dir / "codd.yaml"
76
+ if config_path.exists():
77
+ return config_path, False
78
+
79
+ codd_dir.mkdir(parents=True, exist_ok=True)
80
+ codd_dir_name = _display_path(codd_dir, project_root)
81
+ _render_template(
82
+ "codd.yaml.tmpl",
83
+ config_path,
84
+ {
85
+ "project_name": project_root.name,
86
+ "language": language,
87
+ "source_dirs": _format_yaml_list(source_dirs),
88
+ "graph_path": f"{codd_dir_name}/scan",
89
+ },
90
+ )
91
+ return config_path, True
92
+
93
+
23
94
  @click.group()
24
95
  @click.version_option(package_name="codd-dev")
25
96
  def main():
@@ -66,6 +137,8 @@ def init(project_name: str, language: str, dest: str, requirements: str | None,
66
137
  _render_template("codd.yaml.tmpl", codd_dir / "codd.yaml", {
67
138
  "project_name": project_name,
68
139
  "language": language,
140
+ "source_dirs": _format_yaml_list(["src/"]),
141
+ "graph_path": f"{config_dir}/scan",
69
142
  })
70
143
  _render_template("gitignore.tmpl", codd_dir / ".gitignore", {})
71
144
 
@@ -380,54 +453,16 @@ def assemble(path: str, output_dir: str | None, ai_cmd: str | None):
380
453
  @main.command()
381
454
  @click.option("--path", default=".", help="Project root directory")
382
455
  @click.option("--sprint", default=None, type=click.IntRange(min=1), help="Sprint number to verify")
383
- def verify(path: str, sprint: int | None) -> None:
384
- """Run build + test verification and trace failures to design documents."""
385
- from codd.verifier import VerifyPreflightError, run_verify
386
-
387
- project_root = Path(path).resolve()
388
- codd_dir = _require_codd_dir(project_root)
389
-
390
- try:
391
- result = run_verify(project_root, sprint=sprint)
392
- except VerifyPreflightError as exc:
393
- click.echo(f"Preflight check failed: {exc}")
394
- raise SystemExit(1)
395
- except (FileNotFoundError, ValueError) as exc:
396
- click.echo(f"Error: {exc}")
397
- raise SystemExit(1)
398
-
399
- if result.typecheck.success:
400
- click.echo("Typecheck: PASS")
401
- else:
402
- click.echo(f"Typecheck: FAIL ({result.typecheck.error_count} errors)")
403
-
404
- if result.tests.success:
405
- click.echo(f"Tests: PASS ({result.tests.passed}/{result.tests.total})")
406
- else:
407
- click.echo(f"Tests: FAIL ({result.tests.failed} failed, {result.tests.passed} passed)")
408
-
409
- if result.design_refs:
410
- click.echo("\nDesign documents to review:")
411
- for ref in result.design_refs:
412
- click.echo(f" {ref.node_id} -> {ref.doc_path} (from {ref.source_file})")
413
- propagate_targets = tuple(dict.fromkeys(ref.node_id for ref in result.design_refs))
414
- if propagate_targets:
415
- click.echo("\nSuggested propagate targets:")
416
- for target in propagate_targets:
417
- click.echo(f" {target}")
418
-
419
- for warning in result.warnings:
420
- click.echo(f"Warning: {warning}")
421
-
422
- click.echo(f"\nReport: {result.report_path}")
423
- raise SystemExit(0 if result.success else 1)
456
+ def verify(path: str, sprint: int | None) -> None:
457
+ """Run build + test verification and trace failures to design documents."""
458
+ _run_pro_command("verify", path=path, sprint=sprint)
424
459
 
425
460
 
426
461
  @main.command()
427
462
  @click.option("--path", default=".", help="Project root directory")
428
463
  @click.option("--language", default=None, help="Override language detection (python/typescript/javascript/go — full support; java — symbols only)")
429
464
  @click.option("--source-dirs", default=None, help="Comma-separated source directories (default: auto-detect)")
430
- @click.option("--output", default=None, help="Output directory (default: codd/extracted/)")
465
+ @click.option("--output", default=None, help="Output directory (default: <config-dir>/extracted/)")
431
466
  @click.option("--ai", is_flag=True, default=False, help="Use AI-powered extraction (6-layer MECE design docs)")
432
467
  @click.option(
433
468
  "--ai-cmd",
@@ -446,14 +481,19 @@ def extract(path: str, language: str | None, source_dirs: str | None, output: st
446
481
  Default mode: static analysis (no AI, pure structural facts).
447
482
  With --ai: AI-powered 6-layer MECE extraction using claude --print.
448
483
 
449
- Output goes to codd/extracted/ as draft documents. Review and promote
450
- to codd/ when confirmed.
484
+ Output goes to the discovered CoDD config dir as draft documents
485
+ (`codd/extracted/` or `.codd/extracted/`). Review and promote
486
+ confirmed docs when ready.
451
487
  """
452
488
  project_root = Path(path).resolve()
489
+ bootstrap_codd_dir = _resolve_bootstrap_codd_dir(project_root)
490
+ dirs = [d.strip() for d in source_dirs.split(",") if d.strip()] if source_dirs else None
491
+ output_path = Path(output) if output else bootstrap_codd_dir / "extracted"
453
492
 
454
493
  if ai:
455
494
  from codd.extract_ai import run_extract_ai
456
- from codd.config import load_project_config, find_codd_dir
495
+ from codd.extractor import extract_facts
496
+ from codd.config import load_project_config
457
497
 
458
498
  # Resolve AI command
459
499
  if ai_cmd is None:
@@ -471,39 +511,60 @@ def extract(path: str, language: str | None, source_dirs: str | None, output: st
471
511
  click.echo(f"Scanning project...")
472
512
 
473
513
  try:
474
- result = run_extract_ai(project_root, ai_cmd, output, prompt_file=prompt_file)
514
+ result = run_extract_ai(project_root, ai_cmd, str(output_path), prompt_file=prompt_file)
475
515
  except Exception as exc:
476
516
  click.echo(f"Error: {exc}")
477
517
  raise SystemExit(1)
478
518
 
519
+ facts = extract_facts(project_root, language, dirs)
520
+ config_path, generated_config = _ensure_bootstrap_codd_yaml(
521
+ project_root,
522
+ codd_dir=bootstrap_codd_dir,
523
+ language=facts.language,
524
+ source_dirs=facts.source_dirs,
525
+ )
526
+ output_display = _display_path(result.output_dir, project_root)
527
+ config_display = _display_path(config_path, project_root)
528
+
479
529
  click.echo(f"\nExtracted: {result.module_count} source files analyzed")
480
- click.echo(f"Output: {result.output_dir}/")
530
+ click.echo(f"Output: {output_display}/")
481
531
  for f in result.generated_files:
482
532
  click.echo(f" {f.name}")
533
+ if generated_config:
534
+ click.echo(f"Generated: {config_display} (minimal brownfield config)")
483
535
 
484
536
  click.echo(f"\nNext steps:")
485
- click.echo(f" 1. Review generated docs in {result.output_dir}/")
486
- click.echo(f" 2. Promote confirmed docs: mv codd/extracted/*.md docs/design/")
537
+ click.echo(f" 1. Review generated docs in {output_display}/")
538
+ click.echo(f" 2. Promote confirmed docs: mv {output_display}/*.md docs/design/")
487
539
  click.echo(f" 3. Run: codd scan (to build the dependency graph)")
488
540
  else:
489
541
  from codd.extractor import run_extract
490
542
 
491
- dirs = [d.strip() for d in source_dirs.split(",")] if source_dirs else None
492
-
493
543
  try:
494
- result = run_extract(project_root, language, dirs, output)
544
+ result = run_extract(project_root, language, dirs, str(output_path))
495
545
  except Exception as exc:
496
546
  click.echo(f"Error: {exc}")
497
547
  raise SystemExit(1)
498
548
 
549
+ config_path, generated_config = _ensure_bootstrap_codd_yaml(
550
+ project_root,
551
+ codd_dir=bootstrap_codd_dir,
552
+ language=result.language,
553
+ source_dirs=result.source_dirs,
554
+ )
555
+ output_display = _display_path(result.output_dir, project_root)
556
+ config_display = _display_path(config_path, project_root)
557
+
499
558
  click.echo(f"Extracted: {result.module_count} modules from {result.total_files} files ({result.total_lines:,} lines)")
500
- click.echo(f"Output: {result.output_dir}/")
559
+ click.echo(f"Output: {output_display}/")
501
560
  for f in result.generated_files:
502
561
  click.echo(f" {f.relative_to(result.output_dir)}")
562
+ if generated_config:
563
+ click.echo(f"Generated: {config_display} (minimal brownfield config)")
503
564
 
504
565
  click.echo(f"\nNext steps:")
505
- click.echo(f" 1. Review generated docs in {result.output_dir}/")
506
- click.echo(f" 2. Promote confirmed docs: mv codd/extracted/*.md docs/design/")
566
+ click.echo(f" 1. Review generated docs in {output_display}/")
567
+ click.echo(f" 2. Promote confirmed docs: mv {output_display}/*.md docs/design/")
507
568
  click.echo(f" 3. Run: codd scan (to build the dependency graph)")
508
569
 
509
570
 
@@ -516,7 +577,7 @@ def extract(path: str, language: str | None, source_dirs: str | None, output: st
516
577
  default=None,
517
578
  help="Override AI CLI command (defaults to codd.yaml ai_command)",
518
579
  )
519
- def review(path: str, scope: str | None, as_json: bool, ai_cmd: str | None):
580
+ def review(path: str, scope: str | None, as_json: bool, ai_cmd: str | None):
520
581
  """Review design documents for content quality using AI.
521
582
 
522
583
  Evaluates artifacts against type-specific criteria (architecture soundness,
@@ -526,56 +587,7 @@ def review(path: str, scope: str | None, as_json: bool, ai_cmd: str | None):
526
587
  Without --scope: reviews all documents.
527
588
  With --scope: reviews a single document by node_id.
528
589
  """
529
- from codd.reviewer import run_review
530
-
531
- project_root = Path(path).resolve()
532
- _require_codd_dir(project_root)
533
-
534
- try:
535
- summary = run_review(project_root, scope=scope, ai_command=ai_cmd)
536
- except (FileNotFoundError, ValueError) as exc:
537
- click.echo(f"Error: {exc}")
538
- raise SystemExit(1)
539
-
540
- if not summary.results:
541
- click.echo("No documents found to review.")
542
- return
543
-
544
- if as_json:
545
- output = {
546
- "pass_count": summary.pass_count,
547
- "fail_count": summary.fail_count,
548
- "avg_score": round(summary.avg_score, 1),
549
- "results": [
550
- {
551
- "node_id": r.node_id,
552
- "path": r.path,
553
- "verdict": r.verdict,
554
- "score": r.score,
555
- "issues": [{"severity": i.severity, "message": i.message} for i in r.issues],
556
- "feedback": r.feedback,
557
- }
558
- for r in summary.results
559
- ],
560
- }
561
- click.echo(json.dumps(output, ensure_ascii=False, indent=2))
562
- else:
563
- for r in summary.results:
564
- icon = "PASS" if r.verdict == "PASS" else "FAIL"
565
- click.echo(f" [{icon}] {r.path} ({r.node_id}) — score: {r.score}")
566
- for issue in r.issues:
567
- click.echo(f" [{issue.severity}] {issue.message}")
568
- if r.feedback:
569
- # Show first 200 chars of feedback in summary mode
570
- preview = r.feedback[:200].replace("\n", " ")
571
- if len(r.feedback) > 200:
572
- preview += "..."
573
- click.echo(f" Feedback: {preview}")
574
-
575
- click.echo(f"\nSummary: {summary.pass_count} passed, {summary.fail_count} failed, avg score: {summary.avg_score:.0f}")
576
-
577
- exit_code = 0 if summary.fail_count == 0 else 1
578
- raise SystemExit(exit_code)
590
+ _run_pro_command("review", path=path, scope=scope, as_json=as_json, ai_cmd=ai_cmd)
579
591
 
580
592
 
581
593
  @main.command()
@@ -597,7 +609,7 @@ def validate(path: str):
597
609
  @click.option("--skip-review", is_flag=True, help="Skip AI review (faster, no AI cost)")
598
610
  @click.option("--output", default=None, help="Output file (default: stdout)")
599
611
  @click.option("--ai-cmd", default=None, help="Override AI command for review phase")
600
- def audit(diff: str, path: str, as_json: bool, skip_review: bool, output: str | None, ai_cmd: str | None):
612
+ def audit(diff: str, path: str, as_json: bool, skip_review: bool, output: str | None, ai_cmd: str | None):
601
613
  """Change review pack — validate + impact + policy + review in one report.
602
614
 
603
615
  Produces a consolidated audit report for PM/QA to make merge/release
@@ -607,31 +619,22 @@ def audit(diff: str, path: str, as_json: bool, skip_review: bool, output: str |
607
619
 
608
620
  Exit code: 0 = APPROVE, 1 = CONDITIONAL or REJECT.
609
621
  """
610
- from codd.audit import run_audit, format_audit_text, format_audit_json
611
-
612
- project_root = Path(path).resolve()
613
- _require_codd_dir(project_root)
614
-
615
- try:
616
- result = run_audit(
617
- project_root,
618
- diff_target=diff,
619
- ai_command=ai_cmd,
620
- skip_review=skip_review,
621
- )
622
- except (FileNotFoundError, ValueError) as exc:
623
- click.echo(f"Error: {exc}")
624
- raise SystemExit(1)
625
-
626
- text = format_audit_json(result) if as_json else format_audit_text(result)
627
-
628
- if output:
629
- Path(output).write_text(text, encoding="utf-8")
630
- click.echo(f"Audit report written to {output}")
631
- else:
632
- click.echo(text)
633
-
634
- raise SystemExit(0 if result.verdict == "APPROVE" else 1)
622
+ _run_pro_command(
623
+ "audit",
624
+ diff=diff,
625
+ path=path,
626
+ as_json=as_json,
627
+ skip_review=skip_review,
628
+ output=output,
629
+ ai_cmd=ai_cmd,
630
+ )
631
+
632
+
633
+ @main.command()
634
+ @click.option("--path", default=".", help="Project root directory")
635
+ def risk(path: str):
636
+ """Analyze change risk using the codd-pro extension pack."""
637
+ _run_pro_command("risk", path=path)
635
638
 
636
639
 
637
640
  @main.command()
@@ -17,6 +17,7 @@ from typing import Any
17
17
 
18
18
  import yaml
19
19
 
20
+ from codd.bridge import load_bridge_registry
20
21
  from codd.parsing import (
21
22
  BuildDepsExtractor,
22
23
  BuildDepsInfo,
@@ -120,6 +121,8 @@ class ExtractResult:
120
121
  module_count: int
121
122
  total_files: int
122
123
  total_lines: int
124
+ language: str
125
+ source_dirs: list[str]
123
126
 
124
127
 
125
128
  # ═══════════════════════════════════════════════════════════
@@ -202,9 +205,12 @@ def extract_facts(project_root: Path, language: str | None = None,
202
205
  from codd.wiring import build_runtime_wires
203
206
  build_runtime_wires(facts, project_root)
204
207
 
205
- # R5.4: Change risk scoring (depends on R4.3, R5.1)
206
- from codd.risk import build_change_risks
207
- build_change_risks(facts)
208
+ # R5.4: Change risk scoring is provided by codd-pro when installed.
209
+ risk_builder = load_bridge_registry().risk_builder
210
+ if risk_builder is not None:
211
+ risk_builder(facts)
212
+ else:
213
+ facts.change_risks = []
208
214
 
209
215
  # R8: Environment & config dependency detection
210
216
  from codd.env_refs import build_env_refs
@@ -981,6 +987,8 @@ def run_extract(project_root: Path, language: str | None = None,
981
987
  module_count=len(facts.modules),
982
988
  total_files=facts.total_files,
983
989
  total_lines=facts.total_lines,
990
+ language=facts.language,
991
+ source_dirs=facts.source_dirs,
984
992
  )
985
993
 
986
994
 
@@ -25,6 +25,8 @@ import json
25
25
  import sys
26
26
  from pathlib import Path
27
27
 
28
+ from codd.bridge import load_bridge_registry
29
+
28
30
 
29
31
  # ── JSON-RPC helpers ──────────────────────────────────────────────
30
32
 
@@ -71,21 +73,6 @@ TOOLS = [
71
73
  "required": [],
72
74
  },
73
75
  },
74
- {
75
- "name": "codd_audit",
76
- "description": "Run consolidated change review: validate + impact + policy. Returns APPROVE/CONDITIONAL/REJECT verdict with full details. Use this for PR review decisions.",
77
- "inputSchema": {
78
- "type": "object",
79
- "properties": {
80
- "diff_target": {
81
- "type": "string",
82
- "description": "Git ref to diff against (default: HEAD)",
83
- "default": "HEAD",
84
- },
85
- },
86
- "required": [],
87
- },
88
- },
89
76
  {
90
77
  "name": "codd_scan",
91
78
  "description": "Build or rebuild the dependency graph from frontmatter in design documents.",
@@ -172,13 +159,6 @@ def _handle_policy(project_root: Path, _args: dict) -> dict:
172
159
  return {"content": [{"type": "text", "text": format_policy_text(result)}]}
173
160
 
174
161
 
175
- def _handle_audit(project_root: Path, args: dict) -> dict:
176
- from codd.audit import run_audit, format_audit_json
177
- diff_target = args.get("diff_target", "HEAD")
178
- result = run_audit(project_root, diff_target=diff_target, skip_review=True)
179
- return {"content": [{"type": "text", "text": format_audit_json(result)}]}
180
-
181
-
182
162
  def _handle_scan(project_root: Path, _args: dict) -> dict:
183
163
  from codd.config import find_codd_dir, load_project_config
184
164
  from codd.scanner import scan_project
@@ -204,12 +184,23 @@ HANDLERS = {
204
184
  "codd_validate": _handle_validate,
205
185
  "codd_impact": _handle_impact,
206
186
  "codd_policy": _handle_policy,
207
- "codd_audit": _handle_audit,
208
187
  "codd_scan": _handle_scan,
209
188
  "codd_measure": _handle_measure,
210
189
  }
211
190
 
212
191
 
192
+ def _registered_tools() -> list[dict]:
193
+ tools = list(TOOLS)
194
+ tools.extend(load_bridge_registry().mcp_tools.values())
195
+ return tools
196
+
197
+
198
+ def _registered_handlers() -> dict[str, object]:
199
+ handlers = dict(HANDLERS)
200
+ handlers.update(load_bridge_registry().mcp_handlers)
201
+ return handlers
202
+
203
+
213
204
  # ── MCP Protocol Handler ─────────────────────────────────────────
214
205
 
215
206
  def handle_request(request: dict, project_root: Path) -> dict | None:
@@ -226,7 +217,7 @@ def handle_request(request: dict, project_root: Path) -> dict | None:
226
217
  },
227
218
  "serverInfo": {
228
219
  "name": "codd",
229
- "version": "1.4.0",
220
+ "version": "1.6.0",
230
221
  },
231
222
  })
232
223
 
@@ -234,13 +225,13 @@ def handle_request(request: dict, project_root: Path) -> dict | None:
234
225
  return None # No response for notifications
235
226
 
236
227
  if method == "tools/list":
237
- return _jsonrpc_response(req_id, {"tools": TOOLS})
228
+ return _jsonrpc_response(req_id, {"tools": _registered_tools()})
238
229
 
239
230
  if method == "tools/call":
240
231
  tool_name = params.get("name", "")
241
232
  arguments = params.get("arguments", {})
242
233
 
243
- handler = HANDLERS.get(tool_name)
234
+ handler = _registered_handlers().get(tool_name)
244
235
  if handler is None:
245
236
  return _jsonrpc_error(req_id, -32601, f"Unknown tool: {tool_name}")
246
237