nexo-brain 7.27.3 → 7.28.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.
Files changed (46) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +5 -1
  3. package/bin/windows-wsl-bridge.js +9 -0
  4. package/package.json +1 -1
  5. package/src/causal_graph.py +763 -0
  6. package/src/classifier_local.py +44 -0
  7. package/src/cognitive/_core.py +3 -0
  8. package/src/cognitive_control_observatory.py +2 -0
  9. package/src/db/__init__.py +8 -0
  10. package/src/db/_commitments.py +344 -0
  11. package/src/db/_entities.py +98 -11
  12. package/src/db/_memory_v2.py +130 -2
  13. package/src/db/_schema.py +565 -0
  14. package/src/desktop_bridge.py +1 -1
  15. package/src/doctor/providers/runtime.py +9 -3
  16. package/src/enforcement_engine.py +128 -2
  17. package/src/entity_live_profile.py +1073 -0
  18. package/src/failure_prevention.py +1052 -0
  19. package/src/hook_guardrails.py +104 -0
  20. package/src/knowledge_graph.py +46 -9
  21. package/src/local_context/api.py +54 -22
  22. package/src/local_context/usage_events.py +273 -8
  23. package/src/memory_executive.py +620 -0
  24. package/src/memory_utility.py +952 -0
  25. package/src/plugin_loader.py +9 -5
  26. package/src/plugins/entities.py +84 -7
  27. package/src/plugins/entity_live_profile.py +101 -0
  28. package/src/plugins/failure_prevention.py +162 -0
  29. package/src/plugins/memory_export.py +55 -18
  30. package/src/plugins/protocol.py +133 -0
  31. package/src/plugins/semantic_layers.py +138 -0
  32. package/src/pre_answer_router.py +622 -28
  33. package/src/pre_answer_runtime.py +463 -18
  34. package/src/r14_correction_learning.py +3 -3
  35. package/src/requirements.txt +5 -1
  36. package/src/runtime_versioning.py +11 -1
  37. package/src/saved_not_used_audit.py +44 -3
  38. package/src/scripts/nexo-followup-runner.py +194 -0
  39. package/src/semantic_layers.py +1153 -0
  40. package/src/semantic_reasoner.py +2 -2
  41. package/src/semantic_router.py +58 -11
  42. package/src/server.py +41 -3
  43. package/src/tools_sessions.py +88 -31
  44. package/src/tools_transcripts.py +38 -22
  45. package/src/user_state_model.py +971 -0
  46. package/tool-enforcement-map.json +230 -0
@@ -0,0 +1,1052 @@
1
+ """Failure prevention ledger for autopsy candidates and antibody proposals.
2
+
3
+ This module coordinates existing NEXO owners. It does not create canonical
4
+ learnings, outcomes, guard rules, protocol debt, benchmarks, or followups by
5
+ itself; it records a validated, redacted case and proposed owner actions.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import re
13
+ import sqlite3
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from db import get_db
19
+ from learning_resolver import resolve_learning_candidate
20
+
21
+
22
+ POLICY_VERSION = "failure_prevention.v1"
23
+
24
+ FAILURE_TYPES = {
25
+ "memory", "identity", "promise", "release", "server", "privacy",
26
+ "security", "test", "tool", "workflow", "communication", "performance",
27
+ "other",
28
+ }
29
+ SOURCE_TYPES = {
30
+ "francisco_correction", "explicit_instruction", "test_failure",
31
+ "release_gate_failure", "outcome_miss", "protocol_debt",
32
+ "guard_violation", "guard_check", "guardian_telemetry",
33
+ "error_repetition", "somatic_event", "hook_run", "immune_finding",
34
+ "watchdog_finding", "daily_self_audit", "deep_sleep_finding",
35
+ "manual_review",
36
+ }
37
+ INFERENCE_ONLY_SOURCES = {
38
+ "guardian_telemetry", "immune_finding", "watchdog_finding",
39
+ "daily_self_audit", "deep_sleep_finding",
40
+ }
41
+ SEVERITIES = {"p0", "p1", "p2", "p3", "p4"}
42
+ CASE_STATUSES = {
43
+ "candidate", "analyzing", "action_required", "antibody_pending",
44
+ "verifying", "verified", "resolved", "rejected", "false_positive",
45
+ "expired", "rolled_back", "conflict_review",
46
+ }
47
+ PRIVACY_LEVELS = {"public", "normal", "private", "sensitive", "secret"}
48
+ SURFACES = {"pre_action", "debug_local", "audit", "runtime_internal", "export"}
49
+ ACTION_TYPES = {
50
+ "learning_resolve", "test_add", "benchmark_case_add",
51
+ "guard_rule_proposal", "predictive_context_rule", "docs_update",
52
+ "skill_update", "followup_create", "outcome_register",
53
+ "release_gate_update", "immune_check_update", "watchdog_check_update",
54
+ }
55
+ TARGET_SYSTEMS = {
56
+ "learning_resolver", "learnings", "pytest", "runtime_pack", "guardian",
57
+ "pre_answer_router", "docs", "skills", "followups", "outcomes",
58
+ "release_readiness", "immune", "watchdog",
59
+ }
60
+ ACTION_STATUSES = {
61
+ "proposed", "approved", "applied", "verifying", "verified", "rejected",
62
+ "expired", "rolled_back", "false_positive",
63
+ }
64
+ ACTIVATION_POLICIES = {
65
+ "candidate_only", "shadow", "warn", "block_after_verification",
66
+ "manual_approval_required",
67
+ }
68
+ VERIFICATION_STATUSES = {"missing", "pending", "passed", "failed", "not_applicable"}
69
+ NOT_APPLICABLE_ACTIONS = {"docs_update", "followup_create"}
70
+ APPROVAL_REF_PREFIXES = {"evidence", "protocol_task", "guard_check", "change_log"}
71
+
72
+ SOURCE_REF_PREFIXES: dict[str, set[str]] = {
73
+ "francisco_correction": {"session_correction_requirement"},
74
+ "explicit_instruction": {"protocol_task", "evidence"},
75
+ "test_failure": {"test"},
76
+ "release_gate_failure": {"test", "evidence"},
77
+ "outcome_miss": {"outcome"},
78
+ "protocol_debt": {"protocol_debt"},
79
+ "guard_violation": {"protocol_debt", "guardian_rule"},
80
+ "guard_check": {"guard_check"},
81
+ "guardian_telemetry": {"guardian_telemetry"},
82
+ "error_repetition": {"error_repetition"},
83
+ "somatic_event": {"somatic_event"},
84
+ "hook_run": {"hook_run"},
85
+ "immune_finding": {"immune_finding"},
86
+ "watchdog_finding": {"watchdog_finding"},
87
+ "daily_self_audit": {"evidence", "protocol_debt"},
88
+ "deep_sleep_finding": {"evidence"},
89
+ "manual_review": {"evidence"},
90
+ }
91
+
92
+ DB_REF_TABLES: dict[str, tuple[str, str]] = {
93
+ "learning": ("learnings", "id"),
94
+ "error_repetition": ("error_repetitions", "id"),
95
+ "somatic_event": ("somatic_events", "id"),
96
+ "guard_check": ("guard_checks", "id"),
97
+ "protocol_debt": ("protocol_debt", "id"),
98
+ "session_correction_requirement": ("session_correction_requirements", "id"),
99
+ "outcome": ("outcomes", "id"),
100
+ "hook_run": ("hook_runs", "id"),
101
+ }
102
+
103
+ SECRET_RE = re.compile(
104
+ r"(?i)\b(api[_-]?key|token|secret|password|authorization|bearer|credential|cred_ref)\b"
105
+ r"\s*[:=]\s*['\"]?[^'\"\s,;]+"
106
+ )
107
+ BEARER_RE = re.compile(r"(?i)\bbearer\s+[a-z0-9._~+/=-]{12,}")
108
+ IPV4_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
109
+ ABS_PATH_RE = re.compile(r"(?<![\w.-])/(?:Users|home|var|etc|Volumes)/[^\s,;]+")
110
+ RAW_PAYLOAD_MARKER_RE = re.compile(r"(?i)\b(provider_payload|raw_prompt|raw_response|transcript)\b")
111
+ SENSITIVE_METADATA_KEY_RE = re.compile(
112
+ r"(?i)\b(api[_-]?key|token|secret|password|authorization|bearer|credential|cred_ref|"
113
+ r"idempotency[_-]?key|provider_payload|raw_prompt|raw_response|transcript)\b"
114
+ )
115
+
116
+
117
+ def _now() -> float:
118
+ return time.time()
119
+
120
+
121
+ def _stable_uid(*parts: object) -> str:
122
+ payload = "\0".join(str(part or "") for part in parts)
123
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
124
+
125
+
126
+ def _json(value: Any) -> str:
127
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
128
+
129
+
130
+ def _load_json(value: str, default: Any) -> Any:
131
+ try:
132
+ return json.loads(value or "")
133
+ except Exception:
134
+ return default
135
+
136
+
137
+ def _normalize_text(value: object) -> str:
138
+ clean = re.sub(r"\s+", " ", str(value or "").strip().lower())
139
+ return clean[:1000]
140
+
141
+
142
+ def _clip(value: str, limit: int = 500) -> str:
143
+ clean = str(value or "").strip()
144
+ if len(clean) <= limit:
145
+ return clean
146
+ return clean[: limit - 3] + "..."
147
+
148
+
149
+ def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
150
+ row = conn.execute(
151
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?",
152
+ (table,),
153
+ ).fetchone()
154
+ return bool(row)
155
+
156
+
157
+ def _ensure_tables(conn: sqlite3.Connection) -> None:
158
+ if _table_exists(conn, "failure_prevention_cases"):
159
+ return
160
+ from db._schema import run_migrations
161
+
162
+ run_migrations(conn)
163
+
164
+
165
+ def _as_list(value: Any) -> list[str]:
166
+ if value is None:
167
+ return []
168
+ if isinstance(value, str):
169
+ if not value.strip():
170
+ return []
171
+ try:
172
+ parsed = json.loads(value)
173
+ if isinstance(parsed, list):
174
+ return [str(item).strip() for item in parsed if str(item).strip()]
175
+ except Exception:
176
+ pass
177
+ return [item.strip() for item in value.split(",") if item.strip()]
178
+ if isinstance(value, (list, tuple, set)):
179
+ return [str(item).strip() for item in value if str(item).strip()]
180
+ return [str(value).strip()]
181
+
182
+
183
+ def _append_unique(existing_json: str, items: list[str]) -> str:
184
+ existing = _as_list(_load_json(existing_json, []))
185
+ seen = set(existing)
186
+ for item in items:
187
+ clean = str(item or "").strip()
188
+ if clean and clean not in seen:
189
+ existing.append(clean)
190
+ seen.add(clean)
191
+ return _json(existing)
192
+
193
+
194
+ def _normalize_failure_type(value: str) -> str:
195
+ clean = str(value or "other").strip().lower()
196
+ return clean if clean in FAILURE_TYPES else "other"
197
+
198
+
199
+ def _normalize_source_type(value: str) -> str:
200
+ clean = str(value or "").strip().lower()
201
+ return clean if clean in SOURCE_TYPES else ""
202
+
203
+
204
+ def _normalize_severity(value: str) -> str:
205
+ clean = str(value or "p3").strip().lower()
206
+ return clean if clean in SEVERITIES else "p3"
207
+
208
+
209
+ def _normalize_privacy(value: str) -> str:
210
+ clean = str(value or "normal").strip().lower()
211
+ return clean if clean in PRIVACY_LEVELS else "normal"
212
+
213
+
214
+ def _normalize_surface(value: str) -> str:
215
+ clean = str(value or "audit").strip().lower()
216
+ return clean if clean in SURFACES else "audit"
217
+
218
+
219
+ def _normalize_confidence(value: float | int | str) -> float:
220
+ try:
221
+ raw = float(value)
222
+ except Exception:
223
+ raw = 0.5
224
+ return max(0.0, min(1.0, raw))
225
+
226
+
227
+ def redact_value(value: object) -> str:
228
+ """Return a safe, local-only field preview."""
229
+ text = str(value or "")
230
+ if RAW_PAYLOAD_MARKER_RE.search(text):
231
+ return "[redacted_payload]"
232
+ text = SECRET_RE.sub(r"\1=[redacted]", text)
233
+ text = BEARER_RE.sub("Bearer [redacted]", text)
234
+ text = IPV4_RE.sub("[redacted_ip]", text)
235
+ text = ABS_PATH_RE.sub("[redacted_path]", text)
236
+ return _clip(re.sub(r"\s+", " ", text).strip())
237
+
238
+
239
+ def sanitize_metadata(value: Any, *, _depth: int = 0) -> Any:
240
+ """Recursively redact metadata before it can reach persistent storage."""
241
+ if _depth > 5:
242
+ return "[redacted_depth]"
243
+ if value is None or isinstance(value, (bool, int, float)):
244
+ return value
245
+ if isinstance(value, bytes):
246
+ return "[redacted_bytes]"
247
+ if isinstance(value, dict):
248
+ clean: dict[str, Any] = {}
249
+ for key, item in value.items():
250
+ key_text = str(key or "").strip()
251
+ if SENSITIVE_METADATA_KEY_RE.search(key_text):
252
+ clean[f"redacted:{_stable_uid(key_text)[:12]}"] = "[redacted]"
253
+ continue
254
+ clean_key = redact_value(key_text)[:120] or "field"
255
+ clean[clean_key] = sanitize_metadata(item, _depth=_depth + 1)
256
+ return clean
257
+ if isinstance(value, (list, tuple, set)):
258
+ return [sanitize_metadata(item, _depth=_depth + 1) for item in list(value)[:50]]
259
+ return redact_value(value)
260
+
261
+
262
+ def _idempotency_marker(value: str) -> str:
263
+ clean = str(value or "").strip()
264
+ if not clean:
265
+ return ""
266
+ return f"sha256:{_stable_uid(POLICY_VERSION, 'idempotency_key', clean)[:24]}"
267
+
268
+
269
+ def field_evidence(
270
+ value: object = "",
271
+ *,
272
+ source_refs: list[str] | None = None,
273
+ confidence: float = 0.5,
274
+ privacy_level: str = "normal",
275
+ value_ref: str = "",
276
+ ) -> dict[str, Any]:
277
+ return {
278
+ "value_redacted": redact_value(value),
279
+ "value_ref": _sanitize_ref(value_ref, allow_empty=True),
280
+ "source_refs": [_sanitize_ref(ref) for ref in _as_list(source_refs)],
281
+ "confidence": _normalize_confidence(confidence),
282
+ "privacy_level": _normalize_privacy(privacy_level),
283
+ }
284
+
285
+
286
+ def _surface_allowed(surface: str, allowed_surfaces: Any, *, privacy_level: str) -> bool:
287
+ clean_surface = _normalize_surface(surface)
288
+ privacy = _normalize_privacy(privacy_level)
289
+ if privacy == "secret" and clean_surface not in {"audit", "runtime_internal"}:
290
+ return False
291
+ if privacy in {"private", "sensitive"} and clean_surface in {"pre_action", "export"}:
292
+ return False
293
+ allowed = set(_as_list(allowed_surfaces))
294
+ if not allowed:
295
+ allowed = set(_allowed_surfaces(privacy_level=privacy, status="candidate"))
296
+ return clean_surface in allowed
297
+
298
+
299
+ def _safe_refs(value: Any) -> list[str]:
300
+ return [redact_value(ref) for ref in _as_list(value) if redact_value(ref)]
301
+
302
+
303
+ def _safe_field(value: Any) -> Any:
304
+ if not isinstance(value, dict):
305
+ return value
306
+ clean = dict(value)
307
+ if "value_redacted" in clean:
308
+ clean["value_redacted"] = redact_value(clean.get("value_redacted"))
309
+ if "value_ref" in clean:
310
+ clean["value_ref"] = redact_value(clean.get("value_ref"))
311
+ if "source_refs" in clean:
312
+ clean["source_refs"] = _safe_refs(clean.get("source_refs"))
313
+ return clean
314
+
315
+
316
+ def _safe_case(case: dict[str, Any], *, surface: str = "audit") -> dict[str, Any]:
317
+ if not case:
318
+ return {}
319
+ if not _surface_allowed(surface, case.get("allowed_surfaces"), privacy_level=str(case.get("privacy_level") or "normal")):
320
+ return {}
321
+ clean = dict(case)
322
+ for key in ("entity_refs", "source_event_refs", "evidence_refs", "antibody_refs"):
323
+ clean[key] = _safe_refs(clean.get(key))
324
+ for key in ("primary_source_ref", "area"):
325
+ clean[key] = redact_value(clean.get(key))
326
+ for key in (
327
+ "symptom", "trigger", "missed_signal", "wrong_assumption",
328
+ "root_cause", "corrective_action",
329
+ ):
330
+ clean[key] = _safe_field(clean.get(key))
331
+ clean["learning_resolution"] = sanitize_metadata(clean.get("learning_resolution") or {})
332
+ clean["metadata"] = sanitize_metadata(clean.get("metadata") or {})
333
+ return clean
334
+
335
+
336
+ def _safe_source_event(event: dict[str, Any]) -> dict[str, Any]:
337
+ if not event:
338
+ return {}
339
+ clean = dict(event)
340
+ clean["source_ref"] = redact_value(clean.get("source_ref"))
341
+ clean["evidence_refs"] = _safe_refs(clean.get("evidence_refs"))
342
+ clean["metadata"] = sanitize_metadata(clean.get("metadata") or {})
343
+ return clean
344
+
345
+
346
+ def _safe_antibody(antibody: dict[str, Any]) -> dict[str, Any]:
347
+ if not antibody:
348
+ return {}
349
+ clean = dict(antibody)
350
+ for key in ("target_ref", "action_payload_ref", "verification_ref", "approved_ref", "rollback_ref"):
351
+ clean[key] = redact_value(clean.get(key))
352
+ clean["metadata"] = sanitize_metadata(clean.get("metadata") or {})
353
+ return clean
354
+
355
+
356
+ def _ref_prefix(ref: str) -> str:
357
+ clean = str(ref or "").strip()
358
+ if clean.startswith("test:"):
359
+ return "test"
360
+ return clean.split(":", 1)[0].strip().lower() if ":" in clean else ""
361
+
362
+
363
+ def _ref_value(ref: str) -> str:
364
+ clean = str(ref or "").strip()
365
+ return clean.split(":", 1)[1].strip() if ":" in clean else ""
366
+
367
+
368
+ def _sanitize_ref(ref: str, *, allow_empty: bool = False) -> str:
369
+ clean = str(ref or "").strip()
370
+ if not clean:
371
+ if allow_empty:
372
+ return ""
373
+ raise ValueError("ref_required")
374
+ lower = clean.lower()
375
+ if SECRET_RE.search(clean) or BEARER_RE.search(clean) or "credential:" in lower or "cred_ref" in lower:
376
+ raise ValueError("ref_contains_secret")
377
+ if IPV4_RE.search(clean):
378
+ raise ValueError("ref_contains_ip")
379
+ if ABS_PATH_RE.search(clean):
380
+ raise ValueError("ref_contains_sensitive_path")
381
+ if RAW_PAYLOAD_MARKER_RE.search(clean):
382
+ raise ValueError("ref_contains_raw_payload_marker")
383
+ return clean[:300]
384
+
385
+
386
+ def _validate_test_ref(source_ref: str) -> tuple[bool, str]:
387
+ value = _ref_value(source_ref)
388
+ path_text = value.split("::", 1)[0].strip()
389
+ if not path_text or Path(path_text).is_absolute() or ".." in Path(path_text).parts:
390
+ return False, "test_ref_must_be_relative"
391
+ return True, "test_ref_format"
392
+
393
+
394
+ def _validate_db_ref(conn: sqlite3.Connection, prefix: str, source_ref: str, *, source_type: str) -> tuple[bool, str]:
395
+ table, column = DB_REF_TABLES[prefix]
396
+ if not _table_exists(conn, table):
397
+ return False, f"{table}_table_missing"
398
+ ref_value = _ref_value(source_ref)
399
+ try:
400
+ if prefix in {"protocol_debt", "session_correction_requirement", "outcome", "hook_run", "guard_check", "error_repetition", "somatic_event", "learning"}:
401
+ lookup_value: object = int(ref_value)
402
+ else:
403
+ lookup_value = ref_value
404
+ except Exception:
405
+ return False, f"{prefix}_id_invalid"
406
+ row = conn.execute(f"SELECT * FROM {table} WHERE {column} = ?", (lookup_value,)).fetchone()
407
+ if not row:
408
+ return False, f"{prefix}_not_found"
409
+ data = dict(row)
410
+ if source_type == "outcome_miss" and str(data.get("status") or "").lower() != "missed":
411
+ return False, "outcome_not_missed"
412
+ if prefix == "hook_run" and str(data.get("status") or "").lower() not in {"error", "timeout", "blocked", "failed", "fail"}:
413
+ return False, "hook_run_not_failed"
414
+ if prefix == "protocol_debt" and str(data.get("status") or "").lower() not in {"open", "resolved", "forgiven"}:
415
+ return False, "protocol_debt_status_invalid"
416
+ if prefix == "session_correction_requirement" and str(data.get("status") or "").lower() not in {"open", "resolved"}:
417
+ return False, "correction_requirement_status_invalid"
418
+ return True, "db_ref_exists"
419
+
420
+
421
+ def validate_source_ref(
422
+ source_type: str,
423
+ source_ref: str,
424
+ *,
425
+ conn: sqlite3.Connection | None = None,
426
+ ) -> dict[str, Any]:
427
+ """Validate a source ref enough to decide whether it may reinforce a case."""
428
+ own_conn = conn is None
429
+ conn = conn or get_db()
430
+ clean_type = _normalize_source_type(source_type)
431
+ if not clean_type:
432
+ return {"validated": False, "validator": "source_type", "validation_error": "source_type_invalid"}
433
+ try:
434
+ clean_ref = _sanitize_ref(source_ref)
435
+ except ValueError as exc:
436
+ return {"validated": False, "validator": "ref_sanitize", "validation_error": str(exc)}
437
+
438
+ prefix = _ref_prefix(clean_ref)
439
+ allowed = SOURCE_REF_PREFIXES.get(clean_type, set())
440
+ if prefix not in allowed:
441
+ return {
442
+ "validated": False,
443
+ "validator": "ref_prefix",
444
+ "validation_error": f"ref_prefix_{prefix or 'missing'}_not_allowed_for_{clean_type}",
445
+ }
446
+
447
+ try:
448
+ if prefix in DB_REF_TABLES:
449
+ ok, reason = _validate_db_ref(conn, prefix, clean_ref, source_type=clean_type)
450
+ return {"validated": ok, "validator": "db_ref", "validation_error": "" if ok else reason}
451
+ if prefix == "test":
452
+ ok, reason = _validate_test_ref(clean_ref)
453
+ return {"validated": ok, "validator": "test_ref", "validation_error": "" if ok else reason}
454
+ if prefix in {"guardian_rule", "guardian_telemetry", "immune_finding", "watchdog_finding", "benchmark_case", "evidence"}:
455
+ return {"validated": True, "validator": "format_ref", "validation_error": ""}
456
+ finally:
457
+ if own_conn:
458
+ pass
459
+
460
+ return {"validated": False, "validator": "ref_prefix", "validation_error": f"unsupported_ref_prefix:{prefix}"}
461
+
462
+
463
+ def _status_for_case(*, source_type: str, severity: str, validated: bool, frequency_count: int) -> str:
464
+ if not validated:
465
+ return "candidate"
466
+ if source_type in INFERENCE_ONLY_SOURCES:
467
+ return "candidate"
468
+ if severity in {"p0", "p1"}:
469
+ return "analyzing"
470
+ if severity == "p2" and frequency_count >= 2:
471
+ return "analyzing"
472
+ if severity == "p4":
473
+ return "rejected"
474
+ return "candidate"
475
+
476
+
477
+ def _allowed_surfaces(*, privacy_level: str, status: str) -> list[str]:
478
+ privacy = _normalize_privacy(privacy_level)
479
+ if privacy == "secret":
480
+ return ["audit"]
481
+ if privacy in {"private", "sensitive"}:
482
+ return ["debug_local", "audit"]
483
+ if status in {"verified", "resolved"}:
484
+ return ["debug_local", "audit", "pre_action"]
485
+ return ["debug_local", "audit"]
486
+
487
+
488
+ def _case_from_row(row: sqlite3.Row | None) -> dict[str, Any]:
489
+ if not row:
490
+ return {}
491
+ data = dict(row)
492
+ for key in (
493
+ "entity_refs_json", "source_event_refs_json", "evidence_refs_json",
494
+ "symptom_json", "trigger_json", "missed_signal_json",
495
+ "wrong_assumption_json", "root_cause_json", "corrective_action_json",
496
+ "learning_resolution_json", "antibody_refs_json",
497
+ "allowed_surfaces_json", "metadata_json",
498
+ ):
499
+ default = [] if key.endswith("_refs_json") or key in {"antibody_refs_json", "allowed_surfaces_json"} else {}
500
+ data[key[:-5] if key.endswith("_json") else key] = _load_json(str(data.pop(key) or ""), default)
501
+ return data
502
+
503
+
504
+ def _source_event_from_row(row: sqlite3.Row | None) -> dict[str, Any]:
505
+ if not row:
506
+ return {}
507
+ data = dict(row)
508
+ data["validated"] = bool(data.get("validated"))
509
+ data["evidence_refs"] = _load_json(str(data.pop("evidence_refs_json") or ""), [])
510
+ data["metadata"] = _load_json(str(data.pop("metadata_json") or ""), {})
511
+ return data
512
+
513
+
514
+ def _learning_resolution_for_source(conn: sqlite3.Connection, source_type: str, source_ref: str) -> dict[str, Any]:
515
+ if source_type != "outcome_miss" or _ref_prefix(source_ref) != "outcome":
516
+ return {"action": "none", "learning_id": None, "resolver_reason": ""}
517
+ try:
518
+ outcome_id = int(_ref_value(source_ref))
519
+ except Exception:
520
+ return {"action": "none", "learning_id": None, "resolver_reason": "outcome_ref_invalid"}
521
+ row = conn.execute("SELECT learning_id FROM outcomes WHERE id = ?", (outcome_id,)).fetchone()
522
+ if row and int(row["learning_id"] or 0) > 0:
523
+ return {
524
+ "action": "merge",
525
+ "learning_id": int(row["learning_id"]),
526
+ "resolver_reason": "outcome_already_linked_learning_no_duplicate",
527
+ }
528
+ return {"action": "none", "learning_id": None, "resolver_reason": "outcome_has_no_learning"}
529
+
530
+
531
+ def ingest_failure(
532
+ *,
533
+ failure_type: str,
534
+ area: str,
535
+ primary_source_type: str,
536
+ primary_source_ref: str,
537
+ symptom: object,
538
+ trigger: object = "",
539
+ missed_signal: object = "",
540
+ wrong_assumption: object = "",
541
+ root_cause: object = "",
542
+ corrective_action: object = "",
543
+ severity: str = "p3",
544
+ confidence: float = 0.5,
545
+ entity_refs: list[str] | str | None = None,
546
+ evidence_refs: list[str] | str | None = None,
547
+ privacy_level: str = "normal",
548
+ observed_at: float | None = None,
549
+ idempotency_key: str = "",
550
+ metadata: dict[str, Any] | None = None,
551
+ conn: sqlite3.Connection | None = None,
552
+ ) -> dict[str, Any]:
553
+ """Create or reinforce a redacted failure-prevention case."""
554
+ conn = conn or get_db()
555
+ _ensure_tables(conn)
556
+ now = float(observed_at or _now())
557
+ clean_type = _normalize_failure_type(failure_type)
558
+ clean_source_type = _normalize_source_type(primary_source_type)
559
+ try:
560
+ clean_source_ref = _sanitize_ref(primary_source_ref)
561
+ clean_evidence_refs = [_sanitize_ref(ref) for ref in _as_list(evidence_refs)]
562
+ clean_entity_refs = [_sanitize_ref(ref) for ref in _as_list(entity_refs)]
563
+ except ValueError as exc:
564
+ return {"ok": False, "error": str(exc)}
565
+ clean_severity = _normalize_severity(severity)
566
+ clean_privacy = _normalize_privacy(privacy_level)
567
+ clean_confidence = _normalize_confidence(confidence)
568
+ clean_area = str(area or "").strip()[:160]
569
+ clean_metadata = sanitize_metadata(metadata or {})
570
+ if not isinstance(clean_metadata, dict):
571
+ clean_metadata = {"value": clean_metadata}
572
+
573
+ validation = validate_source_ref(clean_source_type, clean_source_ref, conn=conn)
574
+ validated = bool(validation.get("validated"))
575
+ symptom_field = field_evidence(symptom, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
576
+ trigger_field = field_evidence(trigger, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
577
+ missed_signal_field = field_evidence(missed_signal, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
578
+ wrong_assumption_field = field_evidence(wrong_assumption, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
579
+ root_cause_field = field_evidence(root_cause, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
580
+ corrective_action_field = field_evidence(corrective_action, source_refs=[clean_source_ref], confidence=clean_confidence, privacy_level=clean_privacy)
581
+ failure_uid = _stable_uid(
582
+ POLICY_VERSION,
583
+ clean_type,
584
+ clean_area,
585
+ _normalize_text(symptom_field["value_redacted"]),
586
+ )
587
+ source_event_uid = _stable_uid(POLICY_VERSION, failure_uid, clean_source_type, clean_source_ref)
588
+ learning_resolution = _learning_resolution_for_source(conn, clean_source_type, clean_source_ref)
589
+ source_ref_token = f"{clean_source_type}:{clean_source_ref}"
590
+
591
+ conn.execute(
592
+ """
593
+ INSERT OR IGNORE INTO failure_prevention_cases (
594
+ failure_uid, policy_version, failure_type, area, entity_refs_json,
595
+ primary_source_type, primary_source_ref, source_event_refs_json,
596
+ evidence_refs_json, symptom_json, trigger_json, missed_signal_json,
597
+ wrong_assumption_json, root_cause_json, corrective_action_json,
598
+ severity, frequency_count, confidence, status, learning_resolution_json,
599
+ antibody_refs_json, privacy_level, allowed_surfaces_json, opened_at,
600
+ updated_at, review_due_at, expires_at, false_positive_count,
601
+ last_triggered_at, metadata_json
602
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, 'candidate', ?, '[]', ?, ?, ?, ?, ?, ?, 0, ?, ?)
603
+ """,
604
+ (
605
+ failure_uid,
606
+ POLICY_VERSION,
607
+ clean_type,
608
+ clean_area,
609
+ _json(clean_entity_refs),
610
+ clean_source_type,
611
+ clean_source_ref,
612
+ _json([source_ref_token]),
613
+ _json(clean_evidence_refs),
614
+ _json(symptom_field),
615
+ _json(trigger_field),
616
+ _json(missed_signal_field),
617
+ _json(wrong_assumption_field),
618
+ _json(root_cause_field),
619
+ _json(corrective_action_field),
620
+ clean_severity,
621
+ clean_confidence,
622
+ _json(learning_resolution),
623
+ clean_privacy,
624
+ _json(_allowed_surfaces(privacy_level=clean_privacy, status="candidate")),
625
+ now,
626
+ now,
627
+ now + 14 * 86400,
628
+ now + 90 * 86400,
629
+ now,
630
+ _json(clean_metadata),
631
+ ),
632
+ )
633
+
634
+ cursor = conn.execute(
635
+ """
636
+ INSERT OR IGNORE INTO failure_source_events (
637
+ source_event_uid, failure_uid, policy_version, source_type,
638
+ source_ref, evidence_refs_json, observed_at, validated, validator,
639
+ validation_error, privacy_level, created_at, updated_at, metadata_json
640
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
641
+ """,
642
+ (
643
+ source_event_uid,
644
+ failure_uid,
645
+ POLICY_VERSION,
646
+ clean_source_type,
647
+ clean_source_ref,
648
+ _json(clean_evidence_refs),
649
+ now,
650
+ 1 if validated else 0,
651
+ str(validation.get("validator") or ""),
652
+ str(validation.get("validation_error") or ""),
653
+ clean_privacy,
654
+ now,
655
+ now,
656
+ _json({"idempotency_key_hash": _idempotency_marker(idempotency_key), **clean_metadata}),
657
+ ),
658
+ )
659
+ source_inserted = cursor.rowcount > 0
660
+ if source_inserted and validated:
661
+ conn.execute(
662
+ """
663
+ UPDATE failure_prevention_cases
664
+ SET frequency_count = frequency_count + 1,
665
+ last_triggered_at = ?,
666
+ updated_at = ?,
667
+ source_event_refs_json = ?,
668
+ evidence_refs_json = ?,
669
+ learning_resolution_json = ?
670
+ WHERE failure_uid = ?
671
+ """,
672
+ (
673
+ now,
674
+ now,
675
+ _append_unique(
676
+ conn.execute(
677
+ "SELECT source_event_refs_json FROM failure_prevention_cases WHERE failure_uid = ?",
678
+ (failure_uid,),
679
+ ).fetchone()["source_event_refs_json"],
680
+ [source_ref_token],
681
+ ),
682
+ _append_unique(
683
+ conn.execute(
684
+ "SELECT evidence_refs_json FROM failure_prevention_cases WHERE failure_uid = ?",
685
+ (failure_uid,),
686
+ ).fetchone()["evidence_refs_json"],
687
+ clean_evidence_refs,
688
+ ),
689
+ _json(learning_resolution),
690
+ failure_uid,
691
+ ),
692
+ )
693
+ elif source_inserted:
694
+ clean_metadata["validation_error"] = str(validation.get("validation_error") or "")
695
+ conn.execute(
696
+ "UPDATE failure_prevention_cases SET metadata_json = ?, updated_at = ? WHERE failure_uid = ?",
697
+ (_json(clean_metadata), now, failure_uid),
698
+ )
699
+
700
+ row = conn.execute("SELECT * FROM failure_prevention_cases WHERE failure_uid = ?", (failure_uid,)).fetchone()
701
+ frequency_count = int(row["frequency_count"] or 0) if row else 0
702
+ next_status = _status_for_case(
703
+ source_type=clean_source_type,
704
+ severity=clean_severity,
705
+ validated=validated,
706
+ frequency_count=frequency_count,
707
+ )
708
+ if row and str(row["status"] or "") not in {"verified", "resolved", "rolled_back"}:
709
+ conn.execute(
710
+ """
711
+ UPDATE failure_prevention_cases
712
+ SET status = ?, allowed_surfaces_json = ?, updated_at = ?
713
+ WHERE failure_uid = ?
714
+ """,
715
+ (
716
+ next_status,
717
+ _json(_allowed_surfaces(privacy_level=clean_privacy, status=next_status)),
718
+ now,
719
+ failure_uid,
720
+ ),
721
+ )
722
+ conn.commit()
723
+ case_row = conn.execute("SELECT * FROM failure_prevention_cases WHERE failure_uid = ?", (failure_uid,)).fetchone()
724
+ event_row = conn.execute("SELECT * FROM failure_source_events WHERE source_event_uid = ?", (source_event_uid,)).fetchone()
725
+ return {
726
+ "ok": True,
727
+ "failure_uid": failure_uid,
728
+ "source_event_uid": source_event_uid,
729
+ "source_event_inserted": source_inserted,
730
+ "validated": validated,
731
+ "validation_error": str(validation.get("validation_error") or ""),
732
+ "case": _safe_case(_case_from_row(case_row), surface="audit"),
733
+ "source_event": _safe_source_event(_source_event_from_row(event_row)),
734
+ }
735
+
736
+
737
+ def _case_exists(conn: sqlite3.Connection, failure_uid: str) -> bool:
738
+ return bool(conn.execute("SELECT 1 FROM failure_prevention_cases WHERE failure_uid = ?", (failure_uid,)).fetchone())
739
+
740
+
741
+ def _validate_action_ref(ref: str, *, field: str, allow_empty: bool = True) -> str:
742
+ if not ref and allow_empty:
743
+ return ""
744
+ clean = _sanitize_ref(ref, allow_empty=allow_empty)
745
+ return clean
746
+
747
+
748
+ def _validate_antibody_policy(
749
+ *,
750
+ action_type: str,
751
+ target_system: str,
752
+ target_ref: str,
753
+ activation_policy: str,
754
+ verification_ref: str,
755
+ verification_status: str,
756
+ approved_ref: str,
757
+ rollback_ref: str,
758
+ source_type: str,
759
+ ) -> None:
760
+ if action_type not in ACTION_TYPES:
761
+ raise ValueError("action_type_invalid")
762
+ if target_system not in TARGET_SYSTEMS:
763
+ raise ValueError("target_system_invalid")
764
+ if not target_ref.strip():
765
+ raise ValueError("target_ref_required")
766
+ if activation_policy not in ACTIVATION_POLICIES:
767
+ raise ValueError("activation_policy_invalid")
768
+ if verification_status not in VERIFICATION_STATUSES:
769
+ raise ValueError("verification_status_invalid")
770
+ if source_type in INFERENCE_ONLY_SOURCES and activation_policy != "candidate_only":
771
+ raise ValueError("inference_source_must_remain_candidate_only")
772
+ if verification_status == "not_applicable":
773
+ if action_type not in NOT_APPLICABLE_ACTIONS or activation_policy in {"warn", "block_after_verification"}:
774
+ raise ValueError("not_applicable_verification_not_allowed")
775
+ if activation_policy == "warn" and not verification_ref:
776
+ raise ValueError("warn_requires_verification_ref")
777
+ if activation_policy in {"warn", "block_after_verification"} and verification_ref and not _ref_prefix(verification_ref):
778
+ raise ValueError("verification_ref_must_be_traceable_ref")
779
+ if activation_policy == "block_after_verification":
780
+ if verification_status != "passed" or not verification_ref or not rollback_ref:
781
+ raise ValueError("block_requires_passed_verification_and_rollback")
782
+ if not _ref_prefix(rollback_ref):
783
+ raise ValueError("rollback_ref_must_be_traceable_ref")
784
+ if activation_policy == "manual_approval_required":
785
+ prefix = _ref_prefix(approved_ref)
786
+ if prefix not in APPROVAL_REF_PREFIXES:
787
+ raise ValueError("manual_approval_requires_traceable_approved_ref")
788
+
789
+
790
+ def propose_antibody_action(
791
+ *,
792
+ failure_uid: str,
793
+ action_type: str,
794
+ target_system: str,
795
+ target_ref: str,
796
+ action_payload_ref: str = "",
797
+ activation_policy: str = "candidate_only",
798
+ required_verification: str = "",
799
+ verification_ref: str = "",
800
+ verification_status: str = "missing",
801
+ approved_by: str = "",
802
+ approved_ref: str = "",
803
+ rollback_ref: str = "",
804
+ privacy_level: str = "normal",
805
+ metadata: dict[str, Any] | None = None,
806
+ conn: sqlite3.Connection | None = None,
807
+ ) -> dict[str, Any]:
808
+ """Record a proposed owner action without executing it."""
809
+ conn = conn or get_db()
810
+ _ensure_tables(conn)
811
+ clean_failure_uid = str(failure_uid or "").strip()
812
+ if not _case_exists(conn, clean_failure_uid):
813
+ return {"ok": False, "error": "failure_case_not_found"}
814
+ case = conn.execute(
815
+ "SELECT primary_source_type FROM failure_prevention_cases WHERE failure_uid = ?",
816
+ (clean_failure_uid,),
817
+ ).fetchone()
818
+ clean_action = str(action_type or "").strip()
819
+ clean_target_system = str(target_system or "").strip()
820
+ try:
821
+ clean_target_ref = _sanitize_ref(target_ref)
822
+ clean_payload_ref = _validate_action_ref(action_payload_ref, field="action_payload_ref")
823
+ clean_verification_ref = _validate_action_ref(verification_ref, field="verification_ref")
824
+ clean_approved_ref = _validate_action_ref(approved_ref, field="approved_ref")
825
+ clean_rollback_ref = _validate_action_ref(rollback_ref, field="rollback_ref")
826
+ except ValueError as exc:
827
+ return {"ok": False, "error": str(exc)}
828
+ clean_policy = str(activation_policy or "candidate_only").strip()
829
+ clean_verification_status = str(verification_status or "missing").strip()
830
+ clean_privacy = _normalize_privacy(privacy_level)
831
+ source_type = str(case["primary_source_type"] or "") if case else ""
832
+
833
+ try:
834
+ _validate_antibody_policy(
835
+ action_type=clean_action,
836
+ target_system=clean_target_system,
837
+ target_ref=clean_target_ref,
838
+ activation_policy=clean_policy,
839
+ verification_ref=clean_verification_ref,
840
+ verification_status=clean_verification_status,
841
+ approved_ref=clean_approved_ref,
842
+ rollback_ref=clean_rollback_ref,
843
+ source_type=source_type,
844
+ )
845
+ except ValueError as exc:
846
+ return {"ok": False, "error": str(exc)}
847
+
848
+ clean_metadata = sanitize_metadata(metadata or {})
849
+ if not isinstance(clean_metadata, dict):
850
+ clean_metadata = {"value": clean_metadata}
851
+ learning_resolution = None
852
+ if clean_action == "learning_resolve" and isinstance(clean_metadata.get("learning_candidate"), dict):
853
+ candidate = clean_metadata["learning_candidate"]
854
+ learning_resolution = resolve_learning_candidate(
855
+ category=str(candidate.get("category") or "nexo-ops"),
856
+ title=str(candidate.get("title") or ""),
857
+ content=str(candidate.get("content") or ""),
858
+ reasoning=str(candidate.get("reasoning") or ""),
859
+ prevention=str(candidate.get("prevention") or ""),
860
+ applies_to=str(candidate.get("applies_to") or ""),
861
+ priority=str(candidate.get("priority") or "medium"),
862
+ source_authority=str(candidate.get("source_authority") or "inference"),
863
+ conn=conn,
864
+ )
865
+ clean_metadata["learning_resolution"] = learning_resolution
866
+ conn.execute(
867
+ "UPDATE failure_prevention_cases SET learning_resolution_json = ?, updated_at = ? WHERE failure_uid = ?",
868
+ (_json(learning_resolution), _now(), clean_failure_uid),
869
+ )
870
+
871
+ now = _now()
872
+ antibody_uid = _stable_uid(POLICY_VERSION, clean_failure_uid, clean_action, clean_target_system, clean_target_ref)
873
+ status = "approved" if clean_policy == "manual_approval_required" else "proposed"
874
+ if clean_verification_status == "passed":
875
+ status = "verified"
876
+ conn.execute(
877
+ """
878
+ INSERT OR IGNORE INTO antibody_actions (
879
+ antibody_uid, failure_uid, policy_version, action_type, target_system,
880
+ target_ref, action_payload_ref, status, activation_policy,
881
+ required_verification, verification_ref, verification_status,
882
+ approved_by, approved_ref, rollback_ref, review_due_at, expires_at,
883
+ privacy_level, created_at, updated_at, verified_at, metadata_json
884
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
885
+ """,
886
+ (
887
+ antibody_uid,
888
+ clean_failure_uid,
889
+ POLICY_VERSION,
890
+ clean_action,
891
+ clean_target_system,
892
+ clean_target_ref,
893
+ clean_payload_ref,
894
+ status,
895
+ clean_policy,
896
+ redact_value(str(required_verification or "").strip()),
897
+ clean_verification_ref,
898
+ clean_verification_status,
899
+ redact_value(str(approved_by or "").strip())[:160],
900
+ clean_approved_ref,
901
+ clean_rollback_ref,
902
+ now + 14 * 86400,
903
+ now + 90 * 86400,
904
+ clean_privacy,
905
+ now,
906
+ now,
907
+ now if clean_verification_status == "passed" else None,
908
+ _json(clean_metadata),
909
+ ),
910
+ )
911
+ conn.execute(
912
+ "UPDATE failure_prevention_cases SET antibody_refs_json = ?, updated_at = ? WHERE failure_uid = ?",
913
+ (
914
+ _append_unique(
915
+ conn.execute(
916
+ "SELECT antibody_refs_json FROM failure_prevention_cases WHERE failure_uid = ?",
917
+ (clean_failure_uid,),
918
+ ).fetchone()["antibody_refs_json"],
919
+ [f"antibody:{antibody_uid}"],
920
+ ),
921
+ now,
922
+ clean_failure_uid,
923
+ ),
924
+ )
925
+ conn.commit()
926
+ row = conn.execute("SELECT * FROM antibody_actions WHERE antibody_uid = ?", (antibody_uid,)).fetchone()
927
+ data = dict(row) if row else {}
928
+ if data:
929
+ data["metadata"] = _load_json(str(data.pop("metadata_json") or ""), {})
930
+ return {"ok": True, "antibody_uid": antibody_uid, "antibody": _safe_antibody(data), "learning_resolution": sanitize_metadata(learning_resolution)}
931
+
932
+
933
+ def list_failure_cases(*, status: str = "", limit: int = 20, surface: str = "audit", conn: sqlite3.Connection | None = None) -> list[dict[str, Any]]:
934
+ conn = conn or get_db()
935
+ _ensure_tables(conn)
936
+ clauses = []
937
+ params: list[Any] = []
938
+ if status:
939
+ clauses.append("status = ?")
940
+ params.append(status)
941
+ where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
942
+ rows = conn.execute(
943
+ f"SELECT * FROM failure_prevention_cases {where} ORDER BY updated_at DESC LIMIT ?",
944
+ params + [max(1, int(limit or 20))],
945
+ ).fetchall()
946
+ cases = [_safe_case(_case_from_row(row), surface=surface) for row in rows]
947
+ return [case for case in cases if case]
948
+
949
+
950
+ def get_failure_case(failure_uid: str, *, surface: str = "audit", conn: sqlite3.Connection | None = None) -> dict[str, Any]:
951
+ conn = conn or get_db()
952
+ _ensure_tables(conn)
953
+ row = conn.execute("SELECT * FROM failure_prevention_cases WHERE failure_uid = ?", (failure_uid,)).fetchone()
954
+ return _safe_case(_case_from_row(row), surface=surface)
955
+
956
+
957
+ def mark_false_positive(
958
+ failure_uid: str,
959
+ *,
960
+ antibody_uid: str = "",
961
+ reason: str = "",
962
+ conn: sqlite3.Connection | None = None,
963
+ ) -> dict[str, Any]:
964
+ conn = conn or get_db()
965
+ _ensure_tables(conn)
966
+ clean_failure_uid = str(failure_uid or "").strip()
967
+ if not _case_exists(conn, clean_failure_uid):
968
+ return {"ok": False, "error": "failure_case_not_found"}
969
+ now = _now()
970
+ conn.execute(
971
+ """
972
+ UPDATE failure_prevention_cases
973
+ SET false_positive_count = false_positive_count + 1,
974
+ updated_at = ?
975
+ WHERE failure_uid = ?
976
+ """,
977
+ (now, clean_failure_uid),
978
+ )
979
+ row = conn.execute("SELECT false_positive_count FROM failure_prevention_cases WHERE failure_uid = ?", (clean_failure_uid,)).fetchone()
980
+ count = int(row["false_positive_count"] or 0)
981
+ if count >= 2:
982
+ conn.execute(
983
+ "UPDATE failure_prevention_cases SET status = 'conflict_review', allowed_surfaces_json = ?, updated_at = ? WHERE failure_uid = ?",
984
+ (_json(["debug_local", "audit"]), now, clean_failure_uid),
985
+ )
986
+ if antibody_uid:
987
+ conn.execute(
988
+ """
989
+ UPDATE antibody_actions
990
+ SET status = 'false_positive',
991
+ activation_policy = 'candidate_only',
992
+ metadata_json = ?,
993
+ updated_at = ?
994
+ WHERE antibody_uid = ?
995
+ """,
996
+ (_json({"false_positive_reason": redact_value(reason)}), now, antibody_uid),
997
+ )
998
+ conn.commit()
999
+ return {"ok": True, "false_positive_count": count, "case": get_failure_case(clean_failure_uid, conn=conn)}
1000
+
1001
+
1002
+ def rollback_antibody_action(
1003
+ antibody_uid: str,
1004
+ *,
1005
+ rollback_ref: str,
1006
+ reason: str = "",
1007
+ conn: sqlite3.Connection | None = None,
1008
+ ) -> dict[str, Any]:
1009
+ conn = conn or get_db()
1010
+ _ensure_tables(conn)
1011
+ clean_uid = str(antibody_uid or "").strip()
1012
+ try:
1013
+ clean_rollback_ref = _validate_action_ref(rollback_ref, field="rollback_ref", allow_empty=False)
1014
+ except ValueError as exc:
1015
+ return {"ok": False, "error": str(exc)}
1016
+ row = conn.execute("SELECT failure_uid FROM antibody_actions WHERE antibody_uid = ?", (clean_uid,)).fetchone()
1017
+ if not row:
1018
+ return {"ok": False, "error": "antibody_not_found"}
1019
+ now = _now()
1020
+ conn.execute(
1021
+ """
1022
+ UPDATE antibody_actions
1023
+ SET status = 'rolled_back',
1024
+ activation_policy = 'candidate_only',
1025
+ rollback_ref = ?,
1026
+ metadata_json = ?,
1027
+ updated_at = ?
1028
+ WHERE antibody_uid = ?
1029
+ """,
1030
+ (clean_rollback_ref, _json({"rollback_reason": redact_value(reason)}), now, clean_uid),
1031
+ )
1032
+ conn.execute(
1033
+ "UPDATE failure_prevention_cases SET status = 'rolled_back', updated_at = ? WHERE failure_uid = ?",
1034
+ (now, row["failure_uid"]),
1035
+ )
1036
+ conn.commit()
1037
+ return {"ok": True, "antibody_uid": clean_uid, "failure_uid": row["failure_uid"]}
1038
+
1039
+
1040
+ __all__ = [
1041
+ "POLICY_VERSION",
1042
+ "field_evidence",
1043
+ "get_failure_case",
1044
+ "ingest_failure",
1045
+ "list_failure_cases",
1046
+ "mark_false_positive",
1047
+ "propose_antibody_action",
1048
+ "redact_value",
1049
+ "rollback_antibody_action",
1050
+ "sanitize_metadata",
1051
+ "validate_source_ref",
1052
+ ]