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.
- package/CHANGELOG.md +234 -0
- package/assets/agents/backend-engineer.md +51 -13
- package/assets/agents/change-classifier.md +30 -5
- package/assets/agents/ci-cd-gatekeeper.md +31 -11
- package/assets/agents/contract-reviewer.md +31 -11
- package/assets/agents/dependency-security-reviewer.md +40 -4
- package/assets/agents/e2e-resilience-engineer.md +33 -13
- package/assets/agents/frontend-engineer.md +51 -13
- package/assets/agents/monkey-test-engineer.md +29 -10
- package/assets/agents/qa-reviewer.md +29 -7
- package/assets/agents/repo-context-scanner.md +26 -3
- package/assets/agents/spec-architect.md +30 -10
- package/assets/agents/spec-drift-auditor.md +39 -4
- package/assets/agents/stress-soak-engineer.md +31 -11
- package/assets/agents/test-strategist.md +31 -11
- package/assets/agents/ui-ux-reviewer.md +39 -4
- package/assets/agents/visual-reviewer.md +39 -4
- package/assets/cdd/model-policy.json +3 -3
- package/assets/code-map/python_scanner.py +167 -0
- package/assets/skills/contract-driven-delivery/references/code-map-protocol.md +107 -0
- package/assets/skills/contract-driven-delivery/templates/change-classification.md +36 -2
- package/assets/specs-templates/change-classification.md +36 -2
- package/assets/specs-templates/context-manifest.md +22 -4
- package/assets/specs-templates/tasks.yml +1 -1
- package/dist/cli/index.js +2499 -3641
- package/package.json +6 -2
|
@@ -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
|
|
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
|
-
##
|
|
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
|
|
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
|
-
##
|
|
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
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 }
|