301 lines
11 KiB
Python
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()
|