#!/usr/bin/env python3 """ Guide coverage reconciler — diffs the CODE surface against what the guide documents, so nothing new (a service, an SPA module, an agent module) stays undocumented. Run: python3 hiveops-guide/scripts/coverage-reconcile.py Exits 1 if any gaps are found (handy for a session-start check / hook). """ import os, re, sys, glob ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) # /source/hiveops-src CONTENT = os.path.join(ROOT, "hiveops-guide", "backend", "src", "main", "resources", "content") # product SPAs that should have per-module role-tab guides (app prefix in the guide) PRODUCT_APPS = { "analytics","aria","claims","dashboard","devices","fleet","incident","msp", "profile","recon","reports","transactions","vault", } # guide app-prefixes that are NOT code-derived (concept / process docs) — skip in the code diff NON_CODE_APPS = {"platform","devops","technical","processes"} # the 5 role-tabs expected on a product module PRODUCT_TABS = {"Customer","Internal","Architect","Testing","Claude"} # dir-name (minus hiveops-) → guide app/service key, where they differ SERVICE_ALIAS = {"ai":"ai", "recon":"recon"} # extend as needed def load_guide(): """module_key -> set(tab names). Reads frontmatter of every content/**/*.md.""" mods = {} for f in glob.glob(os.path.join(CONTENT, "**", "*.md"), recursive=True): txt = open(f, encoding="utf-8").read() m = re.match(r"^---\n(.*?)\n---", txt, re.S) if not m: continue fm = dict(re.findall(r"^(\w+):\s*(.+)$", m.group(1), re.M)) key, tab = fm.get("module"), fm.get("tab") if key: mods.setdefault(key, set()).add(tab) return mods def has_backend(repo): return bool(glob.glob(os.path.join(repo, "**", "src", "main", "java", "com", "hiveops"), recursive=True)) def main(): guide = load_guide() apps = {k.split(".")[0] for k in guide} gaps = [] # 1) backend services → expect a technical. guide for repo in sorted(glob.glob(os.path.join(ROOT, "hiveops-*"))): name = os.path.basename(repo) svc = name[len("hiveops-"):] # non-service / special repos, and the DEPRECATED qds-monitor (the early incident # bridge before the agent shipped — superseded, don't document) if svc in ("agent","guide","bom","security-common","template","docs","release", "devops","integration","tools","adoons", "qds-monitor"): # last entry = deprecated continue if not os.path.isdir(repo) or not has_backend(repo): continue if f"technical.{svc}" not in guide: gaps.append(("SERVICE", f"hiveops-{svc}", f"technical.{svc}")) # 2) agent modules → expect an agent. guide for mod in sorted(glob.glob(os.path.join(ROOT, "hiveops-agent", "hiveops-module-*"))): if not os.path.isdir(mod): continue slug = os.path.basename(mod)[len("hiveops-module-"):] if f"agent.{slug}" not in guide: gaps.append(("AGENT MODULE", os.path.basename(mod), f"agent.{slug}")) # 3) product modules present but missing role-tabs incomplete = [] for key, tabs in sorted(guide.items()): app = key.split(".")[0] if app in PRODUCT_APPS: missing = PRODUCT_TABS - tabs if missing: incomplete.append((key, ",".join(sorted(missing)))) # 4) product SPAs with no guide content at all for repo in sorted(glob.glob(os.path.join(ROOT, "hiveops-*"))): app = os.path.basename(repo)[len("hiveops-"):] if app in PRODUCT_APPS and app not in apps and os.path.isdir(os.path.join(repo, "frontend")): gaps.append(("SPA", f"hiveops-{app}", f"{app}.* (no modules documented)")) # ---- report ---- print(f"Guide coverage — {len(guide)} documented modules across {len(apps)} apps\n") if gaps: print(f"❌ {len(gaps)} MISSING (in code, not in guide):") for kind, code, expected in gaps: print(f" [{kind}] {code} → needs {expected}") else: print("✅ no missing services / agent modules / SPAs") if incomplete: print(f"\n⚠️ {len(incomplete)} product modules missing role-tabs:") for key, miss in incomplete: print(f" {key} → missing: {miss}") print() sys.exit(1 if (gaps or incomplete) else 0) if __name__ == "__main__": main()