clay-borg/tools/rule-coverage.py
tegwick 27016af216 Add the AM-1 rule-coverage gate
make coverage compares the GR-rule IDs defined in specs/GroundRules.md
against the covers: lists in scenarios/ground/*.yaml. It exits 1 if a
scenario claims a rule the spec does not define, so coverage cannot be
inflated by an invented ID, and exits 2 while rules remain uncovered.

Current reading: 34/58 (58%). AM-1 requires 100%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 02:23:38 +02:00

34 lines
1.1 KiB
Python
Executable file

#!/usr/bin/env python3
"""AM-1 / M-D1-COV: every numbered GR-rule needs >=1 scenario.
Compares the rule IDs declared in specs/GroundRules.md against the
`covers:` lists in scenarios/ground/*.yaml. Exits non-zero when a
scenario claims a rule the spec does not define, so coverage can never
be inflated by a typo'd or invented rule ID.
"""
import glob
import re
import sys
spec = open("specs/GroundRules.md").read()
rules = sorted(set(re.findall(r"\*\*(GR-[A-Z]+\d+)", spec)))
covered = set()
for path in sorted(glob.glob("scenarios/ground/*.yaml")):
match = re.search(r"covers: \[(.*?)\]", open(path).read(), re.S)
if match:
covered |= {c.strip() for c in match.group(1).split(",") if c.strip()}
known = set(rules)
hit = sorted(known & covered)
missing = [r for r in rules if r not in covered]
invented = sorted(covered - known)
pct = 100 * len(hit) // len(rules) if rules else 0
print(f"AM-1 rule coverage: {len(hit)}/{len(rules)} ({pct}%)")
if missing:
print(" uncovered:", " ".join(missing))
if invented:
print(" ERROR — claimed but not defined in the spec:", " ".join(invented))
sys.exit(1)
sys.exit(0 if not missing else 2)