codd-dev 1.9.2__tar.gz → 1.11.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.9.2 → codd_dev-1.11.0}/PKG-INFO +141 -1
- {codd_dev-1.9.2 → codd_dev-1.11.0}/README.md +140 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/cli.py +113 -17
- codd_dev-1.11.0/codd/drift.py +157 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/extractor.py +41 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/implementer.py +135 -16
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/parsing.py +254 -0
- codd_dev-1.11.0/codd/routes_extractor.py +120 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/scanner.py +55 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/pyproject.toml +1 -1
- {codd_dev-1.9.2 → codd_dev-1.11.0}/.gitignore +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/LICENSE +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/__init__.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/assembler.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/bridge.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/clustering.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/config.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/contracts.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/defaults.yaml +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/e2e_runner.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/env_refs.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/extract_ai.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/fixer.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/generator.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/graph.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/hooks/__init__.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/hooks/pre-commit +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/inheritance.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/mcp_server.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/measure.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/planner.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/policy.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/propagate.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/propagator.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/repair_slice.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/require.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/require_plugins.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/restore.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/schema_refs.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/synth.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/templates/codd.yaml.tmpl +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/templates/conventions.yaml.tmpl +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/templates/data_dependencies.yaml.tmpl +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/templates/doc_links.yaml.tmpl +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/templates/extracted/api-contract.md.j2 +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/templates/extracted/architecture-overview.md.j2 +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/templates/extracted/module-detail.md.j2 +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/templates/extracted/schema-design.md.j2 +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/templates/extracted/system-context.md.j2 +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/templates/gitignore.tmpl +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/templates/overrides.yaml.tmpl +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/traceability.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/validator.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/codd/wiring.py +0 -0
- {codd_dev-1.9.2 → codd_dev-1.11.0}/docs/requirements/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codd-dev
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.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
|
|
@@ -60,6 +60,19 @@ Description-Content-Type: text/markdown
|
|
|
60
60
|
pip install codd-dev
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
## 🆕 v1.11.0 — Filesystem-Routing Aware Drift Detection
|
|
64
|
+
|
|
65
|
+
CoDD now understands filesystem-routing frameworks (Next.js, SvelteKit, Nuxt, Astro, Remix) and can detect URL drift between your design docs and actual implementation.
|
|
66
|
+
|
|
67
|
+
- 📐 `FileSystemRouteExtractor` — endpoint nodes from directory structure
|
|
68
|
+
- 🔗 `DocumentUrlLinker` — auto-link design doc URLs to endpoints
|
|
69
|
+
- 🔍 `codd drift` — find URL gaps between design and implementation
|
|
70
|
+
- 🎨 `codd extract --layer routes` — reverse-engineer screen-flow diagrams
|
|
71
|
+
|
|
72
|
+
See [Filesystem Routing Adapter Recipes](#filesystem-routing-adapter-recipes) for setup.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
63
76
|
**v1.9.0** — `codd implement` now supports **multi-AI engine** (Claude stdout + Codex file-writing) and **automatic parallel execution** within phases via git worktree isolation. Phase milestone format (`#### M1.1`) supported. AI command timeout extended to 1 hour for heavy reasoning models. SWE-bench Verified: **73/73 = 100%** resolved.
|
|
64
77
|
|
|
65
78
|
---
|
|
@@ -325,6 +338,133 @@ codd init --config-dir .codd --project-name "my-project" --language "python"
|
|
|
325
338
|
|
|
326
339
|
All other commands (`scan`, `impact`, `generate`, etc.) automatically discover whichever config directory exists — `codd/` first, then `.codd/`. No extra flags needed.
|
|
327
340
|
|
|
341
|
+
## Filesystem Routing Adapter Recipes
|
|
342
|
+
|
|
343
|
+
CoDD detects URL drift between your design documents and implementation
|
|
344
|
+
using framework conventions declared in `codd.yaml`.
|
|
345
|
+
These recipes cover the five major filesystem-routing frameworks.
|
|
346
|
+
|
|
347
|
+
| Framework | Base dir | Page glob | API glob | Dynamic segment |
|
|
348
|
+
|------------------------|---------------|------------------|---------------------------|-----------------------|
|
|
349
|
+
| Next.js (App Router) | `app/` | `page.{tsx,jsx}` | `route.{ts,js}` | `[param]` → `:param` |
|
|
350
|
+
| Next.js (Pages Router) | `pages/` | `*.{tsx,jsx}` | `api/**/*.{ts,js}` | `[param]` → `:param` |
|
|
351
|
+
| SvelteKit | `src/routes/` | `+page.svelte` | `+server.{ts,js}` | `[param]` → `:param` |
|
|
352
|
+
| Nuxt 3 | `pages/` | `*.vue` | `server/api/**/*.{ts,js}` | `[param]` → `:param` |
|
|
353
|
+
| Astro | `src/pages/` | `*.astro` | `*.{ts,js}` (in pages) | `[...slug]` → `:slug` |
|
|
354
|
+
| Remix | `app/routes/` | `*.{tsx,jsx}` | `*.{ts,js}` | `$param` → `:param` |
|
|
355
|
+
|
|
356
|
+
### Next.js (App Router)
|
|
357
|
+
|
|
358
|
+
```yaml
|
|
359
|
+
filesystem_routes:
|
|
360
|
+
- base_dir: app/
|
|
361
|
+
page_pattern: "page.{tsx,jsx,ts,js}"
|
|
362
|
+
api_pattern: "route.{ts,js}"
|
|
363
|
+
url_template: "/{relative_dir}"
|
|
364
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
365
|
+
ignore_segment: ["\\(.*\\)", "@.*"]
|
|
366
|
+
base_url: ""
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Next.js (Pages Router)
|
|
370
|
+
|
|
371
|
+
```yaml
|
|
372
|
+
filesystem_routes:
|
|
373
|
+
- base_dir: pages/
|
|
374
|
+
page_pattern: "*.{tsx,jsx,ts,js}"
|
|
375
|
+
api_pattern: ""
|
|
376
|
+
url_template: "/{relative_dir}/{stem}"
|
|
377
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
378
|
+
ignore_segment: ["^_.*"]
|
|
379
|
+
base_url: ""
|
|
380
|
+
- base_dir: pages/api/
|
|
381
|
+
page_pattern: ""
|
|
382
|
+
api_pattern: "*.{ts,js}"
|
|
383
|
+
url_template: "/api/{relative_dir}/{stem}"
|
|
384
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
385
|
+
ignore_segment: []
|
|
386
|
+
base_url: ""
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### SvelteKit
|
|
390
|
+
|
|
391
|
+
```yaml
|
|
392
|
+
filesystem_routes:
|
|
393
|
+
- base_dir: src/routes/
|
|
394
|
+
page_pattern: "+page.svelte"
|
|
395
|
+
api_pattern: "+server.{ts,js}"
|
|
396
|
+
url_template: "/{relative_dir}"
|
|
397
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
398
|
+
ignore_segment: ["\\(.*\\)"]
|
|
399
|
+
base_url: ""
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Nuxt 3
|
|
403
|
+
|
|
404
|
+
```yaml
|
|
405
|
+
filesystem_routes:
|
|
406
|
+
- base_dir: pages/
|
|
407
|
+
page_pattern: "*.vue"
|
|
408
|
+
api_pattern: ""
|
|
409
|
+
url_template: "/{relative_dir}/{stem}"
|
|
410
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
411
|
+
ignore_segment: []
|
|
412
|
+
base_url: ""
|
|
413
|
+
- base_dir: server/api/
|
|
414
|
+
page_pattern: ""
|
|
415
|
+
api_pattern: "*.{ts,js}"
|
|
416
|
+
url_template: "/api/{relative_dir}/{stem}"
|
|
417
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
418
|
+
ignore_segment: []
|
|
419
|
+
base_url: ""
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Astro
|
|
423
|
+
|
|
424
|
+
```yaml
|
|
425
|
+
filesystem_routes:
|
|
426
|
+
- base_dir: src/pages/
|
|
427
|
+
page_pattern: "*.astro"
|
|
428
|
+
api_pattern: "*.{ts,js}"
|
|
429
|
+
url_template: "/{relative_dir}/{stem}"
|
|
430
|
+
dynamic_segment: { from: "\\[\\.\\.\\.(\\w+)\\]", to: ":$1" }
|
|
431
|
+
ignore_segment: []
|
|
432
|
+
base_url: ""
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Remix
|
|
436
|
+
|
|
437
|
+
```yaml
|
|
438
|
+
filesystem_routes:
|
|
439
|
+
- base_dir: app/routes/
|
|
440
|
+
page_pattern: "*.{tsx,jsx}"
|
|
441
|
+
api_pattern: "*.{ts,js}"
|
|
442
|
+
url_template: "/{segments}"
|
|
443
|
+
dynamic_segment: { from: "\\$(\\w+)", to: ":$1" }
|
|
444
|
+
ignore_segment: ["^_.*"]
|
|
445
|
+
base_url: ""
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Enable URL Drift Detection
|
|
449
|
+
|
|
450
|
+
Add to `codd.yaml` to automatically link design doc URLs to endpoints:
|
|
451
|
+
|
|
452
|
+
```yaml
|
|
453
|
+
document_url_linking:
|
|
454
|
+
enabled: true
|
|
455
|
+
applies_to: [design, requirement]
|
|
456
|
+
url_pattern: "(?:^|[\\s`(\\[])(/[a-z0-9][a-z0-9/\\-:_\\[\\]]*)"
|
|
457
|
+
edge_type: references
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Then run:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
codd drift # detect URL drift between design docs and implementation
|
|
464
|
+
codd drift --format json # machine-readable output
|
|
465
|
+
codd extract --layer routes --format mermaid # generate screen-flow diagram
|
|
466
|
+
```
|
|
467
|
+
|
|
328
468
|
## Brownfield? Start Here
|
|
329
469
|
|
|
330
470
|
Already have a codebase? CoDD provides a full brownfield workflow — from code extraction to design doc reconstruction.
|
|
@@ -22,6 +22,19 @@
|
|
|
22
22
|
pip install codd-dev
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## 🆕 v1.11.0 — Filesystem-Routing Aware Drift Detection
|
|
26
|
+
|
|
27
|
+
CoDD now understands filesystem-routing frameworks (Next.js, SvelteKit, Nuxt, Astro, Remix) and can detect URL drift between your design docs and actual implementation.
|
|
28
|
+
|
|
29
|
+
- 📐 `FileSystemRouteExtractor` — endpoint nodes from directory structure
|
|
30
|
+
- 🔗 `DocumentUrlLinker` — auto-link design doc URLs to endpoints
|
|
31
|
+
- 🔍 `codd drift` — find URL gaps between design and implementation
|
|
32
|
+
- 🎨 `codd extract --layer routes` — reverse-engineer screen-flow diagrams
|
|
33
|
+
|
|
34
|
+
See [Filesystem Routing Adapter Recipes](#filesystem-routing-adapter-recipes) for setup.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
25
38
|
**v1.9.0** — `codd implement` now supports **multi-AI engine** (Claude stdout + Codex file-writing) and **automatic parallel execution** within phases via git worktree isolation. Phase milestone format (`#### M1.1`) supported. AI command timeout extended to 1 hour for heavy reasoning models. SWE-bench Verified: **73/73 = 100%** resolved.
|
|
26
39
|
|
|
27
40
|
---
|
|
@@ -287,6 +300,133 @@ codd init --config-dir .codd --project-name "my-project" --language "python"
|
|
|
287
300
|
|
|
288
301
|
All other commands (`scan`, `impact`, `generate`, etc.) automatically discover whichever config directory exists — `codd/` first, then `.codd/`. No extra flags needed.
|
|
289
302
|
|
|
303
|
+
## Filesystem Routing Adapter Recipes
|
|
304
|
+
|
|
305
|
+
CoDD detects URL drift between your design documents and implementation
|
|
306
|
+
using framework conventions declared in `codd.yaml`.
|
|
307
|
+
These recipes cover the five major filesystem-routing frameworks.
|
|
308
|
+
|
|
309
|
+
| Framework | Base dir | Page glob | API glob | Dynamic segment |
|
|
310
|
+
|------------------------|---------------|------------------|---------------------------|-----------------------|
|
|
311
|
+
| Next.js (App Router) | `app/` | `page.{tsx,jsx}` | `route.{ts,js}` | `[param]` → `:param` |
|
|
312
|
+
| Next.js (Pages Router) | `pages/` | `*.{tsx,jsx}` | `api/**/*.{ts,js}` | `[param]` → `:param` |
|
|
313
|
+
| SvelteKit | `src/routes/` | `+page.svelte` | `+server.{ts,js}` | `[param]` → `:param` |
|
|
314
|
+
| Nuxt 3 | `pages/` | `*.vue` | `server/api/**/*.{ts,js}` | `[param]` → `:param` |
|
|
315
|
+
| Astro | `src/pages/` | `*.astro` | `*.{ts,js}` (in pages) | `[...slug]` → `:slug` |
|
|
316
|
+
| Remix | `app/routes/` | `*.{tsx,jsx}` | `*.{ts,js}` | `$param` → `:param` |
|
|
317
|
+
|
|
318
|
+
### Next.js (App Router)
|
|
319
|
+
|
|
320
|
+
```yaml
|
|
321
|
+
filesystem_routes:
|
|
322
|
+
- base_dir: app/
|
|
323
|
+
page_pattern: "page.{tsx,jsx,ts,js}"
|
|
324
|
+
api_pattern: "route.{ts,js}"
|
|
325
|
+
url_template: "/{relative_dir}"
|
|
326
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
327
|
+
ignore_segment: ["\\(.*\\)", "@.*"]
|
|
328
|
+
base_url: ""
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Next.js (Pages Router)
|
|
332
|
+
|
|
333
|
+
```yaml
|
|
334
|
+
filesystem_routes:
|
|
335
|
+
- base_dir: pages/
|
|
336
|
+
page_pattern: "*.{tsx,jsx,ts,js}"
|
|
337
|
+
api_pattern: ""
|
|
338
|
+
url_template: "/{relative_dir}/{stem}"
|
|
339
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
340
|
+
ignore_segment: ["^_.*"]
|
|
341
|
+
base_url: ""
|
|
342
|
+
- base_dir: pages/api/
|
|
343
|
+
page_pattern: ""
|
|
344
|
+
api_pattern: "*.{ts,js}"
|
|
345
|
+
url_template: "/api/{relative_dir}/{stem}"
|
|
346
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
347
|
+
ignore_segment: []
|
|
348
|
+
base_url: ""
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### SvelteKit
|
|
352
|
+
|
|
353
|
+
```yaml
|
|
354
|
+
filesystem_routes:
|
|
355
|
+
- base_dir: src/routes/
|
|
356
|
+
page_pattern: "+page.svelte"
|
|
357
|
+
api_pattern: "+server.{ts,js}"
|
|
358
|
+
url_template: "/{relative_dir}"
|
|
359
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
360
|
+
ignore_segment: ["\\(.*\\)"]
|
|
361
|
+
base_url: ""
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Nuxt 3
|
|
365
|
+
|
|
366
|
+
```yaml
|
|
367
|
+
filesystem_routes:
|
|
368
|
+
- base_dir: pages/
|
|
369
|
+
page_pattern: "*.vue"
|
|
370
|
+
api_pattern: ""
|
|
371
|
+
url_template: "/{relative_dir}/{stem}"
|
|
372
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
373
|
+
ignore_segment: []
|
|
374
|
+
base_url: ""
|
|
375
|
+
- base_dir: server/api/
|
|
376
|
+
page_pattern: ""
|
|
377
|
+
api_pattern: "*.{ts,js}"
|
|
378
|
+
url_template: "/api/{relative_dir}/{stem}"
|
|
379
|
+
dynamic_segment: { from: "\\[(.+)\\]", to: ":$1" }
|
|
380
|
+
ignore_segment: []
|
|
381
|
+
base_url: ""
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Astro
|
|
385
|
+
|
|
386
|
+
```yaml
|
|
387
|
+
filesystem_routes:
|
|
388
|
+
- base_dir: src/pages/
|
|
389
|
+
page_pattern: "*.astro"
|
|
390
|
+
api_pattern: "*.{ts,js}"
|
|
391
|
+
url_template: "/{relative_dir}/{stem}"
|
|
392
|
+
dynamic_segment: { from: "\\[\\.\\.\\.(\\w+)\\]", to: ":$1" }
|
|
393
|
+
ignore_segment: []
|
|
394
|
+
base_url: ""
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Remix
|
|
398
|
+
|
|
399
|
+
```yaml
|
|
400
|
+
filesystem_routes:
|
|
401
|
+
- base_dir: app/routes/
|
|
402
|
+
page_pattern: "*.{tsx,jsx}"
|
|
403
|
+
api_pattern: "*.{ts,js}"
|
|
404
|
+
url_template: "/{segments}"
|
|
405
|
+
dynamic_segment: { from: "\\$(\\w+)", to: ":$1" }
|
|
406
|
+
ignore_segment: ["^_.*"]
|
|
407
|
+
base_url: ""
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Enable URL Drift Detection
|
|
411
|
+
|
|
412
|
+
Add to `codd.yaml` to automatically link design doc URLs to endpoints:
|
|
413
|
+
|
|
414
|
+
```yaml
|
|
415
|
+
document_url_linking:
|
|
416
|
+
enabled: true
|
|
417
|
+
applies_to: [design, requirement]
|
|
418
|
+
url_pattern: "(?:^|[\\s`(\\[])(/[a-z0-9][a-z0-9/\\-:_\\[\\]]*)"
|
|
419
|
+
edge_type: references
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Then run:
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
codd drift # detect URL drift between design docs and implementation
|
|
426
|
+
codd drift --format json # machine-readable output
|
|
427
|
+
codd extract --layer routes --format mermaid # generate screen-flow diagram
|
|
428
|
+
```
|
|
429
|
+
|
|
290
430
|
## Brownfield? Start Here
|
|
291
431
|
|
|
292
432
|
Already have a codebase? CoDD provides a full brownfield workflow — from code extraction to design doc reconstruction.
|
|
@@ -589,13 +589,38 @@ def verify(path: str, sprint: int | None, e2e: bool, deploy: bool, base_url: str
|
|
|
589
589
|
default=None,
|
|
590
590
|
help="Override AI CLI command (default: codd.yaml ai_command or claude --print)",
|
|
591
591
|
)
|
|
592
|
-
@click.option(
|
|
593
|
-
"--prompt-file",
|
|
594
|
-
default=None,
|
|
595
|
-
type=click.Path(exists=True),
|
|
596
|
-
help="Custom extraction prompt file (overrides built-in baseline preset)",
|
|
597
|
-
)
|
|
598
|
-
|
|
592
|
+
@click.option(
|
|
593
|
+
"--prompt-file",
|
|
594
|
+
default=None,
|
|
595
|
+
type=click.Path(exists=True),
|
|
596
|
+
help="Custom extraction prompt file (overrides built-in baseline preset)",
|
|
597
|
+
)
|
|
598
|
+
@click.option(
|
|
599
|
+
"--layer",
|
|
600
|
+
default=None,
|
|
601
|
+
type=click.Choice(["routes"]),
|
|
602
|
+
help="Extract specific layer (routes: filesystem routes as Mermaid diagram)",
|
|
603
|
+
)
|
|
604
|
+
@click.option(
|
|
605
|
+
"--format",
|
|
606
|
+
"output_format",
|
|
607
|
+
default="mermaid",
|
|
608
|
+
type=click.Choice(["mermaid"]),
|
|
609
|
+
help="Output format for --layer extraction",
|
|
610
|
+
)
|
|
611
|
+
@click.option("--output-file", default=None, help="Output file for --layer routes (default: stdout)")
|
|
612
|
+
def extract(
|
|
613
|
+
path: str,
|
|
614
|
+
language: str | None,
|
|
615
|
+
source_dirs: str | None,
|
|
616
|
+
output: str | None,
|
|
617
|
+
ai: bool,
|
|
618
|
+
ai_cmd: str | None,
|
|
619
|
+
prompt_file: str | None,
|
|
620
|
+
layer: str | None,
|
|
621
|
+
output_format: str,
|
|
622
|
+
output_file: str | None,
|
|
623
|
+
):
|
|
599
624
|
"""Extract design documents from existing codebase (brownfield bootstrap).
|
|
600
625
|
|
|
601
626
|
Default mode: static analysis (no AI, pure structural facts).
|
|
@@ -604,16 +629,33 @@ def extract(path: str, language: str | None, source_dirs: str | None, output: st
|
|
|
604
629
|
Output goes to the discovered CoDD config dir as draft documents
|
|
605
630
|
(`codd/extracted/` or `.codd/extracted/`). Review and promote
|
|
606
631
|
confirmed docs when ready.
|
|
607
|
-
"""
|
|
608
|
-
project_root = Path(path).resolve()
|
|
609
|
-
bootstrap_codd_dir = _resolve_bootstrap_codd_dir(project_root)
|
|
610
|
-
dirs = [d.strip() for d in source_dirs.split(",") if d.strip()] if source_dirs else None
|
|
611
|
-
output_path = Path(output) if output else bootstrap_codd_dir / "extracted"
|
|
612
|
-
|
|
613
|
-
if
|
|
614
|
-
from codd.
|
|
615
|
-
from codd.
|
|
616
|
-
|
|
632
|
+
"""
|
|
633
|
+
project_root = Path(path).resolve()
|
|
634
|
+
bootstrap_codd_dir = _resolve_bootstrap_codd_dir(project_root)
|
|
635
|
+
dirs = [d.strip() for d in source_dirs.split(",") if d.strip()] if source_dirs else None
|
|
636
|
+
output_path = Path(output) if output else bootstrap_codd_dir / "extracted"
|
|
637
|
+
|
|
638
|
+
if layer == "routes":
|
|
639
|
+
from codd.config import load_project_config
|
|
640
|
+
from codd.routes_extractor import generate_mermaid_screen_flow
|
|
641
|
+
|
|
642
|
+
config = load_project_config(project_root)
|
|
643
|
+
route_configs = config.get("filesystem_routes", [])
|
|
644
|
+
result = generate_mermaid_screen_flow(project_root, route_configs)
|
|
645
|
+
content = f"```{output_format}\n{result.mermaid}\n```\n"
|
|
646
|
+
if output_file:
|
|
647
|
+
destination = Path(output_file)
|
|
648
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
649
|
+
destination.write_text(content, encoding="utf-8")
|
|
650
|
+
click.echo(f"Extracted {result.route_count} routes -> {output_file}")
|
|
651
|
+
else:
|
|
652
|
+
click.echo(content)
|
|
653
|
+
return
|
|
654
|
+
|
|
655
|
+
if ai:
|
|
656
|
+
from codd.extract_ai import run_extract_ai
|
|
657
|
+
from codd.extractor import extract_facts
|
|
658
|
+
from codd.config import load_project_config
|
|
617
659
|
|
|
618
660
|
# Resolve AI command
|
|
619
661
|
if ai_cmd is None:
|
|
@@ -909,6 +951,60 @@ def policy(path: str):
|
|
|
909
951
|
raise SystemExit(0 if result.pass_ else 1)
|
|
910
952
|
|
|
911
953
|
|
|
954
|
+
@main.command("drift")
|
|
955
|
+
@click.option("--path", default=".", help="Project root directory")
|
|
956
|
+
@click.option(
|
|
957
|
+
"--format",
|
|
958
|
+
"output_format",
|
|
959
|
+
default="text",
|
|
960
|
+
type=click.Choice(["text", "json"]),
|
|
961
|
+
help="Output format",
|
|
962
|
+
)
|
|
963
|
+
def drift(path: str, output_format: str):
|
|
964
|
+
"""Detect drift between design-referenced URLs and implementation endpoints.
|
|
965
|
+
|
|
966
|
+
Exit code 0 = no drift. Exit code 1 = drift detected (use in CI).
|
|
967
|
+
"""
|
|
968
|
+
from codd.drift import run_drift
|
|
969
|
+
|
|
970
|
+
project_root = Path(path).resolve()
|
|
971
|
+
codd_dir = _require_codd_dir(project_root)
|
|
972
|
+
result = run_drift(project_root, codd_dir)
|
|
973
|
+
|
|
974
|
+
if output_format == "json":
|
|
975
|
+
click.echo(
|
|
976
|
+
json.dumps(
|
|
977
|
+
{
|
|
978
|
+
"design_urls": result.design_urls,
|
|
979
|
+
"impl_urls": result.impl_urls,
|
|
980
|
+
"drift": [
|
|
981
|
+
{
|
|
982
|
+
"kind": entry.kind,
|
|
983
|
+
"url": entry.url,
|
|
984
|
+
"source": entry.source,
|
|
985
|
+
"closest_match": entry.closest_match,
|
|
986
|
+
}
|
|
987
|
+
for entry in result.drift
|
|
988
|
+
],
|
|
989
|
+
},
|
|
990
|
+
ensure_ascii=False,
|
|
991
|
+
indent=2,
|
|
992
|
+
)
|
|
993
|
+
)
|
|
994
|
+
else:
|
|
995
|
+
for entry in result.drift:
|
|
996
|
+
label = f"[drift {entry.kind}]"
|
|
997
|
+
closest = f" (closest: {entry.closest_match})" if entry.closest_match else ""
|
|
998
|
+
source = f" in {entry.source}" if entry.source else ""
|
|
999
|
+
click.echo(f"{label} {entry.url}{source}{closest}")
|
|
1000
|
+
if not result.drift:
|
|
1001
|
+
click.echo("No drift detected.")
|
|
1002
|
+
else:
|
|
1003
|
+
click.echo(f"\n{len(result.drift)} drift(s) found.")
|
|
1004
|
+
|
|
1005
|
+
raise SystemExit(result.exit_code)
|
|
1006
|
+
|
|
1007
|
+
|
|
912
1008
|
@main.command()
|
|
913
1009
|
@click.option("--path", default=".", help="Project root directory")
|
|
914
1010
|
@click.option("--json", "as_json", is_flag=True, help="Output plan as JSON")
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""codd drift - Detect design-to-implementation URL drift."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Sequence
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class DriftEntry:
|
|
12
|
+
kind: str
|
|
13
|
+
url: str
|
|
14
|
+
source: str
|
|
15
|
+
closest_match: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class DriftResult:
|
|
20
|
+
design_urls: list[str]
|
|
21
|
+
impl_urls: list[str]
|
|
22
|
+
drift: list[DriftEntry] = field(default_factory=list)
|
|
23
|
+
exit_code: int = 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def compute_drift(
|
|
27
|
+
design_urls: Sequence[str],
|
|
28
|
+
impl_urls: Sequence[str],
|
|
29
|
+
design_sources: dict[str, str] | None = None,
|
|
30
|
+
) -> DriftResult:
|
|
31
|
+
"""Compute drift between design-referenced URLs and implementation endpoints."""
|
|
32
|
+
design_sources = design_sources or {}
|
|
33
|
+
normalized_design_urls = _unique_urls(design_urls)
|
|
34
|
+
normalized_impl_urls = _unique_urls(impl_urls)
|
|
35
|
+
design_set = set(normalized_design_urls)
|
|
36
|
+
impl_set = set(normalized_impl_urls)
|
|
37
|
+
|
|
38
|
+
drift: list[DriftEntry] = []
|
|
39
|
+
for url in normalized_design_urls:
|
|
40
|
+
if url not in impl_set:
|
|
41
|
+
drift.append(
|
|
42
|
+
DriftEntry(
|
|
43
|
+
kind="design-only",
|
|
44
|
+
url=url,
|
|
45
|
+
source=design_sources.get(url, ""),
|
|
46
|
+
closest_match=_find_closest(url, normalized_impl_urls),
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
for url in normalized_impl_urls:
|
|
51
|
+
if url not in design_set:
|
|
52
|
+
drift.append(
|
|
53
|
+
DriftEntry(
|
|
54
|
+
kind="impl-only",
|
|
55
|
+
url=url,
|
|
56
|
+
source="implementation",
|
|
57
|
+
closest_match=_find_closest(url, normalized_design_urls),
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return DriftResult(
|
|
62
|
+
design_urls=normalized_design_urls,
|
|
63
|
+
impl_urls=normalized_impl_urls,
|
|
64
|
+
drift=drift,
|
|
65
|
+
exit_code=1 if drift else 0,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _find_closest(url: str, candidates: list[str]) -> str:
|
|
70
|
+
"""Find closest URL from candidates using common-prefix heuristic."""
|
|
71
|
+
if not candidates:
|
|
72
|
+
return ""
|
|
73
|
+
|
|
74
|
+
def score(candidate: str) -> int:
|
|
75
|
+
prefix = 0
|
|
76
|
+
for left, right in zip(url, candidate):
|
|
77
|
+
if left != right:
|
|
78
|
+
break
|
|
79
|
+
prefix += 1
|
|
80
|
+
return prefix
|
|
81
|
+
|
|
82
|
+
return max(candidates, key=score)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def run_drift(project_root: Path, codd_dir: Path) -> DriftResult:
|
|
86
|
+
"""Full drift run: read codd.yaml, extract URLs, and compute drift."""
|
|
87
|
+
import yaml
|
|
88
|
+
|
|
89
|
+
config_path = codd_dir / "codd.yaml"
|
|
90
|
+
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
|
91
|
+
|
|
92
|
+
impl_urls = _extract_impl_urls(project_root, config)
|
|
93
|
+
design_urls, design_sources = _extract_design_urls(project_root, config)
|
|
94
|
+
|
|
95
|
+
return compute_drift(design_urls, impl_urls, design_sources)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _extract_impl_urls(project_root: Path, config: dict[str, Any]) -> list[str]:
|
|
99
|
+
fs_route_configs = config.get("filesystem_routes", [])
|
|
100
|
+
if not fs_route_configs:
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
from codd.parsing import FileSystemRouteExtractor
|
|
105
|
+
except ImportError:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
extractor = FileSystemRouteExtractor()
|
|
109
|
+
route_info = extractor.extract_routes(project_root, fs_route_configs)
|
|
110
|
+
return [_route_url(route) for route in getattr(route_info, "routes", []) if _route_url(route)]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _extract_design_urls(project_root: Path, config: dict[str, Any]) -> tuple[list[str], dict[str, str]]:
|
|
114
|
+
doc_link_config = config.get("document_url_linking", {})
|
|
115
|
+
if not doc_link_config.get("enabled", False):
|
|
116
|
+
return [], {}
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
from codd.extractor import DocumentUrlLinker
|
|
120
|
+
except ImportError:
|
|
121
|
+
return [], {}
|
|
122
|
+
|
|
123
|
+
linker = DocumentUrlLinker(doc_link_config)
|
|
124
|
+
design_urls: list[str] = []
|
|
125
|
+
design_sources: dict[str, str] = {}
|
|
126
|
+
doc_dirs = config.get("scan", {}).get("doc_dirs", [])
|
|
127
|
+
|
|
128
|
+
for doc_dir in doc_dirs:
|
|
129
|
+
full_dir = project_root / doc_dir
|
|
130
|
+
if not full_dir.exists():
|
|
131
|
+
continue
|
|
132
|
+
for md_file in full_dir.rglob("*.md"):
|
|
133
|
+
text = md_file.read_text(encoding="utf-8", errors="ignore")
|
|
134
|
+
rel_path = md_file.relative_to(project_root).as_posix()
|
|
135
|
+
result = linker.extract_urls(text, rel_path)
|
|
136
|
+
for url in getattr(result, "urls", []):
|
|
137
|
+
design_urls.append(url)
|
|
138
|
+
design_sources.setdefault(url, getattr(result, "node_id", rel_path))
|
|
139
|
+
|
|
140
|
+
return design_urls, design_sources
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _unique_urls(urls: Sequence[str]) -> list[str]:
|
|
144
|
+
seen: set[str] = set()
|
|
145
|
+
unique: list[str] = []
|
|
146
|
+
for url in urls:
|
|
147
|
+
if url in seen:
|
|
148
|
+
continue
|
|
149
|
+
seen.add(url)
|
|
150
|
+
unique.append(url)
|
|
151
|
+
return unique
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _route_url(route: Any) -> str:
|
|
155
|
+
if isinstance(route, dict):
|
|
156
|
+
return str(route.get("url", ""))
|
|
157
|
+
return str(getattr(route, "url", ""))
|
|
@@ -1018,3 +1018,44 @@ def run_extract(project_root: Path, language: str | None = None,
|
|
|
1018
1018
|
def _match_glob(path: str, pattern: str) -> bool:
|
|
1019
1019
|
import fnmatch
|
|
1020
1020
|
return fnmatch.fnmatch(path, pattern)
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
@dataclass
|
|
1024
|
+
class DocumentUrlLinkInfo:
|
|
1025
|
+
"""URLs extracted from a document node."""
|
|
1026
|
+
node_id: str
|
|
1027
|
+
urls: list[str]
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
class DocumentUrlLinker:
|
|
1031
|
+
"""Scan design/requirement document text for URL patterns.
|
|
1032
|
+
|
|
1033
|
+
Extracts URL strings referenced in documents (Mermaid diagrams, prose,
|
|
1034
|
+
code blocks) and returns them for downstream drift analysis.
|
|
1035
|
+
FW-agnostic: pattern driven by codd.yaml document_url_linking config.
|
|
1036
|
+
"""
|
|
1037
|
+
|
|
1038
|
+
DEFAULT_URL_PATTERN = r"(?:^|[\s`(\[])(/(?:[a-z0-9][a-z0-9/\-:_\[\]]*)?)"
|
|
1039
|
+
DEFAULT_EDGE_TYPE = "references"
|
|
1040
|
+
|
|
1041
|
+
def __init__(self, config: dict | None = None):
|
|
1042
|
+
cfg = config or {}
|
|
1043
|
+
self._pattern = re.compile(
|
|
1044
|
+
cfg.get("url_pattern", self.DEFAULT_URL_PATTERN),
|
|
1045
|
+
re.MULTILINE,
|
|
1046
|
+
)
|
|
1047
|
+
self.edge_type = cfg.get("edge_type", self.DEFAULT_EDGE_TYPE)
|
|
1048
|
+
|
|
1049
|
+
def extract_urls(self, text: str, node_id: str = "") -> DocumentUrlLinkInfo:
|
|
1050
|
+
"""Extract and normalize URLs from document text."""
|
|
1051
|
+
raw = self._pattern.findall(text)
|
|
1052
|
+
normalized = sorted(set(self._normalize_url(url) for url in raw if url))
|
|
1053
|
+
return DocumentUrlLinkInfo(node_id=node_id, urls=normalized)
|
|
1054
|
+
|
|
1055
|
+
def _normalize_url(self, url: str) -> str:
|
|
1056
|
+
url = url.strip()
|
|
1057
|
+
while url.endswith("]") and url.count("]") > url.count("["):
|
|
1058
|
+
url = url[:-1]
|
|
1059
|
+
if url != "/" and url.endswith("/"):
|
|
1060
|
+
url = url.rstrip("/")
|
|
1061
|
+
return url
|