superlocalmemory 2.8.5 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +9 -1
  3. package/NOTICE +63 -0
  4. package/README.md +165 -480
  5. package/bin/slm +17 -449
  6. package/bin/slm-npm +2 -2
  7. package/bin/slm.bat +4 -2
  8. package/conftest.py +5 -0
  9. package/docs/api-reference.md +284 -0
  10. package/docs/architecture.md +149 -0
  11. package/docs/auto-memory.md +150 -0
  12. package/docs/cli-reference.md +276 -0
  13. package/docs/compliance.md +191 -0
  14. package/docs/configuration.md +182 -0
  15. package/docs/getting-started.md +102 -0
  16. package/docs/ide-setup.md +261 -0
  17. package/docs/mcp-tools.md +220 -0
  18. package/docs/migration-from-v2.md +170 -0
  19. package/docs/profiles.md +173 -0
  20. package/docs/troubleshooting.md +310 -0
  21. package/{configs → ide/configs}/antigravity-mcp.json +3 -3
  22. package/ide/configs/chatgpt-desktop-mcp.json +16 -0
  23. package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
  24. package/{configs → ide/configs}/codex-mcp.toml +4 -4
  25. package/{configs → ide/configs}/continue-mcp.yaml +4 -3
  26. package/{configs → ide/configs}/continue-skills.yaml +6 -6
  27. package/ide/configs/cursor-mcp.json +15 -0
  28. package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
  29. package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
  30. package/{configs → ide/configs}/opencode-mcp.json +2 -2
  31. package/{configs → ide/configs}/perplexity-mcp.json +2 -2
  32. package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
  33. package/{configs → ide/configs}/windsurf-mcp.json +3 -3
  34. package/{configs → ide/configs}/zed-mcp.json +2 -2
  35. package/{hooks → ide/hooks}/context-hook.js +9 -20
  36. package/ide/hooks/memory-list-skill.js +70 -0
  37. package/ide/hooks/memory-profile-skill.js +101 -0
  38. package/ide/hooks/memory-recall-skill.js +62 -0
  39. package/ide/hooks/memory-remember-skill.js +68 -0
  40. package/ide/hooks/memory-reset-skill.js +160 -0
  41. package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
  42. package/ide/integrations/langchain/README.md +106 -0
  43. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
  44. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
  45. package/ide/integrations/langchain/pyproject.toml +38 -0
  46. package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
  47. package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
  48. package/ide/integrations/langchain/tests/test_security.py +117 -0
  49. package/ide/integrations/llamaindex/README.md +81 -0
  50. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
  51. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
  52. package/ide/integrations/llamaindex/pyproject.toml +43 -0
  53. package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
  54. package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
  55. package/ide/integrations/llamaindex/tests/test_security.py +241 -0
  56. package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
  57. package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
  58. package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
  59. package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
  60. package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
  61. package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
  62. package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
  63. package/package.json +13 -22
  64. package/pyproject.toml +85 -0
  65. package/scripts/build-dmg.sh +417 -0
  66. package/scripts/install-skills.ps1 +334 -0
  67. package/{install.ps1 → scripts/install.ps1} +36 -4
  68. package/{install.sh → scripts/install.sh} +14 -13
  69. package/scripts/postinstall.js +2 -2
  70. package/scripts/start-dashboard.ps1 +52 -0
  71. package/scripts/start-dashboard.sh +41 -0
  72. package/scripts/sync-wiki.ps1 +127 -0
  73. package/scripts/sync-wiki.sh +82 -0
  74. package/scripts/test-dmg.sh +161 -0
  75. package/scripts/test-npm-package.ps1 +252 -0
  76. package/scripts/test-npm-package.sh +207 -0
  77. package/scripts/verify-install.ps1 +294 -0
  78. package/scripts/verify-install.sh +266 -0
  79. package/src/superlocalmemory/__init__.py +0 -0
  80. package/src/superlocalmemory/attribution/__init__.py +9 -0
  81. package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
  82. package/src/superlocalmemory/attribution/signer.py +153 -0
  83. package/src/superlocalmemory/attribution/watermark.py +189 -0
  84. package/src/superlocalmemory/cli/__init__.py +5 -0
  85. package/src/superlocalmemory/cli/commands.py +245 -0
  86. package/src/superlocalmemory/cli/main.py +89 -0
  87. package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
  88. package/src/superlocalmemory/cli/post_install.py +99 -0
  89. package/src/superlocalmemory/cli/setup_wizard.py +129 -0
  90. package/src/superlocalmemory/compliance/__init__.py +0 -0
  91. package/src/superlocalmemory/compliance/abac.py +204 -0
  92. package/src/superlocalmemory/compliance/audit.py +314 -0
  93. package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
  94. package/src/superlocalmemory/compliance/gdpr.py +294 -0
  95. package/src/superlocalmemory/compliance/lifecycle.py +158 -0
  96. package/src/superlocalmemory/compliance/retention.py +232 -0
  97. package/src/superlocalmemory/compliance/scheduler.py +148 -0
  98. package/src/superlocalmemory/core/__init__.py +0 -0
  99. package/src/superlocalmemory/core/config.py +391 -0
  100. package/src/superlocalmemory/core/embeddings.py +293 -0
  101. package/src/superlocalmemory/core/engine.py +701 -0
  102. package/src/superlocalmemory/core/hooks.py +65 -0
  103. package/src/superlocalmemory/core/maintenance.py +172 -0
  104. package/src/superlocalmemory/core/modes.py +140 -0
  105. package/src/superlocalmemory/core/profiles.py +234 -0
  106. package/src/superlocalmemory/core/registry.py +117 -0
  107. package/src/superlocalmemory/dynamics/__init__.py +0 -0
  108. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
  109. package/src/superlocalmemory/encoding/__init__.py +0 -0
  110. package/src/superlocalmemory/encoding/consolidator.py +485 -0
  111. package/src/superlocalmemory/encoding/emotional.py +125 -0
  112. package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
  113. package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
  114. package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
  115. package/src/superlocalmemory/encoding/foresight.py +91 -0
  116. package/src/superlocalmemory/encoding/graph_builder.py +302 -0
  117. package/src/superlocalmemory/encoding/observation_builder.py +160 -0
  118. package/src/superlocalmemory/encoding/scene_builder.py +183 -0
  119. package/src/superlocalmemory/encoding/signal_inference.py +90 -0
  120. package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
  121. package/src/superlocalmemory/encoding/type_router.py +235 -0
  122. package/src/superlocalmemory/hooks/__init__.py +3 -0
  123. package/src/superlocalmemory/hooks/auto_capture.py +111 -0
  124. package/src/superlocalmemory/hooks/auto_recall.py +93 -0
  125. package/src/superlocalmemory/hooks/ide_connector.py +204 -0
  126. package/src/superlocalmemory/hooks/rules_engine.py +99 -0
  127. package/src/superlocalmemory/infra/__init__.py +3 -0
  128. package/src/superlocalmemory/infra/auth_middleware.py +82 -0
  129. package/src/superlocalmemory/infra/backup.py +317 -0
  130. package/src/superlocalmemory/infra/cache_manager.py +267 -0
  131. package/src/superlocalmemory/infra/event_bus.py +381 -0
  132. package/src/superlocalmemory/infra/rate_limiter.py +135 -0
  133. package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
  134. package/src/superlocalmemory/learning/__init__.py +0 -0
  135. package/src/superlocalmemory/learning/adaptive.py +172 -0
  136. package/src/superlocalmemory/learning/behavioral.py +490 -0
  137. package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
  138. package/src/superlocalmemory/learning/bootstrap.py +298 -0
  139. package/src/superlocalmemory/learning/cross_project.py +399 -0
  140. package/src/superlocalmemory/learning/database.py +376 -0
  141. package/src/superlocalmemory/learning/engagement.py +323 -0
  142. package/src/superlocalmemory/learning/features.py +138 -0
  143. package/src/superlocalmemory/learning/feedback.py +316 -0
  144. package/src/superlocalmemory/learning/outcomes.py +255 -0
  145. package/src/superlocalmemory/learning/project_context.py +366 -0
  146. package/src/superlocalmemory/learning/ranker.py +155 -0
  147. package/src/superlocalmemory/learning/source_quality.py +303 -0
  148. package/src/superlocalmemory/learning/workflows.py +309 -0
  149. package/src/superlocalmemory/llm/__init__.py +0 -0
  150. package/src/superlocalmemory/llm/backbone.py +316 -0
  151. package/src/superlocalmemory/math/__init__.py +0 -0
  152. package/src/superlocalmemory/math/fisher.py +356 -0
  153. package/src/superlocalmemory/math/langevin.py +398 -0
  154. package/src/superlocalmemory/math/sheaf.py +257 -0
  155. package/src/superlocalmemory/mcp/__init__.py +0 -0
  156. package/src/superlocalmemory/mcp/resources.py +245 -0
  157. package/src/superlocalmemory/mcp/server.py +61 -0
  158. package/src/superlocalmemory/mcp/tools.py +18 -0
  159. package/src/superlocalmemory/mcp/tools_core.py +305 -0
  160. package/src/superlocalmemory/mcp/tools_v28.py +223 -0
  161. package/src/superlocalmemory/mcp/tools_v3.py +286 -0
  162. package/src/superlocalmemory/retrieval/__init__.py +0 -0
  163. package/src/superlocalmemory/retrieval/agentic.py +295 -0
  164. package/src/superlocalmemory/retrieval/ann_index.py +223 -0
  165. package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
  166. package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
  167. package/src/superlocalmemory/retrieval/engine.py +390 -0
  168. package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
  169. package/src/superlocalmemory/retrieval/fusion.py +78 -0
  170. package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
  171. package/src/superlocalmemory/retrieval/reranker.py +154 -0
  172. package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
  173. package/src/superlocalmemory/retrieval/strategy.py +96 -0
  174. package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
  175. package/src/superlocalmemory/server/__init__.py +1 -0
  176. package/src/superlocalmemory/server/api.py +248 -0
  177. package/src/superlocalmemory/server/routes/__init__.py +4 -0
  178. package/src/superlocalmemory/server/routes/agents.py +107 -0
  179. package/src/superlocalmemory/server/routes/backup.py +91 -0
  180. package/src/superlocalmemory/server/routes/behavioral.py +127 -0
  181. package/src/superlocalmemory/server/routes/compliance.py +160 -0
  182. package/src/superlocalmemory/server/routes/data_io.py +188 -0
  183. package/src/superlocalmemory/server/routes/events.py +183 -0
  184. package/src/superlocalmemory/server/routes/helpers.py +85 -0
  185. package/src/superlocalmemory/server/routes/learning.py +273 -0
  186. package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
  187. package/src/superlocalmemory/server/routes/memories.py +399 -0
  188. package/src/superlocalmemory/server/routes/profiles.py +219 -0
  189. package/src/superlocalmemory/server/routes/stats.py +346 -0
  190. package/src/superlocalmemory/server/routes/v3_api.py +365 -0
  191. package/src/superlocalmemory/server/routes/ws.py +82 -0
  192. package/src/superlocalmemory/server/security_middleware.py +57 -0
  193. package/src/superlocalmemory/server/ui.py +245 -0
  194. package/src/superlocalmemory/storage/__init__.py +0 -0
  195. package/src/superlocalmemory/storage/access_control.py +182 -0
  196. package/src/superlocalmemory/storage/database.py +594 -0
  197. package/src/superlocalmemory/storage/migrations.py +303 -0
  198. package/src/superlocalmemory/storage/models.py +406 -0
  199. package/src/superlocalmemory/storage/schema.py +726 -0
  200. package/src/superlocalmemory/storage/v2_migrator.py +317 -0
  201. package/src/superlocalmemory/trust/__init__.py +0 -0
  202. package/src/superlocalmemory/trust/gate.py +130 -0
  203. package/src/superlocalmemory/trust/provenance.py +124 -0
  204. package/src/superlocalmemory/trust/scorer.py +347 -0
  205. package/src/superlocalmemory/trust/signals.py +153 -0
  206. package/ui/index.html +278 -5
  207. package/ui/js/auto-settings.js +70 -0
  208. package/ui/js/dashboard.js +90 -0
  209. package/ui/js/fact-detail.js +92 -0
  210. package/ui/js/feedback.js +2 -2
  211. package/ui/js/ide-status.js +102 -0
  212. package/ui/js/math-health.js +98 -0
  213. package/ui/js/recall-lab.js +127 -0
  214. package/ui/js/settings.js +2 -2
  215. package/ui/js/trust-dashboard.js +73 -0
  216. package/api_server.py +0 -724
  217. package/bin/aider-smart +0 -72
  218. package/bin/superlocalmemoryv2-learning +0 -4
  219. package/bin/superlocalmemoryv2-list +0 -3
  220. package/bin/superlocalmemoryv2-patterns +0 -4
  221. package/bin/superlocalmemoryv2-profile +0 -3
  222. package/bin/superlocalmemoryv2-recall +0 -3
  223. package/bin/superlocalmemoryv2-remember +0 -3
  224. package/bin/superlocalmemoryv2-reset +0 -3
  225. package/bin/superlocalmemoryv2-status +0 -3
  226. package/configs/chatgpt-desktop-mcp.json +0 -16
  227. package/configs/cursor-mcp.json +0 -15
  228. package/docs/SECURITY-QUICK-REFERENCE.md +0 -214
  229. package/hooks/memory-list-skill.js +0 -139
  230. package/hooks/memory-profile-skill.js +0 -273
  231. package/hooks/memory-recall-skill.js +0 -114
  232. package/hooks/memory-remember-skill.js +0 -127
  233. package/hooks/memory-reset-skill.js +0 -274
  234. package/mcp_server.py +0 -1800
  235. package/requirements-core.txt +0 -22
  236. package/requirements-learning.txt +0 -12
  237. package/requirements.txt +0 -12
  238. package/src/agent_registry.py +0 -411
  239. package/src/auth_middleware.py +0 -61
  240. package/src/auto_backup.py +0 -459
  241. package/src/behavioral/__init__.py +0 -49
  242. package/src/behavioral/behavioral_listener.py +0 -203
  243. package/src/behavioral/behavioral_patterns.py +0 -275
  244. package/src/behavioral/cross_project_transfer.py +0 -206
  245. package/src/behavioral/outcome_inference.py +0 -194
  246. package/src/behavioral/outcome_tracker.py +0 -193
  247. package/src/behavioral/tests/__init__.py +0 -4
  248. package/src/behavioral/tests/test_behavioral_integration.py +0 -108
  249. package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
  250. package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
  251. package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
  252. package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
  253. package/src/behavioral/tests/test_outcome_inference.py +0 -107
  254. package/src/behavioral/tests/test_outcome_tracker.py +0 -96
  255. package/src/cache_manager.py +0 -518
  256. package/src/compliance/__init__.py +0 -48
  257. package/src/compliance/abac_engine.py +0 -149
  258. package/src/compliance/abac_middleware.py +0 -116
  259. package/src/compliance/audit_db.py +0 -215
  260. package/src/compliance/audit_logger.py +0 -148
  261. package/src/compliance/retention_manager.py +0 -289
  262. package/src/compliance/retention_scheduler.py +0 -186
  263. package/src/compliance/tests/__init__.py +0 -4
  264. package/src/compliance/tests/test_abac_enforcement.py +0 -95
  265. package/src/compliance/tests/test_abac_engine.py +0 -124
  266. package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
  267. package/src/compliance/tests/test_audit_db.py +0 -123
  268. package/src/compliance/tests/test_audit_logger.py +0 -98
  269. package/src/compliance/tests/test_mcp_audit.py +0 -128
  270. package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
  271. package/src/compliance/tests/test_retention_manager.py +0 -131
  272. package/src/compliance/tests/test_retention_scheduler.py +0 -99
  273. package/src/compression/__init__.py +0 -25
  274. package/src/compression/cli.py +0 -150
  275. package/src/compression/cold_storage.py +0 -217
  276. package/src/compression/config.py +0 -72
  277. package/src/compression/orchestrator.py +0 -133
  278. package/src/compression/tier2_compressor.py +0 -228
  279. package/src/compression/tier3_compressor.py +0 -153
  280. package/src/compression/tier_classifier.py +0 -148
  281. package/src/db_connection_manager.py +0 -536
  282. package/src/embedding_engine.py +0 -63
  283. package/src/embeddings/__init__.py +0 -47
  284. package/src/embeddings/cache.py +0 -70
  285. package/src/embeddings/cli.py +0 -113
  286. package/src/embeddings/constants.py +0 -47
  287. package/src/embeddings/database.py +0 -91
  288. package/src/embeddings/engine.py +0 -247
  289. package/src/embeddings/model_loader.py +0 -145
  290. package/src/event_bus.py +0 -562
  291. package/src/graph/__init__.py +0 -36
  292. package/src/graph/build_helpers.py +0 -74
  293. package/src/graph/cli.py +0 -87
  294. package/src/graph/cluster_builder.py +0 -188
  295. package/src/graph/cluster_summary.py +0 -148
  296. package/src/graph/constants.py +0 -47
  297. package/src/graph/edge_builder.py +0 -162
  298. package/src/graph/entity_extractor.py +0 -95
  299. package/src/graph/graph_core.py +0 -226
  300. package/src/graph/graph_search.py +0 -231
  301. package/src/graph/hierarchical.py +0 -207
  302. package/src/graph/schema.py +0 -99
  303. package/src/graph_engine.py +0 -52
  304. package/src/hnsw_index.py +0 -628
  305. package/src/hybrid_search.py +0 -46
  306. package/src/learning/__init__.py +0 -217
  307. package/src/learning/adaptive_ranker.py +0 -682
  308. package/src/learning/bootstrap/__init__.py +0 -69
  309. package/src/learning/bootstrap/constants.py +0 -93
  310. package/src/learning/bootstrap/db_queries.py +0 -316
  311. package/src/learning/bootstrap/sampling.py +0 -82
  312. package/src/learning/bootstrap/text_utils.py +0 -71
  313. package/src/learning/cross_project_aggregator.py +0 -857
  314. package/src/learning/db/__init__.py +0 -40
  315. package/src/learning/db/constants.py +0 -44
  316. package/src/learning/db/schema.py +0 -279
  317. package/src/learning/engagement_tracker.py +0 -628
  318. package/src/learning/feature_extractor.py +0 -708
  319. package/src/learning/feedback_collector.py +0 -806
  320. package/src/learning/learning_db.py +0 -915
  321. package/src/learning/project_context_manager.py +0 -572
  322. package/src/learning/ranking/__init__.py +0 -33
  323. package/src/learning/ranking/constants.py +0 -84
  324. package/src/learning/ranking/helpers.py +0 -278
  325. package/src/learning/source_quality_scorer.py +0 -676
  326. package/src/learning/synthetic_bootstrap.py +0 -755
  327. package/src/learning/tests/test_adaptive_ranker.py +0 -325
  328. package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
  329. package/src/learning/tests/test_aggregator.py +0 -306
  330. package/src/learning/tests/test_auto_retrain_v28.py +0 -35
  331. package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
  332. package/src/learning/tests/test_feature_extractor_v28.py +0 -93
  333. package/src/learning/tests/test_feedback_collector.py +0 -294
  334. package/src/learning/tests/test_learning_db.py +0 -602
  335. package/src/learning/tests/test_learning_db_v28.py +0 -110
  336. package/src/learning/tests/test_learning_init_v28.py +0 -48
  337. package/src/learning/tests/test_outcome_signals.py +0 -48
  338. package/src/learning/tests/test_project_context.py +0 -292
  339. package/src/learning/tests/test_schema_migration.py +0 -319
  340. package/src/learning/tests/test_signal_inference.py +0 -397
  341. package/src/learning/tests/test_source_quality.py +0 -351
  342. package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
  343. package/src/learning/tests/test_workflow_miner.py +0 -318
  344. package/src/learning/workflow_pattern_miner.py +0 -655
  345. package/src/lifecycle/__init__.py +0 -54
  346. package/src/lifecycle/bounded_growth.py +0 -239
  347. package/src/lifecycle/compaction_engine.py +0 -226
  348. package/src/lifecycle/lifecycle_engine.py +0 -355
  349. package/src/lifecycle/lifecycle_evaluator.py +0 -257
  350. package/src/lifecycle/lifecycle_scheduler.py +0 -130
  351. package/src/lifecycle/retention_policy.py +0 -285
  352. package/src/lifecycle/tests/test_bounded_growth.py +0 -193
  353. package/src/lifecycle/tests/test_compaction.py +0 -179
  354. package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
  355. package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
  356. package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
  357. package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
  358. package/src/lifecycle/tests/test_mcp_compact.py +0 -149
  359. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
  360. package/src/lifecycle/tests/test_retention_policy.py +0 -162
  361. package/src/mcp_tools_v28.py +0 -281
  362. package/src/memory/__init__.py +0 -36
  363. package/src/memory/cli.py +0 -205
  364. package/src/memory/constants.py +0 -39
  365. package/src/memory/helpers.py +0 -28
  366. package/src/memory/schema.py +0 -166
  367. package/src/memory-profiles.py +0 -595
  368. package/src/memory-reset.py +0 -491
  369. package/src/memory_compression.py +0 -989
  370. package/src/memory_store_v2.py +0 -1155
  371. package/src/migrate_v1_to_v2.py +0 -629
  372. package/src/pattern_learner.py +0 -34
  373. package/src/patterns/__init__.py +0 -24
  374. package/src/patterns/analyzers.py +0 -251
  375. package/src/patterns/learner.py +0 -271
  376. package/src/patterns/scoring.py +0 -171
  377. package/src/patterns/store.py +0 -225
  378. package/src/patterns/terminology.py +0 -140
  379. package/src/provenance_tracker.py +0 -312
  380. package/src/qualixar_attribution.py +0 -139
  381. package/src/qualixar_watermark.py +0 -78
  382. package/src/query_optimizer.py +0 -511
  383. package/src/rate_limiter.py +0 -83
  384. package/src/search/__init__.py +0 -20
  385. package/src/search/cli.py +0 -77
  386. package/src/search/constants.py +0 -26
  387. package/src/search/engine.py +0 -241
  388. package/src/search/fusion.py +0 -122
  389. package/src/search/index_loader.py +0 -114
  390. package/src/search/methods.py +0 -162
  391. package/src/search_engine_v2.py +0 -401
  392. package/src/setup_validator.py +0 -482
  393. package/src/subscription_manager.py +0 -391
  394. package/src/tree/__init__.py +0 -59
  395. package/src/tree/builder.py +0 -185
  396. package/src/tree/nodes.py +0 -202
  397. package/src/tree/queries.py +0 -257
  398. package/src/tree/schema.py +0 -80
  399. package/src/tree_manager.py +0 -19
  400. package/src/trust/__init__.py +0 -45
  401. package/src/trust/constants.py +0 -66
  402. package/src/trust/queries.py +0 -157
  403. package/src/trust/schema.py +0 -95
  404. package/src/trust/scorer.py +0 -299
  405. package/src/trust/signals.py +0 -95
  406. package/src/trust_scorer.py +0 -44
  407. package/ui/app.js +0 -1588
  408. package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
  409. package/ui/js/graph-cytoscape.js +0 -1168
  410. package/ui/js/graph-d3-backup.js +0 -32
  411. package/ui/js/graph.js +0 -32
  412. package/ui_server.py +0 -266
  413. /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
  414. /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
  415. /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
  416. /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
  417. /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
  418. /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
  419. /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
  420. /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
  421. /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
  422. /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
  423. /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
  424. /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
  425. /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
  426. /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
  427. /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
  428. /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
  429. /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
  430. /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
  431. /package/{completions → ide/completions}/slm.bash +0 -0
  432. /package/{completions → ide/completions}/slm.zsh +0 -0
  433. /package/{configs → ide/configs}/cody-commands.json +0 -0
  434. /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
@@ -0,0 +1,376 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """SuperLocalMemory V3 — Learning Database.
6
+
7
+ Persistent storage for the adaptive ranker: feedback signals, feature
8
+ vectors, engagement metrics, and serialized model state. Uses direct
9
+ sqlite3 connections (independent of V3 DatabaseManager) for isolation.
10
+
11
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import sqlite3
19
+ import threading
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+ from typing import Any, Optional, Union
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _SCHEMA = """
27
+ CREATE TABLE IF NOT EXISTS learning_signals (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ profile_id TEXT NOT NULL,
30
+ query TEXT NOT NULL,
31
+ fact_id TEXT NOT NULL,
32
+ signal_type TEXT NOT NULL,
33
+ value REAL DEFAULT 1.0,
34
+ created_at TEXT NOT NULL
35
+ );
36
+
37
+ CREATE INDEX IF NOT EXISTS idx_signals_profile
38
+ ON learning_signals(profile_id);
39
+ CREATE INDEX IF NOT EXISTS idx_signals_fact
40
+ ON learning_signals(fact_id);
41
+
42
+ CREATE TABLE IF NOT EXISTS learning_features (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ profile_id TEXT NOT NULL,
45
+ query_id TEXT NOT NULL,
46
+ fact_id TEXT NOT NULL,
47
+ features_json TEXT NOT NULL,
48
+ label REAL NOT NULL,
49
+ created_at TEXT NOT NULL
50
+ );
51
+
52
+ CREATE INDEX IF NOT EXISTS idx_features_profile
53
+ ON learning_features(profile_id);
54
+
55
+ CREATE TABLE IF NOT EXISTS learning_model_state (
56
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ profile_id TEXT NOT NULL UNIQUE,
58
+ state_bytes BLOB NOT NULL,
59
+ updated_at TEXT NOT NULL
60
+ );
61
+
62
+ CREATE TABLE IF NOT EXISTS engagement_metrics (
63
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
64
+ profile_id TEXT NOT NULL,
65
+ metric_type TEXT NOT NULL,
66
+ value REAL NOT NULL DEFAULT 0,
67
+ updated_at TEXT NOT NULL
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_engagement_profile_metric
71
+ ON engagement_metrics(profile_id, metric_type);
72
+ """
73
+
74
+
75
+ class LearningDatabase:
76
+ """Persistent storage for the adaptive ranker's training pipeline.
77
+
78
+ Owns its own sqlite3 connection — independent of the main DB manager.
79
+ Thread-safe writes via a lock. WAL mode for concurrent reads.
80
+
81
+ Args:
82
+ db_path: Path to the learning SQLite database file.
83
+ """
84
+
85
+ def __init__(self, db_path: Union[str, Path]) -> None:
86
+ self._db_path = str(db_path)
87
+ self._lock = threading.Lock()
88
+ self._init_schema()
89
+
90
+ def _connect(self) -> sqlite3.Connection:
91
+ """Create a configured connection to the learning database."""
92
+ conn = sqlite3.connect(self._db_path, timeout=10)
93
+ conn.execute("PRAGMA journal_mode=WAL")
94
+ conn.execute("PRAGMA busy_timeout=5000")
95
+ conn.row_factory = sqlite3.Row
96
+ return conn
97
+
98
+ def _init_schema(self) -> None:
99
+ """Create tables and indexes if they do not exist."""
100
+ conn = self._connect()
101
+ try:
102
+ conn.executescript(_SCHEMA)
103
+ conn.commit()
104
+ finally:
105
+ conn.close()
106
+
107
+ @staticmethod
108
+ def _now() -> str:
109
+ return datetime.now(timezone.utc).isoformat()
110
+
111
+ def store_signal(
112
+ self,
113
+ profile_id: str,
114
+ query: str,
115
+ fact_id: str,
116
+ signal_type: str,
117
+ value: float = 1.0,
118
+ ) -> int:
119
+ """Record a feedback signal (e.g. recall_hit, recall_miss).
120
+
121
+ Args:
122
+ profile_id: Owning profile.
123
+ query: The user query that produced this signal.
124
+ fact_id: The fact that was matched (or missed).
125
+ signal_type: Label for the signal kind.
126
+ value: Numeric value (1.0 = strong positive, 0.0 = negative).
127
+
128
+ Returns:
129
+ Row ID of the inserted signal.
130
+ """
131
+ with self._lock:
132
+ conn = self._connect()
133
+ try:
134
+ cur = conn.execute(
135
+ "INSERT INTO learning_signals "
136
+ "(profile_id, query, fact_id, signal_type, value, created_at) "
137
+ "VALUES (?, ?, ?, ?, ?, ?)",
138
+ (profile_id, query, fact_id, signal_type, value, self._now()),
139
+ )
140
+ conn.commit()
141
+ return cur.lastrowid # type: ignore[return-value]
142
+ except sqlite3.Error as exc:
143
+ conn.rollback()
144
+ logger.error("store_signal failed: %s", exc)
145
+ raise
146
+ finally:
147
+ conn.close()
148
+
149
+ def get_signal_count(self, profile_id: str) -> int:
150
+ """Count feedback signals for a profile.
151
+
152
+ Used to determine the adaptive ranker phase:
153
+ phase 1 (<20 signals) = defaults, phase 2 (20-200) = heuristic,
154
+ phase 3 (>200) = LightGBM.
155
+ """
156
+ conn = self._connect()
157
+ try:
158
+ row = conn.execute(
159
+ "SELECT COUNT(*) AS cnt FROM learning_signals "
160
+ "WHERE profile_id = ?",
161
+ (profile_id,),
162
+ ).fetchone()
163
+ return int(row["cnt"]) if row else 0
164
+ finally:
165
+ conn.close()
166
+
167
+ def store_features(
168
+ self,
169
+ profile_id: str,
170
+ query_id: str,
171
+ fact_id: str,
172
+ features: dict[str, float],
173
+ label: float,
174
+ ) -> int:
175
+ """Store a labeled feature vector for LightGBM training.
176
+
177
+ Args:
178
+ profile_id: Owning profile.
179
+ query_id: Identifier for the query (hash or UUID).
180
+ fact_id: Identifier for the candidate fact.
181
+ features: Feature dict (e.g. semantic_score, bm25_score, ...).
182
+ label: Relevance label (1.0 = relevant, 0.0 = irrelevant).
183
+
184
+ Returns:
185
+ Row ID of the inserted record.
186
+ """
187
+ features_json = json.dumps(features, sort_keys=True)
188
+ with self._lock:
189
+ conn = self._connect()
190
+ try:
191
+ cur = conn.execute(
192
+ "INSERT INTO learning_features "
193
+ "(profile_id, query_id, fact_id, features_json, label, created_at) "
194
+ "VALUES (?, ?, ?, ?, ?, ?)",
195
+ (profile_id, query_id, fact_id, features_json, label, self._now()),
196
+ )
197
+ conn.commit()
198
+ return cur.lastrowid # type: ignore[return-value]
199
+ except sqlite3.Error as exc:
200
+ conn.rollback()
201
+ logger.error("store_features failed: %s", exc)
202
+ raise
203
+ finally:
204
+ conn.close()
205
+
206
+ def get_training_data(
207
+ self, profile_id: str, limit: int = 5000
208
+ ) -> list[dict[str, Any]]:
209
+ """Retrieve labeled feature vectors for model training.
210
+
211
+ Returns newest examples first. Each dict contains:
212
+ query_id, fact_id, features (dict), label, created_at.
213
+ """
214
+ conn = self._connect()
215
+ try:
216
+ rows = conn.execute(
217
+ "SELECT query_id, fact_id, features_json, label, created_at "
218
+ "FROM learning_features WHERE profile_id = ? "
219
+ "ORDER BY created_at DESC LIMIT ?",
220
+ (profile_id, limit),
221
+ ).fetchall()
222
+ results: list[dict[str, Any]] = []
223
+ for row in rows:
224
+ d = dict(row)
225
+ d["features"] = json.loads(d.pop("features_json"))
226
+ results.append(d)
227
+ return results
228
+ finally:
229
+ conn.close()
230
+
231
+ def store_model_state(self, profile_id: str, state_bytes: bytes) -> None:
232
+ """Persist serialized model weights for a profile.
233
+
234
+ Uses INSERT OR REPLACE so only one state row per profile exists.
235
+
236
+ Args:
237
+ profile_id: Owning profile.
238
+ state_bytes: Serialized model bytes (from joblib or similar).
239
+ """
240
+ with self._lock:
241
+ conn = self._connect()
242
+ try:
243
+ conn.execute(
244
+ "INSERT INTO learning_model_state "
245
+ "(profile_id, state_bytes, updated_at) "
246
+ "VALUES (?, ?, ?) "
247
+ "ON CONFLICT(profile_id) DO UPDATE SET "
248
+ "state_bytes = excluded.state_bytes, "
249
+ "updated_at = excluded.updated_at",
250
+ (profile_id, state_bytes, self._now()),
251
+ )
252
+ conn.commit()
253
+ except sqlite3.Error as exc:
254
+ conn.rollback()
255
+ logger.error("store_model_state failed: %s", exc)
256
+ raise
257
+ finally:
258
+ conn.close()
259
+
260
+ def load_model_state(self, profile_id: str) -> Optional[bytes]:
261
+ """Load serialized model weights for a profile.
262
+
263
+ Returns:
264
+ The stored bytes, or None if no model has been persisted.
265
+ """
266
+ conn = self._connect()
267
+ try:
268
+ row = conn.execute(
269
+ "SELECT state_bytes FROM learning_model_state "
270
+ "WHERE profile_id = ?",
271
+ (profile_id,),
272
+ ).fetchone()
273
+ return bytes(row["state_bytes"]) if row else None
274
+ finally:
275
+ conn.close()
276
+
277
+ def record_engagement(
278
+ self,
279
+ profile_id: str,
280
+ metric_type: str,
281
+ value: float = 1.0,
282
+ ) -> None:
283
+ """Increment (or create) an engagement counter.
284
+
285
+ Examples of metric_type: recall_count, store_count,
286
+ search_count, feedback_count.
287
+
288
+ Args:
289
+ profile_id: Owning profile.
290
+ metric_type: The metric name.
291
+ value: Amount to add (default 1).
292
+ """
293
+ with self._lock:
294
+ conn = self._connect()
295
+ try:
296
+ existing = conn.execute(
297
+ "SELECT id, value FROM engagement_metrics "
298
+ "WHERE profile_id = ? AND metric_type = ?",
299
+ (profile_id, metric_type),
300
+ ).fetchone()
301
+
302
+ if existing:
303
+ new_value = float(existing["value"]) + value
304
+ conn.execute(
305
+ "UPDATE engagement_metrics SET value = ?, updated_at = ? "
306
+ "WHERE id = ?",
307
+ (new_value, self._now(), existing["id"]),
308
+ )
309
+ else:
310
+ conn.execute(
311
+ "INSERT INTO engagement_metrics "
312
+ "(profile_id, metric_type, value, updated_at) "
313
+ "VALUES (?, ?, ?, ?)",
314
+ (profile_id, metric_type, value, self._now()),
315
+ )
316
+ conn.commit()
317
+ except sqlite3.Error as exc:
318
+ conn.rollback()
319
+ logger.error("record_engagement failed: %s", exc)
320
+ raise
321
+ finally:
322
+ conn.close()
323
+
324
+ def get_engagement_stats(self, profile_id: str) -> dict[str, float]:
325
+ """Get all engagement counters for a profile.
326
+
327
+ Returns:
328
+ Dict mapping metric_type to cumulative value.
329
+ Empty dict if no engagement data exists.
330
+ """
331
+ conn = self._connect()
332
+ try:
333
+ rows = conn.execute(
334
+ "SELECT metric_type, value FROM engagement_metrics "
335
+ "WHERE profile_id = ?",
336
+ (profile_id,),
337
+ ).fetchall()
338
+ return {row["metric_type"]: float(row["value"]) for row in rows}
339
+ finally:
340
+ conn.close()
341
+
342
+ def reset(self, profile_id: Optional[str] = None) -> None:
343
+ """Delete learning data. GDPR Article 17 handler.
344
+
345
+ Args:
346
+ profile_id: If provided, only erase data for that profile.
347
+ If None, erase ALL learning data.
348
+ """
349
+ with self._lock:
350
+ conn = self._connect()
351
+ try:
352
+ tables = [
353
+ "learning_signals",
354
+ "learning_features",
355
+ "learning_model_state",
356
+ "engagement_metrics",
357
+ ]
358
+ for table in tables:
359
+ if profile_id:
360
+ conn.execute(
361
+ f"DELETE FROM {table} WHERE profile_id = ?",
362
+ (profile_id,),
363
+ )
364
+ else:
365
+ conn.execute(f"DELETE FROM {table}")
366
+ conn.commit()
367
+ logger.info(
368
+ "Learning data reset%s",
369
+ f" for profile {profile_id}" if profile_id else " (all)",
370
+ )
371
+ except sqlite3.Error as exc:
372
+ conn.rollback()
373
+ logger.error("reset failed: %s", exc)
374
+ raise
375
+ finally:
376
+ conn.close()
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 Qualixar / SuperLocalMemory (superlocalmemory.com)
4
+ # Part of Qualixar | Author: Varun Pratap Bhardwaj (qualixar.com | varunpratap.com)
5
+ """
6
+ EngagementTracker -- Local-only engagement metrics for V3 learning.
7
+
8
+ Tracks user engagement events per profile:
9
+ - recall_count, store_count, delete_count
10
+ - session_count, active_days
11
+ - composite engagement_score
12
+ - health classification: active / warm / cold / inactive
13
+
14
+ All data stays local. Uses direct sqlite3 with a self-contained
15
+ ``engagement_events`` table. NOT coupled to V3 DatabaseManager.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import sqlite3
22
+ import threading
23
+ from datetime import date, datetime, timedelta, timezone
24
+ from pathlib import Path
25
+ from typing import Any, Dict, Optional
26
+
27
+ logger = logging.getLogger("superlocalmemory.learning.engagement")
28
+
29
+ # Valid event types
30
+ VALID_EVENT_TYPES = frozenset({"recall", "store", "delete", "session_start"})
31
+
32
+ _CREATE_TABLE = """
33
+ CREATE TABLE IF NOT EXISTS engagement_events (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ profile_id TEXT NOT NULL,
36
+ event_type TEXT NOT NULL,
37
+ created_at TEXT NOT NULL
38
+ )
39
+ """
40
+
41
+ _CREATE_INDEX = """
42
+ CREATE INDEX IF NOT EXISTS idx_engagement_profile
43
+ ON engagement_events (profile_id, event_type)
44
+ """
45
+
46
+ # Health thresholds (events in last 7 days)
47
+ _ACTIVE_THRESHOLD = 10
48
+ _WARM_THRESHOLD = 3
49
+
50
+
51
+ def _utcnow_iso() -> str:
52
+ """Return current UTC time as ISO-8601 string."""
53
+ return datetime.now(timezone.utc).isoformat()
54
+
55
+
56
+ def _today_iso() -> str:
57
+ """Return today's date as ISO-8601 string."""
58
+ return date.today().isoformat()
59
+
60
+
61
+ class EngagementTracker:
62
+ """
63
+ Tracks per-profile engagement events for the V3 learning system.
64
+
65
+ Each instance owns a sqlite3 database at *db_path*.
66
+ Thread-safe: all writes serialised through a lock.
67
+
68
+ Args:
69
+ db_path: Path to the sqlite3 database file.
70
+ """
71
+
72
+ def __init__(self, db_path: Path) -> None:
73
+ self._db_path = Path(db_path)
74
+ self._lock = threading.Lock()
75
+ self._ensure_schema()
76
+
77
+ # ------------------------------------------------------------------
78
+ # Schema
79
+ # ------------------------------------------------------------------
80
+
81
+ def _ensure_schema(self) -> None:
82
+ conn = self._connect()
83
+ try:
84
+ conn.execute(_CREATE_TABLE)
85
+ conn.execute(_CREATE_INDEX)
86
+ conn.commit()
87
+ finally:
88
+ conn.close()
89
+
90
+ def _connect(self) -> sqlite3.Connection:
91
+ conn = sqlite3.connect(str(self._db_path), timeout=10)
92
+ conn.execute("PRAGMA journal_mode=WAL")
93
+ conn.execute("PRAGMA busy_timeout=5000")
94
+ conn.row_factory = sqlite3.Row
95
+ return conn
96
+
97
+ # ------------------------------------------------------------------
98
+ # Public API: record events
99
+ # ------------------------------------------------------------------
100
+
101
+ def record_event(self, profile_id: str, event_type: str) -> Optional[int]:
102
+ """
103
+ Record an engagement event.
104
+
105
+ Args:
106
+ profile_id: Profile that generated the event.
107
+ event_type: One of ``recall``, ``store``, ``delete``,
108
+ ``session_start``.
109
+
110
+ Returns:
111
+ Row ID of the inserted record, or None on invalid input.
112
+ """
113
+ if not profile_id:
114
+ return None
115
+ if event_type not in VALID_EVENT_TYPES:
116
+ logger.warning("Invalid event type: %r", event_type)
117
+ return None
118
+
119
+ now = _utcnow_iso()
120
+ with self._lock:
121
+ conn = self._connect()
122
+ try:
123
+ cursor = conn.execute(
124
+ "INSERT INTO engagement_events "
125
+ "(profile_id, event_type, created_at) VALUES (?, ?, ?)",
126
+ (profile_id, event_type, now),
127
+ )
128
+ conn.commit()
129
+ return cursor.lastrowid
130
+ finally:
131
+ conn.close()
132
+
133
+ # ------------------------------------------------------------------
134
+ # Public API: stats
135
+ # ------------------------------------------------------------------
136
+
137
+ def get_stats(self, profile_id: str) -> Dict[str, Any]:
138
+ """
139
+ Return engagement statistics for a profile.
140
+
141
+ Returns:
142
+ Dict with keys: recall_count, store_count, delete_count,
143
+ session_count, active_days, total_events, engagement_score.
144
+ """
145
+ conn = self._connect()
146
+ try:
147
+ # Count by event type
148
+ rows = conn.execute(
149
+ "SELECT event_type, COUNT(*) AS cnt "
150
+ "FROM engagement_events WHERE profile_id = ? "
151
+ "GROUP BY event_type",
152
+ (profile_id,),
153
+ ).fetchall()
154
+ counts: Dict[str, int] = {r["event_type"]: r["cnt"] for r in rows}
155
+
156
+ recall_count = counts.get("recall", 0)
157
+ store_count = counts.get("store", 0)
158
+ delete_count = counts.get("delete", 0)
159
+ session_count = counts.get("session_start", 0)
160
+ total_events = sum(counts.values())
161
+
162
+ # Active days: count distinct dates
163
+ active_days = self._count_active_days(conn, profile_id)
164
+
165
+ # Composite engagement score
166
+ score = self._compute_engagement_score(
167
+ recall_count=recall_count,
168
+ store_count=store_count,
169
+ session_count=session_count,
170
+ active_days=active_days,
171
+ )
172
+
173
+ return {
174
+ "recall_count": recall_count,
175
+ "store_count": store_count,
176
+ "delete_count": delete_count,
177
+ "session_count": session_count,
178
+ "active_days": active_days,
179
+ "total_events": total_events,
180
+ "engagement_score": round(score, 4),
181
+ }
182
+ finally:
183
+ conn.close()
184
+
185
+ # ------------------------------------------------------------------
186
+ # Public API: health
187
+ # ------------------------------------------------------------------
188
+
189
+ def get_health(self, profile_id: str) -> str:
190
+ """
191
+ Classify engagement health for a profile.
192
+
193
+ Uses events in the last 7 days:
194
+ - **active**: >= 10 events
195
+ - **warm**: >= 3 events
196
+ - **cold**: >= 1 event
197
+ - **inactive**: 0 events
198
+
199
+ Args:
200
+ profile_id: Profile to check.
201
+
202
+ Returns:
203
+ One of ``"active"``, ``"warm"``, ``"cold"``, ``"inactive"``.
204
+ """
205
+ recent = self._count_recent_events(profile_id, days=7)
206
+
207
+ if recent >= _ACTIVE_THRESHOLD:
208
+ return "active"
209
+ if recent >= _WARM_THRESHOLD:
210
+ return "warm"
211
+ if recent >= 1:
212
+ return "cold"
213
+ return "inactive"
214
+
215
+ # ------------------------------------------------------------------
216
+ # Public API: weekly summary
217
+ # ------------------------------------------------------------------
218
+
219
+ def get_weekly_summary(self, profile_id: str) -> Dict[str, Any]:
220
+ """
221
+ Summarise the last 7 days of engagement.
222
+
223
+ Returns:
224
+ Dict with period_start, period_end, total_events,
225
+ by_type, active_days.
226
+ """
227
+ cutoff = (
228
+ datetime.now(timezone.utc) - timedelta(days=7)
229
+ ).isoformat()
230
+
231
+ conn = self._connect()
232
+ try:
233
+ rows = conn.execute(
234
+ "SELECT event_type, COUNT(*) AS cnt "
235
+ "FROM engagement_events "
236
+ "WHERE profile_id = ? AND created_at >= ? "
237
+ "GROUP BY event_type",
238
+ (profile_id, cutoff),
239
+ ).fetchall()
240
+ by_type = {r["event_type"]: r["cnt"] for r in rows}
241
+ total = sum(by_type.values())
242
+
243
+ # Active days in the window
244
+ day_rows = conn.execute(
245
+ "SELECT COUNT(DISTINCT SUBSTR(created_at, 1, 10)) "
246
+ "FROM engagement_events "
247
+ "WHERE profile_id = ? AND created_at >= ?",
248
+ (profile_id, cutoff),
249
+ ).fetchone()
250
+ active_days = day_rows[0] if day_rows else 0
251
+
252
+ return {
253
+ "period_start": (date.today() - timedelta(days=6)).isoformat(),
254
+ "period_end": _today_iso(),
255
+ "total_events": total,
256
+ "by_type": by_type,
257
+ "active_days": active_days,
258
+ }
259
+ finally:
260
+ conn.close()
261
+
262
+ # ------------------------------------------------------------------
263
+ # Internal helpers
264
+ # ------------------------------------------------------------------
265
+
266
+ def _count_active_days(
267
+ self,
268
+ conn: sqlite3.Connection,
269
+ profile_id: str,
270
+ ) -> int:
271
+ """Count distinct dates with at least one event."""
272
+ row = conn.execute(
273
+ "SELECT COUNT(DISTINCT SUBSTR(created_at, 1, 10)) "
274
+ "FROM engagement_events WHERE profile_id = ?",
275
+ (profile_id,),
276
+ ).fetchone()
277
+ return row[0] if row else 0
278
+
279
+ def _count_recent_events(self, profile_id: str, days: int = 7) -> int:
280
+ """Count events in the last *days* days."""
281
+ cutoff = (
282
+ datetime.now(timezone.utc) - timedelta(days=days)
283
+ ).isoformat()
284
+
285
+ conn = self._connect()
286
+ try:
287
+ row = conn.execute(
288
+ "SELECT COUNT(*) FROM engagement_events "
289
+ "WHERE profile_id = ? AND created_at >= ?",
290
+ (profile_id, cutoff),
291
+ ).fetchone()
292
+ return row[0] if row else 0
293
+ finally:
294
+ conn.close()
295
+
296
+ @staticmethod
297
+ def _compute_engagement_score(
298
+ recall_count: int,
299
+ store_count: int,
300
+ session_count: int,
301
+ active_days: int,
302
+ ) -> float:
303
+ """
304
+ Compute a composite engagement score in [0.0, 1.0].
305
+
306
+ Weighted sum normalised by a soft ceiling:
307
+ raw = 0.4 * recall + 0.3 * store + 0.2 * session + 0.1 * days
308
+ score = raw / (raw + 20) (sigmoid-like saturation at ~20)
309
+
310
+ This gives:
311
+ - 0 activity -> 0.0
312
+ - 20 events -> ~0.5
313
+ - 100 events -> ~0.83
314
+ """
315
+ raw = (
316
+ 0.4 * recall_count
317
+ + 0.3 * store_count
318
+ + 0.2 * session_count
319
+ + 0.1 * active_days
320
+ )
321
+ if raw <= 0:
322
+ return 0.0
323
+ return raw / (raw + 20.0)