Complete CASAN backlog tier 1 controls

This commit is contained in:
thanhnv
2026-07-06 11:31:17 +09:00
parent 571c2b7e80
commit e79d7973fb
22 changed files with 685 additions and 55 deletions
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""CASAN Plan-10 traceability matrix generator/gate.
Parses FR-* requirements from docs/input/okr-requirement.md and checks each
requirement has at least one existing code file and one existing test file.
"""
import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
FR_RE = re.compile(r"\|\s*(FR-\d+)\s*\|\s*([^|]+?)\s*\|")
def project_root() -> str:
return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
def parse_requirements(path: str):
seen = {}
with open(path, encoding="utf-8") as f:
for line in f:
match = FR_RE.search(line)
if not match:
continue
fr_id, name = match.groups()
if fr_id not in seen:
seen[fr_id] = {"id": fr_id, "name": " ".join(name.split())}
return [seen[k] for k in sorted(seen)]
def existing_files(root: str, values):
present, missing = [], []
for rel in values or []:
if os.path.isfile(os.path.join(root, rel)):
present.append(rel)
else:
missing.append(rel)
return present, missing
def main() -> int:
root = project_root()
ap = argparse.ArgumentParser()
ap.add_argument("--requirements", default=os.path.join(root, "docs/input/okr-requirement.md"))
ap.add_argument("--map", default=os.path.join(root, ".specify/traceability-map.json"))
ap.add_argument("--out", default=os.path.join(root, "docs/output/casan/traceability-matrix.json"))
ap.add_argument("--gate", action="store_true")
args = ap.parse_args()
reqs = parse_requirements(args.requirements)
with open(args.map, encoding="utf-8") as f:
mapping = json.load(f)
rows = []
failures = []
for req in reqs:
entry = mapping.get(req["id"], {})
code, missing_code = existing_files(root, entry.get("code", []))
tests, missing_tests = existing_files(root, entry.get("tests", []))
status = "PASS" if code and tests and not missing_code and not missing_tests else "FAIL"
row = {
"id": req["id"],
"name": req["name"],
"status": status,
"code": code,
"tests": tests,
"missing_code": missing_code,
"missing_tests": missing_tests,
}
rows.append(row)
if status != "PASS":
failures.append(row)
orphan_mappings = sorted(set(mapping) - {r["id"] for r in reqs})
out = {
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"requirements_source": os.path.relpath(args.requirements, root),
"mapping_source": os.path.relpath(args.map, root),
"summary": {
"requirements": len(reqs),
"passed": sum(1 for r in rows if r["status"] == "PASS"),
"failed": len(failures),
"orphan_mappings": orphan_mappings,
},
"matrix": rows,
}
os.makedirs(os.path.dirname(args.out), exist_ok=True)
with open(args.out, "w", encoding="utf-8") as f:
json.dump(out, f, indent=2, ensure_ascii=False)
f.write("\n")
if failures:
for row in failures:
print(
f"TRACEABILITY_FAIL {row['id']} code={len(row['code'])} tests={len(row['tests'])} "
f"missing_code={len(row['missing_code'])} missing_tests={len(row['missing_tests'])}",
file=sys.stderr,
)
if orphan_mappings:
print(f"TRACEABILITY_WARN orphan_mappings={','.join(orphan_mappings)}", file=sys.stderr)
print(
f"TRACEABILITY_MATRIX requirements={len(reqs)} pass={out['summary']['passed']} "
f"fail={len(failures)} out={args.out}"
)
if args.gate and failures:
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())