codd-dev 0.2.0a1__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """CoDD — Coherence-Driven Development."""
2
+
3
+ __version__ = "0.2.0a1"
codd/cli.py ADDED
@@ -0,0 +1,344 @@
1
+ """CoDD CLI — codd init / scan / impact / plan."""
2
+
3
+ import click
4
+ import json
5
+ import os
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
10
+
11
+
12
+ @click.group()
13
+ @click.version_option(package_name="shogun-codd")
14
+ def main():
15
+ """CoDD: Coherence-Driven Development."""
16
+ pass
17
+
18
+
19
+ @main.command()
20
+ @click.option("--project-name", prompt="Project name", help="Name of the project")
21
+ @click.option("--language", prompt="Primary language", help="Primary language (python/typescript/java/go)")
22
+ @click.option("--dest", default=".", help="Destination directory (default: current dir)")
23
+ def init(project_name: str, language: str, dest: str):
24
+ """Initialize CoDD in a project directory."""
25
+ dest_path = Path(dest).resolve()
26
+ codd_dir = dest_path / "codd"
27
+
28
+ if codd_dir.exists():
29
+ click.echo(f"Error: {codd_dir} already exists.")
30
+ raise SystemExit(1)
31
+
32
+ # Create directory structure
33
+ (codd_dir / "reports").mkdir(parents=True)
34
+ (codd_dir / "scan").mkdir(exist_ok=True)
35
+
36
+ # Copy templates
37
+ _render_template("codd.yaml.tmpl", codd_dir / "codd.yaml", {
38
+ "project_name": project_name,
39
+ "language": language,
40
+ })
41
+ _render_template("gitignore.tmpl", codd_dir / ".gitignore", {})
42
+
43
+ # Version file
44
+ (dest_path / ".codd_version").write_text("0.2.0\n")
45
+
46
+ click.echo(f"CoDD initialized in {codd_dir}")
47
+ click.echo(f" codd.yaml — project config")
48
+ click.echo(f" scan/ — JSONL scan output (nodes.jsonl, edges.jsonl)")
49
+ click.echo(f"")
50
+ click.echo(f"Next: Add codd frontmatter to your documents (docs/*.md)")
51
+ click.echo(f"Then: codd scan → builds scan/*.jsonl from all frontmatter")
52
+
53
+
54
+ @main.command()
55
+ @click.option("--path", default=".", help="Project root directory")
56
+ def scan(path: str):
57
+ """Scan codebase and update dependency graph (Stage 1)."""
58
+ from codd.scanner import run_scan
59
+ project_root = Path(path).resolve()
60
+ codd_dir = project_root / "codd"
61
+
62
+ if not codd_dir.exists():
63
+ click.echo("Error: codd/ not found. Run 'codd init' first.")
64
+ raise SystemExit(1)
65
+
66
+ run_scan(project_root, codd_dir)
67
+
68
+
69
+ @main.command()
70
+ @click.option("--diff", default="HEAD~1", help="Git diff target (default: HEAD~1)")
71
+ @click.option("--path", default=".", help="Project root directory")
72
+ @click.option("--output", default=None, help="Output file (default: stdout)")
73
+ def impact(diff: str, path: str, output: str):
74
+ """Analyze change impact from git diff."""
75
+ from codd.propagate import run_impact
76
+ project_root = Path(path).resolve()
77
+ codd_dir = project_root / "codd"
78
+
79
+ if not codd_dir.exists():
80
+ click.echo("Error: codd/ not found. Run 'codd init' first.")
81
+ raise SystemExit(1)
82
+
83
+ run_impact(project_root, codd_dir, diff, output)
84
+
85
+
86
+ @main.command()
87
+ @click.option("--wave", required=True, type=click.IntRange(min=1), help="Wave number to generate")
88
+ @click.option("--path", default=".", help="Project root directory")
89
+ @click.option("--force", is_flag=True, help="Overwrite existing files")
90
+ @click.option(
91
+ "--ai-cmd",
92
+ default=None,
93
+ help="Override AI CLI command (defaults to codd.yaml ai_command or 'claude --print')",
94
+ )
95
+ def generate(wave: int, path: str, force: bool, ai_cmd: str | None):
96
+ """Generate CoDD documents for a specific wave."""
97
+ from codd.generator import generate_wave
98
+
99
+ project_root = Path(path).resolve()
100
+ codd_dir = project_root / "codd"
101
+
102
+ if not codd_dir.exists():
103
+ click.echo("Error: codd/ not found. Run 'codd init' first.")
104
+ raise SystemExit(1)
105
+
106
+ try:
107
+ results = generate_wave(project_root, wave, force=force, ai_command=ai_cmd)
108
+ except (FileNotFoundError, ValueError) as exc:
109
+ click.echo(f"Error: {exc}")
110
+ raise SystemExit(1)
111
+
112
+ generated = 0
113
+ skipped = 0
114
+
115
+ for result in results:
116
+ rel_path = result.path.relative_to(project_root).as_posix()
117
+ click.echo(f"{result.status.capitalize()}: {rel_path} ({result.node_id})")
118
+ if result.status == "generated":
119
+ generated += 1
120
+ else:
121
+ skipped += 1
122
+
123
+ click.echo(f"Wave {wave}: {generated} generated, {skipped} skipped")
124
+
125
+
126
+ @main.command()
127
+ @click.option("--sprint", required=True, type=click.IntRange(min=1), help="Sprint number to implement")
128
+ @click.option("--path", default=".", help="Project root directory")
129
+ @click.option("--task", default=None, help="Generate only one task by task ID or title match")
130
+ @click.option(
131
+ "--ai-cmd",
132
+ default=None,
133
+ help="Override AI CLI command (defaults to codd.yaml ai_command or merged CoDD defaults)",
134
+ )
135
+ def implement(sprint: int, path: str, task: str | None, ai_cmd: str | None):
136
+ """Generate implementation code for a specific sprint."""
137
+ from codd.implementer import implement_sprint
138
+
139
+ project_root = Path(path).resolve()
140
+ codd_dir = project_root / "codd"
141
+
142
+ if not codd_dir.exists():
143
+ click.echo("Error: codd/ not found. Run 'codd init' first.")
144
+ raise SystemExit(1)
145
+
146
+ try:
147
+ results = implement_sprint(project_root, sprint, task=task, ai_command=ai_cmd)
148
+ except (FileNotFoundError, ValueError) as exc:
149
+ click.echo(f"Error: {exc}")
150
+ raise SystemExit(1)
151
+
152
+ generated_files = 0
153
+ for result in results:
154
+ for generated_file in result.generated_files:
155
+ rel_path = generated_file.relative_to(project_root)
156
+ click.echo(f"Generated: {rel_path} ({result.task_id})")
157
+ generated_files += 1
158
+
159
+ click.echo(f"Sprint {sprint}: {generated_files} files generated across {len(results)} task(s)")
160
+
161
+
162
+ @main.command()
163
+ @click.option("--path", default=".", help="Project root directory")
164
+ @click.option("--sprint", default=None, type=click.IntRange(min=1), help="Sprint number to verify")
165
+ def verify(path: str, sprint: int | None) -> None:
166
+ """Run build + test verification and trace failures to design documents."""
167
+ from codd.verifier import VerifyPreflightError, run_verify
168
+
169
+ project_root = Path(path).resolve()
170
+ codd_dir = project_root / "codd"
171
+
172
+ if not codd_dir.exists():
173
+ click.echo("Error: codd/ not found. Run 'codd init' first.")
174
+ raise SystemExit(1)
175
+
176
+ try:
177
+ result = run_verify(project_root, sprint=sprint)
178
+ except VerifyPreflightError as exc:
179
+ click.echo(f"Preflight check failed: {exc}")
180
+ raise SystemExit(1)
181
+ except (FileNotFoundError, ValueError) as exc:
182
+ click.echo(f"Error: {exc}")
183
+ raise SystemExit(1)
184
+
185
+ if result.typecheck.success:
186
+ click.echo("Typecheck: PASS")
187
+ else:
188
+ click.echo(f"Typecheck: FAIL ({result.typecheck.error_count} errors)")
189
+
190
+ if result.tests.success:
191
+ click.echo(f"Tests: PASS ({result.tests.passed}/{result.tests.total})")
192
+ else:
193
+ click.echo(f"Tests: FAIL ({result.tests.failed} failed, {result.tests.passed} passed)")
194
+
195
+ if result.design_refs:
196
+ click.echo("\nDesign documents to review:")
197
+ for ref in result.design_refs:
198
+ click.echo(f" {ref.node_id} -> {ref.doc_path} (from {ref.source_file})")
199
+ propagate_targets = tuple(dict.fromkeys(ref.node_id for ref in result.design_refs))
200
+ if propagate_targets:
201
+ click.echo("\nSuggested propagate targets:")
202
+ for target in propagate_targets:
203
+ click.echo(f" {target}")
204
+
205
+ for warning in result.warnings:
206
+ click.echo(f"Warning: {warning}")
207
+
208
+ click.echo(f"\nReport: {result.report_path}")
209
+ raise SystemExit(0 if result.success else 1)
210
+
211
+
212
+ @main.command()
213
+ @click.option("--path", default=".", help="Project root directory")
214
+ def validate(path: str):
215
+ """Validate CoDD frontmatter and dependency references."""
216
+ from codd.validator import run_validate
217
+
218
+ project_root = Path(path).resolve()
219
+ codd_dir = project_root / "codd"
220
+
221
+ if not codd_dir.exists():
222
+ click.echo("Error: codd/ not found. Run 'codd init' first.")
223
+ raise SystemExit(1)
224
+
225
+ raise SystemExit(run_validate(project_root, codd_dir))
226
+
227
+
228
+ @main.command()
229
+ @click.option("--path", default=".", help="Project root directory")
230
+ @click.option("--json", "as_json", is_flag=True, help="Output plan as JSON")
231
+ @click.option("--init", "initialize", is_flag=True, help="Generate wave_config from requirement docs")
232
+ @click.option("--force", is_flag=True, help="Overwrite existing wave_config during --init")
233
+ @click.option(
234
+ "--ai-cmd",
235
+ default=None,
236
+ help="Override AI CLI command for --init (defaults to codd.yaml ai_command or 'claude --print')",
237
+ )
238
+ def plan(path: str, as_json: bool, initialize: bool, force: bool, ai_cmd: str | None):
239
+ """Show wave execution status from configured artifacts."""
240
+ from codd.planner import build_plan, plan_init, plan_to_dict, render_plan_text
241
+
242
+ project_root = Path(path).resolve()
243
+ codd_dir = project_root / "codd"
244
+
245
+ if not codd_dir.exists():
246
+ click.echo("Error: codd/ not found. Run 'codd init' first.")
247
+ raise SystemExit(1)
248
+
249
+ if initialize:
250
+ if as_json:
251
+ raise click.BadOptionUsage("json", "--json cannot be used with --init")
252
+
253
+ try:
254
+ result = plan_init(project_root, force=force, ai_command=ai_cmd)
255
+ except FileExistsError:
256
+ if not click.confirm("codd.yaml already contains wave_config. Overwrite it?", default=False):
257
+ click.echo("Aborted: existing wave_config preserved.")
258
+ raise SystemExit(1)
259
+ result = plan_init(project_root, force=True, ai_command=ai_cmd)
260
+ except (FileNotFoundError, ValueError) as exc:
261
+ click.echo(f"Error: {exc}")
262
+ raise SystemExit(1)
263
+
264
+ artifact_count = sum(len(entries) for entries in result.wave_config.values())
265
+ config_rel_path = Path(result.config_path).relative_to(project_root).as_posix()
266
+ click.echo(
267
+ f"Initialized wave_config in {config_rel_path} from {len(result.requirement_paths)} requirement document(s)."
268
+ )
269
+ click.echo(f"Generated {artifact_count} artifact(s) across {len(result.wave_config)} wave(s).")
270
+ return
271
+
272
+ if force:
273
+ raise click.BadOptionUsage("force", "--force requires --init")
274
+ if ai_cmd is not None:
275
+ raise click.BadOptionUsage("ai_cmd", "--ai-cmd requires --init")
276
+
277
+ try:
278
+ result = build_plan(project_root)
279
+ except (FileNotFoundError, ValueError) as exc:
280
+ click.echo(f"Error: {exc}")
281
+ raise SystemExit(1)
282
+
283
+ if as_json:
284
+ click.echo(json.dumps(plan_to_dict(result), ensure_ascii=False, indent=2))
285
+ return
286
+
287
+ click.echo(render_plan_text(result))
288
+
289
+
290
+ @main.group()
291
+ def hooks():
292
+ """Manage Git hook integration."""
293
+ pass
294
+
295
+
296
+ @hooks.command("install")
297
+ @click.option("--path", default=".", help="Project root directory")
298
+ def hooks_install(path: str):
299
+ """Install the CoDD pre-commit hook into .git/hooks."""
300
+ from codd.hooks import install_pre_commit_hook
301
+
302
+ project_root = Path(path).resolve()
303
+
304
+ try:
305
+ hook_path, installed = install_pre_commit_hook(project_root)
306
+ except (FileNotFoundError, FileExistsError) as exc:
307
+ click.echo(f"Error: {exc}")
308
+ raise SystemExit(1)
309
+
310
+ if installed:
311
+ click.echo(f"Installed pre-commit hook: {hook_path}")
312
+ else:
313
+ click.echo(f"Pre-commit hook already installed: {hook_path}")
314
+
315
+
316
+ @hooks.command("run-pre-commit", hidden=True)
317
+ @click.option("--path", default=".", help="Project root directory")
318
+ def hooks_run_pre_commit(path: str):
319
+ """Run CoDD pre-commit checks."""
320
+ from codd.hooks import run_pre_commit
321
+
322
+ project_root = Path(path).resolve()
323
+ raise SystemExit(run_pre_commit(project_root))
324
+
325
+
326
+ def _render_template(template_name: str, dest: Path, variables: dict):
327
+ """Simple template rendering (replace {{key}} with value)."""
328
+ tmpl_path = TEMPLATES_DIR / template_name
329
+ if not tmpl_path.exists():
330
+ # Create empty file if template doesn't exist yet
331
+ dest.write_text(f"# TODO: template {template_name} not yet created\n")
332
+ return
333
+
334
+ content = tmpl_path.read_text()
335
+ for key, value in variables.items():
336
+ content = content.replace(f"{{{{{key}}}}}", value)
337
+ dest.write_text(content)
338
+
339
+
340
+ # _init_graph_db removed — JSONL files are created on first scan
341
+
342
+
343
+ if __name__ == "__main__":
344
+ main()
codd/config.py ADDED
@@ -0,0 +1,62 @@
1
+ """CoDD configuration loader with defaults + project overrides."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+
13
+ DEFAULTS_PATH = Path(__file__).with_name("defaults.yaml")
14
+
15
+
16
+ def load_project_config(project_root: Path) -> dict[str, Any]:
17
+ """Load CoDD defaults and merge project-local overrides."""
18
+ config_path = project_root / "codd" / "codd.yaml"
19
+ if not config_path.exists():
20
+ raise FileNotFoundError(f"{config_path} not found")
21
+
22
+ defaults = _read_yaml_mapping(DEFAULTS_PATH)
23
+ project = _read_yaml_mapping(config_path)
24
+ return _deep_merge(defaults, project)
25
+
26
+
27
+ def _read_yaml_mapping(path: Path) -> dict[str, Any]:
28
+ if not path.exists():
29
+ raise FileNotFoundError(f"{path} not found")
30
+
31
+ payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
32
+ if not isinstance(payload, dict):
33
+ raise ValueError(f"{path.name} must contain a YAML mapping")
34
+ return payload
35
+
36
+
37
+ def _deep_merge(defaults: Any, project: Any) -> Any:
38
+ if isinstance(defaults, dict) and isinstance(project, dict):
39
+ merged = deepcopy(defaults)
40
+ for key, value in project.items():
41
+ if key in merged:
42
+ merged[key] = _deep_merge(merged[key], value)
43
+ else:
44
+ merged[key] = deepcopy(value)
45
+ return merged
46
+
47
+ if isinstance(defaults, list) and isinstance(project, list):
48
+ return _merge_lists(defaults, project)
49
+
50
+ return deepcopy(project)
51
+
52
+
53
+ def _merge_lists(defaults: list[Any], project: list[Any]) -> list[Any]:
54
+ merged: list[Any] = []
55
+ seen: set[str] = set()
56
+ for value in [*defaults, *project]:
57
+ serialized = json.dumps(value, ensure_ascii=False, sort_keys=True)
58
+ if serialized in seen:
59
+ continue
60
+ seen.add(serialized)
61
+ merged.append(deepcopy(value))
62
+ return merged
codd/defaults.yaml ADDED
@@ -0,0 +1,30 @@
1
+ version: "0.2.0a1"
2
+ project:
3
+ frameworks: []
4
+ ai_command: "claude --print"
5
+ coding_principles: null
6
+ scan:
7
+ source_dirs:
8
+ - "src/"
9
+ test_dirs:
10
+ - "tests/"
11
+ doc_dirs:
12
+ - "docs/"
13
+ config_files: []
14
+ exclude:
15
+ - "**/node_modules/**"
16
+ - "**/__pycache__/**"
17
+ - "**/dist/**"
18
+ graph:
19
+ store: "jsonl"
20
+ path: "codd/scan"
21
+ bands:
22
+ green:
23
+ min_confidence: 0.90
24
+ min_evidence_count: 2
25
+ amber:
26
+ min_confidence: 0.50
27
+ propagation:
28
+ max_depth: 10
29
+ stop_at_contract_boundary: true
30
+ conventions: []