nexo-brain 7.2.0 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ """guardian_metrics_aggregate — Plan Consolidado 0.25.
3
+
4
+ Reads ``~/.nexo/logs/guardian-telemetry.ndjson`` (produced per-enqueue
5
+ by ``guardian_telemetry.log_event``) and the latest drift baseline under
6
+ ``~/.nexo/reports/drift-baseline-*.json``, and writes a rolling aggregate
7
+ of KPIs to ``~/.nexo/logs/guardian-metrics.ndjson``.
8
+
9
+ KPIs (Plan Consolidado 0.25):
10
+ - capture_rate (injected / triggered)
11
+ - core_rule_violations_per_session (R13/R14/R16/R25/R30 injects / #sessions)
12
+ - declared_done_without_evidence_ratio (R16 hard hits / sessions)
13
+ - false_positive_correction_rate (events tagged fp / events total)
14
+ - avg_minutes_between_guard_check_failures
15
+
16
+ Pure reader: never writes inside the telemetry file. ``NEXO_HOME`` is
17
+ honoured so tests isolate via tmp_path.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import statistics
24
+ import sys
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+ from typing import Iterable
28
+
29
+
30
+ CORE_RULE_IDS = {
31
+ "R13_pre_edit_guard",
32
+ "R14_correction_learning",
33
+ "R16_declared_done",
34
+ "R25_nora_maria_read_only",
35
+ "R30_pre_done_evidence_system_prompt",
36
+ }
37
+
38
+
39
+ def _nexo_home() -> Path:
40
+ env = os.environ.get("NEXO_HOME")
41
+ if env:
42
+ return Path(env)
43
+ return Path.home() / ".nexo"
44
+
45
+
46
+ def _read_ndjson(path: Path) -> Iterable[dict]:
47
+ try:
48
+ with path.open("r", encoding="utf-8") as fh:
49
+ for line in fh:
50
+ line = line.strip()
51
+ if not line:
52
+ continue
53
+ try:
54
+ yield json.loads(line)
55
+ except json.JSONDecodeError:
56
+ continue
57
+ except OSError:
58
+ return
59
+
60
+
61
+ def _read_latest_drift_baseline(home: Path) -> dict | None:
62
+ reports_dir = home / "reports"
63
+ if not reports_dir.is_dir():
64
+ return None
65
+ candidates = sorted(reports_dir.glob("drift-baseline-*.json"))
66
+ if not candidates:
67
+ return None
68
+ try:
69
+ return json.loads(candidates[-1].read_text(encoding="utf-8"))
70
+ except (OSError, json.JSONDecodeError):
71
+ return None
72
+
73
+
74
+ def aggregate(home: Path | None = None) -> dict:
75
+ home = home if home is not None else _nexo_home()
76
+ telemetry_path = home / "logs" / "guardian-telemetry.ndjson"
77
+ events = list(_read_ndjson(telemetry_path))
78
+
79
+ by_rule: dict[str, dict[str, int]] = {}
80
+ sessions: set[str] = set()
81
+ core_violations = 0
82
+ r16_hard = 0
83
+ fp_count = 0
84
+ guard_check_failure_ts: list[float] = []
85
+
86
+ for ev in events:
87
+ rid = str(ev.get("rule_id") or ev.get("rule") or "").strip() or "unknown"
88
+ event = str(ev.get("event") or "").strip()
89
+ mode = str(ev.get("mode") or "").strip().lower()
90
+ session_id = str(ev.get("session_id") or ev.get("sid") or "").strip()
91
+ if session_id:
92
+ sessions.add(session_id)
93
+ bucket = by_rule.setdefault(rid, {"triggered": 0, "injected": 0, "fp": 0})
94
+ if event in ("trigger", "fire"):
95
+ bucket["triggered"] += 1
96
+ elif event in ("enqueue", "inject"):
97
+ bucket["injected"] += 1
98
+ bucket["triggered"] += 1
99
+ if rid in CORE_RULE_IDS:
100
+ core_violations += 1
101
+ if rid == "R16_declared_done" and mode == "hard":
102
+ r16_hard += 1
103
+ if ev.get("fp") is True:
104
+ bucket["fp"] += 1
105
+ fp_count += 1
106
+ if event == "guard_check_failed":
107
+ try:
108
+ guard_check_failure_ts.append(float(ev.get("ts") or 0))
109
+ except (TypeError, ValueError):
110
+ pass
111
+
112
+ total_triggered = sum(b["triggered"] for b in by_rule.values()) or 0
113
+ total_injected = sum(b["injected"] for b in by_rule.values()) or 0
114
+ capture_rate = (total_injected / total_triggered) if total_triggered else 0.0
115
+
116
+ guard_check_failure_ts.sort()
117
+ deltas_min = [
118
+ (b - a) / 60.0
119
+ for a, b in zip(guard_check_failure_ts, guard_check_failure_ts[1:])
120
+ if (b - a) > 0
121
+ ]
122
+ avg_min_between = statistics.mean(deltas_min) if deltas_min else None
123
+
124
+ n_sessions = len(sessions) or 1
125
+
126
+ baseline = _read_latest_drift_baseline(home)
127
+
128
+ return {
129
+ "generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
130
+ "nexo_home": str(home),
131
+ "events_read": len(events),
132
+ "sessions_seen": len(sessions),
133
+ "capture_rate": round(capture_rate, 4),
134
+ "core_rule_violations_per_session": round(core_violations / n_sessions, 4),
135
+ "declared_done_without_evidence_ratio": round(r16_hard / n_sessions, 4),
136
+ "false_positive_correction_rate": round(
137
+ (fp_count / total_triggered) if total_triggered else 0.0, 4
138
+ ),
139
+ "avg_minutes_between_guard_check_failures": avg_min_between,
140
+ "per_rule": {
141
+ rid: {
142
+ "triggered": b["triggered"],
143
+ "injected": b["injected"],
144
+ "fp": b["fp"],
145
+ "baseline_hits": (baseline or {}).get("rule_counts", {}).get(rid, 0),
146
+ }
147
+ for rid, b in by_rule.items()
148
+ },
149
+ "drift_baseline_source": (
150
+ str(sorted((home / "reports").glob("drift-baseline-*.json"))[-1])
151
+ if baseline else None
152
+ ),
153
+ }
154
+
155
+
156
+ def write_metrics(result: dict, *, home: Path | None = None) -> Path:
157
+ home = home if home is not None else _nexo_home()
158
+ logs_dir = home / "logs"
159
+ logs_dir.mkdir(parents=True, exist_ok=True)
160
+ path = logs_dir / "guardian-metrics.ndjson"
161
+ with path.open("a", encoding="utf-8") as fh:
162
+ fh.write(json.dumps(result, ensure_ascii=False) + "\n")
163
+ return path
164
+
165
+
166
+ def main(argv: list[str] | None = None) -> int:
167
+ result = aggregate()
168
+ path = write_metrics(result)
169
+ print(
170
+ f"guardian_metrics_aggregate: {result['events_read']} events across "
171
+ f"{result['sessions_seen']} sessions → appended to {path}"
172
+ )
173
+ return 0
174
+
175
+
176
+ if __name__ == "__main__": # pragma: no cover
177
+ sys.exit(main(sys.argv[1:]))