Files
claude-code/scripts/validate_cc2_board.py
2026-05-14 16:58:43 +09:00

88 lines
3.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Validate the generated Claw Code 2.0 board coverage and schema."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
REQUIRED = {
"id",
"title",
"source_anchor",
"source_type",
"release_bucket",
"status",
"dependencies",
"verification_required",
"deferral_rationale",
}
STATUSES = {
"context",
"active",
"open",
"done_verify",
"stale_done",
"superseded",
"deferred_with_rationale",
"rejected_not_claw",
}
def roadmap_heading_lines(path: Path) -> list[int]:
lines = []
for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if re.match(r"^#{1,6}\s+", line):
lines.append(line_no)
return lines
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path.cwd())
parser.add_argument("--board", type=Path, default=None)
args = parser.parse_args()
repo_root = args.repo_root.resolve()
board_path = args.board or (repo_root / ".omx" / "cc2" / "board.json")
board = json.loads(board_path.read_text(encoding="utf-8"))
errors: list[str] = []
ids = set()
for index, item in enumerate(board.get("items", []), 1):
missing = REQUIRED - set(item)
if missing:
errors.append(f"item {index} missing required fields: {sorted(missing)}")
if item.get("id") in ids:
errors.append(f"duplicate id: {item.get('id')}")
ids.add(item.get("id"))
if item.get("status") not in STATUSES:
errors.append(f"{item.get('id')} invalid status {item.get('status')}")
if not isinstance(item.get("dependencies"), list):
errors.append(f"{item.get('id')} dependencies must be list")
expected = roadmap_heading_lines(repo_root / "ROADMAP.md")
mapped = [item.get("source_line") for item in board.get("items", []) if item.get("source_type") == "roadmap_heading"]
unmapped = sorted(set(expected) - set(mapped))
duplicates = sorted(line for line in set(mapped) if mapped.count(line) != 1)
if unmapped:
errors.append(f"unmapped ROADMAP headings: {unmapped}")
if duplicates:
errors.append(f"duplicate ROADMAP heading mappings: {duplicates}")
coverage = board.get("coverage", {})
if coverage.get("roadmap_headings_total") != len(expected):
errors.append("coverage roadmap_headings_total does not match ROADMAP.md")
if coverage.get("roadmap_headings_mapped") != len(mapped):
errors.append("coverage roadmap_headings_mapped does not match board items")
if errors:
print("FAIL cc2 board validation")
for error in errors:
print(f"- {error}")
return 1
print("PASS cc2 board validation")
print(f"- board: {board_path}")
print(f"- items: {len(board.get('items', []))}")
print(f"- ROADMAP headings mapped: {len(mapped)}/{len(expected)}")
print(f"- ROADMAP actions mapped: {coverage.get('roadmap_actions_mapped')}/{coverage.get('roadmap_actions_total')}")
return 0
if __name__ == "__main__":
raise SystemExit(main())