Files
rune/scripts/graph_health_report.py
T
Nick 91a91e8e36
local-review.yml (push) Invalid workflow file
Auto-version and Release / release (push) Successful in 9s
CI Pipeline / lint (push) Failing after 28s
CI Pipeline / go-surface (push) Failing after 18s
CI Pipeline / rust-surface (push) Successful in 4m15s
CI Pipeline / unit-tests (push) Failing after 5m21s
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 8s
CI Pipeline / canary (push) Failing after 59s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / type-check (push) Failing after 34s
deploy / deploy (push) Failing after 15m11s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 9s
feat(graph): #42 CLOSED — live graph-health instrument + supersession pins (contract BUILDER-2026-10-08T10:58:57Z-019b73)
- graph_health_report.py: superseded/active/expired ratios, orphan +
  dangling counts, normalization behavior — against ANY store path.
  Empty graphs report no-data (ratios undefined, not 0%), missing
  stores report no-store. The dossier statistic is replaced by a live
  instrument anyone can run.
- supersession state machine pinned: old edge marked (never deleted),
  new edge wins, superseded_by chain intact.
2026-10-08 12:02:37 +01:00

112 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""graph_health_report.py — #42: the live graph-memory health instrument.
Replaces the stale dossier statistic with a live report: superseded /
active / expired edge ratios, orphan rates, normalization behavior —
against ANY store path (default: the repo-local memory store). A store
with no edges reports no-data, never 0%.
Usage: python3 scripts/graph_health_report.py [--store PATH] [--json]
"""
import argparse
import json
import sqlite3
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
DEFAULT_STORE = REPO / "memory" / "store" / "memory.db"
def report(store_path):
store_path = Path(store_path)
if not store_path.exists():
return {"store": str(store_path), "status": "no-store",
"note": "no database at this path"}
db = sqlite3.connect(f"file:{store_path}?mode=ro", uri=True)
db.row_factory = sqlite3.Row
try:
tables = {r[0] for r in db.execute(
"SELECT name FROM sqlite_master WHERE type='table'").fetchall()}
except Exception as e: # noqa: BLE001 — deliberate: unreadable store reports as unreadable, never crashes
return {"store": str(store_path), "status": "unreadable",
"note": str(e)}
out = {"store": str(store_path), "status": "ok", "tables": sorted(tables)}
if "edges" not in tables:
out["status"] = "no-graph"
out["note"] = "no edges table — not a graph store"
return out
try:
total = db.execute("SELECT COUNT(*) FROM edges").fetchone()[0]
except Exception as e: # noqa: BLE001 — deliberate: corrupt edges table reports as unreadable
out["status"] = "unreadable"
out["note"] = str(e)
return out
if total == 0:
out["status"] = "no-data"
out["note"] = ("empty graph: ratios are undefined, not 0% — "
"the dossier statistic cannot be confirmed or denied")
out["edges"] = {"total": 0}
return out
def count(where):
return db.execute(f"SELECT COUNT(*) FROM edges WHERE {where}").fetchone()[0]
active = count("status='active'")
superseded = count("status='superseded'")
expired = count("status='expired'")
orphan = db.execute(
"SELECT COUNT(*) FROM edges WHERE superseded_by IS NOT NULL "
"AND superseded_by NOT IN (SELECT id FROM edges)").fetchone()[0]
dangling = db.execute(
"SELECT COUNT(*) FROM edges WHERE subj_id NOT IN "
"(SELECT id FROM entities) OR obj_id NOT IN "
"(SELECT id FROM entities)").fetchone()[0]
out["edges"] = {
"total": total,
"active": active,
"superseded": superseded,
"expired": expired,
"active_ratio": round(active / total, 4),
"superseded_ratio": round(superseded / total, 4),
"expired_ratio": round(expired / total, 4),
"orphan_superseded_by": orphan,
"dangling_endpoints": dangling,
}
# normalization behavior: distinct predicates vs edges
out["normalization"] = {
"distinct_predicates": db.execute(
"SELECT COUNT(DISTINCT pred) FROM edges").fetchone()[0],
}
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--store", default=str(DEFAULT_STORE))
ap.add_argument("--json", action="store_true")
args = ap.parse_args()
rep = report(args.store)
if args.json:
print(json.dumps(rep, indent=1))
else:
print(f"store: {rep['store']}")
print(f"status: {rep['status']}")
if rep["status"] == "ok":
e = rep["edges"]
print(f"edges: {e['total']} (active {e['active_ratio']}, "
f"superseded {e['superseded_ratio']}, expired {e['expired_ratio']})")
print(f"orphans: {e['orphan_superseded_by']}, "
f"dangling: {e['dangling_endpoints']}")
else:
print(f"note: {rep.get('note', '')}")
return 0
if __name__ == "__main__":
sys.exit(main())