superlocalmemory 2.8.6 → 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 (431) hide show
  1. package/LICENSE +9 -1
  2. package/NOTICE +63 -0
  3. package/README.md +165 -480
  4. package/bin/slm +17 -449
  5. package/bin/slm-npm +1 -1
  6. package/conftest.py +5 -0
  7. package/docs/api-reference.md +284 -0
  8. package/docs/architecture.md +149 -0
  9. package/docs/auto-memory.md +150 -0
  10. package/docs/cli-reference.md +276 -0
  11. package/docs/compliance.md +191 -0
  12. package/docs/configuration.md +182 -0
  13. package/docs/getting-started.md +102 -0
  14. package/docs/ide-setup.md +261 -0
  15. package/docs/mcp-tools.md +220 -0
  16. package/docs/migration-from-v2.md +170 -0
  17. package/docs/profiles.md +173 -0
  18. package/docs/troubleshooting.md +310 -0
  19. package/{configs → ide/configs}/antigravity-mcp.json +3 -3
  20. package/ide/configs/chatgpt-desktop-mcp.json +16 -0
  21. package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
  22. package/{configs → ide/configs}/codex-mcp.toml +4 -4
  23. package/{configs → ide/configs}/continue-mcp.yaml +4 -3
  24. package/{configs → ide/configs}/continue-skills.yaml +6 -6
  25. package/ide/configs/cursor-mcp.json +15 -0
  26. package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
  27. package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
  28. package/{configs → ide/configs}/opencode-mcp.json +2 -2
  29. package/{configs → ide/configs}/perplexity-mcp.json +2 -2
  30. package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
  31. package/{configs → ide/configs}/windsurf-mcp.json +3 -3
  32. package/{configs → ide/configs}/zed-mcp.json +2 -2
  33. package/{hooks → ide/hooks}/context-hook.js +9 -20
  34. package/ide/hooks/memory-list-skill.js +70 -0
  35. package/ide/hooks/memory-profile-skill.js +101 -0
  36. package/ide/hooks/memory-recall-skill.js +62 -0
  37. package/ide/hooks/memory-remember-skill.js +68 -0
  38. package/ide/hooks/memory-reset-skill.js +160 -0
  39. package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
  40. package/ide/integrations/langchain/README.md +106 -0
  41. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
  42. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
  43. package/ide/integrations/langchain/pyproject.toml +38 -0
  44. package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
  45. package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
  46. package/ide/integrations/langchain/tests/test_security.py +117 -0
  47. package/ide/integrations/llamaindex/README.md +81 -0
  48. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
  49. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
  50. package/ide/integrations/llamaindex/pyproject.toml +43 -0
  51. package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
  52. package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
  53. package/ide/integrations/llamaindex/tests/test_security.py +241 -0
  54. package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
  55. package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
  56. package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
  57. package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
  58. package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
  59. package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
  60. package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
  61. package/package.json +13 -22
  62. package/pyproject.toml +85 -0
  63. package/scripts/build-dmg.sh +417 -0
  64. package/scripts/install-skills.ps1 +334 -0
  65. package/scripts/postinstall.js +2 -2
  66. package/scripts/start-dashboard.ps1 +52 -0
  67. package/scripts/start-dashboard.sh +41 -0
  68. package/scripts/sync-wiki.ps1 +127 -0
  69. package/scripts/sync-wiki.sh +82 -0
  70. package/scripts/test-dmg.sh +161 -0
  71. package/scripts/test-npm-package.ps1 +252 -0
  72. package/scripts/test-npm-package.sh +207 -0
  73. package/scripts/verify-install.ps1 +294 -0
  74. package/scripts/verify-install.sh +266 -0
  75. package/src/superlocalmemory/__init__.py +0 -0
  76. package/src/superlocalmemory/attribution/__init__.py +9 -0
  77. package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
  78. package/src/superlocalmemory/attribution/signer.py +153 -0
  79. package/src/superlocalmemory/attribution/watermark.py +189 -0
  80. package/src/superlocalmemory/cli/__init__.py +5 -0
  81. package/src/superlocalmemory/cli/commands.py +245 -0
  82. package/src/superlocalmemory/cli/main.py +89 -0
  83. package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
  84. package/src/superlocalmemory/cli/post_install.py +99 -0
  85. package/src/superlocalmemory/cli/setup_wizard.py +129 -0
  86. package/src/superlocalmemory/compliance/__init__.py +0 -0
  87. package/src/superlocalmemory/compliance/abac.py +204 -0
  88. package/src/superlocalmemory/compliance/audit.py +314 -0
  89. package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
  90. package/src/superlocalmemory/compliance/gdpr.py +294 -0
  91. package/src/superlocalmemory/compliance/lifecycle.py +158 -0
  92. package/src/superlocalmemory/compliance/retention.py +232 -0
  93. package/src/superlocalmemory/compliance/scheduler.py +148 -0
  94. package/src/superlocalmemory/core/__init__.py +0 -0
  95. package/src/superlocalmemory/core/config.py +391 -0
  96. package/src/superlocalmemory/core/embeddings.py +293 -0
  97. package/src/superlocalmemory/core/engine.py +701 -0
  98. package/src/superlocalmemory/core/hooks.py +65 -0
  99. package/src/superlocalmemory/core/maintenance.py +172 -0
  100. package/src/superlocalmemory/core/modes.py +140 -0
  101. package/src/superlocalmemory/core/profiles.py +234 -0
  102. package/src/superlocalmemory/core/registry.py +117 -0
  103. package/src/superlocalmemory/dynamics/__init__.py +0 -0
  104. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
  105. package/src/superlocalmemory/encoding/__init__.py +0 -0
  106. package/src/superlocalmemory/encoding/consolidator.py +485 -0
  107. package/src/superlocalmemory/encoding/emotional.py +125 -0
  108. package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
  109. package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
  110. package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
  111. package/src/superlocalmemory/encoding/foresight.py +91 -0
  112. package/src/superlocalmemory/encoding/graph_builder.py +302 -0
  113. package/src/superlocalmemory/encoding/observation_builder.py +160 -0
  114. package/src/superlocalmemory/encoding/scene_builder.py +183 -0
  115. package/src/superlocalmemory/encoding/signal_inference.py +90 -0
  116. package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
  117. package/src/superlocalmemory/encoding/type_router.py +235 -0
  118. package/src/superlocalmemory/hooks/__init__.py +3 -0
  119. package/src/superlocalmemory/hooks/auto_capture.py +111 -0
  120. package/src/superlocalmemory/hooks/auto_recall.py +93 -0
  121. package/src/superlocalmemory/hooks/ide_connector.py +204 -0
  122. package/src/superlocalmemory/hooks/rules_engine.py +99 -0
  123. package/src/superlocalmemory/infra/__init__.py +3 -0
  124. package/src/superlocalmemory/infra/auth_middleware.py +82 -0
  125. package/src/superlocalmemory/infra/backup.py +317 -0
  126. package/src/superlocalmemory/infra/cache_manager.py +267 -0
  127. package/src/superlocalmemory/infra/event_bus.py +381 -0
  128. package/src/superlocalmemory/infra/rate_limiter.py +135 -0
  129. package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
  130. package/src/superlocalmemory/learning/__init__.py +0 -0
  131. package/src/superlocalmemory/learning/adaptive.py +172 -0
  132. package/src/superlocalmemory/learning/behavioral.py +490 -0
  133. package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
  134. package/src/superlocalmemory/learning/bootstrap.py +298 -0
  135. package/src/superlocalmemory/learning/cross_project.py +399 -0
  136. package/src/superlocalmemory/learning/database.py +376 -0
  137. package/src/superlocalmemory/learning/engagement.py +323 -0
  138. package/src/superlocalmemory/learning/features.py +138 -0
  139. package/src/superlocalmemory/learning/feedback.py +316 -0
  140. package/src/superlocalmemory/learning/outcomes.py +255 -0
  141. package/src/superlocalmemory/learning/project_context.py +366 -0
  142. package/src/superlocalmemory/learning/ranker.py +155 -0
  143. package/src/superlocalmemory/learning/source_quality.py +303 -0
  144. package/src/superlocalmemory/learning/workflows.py +309 -0
  145. package/src/superlocalmemory/llm/__init__.py +0 -0
  146. package/src/superlocalmemory/llm/backbone.py +316 -0
  147. package/src/superlocalmemory/math/__init__.py +0 -0
  148. package/src/superlocalmemory/math/fisher.py +356 -0
  149. package/src/superlocalmemory/math/langevin.py +398 -0
  150. package/src/superlocalmemory/math/sheaf.py +257 -0
  151. package/src/superlocalmemory/mcp/__init__.py +0 -0
  152. package/src/superlocalmemory/mcp/resources.py +245 -0
  153. package/src/superlocalmemory/mcp/server.py +61 -0
  154. package/src/superlocalmemory/mcp/tools.py +18 -0
  155. package/src/superlocalmemory/mcp/tools_core.py +305 -0
  156. package/src/superlocalmemory/mcp/tools_v28.py +223 -0
  157. package/src/superlocalmemory/mcp/tools_v3.py +286 -0
  158. package/src/superlocalmemory/retrieval/__init__.py +0 -0
  159. package/src/superlocalmemory/retrieval/agentic.py +295 -0
  160. package/src/superlocalmemory/retrieval/ann_index.py +223 -0
  161. package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
  162. package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
  163. package/src/superlocalmemory/retrieval/engine.py +390 -0
  164. package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
  165. package/src/superlocalmemory/retrieval/fusion.py +78 -0
  166. package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
  167. package/src/superlocalmemory/retrieval/reranker.py +154 -0
  168. package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
  169. package/src/superlocalmemory/retrieval/strategy.py +96 -0
  170. package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
  171. package/src/superlocalmemory/server/__init__.py +1 -0
  172. package/src/superlocalmemory/server/api.py +248 -0
  173. package/src/superlocalmemory/server/routes/__init__.py +4 -0
  174. package/src/superlocalmemory/server/routes/agents.py +107 -0
  175. package/src/superlocalmemory/server/routes/backup.py +91 -0
  176. package/src/superlocalmemory/server/routes/behavioral.py +127 -0
  177. package/src/superlocalmemory/server/routes/compliance.py +160 -0
  178. package/src/superlocalmemory/server/routes/data_io.py +188 -0
  179. package/src/superlocalmemory/server/routes/events.py +183 -0
  180. package/src/superlocalmemory/server/routes/helpers.py +85 -0
  181. package/src/superlocalmemory/server/routes/learning.py +273 -0
  182. package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
  183. package/src/superlocalmemory/server/routes/memories.py +399 -0
  184. package/src/superlocalmemory/server/routes/profiles.py +219 -0
  185. package/src/superlocalmemory/server/routes/stats.py +346 -0
  186. package/src/superlocalmemory/server/routes/v3_api.py +365 -0
  187. package/src/superlocalmemory/server/routes/ws.py +82 -0
  188. package/src/superlocalmemory/server/security_middleware.py +57 -0
  189. package/src/superlocalmemory/server/ui.py +245 -0
  190. package/src/superlocalmemory/storage/__init__.py +0 -0
  191. package/src/superlocalmemory/storage/access_control.py +182 -0
  192. package/src/superlocalmemory/storage/database.py +594 -0
  193. package/src/superlocalmemory/storage/migrations.py +303 -0
  194. package/src/superlocalmemory/storage/models.py +406 -0
  195. package/src/superlocalmemory/storage/schema.py +726 -0
  196. package/src/superlocalmemory/storage/v2_migrator.py +317 -0
  197. package/src/superlocalmemory/trust/__init__.py +0 -0
  198. package/src/superlocalmemory/trust/gate.py +130 -0
  199. package/src/superlocalmemory/trust/provenance.py +124 -0
  200. package/src/superlocalmemory/trust/scorer.py +347 -0
  201. package/src/superlocalmemory/trust/signals.py +153 -0
  202. package/ui/index.html +278 -5
  203. package/ui/js/auto-settings.js +70 -0
  204. package/ui/js/dashboard.js +90 -0
  205. package/ui/js/fact-detail.js +92 -0
  206. package/ui/js/feedback.js +2 -2
  207. package/ui/js/ide-status.js +102 -0
  208. package/ui/js/math-health.js +98 -0
  209. package/ui/js/recall-lab.js +127 -0
  210. package/ui/js/settings.js +2 -2
  211. package/ui/js/trust-dashboard.js +73 -0
  212. package/api_server.py +0 -724
  213. package/bin/aider-smart +0 -72
  214. package/bin/superlocalmemoryv2-learning +0 -4
  215. package/bin/superlocalmemoryv2-list +0 -3
  216. package/bin/superlocalmemoryv2-patterns +0 -4
  217. package/bin/superlocalmemoryv2-profile +0 -3
  218. package/bin/superlocalmemoryv2-recall +0 -3
  219. package/bin/superlocalmemoryv2-remember +0 -3
  220. package/bin/superlocalmemoryv2-reset +0 -3
  221. package/bin/superlocalmemoryv2-status +0 -3
  222. package/configs/chatgpt-desktop-mcp.json +0 -16
  223. package/configs/cursor-mcp.json +0 -15
  224. package/hooks/memory-list-skill.js +0 -139
  225. package/hooks/memory-profile-skill.js +0 -273
  226. package/hooks/memory-recall-skill.js +0 -114
  227. package/hooks/memory-remember-skill.js +0 -127
  228. package/hooks/memory-reset-skill.js +0 -274
  229. package/mcp_server.py +0 -1808
  230. package/requirements-core.txt +0 -22
  231. package/requirements-learning.txt +0 -12
  232. package/requirements.txt +0 -12
  233. package/src/agent_registry.py +0 -411
  234. package/src/auth_middleware.py +0 -61
  235. package/src/auto_backup.py +0 -459
  236. package/src/behavioral/__init__.py +0 -49
  237. package/src/behavioral/behavioral_listener.py +0 -203
  238. package/src/behavioral/behavioral_patterns.py +0 -275
  239. package/src/behavioral/cross_project_transfer.py +0 -206
  240. package/src/behavioral/outcome_inference.py +0 -194
  241. package/src/behavioral/outcome_tracker.py +0 -193
  242. package/src/behavioral/tests/__init__.py +0 -4
  243. package/src/behavioral/tests/test_behavioral_integration.py +0 -108
  244. package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
  245. package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
  246. package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
  247. package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
  248. package/src/behavioral/tests/test_outcome_inference.py +0 -107
  249. package/src/behavioral/tests/test_outcome_tracker.py +0 -96
  250. package/src/cache_manager.py +0 -518
  251. package/src/compliance/__init__.py +0 -48
  252. package/src/compliance/abac_engine.py +0 -149
  253. package/src/compliance/abac_middleware.py +0 -116
  254. package/src/compliance/audit_db.py +0 -215
  255. package/src/compliance/audit_logger.py +0 -148
  256. package/src/compliance/retention_manager.py +0 -289
  257. package/src/compliance/retention_scheduler.py +0 -186
  258. package/src/compliance/tests/__init__.py +0 -4
  259. package/src/compliance/tests/test_abac_enforcement.py +0 -95
  260. package/src/compliance/tests/test_abac_engine.py +0 -124
  261. package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
  262. package/src/compliance/tests/test_audit_db.py +0 -123
  263. package/src/compliance/tests/test_audit_logger.py +0 -98
  264. package/src/compliance/tests/test_mcp_audit.py +0 -128
  265. package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
  266. package/src/compliance/tests/test_retention_manager.py +0 -131
  267. package/src/compliance/tests/test_retention_scheduler.py +0 -99
  268. package/src/compression/__init__.py +0 -25
  269. package/src/compression/cli.py +0 -150
  270. package/src/compression/cold_storage.py +0 -217
  271. package/src/compression/config.py +0 -72
  272. package/src/compression/orchestrator.py +0 -133
  273. package/src/compression/tier2_compressor.py +0 -228
  274. package/src/compression/tier3_compressor.py +0 -153
  275. package/src/compression/tier_classifier.py +0 -148
  276. package/src/db_connection_manager.py +0 -536
  277. package/src/embedding_engine.py +0 -63
  278. package/src/embeddings/__init__.py +0 -47
  279. package/src/embeddings/cache.py +0 -70
  280. package/src/embeddings/cli.py +0 -113
  281. package/src/embeddings/constants.py +0 -47
  282. package/src/embeddings/database.py +0 -91
  283. package/src/embeddings/engine.py +0 -247
  284. package/src/embeddings/model_loader.py +0 -145
  285. package/src/event_bus.py +0 -562
  286. package/src/graph/__init__.py +0 -36
  287. package/src/graph/build_helpers.py +0 -74
  288. package/src/graph/cli.py +0 -87
  289. package/src/graph/cluster_builder.py +0 -188
  290. package/src/graph/cluster_summary.py +0 -148
  291. package/src/graph/constants.py +0 -47
  292. package/src/graph/edge_builder.py +0 -162
  293. package/src/graph/entity_extractor.py +0 -95
  294. package/src/graph/graph_core.py +0 -226
  295. package/src/graph/graph_search.py +0 -231
  296. package/src/graph/hierarchical.py +0 -207
  297. package/src/graph/schema.py +0 -99
  298. package/src/graph_engine.py +0 -52
  299. package/src/hnsw_index.py +0 -628
  300. package/src/hybrid_search.py +0 -46
  301. package/src/learning/__init__.py +0 -217
  302. package/src/learning/adaptive_ranker.py +0 -682
  303. package/src/learning/bootstrap/__init__.py +0 -69
  304. package/src/learning/bootstrap/constants.py +0 -93
  305. package/src/learning/bootstrap/db_queries.py +0 -316
  306. package/src/learning/bootstrap/sampling.py +0 -82
  307. package/src/learning/bootstrap/text_utils.py +0 -71
  308. package/src/learning/cross_project_aggregator.py +0 -857
  309. package/src/learning/db/__init__.py +0 -40
  310. package/src/learning/db/constants.py +0 -44
  311. package/src/learning/db/schema.py +0 -279
  312. package/src/learning/engagement_tracker.py +0 -628
  313. package/src/learning/feature_extractor.py +0 -708
  314. package/src/learning/feedback_collector.py +0 -806
  315. package/src/learning/learning_db.py +0 -915
  316. package/src/learning/project_context_manager.py +0 -572
  317. package/src/learning/ranking/__init__.py +0 -33
  318. package/src/learning/ranking/constants.py +0 -84
  319. package/src/learning/ranking/helpers.py +0 -278
  320. package/src/learning/source_quality_scorer.py +0 -676
  321. package/src/learning/synthetic_bootstrap.py +0 -755
  322. package/src/learning/tests/test_adaptive_ranker.py +0 -325
  323. package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
  324. package/src/learning/tests/test_aggregator.py +0 -306
  325. package/src/learning/tests/test_auto_retrain_v28.py +0 -35
  326. package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
  327. package/src/learning/tests/test_feature_extractor_v28.py +0 -93
  328. package/src/learning/tests/test_feedback_collector.py +0 -294
  329. package/src/learning/tests/test_learning_db.py +0 -602
  330. package/src/learning/tests/test_learning_db_v28.py +0 -110
  331. package/src/learning/tests/test_learning_init_v28.py +0 -48
  332. package/src/learning/tests/test_outcome_signals.py +0 -48
  333. package/src/learning/tests/test_project_context.py +0 -292
  334. package/src/learning/tests/test_schema_migration.py +0 -319
  335. package/src/learning/tests/test_signal_inference.py +0 -397
  336. package/src/learning/tests/test_source_quality.py +0 -351
  337. package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
  338. package/src/learning/tests/test_workflow_miner.py +0 -318
  339. package/src/learning/workflow_pattern_miner.py +0 -655
  340. package/src/lifecycle/__init__.py +0 -54
  341. package/src/lifecycle/bounded_growth.py +0 -239
  342. package/src/lifecycle/compaction_engine.py +0 -226
  343. package/src/lifecycle/lifecycle_engine.py +0 -355
  344. package/src/lifecycle/lifecycle_evaluator.py +0 -257
  345. package/src/lifecycle/lifecycle_scheduler.py +0 -130
  346. package/src/lifecycle/retention_policy.py +0 -285
  347. package/src/lifecycle/tests/test_bounded_growth.py +0 -193
  348. package/src/lifecycle/tests/test_compaction.py +0 -179
  349. package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
  350. package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
  351. package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
  352. package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
  353. package/src/lifecycle/tests/test_mcp_compact.py +0 -149
  354. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
  355. package/src/lifecycle/tests/test_retention_policy.py +0 -162
  356. package/src/mcp_tools_v28.py +0 -281
  357. package/src/memory/__init__.py +0 -36
  358. package/src/memory/cli.py +0 -205
  359. package/src/memory/constants.py +0 -39
  360. package/src/memory/helpers.py +0 -28
  361. package/src/memory/schema.py +0 -166
  362. package/src/memory-profiles.py +0 -595
  363. package/src/memory-reset.py +0 -491
  364. package/src/memory_compression.py +0 -989
  365. package/src/memory_store_v2.py +0 -1155
  366. package/src/migrate_v1_to_v2.py +0 -629
  367. package/src/pattern_learner.py +0 -34
  368. package/src/patterns/__init__.py +0 -24
  369. package/src/patterns/analyzers.py +0 -251
  370. package/src/patterns/learner.py +0 -271
  371. package/src/patterns/scoring.py +0 -171
  372. package/src/patterns/store.py +0 -225
  373. package/src/patterns/terminology.py +0 -140
  374. package/src/provenance_tracker.py +0 -312
  375. package/src/qualixar_attribution.py +0 -139
  376. package/src/qualixar_watermark.py +0 -78
  377. package/src/query_optimizer.py +0 -511
  378. package/src/rate_limiter.py +0 -83
  379. package/src/search/__init__.py +0 -20
  380. package/src/search/cli.py +0 -77
  381. package/src/search/constants.py +0 -26
  382. package/src/search/engine.py +0 -241
  383. package/src/search/fusion.py +0 -122
  384. package/src/search/index_loader.py +0 -114
  385. package/src/search/methods.py +0 -162
  386. package/src/search_engine_v2.py +0 -401
  387. package/src/setup_validator.py +0 -482
  388. package/src/subscription_manager.py +0 -391
  389. package/src/tree/__init__.py +0 -59
  390. package/src/tree/builder.py +0 -185
  391. package/src/tree/nodes.py +0 -202
  392. package/src/tree/queries.py +0 -257
  393. package/src/tree/schema.py +0 -80
  394. package/src/tree_manager.py +0 -19
  395. package/src/trust/__init__.py +0 -45
  396. package/src/trust/constants.py +0 -66
  397. package/src/trust/queries.py +0 -157
  398. package/src/trust/schema.py +0 -95
  399. package/src/trust/scorer.py +0 -299
  400. package/src/trust/signals.py +0 -95
  401. package/src/trust_scorer.py +0 -44
  402. package/ui/app.js +0 -1588
  403. package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
  404. package/ui/js/graph-cytoscape.js +0 -1168
  405. package/ui/js/graph-d3-backup.js +0 -32
  406. package/ui/js/graph.js +0 -32
  407. package/ui_server.py +0 -286
  408. /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
  409. /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
  410. /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
  411. /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
  412. /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
  413. /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
  414. /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
  415. /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
  416. /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
  417. /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
  418. /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
  419. /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
  420. /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
  421. /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
  422. /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
  423. /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
  424. /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
  425. /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
  426. /package/{completions → ide/completions}/slm.bash +0 -0
  427. /package/{completions → ide/completions}/slm.zsh +0 -0
  428. /package/{configs → ide/configs}/cody-commands.json +0 -0
  429. /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
  430. /package/{install.ps1 → scripts/install.ps1} +0 -0
  431. /package/{install.sh → scripts/install.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)