Files
rune/shared/drift_observability.py
T

301 lines
11 KiB
Python

#!/usr/bin/env python3
"""drift_observability.py — Drift monitoring, alerts, and retrospectives.
Makes drift visible. Observable drift is governable drift.
Components:
- Drift alerts: notify when something significant shifts
- Drift retrospectives: periodic self-assessment of whether drift is positive
- Drift dashboard: comprehensive status for the user
Research basis:
- Layered Mutability (arXiv:2604.14717): most dangerous drift is invisible
- VaG (arXiv:2608.05810): pre-commit gating needs observability
Run under ~/.venvs/memory/bin/python3.
"""
import json
import os
import sys
from datetime import datetime, timezone, timedelta
from typing import Dict, List, Optional
SCRIPTS_DIR = os.path.expanduser("~/.config/opencode/scripts")
ALERTS_FILE = os.path.expanduser("~/.config/opencode/memory/drift_alerts.json")
RETROSPECTIVES_DIR = os.path.expanduser("~/.config/opencode/memory/retrospectives")
# Alert thresholds
VELOCITY_THRESHOLD = 10 # changes per day
NEGATIVE_DRIFT_THRESHOLD = 0.1 # stability ratio below this = alert
PROMOTION_STALL_DAYS = 14 # no promotions in this many days = alert
class DriftObservability:
"""Drift monitoring and alerting system."""
def __init__(self):
os.makedirs(os.path.dirname(ALERTS_FILE), exist_ok=True)
os.makedirs(RETROSPECTIVES_DIR, exist_ok=True)
self.alerts = self._load_alerts()
def _load_alerts(self) -> List[dict]:
"""Load alerts from disk."""
if os.path.exists(ALERTS_FILE):
try:
with open(ALERTS_FILE) as f:
return json.load(f)
except json.JSONDecodeError:
return []
return []
def _save_alerts(self):
"""Save alerts to disk."""
with open(ALERTS_FILE, "w") as f:
json.dump(self.alerts, f, indent=2)
def add_alert(self, alert_type: str, message: str, severity: str = "info"):
"""Add a drift alert."""
alert = {
"type": alert_type,
"message": message,
"severity": severity, # info, warning, critical
"timestamp": datetime.now(timezone.utc).isoformat(),
"acknowledged": False,
}
self.alerts.append(alert)
self._save_alerts()
def check_alerts(self, db) -> List[dict]:
"""Check for conditions that warrant alerts."""
import drift_core
new_alerts = []
# Check velocity
vel = drift_core.compute_drift_velocity(db)
if vel["alert"]:
new_alerts.append({
"type": "high_velocity",
"message": f"Drift velocity is {vel['changes_per_day']} changes/day (threshold: {VELOCITY_THRESHOLD})",
"severity": "warning",
})
# Check direction
direction = drift_core.compute_drift_direction(db)
if direction["stability_ratio"] < NEGATIVE_DRIFT_THRESHOLD:
new_alerts.append({
"type": "negative_drift",
"message": f"Stability ratio is {direction['stability_ratio']:.2%} (threshold: {NEGATIVE_DRIFT_THRESHOLD:.0%})",
"severity": "warning",
})
# Check promotion stall
last_promo = db.execute(
"SELECT MAX(last_washed) FROM entries WHERE rock_status != 'washable' AND status = 'active'"
).fetchone()
if last_promo and last_promo[0]:
try:
last = datetime.fromisoformat(last_promo[0])
days_since = (datetime.now(timezone.utc) - last).days
if days_since > PROMOTION_STALL_DAYS:
new_alerts.append({
"type": "promotion_stall",
"message": f"No promotions in {days_since} days (threshold: {PROMOTION_STALL_DAYS})",
"severity": "info",
})
except (ValueError, TypeError):
pass
# Save new alerts
for alert in new_alerts:
self.add_alert(alert["type"], alert["message"], alert["severity"])
return new_alerts
def run_retrospective(self, db, days: int = 7) -> dict:
"""Run a drift retrospective: assess whether recent drift has been positive.
Returns a report with findings and recommendations.
"""
import drift_core
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
# Count changes by type
new_entries = db.execute(
"SELECT COUNT(*) FROM entries WHERE created_at > ? AND status = 'active'",
(cutoff,)
).fetchone()[0]
washed_entries = db.execute(
"SELECT COUNT(*) FROM entries WHERE last_washed > ? AND status = 'active'",
(cutoff,)
).fetchone()[0]
promoted_entries = db.execute(
"SELECT COUNT(*) FROM entries WHERE rock_status != 'washable' AND last_washed > ? AND status = 'active'",
(cutoff,)
).fetchone()[0]
decayed_entries = db.execute(
"SELECT COUNT(*) FROM entries WHERE status = 'deleted' AND last_accessed > ?",
(cutoff,)
).fetchone()[0]
# Current state
velocity = drift_core.compute_drift_velocity(db, days)
direction = drift_core.compute_drift_direction(db)
# Assessment
if direction["stability_ratio"] > 0.3 and velocity["changes_per_day"] < VELOCITY_THRESHOLD:
assessment = "positive"
summary = "Drift is flowing naturally. Stability is increasing. No intervention needed."
elif direction["stability_ratio"] > 0.1:
assessment = "neutral"
summary = "Drift is active but not concerning. Monitor for changes."
else:
assessment = "concerning"
summary = "Drift is moving away from stability. Consider reviewing recent changes."
report = {
"period_days": days,
"new_entries": new_entries,
"washed_entries": washed_entries,
"promoted_entries": promoted_entries,
"decayed_entries": decayed_entries,
"velocity": velocity,
"direction": direction,
"assessment": assessment,
"summary": summary,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
# Save retrospective
filename = f"retrospective_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
filepath = os.path.join(RETROSPECTIVES_DIR, filename)
with open(filepath, "w") as f:
json.dump(report, f, indent=2)
return report
def get_unacknowledged_alerts(self) -> List[dict]:
"""Get alerts that haven't been acknowledged."""
return [a for a in self.alerts if not a.get("acknowledged")]
def acknowledge_alert(self, index: int):
"""Acknowledge an alert."""
if 0 <= index < len(self.alerts):
self.alerts[index]["acknowledged"] = True
self._save_alerts()
# ═══════════════════════════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
import argparse
parser = argparse.ArgumentParser(description="Drift observability")
sub = parser.add_subparsers(dest="cmd")
# check
sub.add_parser("check", help="Check for drift alerts")
# retrospective [--days N]
p_retro = sub.add_parser("retrospective", help="Run drift retrospective")
p_retro.add_argument("--days", type=int, default=7)
# alerts
sub.add_parser("alerts", help="Show unacknowledged alerts")
# acknowledge <index>
p_ack = sub.add_parser("acknowledge", help="Acknowledge an alert")
p_ack.add_argument("index", type=int)
# dashboard
sub.add_parser("dashboard", help="Show comprehensive drift dashboard")
args = parser.parse_args()
if not args.cmd:
parser.print_help()
sys.exit(1)
obs = DriftObservability()
if args.cmd == "check":
sys.path.insert(0, SCRIPTS_DIR)
import memory_store
db = memory_store.connect()
new_alerts = obs.check_alerts(db)
db.close()
if new_alerts:
print(f"Found {len(new_alerts)} new alerts:")
for a in new_alerts:
print(f" [{a['severity']}] {a['type']}: {a['message']}")
else:
print("No new alerts.")
elif args.cmd == "retrospective":
sys.path.insert(0, SCRIPTS_DIR)
import memory_store
db = memory_store.connect()
report = obs.run_retrospective(db, args.days)
db.close()
print("=== Drift Retrospective ===")
print(f"Period: {report['period_days']} days")
print(f"New entries: {report['new_entries']}")
print(f"Washed entries: {report['washed_entries']}")
print(f"Promoted entries: {report['promoted_entries']}")
print(f"Decayed entries: {report['decayed_entries']}")
print(f"Assessment: {report['assessment']}")
print(f"Summary: {report['summary']}")
elif args.cmd == "alerts":
unacked = obs.get_unacknowledged_alerts()
if unacked:
print(f"Unacknowledged alerts ({len(unacked)}):")
for i, a in enumerate(unacked):
print(f" [{i}] [{a['severity']}] {a['type']}: {a['message']}")
else:
print("No unacknowledged alerts.")
elif args.cmd == "acknowledge":
obs.acknowledge_alert(args.index)
print(f"Alert {args.index} acknowledged.")
elif args.cmd == "dashboard":
sys.path.insert(0, SCRIPTS_DIR)
import memory_store
import drift_core
db = memory_store.connect()
vel = drift_core.compute_drift_velocity(db)
direction = drift_core.compute_drift_direction(db)
unacked = obs.get_unacknowledged_alerts()
print("=== Governed Adaptation Dashboard ===")
print(f"\n--- Trust Level Distribution ---")
print(f"Total entries: {direction['total_entries']}")
print(f"Validated (proven, trusted): {direction['established_count']}")
print(f"Locked (immutable, identity anchors): {direction['rock_count']}")
print(f"Provisional (still being tested): {direction['total_entries'] - direction['established_count'] - direction['rock_count']}")
print(f"Trust ratio: {direction['stability_ratio']:.2%}")
print(f"Evolution direction: {direction['direction']}")
print(f"\n--- State Evolution ---")
print(f"Changes/day: {vel['changes_per_day']}")
print(f"Promotions/day: {vel['promotions_per_day']}")
print(f"Alert: {vel['alert']}")
print(f"\n--- Alerts ---")
print(f"Unacknowledged: {len(unacked)}")
for a in unacked[:5]:
print(f" [{a['severity']}] {a['type']}: {a['message']}")
db.close()
if __name__ == "__main__":
main()