#!/usr/bin/env python3
"""Parity for the agent instruction files themselves (REQ-163).

`workflow_chml_audit` already enforces byte parity across the delivery mirrors
and the three skeleton skill mirrors. `CLAUDE.md` and `AGENTS.md` were in
neither, so everything the instruction files point AT was checked and the
instruction files were not.

Byte parity is the wrong tool here: these four files are deliberately different.
`CLAUDE.md` restates the always-in-force rules for Claude Code; `AGENTS.md` is
the canonical set for every host; the skeleton pair is addressed to a consuming
repo rather than the hub. What must hold is that the same RULES reach all four,
and that a rule declared always-in-force is not quietly demoted to a routing
hint in one of them.

So the registry is explicit (`templates/agent-rule-parity.json`). Adding a rule
to the always-in-force section without registering it fails a test, and a
registered rule missing from any file fails this audit. That replaces the
scattered hand-written phrase probes, each of which only ever checked the one
rule its author happened to remember.
"""

import argparse
import json
import re
import sys
from pathlib import Path

MANIFEST_REL = "templates/agent-rule-parity.json"
INSTRUCTION_FILES = (
    "CLAUDE.md",
    "AGENTS.md",
    "templates/repo-skeleton/CLAUDE.md",
    "templates/repo-skeleton/AGENTS.md",
)
# In CLAUDE.md the always-in-force section runs until the skills table starts.
# Anything after this heading is a routing hint, not an obligation.
FOLD_HEADING = re.compile(r"^##\s+Skills", re.M)
BULLET = re.compile(r"^-\s+\*\*(.+?)\.?\*\*", re.M)


def _slug(text):
    text = re.sub(r"\(.*?\)", " ", text)
    text = re.sub(r"[^a-z0-9]+", "-", text.lower())
    return text.strip("-")


def always_in_force_rules(path):
    """Rule slugs from the bolded bullets of CLAUDE.md's always-in-force section."""
    text = Path(path).read_text(encoding="utf-8")
    fold = FOLD_HEADING.search(text)
    section = text[:fold.start()] if fold else text
    return [_slug(m.group(1)) for m in BULLET.finditer(section)]


def load_manifest(root):
    return json.loads((Path(root) / MANIFEST_REL).read_text(encoding="utf-8-sig"))


def _marker_for(rule, rel):
    markers = rule["markers"]
    return markers.get(rel, markers.get("*"))


def check_rule(rule, root, files=INSTRUCTION_FILES):
    """Findings for one rule across every instruction file."""
    findings = []
    root = Path(root)
    for rel in files:
        path = root / rel
        if not path.is_file():
            continue
        marker = _marker_for(rule, rel)
        if not marker:
            continue
        text = path.read_text(encoding="utf-8")
        if marker.lower() not in text.lower():
            findings.append({
                "severity": "high",
                "check": "instruction_parity",
                "message": (f"{rel} does not state the '{rule['id']}' rule "
                            f"(looked for {marker!r})"),
            })
            continue
        # Demotion only means something where there IS a fold: CLAUDE.md.
        if rule.get("always_in_force") and Path(rel).name == "CLAUDE.md":
            fold = FOLD_HEADING.search(text)
            if fold and text.lower().index(marker.lower()) > fold.start():
                findings.append({
                    "severity": "high",
                    "check": "instruction_demotion",
                    "message": (f"{rel} states '{rule['id']}' only below the skills "
                                f"heading, so it reads as a routing hint rather than "
                                f"an always-in-force rule"),
                })
    return findings


def audit(root="."):
    root = Path(root)
    try:
        manifest = load_manifest(root)
    except (OSError, ValueError) as exc:
        return [{"severity": "high", "check": "instruction_parity",
                 "message": f"cannot read {MANIFEST_REL}: {type(exc).__name__}"}]
    findings = []
    for rule in manifest.get("rules", []):
        findings.extend(check_rule(rule, root))
    return findings


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--repo-root", default=".")
    args = parser.parse_args(argv)
    findings = audit(args.repo_root)
    print(json.dumps({"status": "findings" if findings else "ok",
                      "findings": findings}, indent=2))
    return 1 if findings else 0


if __name__ == "__main__":
    sys.exit(main())
