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,763 @@
1
+ """Operational causal graph facade over the existing Knowledge Graph.
2
+
3
+ Verified causal edges live in ``kg_edges``. Unverified suggestions live in
4
+ ``causal_edge_candidates`` until a caller explicitly promotes them.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import re
12
+ import sqlite3
13
+ import subprocess
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ ROOT = Path(__file__).resolve().parents[1]
20
+ VALID_RELATIONS = {
21
+ "causal:motivated_by",
22
+ "causal:resolved_by",
23
+ "causal:prevented",
24
+ "causal:verified_by",
25
+ "causal:depends_on",
26
+ "causal:blocked_by",
27
+ "causal:superseded_by",
28
+ "causal:regressed_by",
29
+ "causal:reverted_by",
30
+ "ops:contains",
31
+ "ops:produced",
32
+ "ops:reviewed_by",
33
+ "ops:approved_by",
34
+ }
35
+ ACTIVE_EDGE_STATUSES = {"active", "verified", "stale", "contradicted", "superseded", "retracted"}
36
+ CANDIDATE_STATUSES = {"proposed", "review", "approved", "promoted", "rejected", "expired", "superseded"}
37
+ PRIVACY_LEVELS = {"public", "normal", "private", "sensitive", "secret"}
38
+ PRIVACY_ALIASES = {"internal": "normal", "confidential": "sensitive"}
39
+ SECRET_PATTERNS = (
40
+ re.compile(
41
+ r"\b(?:(?:sk|pk|rk)(?:[-_](?:live|test|proj))?[-_][A-Za-z0-9_=-]{10,}|"
42
+ r"(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|"
43
+ r"(?:xoxb|xoxp)-[A-Za-z0-9_=-]{10,})\b"
44
+ ),
45
+ re.compile(r"\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b", re.IGNORECASE),
46
+ re.compile(
47
+ r"\b(api[_-]?key|token|secret|password|passwd|pwd|authorization)\s*[:=]\s*['\"]?[^'\"\s,;]+",
48
+ re.IGNORECASE,
49
+ ),
50
+ )
51
+
52
+
53
+ def _db():
54
+ import db
55
+
56
+ return db.get_db()
57
+
58
+
59
+ def _kg():
60
+ import knowledge_graph
61
+
62
+ return knowledge_graph
63
+
64
+
65
+ def _kg_db():
66
+ import cognitive
67
+
68
+ return cognitive._get_db()
69
+
70
+
71
+ def _table_exists(conn: sqlite3.Connection, table_name: str) -> bool:
72
+ try:
73
+ return conn.execute(
74
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1",
75
+ (table_name,),
76
+ ).fetchone() is not None
77
+ except Exception:
78
+ return False
79
+
80
+
81
+ def _parse_json(value: str, default: Any) -> Any:
82
+ try:
83
+ parsed = json.loads(value or "")
84
+ return parsed if parsed is not None else default
85
+ except Exception:
86
+ return default
87
+
88
+
89
+ def _json(value: Any) -> str:
90
+ return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
91
+
92
+
93
+ def _normalize(value: Any) -> str:
94
+ return " ".join(str(value or "").strip().lower().split())
95
+
96
+
97
+ def _privacy(value: str) -> str:
98
+ clean = _normalize(value)
99
+ clean = PRIVACY_ALIASES.get(clean, clean)
100
+ return clean if clean in PRIVACY_LEVELS else "private"
101
+
102
+
103
+ def redact_reason(reason: str, *, privacy_level: str = "normal", max_chars: int = 240) -> tuple[str, bool]:
104
+ privacy = _privacy(privacy_level)
105
+ if privacy == "secret":
106
+ return "", bool(reason)
107
+ text = str(reason or "").strip()
108
+ redacted = text
109
+ for pattern in SECRET_PATTERNS:
110
+ redacted = pattern.sub("[REDACTED_SECRET]", redacted)
111
+ if len(redacted) > max_chars:
112
+ redacted = redacted[: max(0, max_chars - 3)].rstrip() + "..."
113
+ return redacted, redacted != text
114
+
115
+
116
+ def edge_uid_for(
117
+ *,
118
+ source_type: str,
119
+ source_ref: str,
120
+ relation: str,
121
+ target_type: str,
122
+ target_ref: str,
123
+ evidence_refs: list[str] | tuple[str, ...] | None,
124
+ ) -> str:
125
+ seed = "|".join(
126
+ [
127
+ _normalize(source_type),
128
+ _normalize(source_ref),
129
+ relation.strip(),
130
+ _normalize(target_type),
131
+ _normalize(target_ref),
132
+ ",".join(sorted(str(ref).strip() for ref in (evidence_refs or []) if str(ref).strip())),
133
+ ]
134
+ )
135
+ return hashlib.sha256(seed.encode("utf-8", errors="ignore")).hexdigest()
136
+
137
+
138
+ def candidate_uid_for(
139
+ *,
140
+ project_key: str = "",
141
+ source_type: str,
142
+ source_ref: str,
143
+ relation: str,
144
+ target_type: str,
145
+ target_ref: str,
146
+ producer: str,
147
+ source_event_uid: str = "",
148
+ evidence_refs: list[str] | tuple[str, ...] | None,
149
+ ) -> str:
150
+ seed = "|".join(
151
+ [
152
+ _normalize(project_key),
153
+ _normalize(source_type),
154
+ _normalize(source_ref),
155
+ relation.strip(),
156
+ _normalize(target_type),
157
+ _normalize(target_ref),
158
+ _normalize(producer),
159
+ _normalize(source_event_uid),
160
+ ",".join(sorted(str(ref).strip() for ref in (evidence_refs or []) if str(ref).strip())),
161
+ ]
162
+ )
163
+ return hashlib.sha256(seed.encode("utf-8", errors="ignore")).hexdigest()
164
+
165
+
166
+ def ensure_kg_indexes() -> None:
167
+ conn = _kg_db()
168
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_kg_edges_source_relation_active ON kg_edges(source_id, relation, valid_until)")
169
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_kg_edges_target_relation_active ON kg_edges(target_id, relation, valid_until)")
170
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_kg_edges_relation_active ON kg_edges(relation, valid_until)")
171
+ conn.commit()
172
+
173
+
174
+ def _query_exists(conn, table: str, column: str, value: str) -> bool:
175
+ if not _table_exists(conn, table):
176
+ return False
177
+ row = conn.execute(f"SELECT 1 FROM {table} WHERE {column}=? LIMIT 1", (value,)).fetchone()
178
+ return row is not None
179
+
180
+
181
+ def _path_exists(ref: str, *, repo_root: Path) -> bool:
182
+ raw = str(ref or "").strip()
183
+ if not raw:
184
+ return False
185
+ path = Path(raw).expanduser()
186
+ if not path.is_absolute():
187
+ path = repo_root / raw
188
+ try:
189
+ return path.exists()
190
+ except Exception:
191
+ return False
192
+
193
+
194
+ def _git_commit_exists(ref: str, *, repo_root: Path) -> bool:
195
+ clean = str(ref or "").strip()
196
+ if not re.fullmatch(r"[0-9a-fA-F]{7,40}", clean):
197
+ return False
198
+ if not (repo_root / ".git").exists():
199
+ return True
200
+ result = subprocess.run(
201
+ ["git", "cat-file", "-e", f"{clean}^{{commit}}"],
202
+ cwd=repo_root,
203
+ stdout=subprocess.DEVNULL,
204
+ stderr=subprocess.DEVNULL,
205
+ check=False,
206
+ )
207
+ return result.returncode == 0
208
+
209
+
210
+ def validate_ref(
211
+ ref_type: str,
212
+ ref: str,
213
+ *,
214
+ evidence_refs: list[str] | None = None,
215
+ repo_root: Path = ROOT,
216
+ ) -> tuple[bool, str]:
217
+ clean_type = _normalize(ref_type)
218
+ clean_ref = str(ref or "").strip()
219
+ if not clean_type or not clean_ref:
220
+ return False, "missing_ref"
221
+
222
+ conn = _db()
223
+ if clean_type == "protocol_task":
224
+ return (_query_exists(conn, "protocol_tasks", "task_id", clean_ref), "missing_ref")
225
+ if clean_type in {"workflow", "workflow_run"}:
226
+ return (_query_exists(conn, "workflow_runs", "run_id", clean_ref), "missing_ref")
227
+ if clean_type == "workflow_checkpoint":
228
+ return (_query_exists(conn, "workflow_checkpoints", "id", clean_ref), "missing_ref")
229
+ if clean_type == "commitment":
230
+ return (_query_exists(conn, "commitments", "id", clean_ref), "missing_ref")
231
+ if clean_type in {"change_log", "change"}:
232
+ return (_query_exists(conn, "change_log", "id", clean_ref), "missing_ref")
233
+ if clean_type == "memory_event":
234
+ return (_query_exists(conn, "memory_events", "event_uid", clean_ref), "missing_ref")
235
+ if clean_type in {"file", "artifact", "spec", "audit", "test"}:
236
+ if _path_exists(clean_ref, repo_root=repo_root):
237
+ return True, ""
238
+ return (bool(evidence_refs), "missing_ref")
239
+ if clean_type == "release":
240
+ return (bool(re.fullmatch(r"v?\d+\.\d+(?:\.\d+)?(?:[-+][a-zA-Z0-9_.-]+)?", clean_ref)), "missing_ref")
241
+ if clean_type == "commit":
242
+ return (_git_commit_exists(clean_ref, repo_root=repo_root), "missing_ref")
243
+ if clean_type == "risk":
244
+ return (clean_ref.startswith("risk:") and bool(evidence_refs), "missing_ref")
245
+ if clean_type == "finding":
246
+ return (clean_ref.startswith("finding:") and bool(evidence_refs), "missing_ref")
247
+ return False, "unsupported_ref_type"
248
+
249
+
250
+ def _candidate_row(row: sqlite3.Row | None) -> dict[str, Any]:
251
+ if not row:
252
+ return {}
253
+ item = dict(row)
254
+ item["evidence_refs"] = _parse_json(item.pop("evidence_refs_json", "[]"), [])
255
+ item["metadata"] = _parse_json(item.pop("metadata_json", "{}"), {})
256
+ return item
257
+
258
+
259
+ def propose_candidate(
260
+ *,
261
+ source_type: str,
262
+ source_ref: str,
263
+ relation: str,
264
+ target_type: str,
265
+ target_ref: str,
266
+ reason_public: str = "",
267
+ evidence_refs: list[str] | None = None,
268
+ source_event_uid: str = "",
269
+ producer: str = "manual",
270
+ project_key: str = "",
271
+ privacy_level: str = "normal",
272
+ confidence: float = 0.5,
273
+ status: str = "proposed",
274
+ metadata: dict[str, Any] | None = None,
275
+ now: float | None = None,
276
+ ) -> dict[str, Any]:
277
+ conn = _db()
278
+ stamp = float(now if now is not None else time.time())
279
+ refs = [str(ref).strip() for ref in (evidence_refs or []) if str(ref).strip()]
280
+ privacy = _privacy(privacy_level)
281
+ clean_reason, redacted = redact_reason(reason_public, privacy_level=privacy)
282
+ clean_status = status if status in CANDIDATE_STATUSES else "review"
283
+ review_reason = ""
284
+ if relation not in VALID_RELATIONS:
285
+ clean_status = "review"
286
+ review_reason = "unknown_relation"
287
+ elif privacy == "secret":
288
+ clean_status = "review"
289
+ review_reason = "secret_reference_only"
290
+ elif not refs:
291
+ clean_status = "review"
292
+ review_reason = "missing_evidence"
293
+ candidate_uid = candidate_uid_for(
294
+ project_key=project_key,
295
+ source_type=source_type,
296
+ source_ref=source_ref,
297
+ relation=relation,
298
+ target_type=target_type,
299
+ target_ref=target_ref,
300
+ producer=producer,
301
+ source_event_uid=source_event_uid,
302
+ evidence_refs=refs,
303
+ )
304
+ meta = dict(metadata or {})
305
+ if redacted:
306
+ meta["redaction_applied"] = True
307
+ conn.execute(
308
+ """
309
+ INSERT INTO causal_edge_candidates (
310
+ candidate_uid, created_at, updated_at, source_type, source_ref,
311
+ relation, target_type, target_ref, reason_public, evidence_refs_json,
312
+ source_event_uid, producer, project_key, privacy_level, confidence,
313
+ status, review_reason, promoted_edge_uid, metadata_json
314
+ )
315
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?)
316
+ ON CONFLICT(candidate_uid) DO UPDATE SET
317
+ updated_at = excluded.updated_at,
318
+ reason_public = excluded.reason_public,
319
+ confidence = MAX(causal_edge_candidates.confidence, excluded.confidence),
320
+ status = CASE
321
+ WHEN causal_edge_candidates.status = 'promoted' THEN 'promoted'
322
+ ELSE excluded.status
323
+ END,
324
+ review_reason = excluded.review_reason,
325
+ metadata_json = excluded.metadata_json
326
+ """,
327
+ (
328
+ candidate_uid,
329
+ stamp,
330
+ stamp,
331
+ _normalize(source_type),
332
+ str(source_ref).strip(),
333
+ relation,
334
+ _normalize(target_type),
335
+ str(target_ref).strip(),
336
+ clean_reason,
337
+ _json(refs),
338
+ str(source_event_uid or "").strip(),
339
+ _normalize(producer) or "manual",
340
+ str(project_key or "").strip(),
341
+ privacy,
342
+ max(0.0, min(1.0, float(confidence or 0.0))),
343
+ clean_status,
344
+ review_reason,
345
+ _json(meta),
346
+ ),
347
+ )
348
+ conn.commit()
349
+ row = conn.execute("SELECT * FROM causal_edge_candidates WHERE candidate_uid=?", (candidate_uid,)).fetchone()
350
+ item = _candidate_row(row)
351
+ item["ok"] = True
352
+ return item
353
+
354
+
355
+ def list_candidates(*, status: str = "", limit: int = 20) -> list[dict[str, Any]]:
356
+ conn = _db()
357
+ if status:
358
+ rows = conn.execute(
359
+ "SELECT * FROM causal_edge_candidates WHERE status=? ORDER BY updated_at DESC LIMIT ?",
360
+ (status, max(1, int(limit or 20))),
361
+ ).fetchall()
362
+ else:
363
+ rows = conn.execute(
364
+ "SELECT * FROM causal_edge_candidates ORDER BY updated_at DESC LIMIT ?",
365
+ (max(1, int(limit or 20)),),
366
+ ).fetchall()
367
+ return [_candidate_row(row) for row in rows]
368
+
369
+
370
+ def _active_edge_by_uid(edge_uid: str) -> dict[str, Any]:
371
+ conn = _kg_db()
372
+ rows = conn.execute(
373
+ "SELECT * FROM kg_edges WHERE valid_until IS NULL AND properties LIKE ?",
374
+ (f'%"{edge_uid}"%',),
375
+ ).fetchall()
376
+ for row in rows:
377
+ item = dict(row)
378
+ props = _parse_json(item.get("properties") or "{}", {})
379
+ if props.get("edge_uid") == edge_uid:
380
+ item["properties_dict"] = props
381
+ return item
382
+ return {}
383
+
384
+
385
+ def upsert_active_edge(
386
+ *,
387
+ source_type: str,
388
+ source_ref: str,
389
+ relation: str,
390
+ target_type: str,
391
+ target_ref: str,
392
+ reason_public: str,
393
+ evidence_refs: list[str],
394
+ source_event_uid: str = "",
395
+ producer: str = "manual",
396
+ project_key: str = "",
397
+ privacy_level: str = "normal",
398
+ confidence: float = 0.8,
399
+ status: str = "",
400
+ repo_root: Path = ROOT,
401
+ ) -> dict[str, Any]:
402
+ refs = [str(ref).strip() for ref in (evidence_refs or []) if str(ref).strip()]
403
+ if relation not in VALID_RELATIONS:
404
+ return {"ok": False, "status": "rejected", "review_reason": "unknown_relation"}
405
+ if not refs:
406
+ return {"ok": False, "status": "review", "review_reason": "missing_evidence"}
407
+ privacy = _privacy(privacy_level)
408
+ if privacy == "secret":
409
+ return {"ok": False, "status": "review", "review_reason": "secret_reference_only"}
410
+ source_ok, source_reason = validate_ref(source_type, source_ref, evidence_refs=refs, repo_root=repo_root)
411
+ if not source_ok:
412
+ return {"ok": False, "status": "review", "review_reason": f"source_{source_reason}"}
413
+ target_ok, target_reason = validate_ref(target_type, target_ref, evidence_refs=refs, repo_root=repo_root)
414
+ if not target_ok:
415
+ return {"ok": False, "status": "review", "review_reason": f"target_{target_reason}"}
416
+
417
+ edge_uid = edge_uid_for(
418
+ source_type=source_type,
419
+ source_ref=source_ref,
420
+ relation=relation,
421
+ target_type=target_type,
422
+ target_ref=target_ref,
423
+ evidence_refs=refs,
424
+ )
425
+ existing = _active_edge_by_uid(edge_uid)
426
+ if existing:
427
+ return {"ok": True, "action": "NOOP", "edge_id": existing["id"], "edge_uid": edge_uid}
428
+
429
+ clean_reason, redacted = redact_reason(reason_public, privacy_level=privacy)
430
+ edge_status = status if status in ACTIVE_EDGE_STATUSES else ("verified" if confidence >= 0.9 else "active")
431
+ props = {
432
+ "schema_version": 1,
433
+ "edge_uid": edge_uid,
434
+ "status": edge_status,
435
+ "project_key": str(project_key or "").strip(),
436
+ "reason_public": clean_reason,
437
+ "evidence_refs": refs,
438
+ "source_event_uid": str(source_event_uid or "").strip(),
439
+ "producer": _normalize(producer) or "manual",
440
+ "privacy_level": privacy,
441
+ "redaction_applied": bool(redacted),
442
+ "created_by": "causal_graph",
443
+ }
444
+ ensure_kg_indexes()
445
+ result = _kg().upsert_edge(
446
+ source_type=_normalize(source_type),
447
+ source_ref=str(source_ref).strip(),
448
+ relation=relation,
449
+ target_type=_normalize(target_type),
450
+ target_ref=str(target_ref).strip(),
451
+ weight=1.0,
452
+ confidence=max(0.0, min(1.0, float(confidence or 0.0))),
453
+ source_memory_id=str(source_event_uid or "").strip(),
454
+ properties=props,
455
+ )
456
+ result.update({"ok": True, "edge_uid": edge_uid, "properties": props})
457
+ return result
458
+
459
+
460
+ def promote_candidate(candidate_uid: str) -> dict[str, Any]:
461
+ conn = _db()
462
+ row = conn.execute("SELECT * FROM causal_edge_candidates WHERE candidate_uid=?", (candidate_uid,)).fetchone()
463
+ if not row:
464
+ return {"ok": False, "error": "candidate_not_found"}
465
+ candidate = _candidate_row(row)
466
+ if candidate.get("status") not in {"approved", "proposed"}:
467
+ return {"ok": False, "error": f"candidate_status_not_promotable:{candidate.get('status')}"}
468
+ result = upsert_active_edge(
469
+ source_type=candidate["source_type"],
470
+ source_ref=candidate["source_ref"],
471
+ relation=candidate["relation"],
472
+ target_type=candidate["target_type"],
473
+ target_ref=candidate["target_ref"],
474
+ reason_public=candidate.get("reason_public") or "",
475
+ evidence_refs=candidate.get("evidence_refs") or [],
476
+ source_event_uid=candidate.get("source_event_uid") or "",
477
+ producer=candidate.get("producer") or "candidate",
478
+ project_key=candidate.get("project_key") or "",
479
+ privacy_level=candidate.get("privacy_level") or "normal",
480
+ confidence=float(candidate.get("confidence") or 0.0),
481
+ )
482
+ if not result.get("ok"):
483
+ conn.execute(
484
+ "UPDATE causal_edge_candidates SET status='review', review_reason=?, updated_at=? WHERE candidate_uid=?",
485
+ (result.get("review_reason") or result.get("error") or "promotion_failed", time.time(), candidate_uid),
486
+ )
487
+ conn.commit()
488
+ return result
489
+ conn.execute(
490
+ "UPDATE causal_edge_candidates SET status='promoted', promoted_edge_uid=?, updated_at=? WHERE candidate_uid=?",
491
+ (result.get("edge_uid") or "", time.time(), candidate_uid),
492
+ )
493
+ conn.commit()
494
+ result["candidate_uid"] = candidate_uid
495
+ return result
496
+
497
+
498
+ def record_task_close_edges(
499
+ *,
500
+ task_id: str,
501
+ change_log_id: str | int = "",
502
+ test_refs: list[str] | None = None,
503
+ risk_ref: str = "",
504
+ evidence_refs: list[str] | None = None,
505
+ project_key: str = "",
506
+ reason_public: str = "",
507
+ ) -> list[dict[str, Any]]:
508
+ refs = [str(ref).strip() for ref in (evidence_refs or []) if str(ref).strip()]
509
+ if not refs:
510
+ refs = [f"protocol_task:{task_id}"]
511
+ results: list[dict[str, Any]] = []
512
+ if change_log_id not in ("", None):
513
+ change_ref = str(change_log_id)
514
+ results.append(
515
+ upsert_active_edge(
516
+ source_type="protocol_task",
517
+ source_ref=task_id,
518
+ relation="ops:produced",
519
+ target_type="change_log",
520
+ target_ref=change_ref,
521
+ reason_public=reason_public or "Task produced a change log entry.",
522
+ evidence_refs=refs,
523
+ producer="task_close",
524
+ project_key=project_key,
525
+ confidence=0.9,
526
+ )
527
+ )
528
+ results.append(
529
+ upsert_active_edge(
530
+ source_type="change_log",
531
+ source_ref=change_ref,
532
+ relation="causal:motivated_by",
533
+ target_type="protocol_task",
534
+ target_ref=task_id,
535
+ reason_public=reason_public or "Change was motivated by the closed task.",
536
+ evidence_refs=refs,
537
+ producer="task_close",
538
+ project_key=project_key,
539
+ confidence=0.85,
540
+ )
541
+ )
542
+ for test_ref in test_refs or []:
543
+ results.append(
544
+ upsert_active_edge(
545
+ source_type="protocol_task",
546
+ source_ref=task_id,
547
+ relation="causal:verified_by",
548
+ target_type="test",
549
+ target_ref=test_ref,
550
+ reason_public=reason_public or "Task was verified by test evidence.",
551
+ evidence_refs=[*refs, f"test:{test_ref}"],
552
+ producer="task_close",
553
+ project_key=project_key,
554
+ confidence=0.92,
555
+ )
556
+ )
557
+ if risk_ref:
558
+ results.append(
559
+ upsert_active_edge(
560
+ source_type="change_log",
561
+ source_ref=str(change_log_id),
562
+ relation="causal:prevented",
563
+ target_type="risk",
564
+ target_ref=risk_ref,
565
+ reason_public=reason_public or "Change prevented a documented risk.",
566
+ evidence_refs=refs,
567
+ producer="task_close",
568
+ project_key=project_key,
569
+ confidence=0.8,
570
+ )
571
+ )
572
+ return results
573
+
574
+
575
+ def record_commitment_resolution_edges(commitment_id: str) -> list[dict[str, Any]]:
576
+ conn = _db()
577
+ if not _table_exists(conn, "commitments"):
578
+ return []
579
+ row = conn.execute("SELECT * FROM commitments WHERE id=?", (commitment_id,)).fetchone()
580
+ if not row:
581
+ return []
582
+ item = dict(row)
583
+ if item.get("status") not in {"fulfilled"}:
584
+ return []
585
+ action_ref_type = str(item.get("action_ref_type") or "").strip()
586
+ action_ref_id = str(item.get("action_ref_id") or "").strip()
587
+ evidence_ref = str(item.get("evidence_ref") or "").strip()
588
+ refs = [ref for ref in [evidence_ref, f"commitment:{commitment_id}"] if ref]
589
+ results: list[dict[str, Any]] = []
590
+ if action_ref_type and action_ref_id:
591
+ results.append(
592
+ upsert_active_edge(
593
+ source_type="commitment",
594
+ source_ref=commitment_id,
595
+ relation="causal:resolved_by",
596
+ target_type=action_ref_type,
597
+ target_ref=action_ref_id,
598
+ reason_public="Commitment was fulfilled by the linked action.",
599
+ evidence_refs=refs,
600
+ producer="commitment",
601
+ project_key=str(item.get("project_key") or ""),
602
+ confidence=float(item.get("confidence") or 0.8),
603
+ )
604
+ )
605
+ if evidence_ref:
606
+ results.append(
607
+ upsert_active_edge(
608
+ source_type="commitment",
609
+ source_ref=commitment_id,
610
+ relation="causal:verified_by",
611
+ target_type="artifact",
612
+ target_ref=evidence_ref,
613
+ reason_public="Commitment resolution has explicit evidence.",
614
+ evidence_refs=refs,
615
+ producer="commitment",
616
+ project_key=str(item.get("project_key") or ""),
617
+ confidence=float(item.get("confidence") or 0.8),
618
+ )
619
+ )
620
+ return results
621
+
622
+
623
+ def propose_from_memory_executive(event: dict[str, Any], decision: dict[str, Any]) -> dict[str, Any]:
624
+ if str(decision.get("decision_kind") or "") != "proposed_causal_edge":
625
+ return {"ok": False, "error": "decision_not_causal"}
626
+ metadata = event.get("metadata") if isinstance(event.get("metadata"), dict) else {}
627
+ edge = metadata.get("causal_edge") if isinstance(metadata.get("causal_edge"), dict) else {}
628
+ if not edge:
629
+ return {"ok": False, "error": "missing_causal_edge_payload"}
630
+ return propose_candidate(
631
+ source_type=str(edge.get("source_type") or ""),
632
+ source_ref=str(edge.get("source_ref") or ""),
633
+ relation=str(edge.get("relation") or ""),
634
+ target_type=str(edge.get("target_type") or ""),
635
+ target_ref=str(edge.get("target_ref") or ""),
636
+ reason_public=str(edge.get("reason_public") or decision.get("reason") or ""),
637
+ evidence_refs=[str(ref) for ref in edge.get("evidence_refs") or event.get("evidence_refs") or []],
638
+ source_event_uid=str(event.get("event_uid") or ""),
639
+ producer="memory_executive",
640
+ project_key=str(event.get("project_key") or ""),
641
+ privacy_level=str(edge.get("privacy_level") or event.get("privacy_level") or "normal"),
642
+ confidence=float(edge.get("confidence") or decision.get("confidence") or 0.5),
643
+ status="proposed",
644
+ metadata={"memory_decision": decision.get("dedupe_key") or ""},
645
+ )
646
+
647
+
648
+ def approve_candidate(candidate_uid: str) -> dict[str, Any]:
649
+ conn = _db()
650
+ row = conn.execute("SELECT * FROM causal_edge_candidates WHERE candidate_uid=?", (candidate_uid,)).fetchone()
651
+ if not row:
652
+ return {"ok": False, "error": "candidate_not_found"}
653
+ conn.execute(
654
+ "UPDATE causal_edge_candidates SET status='approved', review_reason='', updated_at=? WHERE candidate_uid=?",
655
+ (time.time(), candidate_uid),
656
+ )
657
+ conn.commit()
658
+ return {"ok": True, "candidate_uid": candidate_uid, "status": "approved"}
659
+
660
+
661
+ def _node_id(ref_type: str, ref: str) -> int | None:
662
+ node = _kg().get_node(_normalize(ref_type), str(ref).strip())
663
+ if not node:
664
+ return None
665
+ return int(node["id"])
666
+
667
+
668
+ def query_edges(
669
+ *,
670
+ ref_type: str,
671
+ ref: str,
672
+ project_key: str = "",
673
+ include_historical: bool = False,
674
+ limit: int = 8,
675
+ ) -> dict[str, Any]:
676
+ node_id = _node_id(ref_type, ref)
677
+ if node_id is None:
678
+ return {"ok": True, "has_evidence": False, "edges": [], "message": "no tengo evidencia suficiente"}
679
+ conn = _kg_db()
680
+ conditions = ["(e.source_id=? OR e.target_id=?)"]
681
+ params: list[Any] = [node_id, node_id]
682
+ if not include_historical:
683
+ conditions.append("e.valid_until IS NULL")
684
+ rows = conn.execute(
685
+ f"""
686
+ SELECT e.*, src.node_type AS source_type, src.node_ref AS source_ref,
687
+ tgt.node_type AS target_type, tgt.node_ref AS target_ref
688
+ FROM kg_edges e
689
+ JOIN kg_nodes src ON src.id=e.source_id
690
+ JOIN kg_nodes tgt ON tgt.id=e.target_id
691
+ WHERE {' AND '.join(conditions)}
692
+ ORDER BY e.confidence DESC, e.id DESC
693
+ LIMIT ?
694
+ """,
695
+ [*params, max(1, min(int(limit or 8), 50))],
696
+ ).fetchall()
697
+ edges: list[dict[str, Any]] = []
698
+ for row in rows:
699
+ item = dict(row)
700
+ relation = str(item.get("relation") or "")
701
+ if not (relation.startswith("causal:") or relation.startswith("ops:")):
702
+ continue
703
+ props = _parse_json(item.get("properties") or "{}", {})
704
+ status = str(props.get("status") or "active")
705
+ privacy = _privacy(props.get("privacy_level") or "normal")
706
+ if not include_historical and status not in {"active", "verified"}:
707
+ continue
708
+ if project_key and props.get("project_key") and props.get("project_key") != project_key:
709
+ continue
710
+ if privacy == "secret":
711
+ continue
712
+ if privacy == "sensitive":
713
+ item["renderable_reason"] = "Tengo una relacion causal con evidencia privada; puedo revisarla si me das permiso para usar ese contexto."
714
+ else:
715
+ item["renderable_reason"] = props.get("reason_public") or ""
716
+ item["properties_dict"] = props
717
+ edges.append(item)
718
+ return {
719
+ "ok": True,
720
+ "has_evidence": bool(edges),
721
+ "edges": edges[: max(1, min(int(limit or 8), 50))],
722
+ "message": "" if edges else "no tengo evidencia suficiente",
723
+ }
724
+
725
+
726
+ def render_query_result(result: dict[str, Any], *, max_chars: int = 1200) -> str:
727
+ edges = result.get("edges") or []
728
+ if not edges:
729
+ return "No tengo evidencia suficiente."
730
+ lines = ["Causal evidence:"]
731
+ for edge in edges:
732
+ props = edge.get("properties_dict") or {}
733
+ refs = props.get("evidence_refs") or []
734
+ reason = edge.get("renderable_reason") or props.get("reason_public") or ""
735
+ lines.append(
736
+ f"- {edge.get('source_type')}:{edge.get('source_ref')} {edge.get('relation')} "
737
+ f"{edge.get('target_type')}:{edge.get('target_ref')} - {reason} "
738
+ f"(evidence: {', '.join(refs) or 'none'})"
739
+ )
740
+ text = "\n".join(lines)
741
+ return text if len(text) <= max_chars else text[: max(0, max_chars - 3)].rstrip() + "..."
742
+
743
+
744
+ __all__ = [
745
+ "ACTIVE_EDGE_STATUSES",
746
+ "CANDIDATE_STATUSES",
747
+ "VALID_RELATIONS",
748
+ "approve_candidate",
749
+ "candidate_uid_for",
750
+ "edge_uid_for",
751
+ "ensure_kg_indexes",
752
+ "list_candidates",
753
+ "promote_candidate",
754
+ "propose_from_memory_executive",
755
+ "propose_candidate",
756
+ "query_edges",
757
+ "record_commitment_resolution_edges",
758
+ "record_task_close_edges",
759
+ "redact_reason",
760
+ "render_query_result",
761
+ "upsert_active_edge",
762
+ "validate_ref",
763
+ ]