codd-dev 1.5.1__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 (53) hide show
  1. {codd_dev-1.5.1 → codd_dev-1.6.0}/PKG-INFO +5 -1
  2. {codd_dev-1.5.1 → codd_dev-1.6.0}/README.md +3 -0
  3. {codd_dev-1.5.1 → 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.1 → codd_dev-1.6.0}/codd/cli.py +40 -123
  6. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/extractor.py +7 -3
  7. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/mcp_server.py +17 -26
  8. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/policy.py +14 -1
  9. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/require_plugins.py +10 -60
  10. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/validator.py +11 -1
  11. {codd_dev-1.5.1 → codd_dev-1.6.0}/pyproject.toml +10 -9
  12. codd_dev-1.5.1/codd/audit.py +0 -354
  13. codd_dev-1.5.1/codd/reviewer.py +0 -342
  14. codd_dev-1.5.1/codd/risk.py +0 -100
  15. codd_dev-1.5.1/codd/verifier.py +0 -679
  16. {codd_dev-1.5.1 → codd_dev-1.6.0}/.gitignore +0 -0
  17. {codd_dev-1.5.1 → codd_dev-1.6.0}/LICENSE +0 -0
  18. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/assembler.py +0 -0
  19. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/clustering.py +0 -0
  20. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/config.py +0 -0
  21. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/contracts.py +0 -0
  22. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/defaults.yaml +0 -0
  23. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/env_refs.py +0 -0
  24. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/extract_ai.py +0 -0
  25. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/generator.py +0 -0
  26. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/graph.py +0 -0
  27. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/hooks/__init__.py +0 -0
  28. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/hooks/pre-commit +0 -0
  29. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/implementer.py +0 -0
  30. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/inheritance.py +0 -0
  31. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/measure.py +0 -0
  32. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/parsing.py +0 -0
  33. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/planner.py +0 -0
  34. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/propagate.py +0 -0
  35. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/propagator.py +0 -0
  36. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/require.py +0 -0
  37. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/restore.py +0 -0
  38. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/scanner.py +0 -0
  39. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/schema_refs.py +0 -0
  40. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/synth.py +0 -0
  41. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/codd.yaml.tmpl +0 -0
  42. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/conventions.yaml.tmpl +0 -0
  43. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
  44. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/doc_links.yaml.tmpl +0 -0
  45. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
  46. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
  47. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
  48. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
  49. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/extracted/system-context.md.j2 +0 -0
  50. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/gitignore.tmpl +0 -0
  51. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/overrides.yaml.tmpl +0 -0
  52. {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/traceability.py +0 -0
  53. {codd_dev-1.5.1 → 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.1
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:
@@ -440,47 +453,9 @@ def assemble(path: str, output_dir: str | None, ai_cmd: str | None):
440
453
  @main.command()
441
454
  @click.option("--path", default=".", help="Project root directory")
442
455
  @click.option("--sprint", default=None, type=click.IntRange(min=1), help="Sprint number to verify")
443
- def verify(path: str, sprint: int | None) -> None:
444
- """Run build + test verification and trace failures to design documents."""
445
- from codd.verifier import VerifyPreflightError, run_verify
446
-
447
- project_root = Path(path).resolve()
448
- codd_dir = _require_codd_dir(project_root)
449
-
450
- try:
451
- result = run_verify(project_root, sprint=sprint)
452
- except VerifyPreflightError as exc:
453
- click.echo(f"Preflight check failed: {exc}")
454
- raise SystemExit(1)
455
- except (FileNotFoundError, ValueError) as exc:
456
- click.echo(f"Error: {exc}")
457
- raise SystemExit(1)
458
-
459
- if result.typecheck.success:
460
- click.echo("Typecheck: PASS")
461
- else:
462
- click.echo(f"Typecheck: FAIL ({result.typecheck.error_count} errors)")
463
-
464
- if result.tests.success:
465
- click.echo(f"Tests: PASS ({result.tests.passed}/{result.tests.total})")
466
- else:
467
- click.echo(f"Tests: FAIL ({result.tests.failed} failed, {result.tests.passed} passed)")
468
-
469
- if result.design_refs:
470
- click.echo("\nDesign documents to review:")
471
- for ref in result.design_refs:
472
- click.echo(f" {ref.node_id} -> {ref.doc_path} (from {ref.source_file})")
473
- propagate_targets = tuple(dict.fromkeys(ref.node_id for ref in result.design_refs))
474
- if propagate_targets:
475
- click.echo("\nSuggested propagate targets:")
476
- for target in propagate_targets:
477
- click.echo(f" {target}")
478
-
479
- for warning in result.warnings:
480
- click.echo(f"Warning: {warning}")
481
-
482
- click.echo(f"\nReport: {result.report_path}")
483
- 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)
484
459
 
485
460
 
486
461
  @main.command()
@@ -602,7 +577,7 @@ def extract(path: str, language: str | None, source_dirs: str | None, output: st
602
577
  default=None,
603
578
  help="Override AI CLI command (defaults to codd.yaml ai_command)",
604
579
  )
605
- 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):
606
581
  """Review design documents for content quality using AI.
607
582
 
608
583
  Evaluates artifacts against type-specific criteria (architecture soundness,
@@ -612,56 +587,7 @@ def review(path: str, scope: str | None, as_json: bool, ai_cmd: str | None):
612
587
  Without --scope: reviews all documents.
613
588
  With --scope: reviews a single document by node_id.
614
589
  """
615
- from codd.reviewer import run_review
616
-
617
- project_root = Path(path).resolve()
618
- _require_codd_dir(project_root)
619
-
620
- try:
621
- summary = run_review(project_root, scope=scope, ai_command=ai_cmd)
622
- except (FileNotFoundError, ValueError) as exc:
623
- click.echo(f"Error: {exc}")
624
- raise SystemExit(1)
625
-
626
- if not summary.results:
627
- click.echo("No documents found to review.")
628
- return
629
-
630
- if as_json:
631
- output = {
632
- "pass_count": summary.pass_count,
633
- "fail_count": summary.fail_count,
634
- "avg_score": round(summary.avg_score, 1),
635
- "results": [
636
- {
637
- "node_id": r.node_id,
638
- "path": r.path,
639
- "verdict": r.verdict,
640
- "score": r.score,
641
- "issues": [{"severity": i.severity, "message": i.message} for i in r.issues],
642
- "feedback": r.feedback,
643
- }
644
- for r in summary.results
645
- ],
646
- }
647
- click.echo(json.dumps(output, ensure_ascii=False, indent=2))
648
- else:
649
- for r in summary.results:
650
- icon = "PASS" if r.verdict == "PASS" else "FAIL"
651
- click.echo(f" [{icon}] {r.path} ({r.node_id}) — score: {r.score}")
652
- for issue in r.issues:
653
- click.echo(f" [{issue.severity}] {issue.message}")
654
- if r.feedback:
655
- # Show first 200 chars of feedback in summary mode
656
- preview = r.feedback[:200].replace("\n", " ")
657
- if len(r.feedback) > 200:
658
- preview += "..."
659
- click.echo(f" Feedback: {preview}")
660
-
661
- click.echo(f"\nSummary: {summary.pass_count} passed, {summary.fail_count} failed, avg score: {summary.avg_score:.0f}")
662
-
663
- exit_code = 0 if summary.fail_count == 0 else 1
664
- raise SystemExit(exit_code)
590
+ _run_pro_command("review", path=path, scope=scope, as_json=as_json, ai_cmd=ai_cmd)
665
591
 
666
592
 
667
593
  @main.command()
@@ -683,7 +609,7 @@ def validate(path: str):
683
609
  @click.option("--skip-review", is_flag=True, help="Skip AI review (faster, no AI cost)")
684
610
  @click.option("--output", default=None, help="Output file (default: stdout)")
685
611
  @click.option("--ai-cmd", default=None, help="Override AI command for review phase")
686
- 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):
687
613
  """Change review pack — validate + impact + policy + review in one report.
688
614
 
689
615
  Produces a consolidated audit report for PM/QA to make merge/release
@@ -693,31 +619,22 @@ def audit(diff: str, path: str, as_json: bool, skip_review: bool, output: str |
693
619
 
694
620
  Exit code: 0 = APPROVE, 1 = CONDITIONAL or REJECT.
695
621
  """
696
- from codd.audit import run_audit, format_audit_text, format_audit_json
697
-
698
- project_root = Path(path).resolve()
699
- _require_codd_dir(project_root)
700
-
701
- try:
702
- result = run_audit(
703
- project_root,
704
- diff_target=diff,
705
- ai_command=ai_cmd,
706
- skip_review=skip_review,
707
- )
708
- except (FileNotFoundError, ValueError) as exc:
709
- click.echo(f"Error: {exc}")
710
- raise SystemExit(1)
711
-
712
- text = format_audit_json(result) if as_json else format_audit_text(result)
713
-
714
- if output:
715
- Path(output).write_text(text, encoding="utf-8")
716
- click.echo(f"Audit report written to {output}")
717
- else:
718
- click.echo(text)
719
-
720
- 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)
721
638
 
722
639
 
723
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,
@@ -204,9 +205,12 @@ def extract_facts(project_root: Path, language: str | None = None,
204
205
  from codd.wiring import build_runtime_wires
205
206
  build_runtime_wires(facts, project_root)
206
207
 
207
- # R5.4: Change risk scoring (depends on R4.3, R5.1)
208
- from codd.risk import build_change_risks
209
- 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 = []
210
214
 
211
215
  # R8: Environment & config dependency detection
212
216
  from codd.env_refs import build_env_refs
@@ -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
 
@@ -14,6 +14,7 @@ from typing import Any
14
14
 
15
15
  import yaml
16
16
 
17
+ from codd.bridge import load_bridge_registry
17
18
  from codd.config import load_project_config
18
19
 
19
20
 
@@ -99,7 +100,7 @@ def load_policies(config: dict[str, Any]) -> list[PolicyRule]:
99
100
  return rules
100
101
 
101
102
 
102
- def run_policy(
103
+ def _run_policy_oss(
103
104
  project_root: Path,
104
105
  *,
105
106
  changed_files: list[str] | None = None,
@@ -172,6 +173,18 @@ def run_policy(
172
173
  return result
173
174
 
174
175
 
176
+ def run_policy(
177
+ project_root: Path,
178
+ *,
179
+ changed_files: list[str] | None = None,
180
+ ) -> PolicyResult:
181
+ """Run the OSS policy pack or delegate to a registered Pro policy pack."""
182
+ handler = load_bridge_registry().policy_handler
183
+ if handler is not None:
184
+ return handler(project_root, changed_files=changed_files, fallback=_run_policy_oss)
185
+ return _run_policy_oss(project_root, changed_files=changed_files)
186
+
187
+
175
188
  def format_policy_text(result: PolicyResult) -> str:
176
189
  """Format policy result as human-readable text."""
177
190
  lines: list[str] = []
@@ -1,30 +1,18 @@
1
1
  """CoDD require plugins — extension point for governance/calibration features.
2
2
 
3
- The plugin system allows require prompts to be enhanced with additional
4
- inference guidelines, tag systems, and output contracts. Plugins extend
5
- the built-in defaults with organisation-specific governance rules,
6
- calibration datasets, and approval workflows.
7
-
8
- Plugin resolution order:
9
- 1. Project-local: {codd_dir}/plugins/require.py
10
- 2. Site-wide: ~/.codd/plugins/require.py
11
- 3. Built-in: default guidelines (this module)
12
-
13
- Each plugin module may define:
14
- INFERENCE_TAGS: list[dict] — tag definitions (name, description)
15
- EVIDENCE_FORMAT: str | None — evidence citation format (overrides builtin)
16
- OUTPUT_SECTIONS: list[str] — additional output contract sections
17
- INFERENCE_GUIDELINES: list[str] — additional inference guidelines
3
+ Bridge plugins register themselves via the ``codd.plugins`` entry-point
4
+ group and can replace the built-in prompt enhancements by calling
5
+ ``registry.register_require_plugin(...)`` during plugin registration.
18
6
  """
19
7
 
20
8
  from __future__ import annotations
21
9
 
22
- import importlib.util
23
- import sys
24
10
  from dataclasses import dataclass, field
25
11
  from pathlib import Path
26
12
  from typing import Any
27
13
 
14
+ from codd.bridge import load_bridge_registry
15
+
28
16
 
29
17
  @dataclass
30
18
  class RequirePlugin:
@@ -76,53 +64,15 @@ BUILTIN_PLUGIN = RequirePlugin(
76
64
 
77
65
 
78
66
  def load_require_plugin(project_root: Path | None = None) -> RequirePlugin:
79
- """Load the require plugin, checking project-local and site-wide locations.
80
-
81
- Falls back to built-in OSS defaults if no plugin is found.
82
- """
83
- candidates: list[Path] = []
84
-
85
- # Project-local
86
- if project_root:
87
- from codd.config import find_codd_dir
88
-
89
- codd_dir = find_codd_dir(project_root)
90
- if codd_dir:
91
- candidates.append(codd_dir / "plugins" / "require.py")
92
-
93
- # Site-wide
94
- site_dir = Path.home() / ".codd" / "plugins"
95
- candidates.append(site_dir / "require.py")
96
-
97
- for path in candidates:
98
- if path.is_file():
99
- plugin = _load_plugin_from_file(path)
100
- if plugin is not None:
101
- return plugin
67
+ """Load the registered require plugin or fall back to the OSS defaults."""
68
+ del project_root # Reserved for API compatibility with older callers.
102
69
 
70
+ plugin = load_bridge_registry().require_plugin
71
+ if isinstance(plugin, RequirePlugin):
72
+ return plugin
103
73
  return BUILTIN_PLUGIN
104
74
 
105
75
 
106
- def _load_plugin_from_file(path: Path) -> RequirePlugin | None:
107
- """Load a plugin module from a file path."""
108
- try:
109
- spec = importlib.util.spec_from_file_location("codd_require_plugin", path)
110
- if spec is None or spec.loader is None:
111
- return None
112
- module = importlib.util.module_from_spec(spec)
113
- spec.loader.exec_module(module)
114
- except Exception:
115
- return None
116
-
117
- return RequirePlugin(
118
- name=getattr(module, "PLUGIN_NAME", path.stem),
119
- inference_tags=getattr(module, "INFERENCE_TAGS", _BUILTIN_TAGS),
120
- evidence_format=getattr(module, "EVIDENCE_FORMAT", None),
121
- output_sections=getattr(module, "OUTPUT_SECTIONS", []),
122
- inference_guidelines=getattr(module, "INFERENCE_GUIDELINES", _BUILTIN_GUIDELINES),
123
- )
124
-
125
-
126
76
  def build_tag_instructions(plugin: RequirePlugin) -> list[str]:
127
77
  """Build the tag instruction lines for the prompt."""
128
78
  tag_names = ", ".join(t["name"] for t in plugin.inference_tags)
@@ -10,6 +10,8 @@ from typing import Any
10
10
 
11
11
  import yaml
12
12
 
13
+ from codd.bridge import load_bridge_registry
14
+
13
15
 
14
16
  NODE_ID_PATTERN = re.compile(r"^(?P<prefix>[a-z_]+):(?P<name>.+)$")
15
17
  ALLOWED_NODE_PREFIXES = {
@@ -126,7 +128,7 @@ def run_validate(project_root: Path, codd_dir: Path) -> int:
126
128
  return result.exit_code
127
129
 
128
130
 
129
- def validate_project(project_root: Path, codd_dir: Path | None = None) -> ValidationResult:
131
+ def _validate_project_oss(project_root: Path, codd_dir: Path | None = None) -> ValidationResult:
130
132
  """Validate CoDD frontmatter, references, wave config, and dependency cycles."""
131
133
  codd_dir = codd_dir or (project_root / "codd")
132
134
  config_path = codd_dir / "codd.yaml"
@@ -262,6 +264,14 @@ def validate_project(project_root: Path, codd_dir: Path | None = None) -> Valida
262
264
  return result
263
265
 
264
266
 
267
+ def validate_project(project_root: Path, codd_dir: Path | None = None) -> ValidationResult:
268
+ """Validate the project, delegating to a Pro bridge when registered."""
269
+ handler = load_bridge_registry().validator_handler
270
+ if handler is not None:
271
+ return handler(project_root, codd_dir, _validate_project_oss)
272
+ return _validate_project_oss(project_root, codd_dir)
273
+
274
+
265
275
  @dataclass
266
276
  class FrontmatterParseResult:
267
277
  codd: dict[str, Any] | None = None
@@ -2,9 +2,9 @@
2
2
  requires = ["hatchling"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
- [project]
6
- name = "codd-dev"
7
- version = "1.5.1"
5
+ [project]
6
+ name = "codd-dev"
7
+ version = "1.6.0"
8
8
  description = "CoDD: Coherence-Driven Development — cross-artifact change impact analysis"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -27,12 +27,13 @@ dependencies = [
27
27
  "tomli>=2.0.1; python_version < '3.11'",
28
28
  ]
29
29
 
30
- [project.optional-dependencies]
31
- api-parsers = [
32
- "graphql-core>=3.2.0",
33
- ]
34
- infra = [
35
- "python-hcl2>=7.0.0",
30
+ [project.optional-dependencies]
31
+ ai = []
32
+ api-parsers = [
33
+ "graphql-core>=3.2.0",
34
+ ]
35
+ infra = [
36
+ "python-hcl2>=7.0.0",
36
37
  ]
37
38
  tree-sitter = [
38
39
  "tree-sitter>=0.25.0,<0.26.0",