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,298 @@
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
+ """Synthetic bootstrap for cold-start ML ranking.
6
+
7
+ Generates (query, fact, label) triples from existing memory patterns
8
+ so the adaptive ranker can operate before real feedback accumulates.
9
+
10
+ Four strategies:
11
+ 1. Access-based -- frequently accessed memories are positive for their keywords
12
+ 2. Importance-based -- high-importance memories are positive for their tags
13
+ 3. Pattern-based -- learned identity patterns generate synthetic queries
14
+ 4. Recency-based -- recent memories rank higher for shared-topic queries
15
+
16
+ Ported from V2 SyntheticBootstrapper with V2-specific deps removed.
17
+
18
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import hashlib
24
+ import json
25
+ import logging
26
+ import re
27
+ import sqlite3
28
+ from collections import Counter
29
+ from pathlib import Path
30
+ from typing import Any, Optional
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Stopwords for keyword extraction (no NLP dep)
36
+ # ---------------------------------------------------------------------------
37
+ _STOPWORDS = frozenset({
38
+ "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
39
+ "have", "has", "had", "do", "does", "did", "will", "would", "shall",
40
+ "should", "may", "might", "can", "could", "to", "of", "in", "for",
41
+ "on", "with", "at", "by", "from", "as", "into", "through", "during",
42
+ "before", "after", "above", "below", "between", "under", "again",
43
+ "further", "then", "once", "here", "there", "when", "where", "why",
44
+ "how", "all", "each", "every", "both", "few", "more", "most", "other",
45
+ "some", "such", "no", "not", "only", "own", "same", "so", "than",
46
+ "too", "very", "just", "because", "but", "and", "or", "if", "while",
47
+ "about", "up", "out", "off", "over", "also", "it", "its", "this",
48
+ "that", "i", "me", "my", "we", "our", "you", "your", "he", "him",
49
+ "she", "her", "they", "them", "what", "which", "who", "whom",
50
+ })
51
+
52
+ _WORD_RE = re.compile(r"[a-zA-Z0-9_]+")
53
+
54
+
55
+ class SyntheticBootstrap:
56
+ """Generate synthetic training data for cold-start ML ranking.
57
+
58
+ Args:
59
+ db_path: Path to the sqlite database containing a ``memories`` table.
60
+ A fresh database is created if the file does not exist.
61
+ """
62
+
63
+ def __init__(self, db_path: Path | str) -> None:
64
+ self._db_path = Path(db_path)
65
+ self._ensure_schema()
66
+
67
+ # ------------------------------------------------------------------
68
+ # Public API
69
+ # ------------------------------------------------------------------
70
+
71
+ def generate(self, profile_id: str, count: int = 200) -> list[dict]:
72
+ """Generate synthetic (query, fact_id, label) triples.
73
+
74
+ Returns up to *count* records. Returns an empty list when
75
+ the profile has no memories to mine from.
76
+ """
77
+ memories = self._fetch_memories(profile_id)
78
+ if not memories:
79
+ return []
80
+
81
+ records: list[dict] = []
82
+ records.extend(self._access_based(memories))
83
+ records.extend(self._importance_based(memories))
84
+ records.extend(self._recency_based(memories))
85
+
86
+ # Trim to target count with source diversity
87
+ if len(records) > count:
88
+ records = self._diverse_sample(records, count)
89
+
90
+ return records
91
+
92
+ # ------------------------------------------------------------------
93
+ # Strategy 1: access-based
94
+ # ------------------------------------------------------------------
95
+
96
+ def _access_based(self, memories: list[dict]) -> list[dict]:
97
+ """Memories accessed 5+ times are positive for their keywords."""
98
+ records: list[dict] = []
99
+ high_access = [m for m in memories if m.get("access_count", 0) >= 5]
100
+
101
+ for mem in high_access:
102
+ keywords = _extract_keywords(mem.get("content", ""))
103
+ if not keywords:
104
+ continue
105
+ query = " ".join(keywords)
106
+
107
+ records.append(_build_record(query, mem, label=1.0, source="access_pos"))
108
+
109
+ # Pick negatives from memories with different tags
110
+ for neg in self._find_negatives(memories, mem, limit=2):
111
+ records.append(_build_record(query, neg, label=0.0, source="access_neg"))
112
+
113
+ return records
114
+
115
+ # ------------------------------------------------------------------
116
+ # Strategy 2: importance-based
117
+ # ------------------------------------------------------------------
118
+
119
+ def _importance_based(self, memories: list[dict]) -> list[dict]:
120
+ """High-importance memories (>=8) are positive for their tags."""
121
+ records: list[dict] = []
122
+ important = [m for m in memories if (m.get("importance") or 0) >= 8]
123
+
124
+ for mem in important:
125
+ query = self._tags_to_query(mem)
126
+ if not query:
127
+ keywords = _extract_keywords(mem.get("content", ""))
128
+ query = " ".join(keywords) if keywords else ""
129
+ if not query:
130
+ continue
131
+
132
+ records.append(_build_record(query, mem, label=1.0, source="importance_pos"))
133
+
134
+ for neg in self._find_negatives(memories, mem, limit=2):
135
+ records.append(_build_record(query, neg, label=0.0, source="importance_neg"))
136
+
137
+ return records
138
+
139
+ # ------------------------------------------------------------------
140
+ # Strategy 3: recency-based
141
+ # ------------------------------------------------------------------
142
+
143
+ def _recency_based(self, memories: list[dict]) -> list[dict]:
144
+ """Recent memories rank higher for shared-topic queries."""
145
+ records: list[dict] = []
146
+ recent = memories[:30] # Already sorted by created_at DESC
147
+ if len(recent) < 4:
148
+ return records
149
+
150
+ seen_queries: set[str] = set()
151
+ for mem in recent[:15]:
152
+ keywords = _extract_keywords(mem.get("content", ""))
153
+ query = " ".join(keywords) if keywords else ""
154
+ if not query or query in seen_queries:
155
+ continue
156
+ seen_queries.add(query)
157
+
158
+ records.append(_build_record(query, mem, label=0.8, source="recency_pos"))
159
+
160
+ # Older memories about the same topic are weak negatives
161
+ for older in memories[30:]:
162
+ content = (older.get("content") or "").lower()
163
+ if any(kw in content for kw in keywords):
164
+ records.append(
165
+ _build_record(query, older, label=0.3, source="recency_neg")
166
+ )
167
+ break
168
+
169
+ return records
170
+
171
+ # ------------------------------------------------------------------
172
+ # Database
173
+ # ------------------------------------------------------------------
174
+
175
+ def _ensure_schema(self) -> None:
176
+ """Create the memories table if it does not exist (for testing)."""
177
+ conn = sqlite3.connect(str(self._db_path))
178
+ try:
179
+ conn.execute(
180
+ """CREATE TABLE IF NOT EXISTS memories (
181
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
182
+ profile_id TEXT DEFAULT 'default',
183
+ content TEXT,
184
+ tags TEXT,
185
+ importance INTEGER DEFAULT 5,
186
+ access_count INTEGER DEFAULT 0,
187
+ created_at TEXT
188
+ )"""
189
+ )
190
+ conn.commit()
191
+ finally:
192
+ conn.close()
193
+
194
+ def _fetch_memories(self, profile_id: str) -> list[dict]:
195
+ """Fetch memories for a profile, ordered by recency."""
196
+ conn = sqlite3.connect(str(self._db_path))
197
+ conn.row_factory = sqlite3.Row
198
+ try:
199
+ cur = conn.execute(
200
+ "SELECT * FROM memories WHERE profile_id = ? "
201
+ "ORDER BY created_at DESC LIMIT 500",
202
+ (profile_id,),
203
+ )
204
+ return [dict(r) for r in cur.fetchall()]
205
+ except sqlite3.OperationalError:
206
+ return []
207
+ finally:
208
+ conn.close()
209
+
210
+ # ------------------------------------------------------------------
211
+ # Helpers
212
+ # ------------------------------------------------------------------
213
+
214
+ @staticmethod
215
+ def _tags_to_query(memory: dict) -> str:
216
+ """Extract a query string from the memory's tags field."""
217
+ tags = memory.get("tags", "")
218
+ if not tags:
219
+ return ""
220
+ if isinstance(tags, list):
221
+ return " ".join(tags[:5])
222
+ try:
223
+ parsed = json.loads(tags)
224
+ if isinstance(parsed, list):
225
+ return " ".join(str(t) for t in parsed[:5])
226
+ except (json.JSONDecodeError, TypeError):
227
+ pass
228
+ return " ".join(t.strip() for t in str(tags).split(",") if t.strip())[:5]
229
+
230
+ @staticmethod
231
+ def _find_negatives(
232
+ memories: list[dict], anchor: dict, limit: int = 2
233
+ ) -> list[dict]:
234
+ """Pick memories dissimilar to anchor (different tags or id)."""
235
+ anchor_id = anchor.get("id")
236
+ anchor_tags = set(
237
+ t.strip().lower()
238
+ for t in str(anchor.get("tags", "")).split(",")
239
+ if t.strip()
240
+ )
241
+ negatives: list[dict] = []
242
+ for mem in memories:
243
+ if mem.get("id") == anchor_id:
244
+ continue
245
+ mem_tags = set(
246
+ t.strip().lower()
247
+ for t in str(mem.get("tags", "")).split(",")
248
+ if t.strip()
249
+ )
250
+ if not mem_tags & anchor_tags:
251
+ negatives.append(mem)
252
+ if len(negatives) >= limit:
253
+ break
254
+ return negatives
255
+
256
+ @staticmethod
257
+ def _diverse_sample(records: list[dict], target: int) -> list[dict]:
258
+ """Sample records proportionally across sources."""
259
+ by_source: dict[str, list[dict]] = {}
260
+ for r in records:
261
+ by_source.setdefault(r["source"], []).append(r)
262
+ n_sources = len(by_source) or 1
263
+ per_source = max(1, target // n_sources)
264
+ sampled: list[dict] = []
265
+ for src_records in by_source.values():
266
+ sampled.extend(src_records[:per_source])
267
+ return sampled[:target]
268
+
269
+
270
+ # ----------------------------------------------------------------------
271
+ # Module-level helpers (stateless)
272
+ # ----------------------------------------------------------------------
273
+
274
+
275
+ def _extract_keywords(content: str, top_n: int = 3) -> list[str]:
276
+ """Extract top-N keywords from text via frequency (no NLP deps)."""
277
+ if not content:
278
+ return []
279
+ tokens = _WORD_RE.findall(content.lower())
280
+ filtered = [t for t in tokens if t not in _STOPWORDS and len(t) > 2]
281
+ counts = Counter(filtered)
282
+ return [word for word, _ in counts.most_common(top_n)]
283
+
284
+
285
+ def _build_record(
286
+ query: str,
287
+ memory: dict,
288
+ label: float,
289
+ source: str,
290
+ ) -> dict:
291
+ """Build a single training record."""
292
+ return {
293
+ "query": query,
294
+ "query_hash": hashlib.sha256(query.encode()).hexdigest()[:16],
295
+ "fact_id": memory.get("id", 0),
296
+ "label": label,
297
+ "source": source,
298
+ }
@@ -0,0 +1,399 @@
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
+ """Cross-project knowledge aggregator.
6
+
7
+ Transfers learned preferences and patterns from completed projects to
8
+ new ones via temporal-decay merging and contradiction detection.
9
+ Ported from V2 CrossProjectAggregator with inline sqlite3 queries.
10
+
11
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import math
19
+ import re
20
+ import sqlite3
21
+ from collections import Counter
22
+ from datetime import UTC, datetime
23
+ from pathlib import Path
24
+ from typing import Any, Optional
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Temporal decay half-life in days (1 year)
29
+ DECAY_HALF_LIFE_DAYS = 365.0
30
+
31
+ # Contradiction detection window in days
32
+ CONTRADICTION_WINDOW_DAYS = 90
33
+
34
+ # Minimum evidence to consider a pattern valid
35
+ MIN_EVIDENCE_FOR_MERGE = 2
36
+
37
+ # Minimum confidence for a merged pattern to be stored
38
+ MIN_MERGE_CONFIDENCE = 0.3
39
+
40
+ # Simple keyword regex for frequency analysis
41
+ _WORD_RE = re.compile(r"[a-zA-Z0-9_]+")
42
+
43
+ # Technology categories detected from content
44
+ TECH_CATEGORIES: dict[str, list[str]] = {
45
+ "frontend_framework": [
46
+ "react", "vue", "angular", "svelte", "next", "nuxt", "solid",
47
+ ],
48
+ "backend_framework": [
49
+ "express", "fastapi", "django", "flask", "spring", "rails", "nest",
50
+ ],
51
+ "language": [
52
+ "python", "typescript", "javascript", "rust", "go", "java", "kotlin",
53
+ ],
54
+ "database": [
55
+ "postgres", "mysql", "sqlite", "mongo", "redis", "dynamodb", "supabase",
56
+ ],
57
+ "cloud": ["aws", "azure", "gcp", "vercel", "cloudflare", "netlify"],
58
+ "testing": ["pytest", "jest", "vitest", "cypress", "playwright"],
59
+ }
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Schema for transferable patterns
63
+ # ---------------------------------------------------------------------------
64
+
65
+ _SCHEMA = """
66
+ CREATE TABLE IF NOT EXISTS transferable_patterns (
67
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ pattern_type TEXT NOT NULL DEFAULT 'preference',
69
+ key TEXT NOT NULL,
70
+ value TEXT NOT NULL,
71
+ confidence REAL DEFAULT 0.0,
72
+ evidence_count INTEGER DEFAULT 0,
73
+ profiles_seen INTEGER DEFAULT 1,
74
+ decay_factor REAL DEFAULT 1.0,
75
+ contradictions TEXT DEFAULT '[]',
76
+ first_seen TEXT,
77
+ last_seen TEXT,
78
+ UNIQUE(pattern_type, key)
79
+ );
80
+
81
+ CREATE TABLE IF NOT EXISTS memories (
82
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
83
+ profile_id TEXT DEFAULT 'default',
84
+ content TEXT,
85
+ project_name TEXT,
86
+ created_at TEXT
87
+ );
88
+ """
89
+
90
+
91
+ class CrossProjectAggregator:
92
+ """Aggregate technology preferences across profiles."""
93
+
94
+ def __init__(self, db_path: Path | str) -> None:
95
+ self._db_path = Path(db_path)
96
+ self._ensure_schema()
97
+
98
+ def aggregate(
99
+ self, source_profiles: list[str], target_profile: str
100
+ ) -> list[dict]:
101
+ """Transfer patterns from *source_profiles* to *target_profile*."""
102
+ if not source_profiles:
103
+ return []
104
+
105
+ # Step 1: Analyse each source profile
106
+ profile_patterns: list[dict] = []
107
+ for profile_id in source_profiles:
108
+ pdata = self._analyse_profile(profile_id)
109
+ if pdata:
110
+ profile_patterns.append(pdata)
111
+
112
+ if not profile_patterns:
113
+ return []
114
+
115
+ # Step 2: Merge with temporal decay
116
+ merged = self._merge_with_decay(profile_patterns)
117
+
118
+ # Step 3: Detect contradictions
119
+ for key, pattern_data in merged.items():
120
+ pattern_data["contradictions"] = self._detect_contradictions(
121
+ key, pattern_data
122
+ )
123
+
124
+ # Step 4: Store and return
125
+ self._store_patterns(merged)
126
+ return [{"key": k, **v} for k, v in merged.items()]
127
+
128
+ def get_preferences(self, min_confidence: float = 0.6) -> dict[str, dict]:
129
+ """Retrieve stored transferable preferences above *min_confidence*."""
130
+ conn = sqlite3.connect(str(self._db_path))
131
+ conn.row_factory = sqlite3.Row
132
+ try:
133
+ cur = conn.execute(
134
+ "SELECT * FROM transferable_patterns "
135
+ "WHERE confidence >= ? ORDER BY confidence DESC",
136
+ (min_confidence,),
137
+ )
138
+ result: dict[str, dict] = {}
139
+ for row in cur.fetchall():
140
+ d = dict(row)
141
+ contradictions = _parse_json_list(d.get("contradictions", "[]"))
142
+ result[d["key"]] = {
143
+ "value": d["value"],
144
+ "confidence": d["confidence"],
145
+ "evidence_count": d["evidence_count"],
146
+ "profiles_seen": d.get("profiles_seen", 1),
147
+ "decay_factor": d.get("decay_factor", 1.0),
148
+ "contradictions": contradictions,
149
+ }
150
+ return result
151
+ except sqlite3.OperationalError:
152
+ return {}
153
+ finally:
154
+ conn.close()
155
+
156
+ def _analyse_profile(self, profile_id: str) -> Optional[dict]:
157
+ """Detect tech category preferences in a single profile."""
158
+ conn = sqlite3.connect(str(self._db_path))
159
+ conn.row_factory = sqlite3.Row
160
+ try:
161
+ cur = conn.execute(
162
+ "SELECT content, created_at FROM memories "
163
+ "WHERE profile_id = ? ORDER BY created_at DESC LIMIT 500",
164
+ (profile_id,),
165
+ )
166
+ rows = cur.fetchall()
167
+ except sqlite3.OperationalError:
168
+ return None
169
+ finally:
170
+ conn.close()
171
+
172
+ if not rows:
173
+ return None
174
+
175
+ # Detect tech keywords per category
176
+ category_counts: dict[str, Counter] = {
177
+ cat: Counter() for cat in TECH_CATEGORIES
178
+ }
179
+ for row in rows:
180
+ content = (dict(row).get("content") or "").lower()
181
+ tokens = set(_WORD_RE.findall(content))
182
+ for category, keywords in TECH_CATEGORIES.items():
183
+ for kw in keywords:
184
+ if kw in tokens:
185
+ category_counts[category][kw] += 1
186
+
187
+ # Build patterns: winner per category with enough evidence
188
+ patterns: dict[str, dict] = {}
189
+ for category, counter in category_counts.items():
190
+ if not counter:
191
+ continue
192
+ winner, win_count = counter.most_common(1)[0]
193
+ total = sum(counter.values())
194
+ if win_count >= MIN_EVIDENCE_FOR_MERGE:
195
+ patterns[category] = {
196
+ "value": winner,
197
+ "confidence": round(win_count / total, 3) if total else 0.0,
198
+ "evidence_count": win_count,
199
+ }
200
+
201
+ if not patterns:
202
+ return None
203
+
204
+ # Latest timestamp for decay calculation
205
+ latest_ts = dict(rows[0]).get("created_at", datetime.now(UTC).isoformat())
206
+
207
+ return {
208
+ "profile": profile_id,
209
+ "patterns": patterns,
210
+ "latest_timestamp": latest_ts,
211
+ "memory_count": len(rows),
212
+ }
213
+
214
+ def _merge_with_decay(
215
+ self, profile_patterns: list[dict]
216
+ ) -> dict[str, dict]:
217
+ now = datetime.now(UTC)
218
+ contributions: dict[str, list[dict]] = {}
219
+
220
+ for pdata in profile_patterns:
221
+ age_days = _days_since(pdata["latest_timestamp"], now)
222
+ weight = math.exp(-age_days / DECAY_HALF_LIFE_DAYS)
223
+
224
+ for cat_key, pattern in pdata["patterns"].items():
225
+ contributions.setdefault(cat_key, []).append({
226
+ "value": pattern["value"],
227
+ "confidence": pattern["confidence"],
228
+ "evidence_count": pattern["evidence_count"],
229
+ "weight": weight,
230
+ "profile": pdata["profile"],
231
+ "latest_timestamp": pdata["latest_timestamp"],
232
+ })
233
+
234
+ merged: dict[str, dict] = {}
235
+ for cat_key, contribs in contributions.items():
236
+ result = self._merge_single(contribs)
237
+ if result is not None:
238
+ merged[cat_key] = result
239
+ return merged
240
+
241
+ @staticmethod
242
+ def _merge_single(contributions: list[dict]) -> Optional[dict]:
243
+ """Merge contributions for one category across profiles."""
244
+ value_scores: dict[str, float] = {}
245
+ value_evidence: dict[str, int] = {}
246
+ value_profiles: dict[str, set] = {}
247
+ total_weighted = 0.0
248
+
249
+ for c in contributions:
250
+ v = c["value"]
251
+ w_ev = c["evidence_count"] * c["weight"]
252
+ value_scores[v] = value_scores.get(v, 0.0) + w_ev
253
+ value_evidence[v] = value_evidence.get(v, 0) + c["evidence_count"]
254
+ value_profiles.setdefault(v, set()).add(c["profile"])
255
+ total_weighted += w_ev
256
+
257
+ if total_weighted == 0:
258
+ return None
259
+
260
+ winner = max(value_scores, key=lambda k: value_scores[k])
261
+ confidence = value_scores[winner] / total_weighted
262
+ ev = value_evidence[winner]
263
+
264
+ if ev < MIN_EVIDENCE_FOR_MERGE or confidence < MIN_MERGE_CONFIDENCE:
265
+ return None
266
+
267
+ return {
268
+ "value": winner,
269
+ "confidence": round(min(0.95, confidence), 3),
270
+ "evidence_count": ev,
271
+ "profiles_seen": len(value_profiles[winner]),
272
+ "decay_factor": round(
273
+ max(c["weight"] for c in contributions if c["value"] == winner),
274
+ 4,
275
+ ),
276
+ "contradictions": [],
277
+ }
278
+
279
+ def _detect_contradictions(
280
+ self, pattern_key: str, pattern_data: dict
281
+ ) -> list[str]:
282
+ contradictions: list[str] = []
283
+
284
+ # Check stored value vs new value
285
+ conn = sqlite3.connect(str(self._db_path))
286
+ conn.row_factory = sqlite3.Row
287
+ try:
288
+ cur = conn.execute(
289
+ "SELECT value, last_seen FROM transferable_patterns "
290
+ "WHERE key = ? AND pattern_type = 'preference'",
291
+ (pattern_key,),
292
+ )
293
+ row = cur.fetchone()
294
+ if row:
295
+ d = dict(row)
296
+ old_val = d.get("value", "")
297
+ if old_val and old_val != pattern_data["value"]:
298
+ old_ts = d.get("last_seen", "")
299
+ if old_ts and _is_within_window(old_ts, CONTRADICTION_WINDOW_DAYS):
300
+ contradictions.append(
301
+ f"Changed from '{old_val}' to "
302
+ f"'{pattern_data['value']}' within "
303
+ f"{CONTRADICTION_WINDOW_DAYS} days"
304
+ )
305
+ except sqlite3.OperationalError:
306
+ pass
307
+ finally:
308
+ conn.close()
309
+
310
+ return contradictions
311
+
312
+ def _ensure_schema(self) -> None:
313
+ conn = sqlite3.connect(str(self._db_path))
314
+ try:
315
+ conn.executescript(_SCHEMA)
316
+ finally:
317
+ conn.close()
318
+
319
+ def _store_patterns(self, merged: dict[str, dict]) -> None:
320
+ conn = sqlite3.connect(str(self._db_path))
321
+ now = datetime.now(UTC).isoformat()
322
+ try:
323
+ for key, data in merged.items():
324
+ conn.execute(
325
+ """INSERT INTO transferable_patterns
326
+ (pattern_type, key, value, confidence, evidence_count,
327
+ profiles_seen, decay_factor, contradictions,
328
+ first_seen, last_seen)
329
+ VALUES ('preference', ?, ?, ?, ?, ?, ?, ?, ?, ?)
330
+ ON CONFLICT(pattern_type, key) DO UPDATE SET
331
+ value = excluded.value,
332
+ confidence = excluded.confidence,
333
+ evidence_count = excluded.evidence_count,
334
+ profiles_seen = excluded.profiles_seen,
335
+ decay_factor = excluded.decay_factor,
336
+ contradictions = excluded.contradictions,
337
+ last_seen = excluded.last_seen
338
+ """,
339
+ (
340
+ key,
341
+ data["value"],
342
+ data["confidence"],
343
+ data["evidence_count"],
344
+ data.get("profiles_seen", 1),
345
+ data.get("decay_factor", 1.0),
346
+ json.dumps(data.get("contradictions", [])),
347
+ now,
348
+ now,
349
+ ),
350
+ )
351
+ conn.commit()
352
+ finally:
353
+ conn.close()
354
+
355
+
356
+ # ----------------------------------------------------------------------
357
+ # Module-level helpers
358
+ # ----------------------------------------------------------------------
359
+
360
+
361
+ def _parse_json_list(raw: Any) -> list:
362
+ """Parse a JSON string into a list, returning [] on failure."""
363
+ if isinstance(raw, list):
364
+ return raw
365
+ if isinstance(raw, str):
366
+ try:
367
+ return json.loads(raw)
368
+ except (json.JSONDecodeError, TypeError):
369
+ pass
370
+ return []
371
+
372
+
373
+ def _days_since(timestamp_str: str, now: datetime | None = None) -> float:
374
+ if now is None:
375
+ now = datetime.now(UTC)
376
+ if not timestamp_str:
377
+ return 0.0
378
+ try:
379
+ ts = datetime.fromisoformat(str(timestamp_str).replace(" ", "T"))
380
+ return max(0.0, (now - ts).total_seconds() / 86400.0)
381
+ except (ValueError, AttributeError, TypeError):
382
+ pass
383
+ for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%Y-%m-%dT%H:%M:%S.%f"):
384
+ try:
385
+ ts = datetime.strptime(str(timestamp_str), fmt)
386
+ return max(0.0, (now - ts).total_seconds() / 86400.0)
387
+ except (ValueError, TypeError):
388
+ continue
389
+ return 0.0
390
+
391
+
392
+ def _is_within_window(timestamp_str: str, window_days: int) -> bool:
393
+ if not timestamp_str:
394
+ return False
395
+ try:
396
+ ts = datetime.fromisoformat(str(timestamp_str).replace(" ", "T"))
397
+ return (datetime.now(UTC) - ts).days <= window_days
398
+ except (ValueError, AttributeError, TypeError):
399
+ return False