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,138 @@
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
+ """V3-native feature extractor for adaptive ranking.
6
+
7
+ Extracts features from retrieval results for LightGBM training.
8
+ Features are grouped by source: channel scores, fusion, reranker,
9
+ math layers, query analysis, memory metadata, user history.
10
+
11
+ Each feature vector has a fixed dimension (FEATURE_DIM).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Feature dimension — total number of features extracted
23
+ FEATURE_DIM = 20
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FeatureVector:
28
+ """Immutable feature vector for one retrieval result."""
29
+ fact_id: str
30
+ query_id: str
31
+ features: dict[str, float]
32
+ label: float | None = None # None = unlabeled (inference), float = labeled (training)
33
+
34
+ def to_list(self) -> list[float]:
35
+ """Convert to ordered list for LightGBM input."""
36
+ return [self.features.get(name, 0.0) for name in FEATURE_NAMES]
37
+
38
+
39
+ # Ordered feature names — must match FEATURE_DIM
40
+ FEATURE_NAMES: list[str] = [
41
+ # Channel scores (4)
42
+ "semantic_score",
43
+ "bm25_score",
44
+ "entity_score",
45
+ "temporal_score",
46
+ # Fusion (2)
47
+ "rrf_rank",
48
+ "rrf_score",
49
+ # Reranker (1)
50
+ "cross_encoder_score",
51
+ # Math layers (3)
52
+ "fisher_distance",
53
+ "fisher_confidence",
54
+ "sheaf_consistent",
55
+ # Query features (4)
56
+ "query_type_sh", # one-hot: single-hop
57
+ "query_type_mh", # one-hot: multi-hop
58
+ "query_type_temp", # one-hot: temporal
59
+ "query_type_od", # one-hot: open-domain
60
+ # Memory metadata (4)
61
+ "fact_age_days",
62
+ "access_count",
63
+ "fact_trust_score",
64
+ "fact_confidence",
65
+ # User/profile (2)
66
+ "profile_recall_count",
67
+ "topic_affinity",
68
+ ]
69
+
70
+ assert len(FEATURE_NAMES) == FEATURE_DIM
71
+
72
+
73
+ class FeatureExtractor:
74
+ """Extract features from V3 retrieval results for LightGBM ranking."""
75
+
76
+ @staticmethod
77
+ def extract(result: dict[str, Any], query_context: dict[str, Any]) -> FeatureVector:
78
+ """Extract features from a single retrieval result.
79
+
80
+ Args:
81
+ result: dict with keys from RetrievalResult (score, channel_scores,
82
+ fact metadata, etc.)
83
+ query_context: dict with query_type, query_length, profile stats, etc.
84
+
85
+ Returns:
86
+ FeatureVector with FEATURE_DIM features.
87
+ """
88
+ channel = result.get("channel_scores", {})
89
+ fact = result.get("fact", {})
90
+
91
+ features = {
92
+ # Channel scores
93
+ "semantic_score": channel.get("semantic", 0.0),
94
+ "bm25_score": channel.get("bm25", 0.0),
95
+ "entity_score": channel.get("entity_graph", 0.0),
96
+ "temporal_score": channel.get("temporal", 0.0),
97
+ # Fusion
98
+ "rrf_rank": result.get("rrf_rank", 0.0),
99
+ "rrf_score": result.get("rrf_score", 0.0),
100
+ # Reranker
101
+ "cross_encoder_score": result.get("cross_encoder_score", 0.0),
102
+ # Math
103
+ "fisher_distance": result.get("fisher_distance", 0.0),
104
+ "fisher_confidence": result.get("fisher_confidence", 0.0),
105
+ "sheaf_consistent": 1.0 if result.get("sheaf_consistent", True) else 0.0,
106
+ # Query (one-hot)
107
+ "query_type_sh": 1.0 if query_context.get("query_type") == "single_hop" else 0.0,
108
+ "query_type_mh": 1.0 if query_context.get("query_type") == "multi_hop" else 0.0,
109
+ "query_type_temp": 1.0 if query_context.get("query_type") == "temporal" else 0.0,
110
+ "query_type_od": 1.0 if query_context.get("query_type") == "open_domain" else 0.0,
111
+ # Memory metadata
112
+ "fact_age_days": _safe_float(fact.get("age_days", 0)),
113
+ "access_count": _safe_float(fact.get("access_count", 0)),
114
+ "fact_trust_score": _safe_float(result.get("trust_score", 0.5)),
115
+ "fact_confidence": _safe_float(fact.get("confidence", 0.7)),
116
+ # User/profile
117
+ "profile_recall_count": _safe_float(query_context.get("profile_recall_count", 0)),
118
+ "topic_affinity": _safe_float(query_context.get("topic_affinity", 0.0)),
119
+ }
120
+
121
+ return FeatureVector(
122
+ fact_id=result.get("fact_id", ""),
123
+ query_id=query_context.get("query_id", ""),
124
+ features=features,
125
+ )
126
+
127
+ @staticmethod
128
+ def extract_batch(results: list[dict], query_context: dict) -> list[FeatureVector]:
129
+ """Extract features from a batch of retrieval results."""
130
+ return [FeatureExtractor.extract(r, query_context) for r in results]
131
+
132
+
133
+ def _safe_float(value: Any) -> float:
134
+ """Convert to float safely, defaulting to 0.0."""
135
+ try:
136
+ return float(value)
137
+ except (TypeError, ValueError):
138
+ return 0.0
@@ -0,0 +1,316 @@
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
+ FeedbackCollector -- Multi-signal feedback collection for V3 learning.
7
+
8
+ Collects implicit and explicit relevance signals:
9
+ - Implicit: recall_hit (fact returned), recall_miss (fact not in results)
10
+ - Explicit: user_positive, user_negative, user_correction
11
+ - Derived: access_pattern (frequent recall = positive signal)
12
+
13
+ Privacy:
14
+ - Full query text is NEVER stored.
15
+ - Queries are hashed to SHA-256[:16] for grouping.
16
+
17
+ Storage:
18
+ Uses direct sqlite3 with a self-contained ``learning_feedback`` table.
19
+ NOT coupled to V3 DatabaseManager -- this is a standalone data collector.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import hashlib
25
+ import logging
26
+ import sqlite3
27
+ import threading
28
+ from datetime import datetime, timezone
29
+ from pathlib import Path
30
+ from typing import Any, Dict, List, Optional
31
+
32
+ logger = logging.getLogger("superlocalmemory.learning.feedback")
33
+
34
+ # Signal type -> numeric value for downstream consumers
35
+ SIGNAL_VALUES: Dict[str, float] = {
36
+ "recall_hit": 0.7,
37
+ "recall_miss": 0.0,
38
+ "user_positive": 1.0,
39
+ "user_negative": 0.0,
40
+ "user_correction": 0.2,
41
+ "access_pattern": 0.6,
42
+ }
43
+
44
+ _CREATE_TABLE = """
45
+ CREATE TABLE IF NOT EXISTS learning_feedback (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ profile_id TEXT NOT NULL,
48
+ fact_id TEXT NOT NULL,
49
+ signal_type TEXT NOT NULL,
50
+ signal_value REAL NOT NULL,
51
+ query_hash TEXT,
52
+ created_at TEXT NOT NULL,
53
+ metadata TEXT
54
+ )
55
+ """
56
+
57
+ _CREATE_INDEX = """
58
+ CREATE INDEX IF NOT EXISTS idx_feedback_profile
59
+ ON learning_feedback (profile_id, created_at DESC)
60
+ """
61
+
62
+
63
+ def _utcnow_iso() -> str:
64
+ """Return current UTC time as ISO-8601 string."""
65
+ return datetime.now(timezone.utc).isoformat()
66
+
67
+
68
+ def _hash_query(query: str) -> str:
69
+ """Privacy-preserving SHA-256[:16] query hash."""
70
+ return hashlib.sha256(query.encode("utf-8")).hexdigest()[:16]
71
+
72
+
73
+ class FeedbackCollector:
74
+ """
75
+ Collects multi-signal relevance feedback for the V3 learning system.
76
+
77
+ Each instance owns a sqlite3 database at *db_path*. All writes are
78
+ serialised through a threading lock for safety.
79
+
80
+ Args:
81
+ db_path: Path to the sqlite3 database file.
82
+ """
83
+
84
+ def __init__(self, db_path: Path) -> None:
85
+ self._db_path = Path(db_path)
86
+ self._lock = threading.Lock()
87
+ self._ensure_schema()
88
+
89
+ # ------------------------------------------------------------------
90
+ # Schema
91
+ # ------------------------------------------------------------------
92
+
93
+ def _ensure_schema(self) -> None:
94
+ """Create tables/indexes if they do not exist."""
95
+ conn = self._connect()
96
+ try:
97
+ conn.execute(_CREATE_TABLE)
98
+ conn.execute(_CREATE_INDEX)
99
+ conn.commit()
100
+ finally:
101
+ conn.close()
102
+
103
+ def _connect(self) -> sqlite3.Connection:
104
+ """Open a connection with WAL mode and busy timeout."""
105
+ conn = sqlite3.connect(str(self._db_path), timeout=10)
106
+ conn.execute("PRAGMA journal_mode=WAL")
107
+ conn.execute("PRAGMA busy_timeout=5000")
108
+ conn.row_factory = sqlite3.Row
109
+ return conn
110
+
111
+ # ------------------------------------------------------------------
112
+ # Public API: record implicit feedback
113
+ # ------------------------------------------------------------------
114
+
115
+ def record_implicit(
116
+ self,
117
+ profile_id: str,
118
+ query: str,
119
+ fact_ids_returned: List[str],
120
+ fact_ids_available: List[str],
121
+ ) -> int:
122
+ """
123
+ Record implicit feedback from a recall operation.
124
+
125
+ Facts in *fact_ids_returned* get a ``recall_hit`` signal.
126
+ Facts in *fact_ids_available* but NOT in *fact_ids_returned* get
127
+ a ``recall_miss`` signal.
128
+
129
+ Args:
130
+ profile_id: Profile that performed the recall.
131
+ query: The recall query (hashed, never stored raw).
132
+ fact_ids_returned: Fact IDs that appeared in results.
133
+ fact_ids_available: All candidate fact IDs for this query.
134
+
135
+ Returns:
136
+ Number of feedback records created.
137
+ """
138
+ if not profile_id or not query:
139
+ return 0
140
+
141
+ qhash = _hash_query(query)
142
+ returned_set = set(fact_ids_returned)
143
+ now = _utcnow_iso()
144
+ records: list[tuple] = []
145
+
146
+ for fid in returned_set:
147
+ records.append((
148
+ profile_id, fid, "recall_hit",
149
+ SIGNAL_VALUES["recall_hit"], qhash, now, None,
150
+ ))
151
+
152
+ for fid in fact_ids_available:
153
+ if fid not in returned_set:
154
+ records.append((
155
+ profile_id, fid, "recall_miss",
156
+ SIGNAL_VALUES["recall_miss"], qhash, now, None,
157
+ ))
158
+
159
+ if not records:
160
+ return 0
161
+
162
+ with self._lock:
163
+ conn = self._connect()
164
+ try:
165
+ conn.executemany(
166
+ "INSERT INTO learning_feedback "
167
+ "(profile_id, fact_id, signal_type, signal_value, "
168
+ "query_hash, created_at, metadata) "
169
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
170
+ records,
171
+ )
172
+ conn.commit()
173
+ return len(records)
174
+ finally:
175
+ conn.close()
176
+
177
+ # ------------------------------------------------------------------
178
+ # Public API: record explicit feedback
179
+ # ------------------------------------------------------------------
180
+
181
+ def record_explicit(
182
+ self,
183
+ profile_id: str,
184
+ fact_id: str,
185
+ signal_type: str,
186
+ value: float,
187
+ ) -> Optional[int]:
188
+ """
189
+ Record explicit user feedback on a specific fact.
190
+
191
+ Args:
192
+ profile_id: Profile providing feedback.
193
+ fact_id: The fact being rated.
194
+ signal_type: One of ``user_positive``, ``user_negative``,
195
+ ``user_correction``, or any custom type.
196
+ value: Numeric signal value (0.0 to 1.0).
197
+
198
+ Returns:
199
+ Row ID of the inserted record, or None on error.
200
+ """
201
+ if not profile_id or not fact_id:
202
+ return None
203
+
204
+ clamped = max(0.0, min(1.0, float(value)))
205
+ now = _utcnow_iso()
206
+
207
+ with self._lock:
208
+ conn = self._connect()
209
+ try:
210
+ cursor = conn.execute(
211
+ "INSERT INTO learning_feedback "
212
+ "(profile_id, fact_id, signal_type, signal_value, "
213
+ "query_hash, created_at, metadata) "
214
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
215
+ (profile_id, fact_id, signal_type, clamped, None, now, None),
216
+ )
217
+ conn.commit()
218
+ return cursor.lastrowid
219
+ finally:
220
+ conn.close()
221
+
222
+ # ------------------------------------------------------------------
223
+ # Public API: read feedback
224
+ # ------------------------------------------------------------------
225
+
226
+ def get_feedback(
227
+ self,
228
+ profile_id: str,
229
+ limit: int = 100,
230
+ ) -> List[Dict[str, Any]]:
231
+ """
232
+ Retrieve recent feedback records for a profile.
233
+
234
+ Args:
235
+ profile_id: Profile to query.
236
+ limit: Maximum records to return.
237
+
238
+ Returns:
239
+ List of dicts with keys: id, fact_id, signal_type,
240
+ signal_value, query_hash, created_at.
241
+ """
242
+ conn = self._connect()
243
+ try:
244
+ rows = conn.execute(
245
+ "SELECT id, fact_id, signal_type, signal_value, "
246
+ "query_hash, created_at "
247
+ "FROM learning_feedback "
248
+ "WHERE profile_id = ? "
249
+ "ORDER BY created_at DESC LIMIT ?",
250
+ (profile_id, limit),
251
+ ).fetchall()
252
+ return [dict(r) for r in rows]
253
+ finally:
254
+ conn.close()
255
+
256
+ def get_feedback_count(self, profile_id: str) -> int:
257
+ """
258
+ Return the total number of feedback records for a profile.
259
+
260
+ Args:
261
+ profile_id: Profile to query.
262
+
263
+ Returns:
264
+ Integer count of feedback records.
265
+ """
266
+ conn = self._connect()
267
+ try:
268
+ row = conn.execute(
269
+ "SELECT COUNT(*) FROM learning_feedback WHERE profile_id = ?",
270
+ (profile_id,),
271
+ ).fetchone()
272
+ return row[0] if row else 0
273
+ finally:
274
+ conn.close()
275
+
276
+ # ------------------------------------------------------------------
277
+ # Public API: summary
278
+ # ------------------------------------------------------------------
279
+
280
+ def get_summary(self, profile_id: str) -> Dict[str, Any]:
281
+ """
282
+ Return summary statistics for a profile's feedback.
283
+
284
+ Returns:
285
+ Dict with total, by_type counts, and latest timestamp.
286
+ """
287
+ conn = self._connect()
288
+ try:
289
+ total_row = conn.execute(
290
+ "SELECT COUNT(*) FROM learning_feedback WHERE profile_id = ?",
291
+ (profile_id,),
292
+ ).fetchone()
293
+ total = total_row[0] if total_row else 0
294
+
295
+ type_rows = conn.execute(
296
+ "SELECT signal_type, COUNT(*) AS cnt "
297
+ "FROM learning_feedback WHERE profile_id = ? "
298
+ "GROUP BY signal_type",
299
+ (profile_id,),
300
+ ).fetchall()
301
+ by_type = {r["signal_type"]: r["cnt"] for r in type_rows}
302
+
303
+ latest_row = conn.execute(
304
+ "SELECT MAX(created_at) FROM learning_feedback "
305
+ "WHERE profile_id = ?",
306
+ (profile_id,),
307
+ ).fetchone()
308
+ latest = latest_row[0] if latest_row else None
309
+
310
+ return {
311
+ "total": total,
312
+ "by_type": by_type,
313
+ "latest": latest,
314
+ }
315
+ finally:
316
+ conn.close()
@@ -0,0 +1,255 @@
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 — Outcome Tracking & Inference.
6
+
7
+ Records what happens AFTER memories are recalled: success, failure,
8
+ or partial outcomes. Also provides signal-based outcome inference
9
+ for implicit feedback loops.
10
+
11
+ Uses the ``action_outcomes`` table from the V3 schema and returns
12
+ ``ActionOutcome`` dataclass instances for type safety.
13
+
14
+ The feedback loop:
15
+ recall() -> user action -> record_outcome() -> learning engine
16
+
17
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import logging
24
+ from typing import Any
25
+
26
+ from superlocalmemory.storage.models import ActionOutcome
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Valid outcome labels
31
+ VALID_OUTCOMES = frozenset({"success", "failure", "partial"})
32
+
33
+ # Inference signal weights
34
+ _SIGNAL_WEIGHTS: dict[str, tuple[str, float]] = {
35
+ "used_immediately": ("success", 0.9),
36
+ "mcp_used_high": ("success", 0.8),
37
+ "cross_tool_access": ("success", 0.7),
38
+ "no_requery_10m": ("success", 0.6),
39
+ "partial_use": ("partial", 0.5),
40
+ "requery_different_terms": ("failure", 0.3),
41
+ "rapid_fire_queries": ("failure", 0.2),
42
+ "deleted_after_recall": ("failure", 0.1),
43
+ "ignored": ("failure", 0.4),
44
+ }
45
+
46
+
47
+ class OutcomeTracker:
48
+ """Track retrieval outcomes and feed into learning.
49
+
50
+ Accepts a ``DatabaseManager`` and operates on the ``action_outcomes``
51
+ table created by the V3 schema. Returns ``ActionOutcome`` dataclasses.
52
+ """
53
+
54
+ def __init__(self, db) -> None:
55
+ self._db = db
56
+
57
+ # ------------------------------------------------------------------
58
+ # Public API — Recording
59
+ # ------------------------------------------------------------------
60
+
61
+ def record_outcome(
62
+ self,
63
+ query: str,
64
+ fact_ids: list[str],
65
+ outcome: str,
66
+ profile_id: str,
67
+ context: dict[str, Any] | None = None,
68
+ ) -> ActionOutcome:
69
+ """Record an outcome against one or more facts.
70
+
71
+ Args:
72
+ query: The recall query that produced these facts.
73
+ fact_ids: List of fact IDs involved in the outcome.
74
+ outcome: One of "success", "failure", "partial".
75
+ profile_id: Profile scope.
76
+ context: Arbitrary metadata dict.
77
+
78
+ Returns:
79
+ The persisted ActionOutcome.
80
+ """
81
+ if outcome not in VALID_OUTCOMES:
82
+ logger.warning(
83
+ "Invalid outcome '%s'. Must be one of %s", outcome, VALID_OUTCOMES
84
+ )
85
+ outcome = "partial"
86
+
87
+ ao = ActionOutcome(
88
+ profile_id=profile_id,
89
+ query=query,
90
+ fact_ids=list(fact_ids),
91
+ outcome=outcome,
92
+ context=dict(context) if context else {},
93
+ )
94
+
95
+ self._db.execute(
96
+ "INSERT OR REPLACE INTO action_outcomes "
97
+ "(outcome_id, profile_id, query, fact_ids_json, outcome, "
98
+ " context_json, timestamp) "
99
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
100
+ (
101
+ ao.outcome_id,
102
+ ao.profile_id,
103
+ ao.query,
104
+ json.dumps(ao.fact_ids),
105
+ ao.outcome,
106
+ json.dumps(ao.context),
107
+ ao.timestamp,
108
+ ),
109
+ )
110
+ return ao
111
+
112
+ # ------------------------------------------------------------------
113
+ # Public API — Querying
114
+ # ------------------------------------------------------------------
115
+
116
+ def get_outcomes(
117
+ self,
118
+ profile_id: str,
119
+ limit: int = 50,
120
+ outcome_filter: str | None = None,
121
+ ) -> list[ActionOutcome]:
122
+ """Get recent outcomes for a profile.
123
+
124
+ Returns:
125
+ List of ``ActionOutcome`` objects, newest first.
126
+ """
127
+ sql = "SELECT * FROM action_outcomes WHERE profile_id = ?"
128
+ params: list[Any] = [profile_id]
129
+
130
+ if outcome_filter and outcome_filter in VALID_OUTCOMES:
131
+ sql += " AND outcome = ?"
132
+ params.append(outcome_filter)
133
+
134
+ sql += " ORDER BY timestamp DESC LIMIT ?"
135
+ params.append(limit)
136
+
137
+ rows = self._db.execute(sql, tuple(params))
138
+ return [self._row_to_outcome(r) for r in rows]
139
+
140
+ def get_success_rate(self, profile_id: str) -> float:
141
+ """Overall success rate. ``success`` = 1.0, ``partial`` = 0.5."""
142
+ rows = self._db.execute(
143
+ "SELECT outcome, COUNT(*) AS cnt FROM action_outcomes "
144
+ "WHERE profile_id = ? GROUP BY outcome",
145
+ (profile_id,),
146
+ )
147
+ if not rows:
148
+ return 0.0
149
+
150
+ counts = {dict(r)["outcome"]: dict(r)["cnt"] for r in rows}
151
+ total = sum(counts.values())
152
+ if total == 0:
153
+ return 0.0
154
+
155
+ success = counts.get("success", 0)
156
+ partial = counts.get("partial", 0) * 0.5
157
+ return round((success + partial) / total, 4)
158
+
159
+ def get_fact_success_rate(self, fact_id: str, profile_id: str) -> float:
160
+ """How often a specific fact led to successful outcomes.
161
+
162
+ Returns 0.5 (neutral) if no relevant data exists.
163
+ """
164
+ outcomes = self.get_outcomes(profile_id, limit=500)
165
+ relevant = [o for o in outcomes if fact_id in o.fact_ids]
166
+
167
+ if not relevant:
168
+ return 0.5
169
+
170
+ successes = sum(1 for o in relevant if o.outcome == "success")
171
+ return round(successes / len(relevant), 4)
172
+
173
+ # ------------------------------------------------------------------
174
+ # Public API — Inference
175
+ # ------------------------------------------------------------------
176
+
177
+ def infer_outcome(
178
+ self,
179
+ profile_id: str,
180
+ fact_ids: list[str],
181
+ signals: dict[str, Any],
182
+ ) -> str:
183
+ """Infer outcome from behavioral signals and auto-record it."""
184
+ success_score = 0.0
185
+ failure_score = 0.0
186
+ matched_signals: list[str] = []
187
+
188
+ for signal_name, value in signals.items():
189
+ if not value:
190
+ continue
191
+ if signal_name in _SIGNAL_WEIGHTS:
192
+ outcome_label, weight = _SIGNAL_WEIGHTS[signal_name]
193
+ matched_signals.append(signal_name)
194
+ if outcome_label == "success":
195
+ success_score += weight
196
+ elif outcome_label == "failure":
197
+ failure_score += weight
198
+ else:
199
+ success_score += weight * 0.5
200
+ failure_score += weight * 0.5
201
+
202
+ if success_score > failure_score:
203
+ inferred = "success"
204
+ elif failure_score > success_score:
205
+ inferred = "failure"
206
+ else:
207
+ inferred = "partial"
208
+
209
+ self.record_outcome(
210
+ query="[inferred]",
211
+ fact_ids=fact_ids,
212
+ outcome=inferred,
213
+ profile_id=profile_id,
214
+ context={
215
+ "signals": matched_signals,
216
+ "success_score": round(success_score, 3),
217
+ "failure_score": round(failure_score, 3),
218
+ },
219
+ )
220
+ return inferred
221
+
222
+ # ------------------------------------------------------------------
223
+ # Public API — Maintenance
224
+ # ------------------------------------------------------------------
225
+
226
+ def delete_outcomes(self, profile_id: str) -> int:
227
+ """Delete all outcomes for a profile. Returns count deleted."""
228
+ rows = self._db.execute(
229
+ "SELECT COUNT(*) AS c FROM action_outcomes WHERE profile_id = ?",
230
+ (profile_id,),
231
+ )
232
+ count = int(dict(rows[0])["c"]) if rows else 0
233
+ self._db.execute(
234
+ "DELETE FROM action_outcomes WHERE profile_id = ?",
235
+ (profile_id,),
236
+ )
237
+ return count
238
+
239
+ # ------------------------------------------------------------------
240
+ # Internal helpers
241
+ # ------------------------------------------------------------------
242
+
243
+ @staticmethod
244
+ def _row_to_outcome(row) -> ActionOutcome:
245
+ """Convert a DB row to ActionOutcome."""
246
+ d = dict(row)
247
+ return ActionOutcome(
248
+ outcome_id=d["outcome_id"],
249
+ profile_id=d["profile_id"],
250
+ query=d.get("query", ""),
251
+ fact_ids=json.loads(d.get("fact_ids_json", "[]")),
252
+ outcome=d.get("outcome", ""),
253
+ context=json.loads(d.get("context_json", "{}")),
254
+ timestamp=d.get("timestamp", ""),
255
+ )