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
@@ -1,857 +0,0 @@
1
- #!/usr/bin/env python3
2
- # SPDX-License-Identifier: MIT
3
- # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
- """
5
- CrossProjectAggregator — Layer 1: Transferable Tech Preferences.
6
-
7
- Aggregates technology preferences across ALL user profiles by wrapping
8
- the existing FrequencyAnalyzer from pattern_learner.py. This module
9
- does NOT replace pattern_learner.py — it sits on top, reading its
10
- per-profile results and merging them into cross-project patterns stored
11
- in learning.db's `transferable_patterns` table.
12
-
13
- Key behaviors:
14
- - Reads memories from memory.db across all profiles (READ-ONLY)
15
- - Wraps FrequencyAnalyzer.analyze_preferences() for per-profile analysis
16
- - Merges profile results with exponential temporal decay (1-year half-life)
17
- - Detects contradictions when preferences change across profiles or time
18
- - Stores merged patterns in learning.db via LearningDB.upsert_transferable_pattern()
19
-
20
- Temporal Decay:
21
- weight = exp(-age_days / 365)
22
- This gives a 1-year half-life: memories from 365 days ago contribute ~37%
23
- of their original weight. Recent profiles dominate, but old preferences
24
- are not forgotten unless contradicted.
25
-
26
- Contradiction Detection:
27
- If the preferred value for a category changed within the last 90 days
28
- (comparing the current top choice against previous stored value),
29
- a contradiction is logged. This signals preference evolution — not an
30
- error. The adaptive ranker can use contradictions to weight recent
31
- preferences higher.
32
-
33
- Research Backing:
34
- - MACLA (arXiv:2512.18950): Bayesian confidence with temporal priors
35
- - MemoryBank (AAAI 2024): Cross-session preference persistence
36
- - Pattern originally from pattern_learner.py Layer 4
37
-
38
- Thread Safety:
39
- Write operations to learning.db are protected by LearningDB's internal
40
- write lock. Read operations to memory.db use per-call connections (SQLite
41
- WAL mode supports concurrent reads).
42
- """
43
-
44
- import json
45
- import logging
46
- import math
47
- import sqlite3
48
- import sys
49
- import threading
50
- from datetime import datetime, timedelta
51
- from pathlib import Path
52
- from typing import Dict, List, Optional, Any, Tuple
53
-
54
- logger = logging.getLogger("superlocalmemory.learning.aggregator")
55
-
56
- # ---------------------------------------------------------------------------
57
- # Import FrequencyAnalyzer from pattern_learner.py (lives in ~/.claude-memory/)
58
- # ---------------------------------------------------------------------------
59
- MEMORY_DIR = Path.home() / ".claude-memory"
60
- DEFAULT_MEMORY_DB = MEMORY_DIR / "memory.db"
61
-
62
- if str(MEMORY_DIR) not in sys.path:
63
- sys.path.insert(0, str(MEMORY_DIR))
64
-
65
- try:
66
- from pattern_learner import FrequencyAnalyzer
67
- HAS_FREQ_ANALYZER = True
68
- except ImportError:
69
- HAS_FREQ_ANALYZER = False
70
- logger.warning(
71
- "FrequencyAnalyzer not available. "
72
- "Ensure pattern_learner.py is in %s",
73
- MEMORY_DIR,
74
- )
75
-
76
- # ---------------------------------------------------------------------------
77
- # Import LearningDB (sibling module in src/learning/)
78
- # ---------------------------------------------------------------------------
79
- try:
80
- from .learning_db import LearningDB
81
- except ImportError:
82
- try:
83
- from learning_db import LearningDB
84
- except ImportError:
85
- LearningDB = None
86
- logger.warning("LearningDB not available — aggregator results will not persist.")
87
-
88
- # ---------------------------------------------------------------------------
89
- # Constants
90
- # ---------------------------------------------------------------------------
91
-
92
- # Temporal decay half-life: 365 days (1 year)
93
- DECAY_HALF_LIFE_DAYS = 365.0
94
-
95
- # Contradiction detection window: 90 days
96
- CONTRADICTION_WINDOW_DAYS = 90
97
-
98
- # Minimum evidence to consider a pattern valid for merging
99
- MIN_EVIDENCE_FOR_MERGE = 2
100
-
101
- # Minimum confidence for a merged pattern to be stored
102
- MIN_MERGE_CONFIDENCE = 0.3
103
-
104
-
105
- class CrossProjectAggregator:
106
- """
107
- Aggregates tech preferences across all user profiles.
108
-
109
- Wraps FrequencyAnalyzer to analyze per-profile memories, then merges
110
- results with temporal decay into transferable patterns stored in
111
- learning.db.
112
-
113
- Usage:
114
- aggregator = CrossProjectAggregator()
115
- results = aggregator.aggregate_all_profiles()
116
- prefs = aggregator.get_tech_preferences(min_confidence=0.6)
117
- """
118
-
119
- def __init__(
120
- self,
121
- memory_db_path: Optional[Path] = None,
122
- learning_db: Optional[Any] = None,
123
- ):
124
- """
125
- Initialize the cross-project aggregator.
126
-
127
- Args:
128
- memory_db_path: Path to memory.db. Defaults to ~/.claude-memory/memory.db.
129
- This database is READ-ONLY from this module's perspective.
130
- learning_db: A LearningDB instance for storing results. If None, one is
131
- created using the default path.
132
- """
133
- self.memory_db_path = Path(memory_db_path) if memory_db_path else DEFAULT_MEMORY_DB
134
- self._lock = threading.Lock()
135
-
136
- # Initialize LearningDB for storing aggregated patterns
137
- if learning_db is not None:
138
- self._learning_db = learning_db
139
- elif LearningDB is not None:
140
- try:
141
- self._learning_db = LearningDB.get_instance()
142
- except Exception as e:
143
- logger.error("Failed to initialize LearningDB: %s", e)
144
- self._learning_db = None
145
- else:
146
- self._learning_db = None
147
-
148
- # Initialize FrequencyAnalyzer if available
149
- if HAS_FREQ_ANALYZER:
150
- self._analyzer = FrequencyAnalyzer(self.memory_db_path)
151
- else:
152
- self._analyzer = None
153
-
154
- logger.info(
155
- "CrossProjectAggregator initialized: memory_db=%s, "
156
- "freq_analyzer=%s, learning_db=%s",
157
- self.memory_db_path,
158
- "available" if self._analyzer else "unavailable",
159
- "available" if self._learning_db else "unavailable",
160
- )
161
-
162
- # ======================================================================
163
- # Core Aggregation
164
- # ======================================================================
165
-
166
- def aggregate_all_profiles(self) -> Dict[str, dict]:
167
- """
168
- Aggregate tech preferences across ALL profiles in memory.db.
169
-
170
- Workflow:
171
- 1. List all distinct profiles from memory.db
172
- 2. For each profile, collect memory IDs and timestamps
173
- 3. Run FrequencyAnalyzer.analyze_preferences() per profile
174
- 4. Merge results with exponential temporal decay
175
- 5. Detect contradictions against previously stored patterns
176
- 6. Store merged patterns in learning.db
177
-
178
- Returns:
179
- Dict mapping pattern_key -> {value, confidence, evidence_count,
180
- profiles_seen, contradictions, decay_factor}
181
- """
182
- if not self._analyzer:
183
- logger.warning("FrequencyAnalyzer unavailable — cannot aggregate.")
184
- return {}
185
-
186
- # Step 1: List all profiles and their memory data
187
- profile_data = self._get_all_profile_data()
188
- if not profile_data:
189
- logger.info("No profiles found in memory.db — nothing to aggregate.")
190
- return {}
191
-
192
- logger.info(
193
- "Aggregating preferences across %d profile(s): %s",
194
- len(profile_data),
195
- ", ".join(p["profile"] for p in profile_data),
196
- )
197
-
198
- # Step 2-3: Analyze each profile
199
- profile_patterns = []
200
- for pdata in profile_data:
201
- profile_name = pdata["profile"]
202
- memory_ids = pdata["memory_ids"]
203
-
204
- if not memory_ids:
205
- logger.debug("Profile '%s' has no memories — skipping.", profile_name)
206
- continue
207
-
208
- try:
209
- patterns = self._analyzer.analyze_preferences(memory_ids)
210
- if patterns:
211
- profile_patterns.append({
212
- "profile": profile_name,
213
- "patterns": patterns,
214
- "latest_timestamp": pdata["latest_timestamp"],
215
- "memory_count": len(memory_ids),
216
- })
217
- logger.debug(
218
- "Profile '%s': %d patterns from %d memories",
219
- profile_name, len(patterns), len(memory_ids),
220
- )
221
- except Exception as e:
222
- logger.error(
223
- "Failed to analyze profile '%s': %s",
224
- profile_name, e,
225
- )
226
- continue
227
-
228
- if not profile_patterns:
229
- logger.info("No patterns found across any profile.")
230
- return {}
231
-
232
- # Step 4: Merge with temporal decay
233
- merged = self._merge_with_decay(profile_patterns)
234
-
235
- # Step 5: Detect contradictions
236
- for key, pattern_data in merged.items():
237
- contradictions = self._detect_contradictions(key, pattern_data)
238
- pattern_data["contradictions"] = contradictions
239
-
240
- # Step 6: Store in learning.db
241
- self._store_merged_patterns(merged)
242
-
243
- logger.info(
244
- "Aggregation complete: %d transferable patterns stored.",
245
- len(merged),
246
- )
247
- return merged
248
-
249
- # ======================================================================
250
- # Profile Data Extraction (READ-ONLY on memory.db)
251
- # ======================================================================
252
-
253
- def _get_all_profile_data(self) -> List[dict]:
254
- """
255
- Get all profiles and their memory IDs from memory.db.
256
-
257
- Returns list of {profile, memory_ids, latest_timestamp, memory_count}.
258
- """
259
- results = []
260
-
261
- try:
262
- conn = sqlite3.connect(str(self.memory_db_path), timeout=10)
263
- try:
264
- conn.execute("PRAGMA busy_timeout=5000")
265
- cursor = conn.cursor()
266
-
267
- # Get distinct profiles
268
- cursor.execute(
269
- "SELECT DISTINCT profile FROM memories "
270
- "WHERE profile IS NOT NULL ORDER BY profile"
271
- )
272
- profiles = [row[0] for row in cursor.fetchall()]
273
-
274
- if not profiles:
275
- # Fallback: if no profile column or all NULL, treat as 'default'
276
- cursor.execute("SELECT id FROM memories ORDER BY created_at")
277
- all_ids = [row[0] for row in cursor.fetchall()]
278
- if all_ids:
279
- # Get the latest timestamp
280
- cursor.execute(
281
- "SELECT MAX(created_at) FROM memories"
282
- )
283
- latest = cursor.fetchone()[0] or datetime.now().isoformat()
284
- results.append({
285
- "profile": "default",
286
- "memory_ids": all_ids,
287
- "latest_timestamp": latest,
288
- })
289
-
290
- # For each profile, get memory IDs and latest timestamp
291
- for profile in profiles:
292
- cursor.execute(
293
- "SELECT id FROM memories WHERE profile = ? ORDER BY created_at",
294
- (profile,),
295
- )
296
- memory_ids = [row[0] for row in cursor.fetchall()]
297
-
298
- cursor.execute(
299
- "SELECT MAX(created_at) FROM memories WHERE profile = ?",
300
- (profile,),
301
- )
302
- latest = cursor.fetchone()[0] or datetime.now().isoformat()
303
-
304
- if memory_ids:
305
- results.append({
306
- "profile": profile,
307
- "memory_ids": memory_ids,
308
- "latest_timestamp": latest,
309
- })
310
- finally:
311
- conn.close()
312
-
313
- except sqlite3.OperationalError as e:
314
- # Handle case where 'profile' column doesn't exist
315
- logger.warning(
316
- "Could not query profiles from memory.db: %s. "
317
- "Falling back to all memories as 'default' profile.",
318
- e,
319
- )
320
- try:
321
- conn = sqlite3.connect(str(self.memory_db_path), timeout=10)
322
- try:
323
- cursor = conn.cursor()
324
- cursor.execute("SELECT id FROM memories ORDER BY created_at")
325
- all_ids = [row[0] for row in cursor.fetchall()]
326
- if all_ids:
327
- cursor.execute("SELECT MAX(created_at) FROM memories")
328
- latest = cursor.fetchone()[0] or datetime.now().isoformat()
329
- results.append({
330
- "profile": "default",
331
- "memory_ids": all_ids,
332
- "latest_timestamp": latest,
333
- })
334
- finally:
335
- conn.close()
336
- except Exception as inner_e:
337
- logger.error("Failed to read memory.db: %s", inner_e)
338
-
339
- except Exception as e:
340
- logger.error("Unexpected error reading profiles: %s", e)
341
-
342
- return results
343
-
344
- # ======================================================================
345
- # Temporal Decay Merging
346
- # ======================================================================
347
-
348
- def _merge_with_decay(
349
- self,
350
- profile_patterns: List[dict],
351
- ) -> Dict[str, dict]:
352
- """
353
- Merge per-profile patterns with exponential temporal decay.
354
-
355
- Each profile's contribution is weighted by:
356
- weight = exp(-age_days / DECAY_HALF_LIFE_DAYS)
357
-
358
- where age_days is the number of days since the profile's most
359
- recent memory was created. This ensures recent profiles dominate
360
- while old preferences decay gracefully.
361
-
362
- Args:
363
- profile_patterns: List of {profile, patterns, latest_timestamp, memory_count}
364
-
365
- Returns:
366
- Dict[category_key, {value, confidence, evidence_count, profiles_seen,
367
- decay_factor, profile_history}]
368
- """
369
- now = datetime.now()
370
-
371
- # Collect all contributions per category key
372
- # key -> list of {value, confidence, evidence_count, weight, profile}
373
- contributions: Dict[str, List[dict]] = {}
374
-
375
- for pdata in profile_patterns:
376
- # Calculate temporal weight for this profile
377
- age_days = self._days_since(pdata["latest_timestamp"], now)
378
- weight = math.exp(-age_days / DECAY_HALF_LIFE_DAYS)
379
-
380
- for category_key, pattern in pdata["patterns"].items():
381
- if category_key not in contributions:
382
- contributions[category_key] = []
383
-
384
- contributions[category_key].append({
385
- "value": pattern.get("value", ""),
386
- "confidence": pattern.get("confidence", 0.0),
387
- "evidence_count": pattern.get("evidence_count", 0),
388
- "weight": weight,
389
- "profile": pdata["profile"],
390
- "latest_timestamp": pdata["latest_timestamp"],
391
- })
392
-
393
- # Merge contributions per category
394
- merged = {}
395
- for category_key, contribs in contributions.items():
396
- merged_pattern = self._merge_category_contributions(
397
- category_key, contribs
398
- )
399
- if merged_pattern is not None:
400
- merged[category_key] = merged_pattern
401
-
402
- return merged
403
-
404
- def _merge_category_contributions(
405
- self,
406
- category_key: str,
407
- contributions: List[dict],
408
- ) -> Optional[dict]:
409
- """
410
- Merge contributions for a single category across profiles.
411
-
412
- Strategy:
413
- 1. Group contributions by value (the preferred tech)
414
- 2. For each value, sum weighted evidence
415
- 3. The value with highest weighted evidence wins
416
- 4. Confidence = weighted_evidence / total_weighted_evidence
417
- """
418
- if not contributions:
419
- return None
420
-
421
- # Group by value
422
- value_scores: Dict[str, float] = {}
423
- value_evidence: Dict[str, int] = {}
424
- value_profiles: Dict[str, set] = {}
425
- value_weights: Dict[str, float] = {}
426
-
427
- total_weighted_evidence = 0.0
428
-
429
- for contrib in contributions:
430
- value = contrib["value"]
431
- weighted_ev = contrib["evidence_count"] * contrib["weight"]
432
-
433
- if value not in value_scores:
434
- value_scores[value] = 0.0
435
- value_evidence[value] = 0
436
- value_profiles[value] = set()
437
- value_weights[value] = 0.0
438
-
439
- value_scores[value] += weighted_ev
440
- value_evidence[value] += contrib["evidence_count"]
441
- value_profiles[value].add(contrib["profile"])
442
- value_weights[value] = max(value_weights[value], contrib["weight"])
443
- total_weighted_evidence += weighted_ev
444
-
445
- if total_weighted_evidence == 0:
446
- return None
447
-
448
- # Find the winning value
449
- winning_value = max(value_scores, key=value_scores.get)
450
- winning_score = value_scores[winning_value]
451
-
452
- # Calculate merged confidence
453
- confidence = winning_score / total_weighted_evidence if total_weighted_evidence > 0 else 0.0
454
-
455
- total_evidence = sum(value_evidence.values())
456
- winning_evidence = value_evidence[winning_value]
457
-
458
- if winning_evidence < MIN_EVIDENCE_FOR_MERGE:
459
- return None
460
-
461
- if confidence < MIN_MERGE_CONFIDENCE:
462
- return None
463
-
464
- # Average decay factor across contributing profiles for the winner
465
- winning_decay = value_weights[winning_value]
466
-
467
- # Build profile history for contradiction detection
468
- profile_history = []
469
- for contrib in contributions:
470
- profile_history.append({
471
- "profile": contrib["profile"],
472
- "value": contrib["value"],
473
- "confidence": round(contrib["confidence"], 3),
474
- "weight": round(contrib["weight"], 3),
475
- "timestamp": contrib["latest_timestamp"],
476
- })
477
-
478
- return {
479
- "value": winning_value,
480
- "confidence": round(min(0.95, confidence), 3),
481
- "evidence_count": winning_evidence,
482
- "profiles_seen": len(value_profiles[winning_value]),
483
- "total_profiles": len(set(c["profile"] for c in contributions)),
484
- "decay_factor": round(winning_decay, 4),
485
- "profile_history": profile_history,
486
- "contradictions": [], # Filled in by _detect_contradictions
487
- }
488
-
489
- # ======================================================================
490
- # Contradiction Detection
491
- # ======================================================================
492
-
493
- def _detect_contradictions(
494
- self,
495
- pattern_key: str,
496
- pattern_data: dict,
497
- ) -> List[str]:
498
- """
499
- Detect if the preferred value changed recently.
500
-
501
- A contradiction is logged when:
502
- 1. The current winning value differs from the previously stored value
503
- 2. The change happened within the last CONTRADICTION_WINDOW_DAYS
504
- 3. Multiple profiles disagree on the preferred value
505
-
506
- Contradictions are informational — they signal preference evolution,
507
- not errors. The adaptive ranker uses them to weight recent preferences.
508
-
509
- Args:
510
- pattern_key: Category key (e.g., 'frontend_framework')
511
- pattern_data: Merged pattern data with profile_history
512
-
513
- Returns:
514
- List of contradiction description strings.
515
- """
516
- contradictions = []
517
- current_value = pattern_data["value"]
518
-
519
- # Check 1: Cross-profile disagreement
520
- profile_history = pattern_data.get("profile_history", [])
521
- distinct_values = set(h["value"] for h in profile_history)
522
-
523
- if len(distinct_values) > 1:
524
- other_values = distinct_values - {current_value}
525
- for other_val in other_values:
526
- disagreeing_profiles = [
527
- h["profile"] for h in profile_history
528
- if h["value"] == other_val
529
- ]
530
- contradictions.append(
531
- "Profile(s) %s prefer '%s' instead of '%s'" % (
532
- ", ".join(disagreeing_profiles),
533
- other_val,
534
- current_value,
535
- )
536
- )
537
-
538
- # Check 2: Change from previously stored value (in learning.db)
539
- if self._learning_db is not None:
540
- try:
541
- stored = self._learning_db.get_transferable_patterns(
542
- min_confidence=0.0,
543
- pattern_type="preference",
544
- )
545
- for row in stored:
546
- if row.get("key") == pattern_key:
547
- old_value = row.get("value", "")
548
- old_updated = row.get("updated_at") or row.get("last_seen")
549
- if old_value and old_value != current_value:
550
- # Check if the old pattern was updated recently
551
- if old_updated and self._is_within_window(
552
- old_updated, CONTRADICTION_WINDOW_DAYS
553
- ):
554
- contradictions.append(
555
- "Preference changed from '%s' to '%s' "
556
- "within last %d days" % (
557
- old_value,
558
- current_value,
559
- CONTRADICTION_WINDOW_DAYS,
560
- )
561
- )
562
- break
563
- except Exception as e:
564
- logger.debug(
565
- "Could not check stored patterns for contradictions: %s", e
566
- )
567
-
568
- if contradictions:
569
- logger.info(
570
- "Contradictions for '%s': %s",
571
- pattern_key,
572
- "; ".join(contradictions),
573
- )
574
-
575
- return contradictions
576
-
577
- # ======================================================================
578
- # Storage (learning.db)
579
- # ======================================================================
580
-
581
- def _store_merged_patterns(self, merged: Dict[str, dict]):
582
- """
583
- Store merged patterns in learning.db's transferable_patterns table.
584
-
585
- Uses LearningDB.upsert_transferable_pattern() which handles
586
- INSERT ON CONFLICT UPDATE internally with its own write lock.
587
- """
588
- if self._learning_db is None:
589
- logger.warning(
590
- "LearningDB unavailable — %d patterns computed but not stored.",
591
- len(merged),
592
- )
593
- return
594
-
595
- stored_count = 0
596
- for key, data in merged.items():
597
- try:
598
- self._learning_db.upsert_transferable_pattern(
599
- pattern_type="preference",
600
- key=key,
601
- value=data["value"],
602
- confidence=data["confidence"],
603
- evidence_count=data["evidence_count"],
604
- profiles_seen=data.get("profiles_seen", 1),
605
- decay_factor=data.get("decay_factor", 1.0),
606
- contradictions=data.get("contradictions"),
607
- )
608
- stored_count += 1
609
- except Exception as e:
610
- logger.error(
611
- "Failed to store pattern '%s': %s", key, e
612
- )
613
-
614
- logger.info(
615
- "Stored %d/%d merged patterns in learning.db.",
616
- stored_count, len(merged),
617
- )
618
-
619
- # ======================================================================
620
- # Query Interface
621
- # ======================================================================
622
-
623
- def get_tech_preferences(
624
- self,
625
- min_confidence: float = 0.6,
626
- ) -> Dict[str, dict]:
627
- """
628
- Retrieve aggregated tech preferences from learning.db.
629
-
630
- This reads from the `transferable_patterns` table — the stored
631
- results of a previous aggregate_all_profiles() call.
632
-
633
- Args:
634
- min_confidence: Minimum confidence threshold (0.0 to 1.0).
635
- Default 0.6 matches FrequencyAnalyzer's threshold.
636
-
637
- Returns:
638
- Dict mapping category_key -> {value, confidence, evidence_count,
639
- profiles_seen, decay_factor, contradictions}
640
- """
641
- if self._learning_db is None:
642
- logger.warning("LearningDB unavailable — cannot read preferences.")
643
- return {}
644
-
645
- try:
646
- rows = self._learning_db.get_transferable_patterns(
647
- min_confidence=min_confidence,
648
- pattern_type="preference",
649
- )
650
-
651
- preferences = {}
652
- for row in rows:
653
- key = row.get("key", "")
654
- if not key:
655
- continue
656
-
657
- # Parse contradictions from JSON
658
- contradictions = []
659
- raw_contradictions = row.get("contradictions", "[]")
660
- if isinstance(raw_contradictions, str):
661
- try:
662
- contradictions = json.loads(raw_contradictions)
663
- except (json.JSONDecodeError, TypeError):
664
- contradictions = []
665
- elif isinstance(raw_contradictions, list):
666
- contradictions = raw_contradictions
667
-
668
- preferences[key] = {
669
- "value": row.get("value", ""),
670
- "confidence": row.get("confidence", 0.0),
671
- "evidence_count": row.get("evidence_count", 0),
672
- "profiles_seen": row.get("profiles_seen", 1),
673
- "decay_factor": row.get("decay_factor", 1.0),
674
- "contradictions": contradictions,
675
- "first_seen": row.get("first_seen"),
676
- "last_seen": row.get("last_seen"),
677
- }
678
-
679
- return preferences
680
-
681
- except Exception as e:
682
- logger.error("Failed to read tech preferences: %s", e)
683
- return {}
684
-
685
- def get_preference_context(self, min_confidence: float = 0.6) -> str:
686
- """
687
- Format transferable preferences for injection into AI context.
688
-
689
- Returns a human-readable markdown string suitable for CLAUDE.md
690
- or system prompt injection.
691
-
692
- Args:
693
- min_confidence: Minimum confidence threshold.
694
-
695
- Returns:
696
- Formatted markdown string.
697
- """
698
- prefs = self.get_tech_preferences(min_confidence)
699
-
700
- if not prefs:
701
- return (
702
- "## Cross-Project Tech Preferences\n\n"
703
- "No transferable preferences learned yet. "
704
- "Use more profiles and add memories to build your tech profile."
705
- )
706
-
707
- lines = ["## Cross-Project Tech Preferences\n"]
708
-
709
- for key, data in sorted(prefs.items(), key=lambda x: -x[1]["confidence"]):
710
- display_key = key.replace("_", " ").title()
711
- conf_pct = data["confidence"] * 100
712
- evidence = data["evidence_count"]
713
- profiles = data["profiles_seen"]
714
- line = (
715
- "- **%s:** %s (%.0f%% confidence, %d evidence, %d profile%s)"
716
- % (
717
- display_key,
718
- data["value"],
719
- conf_pct,
720
- evidence,
721
- profiles,
722
- "s" if profiles != 1 else "",
723
- )
724
- )
725
-
726
- # Flag contradictions
727
- if data.get("contradictions"):
728
- line += " [EVOLVING]"
729
-
730
- lines.append(line)
731
-
732
- return "\n".join(lines)
733
-
734
- # ======================================================================
735
- # Utility Methods
736
- # ======================================================================
737
-
738
- @staticmethod
739
- def _days_since(timestamp_str: str, now: Optional[datetime] = None) -> float:
740
- """
741
- Calculate days between a timestamp string and now.
742
-
743
- Handles multiple timestamp formats from SQLite (ISO 8601, space-separated).
744
- Returns 0.0 on parse failure (treat as recent).
745
- """
746
- if now is None:
747
- now = datetime.now()
748
-
749
- if not timestamp_str:
750
- return 0.0
751
-
752
- try:
753
- ts = datetime.fromisoformat(timestamp_str.replace(" ", "T"))
754
- delta = now - ts
755
- return max(0.0, delta.total_seconds() / 86400.0)
756
- except (ValueError, AttributeError, TypeError):
757
- pass
758
-
759
- # Fallback: try common formats
760
- for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%Y-%m-%dT%H:%M:%S.%f"):
761
- try:
762
- ts = datetime.strptime(str(timestamp_str), fmt)
763
- delta = now - ts
764
- return max(0.0, delta.total_seconds() / 86400.0)
765
- except (ValueError, TypeError):
766
- continue
767
-
768
- logger.debug("Could not parse timestamp: %s", timestamp_str)
769
- return 0.0
770
-
771
- @staticmethod
772
- def _is_within_window(timestamp_str: str, window_days: int) -> bool:
773
- """Check if a timestamp is within the given window (in days)."""
774
- if not timestamp_str:
775
- return False
776
- try:
777
- ts = datetime.fromisoformat(
778
- str(timestamp_str).replace(" ", "T")
779
- )
780
- return (datetime.now() - ts).days <= window_days
781
- except (ValueError, AttributeError, TypeError):
782
- return False
783
-
784
-
785
- # ===========================================================================
786
- # CLI Interface
787
- # ===========================================================================
788
-
789
- if __name__ == "__main__":
790
- import sys as _sys
791
-
792
- logging.basicConfig(
793
- level=logging.INFO,
794
- format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
795
- )
796
-
797
- aggregator = CrossProjectAggregator()
798
-
799
- if len(_sys.argv) < 2:
800
- print("CrossProjectAggregator — Layer 1: Transferable Tech Preferences")
801
- print()
802
- print("Usage:")
803
- print(" python cross_project_aggregator.py aggregate # Run full aggregation")
804
- print(" python cross_project_aggregator.py preferences # Show stored preferences")
805
- print(" python cross_project_aggregator.py context [min] # Get context for AI injection")
806
- _sys.exit(0)
807
-
808
- command = _sys.argv[1]
809
-
810
- if command == "aggregate":
811
- results = aggregator.aggregate_all_profiles()
812
- if results:
813
- print("\nAggregated %d transferable patterns:" % len(results))
814
- for key, data in sorted(results.items()):
815
- print(
816
- " %-25s %-30s conf=%.2f evidence=%d profiles=%d%s"
817
- % (
818
- key,
819
- data["value"],
820
- data["confidence"],
821
- data["evidence_count"],
822
- data.get("profiles_seen", 1),
823
- " [CONTRADICTIONS]" if data.get("contradictions") else "",
824
- )
825
- )
826
- else:
827
- print("No patterns found. Add memories across profiles first.")
828
-
829
- elif command == "preferences":
830
- min_conf = float(_sys.argv[2]) if len(_sys.argv) > 2 else 0.6
831
- prefs = aggregator.get_tech_preferences(min_confidence=min_conf)
832
- if prefs:
833
- print("\nTransferable Tech Preferences (min confidence: %.0f%%):" % (min_conf * 100))
834
- for key, data in sorted(prefs.items(), key=lambda x: -x[1]["confidence"]):
835
- print(
836
- " %-25s %-30s conf=%.2f evidence=%d profiles=%d"
837
- % (
838
- key,
839
- data["value"],
840
- data["confidence"],
841
- data["evidence_count"],
842
- data.get("profiles_seen", 1),
843
- )
844
- )
845
- if data.get("contradictions"):
846
- for c in data["contradictions"]:
847
- print(" ^-- %s" % c)
848
- else:
849
- print("No preferences stored. Run 'aggregate' first.")
850
-
851
- elif command == "context":
852
- min_conf = float(_sys.argv[2]) if len(_sys.argv) > 2 else 0.6
853
- print(aggregator.get_preference_context(min_conf))
854
-
855
- else:
856
- print("Unknown command: %s" % command)
857
- _sys.exit(1)