# -*- coding: utf-8 -*- """Which instruction files actually load, how often — and which NEVER have. Usage: python3 scripts/rules_report.py [--log FILE] [--workspace DIR] [--days N] READS the InstructionsLoaded hook log (~/.claude/instructions-loaded.jsonl, plus its rotated .1) and CROSS-REFERENCES it against every .claude/rules/*.md across the workspace. THE COLUMN THAT MATTERS IS THE EMPTY ONE. A rule that has never fired is not a rule — it is either mis-globbed (its `paths:` never match the files people actually edit) or dead (the surface it guards is gone). That is the built-but-never-wired failure class, which this project has shipped four times, applied to instructions: the rule file exists, reads correctly, passes the gate that checks it declares `paths:` — and reaches the model never. A never-fired rule is NOT automatically a defect. A rule scoped to a surface nobody has touched since it was written is simply untested. The report says which, it does not judge: `paths:` globs are printed beside each silent rule so the reader can tell "wrong glob" from "quiet month". Exit code is always 0 — this is a report, not a gate. """ import argparse import collections import json import os import re import sys def load_events(paths, days): import datetime cutoff = None if days: cutoff = (datetime.datetime.utcnow() - datetime.timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ") events, bad = [], 0 for p in paths: if not os.path.exists(p): continue with open(p, "r", encoding="utf-8", errors="replace") as fh: for line in fh: line = line.strip() if not line: continue try: e = json.loads(line) except ValueError: bad += 1 continue if cutoff and (e.get("ts") or "") < cutoff: continue events.append(e) return events, bad def find_rule_files(workspace): """Every .claude/rules/*.md in the workspace, with its declared paths: globs.""" out = {} for repo in sorted(os.listdir(workspace)): rd = os.path.join(workspace, repo, ".claude", "rules") if not os.path.isdir(rd): continue for name in sorted(os.listdir(rd)): if not name.endswith(".md"): continue full = os.path.join(rd, name) try: with open(full, "r", encoding="utf-8") as fh: head = fh.read(2048) except OSError: head = "" m = re.search(r"^paths:\s*(.+)$", head, re.M) globs = m.group(1).strip() if m else ("(unconditional)" if re.search( r"^unconditional:\s*true", head, re.M) else "(NO paths: — loads every session)") out[full] = (repo, name, globs) return out def main(argv): home = os.path.expanduser("~") ap = argparse.ArgumentParser() ap.add_argument("--log", default=os.path.join(home, ".claude", "instructions-loaded.jsonl")) ap.add_argument("--workspace", default=os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) ap.add_argument("--days", type=int, default=0, help="only count events newer than N days") args = ap.parse_args(argv[1:]) logs = [args.log, args.log + ".1"] events, bad = load_events(logs, args.days) print("rules_report") print(" log : %s%s" % (args.log, " (+ rotated .1)" if os.path.exists(args.log + ".1") else "")) print(" workspace : %s" % args.workspace) print(" events : %d%s%s" % (len(events), (" (last %d days)" % args.days) if args.days else "", (" [%d unparseable lines skipped]" % bad) if bad else "")) if not events: print("\n No events. The hook may not be installed — see scripts/install_workspace.py.") by_reason = collections.Counter(e.get("load_reason") or "?" for e in events) print("\n== why instructions loaded ==") for reason, n in by_reason.most_common(): print(" %-18s %d" % (reason, n)) fired = collections.Counter() triggers = collections.defaultdict(collections.Counter) for e in events: fp = e.get("file_path") or "?" fired[fp] += 1 t = e.get("trigger_file_path") if t: triggers[fp][os.path.basename(t)] += 1 print("\n== instruction files that LOADED ==") if not fired: print(" (none)") for fp, n in fired.most_common(): short = fp.replace(args.workspace + os.sep, "") ex = triggers[fp].most_common(1) print(" %5d %-58s %s" % (n, short, ("e.g. via %s" % ex[0][0]) if ex else "")) rules = find_rule_files(args.workspace) silent = [(p, v) for p, v in sorted(rules.items()) if p not in fired] print("\n== rule files that have NEVER fired (%d of %d) ==" % (len(silent), len(rules))) if not silent: print(" none — every rule file in the workspace has loaded at least once") for p, (repo, name, globs) in silent: print(" %-24s %-18s paths: %s" % (repo, name, globs)) if silent: print("\n A never-fired rule is mis-globbed, dead, or simply guarding a surface nobody has") print(" edited since it was written. The globs above are printed so you can tell which.") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))