superlocalmemory 2.8.6 → 3.0.1

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