contract-driven-delivery 1.0.1 → 1.6.0

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.
Files changed (58) hide show
  1. package/README.md +96 -1
  2. package/assets/CLAUDE.template.md +59 -3
  3. package/assets/agents/backend-engineer.md +43 -0
  4. package/assets/agents/change-classifier.md +40 -0
  5. package/assets/agents/ci-cd-gatekeeper.md +53 -4
  6. package/assets/agents/contract-reviewer.md +49 -3
  7. package/assets/agents/dependency-security-reviewer.md +95 -0
  8. package/assets/agents/e2e-resilience-engineer.md +42 -1
  9. package/assets/agents/frontend-engineer.md +44 -1
  10. package/assets/agents/monkey-test-engineer.md +40 -1
  11. package/assets/agents/qa-reviewer.md +52 -0
  12. package/assets/agents/repo-context-scanner.md +40 -0
  13. package/assets/agents/spec-architect.md +77 -3
  14. package/assets/agents/spec-drift-auditor.md +40 -0
  15. package/assets/agents/stress-soak-engineer.md +42 -0
  16. package/assets/agents/test-strategist.md +44 -1
  17. package/assets/agents/ui-ux-reviewer.md +41 -1
  18. package/assets/agents/visual-reviewer.md +41 -1
  19. package/assets/ci/github-actions/contract-driven-gates.yml +50 -5
  20. package/assets/ci-templates/bun.yml +5 -0
  21. package/assets/ci-templates/conda.yml +11 -0
  22. package/assets/ci-templates/go.yml +12 -0
  23. package/assets/ci-templates/npm.yml +6 -0
  24. package/assets/ci-templates/pip.yml +10 -0
  25. package/assets/ci-templates/pnpm.yml +9 -0
  26. package/assets/ci-templates/poetry.yml +12 -0
  27. package/assets/ci-templates/rust.yml +12 -0
  28. package/assets/ci-templates/unknown.yml +4 -0
  29. package/assets/ci-templates/uv.yml +12 -0
  30. package/assets/ci-templates/yarn.yml +6 -0
  31. package/assets/contracts/CHANGELOG.md +27 -0
  32. package/assets/contracts/api/api-contract.md +7 -0
  33. package/assets/contracts/business/business-rules.md +7 -0
  34. package/assets/contracts/ci/ci-gate-contract.md +7 -0
  35. package/assets/contracts/css/css-contract.md +7 -0
  36. package/assets/contracts/data/data-shape-contract.md +7 -0
  37. package/assets/contracts/env/env-contract.md +7 -0
  38. package/assets/hooks/pre-commit +23 -0
  39. package/assets/skill/SKILL.md +20 -4
  40. package/assets/skill/scripts/detect_project_profile.py +68 -1
  41. package/assets/skill/scripts/generate_change_scaffold.py +2 -2
  42. package/assets/skill/scripts/validate_api_semantic.py +162 -0
  43. package/assets/skill/scripts/validate_ci_gates.py +34 -6
  44. package/assets/skill/scripts/validate_contract_versions.py +385 -0
  45. package/assets/skill/scripts/validate_contracts.py +25 -1
  46. package/assets/skill/scripts/validate_env_contract.py +3 -1
  47. package/assets/skill/scripts/validate_env_semantic.py +182 -0
  48. package/assets/skill/scripts/validate_spec_traceability.py +34 -8
  49. package/assets/tests-templates/soak/k6-example.js +19 -0
  50. package/assets/tests-templates/soak/locust-example.py +21 -0
  51. package/assets/tests-templates/soak/soak-profile.md +16 -0
  52. package/assets/tests-templates/stress/artillery-example.yml +27 -0
  53. package/assets/tests-templates/stress/k6-example.js +22 -0
  54. package/assets/tests-templates/stress/load-profile.md +14 -0
  55. package/assets/tests-templates/stress/locust-example.py +21 -0
  56. package/dist/cli/index.js +593 -106
  57. package/package.json +6 -3
  58. package/assets/skill/agents/openai.yaml +0 -2
@@ -1,3 +1,7 @@
1
+ # Contract-driven gates baseline (provided by cdd-kit).
2
+ # Contract validation steps run as-is; application/test commands MUST be customized for your stack.
3
+ # See README.md and ci/gate-policy.md for tier semantics and promotion policy.
4
+
1
5
  name: contract-driven-gates
2
6
 
3
7
  on:
@@ -11,28 +15,69 @@ jobs:
11
15
  runs-on: ubuntu-latest
12
16
  steps:
13
17
  - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: '3.10'
23
+
14
24
  - name: Detect project profile
15
25
  run: python .claude/skills/contract-driven-delivery/scripts/detect_project_profile.py . --write project-profile.generated.md || true
26
+
16
27
  - name: Validate contracts
17
28
  run: python .claude/skills/contract-driven-delivery/scripts/validate_contracts.py .
29
+
18
30
  - name: Validate env contract
19
31
  run: python .claude/skills/contract-driven-delivery/scripts/validate_env_contract.py contracts/env/env-contract.md
32
+
33
+ - name: Validate ci gates
34
+ run: python .claude/skills/contract-driven-delivery/scripts/validate_ci_gates.py
35
+
36
+ - name: Validate spec traceability
37
+ run: python .claude/skills/contract-driven-delivery/scripts/validate_spec_traceability.py
38
+
39
+ - name: Validate contract versions
40
+ run: python .claude/skills/contract-driven-delivery/scripts/validate_contract_versions.py .
41
+
20
42
  - name: Repository-specific fast gate
21
43
  run: |
22
- echo "Replace with repo commands: lint, typecheck, build, unit, contract tests"
44
+ # TODO: add your stack-specific commands here.
45
+ # This step is intentionally left as a placeholder — cdd-kit does not assume your stack.
46
+ # Examples of what belongs here:
47
+ # lint: npx eslint src/ / ruff check . / golangci-lint run
48
+ # typecheck: npx tsc --noEmit / mypy src/
49
+ # build: npm run build / go build ./...
50
+ # unit tests: your test runner command (jest, vitest, pytest, go test, etc.)
51
+ echo "No stack-specific fast gate configured — add lint/typecheck/build/unit-test commands above."
23
52
 
24
53
  e2e-critical:
25
54
  if: github.event_name == 'pull_request'
26
55
  runs-on: ubuntu-latest
27
56
  steps:
28
57
  - uses: actions/checkout@v4
29
- - name: Critical E2E placeholder
30
- run: echo "Replace with Playwright/Cypress critical journey command"
58
+
59
+ - name: Critical E2E
60
+ run: |
61
+ # TODO: add your critical E2E command here (Tier 1 — blocks merge).
62
+ # This step is intentionally left as a placeholder — cdd-kit does not assume your stack.
63
+ # Examples:
64
+ # Playwright: npx playwright test --project=critical
65
+ # Cypress: npx cypress run --spec "cypress/e2e/critical/**"
66
+ # pytest: python -m pytest tests/e2e/critical/ -x
67
+ echo "No critical E2E command configured — add your runner command above."
31
68
 
32
69
  scheduled-stress-soak:
33
70
  if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
34
71
  runs-on: ubuntu-latest
35
72
  steps:
36
73
  - uses: actions/checkout@v4
37
- - name: Stress/soak placeholder
38
- run: echo "Replace with production-like stress/soak workflow"
74
+
75
+ - name: Stress and soak tests
76
+ run: |
77
+ # TODO: add your stress/soak workflow here (Tier 4 — weekly long-run gate).
78
+ # This step is intentionally left as a placeholder — cdd-kit does not assume your stack.
79
+ # Examples:
80
+ # pytest soak: python -m pytest tests/stress/ --soak-hours=4
81
+ # k6 load: k6 run --vus=50 --duration=4h tests/load/scenario.js
82
+ # locust: locust -f tests/stress/locustfile.py --headless -u 100 -r 10 --run-time 4h
83
+ echo "No stress/soak command configured — add your runner command above."
@@ -0,0 +1,5 @@
1
+ - uses: oven-sh/setup-bun@v1
2
+ with:
3
+ bun-version: latest
4
+ - run: bun install --frozen-lockfile
5
+ - run: bun run lint && bun run typecheck && bun test
@@ -0,0 +1,11 @@
1
+ - uses: conda-incubator/setup-miniconda@v3
2
+ with:
3
+ environment-file: environment.yml
4
+ activate-environment: "" # 空字串讓 conda 從 environment.yml 讀 name
5
+ auto-activate-base: false
6
+ - name: Lint + Type + Test
7
+ shell: bash -el {0}
8
+ run: |
9
+ ruff check . || true
10
+ mypy src/ || true
11
+ pytest
@@ -0,0 +1,12 @@
1
+ - uses: actions/setup-go@v5
2
+ with:
3
+ go-version: '1.22'
4
+ cache: true
5
+ - name: Lint
6
+ run: |
7
+ go vet ./... || true
8
+ which golangci-lint && golangci-lint run || true
9
+ - name: Build + Test
10
+ run: |
11
+ go build ./...
12
+ go test ./...
@@ -0,0 +1,6 @@
1
+ - uses: actions/setup-node@v4
2
+ with:
3
+ node-version: '20'
4
+ cache: 'npm'
5
+ - run: npm ci
6
+ - run: npm run lint && npm run typecheck && npm test
@@ -0,0 +1,10 @@
1
+ - uses: actions/setup-python@v5
2
+ with:
3
+ python-version: '3.11'
4
+ - name: Install dependencies
5
+ run: pip install -r requirements.txt
6
+ - name: Lint + Type + Test
7
+ run: |
8
+ ruff check . || true
9
+ mypy src/ || true
10
+ pytest
@@ -0,0 +1,9 @@
1
+ - uses: pnpm/action-setup@v3
2
+ with:
3
+ version: 9
4
+ - uses: actions/setup-node@v4
5
+ with:
6
+ node-version: '20'
7
+ cache: 'pnpm'
8
+ - run: pnpm install --frozen-lockfile
9
+ - run: pnpm run lint && pnpm run typecheck && pnpm test
@@ -0,0 +1,12 @@
1
+ - uses: actions/setup-python@v5
2
+ with:
3
+ python-version: '3.11'
4
+ - name: Install Poetry
5
+ run: pip install poetry
6
+ - name: Install dependencies
7
+ run: poetry install --no-interaction
8
+ - name: Lint + Type + Test
9
+ run: |
10
+ poetry run ruff check . || true
11
+ poetry run mypy src/ || true
12
+ poetry run pytest
@@ -0,0 +1,12 @@
1
+ - uses: dtolnay/rust-toolchain@stable
2
+ with:
3
+ components: clippy, rustfmt
4
+ - uses: Swatinem/rust-cache@v2
5
+ - name: Lint
6
+ run: |
7
+ cargo fmt --check || true
8
+ cargo clippy -- -D warnings || true
9
+ - name: Build + Test
10
+ run: |
11
+ cargo build
12
+ cargo test
@@ -0,0 +1,4 @@
1
+ - name: Repository-specific fast gate (unconfigured)
2
+ run: |
3
+ # TODO: cdd-kit could not detect a stack. Add lint/typecheck/test here.
4
+ echo "No stack detected — fill this step in manually."
@@ -0,0 +1,12 @@
1
+ - uses: actions/setup-python@v5
2
+ with:
3
+ python-version: '3.11'
4
+ - name: Install uv
5
+ run: pip install uv
6
+ - name: Install dependencies
7
+ run: uv sync
8
+ - name: Lint + Type + Test
9
+ run: |
10
+ uv run ruff check . || true
11
+ uv run mypy src/ || true
12
+ uv run pytest
@@ -0,0 +1,6 @@
1
+ - uses: actions/setup-node@v4
2
+ with:
3
+ node-version: '20'
4
+ cache: 'yarn'
5
+ - run: yarn install --frozen-lockfile
6
+ - run: yarn lint && yarn typecheck && yarn test
@@ -0,0 +1,27 @@
1
+ # Contracts Changelog
2
+
3
+ All notable contract surface changes belong here.
4
+ Format: Keep-a-Changelog (https://keepachangelog.com/).
5
+ Versions are semantic per contract type.
6
+
7
+ While a contract is at 0.x (draft), entries here are optional.
8
+ Once a contract reaches 1.0.0, every schema-version bump must have
9
+ a corresponding entry below.
10
+
11
+ ## [api 0.1.0] — 2026-04-27
12
+ Initial draft.
13
+
14
+ ## [css 0.1.0] — 2026-04-27
15
+ Initial draft.
16
+
17
+ ## [env 0.1.0] — 2026-04-27
18
+ Initial draft.
19
+
20
+ ## [data 0.1.0] — 2026-04-27
21
+ Initial draft.
22
+
23
+ ## [business 0.1.0] — 2026-04-27
24
+ Initial draft.
25
+
26
+ ## [ci 0.1.0] — 2026-04-27
27
+ Initial draft.
@@ -1,3 +1,10 @@
1
+ ---
2
+ contract: api
3
+ schema-version: 0.1.0
4
+ last-changed: 2026-04-27
5
+ breaking-change-policy: deprecate-2-minors
6
+ ---
7
+
1
8
  # API Contract
2
9
 
3
10
  ## API Style
@@ -1,3 +1,10 @@
1
+ ---
2
+ contract: business
3
+ schema-version: 0.1.0
4
+ last-changed: 2026-04-27
5
+ breaking-change-policy: deprecate-2-minors
6
+ ---
7
+
1
8
  # Business Rules
2
9
 
3
10
  ## Rule Inventory
@@ -1,3 +1,10 @@
1
+ ---
2
+ contract: ci
3
+ schema-version: 0.1.0
4
+ last-changed: 2026-04-27
5
+ breaking-change-policy: deprecate-2-minors
6
+ ---
7
+
1
8
  # CI/CD Gate Contract
2
9
 
3
10
  ## Gate Inventory
@@ -1,3 +1,10 @@
1
+ ---
2
+ contract: css
3
+ schema-version: 0.1.0
4
+ last-changed: 2026-04-27
5
+ breaking-change-policy: deprecate-2-minors
6
+ ---
7
+
1
8
  # CSS / UI Contract
2
9
 
3
10
  ## Token Source of Truth
@@ -1,3 +1,10 @@
1
+ ---
2
+ contract: data
3
+ schema-version: 0.1.0
4
+ last-changed: 2026-04-27
5
+ breaking-change-policy: deprecate-2-minors
6
+ ---
7
+
1
8
  # Data Shape Contract
2
9
 
3
10
  ## Required Columns
@@ -1,3 +1,10 @@
1
+ ---
2
+ contract: env
3
+ schema-version: 0.1.0
4
+ last-changed: 2026-04-27
5
+ breaking-change-policy: deprecate-2-minors
6
+ ---
7
+
1
8
  # Env Contract
2
9
 
3
10
  | name | scope | environments | required | secret | default | example | owner | validation | restart required | failure behavior |
@@ -0,0 +1,23 @@
1
+ #!/bin/sh
2
+ # cdd-kit-managed-block-start
3
+ # Auto-runs cdd-kit gate for any change folder touched in this commit.
4
+ # Generated by: cdd-kit install-hooks (re-run to update).
5
+ # Bypass with --no-verify (NOT recommended; defeats AI process enforcement).
6
+
7
+ set -e
8
+
9
+ staged=$(git diff --cached --name-only --diff-filter=ACM)
10
+ [ -z "$staged" ] && exit 0
11
+
12
+ change_ids=$(echo "$staged" | grep -oE '^specs/changes/[^/]+' | sort -u | sed 's|specs/changes/||')
13
+ [ -z "$change_ids" ] && exit 0
14
+
15
+ for id in $change_ids; do
16
+ echo "[cdd-kit] running gate for change: $id"
17
+ if ! cdd-kit gate "$id"; then
18
+ echo "[cdd-kit] gate failed for $id — commit rejected."
19
+ echo "[cdd-kit] fix issues above; --no-verify bypasses but defeats the kit."
20
+ exit 1
21
+ fi
22
+ done
23
+ # cdd-kit-managed-block-end
@@ -13,32 +13,47 @@ Use this skill to turn software requests into traceable, testable, CI/CD-gated c
13
13
 
14
14
  1. Classify the request.
15
15
  - Use `references/workflow-router.md`.
16
- - Create or update `classification.md`.
16
+ - Create or update `change-classification.md`.
17
+ - Invoke change-classifier to perform classification.
17
18
  2. Scan project context.
18
19
  - Use `scripts/detect_project_profile.py` when useful.
19
20
  - Capture stack, commands, contracts, tests, and CI/CD.
21
+ - Invoke repo-context-scanner to capture project profile and standardization gaps.
20
22
  3. Select required artifacts.
21
23
  - Use templates in `templates/`.
22
- - Do not force every artifact for tiny changes, but do require `classification.md`, `test-plan.md`, and `ci-gates.md` for implementation changes.
23
- 4. Update contracts before or alongside implementation.
24
+ - Do not force every artifact for tiny changes, but do require `change-classification.md`, `test-plan.md`, and `ci-gates.md` for implementation changes.
25
+ 4. Update contracts before or alongside implementation. Invoke contract-reviewer to validate API/CSS/env/data/business/CI-CD contracts before or alongside implementation.
24
26
  - API: `references/api-contract-standard.md`
25
27
  - CSS/UI: `references/css-contract-standard.md`
26
28
  - Env: `references/env-contract-standard.md`
27
29
  - Data/report shape: `references/data-contract-standard.md`
28
30
  - Business logic: `references/business-logic-standard.md`
29
31
  - CI/CD: `references/ci-cd-policy.md`
30
- 5. Apply SDD + TDD discipline.
32
+ - Deps/migrations: invoke `dependency-security-reviewer` whenever the change touches lockfiles, dependency manifests, or database migrations.
33
+ 5. Apply SDD + TDD discipline and commission test engineers.
31
34
  - Use `references/sdd-tdd-policy.md`.
32
35
  - Tests should be planned before implementation and should fail first when feasible.
36
+ - `test-strategist` authors the test plan (write target: specs/changes/<id>/test-plan.md only).
37
+ - `e2e-resilience-engineer` implements E2E, failure-injection, and data-boundary tests.
38
+ - `monkey-test-engineer` implements adversarial-input, fuzz, and rapid-UI-action tests.
39
+ - `stress-soak-engineer` implements load, soak, and long-running stability tests.
40
+ - Invoke the relevant test engineer(s) before or alongside implementation based on the risk tier.
41
+ - Each engineer must read the matching standard before authoring tests: e2e-resilience-engineer → references/e2e-standard.md, monkey-test-engineer → references/monkey-operation-standard.md, stress-soak-engineer → references/stress-soak-standard.md.
33
42
  6. Implement through the right role.
34
43
  - Backend/frontend work must follow contracts and tests.
35
44
  - UI changes require UI/UX and visual review.
45
+ - Invoke ui-ux-reviewer for interaction, copy, accessibility, and information hierarchy review whenever UI changes.
46
+ - Invoke visual-reviewer for layout, responsive, CSS contract, and screenshot diff review whenever UI changes.
47
+ - If implementation reveals an unexpected boundary or architectural constraint, halt and re-invoke `spec-architect` before continuing.
36
48
  7. Run quality gates.
37
49
  - Use `references/qa-gates.md`.
38
50
  - CI/CD gate plan is mandatory.
51
+ - `qa-reviewer` decides release readiness; Tier 1 gates must be green; Tier 3+ gates must be green or explicitly deferred with a recorded promotion policy.
52
+ - Invoke ci-cd-gatekeeper to design and enforce the gate plan.
39
53
  8. Archive and audit drift.
40
54
  - Use `references/spec-drift-policy.md`.
41
55
  - Durable learnings must be promoted back to contracts or CLAUDE.md.
56
+ - `spec-drift-auditor` must run before every release to main and weekly during active multi-iteration development.
42
57
 
43
58
  ## Required gates by risk
44
59
 
@@ -66,6 +81,7 @@ Use this skill to turn software requests into traceable, testable, CI/CD-gated c
66
81
  - visual review evidence
67
82
  - E2E or component interaction coverage
68
83
  - accessibility check
84
+ - See references/visual-review-standard.md for the required state matrix.
69
85
 
70
86
  ### API/backend/data/report change
71
87
 
@@ -8,12 +8,54 @@ BACKEND_MARKERS = ['pyproject.toml', 'requirements.txt', 'app.py', 'main.py', 'm
8
8
  CI_MARKERS = ['.github/workflows', '.gitlab-ci.yml', 'bitbucket-pipelines.yml', '.circleci']
9
9
  CONTRACT_DIRS = ['contracts', 'contract']
10
10
 
11
+ # The cdd-kit skill directory name — used to detect whether cdd-kit is installed.
12
+ CDD_KIT_SKILL = 'contract-driven-delivery'
13
+
11
14
  def exists(root: Path, rel: str) -> bool:
12
15
  return (root / rel).exists()
13
16
 
14
17
  def list_existing(root: Path, rels):
15
18
  return [r for r in rels if exists(root, r)]
16
19
 
20
+ def cdd_kit_installed(root: Path) -> bool:
21
+ """Return True if cdd-kit's skill is present in this repo's .claude directory."""
22
+ return (root / '.claude' / 'skills' / CDD_KIT_SKILL).exists()
23
+
24
+ def classify_agents(root: Path, kit_installed: bool):
25
+ """
26
+ Return (kit_agents, user_agents) lists of agent .md basenames.
27
+
28
+ When cdd-kit is installed, ALL agents found under .claude/agents/ are
29
+ assumed to originate from the kit installation (the kit ships its own
30
+ agent suite and deploys it there). Users who add their own agents
31
+ alongside the kit should note this in their AGENTS.md.
32
+ If cdd-kit is NOT installed, all agents are treated as user-authored.
33
+ """
34
+ agents_dir = root / '.claude' / 'agents'
35
+ if not agents_dir.is_dir():
36
+ return [], []
37
+ all_agents = sorted(p.name for p in agents_dir.glob('*.md'))
38
+ if not all_agents:
39
+ return [], []
40
+ if kit_installed:
41
+ return all_agents, []
42
+ return [], all_agents
43
+
44
+ def classify_skills(root: Path, kit_installed: bool):
45
+ """
46
+ Return (kit_skills, user_skills) lists of skill directory names.
47
+
48
+ contract-driven-delivery is always flagged as a cdd-kit skill when present.
49
+ Other skills are user-authored.
50
+ """
51
+ skills_dir = root / '.claude' / 'skills'
52
+ if not skills_dir.is_dir():
53
+ return [], []
54
+ all_skills = sorted(p.name for p in skills_dir.iterdir() if p.is_dir())
55
+ kit_skills = [s for s in all_skills if s == CDD_KIT_SKILL]
56
+ user_skills = [s for s in all_skills if s != CDD_KIT_SKILL]
57
+ return kit_skills, user_skills
58
+
17
59
  def detect_commands(root: Path):
18
60
  commands = {}
19
61
  pkg = root / 'package.json'
@@ -32,18 +74,42 @@ def main():
32
74
  ap.add_argument('--write', help='optional output markdown path')
33
75
  args = ap.parse_args()
34
76
  root = Path(args.root).resolve()
77
+
35
78
  frontend = list_existing(root, FRONTEND_MARKERS)
36
79
  backend = list_existing(root, BACKEND_MARKERS)
37
80
  ci = list_existing(root, CI_MARKERS)
38
81
  contracts = [d for d in CONTRACT_DIRS if (root/d).exists()]
39
82
  tests = [p for p in ['tests','frontend/tests','e2e','playwright.config.ts','playwright.config.js'] if (root/p).exists()]
40
83
  commands = detect_commands(root)
41
- lines = ['# Project Profile', '', f'## Root', str(root), '', '## Detected Markers']
84
+
85
+ kit_installed = cdd_kit_installed(root)
86
+ kit_agents, user_agents = classify_agents(root, kit_installed)
87
+ kit_skills, user_skills = classify_skills(root, kit_installed)
88
+
89
+ lines = ['# Project Profile', '', '## Root', str(root), '', '## Detected Markers']
42
90
  lines += ['- frontend: ' + (', '.join(frontend) if frontend else 'none detected')]
43
91
  lines += ['- backend: ' + (', '.join(backend) if backend else 'none detected')]
44
92
  lines += ['- contracts: ' + (', '.join(contracts) if contracts else 'none detected')]
45
93
  lines += ['- tests: ' + (', '.join(tests) if tests else 'none detected')]
46
94
  lines += ['- ci/cd: ' + (', '.join(ci) if ci else 'none detected')]
95
+
96
+ lines += ['', '## Claude Assets (.claude/)']
97
+ if kit_installed:
98
+ lines += [f'- cdd-kit detected (skill: {CDD_KIT_SKILL})']
99
+ if kit_agents:
100
+ lines += [f'- agents installed by cdd-kit ({len(kit_agents)}): ' + ', '.join(kit_agents)]
101
+ lines += [' (these are bundled by cdd-kit; not user-authored)']
102
+ if user_agents:
103
+ lines += [f'- user-authored agents ({len(user_agents)}): ' + ', '.join(user_agents)]
104
+ if not kit_agents and not user_agents:
105
+ lines += ['- agents: none detected']
106
+ if kit_skills:
107
+ lines += [f'- skills installed by cdd-kit ({len(kit_skills)}): ' + ', '.join(kit_skills)]
108
+ if user_skills:
109
+ lines += [f'- user-authored skills ({len(user_skills)}): ' + ', '.join(user_skills)]
110
+ if not kit_skills and not user_skills:
111
+ lines += ['- skills: none detected']
112
+
47
113
  lines += ['', '## Suggested Commands']
48
114
  if commands:
49
115
  lines += [f'- {k}: `{v}`' for k,v in sorted(commands.items())]
@@ -57,5 +123,6 @@ def main():
57
123
  if args.write:
58
124
  Path(args.write).write_text(out, encoding='utf-8')
59
125
  print(out)
126
+
60
127
  if __name__ == '__main__':
61
128
  main()
@@ -15,8 +15,8 @@ def main():
15
15
  dest=root/'specs'/'changes'/args.change_id
16
16
  dest.mkdir(parents=True, exist_ok=False)
17
17
  mapping={
18
- 'change-request.md':'request.md',
19
- 'change-classification.md':'classification.md',
18
+ 'change-request.md':'change-request.md',
19
+ 'change-classification.md':'change-classification.md',
20
20
  'current-behavior.md':'current-behavior.md',
21
21
  'proposal.md':'proposal.md',
22
22
  'spec.md':'spec.md',
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python3
2
+ """Semantic validation of the API contract endpoint table.
3
+
4
+ Reads contracts/api/api-contract.md (relative to cwd), skips YAML frontmatter,
5
+ finds the endpoint table (first markdown table whose header starts with '| method |'),
6
+ and validates each data row for:
7
+ - method ∈ VALID_METHODS
8
+ - path starts with '/'
9
+ - auth ∈ VALID_AUTH
10
+ - at least 5 columns present
11
+ """
12
+ import sys
13
+ import re
14
+ from pathlib import Path
15
+
16
+ VALID_METHODS = {'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'}
17
+ VALID_AUTH = {'required', 'optional', 'admin', 'none', 'public'}
18
+
19
+ CONTRACT_PATH = Path('contracts/api/api-contract.md')
20
+
21
+
22
+ def strip_frontmatter(text: str) -> str:
23
+ """Remove YAML frontmatter delimited by --- ... ---."""
24
+ if text.startswith('---'):
25
+ end = text.find('\n---', 3)
26
+ if end != -1:
27
+ return text[end + 4:].lstrip('\n')
28
+ return text
29
+
30
+
31
+ def parse_table_row(line: str) -> list[str]:
32
+ """Split a markdown table row into stripped cell values."""
33
+ # Remove leading/trailing pipes and whitespace, then split on '|'
34
+ row = line.strip().strip('|')
35
+ return [cell.strip() for cell in row.split('|')]
36
+
37
+
38
+ def is_separator_row(cells: list[str]) -> bool:
39
+ """Return True if this is a markdown table separator (---|---|...)."""
40
+ return all(re.match(r'^:?-+:?$', c) for c in cells if c)
41
+
42
+
43
+ def find_endpoint_table(lines: list[str]) -> list[tuple[int, str]]:
44
+ """
45
+ Find all data rows across ALL '| method |' tables in the document.
46
+ Blank lines and prose between rows do not end collection, making this
47
+ robust to files where content is appended after the original table block.
48
+ """
49
+ in_table = False
50
+ sep_seen = False
51
+ data_rows: list[tuple[int, str]] = []
52
+
53
+ for i, line in enumerate(lines):
54
+ stripped = line.strip()
55
+
56
+ if not stripped:
57
+ continue # blank lines never end table mode
58
+
59
+ if not stripped.startswith('|'):
60
+ # Non-pipe line: keep searching (don't break)
61
+ # A new `## heading` may precede a duplicate table header
62
+ continue
63
+
64
+ cells = parse_table_row(stripped)
65
+ if not cells:
66
+ continue
67
+
68
+ # A header row for an endpoint table
69
+ if cells[0].lower() == 'method':
70
+ in_table = True
71
+ sep_seen = False
72
+ continue # skip header row
73
+
74
+ if not in_table:
75
+ continue
76
+
77
+ # Separator row
78
+ if not sep_seen and is_separator_row(cells):
79
+ sep_seen = True
80
+ continue
81
+
82
+ # Data row
83
+ data_rows.append((i + 1, line)) # 1-based line numbers
84
+
85
+ return data_rows
86
+
87
+
88
+ def main() -> None:
89
+ cwd = Path('.')
90
+ contract = cwd / CONTRACT_PATH
91
+
92
+ if not contract.exists():
93
+ print(f'API contract not found: {CONTRACT_PATH}')
94
+ sys.exit(1)
95
+
96
+ try:
97
+ raw = contract.read_text(encoding='utf-8', errors='ignore')
98
+ except OSError as e:
99
+ print(f'Cannot read {CONTRACT_PATH}: {e}')
100
+ sys.exit(1)
101
+
102
+ if not raw.strip():
103
+ print('API contract: file is empty.')
104
+ sys.exit(1)
105
+
106
+ body = strip_frontmatter(raw)
107
+ lines = body.splitlines()
108
+
109
+ data_rows = find_endpoint_table(lines)
110
+
111
+ if not data_rows:
112
+ print('API contract: no endpoint table found')
113
+ sys.exit(1)
114
+
115
+ errors: list[str] = []
116
+
117
+ for lineno, raw_line in data_rows:
118
+ cells = parse_table_row(raw_line)
119
+
120
+ # Skip entirely empty rows (blank table filler lines)
121
+ if not any(cells):
122
+ continue
123
+
124
+ # Must have at least 5 columns: method, path, auth, request, response
125
+ if len(cells) < 5:
126
+ errors.append(
127
+ f'Line {lineno}: row has only {len(cells)} column(s), need at least 5: {raw_line.strip()}'
128
+ )
129
+ continue
130
+
131
+ method = cells[0].upper()
132
+ path = cells[1]
133
+ auth = cells[2].lower()
134
+
135
+ if method not in VALID_METHODS:
136
+ errors.append(
137
+ f'Line {lineno}: invalid method "{cells[0]}" '
138
+ f'(valid: {", ".join(sorted(VALID_METHODS))})'
139
+ )
140
+
141
+ if not path.startswith('/'):
142
+ errors.append(
143
+ f'Line {lineno}: path "{path}" does not start with "/"'
144
+ )
145
+
146
+ if auth not in VALID_AUTH:
147
+ errors.append(
148
+ f'Line {lineno}: invalid auth "{cells[2]}" '
149
+ f'(valid: {", ".join(sorted(VALID_AUTH))})'
150
+ )
151
+
152
+ if errors:
153
+ print('API semantic validation failed:')
154
+ for err in errors:
155
+ print(f' {err}')
156
+ sys.exit(1)
157
+
158
+ print(f'API semantic validation passed ({len(data_rows)} endpoint(s) checked).')
159
+
160
+
161
+ if __name__ == '__main__':
162
+ main()