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.
- package/README.md +1 -1
- package/bin/nexo-brain.js +92 -9
- package/bin/postinstall.js +22 -15
- package/package.json +7 -4
- package/src/auto_update.py +194 -5
- package/src/crons/sync.py +6 -2
- package/src/db/_core.py +1 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +1 -0
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +11 -1
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +1 -0
- package/src/hooks/capture-tool-logs.sh +23 -6
- package/src/hooks/session-start.sh +4 -3
- package/src/plugin_loader.py +1 -0
- package/src/plugins/update.py +377 -26
- package/src/scripts/deep-sleep/apply_findings.py +1 -0
- package/src/scripts/deep-sleep/collect.py +1 -0
- package/src/scripts/deep-sleep/extract.py +1 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-catchup.py +29 -4
- package/src/scripts/nexo-daily-self-audit.py +21 -1
- package/src/scripts/nexo-evolution-run.py +21 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -0
- package/src/scripts/nexo-postmortem-consolidator.py +34 -9
- package/src/scripts/nexo-sleep.py +32 -10
- package/src/scripts/nexo-synthesis.py +29 -9
- package/src/scripts/nexo-update.sh +109 -7
- package/src/scripts/nexo-watchdog.sh +122 -58
- package/src/server.py +66 -1
- package/src/tools_coordination.py +1 -0
- package/src/tools_sessions.py +1 -0
- package/scripts/migrate-to-unified 2.sh +0 -813
- package/scripts/migrate-to-unified.sh +0 -813
- package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
- package/scripts/migrate-v1.5-to-v1.6.py +0 -778
- package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
- package/scripts/migrate-v1.7-to-v1.8.py +0 -214
- package/scripts/nexo-preflight.sh +0 -236
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +0 -159
- package/src/auto_update 2.py +0 -634
- package/src/claim_graph 2.py +0 -323
- package/src/cognitive/__init__ 2.py +0 -62
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/cognitive/_core 2.py +0 -567
- package/src/cognitive/_decay 2.py +0 -382
- package/src/cognitive/_ingest 2.py +0 -892
- package/src/cognitive/_memory 2.py +0 -912
- package/src/cognitive/_search 2.py +0 -949
- package/src/cognitive/_trust 2.py +0 -464
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest 2.json +0 -106
- package/src/crons/sync 2.py +0 -217
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +0 -789
- package/src/db/__init__ 2.py +0 -89
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +0 -417
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_entities 2.py +0 -178
- package/src/db/_episodic 2.py +0 -738
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_learnings 2.py +0 -168
- package/src/db/_reminders 2.py +0 -338
- package/src/db/_schema 2.py +0 -364
- package/src/db/_sessions 2.py +0 -300
- package/src/db/_tasks 2.py +0 -91
- package/src/evolution_cycle 2.py +0 -266
- package/src/hnsw_index 2.py +0 -254
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -127
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -148
- package/src/hooks/pre-compact 2.sh +0 -151
- package/src/hooks/session-start 2.sh +0 -268
- package/src/hooks/session-stop 2.sh +0 -140
- package/src/kg_populate 2.py +0 -290
- package/src/knowledge_graph 2.py +0 -257
- package/src/maintenance 2.py +0 -59
- package/src/migrate_embeddings 2.py +0 -122
- package/src/plugin_loader 2.py +0 -202
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +0 -805
- package/src/plugins/agents 2.py +0 -52
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -104
- package/src/plugins/cognitive_memory 2.py +0 -564
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -299
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -533
- package/src/plugins/evolution 2.py +0 -115
- package/src/plugins/guard 2.py +0 -746
- package/src/plugins/knowledge_graph_tools 2.py +0 -105
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/update 2.py +0 -256
- package/src/requirements 2.txt +0 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/check-context 2.py +0 -264
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -242
- package/src/scripts/nexo-cognitive-decay 2.py +0 -182
- package/src/scripts/nexo-daily-self-audit 2.py +0 -552
- package/src/scripts/nexo-deep-sleep 2.sh +0 -97
- package/src/scripts/nexo-evolution-run 2.py +0 -597
- package/src/scripts/nexo-followup-hygiene 2.py +0 -112
- package/src/scripts/nexo-github-monitor 2.py +0 -256
- package/src/scripts/nexo-immune 2.py +0 -927
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -245
- package/src/scripts/nexo-learning-validator 2.py +0 -207
- package/src/scripts/nexo-migrate 2.py +0 -232
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
- package/src/scripts/nexo-reflection 2.py +0 -253
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-send-email 2.py +0 -25
- package/src/scripts/nexo-send-email.py +0 -25
- package/src/scripts/nexo-send-reply 2.py +0 -178
- package/src/scripts/nexo-send-reply.py +0 -178
- package/src/scripts/nexo-sleep 2.py +0 -592
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-synthesis 2.py +0 -253
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -161
- package/src/scripts/nexo-watchdog 2.sh +0 -878
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/server 2.py +0 -733
- package/src/storage_router 2.py +0 -32
- package/src/tools_coordination 2.py +0 -102
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_learnings 2.py +0 -220
- package/src/tools_menu 2.py +0 -227
- package/src/tools_reminders 2.py +0 -86
- package/src/tools_reminders_crud 2.py +0 -159
- package/src/tools_sessions 2.py +0 -476
- package/src/tools_task_history 2.py +0 -57
- package/templates/CLAUDE.md 2.template +0 -63
- package/templates/openclaw 2.json +0 -13
- package/tests/__init__ 2.py +0 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest 2.py +0 -71
- package/tests/conftest.py +0 -71
- package/tests/test_cognitive 2.py +0 -205
- package/tests/test_cognitive.py +0 -205
- package/tests/test_knowledge_graph 2.py +0 -140
- package/tests/test_knowledge_graph.py +0 -140
- package/tests/test_migrations 2.py +0 -137
- package/tests/test_migrations.py +0 -137
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# nexo-inbox-hook.sh — PostToolUse: automatic inter-terminal inbox check (D+)
|
|
3
|
-
#
|
|
4
|
-
# Zero output when no messages = zero tokens consumed in Claude's context.
|
|
5
|
-
# Reads SQLite directly (no MCP overhead). Write-only: INSERT OR IGNORE for mark-as-read.
|
|
6
|
-
# Debounce: skips if last check was <2 seconds ago.
|
|
7
|
-
|
|
8
|
-
INPUT=$(cat)
|
|
9
|
-
|
|
10
|
-
# 1. Skip read-only tools (same logic as capture-tool-logs.sh)
|
|
11
|
-
TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null)
|
|
12
|
-
case "$TOOL_NAME" in
|
|
13
|
-
Read|Grep|Glob|LS|Skill|ToolSearch|Agent) exit 0 ;;
|
|
14
|
-
esac
|
|
15
|
-
|
|
16
|
-
# 2. Extract Claude Code session_id
|
|
17
|
-
CLAUDE_SID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id',''))" 2>/dev/null)
|
|
18
|
-
[ -z "$CLAUDE_SID" ] && exit 0
|
|
19
|
-
|
|
20
|
-
# 3. Debounce: skip if last check <2s ago
|
|
21
|
-
DEBOUNCE_FILE="/tmp/nexo-inbox-ts-${CLAUDE_SID}"
|
|
22
|
-
NOW=$(date +%s)
|
|
23
|
-
LAST=$(cat "$DEBOUNCE_FILE" 2>/dev/null || echo 0)
|
|
24
|
-
DIFF=$((NOW - LAST))
|
|
25
|
-
[ "$DIFF" -lt 2 ] && exit 0
|
|
26
|
-
echo "$NOW" > "$DEBOUNCE_FILE"
|
|
27
|
-
|
|
28
|
-
# 4. Find NEXO SID mapped to this Claude session_id
|
|
29
|
-
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
30
|
-
DB="$NEXO_HOME/data/nexo.db"
|
|
31
|
-
[ -f "$DB" ] || exit 0
|
|
32
|
-
|
|
33
|
-
NEXO_SID=$(sqlite3 "$DB" "SELECT sid FROM sessions WHERE claude_session_id = '${CLAUDE_SID}' AND last_update_epoch > (strftime('%s','now') - 900) ORDER BY last_update_epoch DESC LIMIT 1;" 2>/dev/null)
|
|
34
|
-
[ -z "$NEXO_SID" ] && exit 0
|
|
35
|
-
|
|
36
|
-
# 5. Check inbox — messages addressed to this session or broadcast
|
|
37
|
-
MESSAGES=$(sqlite3 -separator '|' "$DB" "
|
|
38
|
-
SELECT m.id, m.from_sid, m.text FROM messages m
|
|
39
|
-
WHERE (m.to_sid = 'all' OR m.to_sid = '${NEXO_SID}')
|
|
40
|
-
AND m.from_sid != '${NEXO_SID}'
|
|
41
|
-
AND m.id NOT IN (SELECT message_id FROM message_reads WHERE sid = '${NEXO_SID}')
|
|
42
|
-
LIMIT 5;
|
|
43
|
-
" 2>/dev/null)
|
|
44
|
-
|
|
45
|
-
# 6. Check pending questions
|
|
46
|
-
QUESTIONS=$(sqlite3 -separator '|' "$DB" "
|
|
47
|
-
SELECT qid, from_sid, question FROM questions
|
|
48
|
-
WHERE to_sid = '${NEXO_SID}' AND answer IS NULL
|
|
49
|
-
LIMIT 3;
|
|
50
|
-
" 2>/dev/null)
|
|
51
|
-
|
|
52
|
-
# 7. If empty → silent exit (0 tokens consumed)
|
|
53
|
-
[ -z "$MESSAGES" ] && [ -z "$QUESTIONS" ] && exit 0
|
|
54
|
-
|
|
55
|
-
# 8. Format and output (injected into Claude's context)
|
|
56
|
-
echo ""
|
|
57
|
-
echo "📨 INTER-TERMINAL MESSAGE (auto-detected):"
|
|
58
|
-
|
|
59
|
-
if [ -n "$MESSAGES" ]; then
|
|
60
|
-
echo "$MESSAGES" | while IFS='|' read -r mid from text; do
|
|
61
|
-
echo " [$from]: $text"
|
|
62
|
-
# Mark as read (lightweight INSERT, WAL mode, no lock contention)
|
|
63
|
-
sqlite3 "$DB" "INSERT OR IGNORE INTO message_reads (message_id, sid) VALUES ('${mid}', '${NEXO_SID}');" 2>/dev/null
|
|
64
|
-
done
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
if [ -n "$QUESTIONS" ]; then
|
|
68
|
-
echo " ⚠ PREGUNTAS de otra terminal — responder con nexo_answer:"
|
|
69
|
-
echo "$QUESTIONS" | while IFS='|' read -r qid from question; do
|
|
70
|
-
echo " Q[$qid] de [$from]: $question"
|
|
71
|
-
done
|
|
72
|
-
fi
|
|
73
|
-
|
|
74
|
-
exit 0
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""DEPRECATED: Use 'npx nexo-brain' instead. This installer is no longer maintained."""
|
|
3
|
-
import sys
|
|
4
|
-
print("This installer is deprecated. Please use: npx nexo-brain")
|
|
5
|
-
print("See: https://github.com/wazionapps/nexo#installation")
|
|
6
|
-
sys.exit(1)
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""NEXO Learning Housekeeping — Nightly dedup, weight adjustment, and review.
|
|
3
|
-
|
|
4
|
-
Runs daily. Adjusts learning weights based on usage (guard_hits),
|
|
5
|
-
detects duplicates via semantic similarity, and archives stale learnings.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
import os
|
|
10
|
-
import sqlite3
|
|
11
|
-
import sys
|
|
12
|
-
import time
|
|
13
|
-
from datetime import datetime, timedelta
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
17
|
-
# Auto-detect: if running from repo (src/scripts/), use src/ as NEXO_CODE
|
|
18
|
-
_script_dir = Path(__file__).resolve().parent
|
|
19
|
-
_repo_src = _script_dir.parent # src/scripts/ -> src/
|
|
20
|
-
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
|
|
21
|
-
|
|
22
|
-
sys.path.insert(0, str(NEXO_CODE))
|
|
23
|
-
|
|
24
|
-
DB_PATH = NEXO_HOME / "data" / "nexo.db"
|
|
25
|
-
STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
|
|
26
|
-
|
|
27
|
-
# Weight adjustment rates
|
|
28
|
-
GUARD_HIT_BOOST = 0.02 # per guard hit since last run
|
|
29
|
-
DECAY_RATE = 0.005 # daily decay for unused learnings
|
|
30
|
-
MIN_WEIGHT = 0.05
|
|
31
|
-
MAX_WEIGHT = 1.0
|
|
32
|
-
DEDUP_THRESHOLD = 0.85 # cosine similarity for duplicate detection
|
|
33
|
-
ARCHIVE_AFTER_DAYS = 90 # archive if weight < 0.1 and no hits in this many days
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def get_db():
|
|
37
|
-
conn = sqlite3.connect(str(DB_PATH))
|
|
38
|
-
conn.row_factory = sqlite3.Row
|
|
39
|
-
return conn
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def update_catchup_state():
|
|
43
|
-
try:
|
|
44
|
-
state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
|
|
45
|
-
except Exception:
|
|
46
|
-
state = {}
|
|
47
|
-
state["learning-housekeep"] = datetime.now().isoformat()
|
|
48
|
-
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
-
STATE_FILE.write_text(json.dumps(state, indent=2))
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def adjust_weights(conn):
|
|
53
|
-
"""Boost weight for frequently-used learnings, decay unused ones."""
|
|
54
|
-
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
55
|
-
now = time.time()
|
|
56
|
-
one_day_ago = now - 86400
|
|
57
|
-
|
|
58
|
-
learnings = conn.execute(
|
|
59
|
-
"SELECT id, weight, guard_hits, last_guard_hit_at, priority, created_at "
|
|
60
|
-
"FROM learnings WHERE status = 'active'"
|
|
61
|
-
).fetchall()
|
|
62
|
-
|
|
63
|
-
adjusted = 0
|
|
64
|
-
for l in learnings:
|
|
65
|
-
old_weight = l["weight"] or 0.5
|
|
66
|
-
hits = l["guard_hits"] or 0
|
|
67
|
-
last_hit = l["last_guard_hit_at"] or 0
|
|
68
|
-
priority = l["priority"] or "medium"
|
|
69
|
-
|
|
70
|
-
# Priority floor — critical learnings never drop below 0.5
|
|
71
|
-
priority_floor = {"critical": 0.5, "high": 0.3, "medium": 0.1, "low": 0.05}[priority]
|
|
72
|
-
|
|
73
|
-
new_weight = old_weight
|
|
74
|
-
|
|
75
|
-
if last_hit > one_day_ago:
|
|
76
|
-
# Recent guard hit — boost
|
|
77
|
-
recent_hits = 1 # Simplified: at least 1 hit today
|
|
78
|
-
new_weight = min(MAX_WEIGHT, old_weight + (GUARD_HIT_BOOST * recent_hits))
|
|
79
|
-
else:
|
|
80
|
-
# No recent hits — decay
|
|
81
|
-
new_weight = max(priority_floor, old_weight - DECAY_RATE)
|
|
82
|
-
|
|
83
|
-
new_weight = max(MIN_WEIGHT, min(MAX_WEIGHT, new_weight))
|
|
84
|
-
|
|
85
|
-
if abs(new_weight - old_weight) > 0.001:
|
|
86
|
-
conn.execute("UPDATE learnings SET weight = ? WHERE id = ?", (round(new_weight, 4), l["id"]))
|
|
87
|
-
adjusted += 1
|
|
88
|
-
|
|
89
|
-
conn.commit()
|
|
90
|
-
print(f"[{ts}] Weight adjustment: {adjusted}/{len(learnings)} learnings adjusted")
|
|
91
|
-
return adjusted
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def auto_prioritize(conn):
|
|
95
|
-
"""Auto-upgrade priority based on guard hits and repetitions."""
|
|
96
|
-
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
97
|
-
|
|
98
|
-
# Learnings with 10+ guard hits that are still medium → upgrade to high
|
|
99
|
-
upgraded = conn.execute(
|
|
100
|
-
"UPDATE learnings SET priority = 'high', weight = MAX(weight, 0.7) "
|
|
101
|
-
"WHERE status = 'active' AND priority = 'medium' AND guard_hits >= 10"
|
|
102
|
-
).rowcount
|
|
103
|
-
|
|
104
|
-
# Learnings with repetitions (same error happened again) → upgrade to high
|
|
105
|
-
repeated = conn.execute(
|
|
106
|
-
"""UPDATE learnings SET priority = 'high', weight = MAX(weight, 0.7)
|
|
107
|
-
WHERE status = 'active' AND priority IN ('medium', 'low')
|
|
108
|
-
AND id IN (SELECT original_learning_id FROM error_repetitions)"""
|
|
109
|
-
).rowcount
|
|
110
|
-
|
|
111
|
-
conn.commit()
|
|
112
|
-
total = upgraded + repeated
|
|
113
|
-
if total > 0:
|
|
114
|
-
print(f"[{ts}] Auto-prioritize: {upgraded} by guard_hits, {repeated} by repetitions")
|
|
115
|
-
return total
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def detect_duplicates(conn):
|
|
119
|
-
"""Find semantically similar learnings using fastembed."""
|
|
120
|
-
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
121
|
-
try:
|
|
122
|
-
from fastembed import TextEmbedding
|
|
123
|
-
import numpy as np
|
|
124
|
-
except ImportError:
|
|
125
|
-
print(f"[{ts}] Dedup skipped: fastembed not available")
|
|
126
|
-
return []
|
|
127
|
-
|
|
128
|
-
learnings = conn.execute(
|
|
129
|
-
"SELECT id, title, content, weight, guard_hits FROM learnings WHERE status = 'active'"
|
|
130
|
-
).fetchall()
|
|
131
|
-
|
|
132
|
-
if len(learnings) < 2:
|
|
133
|
-
return []
|
|
134
|
-
|
|
135
|
-
model = TextEmbedding("BAAI/bge-base-en-v1.5")
|
|
136
|
-
texts = [f"{l['title']}: {l['content'][:300]}" for l in learnings]
|
|
137
|
-
embeddings = list(model.embed(texts))
|
|
138
|
-
embeddings = np.array(embeddings)
|
|
139
|
-
|
|
140
|
-
# Normalize
|
|
141
|
-
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
|
|
142
|
-
norms[norms == 0] = 1
|
|
143
|
-
embeddings = embeddings / norms
|
|
144
|
-
|
|
145
|
-
duplicates = []
|
|
146
|
-
for i in range(len(learnings)):
|
|
147
|
-
for j in range(i + 1, len(learnings)):
|
|
148
|
-
sim = float(np.dot(embeddings[i], embeddings[j]))
|
|
149
|
-
if sim >= DEDUP_THRESHOLD:
|
|
150
|
-
# Keep the one with higher weight/hits
|
|
151
|
-
a, b = learnings[i], learnings[j]
|
|
152
|
-
score_a = (a["weight"] or 0.5) + (a["guard_hits"] or 0) * 0.01
|
|
153
|
-
score_b = (b["weight"] or 0.5) + (b["guard_hits"] or 0) * 0.01
|
|
154
|
-
keep, drop = (a, b) if score_a >= score_b else (b, a)
|
|
155
|
-
duplicates.append({
|
|
156
|
-
"keep_id": keep["id"], "keep_title": keep["title"],
|
|
157
|
-
"drop_id": drop["id"], "drop_title": drop["title"],
|
|
158
|
-
"similarity": round(sim, 3)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
if duplicates:
|
|
162
|
-
print(f"[{ts}] Duplicates found: {len(duplicates)} pairs (>= {DEDUP_THRESHOLD})")
|
|
163
|
-
for d in duplicates[:10]:
|
|
164
|
-
print(f"[{ts}] [{d['similarity']}] keep #{d['keep_id']} '{d['keep_title'][:40]}', archive #{d['drop_id']} '{d['drop_title'][:40]}'")
|
|
165
|
-
# Archive the duplicate (don't delete — just mark inactive)
|
|
166
|
-
conn.execute("UPDATE learnings SET status = 'archived' WHERE id = ?", (d["drop_id"],))
|
|
167
|
-
conn.commit()
|
|
168
|
-
else:
|
|
169
|
-
print(f"[{ts}] No duplicates found ({len(learnings)} learnings scanned)")
|
|
170
|
-
|
|
171
|
-
return duplicates
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def archive_stale(conn):
|
|
175
|
-
"""Archive learnings with very low weight and no recent guard hits."""
|
|
176
|
-
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
177
|
-
cutoff = time.time() - (ARCHIVE_AFTER_DAYS * 86400)
|
|
178
|
-
|
|
179
|
-
stale = conn.execute(
|
|
180
|
-
"SELECT id, title, weight, last_guard_hit_at FROM learnings "
|
|
181
|
-
"WHERE status = 'active' AND weight < 0.1 AND priority NOT IN ('critical', 'high') "
|
|
182
|
-
"AND (last_guard_hit_at IS NULL OR last_guard_hit_at < ?)",
|
|
183
|
-
(cutoff,)
|
|
184
|
-
).fetchall()
|
|
185
|
-
|
|
186
|
-
if stale:
|
|
187
|
-
for s in stale:
|
|
188
|
-
conn.execute("UPDATE learnings SET status = 'archived' WHERE id = ?", (s["id"],))
|
|
189
|
-
print(f"[{ts}] Archived #{s['id']} '{s['title'][:50]}' (weight={s['weight']:.2f})")
|
|
190
|
-
conn.commit()
|
|
191
|
-
print(f"[{ts}] Archived {len(stale)} stale learnings")
|
|
192
|
-
else:
|
|
193
|
-
print(f"[{ts}] No stale learnings to archive")
|
|
194
|
-
|
|
195
|
-
return len(stale)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def print_summary(conn):
|
|
199
|
-
"""Print summary stats."""
|
|
200
|
-
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
201
|
-
stats = conn.execute(
|
|
202
|
-
"""SELECT
|
|
203
|
-
COUNT(*) as total,
|
|
204
|
-
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
|
|
205
|
-
SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END) as archived,
|
|
206
|
-
SUM(CASE WHEN priority = 'critical' THEN 1 ELSE 0 END) as critical,
|
|
207
|
-
SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) as high,
|
|
208
|
-
SUM(CASE WHEN priority = 'medium' THEN 1 ELSE 0 END) as medium,
|
|
209
|
-
SUM(CASE WHEN priority = 'low' THEN 1 ELSE 0 END) as low,
|
|
210
|
-
printf('%.2f', AVG(CASE WHEN status = 'active' THEN weight END)) as avg_weight
|
|
211
|
-
FROM learnings"""
|
|
212
|
-
).fetchone()
|
|
213
|
-
print(f"[{ts}] Summary: {stats['active']} active, {stats['archived']} archived | "
|
|
214
|
-
f"Priority: {stats['critical']}C {stats['high']}H {stats['medium']}M {stats['low']}L | "
|
|
215
|
-
f"Avg weight: {stats['avg_weight']}")
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def main():
|
|
219
|
-
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
220
|
-
print(f"[{ts}] Learning housekeeping starting...")
|
|
221
|
-
|
|
222
|
-
conn = get_db()
|
|
223
|
-
|
|
224
|
-
# 1. Adjust weights based on usage
|
|
225
|
-
adjust_weights(conn)
|
|
226
|
-
|
|
227
|
-
# 2. Auto-prioritize based on guard hits and repetitions
|
|
228
|
-
auto_prioritize(conn)
|
|
229
|
-
|
|
230
|
-
# 3. Detect and archive duplicates
|
|
231
|
-
detect_duplicates(conn)
|
|
232
|
-
|
|
233
|
-
# 4. Archive stale learnings
|
|
234
|
-
archive_stale(conn)
|
|
235
|
-
|
|
236
|
-
# 5. Summary
|
|
237
|
-
print_summary(conn)
|
|
238
|
-
|
|
239
|
-
conn.close()
|
|
240
|
-
update_catchup_state()
|
|
241
|
-
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Done.")
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if __name__ == "__main__":
|
|
245
|
-
main()
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
NEXO Learning Validator — Cross-checks findings against existing learnings.
|
|
4
|
-
|
|
5
|
-
Wrapper collects the finding + all learnings from SQLite, then passes
|
|
6
|
-
to Claude CLI (opus) to make an intelligent determination of whether
|
|
7
|
-
the finding is known, related, or genuinely new.
|
|
8
|
-
|
|
9
|
-
Usage as CLI:
|
|
10
|
-
python3 nexo-learning-validator.py "finding text to validate"
|
|
11
|
-
python3 nexo-learning-validator.py --category project "finding text"
|
|
12
|
-
|
|
13
|
-
Usage as library:
|
|
14
|
-
from nexo_learning_validator import validate_finding
|
|
15
|
-
result = validate_finding("CRITICAL: message_id column is NULL")
|
|
16
|
-
if result["known"]:
|
|
17
|
-
print(f"Already known: {result['matching_learnings']}")
|
|
18
|
-
|
|
19
|
-
Exit codes:
|
|
20
|
-
0 = Finding is NEW (not known)
|
|
21
|
-
1 = Finding is KNOWN (matches existing learning)
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
import json
|
|
25
|
-
import os
|
|
26
|
-
import sqlite3
|
|
27
|
-
import subprocess
|
|
28
|
-
import sys
|
|
29
|
-
from pathlib import Path
|
|
30
|
-
|
|
31
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
32
|
-
|
|
33
|
-
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
34
|
-
CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def get_all_learnings(category: str = None) -> list[dict]:
|
|
38
|
-
"""Fetch all learnings from nexo.db."""
|
|
39
|
-
conn = sqlite3.connect(str(NEXO_DB), timeout=10)
|
|
40
|
-
conn.row_factory = sqlite3.Row
|
|
41
|
-
if category:
|
|
42
|
-
rows = conn.execute(
|
|
43
|
-
"SELECT id, category, title, content FROM learnings WHERE category = ?",
|
|
44
|
-
(category,)
|
|
45
|
-
).fetchall()
|
|
46
|
-
else:
|
|
47
|
-
rows = conn.execute(
|
|
48
|
-
"SELECT id, category, title, content FROM learnings"
|
|
49
|
-
).fetchall()
|
|
50
|
-
conn.close()
|
|
51
|
-
return [dict(r) for r in rows]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def validate_finding(finding: str, category: str = None) -> dict:
|
|
55
|
-
"""
|
|
56
|
-
Validate a finding against existing learnings using Claude CLI.
|
|
57
|
-
|
|
58
|
-
Returns:
|
|
59
|
-
{
|
|
60
|
-
"known": bool,
|
|
61
|
-
"confidence": float (0-1),
|
|
62
|
-
"matching_learnings": [{"id": int, "title": str, "similarity": float}],
|
|
63
|
-
"recommendation": str
|
|
64
|
-
}
|
|
65
|
-
"""
|
|
66
|
-
learnings = get_all_learnings(category)
|
|
67
|
-
|
|
68
|
-
if not learnings:
|
|
69
|
-
return {
|
|
70
|
-
"known": False,
|
|
71
|
-
"confidence": 0,
|
|
72
|
-
"matching_learnings": [],
|
|
73
|
-
"recommendation": "No learnings in DB — finding is new by default"
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
# Build compact learnings reference for CLI
|
|
77
|
-
learnings_ref = []
|
|
78
|
-
for l in learnings:
|
|
79
|
-
learnings_ref.append({
|
|
80
|
-
"id": l["id"],
|
|
81
|
-
"cat": l["category"],
|
|
82
|
-
"title": l["title"],
|
|
83
|
-
"content": (l["content"] or "")[:300],
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
prompt = f"""You are a finding deduplication engine. Compare a new finding against existing learnings and determine if it's already known.
|
|
87
|
-
|
|
88
|
-
NEW FINDING:
|
|
89
|
-
{finding}
|
|
90
|
-
|
|
91
|
-
EXISTING LEARNINGS ({len(learnings_ref)} total):
|
|
92
|
-
{json.dumps(learnings_ref, indent=1)}
|
|
93
|
-
|
|
94
|
-
Respond with ONLY valid JSON (no markdown, no code fences):
|
|
95
|
-
{{
|
|
96
|
-
"known": true/false,
|
|
97
|
-
"confidence": 0.0-1.0,
|
|
98
|
-
"matching_learnings": [
|
|
99
|
-
{{"id": <learning_id>, "title": "<title>", "similarity": 0.0-1.0}}
|
|
100
|
-
],
|
|
101
|
-
"recommendation": "<one line: KNOWN/LIKELY KNOWN/POSSIBLY RELATED/NEW>"
|
|
102
|
-
}}
|
|
103
|
-
|
|
104
|
-
Rules:
|
|
105
|
-
- confidence >= 0.7 and same root cause = known: true
|
|
106
|
-
- confidence 0.55-0.7 and related topic = known: true, say LIKELY KNOWN
|
|
107
|
-
- confidence < 0.55 = known: false
|
|
108
|
-
- Max 5 matching_learnings, sorted by similarity descending
|
|
109
|
-
- If the finding describes the SAME bug/issue/pattern as a learning, it's known even if worded differently
|
|
110
|
-
- Be strict: different symptoms of different bugs are NOT the same even if they mention the same file"""
|
|
111
|
-
|
|
112
|
-
# Try CLI first, fall back to mechanical similarity
|
|
113
|
-
if CLAUDE_CLI.exists():
|
|
114
|
-
# Fallback: mechanical SequenceMatcher (original logic)
|
|
115
|
-
return _mechanical_validate(finding, learnings)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def _mechanical_validate(finding: str, learnings: list[dict]) -> dict:
|
|
119
|
-
"""Fallback validation using SequenceMatcher when CLI is unavailable."""
|
|
120
|
-
from difflib import SequenceMatcher
|
|
121
|
-
|
|
122
|
-
threshold = 0.45
|
|
123
|
-
finding_kw = _extract_keywords(finding)
|
|
124
|
-
matches = []
|
|
125
|
-
|
|
126
|
-
for learning in learnings:
|
|
127
|
-
title_sim = SequenceMatcher(None, finding.lower(), learning["title"].lower()).ratio()
|
|
128
|
-
content_sim = SequenceMatcher(None, finding.lower(), (learning["content"] or "").lower()).ratio()
|
|
129
|
-
|
|
130
|
-
learning_text = f"{learning['title']} {learning['content'] or ''}"
|
|
131
|
-
learning_kw = _extract_keywords(learning_text)
|
|
132
|
-
kw_overlap = len(finding_kw & learning_kw) / len(finding_kw) if finding_kw and learning_kw else 0
|
|
133
|
-
|
|
134
|
-
combined = max(title_sim, content_sim) * 0.6 + kw_overlap * 0.4
|
|
135
|
-
|
|
136
|
-
if combined >= threshold:
|
|
137
|
-
matches.append({
|
|
138
|
-
"id": learning["id"],
|
|
139
|
-
"category": learning["category"],
|
|
140
|
-
"title": learning["title"],
|
|
141
|
-
"similarity": round(combined, 3),
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
matches.sort(key=lambda x: x["similarity"], reverse=True)
|
|
145
|
-
top = matches[:5]
|
|
146
|
-
|
|
147
|
-
if not top:
|
|
148
|
-
return {"known": False, "confidence": 0, "matching_learnings": [], "recommendation": "NEW finding"}
|
|
149
|
-
|
|
150
|
-
best = top[0]["similarity"]
|
|
151
|
-
if best >= 0.7:
|
|
152
|
-
return {"known": True, "confidence": best, "matching_learnings": top,
|
|
153
|
-
"recommendation": f"KNOWN issue (learning #{top[0]['id']})"}
|
|
154
|
-
elif best >= 0.55:
|
|
155
|
-
return {"known": True, "confidence": best, "matching_learnings": top,
|
|
156
|
-
"recommendation": f"LIKELY KNOWN (learning #{top[0]['id']})"}
|
|
157
|
-
else:
|
|
158
|
-
return {"known": False, "confidence": best, "matching_learnings": top,
|
|
159
|
-
"recommendation": "POSSIBLY RELATED but different enough to report"}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def _extract_keywords(text: str) -> set:
|
|
163
|
-
"""Extract meaningful keywords from text."""
|
|
164
|
-
stop_words = {
|
|
165
|
-
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
166
|
-
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
167
|
-
'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare',
|
|
168
|
-
'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as',
|
|
169
|
-
'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'both', 'either',
|
|
170
|
-
'error', 'critical', 'warning', 'bug', 'issue', 'problem', 'fix',
|
|
171
|
-
'el', 'la', 'los', 'las', 'un', 'una', 'de', 'en', 'que', 'por',
|
|
172
|
-
}
|
|
173
|
-
words = set()
|
|
174
|
-
for word in text.lower().split():
|
|
175
|
-
clean = ''.join(c for c in word if c.isalnum() or c == '_')
|
|
176
|
-
if clean and len(clean) > 2 and clean not in stop_words:
|
|
177
|
-
words.add(clean)
|
|
178
|
-
return words
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def main():
|
|
182
|
-
import argparse
|
|
183
|
-
parser = argparse.ArgumentParser(description="Validate findings against existing NEXO learnings")
|
|
184
|
-
parser.add_argument("finding", help="The finding text to validate")
|
|
185
|
-
parser.add_argument("--category", "-c", help="Filter learnings by category")
|
|
186
|
-
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
|
187
|
-
args = parser.parse_args()
|
|
188
|
-
|
|
189
|
-
result = validate_finding(args.finding, args.category)
|
|
190
|
-
|
|
191
|
-
if args.json:
|
|
192
|
-
print(json.dumps(result, indent=2))
|
|
193
|
-
else:
|
|
194
|
-
status = "KNOWN" if result["known"] else "NEW"
|
|
195
|
-
print(f"Status: {status} (confidence: {result['confidence']:.0%})")
|
|
196
|
-
print(f"Recommendation: {result['recommendation']}")
|
|
197
|
-
if result["matching_learnings"]:
|
|
198
|
-
print(f"Related learnings:")
|
|
199
|
-
for m in result["matching_learnings"]:
|
|
200
|
-
cat = m.get('category', '?')
|
|
201
|
-
print(f" #{m['id']} [{cat}] {m['title']} ({m['similarity']:.0%})")
|
|
202
|
-
|
|
203
|
-
sys.exit(1 if result["known"] else 0)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if __name__ == "__main__":
|
|
207
|
-
main()
|