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,325 +0,0 @@
1
- #!/usr/bin/env python3
2
- # SPDX-License-Identifier: MIT
3
- # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
- import pytest
5
-
6
-
7
- # Detect optional dependencies at import time
8
- try:
9
- import lightgbm
10
- HAS_LIGHTGBM = True
11
- except ImportError:
12
- HAS_LIGHTGBM = False
13
-
14
- try:
15
- import numpy as np
16
- HAS_NUMPY = True
17
- except ImportError:
18
- np = None
19
- HAS_NUMPY = False
20
-
21
-
22
- # ---------------------------------------------------------------------------
23
- # Fixtures
24
- # ---------------------------------------------------------------------------
25
-
26
- @pytest.fixture(autouse=True)
27
- def reset_singleton():
28
- from src.learning.learning_db import LearningDB
29
- LearningDB.reset_instance()
30
- yield
31
- LearningDB.reset_instance()
32
-
33
-
34
- @pytest.fixture
35
- def learning_db(tmp_path):
36
- from src.learning.learning_db import LearningDB
37
- db_path = tmp_path / "learning.db"
38
- return LearningDB(db_path=db_path)
39
-
40
-
41
- @pytest.fixture
42
- def ranker(learning_db):
43
- from src.learning.adaptive_ranker import AdaptiveRanker
44
- return AdaptiveRanker(learning_db=learning_db)
45
-
46
-
47
- def _make_result(memory_id, score=0.5, content="test memory", importance=5,
48
- project_name=None, created_at="2026-02-16 10:00:00",
49
- access_count=0, match_type="keyword"):
50
- """Helper to build a search result dict."""
51
- return {
52
- "id": memory_id,
53
- "content": content,
54
- "score": score,
55
- "match_type": match_type,
56
- "importance": importance,
57
- "created_at": created_at,
58
- "access_count": access_count,
59
- "project_name": project_name,
60
- "tags": [],
61
- "created_by": None,
62
- }
63
-
64
-
65
- # ---------------------------------------------------------------------------
66
- # Phase Detection
67
- # ---------------------------------------------------------------------------
68
-
69
- class TestGetPhase:
70
- def test_baseline_with_zero_signals(self, ranker):
71
- assert ranker.get_phase() == "baseline"
72
-
73
- def test_baseline_with_few_signals(self, ranker, learning_db):
74
- """Less than 20 signals should stay in baseline."""
75
- for i in range(10):
76
- learning_db.store_feedback(
77
- query_hash=f"q{i}",
78
- memory_id=i,
79
- signal_type="mcp_used",
80
- )
81
- assert ranker.get_phase() == "baseline"
82
-
83
- def test_rule_based_at_20_signals(self, ranker, learning_db):
84
- """20+ signals should enter rule_based phase."""
85
- for i in range(25):
86
- learning_db.store_feedback(
87
- query_hash=f"q{i}",
88
- memory_id=i,
89
- signal_type="mcp_used",
90
- )
91
- assert ranker.get_phase() == "rule_based"
92
-
93
- @pytest.mark.skipif(not HAS_LIGHTGBM or not HAS_NUMPY,
94
- reason="LightGBM/NumPy required for ML phase")
95
- def test_ml_model_at_200_signals(self, ranker, learning_db):
96
- """200+ signals across 50+ queries should trigger ml_model."""
97
- for i in range(250):
98
- learning_db.store_feedback(
99
- query_hash=f"q{i % 60}", # 60 unique queries
100
- memory_id=i,
101
- signal_type="mcp_used",
102
- )
103
- assert ranker.get_phase() == "ml_model"
104
-
105
- def test_ml_model_requires_enough_unique_queries(self, ranker, learning_db):
106
- """200+ signals but only 10 unique queries should stay rule_based."""
107
- for i in range(250):
108
- learning_db.store_feedback(
109
- query_hash=f"q{i % 10}", # Only 10 unique queries
110
- memory_id=i,
111
- signal_type="mcp_used",
112
- )
113
- # Even with LightGBM available, not enough unique queries
114
- phase = ranker.get_phase()
115
- assert phase in ("rule_based", "ml_model")
116
- if HAS_LIGHTGBM and HAS_NUMPY:
117
- assert phase == "rule_based" # 10 < 50 unique queries
118
-
119
- def test_no_learning_db_auto_creates(self):
120
- """v2.7.2+: AdaptiveRanker auto-creates LearningDB. Phase depends on existing data."""
121
- from src.learning.adaptive_ranker import AdaptiveRanker
122
- ranker = AdaptiveRanker(learning_db=None)
123
- phase = ranker.get_phase()
124
- # Phase is valid (auto-created DB may have data from other tests)
125
- assert phase in ("baseline", "rule_based", "ml_model")
126
-
127
-
128
- # ---------------------------------------------------------------------------
129
- # Phase Info
130
- # ---------------------------------------------------------------------------
131
-
132
- class TestGetPhaseInfo:
133
- def test_phase_info_structure(self, ranker):
134
- info = ranker.get_phase_info()
135
- assert "phase" in info
136
- assert "feedback_count" in info
137
- assert "unique_queries" in info
138
- assert "thresholds" in info
139
- assert "model_loaded" in info
140
- assert "lightgbm_available" in info
141
- assert "numpy_available" in info
142
-
143
- def test_phase_info_values(self, ranker):
144
- info = ranker.get_phase_info()
145
- assert info["phase"] == "baseline"
146
- assert info["feedback_count"] == 0
147
- assert info["unique_queries"] == 0
148
- assert info["model_loaded"] is False
149
-
150
-
151
- # ---------------------------------------------------------------------------
152
- # Rerank Routing
153
- # ---------------------------------------------------------------------------
154
-
155
- class TestRerank:
156
- def test_empty_results(self, ranker):
157
- result = ranker.rerank([], "query")
158
- assert result == []
159
-
160
- def test_single_result_baseline(self, ranker):
161
- """Single result should get baseline phase annotation."""
162
- results = [_make_result(1, score=0.8)]
163
- reranked = ranker.rerank(results, "test query")
164
- assert len(reranked) == 1
165
- assert reranked[0]["ranking_phase"] == "baseline"
166
- assert reranked[0]["base_score"] == 0.8
167
-
168
- def test_baseline_preserves_order(self, ranker):
169
- """In baseline phase, original order should be preserved."""
170
- results = [
171
- _make_result(1, score=0.9),
172
- _make_result(2, score=0.5),
173
- _make_result(3, score=0.3),
174
- ]
175
- reranked = ranker.rerank(results, "test query")
176
- # All should be baseline
177
- for r in reranked:
178
- assert r["ranking_phase"] == "baseline"
179
- # Order preserved (no re-sorting in baseline)
180
- assert reranked[0]["id"] == 1
181
- assert reranked[1]["id"] == 2
182
- assert reranked[2]["id"] == 3
183
-
184
- def test_base_score_preserved(self, ranker, learning_db):
185
- """base_score should always contain the original score."""
186
- # Add enough feedback for rule_based
187
- for i in range(25):
188
- learning_db.store_feedback(
189
- query_hash=f"q{i}", memory_id=i, signal_type="mcp_used",
190
- )
191
-
192
- results = [
193
- _make_result(1, score=0.8),
194
- _make_result(2, score=0.5),
195
- ]
196
- reranked = ranker.rerank(results, "test query")
197
- for r in reranked:
198
- assert "base_score" in r
199
-
200
-
201
- # ---------------------------------------------------------------------------
202
- # Rule-Based Re-ranking
203
- # ---------------------------------------------------------------------------
204
-
205
- class TestRuleBasedReranking:
206
- def test_boost_applied(self, ranker, learning_db):
207
- """Rule-based should modify scores based on features."""
208
- for i in range(25):
209
- learning_db.store_feedback(
210
- query_hash=f"q{i}", memory_id=i, signal_type="mcp_used",
211
- )
212
-
213
- results = [
214
- _make_result(1, score=0.5, importance=9, access_count=8),
215
- _make_result(2, score=0.5, importance=2, access_count=0),
216
- ]
217
- reranked = ranker.rerank(results, "test query")
218
-
219
- # Both should be rule_based
220
- assert all(r["ranking_phase"] == "rule_based" for r in reranked)
221
-
222
- # High importance + access should get higher score
223
- high_imp = next(r for r in reranked if r["id"] == 1)
224
- low_imp = next(r for r in reranked if r["id"] == 2)
225
- assert high_imp["score"] > low_imp["score"]
226
-
227
- def test_project_match_boost(self, ranker, learning_db):
228
- """Memory matching current project should be boosted."""
229
- for i in range(25):
230
- learning_db.store_feedback(
231
- query_hash=f"q{i}", memory_id=i, signal_type="mcp_used",
232
- )
233
-
234
- results = [
235
- _make_result(1, score=0.5, project_name="SLM"),
236
- _make_result(2, score=0.5, project_name="OTHER"),
237
- ]
238
- context = {"current_project": "SLM"}
239
- reranked = ranker.rerank(results, "test query", context=context)
240
-
241
- slm_result = next(r for r in reranked if r["id"] == 1)
242
- other_result = next(r for r in reranked if r["id"] == 2)
243
- assert slm_result["score"] > other_result["score"]
244
-
245
- def test_results_resorted(self, ranker, learning_db):
246
- """Results should be re-sorted by boosted score."""
247
- for i in range(25):
248
- learning_db.store_feedback(
249
- query_hash=f"q{i}", memory_id=i, signal_type="mcp_used",
250
- )
251
-
252
- # Second result has much higher importance
253
- results = [
254
- _make_result(1, score=0.5, importance=2),
255
- _make_result(2, score=0.5, importance=10, access_count=10),
256
- ]
257
- reranked = ranker.rerank(results, "test query")
258
- # Higher importance should float to top
259
- assert reranked[0]["id"] == 2
260
-
261
-
262
- # ---------------------------------------------------------------------------
263
- # ML Training (skipped if LightGBM not available)
264
- # ---------------------------------------------------------------------------
265
-
266
- class TestTraining:
267
- @pytest.mark.skipif(not HAS_LIGHTGBM or not HAS_NUMPY,
268
- reason="LightGBM/NumPy required")
269
- def test_train_insufficient_data(self, ranker, learning_db):
270
- """Training should return None with insufficient data."""
271
- result = ranker.train()
272
- assert result is None
273
-
274
- def test_train_without_lightgbm(self, ranker):
275
- """Should gracefully handle missing LightGBM."""
276
- from src.learning import adaptive_ranker as ar_module
277
- original = ar_module.HAS_LIGHTGBM
278
- ar_module.HAS_LIGHTGBM = False
279
- try:
280
- result = ranker.train()
281
- assert result is None
282
- finally:
283
- ar_module.HAS_LIGHTGBM = original
284
-
285
-
286
- # ---------------------------------------------------------------------------
287
- # Model Loading Fallback
288
- # ---------------------------------------------------------------------------
289
-
290
- class TestModelLoading:
291
- def test_load_nonexistent_model(self, ranker):
292
- """Loading a model that doesn't exist should return None."""
293
- model = ranker._load_model()
294
- assert model is None
295
-
296
- def test_load_attempt_cached(self, ranker):
297
- """After first failed load, _model_load_attempted should be True."""
298
- ranker._load_model()
299
- assert ranker._model_load_attempted is True
300
-
301
- def test_second_load_returns_cached_none(self, ranker):
302
- """Second load attempt should return None immediately (cached failure)."""
303
- ranker._load_model()
304
- result = ranker._load_model()
305
- assert result is None
306
-
307
- def test_reload_model_resets_flag(self, ranker):
308
- """reload_model should reset the _model_load_attempted flag."""
309
- ranker._load_model()
310
- assert ranker._model_load_attempted is True
311
- ranker.reload_model()
312
- # After reload, the flag should have been reset and tried again
313
- # (and failed again since no model file exists)
314
- assert ranker._model is None
315
-
316
-
317
- # ---------------------------------------------------------------------------
318
- # Module-level convenience
319
- # ---------------------------------------------------------------------------
320
-
321
- class TestModuleLevel:
322
- def test_get_phase_function(self):
323
- from src.learning.adaptive_ranker import get_phase
324
- phase = get_phase()
325
- assert phase in ("baseline", "rule_based", "ml_model")
@@ -1,60 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
3
- """Tests for v2.8 adaptive ranker — 20 features + lifecycle/behavioral boosts.
4
- """
5
- import sys
6
- from pathlib import Path
7
- sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
8
-
9
-
10
- class TestAdaptiveRankerV28:
11
- def test_num_features_is_20(self):
12
- from learning.feature_extractor import NUM_FEATURES
13
- assert NUM_FEATURES == 20
14
-
15
- def test_new_rule_boosts_exist(self):
16
- from learning.adaptive_ranker import _RULE_BOOST
17
- assert 'lifecycle_active' in _RULE_BOOST
18
- assert 'outcome_success_high' in _RULE_BOOST
19
- assert 'behavioral_match_strong' in _RULE_BOOST
20
- assert 'cross_project_boost' in _RULE_BOOST
21
-
22
- def test_lifecycle_active_boost(self):
23
- from learning.adaptive_ranker import _RULE_BOOST
24
- assert _RULE_BOOST['lifecycle_active'] == 1.0
25
- assert _RULE_BOOST['lifecycle_warm'] == 0.85
26
- assert _RULE_BOOST['lifecycle_cold'] == 0.6
27
-
28
- def test_outcome_boosts(self):
29
- from learning.adaptive_ranker import _RULE_BOOST
30
- assert _RULE_BOOST['outcome_success_high'] == 1.3
31
- assert _RULE_BOOST['outcome_failure_high'] == 0.7
32
-
33
- def test_phase1_works_with_20_features(self):
34
- """Phase 1 (rule-based) ranking should work with 20-feature vectors."""
35
- from learning.adaptive_ranker import AdaptiveRanker
36
- ranker = AdaptiveRanker()
37
- results = [
38
- {'id': 1, 'content': 'Python memory', 'score': 0.8, 'match_type': 'semantic',
39
- 'importance': 7, 'lifecycle_state': 'active'},
40
- {'id': 2, 'content': 'Old memory', 'score': 0.7, 'match_type': 'semantic',
41
- 'importance': 3, 'lifecycle_state': 'cold'},
42
- ]
43
- ranked = ranker.rerank(results, "Python")
44
- assert len(ranked) == 2
45
- assert all('score' in r for r in ranked)
46
-
47
- def test_active_memory_ranks_higher_than_cold(self):
48
- """Active memories should score higher than cold with same base score."""
49
- from learning.adaptive_ranker import AdaptiveRanker
50
- ranker = AdaptiveRanker()
51
- results = [
52
- {'id': 1, 'content': 'same content', 'score': 0.8, 'match_type': 'semantic',
53
- 'importance': 5, 'lifecycle_state': 'cold'},
54
- {'id': 2, 'content': 'same content', 'score': 0.8, 'match_type': 'semantic',
55
- 'importance': 5, 'lifecycle_state': 'active'},
56
- ]
57
- ranked = ranker.rerank(results, "test")
58
- # Active (id=2) should rank higher than cold (id=1)
59
- if len(ranked) >= 2:
60
- assert ranked[0]['id'] == 2 or ranked[0]['score'] >= ranked[1]['score']
@@ -1,306 +0,0 @@
1
- #!/usr/bin/env python3
2
- # SPDX-License-Identifier: MIT
3
- # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
- import math
5
- import sqlite3
6
- from datetime import datetime, timedelta
7
- from pathlib import Path
8
- from unittest.mock import patch, MagicMock
9
-
10
- import pytest
11
-
12
-
13
- # ---------------------------------------------------------------------------
14
- # Fixtures
15
- # ---------------------------------------------------------------------------
16
-
17
- @pytest.fixture(autouse=True)
18
- def reset_singleton():
19
- from src.learning.learning_db import LearningDB
20
- LearningDB.reset_instance()
21
- yield
22
- LearningDB.reset_instance()
23
-
24
-
25
- @pytest.fixture
26
- def learning_db(tmp_path):
27
- from src.learning.learning_db import LearningDB
28
- db_path = tmp_path / "learning.db"
29
- return LearningDB(db_path=db_path)
30
-
31
-
32
- @pytest.fixture
33
- def memory_db(tmp_path):
34
- """Create a minimal memory.db with test data."""
35
- db_path = tmp_path / "memory.db"
36
- conn = sqlite3.connect(str(db_path))
37
- cursor = conn.cursor()
38
- cursor.execute('''
39
- CREATE TABLE IF NOT EXISTS memories (
40
- id INTEGER PRIMARY KEY AUTOINCREMENT,
41
- content TEXT NOT NULL,
42
- summary TEXT,
43
- project_path TEXT,
44
- project_name TEXT,
45
- tags TEXT DEFAULT '[]',
46
- category TEXT,
47
- parent_id INTEGER,
48
- tree_path TEXT DEFAULT '/',
49
- depth INTEGER DEFAULT 0,
50
- memory_type TEXT DEFAULT 'session',
51
- importance INTEGER DEFAULT 5,
52
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
53
- last_accessed TIMESTAMP,
54
- access_count INTEGER DEFAULT 0,
55
- content_hash TEXT,
56
- cluster_id INTEGER,
57
- profile TEXT DEFAULT 'default',
58
- created_by TEXT,
59
- source_protocol TEXT,
60
- trust_score REAL DEFAULT 1.0
61
- )
62
- ''')
63
- conn.commit()
64
- conn.close()
65
- return db_path
66
-
67
-
68
- def _insert_memories(db_path, memories):
69
- conn = sqlite3.connect(str(db_path))
70
- cursor = conn.cursor()
71
- for m in memories:
72
- cursor.execute('''
73
- INSERT INTO memories (content, tags, project_name, project_path,
74
- importance, access_count, profile, created_by,
75
- source_protocol, created_at)
76
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
77
- ''', (
78
- m.get('content', 'test'),
79
- m.get('tags', '[]'),
80
- m.get('project_name'),
81
- m.get('project_path'),
82
- m.get('importance', 5),
83
- m.get('access_count', 0),
84
- m.get('profile', 'default'),
85
- m.get('created_by'),
86
- m.get('source_protocol'),
87
- m.get('created_at', '2026-02-16 10:00:00'),
88
- ))
89
- conn.commit()
90
- conn.close()
91
-
92
-
93
- # ---------------------------------------------------------------------------
94
- # Temporal Decay
95
- # ---------------------------------------------------------------------------
96
-
97
- class TestTemporalDecay:
98
- def test_days_since_recent(self):
99
- from src.learning.cross_project_aggregator import CrossProjectAggregator
100
- now = datetime(2026, 2, 16, 12, 0, 0)
101
- ts = "2026-02-16T10:00:00"
102
- days = CrossProjectAggregator._days_since(ts, now)
103
- assert 0.0 <= days < 1.0
104
-
105
- def test_days_since_365_days_ago(self):
106
- from src.learning.cross_project_aggregator import CrossProjectAggregator
107
- now = datetime(2026, 2, 16, 12, 0, 0)
108
- old = (now - timedelta(days=365)).isoformat()
109
- days = CrossProjectAggregator._days_since(old, now)
110
- assert abs(days - 365.0) < 1.0
111
-
112
- def test_days_since_empty_string(self):
113
- from src.learning.cross_project_aggregator import CrossProjectAggregator
114
- assert CrossProjectAggregator._days_since("") == 0.0
115
-
116
- def test_days_since_invalid_string(self):
117
- from src.learning.cross_project_aggregator import CrossProjectAggregator
118
- assert CrossProjectAggregator._days_since("not-a-date") == 0.0
119
-
120
- def test_days_since_space_separated(self):
121
- from src.learning.cross_project_aggregator import CrossProjectAggregator
122
- now = datetime(2026, 2, 16, 12, 0, 0)
123
- ts = "2026-02-15 12:00:00"
124
- days = CrossProjectAggregator._days_since(ts, now)
125
- assert abs(days - 1.0) < 0.01
126
-
127
- def test_decay_weight_recent(self):
128
- """Recent timestamp -> weight close to 1.0."""
129
- from src.learning.cross_project_aggregator import DECAY_HALF_LIFE_DAYS
130
- # 0 days -> exp(0) = 1.0
131
- weight = math.exp(-0.0 / DECAY_HALF_LIFE_DAYS)
132
- assert abs(weight - 1.0) < 0.001
133
-
134
- def test_decay_weight_365_days(self):
135
- """365-day-old pattern -> weight ~ 0.37."""
136
- from src.learning.cross_project_aggregator import DECAY_HALF_LIFE_DAYS
137
- weight = math.exp(-365.0 / DECAY_HALF_LIFE_DAYS)
138
- assert 0.30 < weight < 0.40
139
-
140
-
141
- # ---------------------------------------------------------------------------
142
- # Contradiction Detection
143
- # ---------------------------------------------------------------------------
144
-
145
- class TestContradictionDetection:
146
- def test_cross_profile_disagreement(self, learning_db, memory_db):
147
- """Two profiles with different values should trigger a contradiction."""
148
- from src.learning.cross_project_aggregator import CrossProjectAggregator
149
-
150
- aggregator = CrossProjectAggregator(
151
- memory_db_path=memory_db,
152
- learning_db=learning_db,
153
- )
154
-
155
- pattern_data = {
156
- "value": "react",
157
- "profile_history": [
158
- {"profile": "work", "value": "react", "confidence": 0.8,
159
- "weight": 1.0, "timestamp": "2026-02-16"},
160
- {"profile": "personal", "value": "vue", "confidence": 0.7,
161
- "weight": 0.9, "timestamp": "2026-02-15"},
162
- ],
163
- }
164
-
165
- contradictions = aggregator._detect_contradictions("frontend", pattern_data)
166
- assert len(contradictions) >= 1
167
- assert any("vue" in c and "react" in c for c in contradictions)
168
-
169
- def test_no_contradiction_when_unanimous(self, learning_db, memory_db):
170
- from src.learning.cross_project_aggregator import CrossProjectAggregator
171
- aggregator = CrossProjectAggregator(
172
- memory_db_path=memory_db,
173
- learning_db=learning_db,
174
- )
175
- pattern_data = {
176
- "value": "python",
177
- "profile_history": [
178
- {"profile": "work", "value": "python", "confidence": 0.9,
179
- "weight": 1.0, "timestamp": "2026-02-16"},
180
- {"profile": "personal", "value": "python", "confidence": 0.8,
181
- "weight": 0.9, "timestamp": "2026-02-15"},
182
- ],
183
- }
184
- contradictions = aggregator._detect_contradictions("lang", pattern_data)
185
- assert len(contradictions) == 0
186
-
187
-
188
- # ---------------------------------------------------------------------------
189
- # get_tech_preferences from learning.db
190
- # ---------------------------------------------------------------------------
191
-
192
- class TestGetTechPreferences:
193
- def test_empty_db_returns_empty(self, learning_db, memory_db):
194
- from src.learning.cross_project_aggregator import CrossProjectAggregator
195
- aggregator = CrossProjectAggregator(
196
- memory_db_path=memory_db,
197
- learning_db=learning_db,
198
- )
199
- prefs = aggregator.get_tech_preferences(min_confidence=0.0)
200
- assert prefs == {}
201
-
202
- def test_stored_patterns_returned(self, learning_db, memory_db):
203
- from src.learning.cross_project_aggregator import CrossProjectAggregator
204
-
205
- # Pre-populate learning.db with a preference pattern
206
- learning_db.upsert_transferable_pattern(
207
- pattern_type="preference",
208
- key="language",
209
- value="python",
210
- confidence=0.85,
211
- evidence_count=15,
212
- profiles_seen=2,
213
- )
214
-
215
- aggregator = CrossProjectAggregator(
216
- memory_db_path=memory_db,
217
- learning_db=learning_db,
218
- )
219
- prefs = aggregator.get_tech_preferences(min_confidence=0.5)
220
- assert "language" in prefs
221
- assert prefs["language"]["value"] == "python"
222
- assert prefs["language"]["confidence"] == 0.85
223
-
224
- def test_confidence_filter(self, learning_db, memory_db):
225
- from src.learning.cross_project_aggregator import CrossProjectAggregator
226
-
227
- learning_db.upsert_transferable_pattern(
228
- pattern_type="preference", key="low", value="x",
229
- confidence=0.3, evidence_count=2,
230
- )
231
- learning_db.upsert_transferable_pattern(
232
- pattern_type="preference", key="high", value="y",
233
- confidence=0.9, evidence_count=20,
234
- )
235
-
236
- aggregator = CrossProjectAggregator(
237
- memory_db_path=memory_db,
238
- learning_db=learning_db,
239
- )
240
- prefs = aggregator.get_tech_preferences(min_confidence=0.6)
241
- assert "high" in prefs
242
- assert "low" not in prefs
243
-
244
- def test_no_learning_db_auto_creates(self, memory_db):
245
- """v2.7.2+: Aggregator auto-creates LearningDB. Should not crash."""
246
- from src.learning.cross_project_aggregator import CrossProjectAggregator
247
- aggregator = CrossProjectAggregator(
248
- memory_db_path=memory_db,
249
- learning_db=None,
250
- )
251
- # Should not crash — may return data from auto-created DB
252
- prefs = aggregator.get_tech_preferences()
253
- assert isinstance(prefs, dict)
254
-
255
-
256
- # ---------------------------------------------------------------------------
257
- # is_within_window
258
- # ---------------------------------------------------------------------------
259
-
260
- class TestIsWithinWindow:
261
- def test_recent_within_window(self):
262
- from src.learning.cross_project_aggregator import CrossProjectAggregator
263
- now_str = datetime.now().isoformat()
264
- assert CrossProjectAggregator._is_within_window(now_str, 90) is True
265
-
266
- def test_old_outside_window(self):
267
- from src.learning.cross_project_aggregator import CrossProjectAggregator
268
- old = (datetime.now() - timedelta(days=200)).isoformat()
269
- assert CrossProjectAggregator._is_within_window(old, 90) is False
270
-
271
- def test_empty_timestamp(self):
272
- from src.learning.cross_project_aggregator import CrossProjectAggregator
273
- assert CrossProjectAggregator._is_within_window("", 90) is False
274
-
275
- def test_invalid_timestamp(self):
276
- from src.learning.cross_project_aggregator import CrossProjectAggregator
277
- assert CrossProjectAggregator._is_within_window("not-a-date", 90) is False
278
-
279
-
280
- # ---------------------------------------------------------------------------
281
- # Preference Context Formatting
282
- # ---------------------------------------------------------------------------
283
-
284
- class TestPreferenceContext:
285
- def test_no_preferences(self, learning_db, memory_db):
286
- from src.learning.cross_project_aggregator import CrossProjectAggregator
287
- aggregator = CrossProjectAggregator(
288
- memory_db_path=memory_db,
289
- learning_db=learning_db,
290
- )
291
- ctx = aggregator.get_preference_context()
292
- assert "No transferable preferences learned yet" in ctx
293
-
294
- def test_with_preferences(self, learning_db, memory_db):
295
- from src.learning.cross_project_aggregator import CrossProjectAggregator
296
- learning_db.upsert_transferable_pattern(
297
- pattern_type="preference", key="framework", value="FastAPI",
298
- confidence=0.8, evidence_count=10, profiles_seen=2,
299
- )
300
- aggregator = CrossProjectAggregator(
301
- memory_db_path=memory_db,
302
- learning_db=learning_db,
303
- )
304
- ctx = aggregator.get_preference_context(min_confidence=0.5)
305
- assert "FastAPI" in ctx
306
- assert "Framework" in ctx # Title-cased key