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