Complete CASAN backlog tier 1 controls
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user