nexo-brain 2.3.0 → 2.3.2

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 (299) hide show
  1. package/README.md +1 -1
  2. package/bin/nexo-brain.js +92 -9
  3. package/bin/postinstall.js +22 -15
  4. package/package.json +7 -4
  5. package/src/auto_update.py +194 -5
  6. package/src/crons/sync.py +6 -2
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_entities.py +1 -0
  9. package/src/db/_episodic.py +1 -0
  10. package/src/db/_learnings.py +1 -0
  11. package/src/db/_reminders.py +1 -0
  12. package/src/db/_schema.py +11 -1
  13. package/src/db/_sessions.py +1 -0
  14. package/src/db/_skills.py +1 -0
  15. package/src/hooks/capture-tool-logs.sh +23 -6
  16. package/src/hooks/session-start.sh +4 -3
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/update.py +377 -26
  19. package/src/scripts/deep-sleep/apply_findings.py +1 -0
  20. package/src/scripts/deep-sleep/collect.py +1 -0
  21. package/src/scripts/deep-sleep/extract.py +1 -0
  22. package/src/scripts/deep-sleep/synthesize.py +1 -0
  23. package/src/scripts/nexo-catchup.py +29 -4
  24. package/src/scripts/nexo-daily-self-audit.py +21 -1
  25. package/src/scripts/nexo-evolution-run.py +21 -1
  26. package/src/scripts/nexo-learning-housekeep.py +1 -0
  27. package/src/scripts/nexo-postmortem-consolidator.py +34 -9
  28. package/src/scripts/nexo-sleep.py +32 -10
  29. package/src/scripts/nexo-synthesis.py +29 -9
  30. package/src/scripts/nexo-update.sh +109 -7
  31. package/src/scripts/nexo-watchdog.sh +122 -58
  32. package/src/server.py +66 -1
  33. package/src/tools_coordination.py +1 -0
  34. package/src/tools_sessions.py +1 -0
  35. package/scripts/migrate-to-unified 2.sh +0 -813
  36. package/scripts/migrate-to-unified.sh +0 -813
  37. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  38. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  39. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  40. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  41. package/scripts/nexo-preflight.sh +0 -236
  42. package/scripts/pre-commit-check 2.sh +0 -55
  43. package/scripts/pre-commit-check.sh +0 -55
  44. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  45. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  46. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  47. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  48. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  49. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  50. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  51. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  52. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  53. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  54. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  60. package/src/auto_close_sessions 2.py +0 -159
  61. package/src/auto_update 2.py +0 -634
  62. package/src/claim_graph 2.py +0 -323
  63. package/src/cognitive/__init__ 2.py +0 -62
  64. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  65. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  66. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  67. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  69. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  70. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  72. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  73. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  74. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  75. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  76. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  77. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  78. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  79. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  80. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  81. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  82. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  83. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  84. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  85. package/src/cognitive/_core 2.py +0 -567
  86. package/src/cognitive/_decay 2.py +0 -382
  87. package/src/cognitive/_ingest 2.py +0 -892
  88. package/src/cognitive/_memory 2.py +0 -912
  89. package/src/cognitive/_search 2.py +0 -949
  90. package/src/cognitive/_trust 2.py +0 -464
  91. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  92. package/src/crons/manifest 2.json +0 -106
  93. package/src/crons/sync 2.py +0 -217
  94. package/src/dashboard/__init__ 2.py +0 -0
  95. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  96. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  97. package/src/dashboard/app 2.py +0 -789
  98. package/src/db/__init__ 2.py +0 -89
  99. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  110. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  111. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  112. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  113. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  114. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  115. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  116. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  117. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  118. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  119. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  120. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  121. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  122. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  123. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  124. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  125. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  126. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  127. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  128. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  129. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  130. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  131. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  132. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  133. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  134. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  135. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  136. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  137. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  138. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  139. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  140. package/src/db/_core 2.py +0 -417
  141. package/src/db/_credentials 2.py +0 -124
  142. package/src/db/_entities 2.py +0 -178
  143. package/src/db/_episodic 2.py +0 -738
  144. package/src/db/_evolution 2.py +0 -54
  145. package/src/db/_fts 2.py +0 -406
  146. package/src/db/_learnings 2.py +0 -168
  147. package/src/db/_reminders 2.py +0 -338
  148. package/src/db/_schema 2.py +0 -364
  149. package/src/db/_sessions 2.py +0 -300
  150. package/src/db/_tasks 2.py +0 -91
  151. package/src/evolution_cycle 2.py +0 -266
  152. package/src/hnsw_index 2.py +0 -254
  153. package/src/hooks/auto_capture 2.py +0 -208
  154. package/src/hooks/caffeinate-guard 2.sh +0 -8
  155. package/src/hooks/capture-session 2.sh +0 -21
  156. package/src/hooks/capture-tool-logs 2.sh +0 -127
  157. package/src/hooks/daily-briefing-check 2.sh +0 -33
  158. package/src/hooks/inbox-hook 2.sh +0 -76
  159. package/src/hooks/post-compact 2.sh +0 -148
  160. package/src/hooks/pre-compact 2.sh +0 -151
  161. package/src/hooks/session-start 2.sh +0 -268
  162. package/src/hooks/session-stop 2.sh +0 -140
  163. package/src/kg_populate 2.py +0 -290
  164. package/src/knowledge_graph 2.py +0 -257
  165. package/src/maintenance 2.py +0 -59
  166. package/src/migrate_embeddings 2.py +0 -122
  167. package/src/plugin_loader 2.py +0 -202
  168. package/src/plugins/__init__ 2.py +0 -0
  169. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  172. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  175. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  183. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  185. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  187. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  188. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  189. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  190. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  191. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  192. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  193. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  194. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  195. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  196. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  197. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  198. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  199. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  200. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  201. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  202. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  203. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  204. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  205. package/src/plugins/adaptive_mode 2.py +0 -805
  206. package/src/plugins/agents 2.py +0 -52
  207. package/src/plugins/artifact_registry 2.py +0 -450
  208. package/src/plugins/backup 2.py +0 -104
  209. package/src/plugins/cognitive_memory 2.py +0 -564
  210. package/src/plugins/core_rules 2.py +0 -252
  211. package/src/plugins/cortex 2.py +0 -299
  212. package/src/plugins/entities 2.py +0 -67
  213. package/src/plugins/episodic_memory 2.py +0 -533
  214. package/src/plugins/evolution 2.py +0 -115
  215. package/src/plugins/guard 2.py +0 -746
  216. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  217. package/src/plugins/preferences 2.py +0 -47
  218. package/src/plugins/update 2.py +0 -256
  219. package/src/requirements 2.txt +0 -12
  220. package/src/rules/__init__ 2.py +0 -0
  221. package/src/rules/core-rules 2.json +0 -331
  222. package/src/rules/migrate 2.py +0 -207
  223. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  229. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  230. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  231. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  232. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  233. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  234. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  235. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  236. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  237. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  238. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  239. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  240. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  241. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  242. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  243. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  244. package/src/scripts/check-context 2.py +0 -264
  245. package/src/scripts/nexo-auto-update 2.py +0 -6
  246. package/src/scripts/nexo-backup 2.sh +0 -25
  247. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  248. package/src/scripts/nexo-catchup 2.py +0 -242
  249. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  250. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  251. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  252. package/src/scripts/nexo-evolution-run 2.py +0 -597
  253. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  254. package/src/scripts/nexo-github-monitor 2.py +0 -256
  255. package/src/scripts/nexo-immune 2.py +0 -927
  256. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  257. package/src/scripts/nexo-install 2.py +0 -6
  258. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  259. package/src/scripts/nexo-learning-validator 2.py +0 -207
  260. package/src/scripts/nexo-migrate 2.py +0 -232
  261. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  262. package/src/scripts/nexo-pre-commit 2.py +0 -120
  263. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  264. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  265. package/src/scripts/nexo-reflection 2.py +0 -253
  266. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  267. package/src/scripts/nexo-send-email 2.py +0 -25
  268. package/src/scripts/nexo-send-email.py +0 -25
  269. package/src/scripts/nexo-send-reply 2.py +0 -178
  270. package/src/scripts/nexo-send-reply.py +0 -178
  271. package/src/scripts/nexo-sleep 2.py +0 -592
  272. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  273. package/src/scripts/nexo-synthesis 2.py +0 -253
  274. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  275. package/src/scripts/nexo-update 2.sh +0 -161
  276. package/src/scripts/nexo-watchdog 2.sh +0 -878
  277. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  278. package/src/server 2.py +0 -733
  279. package/src/storage_router 2.py +0 -32
  280. package/src/tools_coordination 2.py +0 -102
  281. package/src/tools_credentials 2.py +0 -68
  282. package/src/tools_learnings 2.py +0 -220
  283. package/src/tools_menu 2.py +0 -227
  284. package/src/tools_reminders 2.py +0 -86
  285. package/src/tools_reminders_crud 2.py +0 -159
  286. package/src/tools_sessions 2.py +0 -476
  287. package/src/tools_task_history 2.py +0 -57
  288. package/templates/CLAUDE.md 2.template +0 -63
  289. package/templates/openclaw 2.json +0 -13
  290. package/tests/__init__ 2.py +0 -0
  291. package/tests/__init__.py +0 -0
  292. package/tests/conftest 2.py +0 -71
  293. package/tests/conftest.py +0 -71
  294. package/tests/test_cognitive 2.py +0 -205
  295. package/tests/test_cognitive.py +0 -205
  296. package/tests/test_knowledge_graph 2.py +0 -140
  297. package/tests/test_knowledge_graph.py +0 -140
  298. package/tests/test_migrations 2.py +0 -137
  299. package/tests/test_migrations.py +0 -137
@@ -1,746 +0,0 @@
1
- """Guard plugin — Error prevention closed-loop system.
2
-
3
- Surfaces relevant learnings at the moment of action, tracks repetitions,
4
- and provides stats on error prevention effectiveness.
5
- """
6
- import json
7
- import os
8
- from datetime import datetime, timedelta
9
- from db import get_db, find_similar_learnings, extract_keywords, search_learnings, search_changes
10
-
11
-
12
-
13
- def _load_schema_cache() -> dict:
14
- """Load cached DB schemas from schema_cache.json."""
15
- try:
16
- path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "schema_cache.json")
17
- if os.path.exists(path):
18
- with open(path) as f:
19
- return json.load(f)
20
- except Exception:
21
- pass
22
- return {}
23
-
24
-
25
- def _get_nexo_table_schema(table_name: str) -> str:
26
- """Get schema for a nexo.db table via PRAGMA."""
27
- conn = get_db()
28
- try:
29
- rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall()
30
- if rows:
31
- cols = [f"{r['name']}({r['type']})" for r in rows]
32
- return ", ".join(cols)
33
- except Exception:
34
- pass
35
- return ""
36
-
37
-
38
- def _extract_table_names(content: str) -> set:
39
- """Extract SQL table names from source code."""
40
- import re
41
- tables = set()
42
- # Match FROM/JOIN/INTO/UPDATE/TABLE patterns
43
- patterns = [
44
- r'(?:FROM|JOIN|INTO|UPDATE)\s+`?(\w+)`?',
45
- r'CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?',
46
- r'DESCRIBE\s+`?(\w+)`?',
47
- r'table_info\([\'\"]?(\w+)[\'\"]?\)',
48
- ]
49
- for pat in patterns:
50
- for m in re.finditer(pat, content, re.IGNORECASE):
51
- tables.add(m.group(1))
52
- # Filter out SQL keywords that might match
53
- sql_keywords = {'SELECT', 'WHERE', 'AND', 'OR', 'NOT', 'NULL', 'SET', 'VALUES', 'INTO', 'AS'}
54
- return {t for t in tables if t.upper() not in sql_keywords}
55
-
56
-
57
- def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "true") -> str:
58
- """Check learnings relevant to files/area before editing. Call BEFORE any code change.
59
-
60
- Args:
61
- files: Comma-separated file paths about to be edited
62
- area: System area (webapp, shopify, infrastructure, nexo-ops, etc.)
63
- include_schemas: Include DB table schemas if files touch database code (true/false)
64
- """
65
- conn = get_db()
66
- include_schemas_bool = include_schemas.lower() in ("true", "1", "yes")
67
- file_list = [f.strip() for f in files.split(",") if f.strip()] if files else []
68
-
69
- result = {
70
- "learnings": [],
71
- "universal_rules": [],
72
- "schemas": {},
73
- "area_repetition_rate": 0.0,
74
- "blocking_rules": [],
75
- }
76
-
77
- seen_ids = set()
78
-
79
- # 1. By file path — learnings mentioning the file name or parent directory
80
- hit_ids = []
81
- for filepath in file_list:
82
- from pathlib import Path
83
- p = Path(filepath)
84
- filename = p.name
85
- parent_dir = p.parent.name
86
-
87
- rows = conn.execute(
88
- "SELECT id, category, title, content, priority, weight FROM learnings WHERE INSTR(content, ?) > 0 OR INSTR(content, ?) > 0",
89
- (filename, parent_dir)
90
- ).fetchall()
91
- for r in rows:
92
- if r["id"] not in seen_ids:
93
- seen_ids.add(r["id"])
94
- hit_ids.append(r["id"])
95
- pri = r["priority"] or "medium"
96
- w = r["weight"] or 0.5
97
- result["learnings"].append({"id": r["id"], "category": r["category"], "rule": r["title"], "priority": pri, "weight": w})
98
-
99
- # 2. By area/category
100
- if area:
101
- rows = conn.execute(
102
- "SELECT id, category, title, content, priority, weight FROM learnings WHERE category = ?",
103
- (area,)
104
- ).fetchall()
105
- for r in rows:
106
- if r["id"] not in seen_ids:
107
- seen_ids.add(r["id"])
108
- hit_ids.append(r["id"])
109
- pri = r["priority"] or "medium"
110
- w = r["weight"] or 0.5
111
- result["learnings"].append({"id": r["id"], "category": r["category"], "rule": r["title"], "priority": pri, "weight": w})
112
-
113
- # 3. Universal rules — only from matching area or nexo-ops (not ALL learnings)
114
- universal_categories = {"nexo-ops"}
115
- if area:
116
- universal_categories.add(area)
117
- placeholders = ",".join("?" for _ in universal_categories)
118
- rows = conn.execute(
119
- f"SELECT id, category, title, content, priority FROM learnings WHERE "
120
- f"category IN ({placeholders}) AND ("
121
- f"content LIKE '%SIEMPRE%' OR content LIKE '%NUNCA%' OR content LIKE '%ANTES%' "
122
- f"OR content LIKE '%always%' OR content LIKE '%never%')",
123
- tuple(universal_categories)
124
- ).fetchall()
125
- for r in rows:
126
- if r["id"] not in seen_ids:
127
- seen_ids.add(r["id"])
128
- result["universal_rules"].append({"id": r["id"], "rule": r["title"], "category": r["category"], "priority": r["priority"] or "medium"})
129
-
130
- # 4. DB schemas if files contain SQL keywords
131
- if include_schemas_bool and file_list:
132
- all_tables = set()
133
- for filepath in file_list:
134
- try:
135
- with open(filepath, 'r', errors='ignore') as f:
136
- content = f.read()
137
- sql_keywords = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE TABLE']
138
- if any(kw in content.upper() for kw in sql_keywords):
139
- all_tables.update(_extract_table_names(content))
140
- except (FileNotFoundError, PermissionError):
141
- continue
142
-
143
- cache = _load_schema_cache()
144
- for table in all_tables:
145
- # Try nexo.db first
146
- schema = _get_nexo_table_schema(table)
147
- if schema:
148
- result["schemas"][table] = schema
149
- elif "cloud_sql" in cache and table in cache["cloud_sql"]:
150
- result["schemas"][table] = cache["cloud_sql"][table]
151
-
152
- # 5. Check for blocking rules — two paths:
153
- # (a) 5+ repetitions (existing behavior)
154
- # (b) Learning contains NUNCA/NEVER/PROHIBIDO and matches semantically (aggressive mode)
155
- import re
156
- BLOCKING_KEYWORDS = re.compile(
157
- r'\bNUNCA\b|\bNEVER\b|\bPROHIBIDO\b|\bNO\s+\w+\b|\bFORBIDDEN\b|\bBLOCKING\b|\bSIEMPRE\b|\bALWAYS\b',
158
- re.IGNORECASE
159
- )
160
- # Check both learnings and universal_rules for blocking
161
- all_candidates = [(l, "learning") for l in result["learnings"]] + \
162
- [(u, "universal") for u in result["universal_rules"]]
163
- blocking_seen = set()
164
- for learning, source in all_candidates:
165
- lid = learning["id"]
166
- if lid in blocking_seen:
167
- continue
168
- rep_count = conn.execute(
169
- "SELECT COUNT(*) as cnt FROM error_repetitions WHERE original_learning_id = ?",
170
- (lid,)
171
- ).fetchone()["cnt"]
172
-
173
- # Path (a): 5+ repetitions
174
- if rep_count >= 5:
175
- blocking_seen.add(lid)
176
- result["blocking_rules"].append({
177
- "id": lid, "rule": learning["rule"], "repetitions": rep_count,
178
- "reason": "repeated_error"
179
- })
180
- continue
181
-
182
- # Path (b): Only promote to blocking if high/critical priority AND title has prohibition keyword
183
- pri = learning.get("priority", "medium")
184
- if pri in ("critical", "high") and BLOCKING_KEYWORDS.search(learning["rule"]):
185
- blocking_seen.add(lid)
186
- result["blocking_rules"].append({
187
- "id": lid, "rule": learning["rule"], "repetitions": rep_count,
188
- "reason": "prohibition_keyword"
189
- })
190
-
191
- # 5b. Behavioral rules — when called without files (session-level check)
192
- if not file_list:
193
- behavioral = conn.execute(
194
- """SELECT l.id, l.title, l.category, COUNT(e.id) as violations
195
- FROM learnings l
196
- LEFT JOIN error_repetitions e ON e.original_learning_id = l.id
197
- WHERE l.category = 'nexo-ops' AND l.status = 'active'
198
- GROUP BY l.id
199
- ORDER BY violations DESC, l.created_at DESC
200
- LIMIT 5"""
201
- ).fetchall()
202
- if behavioral:
203
- result["behavioral_rules"] = [
204
- {"id": r["id"], "rule": r["title"], "violations": r["violations"]}
205
- for r in behavioral
206
- ]
207
-
208
- # 6. Area repetition rate
209
- if area:
210
- total_area = conn.execute(
211
- "SELECT COUNT(*) as cnt FROM learnings WHERE category = ?", (area,)
212
- ).fetchone()["cnt"]
213
- reps_area = conn.execute(
214
- "SELECT COUNT(*) as cnt FROM error_repetitions WHERE area = ?", (area,)
215
- ).fetchone()["cnt"]
216
- if total_area > 0:
217
- result["area_repetition_rate"] = round(reps_area / total_area, 2)
218
-
219
- # 7. Cognitive metacognition — semantic search for related warnings
220
- # Trust score modulates rigor: <40 = paranoid mode (more results, lower threshold)
221
- cognitive_warnings = []
222
- trust_note = ""
223
- try:
224
- import cognitive
225
- trust = cognitive.get_trust_score()
226
-
227
- # Rigor modulation based on trust
228
- if trust < 40:
229
- cog_top_k = 6 # More results
230
- cog_min_score = 0.55 # Lower threshold = catch more
231
- trust_note = f" [RIGOR: PARANOID — trust={trust:.0f}]"
232
- elif trust > 80:
233
- cog_top_k = 2 # Fewer results
234
- cog_min_score = 0.75 # Higher threshold = only strong matches
235
- trust_note = f" [RIGOR: FLUENT — trust={trust:.0f}]"
236
- else:
237
- cog_top_k = 3
238
- cog_min_score = 0.65
239
-
240
- query_parts = []
241
- if file_list:
242
- query_parts.append(f"editing files: {', '.join(file_list[:5])}")
243
- if area:
244
- query_parts.append(f"area: {area}")
245
- if query_parts:
246
- query_text = ". ".join(query_parts)
247
- cog_results = cognitive.search(
248
- query_text, top_k=cog_top_k, min_score=cog_min_score,
249
- stores="ltm", source_type_filter="learning", rehearse=False
250
- )
251
- for r in cog_results:
252
- cognitive_warnings.append(
253
- f"[{r['score']:.2f}]: {r['source_title']} — {r['content'][:200]}"
254
- )
255
- except Exception:
256
- pass # Cognitive is optional
257
-
258
- # 8. Somatic markers — risk score per file/area
259
- somatic_risk = 0.0
260
- somatic_details = {}
261
- try:
262
- import cognitive
263
- risk_result = cognitive.somatic_get_risk(file_list, area)
264
- somatic_risk = risk_result["max_risk"]
265
- somatic_details = risk_result["scores"]
266
- # Validated recovery: if no learnings found, guard check is "clean"
267
- if not result["learnings"]:
268
- for fp in file_list:
269
- cognitive.somatic_guard_decay(fp, "file")
270
- except Exception:
271
- pass
272
-
273
- # Record guard hits on learnings (for weight auto-adjustment)
274
- import time
275
- if hit_ids:
276
- for lid in hit_ids:
277
- conn.execute(
278
- "UPDATE learnings SET guard_hits = COALESCE(guard_hits, 0) + 1, last_guard_hit_at = ? WHERE id = ?",
279
- (time.time(), lid)
280
- )
281
-
282
- # Log the guard check
283
- conn.execute(
284
- "INSERT INTO guard_checks (session_id, files, area, learnings_returned, blocking_rules_returned) "
285
- "VALUES (?, ?, ?, ?, ?)",
286
- ("", files, area, len(result["learnings"]) + len(result["universal_rules"]),
287
- len(result["blocking_rules"]))
288
- )
289
- conn.commit()
290
-
291
- # Sort learnings by weight (highest first)
292
- result["learnings"].sort(key=lambda x: x.get("weight", 0.5), reverse=True)
293
-
294
- # Format output
295
- lines = []
296
- if result["blocking_rules"]:
297
- lines.append("BLOCKING RULES (resolve BEFORE writing):")
298
- for r in result["blocking_rules"]:
299
- reason = r.get("reason", "repeated_error")
300
- if reason == "prohibition_keyword":
301
- lines.append(f" #{r['id']} [PROHIBIT]: {r['rule']}")
302
- else:
303
- lines.append(f" #{r['id']} ({r['repetitions']}x repeated): {r['rule']}")
304
- lines.append("")
305
-
306
- if result["learnings"]:
307
- shown = result["learnings"][:10] # Cap at 10, not 15
308
- lines.append(f"RELEVANT LEARNINGS ({len(result['learnings'])}, showing {len(shown)}):")
309
- for l in shown:
310
- lines.append(f" #{l['id']} [{l['category']}] {l['rule']}")
311
- lines.append("")
312
-
313
- if result.get("behavioral_rules"):
314
- lines.append("SESSION BEHAVIORAL RULES (top 5 most-violated):")
315
- for r in result["behavioral_rules"]:
316
- v = f" ({r['violations']}x violated)" if r["violations"] > 0 else ""
317
- lines.append(f" #{r['id']} {r['rule']}{v}")
318
- lines.append("")
319
-
320
- if result["universal_rules"]:
321
- shown_u = result["universal_rules"][:5] # Cap at 5
322
- lines.append(f"UNIVERSAL RULES ({len(result['universal_rules'])}, showing {len(shown_u)}):")
323
- for r in shown_u:
324
- lines.append(f" #{r['id']} {r['rule']}")
325
- lines.append("")
326
-
327
- if result["schemas"]:
328
- lines.append("DB SCHEMAS:")
329
- for table, schema in result["schemas"].items():
330
- lines.append(f" {table}: {schema}")
331
- lines.append("")
332
-
333
- if result["area_repetition_rate"] > 0:
334
- lines.append(f"Area repetition rate: {result['area_repetition_rate']:.0%}")
335
-
336
- if cognitive_warnings:
337
- lines.append(f"\nCOGNITIVE SEMANTIC MATCHES{trust_note}:")
338
- for w in cognitive_warnings:
339
- lines.append(f" COGNITIVE MATCH {w}")
340
-
341
- if somatic_risk > 0:
342
- if somatic_risk > 0.8:
343
- lines.insert(0, "CRITICAL RISK (score {:.2f}) — suggest code review before editing".format(somatic_risk))
344
- elif somatic_risk > 0.5:
345
- lines.insert(0, "HIGH RISK (score {:.2f}) — extra caution recommended".format(somatic_risk))
346
- else:
347
- lines.append("\nSomatic risk: {:.2f} (low)".format(somatic_risk))
348
- if somatic_details:
349
- lines.append("Risk scores:")
350
- for target, data in somatic_details.items():
351
- lines.append(" {}: {:.2f} ({} incidents, last: {})".format(
352
- target, data["risk"], data["incidents"], data["last"][:10] if data["last"] else "unknown"))
353
-
354
- if not lines:
355
- return "No relevant learnings found for these files/area."
356
-
357
- return "\n".join(lines)
358
-
359
-
360
- def handle_guard_stats(period_days: int = 7) -> str:
361
- """Get guard system statistics for the specified period.
362
-
363
- Args:
364
- period_days: Number of days to look back (default 7)
365
- """
366
- conn = get_db()
367
- cutoff = (datetime.now() - timedelta(days=period_days)).strftime("%Y-%m-%d %H:%M:%S")
368
-
369
- total_learnings = conn.execute("SELECT COUNT(*) as cnt FROM learnings").fetchone()["cnt"]
370
-
371
- total_reps = conn.execute(
372
- "SELECT COUNT(*) as cnt FROM error_repetitions WHERE created_at > ?", (cutoff,)
373
- ).fetchone()["cnt"]
374
-
375
- # Repetition rate
376
- new_learnings_period = conn.execute(
377
- "SELECT COUNT(*) as cnt FROM learnings WHERE created_at > ?",
378
- ((datetime.now() - timedelta(days=period_days)).timestamp(),)
379
- ).fetchone()["cnt"]
380
- rep_rate = round(total_reps / new_learnings_period, 2) if new_learnings_period > 0 else 0.0
381
-
382
- # Previous period for trend
383
- prev_cutoff = (datetime.now() - timedelta(days=period_days * 2)).strftime("%Y-%m-%d %H:%M:%S")
384
- prev_reps = conn.execute(
385
- "SELECT COUNT(*) as cnt FROM error_repetitions WHERE created_at > ? AND created_at <= ?",
386
- (prev_cutoff, cutoff)
387
- ).fetchone()["cnt"]
388
- trend = "stable"
389
- if total_reps < prev_reps:
390
- trend = "improving"
391
- elif total_reps > prev_reps:
392
- trend = "worsening"
393
-
394
- # Top areas
395
- area_rows = conn.execute(
396
- "SELECT area, COUNT(*) as cnt FROM error_repetitions WHERE created_at > ? GROUP BY area ORDER BY cnt DESC LIMIT 5",
397
- (cutoff,)
398
- ).fetchall()
399
-
400
- # Most ignored learnings (most repetitions)
401
- ignored_rows = conn.execute(
402
- "SELECT original_learning_id, COUNT(*) as cnt FROM error_repetitions "
403
- "GROUP BY original_learning_id ORDER BY cnt DESC LIMIT 5"
404
- ).fetchall()
405
- most_ignored = []
406
- for r in ignored_rows:
407
- lr = conn.execute("SELECT title FROM learnings WHERE id = ?", (r["original_learning_id"],)).fetchone()
408
- if lr:
409
- most_ignored.append({"id": r["original_learning_id"], "title": lr["title"], "times_repeated": r["cnt"]})
410
-
411
- # Guard checks performed
412
- checks_count = conn.execute(
413
- "SELECT COUNT(*) as cnt FROM guard_checks WHERE created_at > ?", (cutoff,)
414
- ).fetchone()["cnt"]
415
-
416
- lines = [
417
- f"GUARD STATS (last {period_days} days):",
418
- f" Repetition rate: {rep_rate:.0%} ({trend})",
419
- f" Total learnings: {total_learnings}",
420
- f" Repetitions in period: {total_reps}",
421
- f" Guard checks performed: {checks_count}",
422
- ]
423
-
424
- if area_rows:
425
- lines.append(" Top areas:")
426
- for r in area_rows:
427
- lines.append(f" {r['area']}: {r['cnt']} repetitions")
428
-
429
- if most_ignored:
430
- lines.append(" Most repeated learnings:")
431
- for m in most_ignored:
432
- lines.append(f" #{m['id']} ({m['times_repeated']}x): {m['title'][:60]}")
433
-
434
- return "\n".join(lines)
435
-
436
-
437
- def handle_guard_log_repetition(new_learning_id: int, original_learning_id: int, similarity: float = 0.75) -> str:
438
- """Log that a new learning is similar to an existing one (repetition detected).
439
-
440
- Args:
441
- new_learning_id: ID of the new learning
442
- original_learning_id: ID of the original learning it matches
443
- similarity: Similarity score (0-1)
444
- """
445
- conn = get_db()
446
-
447
- # Get the area from the new learning
448
- row = conn.execute("SELECT category FROM learnings WHERE id = ?", (new_learning_id,)).fetchone()
449
- if not row:
450
- return f"ERROR: Learning #{new_learning_id} not found."
451
- area = row["category"]
452
-
453
- conn.execute(
454
- "INSERT INTO error_repetitions (new_learning_id, original_learning_id, similarity, area) VALUES (?,?,?,?)",
455
- (new_learning_id, original_learning_id, similarity, area)
456
- )
457
- conn.commit()
458
-
459
- return f"Repetition logged: #{new_learning_id} similar to #{original_learning_id} ({similarity:.0%})"
460
-
461
-
462
- def handle_somatic_check(files: str = "", area: str = "") -> str:
463
- """View somatic risk scores for specific files and/or area.
464
- Args:
465
- files: Comma-separated file paths to check
466
- area: System area to check
467
- """
468
- try:
469
- import cognitive
470
- file_list = [f.strip() for f in files.split(",") if f.strip()] if files else []
471
- result = cognitive.somatic_get_risk(file_list, area)
472
- if not result["scores"]:
473
- return "No somatic markers found for these targets."
474
- lines = ["Max risk: {:.2f}".format(result["max_risk"]), ""]
475
- for target, data in result["scores"].items():
476
- level = "CRITICAL" if data["risk"] > 0.8 else "HIGH" if data["risk"] > 0.5 else "Low"
477
- lines.append(" {} {}: {:.2f} ({} incidents, last: {})".format(
478
- level, target, data["risk"], data["incidents"], data["last"][:10] if data["last"] else "unknown"))
479
- return "\n".join(lines)
480
- except Exception as e:
481
- return "Error: {}".format(e)
482
-
483
-
484
- def handle_somatic_stats() -> str:
485
- """View top 10 riskiest files/areas and system-wide risk distribution."""
486
- try:
487
- import cognitive
488
- top = cognitive.somatic_top_risks(limit=10)
489
- if not top:
490
- return "No somatic markers recorded yet."
491
- lines = ["TOP RISK TARGETS:", ""]
492
- for r in top:
493
- level = "CRIT" if r["risk_score"] > 0.8 else "HIGH" if r["risk_score"] > 0.5 else "low"
494
- lines.append(" [{}] [{}] {}: {:.2f} ({} incidents)".format(
495
- level, r["target_type"], r["target"], r["risk_score"], r["incident_count"]))
496
- db = cognitive._get_db()
497
- total = db.execute("SELECT COUNT(*) FROM somatic_markers WHERE risk_score > 0").fetchone()[0]
498
- high = db.execute("SELECT COUNT(*) FROM somatic_markers WHERE risk_score > 0.5").fetchone()[0]
499
- critical = db.execute("SELECT COUNT(*) FROM somatic_markers WHERE risk_score > 0.8").fetchone()[0]
500
- lines.extend(["", "Distribution: {} tracked | {} high risk | {} critical".format(total, high, critical)])
501
- return "\n".join(lines)
502
- except Exception as e:
503
- return "Error: {}".format(e)
504
-
505
-
506
- def handle_guard_cross_check(findings: list, area: str = "") -> str:
507
- """Cross-check audit findings against known learnings to filter false positives.
508
-
509
- Args:
510
- findings: List of audit finding strings to cross-check
511
- area: System area to narrow the learning search (webapp, shopify, etc.)
512
- """
513
- # Common English/Spanish stopwords to skip during keyword extraction
514
- STOPWORDS = {
515
- "the", "a", "an", "is", "in", "on", "at", "to", "of", "and", "or", "but",
516
- "for", "with", "that", "this", "it", "as", "are", "was", "be", "by", "not",
517
- "has", "have", "from", "which", "when", "if", "then", "do", "does", "can",
518
- "el", "la", "los", "las", "un", "una", "en", "de", "del", "al", "y", "o",
519
- "que", "se", "no", "es", "por", "con", "su", "pero", "como", "para",
520
- "este", "esta", "esto", "son", "hay", "más", "ya",
521
- }
522
-
523
- new_issues = []
524
- known_issues = []
525
-
526
- for finding in findings:
527
- if not finding or not finding.strip():
528
- continue
529
-
530
- # Extract significant keywords from the finding text
531
- words = finding.lower().split()
532
- keywords = [
533
- w.strip(".,;:!?\"'()[]{}") for w in words
534
- if len(w) >= 4 and w.lower() not in STOPWORDS
535
- ]
536
- # Use up to 5 most distinctive keywords to build the search query
537
- query_keywords = keywords[:5]
538
-
539
- matched_learnings = []
540
- if query_keywords:
541
- query = " ".join(query_keywords)
542
- try:
543
- results = search_learnings(query, category=area if area else None)
544
- if not results and area:
545
- # Retry without category filter if area-filtered search returns nothing
546
- results = search_learnings(query)
547
- matched_learnings = results[:3] # Top 3 matches per finding
548
- except Exception:
549
- pass
550
-
551
- if matched_learnings:
552
- refs = [
553
- {"id": r["id"], "title": r["title"], "category": r.get("category", "")}
554
- for r in matched_learnings
555
- ]
556
- known_issues.append({
557
- "finding": finding,
558
- "status": "known",
559
- "learning_refs": refs,
560
- })
561
- else:
562
- new_issues.append({
563
- "finding": finding,
564
- "status": "new",
565
- })
566
-
567
- # Build output
568
- lines = [
569
- f"CROSS-CHECK RESULTS: {len(findings)} findings — "
570
- f"{len(new_issues)} new, {len(known_issues)} already documented",
571
- "",
572
- ]
573
-
574
- if new_issues:
575
- lines.append(f"NEW ISSUES ({len(new_issues)}) — not in learnings, investigate:")
576
- for i, item in enumerate(new_issues, 1):
577
- lines.append(f" {i}. {item['finding']}")
578
- lines.append("")
579
-
580
- if known_issues:
581
- lines.append(f"KNOWN ISSUES ({len(known_issues)}) — covered by existing learnings:")
582
- for i, item in enumerate(known_issues, 1):
583
- refs_str = ", ".join(
584
- f"#{r['id']} [{r['category']}] {r['title'][:60]}"
585
- for r in item["learning_refs"]
586
- )
587
- lines.append(f" {i}. {item['finding']}")
588
- lines.append(f" -> {refs_str}")
589
- lines.append("")
590
-
591
- summary = {
592
- "total": len(findings),
593
- "new_count": len(new_issues),
594
- "known_count": len(known_issues),
595
- "new_issues": [i["finding"] for i in new_issues],
596
- "known_issues": [
597
- {"finding": i["finding"], "refs": i["learning_refs"]}
598
- for i in known_issues
599
- ],
600
- }
601
- lines.append(f"SUMMARY JSON: {json.dumps(summary)}")
602
-
603
- return "\n".join(lines)
604
-
605
-
606
- def handle_guard_file_check(files: list) -> str:
607
- """Pre-edit check: surfaces learnings and recent changes for files about to be modified.
608
-
609
- Args:
610
- files: List of file paths about to be edited
611
- """
612
- from pathlib import Path
613
- import re
614
-
615
- BLOCKING_KEYWORDS = re.compile(
616
- r'\bNUNCA\b|\bNEVER\b|\bPROHIBIDO\b|\bFORBIDDEN\b|\bBLOCKING\b',
617
- re.IGNORECASE
618
- )
619
-
620
- if not files:
621
- return "ERROR: No files provided."
622
-
623
- file_learnings: dict = {}
624
- recent_changes: dict = {}
625
- warnings: list = []
626
- seen_learning_ids: set = set()
627
-
628
- for filepath in files:
629
- p = Path(filepath)
630
- filename = p.name
631
- parent_dir = p.parent.name
632
- stem = p.stem # filename without extension
633
-
634
- # Build search keywords: filename, stem, parent directory (deduplicated)
635
- keywords = [kw for kw in [filename, stem, parent_dir] if kw and kw not in (".", "")]
636
- seen_kw: set = set()
637
- unique_keywords = []
638
- for kw in keywords:
639
- if kw not in seen_kw:
640
- seen_kw.add(kw)
641
- unique_keywords.append(kw)
642
-
643
- file_results = []
644
- file_seen_ids: set = set()
645
-
646
- for keyword in unique_keywords:
647
- try:
648
- rows = search_learnings(keyword)
649
- for r in rows:
650
- lid = r.get("id")
651
- if lid and lid not in seen_learning_ids and lid not in file_seen_ids:
652
- file_seen_ids.add(lid)
653
- seen_learning_ids.add(lid)
654
- entry = {
655
- "id": lid,
656
- "category": r.get("category", ""),
657
- "title": r.get("title", ""),
658
- "content": (r.get("content") or "")[:300],
659
- }
660
- file_results.append(entry)
661
- # Flag blocking learnings
662
- if BLOCKING_KEYWORDS.search(r.get("title", "")) or \
663
- BLOCKING_KEYWORDS.search(r.get("content") or ""):
664
- warnings.append(
665
- f"[BLOCKING] #{lid} ({filepath}): {r.get('title', '')}"
666
- )
667
- except Exception:
668
- pass
669
-
670
- file_learnings[filepath] = file_results
671
-
672
- # Search recent changes (last 7 days) for this file by filename/stem
673
- file_changes = []
674
- for keyword in unique_keywords[:2]: # filename + stem are most specific
675
- try:
676
- changes = search_changes(files=keyword, days=7)
677
- for c in changes:
678
- cid = c.get("id")
679
- if cid and not any(fc.get("id") == cid for fc in file_changes):
680
- file_changes.append({
681
- "id": cid,
682
- "files": c.get("files", ""),
683
- "what_changed": (c.get("what_changed") or "")[:200],
684
- "why": (c.get("why") or "")[:150],
685
- "created_at": (c.get("created_at") or "")[:16],
686
- })
687
- except Exception:
688
- pass
689
-
690
- recent_changes[filepath] = file_changes
691
-
692
- # Build summary line
693
- total_learnings = sum(len(v) for v in file_learnings.values())
694
- total_changes = sum(len(v) for v in recent_changes.values())
695
- summary_parts = []
696
- if total_learnings:
697
- summary_parts.append(f"{total_learnings} learning(s) found")
698
- if total_changes:
699
- summary_parts.append(f"{total_changes} recent change(s) in last 7 days")
700
- if warnings:
701
- summary_parts.append(f"{len(warnings)} BLOCKING warning(s)")
702
- summary = ", ".join(summary_parts) if summary_parts else "No relevant learnings or recent changes found."
703
-
704
- # Format output
705
- lines = []
706
-
707
- if warnings:
708
- lines.append("WARNINGS — resolve before editing:")
709
- for w in warnings:
710
- lines.append(f" {w}")
711
- lines.append("")
712
-
713
- for filepath in files:
714
- learnings = file_learnings.get(filepath, [])
715
- changes = recent_changes.get(filepath, [])
716
- if not learnings and not changes:
717
- continue
718
- lines.append(f"FILE: {filepath}")
719
- if learnings:
720
- lines.append(f" Learnings ({len(learnings)}):")
721
- for entry in learnings[:10]:
722
- lines.append(f" #{entry['id']} [{entry['category']}] {entry['title']}")
723
- if entry["content"]:
724
- lines.append(f" {entry['content'][:120]}")
725
- if changes:
726
- lines.append(f" Recent changes ({len(changes)}, last 7d):")
727
- for c in changes[:5]:
728
- lines.append(f" [{c['created_at']}] {c['what_changed'][:100]}")
729
- if c["why"]:
730
- lines.append(f" Why: {c['why'][:80]}")
731
- lines.append("")
732
-
733
- lines.append(f"SUMMARY: {summary}")
734
-
735
- return "\n".join(lines) if lines else summary
736
-
737
-
738
- TOOLS = [
739
- (handle_guard_check, "nexo_guard_check", "Check learnings relevant to files/area BEFORE editing code. Call this before any code change."),
740
- (handle_guard_stats, "nexo_guard_stats", "Get guard system statistics: repetition rate, trends, top problem areas"),
741
- (handle_guard_log_repetition, "nexo_guard_log_repetition", "Log a learning repetition (new learning matches existing one)"),
742
- (handle_somatic_check, "nexo_somatic_check", "View somatic risk scores for files/areas — pain memory"),
743
- (handle_somatic_stats, "nexo_somatic_stats", "Top 10 riskiest targets + risk distribution"),
744
- (handle_guard_cross_check, "nexo_guard_cross_check", "Cross-check audit findings against known learnings to filter false positives"),
745
- (handle_guard_file_check, "nexo_guard_file_check", "Pre-edit check: surfaces learnings and recent changes for files about to be modified"),
746
- ]