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
- 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.
112 lines
4.0 KiB
Python
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())
|