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.
- {codd_dev-1.5.1 → codd_dev-1.6.0}/PKG-INFO +5 -1
- {codd_dev-1.5.1 → codd_dev-1.6.0}/README.md +3 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/__init__.py +1 -1
- codd_dev-1.6.0/codd/bridge.py +83 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/cli.py +40 -123
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/extractor.py +7 -3
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/mcp_server.py +17 -26
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/policy.py +14 -1
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/require_plugins.py +10 -60
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/validator.py +11 -1
- {codd_dev-1.5.1 → codd_dev-1.6.0}/pyproject.toml +10 -9
- codd_dev-1.5.1/codd/audit.py +0 -354
- codd_dev-1.5.1/codd/reviewer.py +0 -342
- codd_dev-1.5.1/codd/risk.py +0 -100
- codd_dev-1.5.1/codd/verifier.py +0 -679
- {codd_dev-1.5.1 → codd_dev-1.6.0}/.gitignore +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/LICENSE +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/assembler.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/clustering.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/config.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/contracts.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/defaults.yaml +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/env_refs.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/extract_ai.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/generator.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/graph.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/hooks/__init__.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/hooks/pre-commit +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/implementer.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/inheritance.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/measure.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/parsing.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/planner.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/propagate.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/propagator.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/require.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/restore.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/scanner.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/schema_refs.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/synth.py +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/codd.yaml.tmpl +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/conventions.yaml.tmpl +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/doc_links.yaml.tmpl +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/extracted/system-context.md.j2 +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/gitignore.tmpl +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/templates/overrides.yaml.tmpl +0 -0
- {codd_dev-1.5.1 → codd_dev-1.6.0}/codd/traceability.py +0 -0
- {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.
|
|
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
|
|
@@ -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.
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
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":
|
|
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 =
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
|
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
|
+
[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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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",
|