#!/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)