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,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
+ )