contract-driven-delivery 2.0.2 → 2.0.7

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.
@@ -0,0 +1,167 @@
1
+ """cdd-kit Python AST scanner — batch mode, NDJSON output."""
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ import json
6
+ import os
7
+ import sys
8
+ import traceback
9
+
10
+
11
+ def _check_python_version() -> None:
12
+ if sys.version_info < (3, 9):
13
+ print(
14
+ f"cdd-kit: python interpreter is {sys.version_info.major}.{sys.version_info.minor} "
15
+ f"(need 3.9+ for ast.unparse); skipping .py files",
16
+ file=sys.stderr,
17
+ )
18
+ sys.exit(3)
19
+
20
+
21
+ def _rel_path(abs_path: str, repo_root: str) -> str:
22
+ try:
23
+ rel = os.path.relpath(abs_path, repo_root)
24
+ except ValueError:
25
+ # Windows: different drive — use abs_path
26
+ rel = abs_path
27
+ return rel.replace("\\", "/")
28
+
29
+
30
+ def _is_all_caps(name: str) -> bool:
31
+ """Return True if name matches /^[A-Z][A-Z0-9_]*$/ and contains at least one letter."""
32
+ if not name:
33
+ return False
34
+ if not name[0].isupper():
35
+ return False
36
+ if not all(c.isupper() or c.isdigit() or c == '_' for c in name):
37
+ return False
38
+ return any(c.isalpha() for c in name)
39
+
40
+
41
+ def scan_file(abs_path: str, repo_root: str) -> dict:
42
+ src = open(abs_path, encoding="utf-8").read()
43
+ total_lines = len(src.splitlines()) if src else 0
44
+
45
+ # Check first 4KB for binary content
46
+ head = src[:4096]
47
+ if '\x00' in head:
48
+ return {
49
+ "path": _rel_path(abs_path, repo_root),
50
+ "ok": False,
51
+ "error": "binary file (null bytes detected)",
52
+ }
53
+
54
+ try:
55
+ tree = ast.parse(src, filename=abs_path)
56
+ except SyntaxError as exc:
57
+ return {
58
+ "path": _rel_path(abs_path, repo_root),
59
+ "ok": False,
60
+ "error": str(exc),
61
+ }
62
+ except UnicodeDecodeError as exc:
63
+ return {
64
+ "path": _rel_path(abs_path, repo_root),
65
+ "ok": False,
66
+ "error": str(exc),
67
+ }
68
+
69
+ imports: list[dict] = []
70
+ constants: list[dict] = []
71
+ classes: list[dict] = []
72
+ functions: list[dict] = []
73
+
74
+ for node in ast.iter_child_nodes(tree):
75
+ # ── imports ──────────────────────────────────────────────────────────
76
+ if isinstance(node, ast.Import):
77
+ for alias in node.names:
78
+ imports.append({
79
+ "module": alias.name,
80
+ "items": [],
81
+ "line": node.lineno,
82
+ })
83
+ elif isinstance(node, ast.ImportFrom):
84
+ level = node.level or 0
85
+ module_name = ("." * level) + (node.module or "")
86
+ items = [a.asname if a.asname else a.name for a in node.names]
87
+ imports.append({
88
+ "module": module_name,
89
+ "items": items,
90
+ "line": node.lineno,
91
+ })
92
+
93
+ # ── constants ────────────────────────────────────────────────────────
94
+ elif isinstance(node, ast.Assign):
95
+ for target in node.targets:
96
+ if isinstance(target, ast.Name) and _is_all_caps(target.id):
97
+ constants.append({"name": target.id, "line": node.lineno})
98
+ elif isinstance(node, ast.AnnAssign):
99
+ if isinstance(node.target, ast.Name) and _is_all_caps(node.target.id):
100
+ constants.append({"name": node.target.id, "line": node.lineno})
101
+
102
+ # ── classes ──────────────────────────────────────────────────────────
103
+ elif isinstance(node, ast.ClassDef):
104
+ methods: list[dict] = []
105
+ for sub in ast.iter_child_nodes(node):
106
+ if isinstance(sub, (ast.FunctionDef, ast.AsyncFunctionDef)):
107
+ methods.append({
108
+ "name": sub.name,
109
+ "lines": [sub.lineno, sub.end_lineno],
110
+ "async": isinstance(sub, ast.AsyncFunctionDef),
111
+ })
112
+ classes.append({
113
+ "name": node.name,
114
+ "lines": [node.lineno, node.end_lineno],
115
+ "methods": methods,
116
+ })
117
+
118
+ # ── functions ────────────────────────────────────────────────────────
119
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
120
+ decos: list[str] = []
121
+ for d in node.decorator_list:
122
+ try:
123
+ decos.append(ast.unparse(d))
124
+ except Exception:
125
+ decos.append("?")
126
+ functions.append({
127
+ "name": node.name,
128
+ "lines": [node.lineno, node.end_lineno],
129
+ "decorators": decos,
130
+ "async": isinstance(node, ast.AsyncFunctionDef),
131
+ })
132
+
133
+ return {
134
+ "path": _rel_path(abs_path, repo_root),
135
+ "total_lines": total_lines,
136
+ "imports": imports,
137
+ "constants": constants,
138
+ "classes": classes,
139
+ "functions": functions,
140
+ "ok": True,
141
+ }
142
+
143
+
144
+ def main() -> None:
145
+ _check_python_version()
146
+
147
+ import argparse
148
+ parser = argparse.ArgumentParser(description="cdd-kit Python AST scanner")
149
+ parser.add_argument("--batch-file", required=True, help="File containing one abs path per line")
150
+ parser.add_argument("--repo-root", required=True, help="Repository root for relative paths")
151
+ args = parser.parse_args()
152
+
153
+ with open(args.batch_file, encoding="utf-8") as fh:
154
+ paths = [line.rstrip("\n") for line in fh if line.strip()]
155
+
156
+ for abs_path in paths:
157
+ try:
158
+ result = scan_file(abs_path, args.repo_root)
159
+ except Exception:
160
+ tb = traceback.format_exc()
161
+ print(json.dumps({"fatal": tb}), file=sys.stderr)
162
+ sys.exit(2)
163
+ print(json.dumps(result, ensure_ascii=False))
164
+
165
+
166
+ if __name__ == "__main__":
167
+ main()
@@ -0,0 +1,107 @@
1
+ # Code Map Protocol
2
+
3
+ `.cdd/code-map.yml` is a deterministic structural index of every source
4
+ file in the repo (`.py`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`,
5
+ `.vue`). Generated by `cdd-kit code-map`, committed to git, refreshed
6
+ automatically when `cdd-kit init --hooks` is installed. `cdd-kit gate`
7
+ hard-fails when any source file is newer than the map.
8
+
9
+ ## Why agents must read it FIRST
10
+
11
+ Agents have no built-in way to know a file's line count before reading it.
12
+ Reading a 1300-line file when you only need lines 200–250 wastes 80%+
13
+ of the token budget. The code map is the size oracle: it tells you how
14
+ big a file is and exactly which lines hold the symbol you need.
15
+
16
+ ## The 300-line rule
17
+
18
+ Before reading any source file:
19
+
20
+ 1. `Read .cdd/code-map.yml` once at the start of your task (cache it for
21
+ the rest of your session).
22
+ 2. Find the file's entry. The first line is `<path>: # N lines`.
23
+ 3. If `N <= 300`: do a normal full `Read <path>`.
24
+ 4. If `N > 300`: locate the class / method / function in the entry's
25
+ `classes:` / `functions:` block (or, for `.ts`/`.tsx`, the
26
+ `interfaces:` / `types:` / `enums:` blocks); its `lines: A-B`
27
+ field is the exact range. Use `Read <path> offset:A limit:(B-A+1)`.
28
+ 5. To understand a file's surface area without reading code, the
29
+ `imports:` / `constants:` / `interfaces:` / `types:` sections are
30
+ usually sufficient.
31
+
32
+ ## Fallback when the map is missing
33
+
34
+ If `.cdd/code-map.yml` does not exist, do NOT proceed by reading whole
35
+ files. Instead, write an agent-log entry with `status: needs-review`
36
+ and `next-action: "regenerate code-map (run \`cdd-kit code-map\`)"`.
37
+ The user must regenerate the map before you continue. This forces
38
+ attention to missing infrastructure rather than burning tokens
39
+ silently.
40
+
41
+ ## Fallback when the map is stale
42
+
43
+ If `cdd-kit gate` reports `code-map stale`, the map is out of date.
44
+ Same protocol as missing: emit `needs-review`, ask for a regen.
45
+
46
+ ## What the map does NOT contain
47
+
48
+ - Function bodies, parameter types, JSDoc, TS generic constraints — read
49
+ the source for these using offset/limit.
50
+ - Vue `<script lang="ts">` blocks — fall back to a full `Read` for now.
51
+ - Anything inside `node_modules/`, `dist/`, `build/`, `.venv/`, `__pycache__/`.
52
+
53
+ ## Customising what gets indexed
54
+
55
+ Optional file: `.cdd/code-map-config.yml`
56
+
57
+ ```yaml
58
+ # Both keys optional. When present, each REPLACES (not merges) the built-in
59
+ # default — copy the built-in list and edit it for partial overrides.
60
+ include:
61
+ - "**/*.py"
62
+ - "**/*.ts"
63
+ - "**/*.tsx"
64
+ exclude:
65
+ - "**/node_modules/**"
66
+ - "**/legacy/**"
67
+ ```
68
+
69
+ Without this file, sensible built-in defaults apply.
70
+
71
+ ## Field reference
72
+
73
+ Each file entry:
74
+
75
+ ```yaml
76
+ backend/routes/todos.py: # 1371 lines
77
+ imports:
78
+ - { module: flask, items: [Blueprint, request], line: 1 }
79
+ constants:
80
+ - { name: MAX_BATCH_SIZE, line: 18 }
81
+ classes:
82
+ - name: TodoService
83
+ lines: 22-104
84
+ methods:
85
+ - { name: create, lines: 30-62 }
86
+ functions:
87
+ - { name: get_todos, lines: 105-253 } # @todos_bp.route(...)
88
+ ```
89
+
90
+ For `.ts` / `.tsx` files, three additional optional sections may appear:
91
+
92
+ ```yaml
93
+ src/types/index.ts: # 625 lines
94
+ interfaces:
95
+ - { name: User, lines: 12-40 }
96
+ - { name: InternalState, lines: 42-50 } # local
97
+ types:
98
+ - { name: UserId, lines: 52-52 }
99
+ enums:
100
+ - name: Status
101
+ lines: 60-66
102
+ members: [Pending, Active, Done]
103
+ ```
104
+
105
+ `async ` prefix on a function/method name indicates `async def` (Python)
106
+ or `async function` (JS/TS). `# local` annotation on an interface/type/enum
107
+ means it is NOT exported.
@@ -38,7 +38,7 @@ Always required: change-request.md, change-classification.md, test-plan.md, ci-g
38
38
  - Business logic:
39
39
  - CI/CD:
40
40
 
41
- ## Required Test Families
41
+ ## Required Tests
42
42
  - unit:
43
43
  - contract:
44
44
  - integration:
@@ -52,4 +52,38 @@ Always required: change-request.md, change-classification.md, test-plan.md, ci-g
52
52
 
53
53
  ## Required Agents
54
54
 
55
- ## Assumptions / Clarifications
55
+ ## Inferred Acceptance Criteria
56
+ <!-- 3-8 testable acceptance criteria derived from the change request. Format: AC-N: <criterion>.
57
+ test-strategist uses these to populate the Acceptance Criteria → Test Mapping table. -->
58
+ - AC-1:
59
+ - AC-2:
60
+ - AC-3:
61
+
62
+ ## Tasks Not Applicable
63
+ <!-- Comma-separated task IDs from tasks.yml that do NOT apply to this change.
64
+ /cdd-new SKILL marks these as `status: skipped` in tasks.yml. -->
65
+ - not-applicable:
66
+
67
+ ## Clarifications or Assumptions
68
+
69
+ ## Context Manifest Draft
70
+ <!-- Classifier fills this section. In /cdd-new Step 2.3, Claude copies it verbatim into
71
+ specs/changes/<change-id>/context-manifest.md, replacing the scaffold.
72
+ All paths must be repo-relative. Gate enforces Allowed Paths against agent files-read logs. -->
73
+
74
+ ### Affected Surfaces
75
+ -
76
+
77
+ ### Allowed Paths
78
+ <!-- Union of ALL paths any agent will read. Add change-specific paths below the defaults. -->
79
+ - specs/changes/<change-id>/
80
+ - specs/context/project-map.md
81
+ - specs/context/contracts-index.md
82
+
83
+ ### Agent Work Packets
84
+ <!-- One sub-section per required agent (paths must be a subset of Allowed Paths above). -->
85
+
86
+ #### change-classifier
87
+ - specs/changes/<change-id>/
88
+ - specs/context/project-map.md
89
+ - specs/context/contracts-index.md
@@ -38,7 +38,7 @@ Always required: change-request.md, change-classification.md, test-plan.md, ci-g
38
38
  - Business logic:
39
39
  - CI/CD:
40
40
 
41
- ## Required Test Families
41
+ ## Required Tests
42
42
  - unit:
43
43
  - contract:
44
44
  - integration:
@@ -52,4 +52,38 @@ Always required: change-request.md, change-classification.md, test-plan.md, ci-g
52
52
 
53
53
  ## Required Agents
54
54
 
55
- ## Assumptions / Clarifications
55
+ ## Inferred Acceptance Criteria
56
+ <!-- 3-8 testable acceptance criteria derived from the change request. Format: AC-N: <criterion>.
57
+ test-strategist uses these to populate the Acceptance Criteria → Test Mapping table. -->
58
+ - AC-1:
59
+ - AC-2:
60
+ - AC-3:
61
+
62
+ ## Tasks Not Applicable
63
+ <!-- Comma-separated task IDs from tasks.yml that do NOT apply to this change.
64
+ /cdd-new SKILL marks these as `status: skipped` in tasks.yml. -->
65
+ - not-applicable:
66
+
67
+ ## Clarifications or Assumptions
68
+
69
+ ## Context Manifest Draft
70
+ <!-- Classifier fills this section. In /cdd-new Step 2.3, Claude copies it verbatim into
71
+ specs/changes/<change-id>/context-manifest.md, replacing the scaffold.
72
+ All paths must be repo-relative. Gate enforces Allowed Paths against agent files-read logs. -->
73
+
74
+ ### Affected Surfaces
75
+ -
76
+
77
+ ### Allowed Paths
78
+ <!-- Union of ALL paths any agent will read. Add change-specific paths below the defaults. -->
79
+ - specs/changes/<change-id>/
80
+ - specs/context/project-map.md
81
+ - specs/context/contracts-index.md
82
+
83
+ ### Agent Work Packets
84
+ <!-- One sub-section per required agent (paths must be a subset of Allowed Paths above). -->
85
+
86
+ #### change-classifier
87
+ - specs/changes/<change-id>/
88
+ - specs/context/project-map.md
89
+ - specs/context/contracts-index.md
@@ -8,6 +8,10 @@ and is automatically applied by `cdd-kit gate` — do not duplicate it here.
8
8
  -
9
9
 
10
10
  ## Allowed Paths
11
+ <!-- UNION of all repo-relative paths (or globs) any agent may read for this change.
12
+ cdd-kit gate validates every agent's files-read log against this list.
13
+ Be specific — wide globs (e.g. src/) defeat read-scope governance.
14
+ Always include the three defaults below; add change-specific paths beneath them. -->
11
15
  - specs/changes/<change-id>/
12
16
  - specs/context/project-map.md
13
17
  - specs/context/contracts-index.md
@@ -19,12 +23,26 @@ and is automatically applied by `cdd-kit gate` — do not duplicate it here.
19
23
  -
20
24
 
21
25
  ## Agent Work Packets
26
+ <!-- One sub-section per required agent. Each path list must be a subset of Allowed Paths above.
27
+ Add or remove sub-sections to match Required Agents in change-classification.md.
28
+ These sub-sections are documentation only — gate enforces Allowed Paths, not individual packets. -->
22
29
 
23
30
  ### change-classifier
24
- - allowed:
25
- - specs/changes/<change-id>/
26
- - specs/context/project-map.md
27
- - specs/context/contracts-index.md
31
+ - specs/changes/<change-id>/
32
+ - specs/context/project-map.md
33
+ - specs/context/contracts-index.md
34
+
35
+ ### <implementation-agent>
36
+ <!-- Replace with actual agent name, e.g. backend-engineer, frontend-engineer -->
37
+ - specs/changes/<change-id>/
38
+ - contracts/
39
+ - src/
40
+ - tests/
41
+
42
+ ### <review-agent>
43
+ <!-- Replace with actual agent name, e.g. contract-reviewer, qa-reviewer -->
44
+ - specs/changes/<change-id>/
45
+ - contracts/
28
46
 
29
47
  ## Context Expansion Requests
30
48
 
@@ -8,7 +8,7 @@ archive-tasks:
8
8
  depends-on: []
9
9
 
10
10
  tasks:
11
- # status: pending | done | skipped
11
+ # status: pending | done | skipped (optional: note: "reason or context")
12
12
  - { id: "1.1", section: Preparation, title: "Confirm classification and required artifacts", status: pending }
13
13
  - { id: "1.2", section: Preparation, title: "Confirm contracts to update", status: pending }
14
14
  - { id: "1.3", section: Preparation, title: "Confirm CI/CD gate plan", status: pending }