superlocalmemory 2.8.6 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (431) hide show
  1. package/LICENSE +9 -1
  2. package/NOTICE +63 -0
  3. package/README.md +165 -480
  4. package/bin/slm +17 -449
  5. package/bin/slm-npm +1 -1
  6. package/conftest.py +5 -0
  7. package/docs/api-reference.md +284 -0
  8. package/docs/architecture.md +149 -0
  9. package/docs/auto-memory.md +150 -0
  10. package/docs/cli-reference.md +276 -0
  11. package/docs/compliance.md +191 -0
  12. package/docs/configuration.md +182 -0
  13. package/docs/getting-started.md +102 -0
  14. package/docs/ide-setup.md +261 -0
  15. package/docs/mcp-tools.md +220 -0
  16. package/docs/migration-from-v2.md +170 -0
  17. package/docs/profiles.md +173 -0
  18. package/docs/troubleshooting.md +310 -0
  19. package/{configs → ide/configs}/antigravity-mcp.json +3 -3
  20. package/ide/configs/chatgpt-desktop-mcp.json +16 -0
  21. package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
  22. package/{configs → ide/configs}/codex-mcp.toml +4 -4
  23. package/{configs → ide/configs}/continue-mcp.yaml +4 -3
  24. package/{configs → ide/configs}/continue-skills.yaml +6 -6
  25. package/ide/configs/cursor-mcp.json +15 -0
  26. package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
  27. package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
  28. package/{configs → ide/configs}/opencode-mcp.json +2 -2
  29. package/{configs → ide/configs}/perplexity-mcp.json +2 -2
  30. package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
  31. package/{configs → ide/configs}/windsurf-mcp.json +3 -3
  32. package/{configs → ide/configs}/zed-mcp.json +2 -2
  33. package/{hooks → ide/hooks}/context-hook.js +9 -20
  34. package/ide/hooks/memory-list-skill.js +70 -0
  35. package/ide/hooks/memory-profile-skill.js +101 -0
  36. package/ide/hooks/memory-recall-skill.js +62 -0
  37. package/ide/hooks/memory-remember-skill.js +68 -0
  38. package/ide/hooks/memory-reset-skill.js +160 -0
  39. package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
  40. package/ide/integrations/langchain/README.md +106 -0
  41. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
  42. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
  43. package/ide/integrations/langchain/pyproject.toml +38 -0
  44. package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
  45. package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
  46. package/ide/integrations/langchain/tests/test_security.py +117 -0
  47. package/ide/integrations/llamaindex/README.md +81 -0
  48. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
  49. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
  50. package/ide/integrations/llamaindex/pyproject.toml +43 -0
  51. package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
  52. package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
  53. package/ide/integrations/llamaindex/tests/test_security.py +241 -0
  54. package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
  55. package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
  56. package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
  57. package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
  58. package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
  59. package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
  60. package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
  61. package/package.json +13 -22
  62. package/pyproject.toml +85 -0
  63. package/scripts/build-dmg.sh +417 -0
  64. package/scripts/install-skills.ps1 +334 -0
  65. package/scripts/postinstall.js +2 -2
  66. package/scripts/start-dashboard.ps1 +52 -0
  67. package/scripts/start-dashboard.sh +41 -0
  68. package/scripts/sync-wiki.ps1 +127 -0
  69. package/scripts/sync-wiki.sh +82 -0
  70. package/scripts/test-dmg.sh +161 -0
  71. package/scripts/test-npm-package.ps1 +252 -0
  72. package/scripts/test-npm-package.sh +207 -0
  73. package/scripts/verify-install.ps1 +294 -0
  74. package/scripts/verify-install.sh +266 -0
  75. package/src/superlocalmemory/__init__.py +0 -0
  76. package/src/superlocalmemory/attribution/__init__.py +9 -0
  77. package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
  78. package/src/superlocalmemory/attribution/signer.py +153 -0
  79. package/src/superlocalmemory/attribution/watermark.py +189 -0
  80. package/src/superlocalmemory/cli/__init__.py +5 -0
  81. package/src/superlocalmemory/cli/commands.py +245 -0
  82. package/src/superlocalmemory/cli/main.py +89 -0
  83. package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
  84. package/src/superlocalmemory/cli/post_install.py +99 -0
  85. package/src/superlocalmemory/cli/setup_wizard.py +129 -0
  86. package/src/superlocalmemory/compliance/__init__.py +0 -0
  87. package/src/superlocalmemory/compliance/abac.py +204 -0
  88. package/src/superlocalmemory/compliance/audit.py +314 -0
  89. package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
  90. package/src/superlocalmemory/compliance/gdpr.py +294 -0
  91. package/src/superlocalmemory/compliance/lifecycle.py +158 -0
  92. package/src/superlocalmemory/compliance/retention.py +232 -0
  93. package/src/superlocalmemory/compliance/scheduler.py +148 -0
  94. package/src/superlocalmemory/core/__init__.py +0 -0
  95. package/src/superlocalmemory/core/config.py +391 -0
  96. package/src/superlocalmemory/core/embeddings.py +293 -0
  97. package/src/superlocalmemory/core/engine.py +701 -0
  98. package/src/superlocalmemory/core/hooks.py +65 -0
  99. package/src/superlocalmemory/core/maintenance.py +172 -0
  100. package/src/superlocalmemory/core/modes.py +140 -0
  101. package/src/superlocalmemory/core/profiles.py +234 -0
  102. package/src/superlocalmemory/core/registry.py +117 -0
  103. package/src/superlocalmemory/dynamics/__init__.py +0 -0
  104. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
  105. package/src/superlocalmemory/encoding/__init__.py +0 -0
  106. package/src/superlocalmemory/encoding/consolidator.py +485 -0
  107. package/src/superlocalmemory/encoding/emotional.py +125 -0
  108. package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
  109. package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
  110. package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
  111. package/src/superlocalmemory/encoding/foresight.py +91 -0
  112. package/src/superlocalmemory/encoding/graph_builder.py +302 -0
  113. package/src/superlocalmemory/encoding/observation_builder.py +160 -0
  114. package/src/superlocalmemory/encoding/scene_builder.py +183 -0
  115. package/src/superlocalmemory/encoding/signal_inference.py +90 -0
  116. package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
  117. package/src/superlocalmemory/encoding/type_router.py +235 -0
  118. package/src/superlocalmemory/hooks/__init__.py +3 -0
  119. package/src/superlocalmemory/hooks/auto_capture.py +111 -0
  120. package/src/superlocalmemory/hooks/auto_recall.py +93 -0
  121. package/src/superlocalmemory/hooks/ide_connector.py +204 -0
  122. package/src/superlocalmemory/hooks/rules_engine.py +99 -0
  123. package/src/superlocalmemory/infra/__init__.py +3 -0
  124. package/src/superlocalmemory/infra/auth_middleware.py +82 -0
  125. package/src/superlocalmemory/infra/backup.py +317 -0
  126. package/src/superlocalmemory/infra/cache_manager.py +267 -0
  127. package/src/superlocalmemory/infra/event_bus.py +381 -0
  128. package/src/superlocalmemory/infra/rate_limiter.py +135 -0
  129. package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
  130. package/src/superlocalmemory/learning/__init__.py +0 -0
  131. package/src/superlocalmemory/learning/adaptive.py +172 -0
  132. package/src/superlocalmemory/learning/behavioral.py +490 -0
  133. package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
  134. package/src/superlocalmemory/learning/bootstrap.py +298 -0
  135. package/src/superlocalmemory/learning/cross_project.py +399 -0
  136. package/src/superlocalmemory/learning/database.py +376 -0
  137. package/src/superlocalmemory/learning/engagement.py +323 -0
  138. package/src/superlocalmemory/learning/features.py +138 -0
  139. package/src/superlocalmemory/learning/feedback.py +316 -0
  140. package/src/superlocalmemory/learning/outcomes.py +255 -0
  141. package/src/superlocalmemory/learning/project_context.py +366 -0
  142. package/src/superlocalmemory/learning/ranker.py +155 -0
  143. package/src/superlocalmemory/learning/source_quality.py +303 -0
  144. package/src/superlocalmemory/learning/workflows.py +309 -0
  145. package/src/superlocalmemory/llm/__init__.py +0 -0
  146. package/src/superlocalmemory/llm/backbone.py +316 -0
  147. package/src/superlocalmemory/math/__init__.py +0 -0
  148. package/src/superlocalmemory/math/fisher.py +356 -0
  149. package/src/superlocalmemory/math/langevin.py +398 -0
  150. package/src/superlocalmemory/math/sheaf.py +257 -0
  151. package/src/superlocalmemory/mcp/__init__.py +0 -0
  152. package/src/superlocalmemory/mcp/resources.py +245 -0
  153. package/src/superlocalmemory/mcp/server.py +61 -0
  154. package/src/superlocalmemory/mcp/tools.py +18 -0
  155. package/src/superlocalmemory/mcp/tools_core.py +305 -0
  156. package/src/superlocalmemory/mcp/tools_v28.py +223 -0
  157. package/src/superlocalmemory/mcp/tools_v3.py +286 -0
  158. package/src/superlocalmemory/retrieval/__init__.py +0 -0
  159. package/src/superlocalmemory/retrieval/agentic.py +295 -0
  160. package/src/superlocalmemory/retrieval/ann_index.py +223 -0
  161. package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
  162. package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
  163. package/src/superlocalmemory/retrieval/engine.py +390 -0
  164. package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
  165. package/src/superlocalmemory/retrieval/fusion.py +78 -0
  166. package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
  167. package/src/superlocalmemory/retrieval/reranker.py +154 -0
  168. package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
  169. package/src/superlocalmemory/retrieval/strategy.py +96 -0
  170. package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
  171. package/src/superlocalmemory/server/__init__.py +1 -0
  172. package/src/superlocalmemory/server/api.py +248 -0
  173. package/src/superlocalmemory/server/routes/__init__.py +4 -0
  174. package/src/superlocalmemory/server/routes/agents.py +107 -0
  175. package/src/superlocalmemory/server/routes/backup.py +91 -0
  176. package/src/superlocalmemory/server/routes/behavioral.py +127 -0
  177. package/src/superlocalmemory/server/routes/compliance.py +160 -0
  178. package/src/superlocalmemory/server/routes/data_io.py +188 -0
  179. package/src/superlocalmemory/server/routes/events.py +183 -0
  180. package/src/superlocalmemory/server/routes/helpers.py +85 -0
  181. package/src/superlocalmemory/server/routes/learning.py +273 -0
  182. package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
  183. package/src/superlocalmemory/server/routes/memories.py +399 -0
  184. package/src/superlocalmemory/server/routes/profiles.py +219 -0
  185. package/src/superlocalmemory/server/routes/stats.py +346 -0
  186. package/src/superlocalmemory/server/routes/v3_api.py +365 -0
  187. package/src/superlocalmemory/server/routes/ws.py +82 -0
  188. package/src/superlocalmemory/server/security_middleware.py +57 -0
  189. package/src/superlocalmemory/server/ui.py +245 -0
  190. package/src/superlocalmemory/storage/__init__.py +0 -0
  191. package/src/superlocalmemory/storage/access_control.py +182 -0
  192. package/src/superlocalmemory/storage/database.py +594 -0
  193. package/src/superlocalmemory/storage/migrations.py +303 -0
  194. package/src/superlocalmemory/storage/models.py +406 -0
  195. package/src/superlocalmemory/storage/schema.py +726 -0
  196. package/src/superlocalmemory/storage/v2_migrator.py +317 -0
  197. package/src/superlocalmemory/trust/__init__.py +0 -0
  198. package/src/superlocalmemory/trust/gate.py +130 -0
  199. package/src/superlocalmemory/trust/provenance.py +124 -0
  200. package/src/superlocalmemory/trust/scorer.py +347 -0
  201. package/src/superlocalmemory/trust/signals.py +153 -0
  202. package/ui/index.html +278 -5
  203. package/ui/js/auto-settings.js +70 -0
  204. package/ui/js/dashboard.js +90 -0
  205. package/ui/js/fact-detail.js +92 -0
  206. package/ui/js/feedback.js +2 -2
  207. package/ui/js/ide-status.js +102 -0
  208. package/ui/js/math-health.js +98 -0
  209. package/ui/js/recall-lab.js +127 -0
  210. package/ui/js/settings.js +2 -2
  211. package/ui/js/trust-dashboard.js +73 -0
  212. package/api_server.py +0 -724
  213. package/bin/aider-smart +0 -72
  214. package/bin/superlocalmemoryv2-learning +0 -4
  215. package/bin/superlocalmemoryv2-list +0 -3
  216. package/bin/superlocalmemoryv2-patterns +0 -4
  217. package/bin/superlocalmemoryv2-profile +0 -3
  218. package/bin/superlocalmemoryv2-recall +0 -3
  219. package/bin/superlocalmemoryv2-remember +0 -3
  220. package/bin/superlocalmemoryv2-reset +0 -3
  221. package/bin/superlocalmemoryv2-status +0 -3
  222. package/configs/chatgpt-desktop-mcp.json +0 -16
  223. package/configs/cursor-mcp.json +0 -15
  224. package/hooks/memory-list-skill.js +0 -139
  225. package/hooks/memory-profile-skill.js +0 -273
  226. package/hooks/memory-recall-skill.js +0 -114
  227. package/hooks/memory-remember-skill.js +0 -127
  228. package/hooks/memory-reset-skill.js +0 -274
  229. package/mcp_server.py +0 -1808
  230. package/requirements-core.txt +0 -22
  231. package/requirements-learning.txt +0 -12
  232. package/requirements.txt +0 -12
  233. package/src/agent_registry.py +0 -411
  234. package/src/auth_middleware.py +0 -61
  235. package/src/auto_backup.py +0 -459
  236. package/src/behavioral/__init__.py +0 -49
  237. package/src/behavioral/behavioral_listener.py +0 -203
  238. package/src/behavioral/behavioral_patterns.py +0 -275
  239. package/src/behavioral/cross_project_transfer.py +0 -206
  240. package/src/behavioral/outcome_inference.py +0 -194
  241. package/src/behavioral/outcome_tracker.py +0 -193
  242. package/src/behavioral/tests/__init__.py +0 -4
  243. package/src/behavioral/tests/test_behavioral_integration.py +0 -108
  244. package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
  245. package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
  246. package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
  247. package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
  248. package/src/behavioral/tests/test_outcome_inference.py +0 -107
  249. package/src/behavioral/tests/test_outcome_tracker.py +0 -96
  250. package/src/cache_manager.py +0 -518
  251. package/src/compliance/__init__.py +0 -48
  252. package/src/compliance/abac_engine.py +0 -149
  253. package/src/compliance/abac_middleware.py +0 -116
  254. package/src/compliance/audit_db.py +0 -215
  255. package/src/compliance/audit_logger.py +0 -148
  256. package/src/compliance/retention_manager.py +0 -289
  257. package/src/compliance/retention_scheduler.py +0 -186
  258. package/src/compliance/tests/__init__.py +0 -4
  259. package/src/compliance/tests/test_abac_enforcement.py +0 -95
  260. package/src/compliance/tests/test_abac_engine.py +0 -124
  261. package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
  262. package/src/compliance/tests/test_audit_db.py +0 -123
  263. package/src/compliance/tests/test_audit_logger.py +0 -98
  264. package/src/compliance/tests/test_mcp_audit.py +0 -128
  265. package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
  266. package/src/compliance/tests/test_retention_manager.py +0 -131
  267. package/src/compliance/tests/test_retention_scheduler.py +0 -99
  268. package/src/compression/__init__.py +0 -25
  269. package/src/compression/cli.py +0 -150
  270. package/src/compression/cold_storage.py +0 -217
  271. package/src/compression/config.py +0 -72
  272. package/src/compression/orchestrator.py +0 -133
  273. package/src/compression/tier2_compressor.py +0 -228
  274. package/src/compression/tier3_compressor.py +0 -153
  275. package/src/compression/tier_classifier.py +0 -148
  276. package/src/db_connection_manager.py +0 -536
  277. package/src/embedding_engine.py +0 -63
  278. package/src/embeddings/__init__.py +0 -47
  279. package/src/embeddings/cache.py +0 -70
  280. package/src/embeddings/cli.py +0 -113
  281. package/src/embeddings/constants.py +0 -47
  282. package/src/embeddings/database.py +0 -91
  283. package/src/embeddings/engine.py +0 -247
  284. package/src/embeddings/model_loader.py +0 -145
  285. package/src/event_bus.py +0 -562
  286. package/src/graph/__init__.py +0 -36
  287. package/src/graph/build_helpers.py +0 -74
  288. package/src/graph/cli.py +0 -87
  289. package/src/graph/cluster_builder.py +0 -188
  290. package/src/graph/cluster_summary.py +0 -148
  291. package/src/graph/constants.py +0 -47
  292. package/src/graph/edge_builder.py +0 -162
  293. package/src/graph/entity_extractor.py +0 -95
  294. package/src/graph/graph_core.py +0 -226
  295. package/src/graph/graph_search.py +0 -231
  296. package/src/graph/hierarchical.py +0 -207
  297. package/src/graph/schema.py +0 -99
  298. package/src/graph_engine.py +0 -52
  299. package/src/hnsw_index.py +0 -628
  300. package/src/hybrid_search.py +0 -46
  301. package/src/learning/__init__.py +0 -217
  302. package/src/learning/adaptive_ranker.py +0 -682
  303. package/src/learning/bootstrap/__init__.py +0 -69
  304. package/src/learning/bootstrap/constants.py +0 -93
  305. package/src/learning/bootstrap/db_queries.py +0 -316
  306. package/src/learning/bootstrap/sampling.py +0 -82
  307. package/src/learning/bootstrap/text_utils.py +0 -71
  308. package/src/learning/cross_project_aggregator.py +0 -857
  309. package/src/learning/db/__init__.py +0 -40
  310. package/src/learning/db/constants.py +0 -44
  311. package/src/learning/db/schema.py +0 -279
  312. package/src/learning/engagement_tracker.py +0 -628
  313. package/src/learning/feature_extractor.py +0 -708
  314. package/src/learning/feedback_collector.py +0 -806
  315. package/src/learning/learning_db.py +0 -915
  316. package/src/learning/project_context_manager.py +0 -572
  317. package/src/learning/ranking/__init__.py +0 -33
  318. package/src/learning/ranking/constants.py +0 -84
  319. package/src/learning/ranking/helpers.py +0 -278
  320. package/src/learning/source_quality_scorer.py +0 -676
  321. package/src/learning/synthetic_bootstrap.py +0 -755
  322. package/src/learning/tests/test_adaptive_ranker.py +0 -325
  323. package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
  324. package/src/learning/tests/test_aggregator.py +0 -306
  325. package/src/learning/tests/test_auto_retrain_v28.py +0 -35
  326. package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
  327. package/src/learning/tests/test_feature_extractor_v28.py +0 -93
  328. package/src/learning/tests/test_feedback_collector.py +0 -294
  329. package/src/learning/tests/test_learning_db.py +0 -602
  330. package/src/learning/tests/test_learning_db_v28.py +0 -110
  331. package/src/learning/tests/test_learning_init_v28.py +0 -48
  332. package/src/learning/tests/test_outcome_signals.py +0 -48
  333. package/src/learning/tests/test_project_context.py +0 -292
  334. package/src/learning/tests/test_schema_migration.py +0 -319
  335. package/src/learning/tests/test_signal_inference.py +0 -397
  336. package/src/learning/tests/test_source_quality.py +0 -351
  337. package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
  338. package/src/learning/tests/test_workflow_miner.py +0 -318
  339. package/src/learning/workflow_pattern_miner.py +0 -655
  340. package/src/lifecycle/__init__.py +0 -54
  341. package/src/lifecycle/bounded_growth.py +0 -239
  342. package/src/lifecycle/compaction_engine.py +0 -226
  343. package/src/lifecycle/lifecycle_engine.py +0 -355
  344. package/src/lifecycle/lifecycle_evaluator.py +0 -257
  345. package/src/lifecycle/lifecycle_scheduler.py +0 -130
  346. package/src/lifecycle/retention_policy.py +0 -285
  347. package/src/lifecycle/tests/test_bounded_growth.py +0 -193
  348. package/src/lifecycle/tests/test_compaction.py +0 -179
  349. package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
  350. package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
  351. package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
  352. package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
  353. package/src/lifecycle/tests/test_mcp_compact.py +0 -149
  354. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
  355. package/src/lifecycle/tests/test_retention_policy.py +0 -162
  356. package/src/mcp_tools_v28.py +0 -281
  357. package/src/memory/__init__.py +0 -36
  358. package/src/memory/cli.py +0 -205
  359. package/src/memory/constants.py +0 -39
  360. package/src/memory/helpers.py +0 -28
  361. package/src/memory/schema.py +0 -166
  362. package/src/memory-profiles.py +0 -595
  363. package/src/memory-reset.py +0 -491
  364. package/src/memory_compression.py +0 -989
  365. package/src/memory_store_v2.py +0 -1155
  366. package/src/migrate_v1_to_v2.py +0 -629
  367. package/src/pattern_learner.py +0 -34
  368. package/src/patterns/__init__.py +0 -24
  369. package/src/patterns/analyzers.py +0 -251
  370. package/src/patterns/learner.py +0 -271
  371. package/src/patterns/scoring.py +0 -171
  372. package/src/patterns/store.py +0 -225
  373. package/src/patterns/terminology.py +0 -140
  374. package/src/provenance_tracker.py +0 -312
  375. package/src/qualixar_attribution.py +0 -139
  376. package/src/qualixar_watermark.py +0 -78
  377. package/src/query_optimizer.py +0 -511
  378. package/src/rate_limiter.py +0 -83
  379. package/src/search/__init__.py +0 -20
  380. package/src/search/cli.py +0 -77
  381. package/src/search/constants.py +0 -26
  382. package/src/search/engine.py +0 -241
  383. package/src/search/fusion.py +0 -122
  384. package/src/search/index_loader.py +0 -114
  385. package/src/search/methods.py +0 -162
  386. package/src/search_engine_v2.py +0 -401
  387. package/src/setup_validator.py +0 -482
  388. package/src/subscription_manager.py +0 -391
  389. package/src/tree/__init__.py +0 -59
  390. package/src/tree/builder.py +0 -185
  391. package/src/tree/nodes.py +0 -202
  392. package/src/tree/queries.py +0 -257
  393. package/src/tree/schema.py +0 -80
  394. package/src/tree_manager.py +0 -19
  395. package/src/trust/__init__.py +0 -45
  396. package/src/trust/constants.py +0 -66
  397. package/src/trust/queries.py +0 -157
  398. package/src/trust/schema.py +0 -95
  399. package/src/trust/scorer.py +0 -299
  400. package/src/trust/signals.py +0 -95
  401. package/src/trust_scorer.py +0 -44
  402. package/ui/app.js +0 -1588
  403. package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
  404. package/ui/js/graph-cytoscape.js +0 -1168
  405. package/ui/js/graph-d3-backup.js +0 -32
  406. package/ui/js/graph.js +0 -32
  407. package/ui_server.py +0 -286
  408. /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
  409. /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
  410. /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
  411. /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
  412. /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
  413. /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
  414. /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
  415. /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
  416. /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
  417. /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
  418. /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
  419. /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
  420. /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
  421. /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
  422. /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
  423. /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
  424. /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
  425. /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
  426. /package/{completions → ide/completions}/slm.bash +0 -0
  427. /package/{completions → ide/completions}/slm.zsh +0 -0
  428. /package/{configs → ide/configs}/cody-commands.json +0 -0
  429. /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
  430. /package/{install.ps1 → scripts/install.ps1} +0 -0
  431. /package/{install.sh → scripts/install.sh} +0 -0
@@ -0,0 +1,294 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """SuperLocalMemory V3 — GDPR Compliance.
6
+
7
+ Implements GDPR rights: right to access, right to erasure (forget),
8
+ right to data portability (export), and audit trail.
9
+ Profile-scoped. All operations logged to compliance_audit.
10
+
11
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ from datetime import UTC, datetime
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class GDPRCompliance:
24
+ """GDPR compliance operations for memory data.
25
+
26
+ Supports:
27
+ - Right to Access (Art. 15): Export all data for a profile
28
+ - Right to Erasure (Art. 17): Delete all data for a profile/entity
29
+ - Right to Portability (Art. 20): Export in machine-readable format
30
+ - Audit Trail: Log all data operations
31
+ """
32
+
33
+ def __init__(self, db) -> None:
34
+ self._db = db
35
+
36
+ # -- Right to Access (Art. 15) -----------------------------------------
37
+
38
+ def export_profile_data(self, profile_id: str) -> dict:
39
+ """Export ALL data for a profile in machine-readable format.
40
+
41
+ Returns a dict containing all memories, facts, entities,
42
+ edges, trust scores, feedback, and behavioral patterns.
43
+ """
44
+ self._audit("export", "profile", profile_id, "Full data export")
45
+
46
+ data: dict = {"profile_id": profile_id, "exported_at": _now()}
47
+
48
+ # Memories
49
+ rows = self._db.execute(
50
+ "SELECT * FROM memories WHERE profile_id = ?", (profile_id,)
51
+ )
52
+ data["memories"] = [dict(r) for r in rows]
53
+
54
+ # Facts
55
+ rows = self._db.execute(
56
+ "SELECT * FROM atomic_facts WHERE profile_id = ?", (profile_id,)
57
+ )
58
+ data["facts"] = [dict(r) for r in rows]
59
+
60
+ # Entities
61
+ rows = self._db.execute(
62
+ "SELECT * FROM canonical_entities WHERE profile_id = ?", (profile_id,)
63
+ )
64
+ data["entities"] = [dict(r) for r in rows]
65
+
66
+ # Graph edges
67
+ rows = self._db.execute(
68
+ "SELECT * FROM graph_edges WHERE profile_id = ?", (profile_id,)
69
+ )
70
+ data["edges"] = [dict(r) for r in rows]
71
+
72
+ # Trust scores
73
+ rows = self._db.execute(
74
+ "SELECT * FROM trust_scores WHERE profile_id = ?", (profile_id,)
75
+ )
76
+ data["trust_scores"] = [dict(r) for r in rows]
77
+
78
+ # Feedback
79
+ rows = self._db.execute(
80
+ "SELECT * FROM feedback_records WHERE profile_id = ?", (profile_id,)
81
+ )
82
+ data["feedback"] = [dict(r) for r in rows]
83
+
84
+ # Entity profiles
85
+ rows = self._db.execute(
86
+ "SELECT * FROM entity_profiles WHERE profile_id = ?", (profile_id,)
87
+ )
88
+ data["entity_profiles"] = [dict(r) for r in rows]
89
+
90
+ # Memory scenes
91
+ rows = self._db.execute(
92
+ "SELECT * FROM memory_scenes WHERE profile_id = ?", (profile_id,)
93
+ )
94
+ data["scenes"] = [dict(r) for r in rows]
95
+
96
+ # Temporal events
97
+ rows = self._db.execute(
98
+ "SELECT * FROM temporal_events WHERE profile_id = ?", (profile_id,)
99
+ )
100
+ data["temporal_events"] = [dict(r) for r in rows]
101
+
102
+ # Consolidation log
103
+ rows = self._db.execute(
104
+ "SELECT * FROM consolidation_log WHERE profile_id = ?", (profile_id,)
105
+ )
106
+ data["consolidation_log"] = [dict(r) for r in rows]
107
+
108
+ # Behavioral patterns
109
+ rows = self._db.execute(
110
+ "SELECT * FROM behavioral_patterns WHERE profile_id = ?", (profile_id,)
111
+ )
112
+ data["behavioral_patterns"] = [dict(r) for r in rows]
113
+
114
+ # Action outcomes
115
+ rows = self._db.execute(
116
+ "SELECT * FROM action_outcomes WHERE profile_id = ?", (profile_id,)
117
+ )
118
+ data["action_outcomes"] = [dict(r) for r in rows]
119
+
120
+ # Compliance audit trail
121
+ rows = self._db.execute(
122
+ "SELECT * FROM compliance_audit WHERE profile_id = ?", (profile_id,)
123
+ )
124
+ data["compliance_audit"] = [dict(r) for r in rows]
125
+
126
+ # Provenance (data lineage — EU AI Act Art. 10)
127
+ rows = self._db.execute(
128
+ "SELECT * FROM provenance WHERE profile_id = ?", (profile_id,)
129
+ )
130
+ data["provenance"] = [dict(r) for r in rows]
131
+
132
+ # Entity aliases (indirect PII via entity relationships)
133
+ rows = self._db.execute(
134
+ "SELECT ea.* FROM entity_aliases ea "
135
+ "JOIN canonical_entities ce ON ea.entity_id = ce.entity_id "
136
+ "WHERE ce.profile_id = ?", (profile_id,)
137
+ )
138
+ data["entity_aliases"] = [dict(r) for r in rows]
139
+
140
+ # Profile record itself
141
+ rows = self._db.execute(
142
+ "SELECT * FROM profiles WHERE profile_id = ?", (profile_id,)
143
+ )
144
+ data["profile_record"] = [dict(r) for r in rows]
145
+
146
+ data["total_items"] = sum(
147
+ len(v) for v in data.values() if isinstance(v, list)
148
+ )
149
+
150
+ logger.info("Exported %d items for profile '%s'", data["total_items"], profile_id)
151
+ return data
152
+
153
+ # -- Right to Erasure (Art. 17) ----------------------------------------
154
+
155
+ def forget_profile(self, profile_id: str) -> dict:
156
+ """Delete ALL data for a profile (right to be forgotten).
157
+
158
+ CASCADE deletes handle most cleanup via foreign keys.
159
+ Returns counts of deleted items.
160
+ """
161
+ if profile_id == "default":
162
+ raise ValueError("Cannot delete the default profile via GDPR erasure. "
163
+ "Use profile deletion instead.")
164
+
165
+ self._audit("delete", "profile", profile_id, "GDPR erasure request")
166
+
167
+ counts: dict[str, int] = {}
168
+ tables = [
169
+ "compliance_audit", "action_outcomes", "behavioral_patterns",
170
+ "feedback_records", "trust_scores", "provenance",
171
+ "consolidation_log", "graph_edges", "temporal_events",
172
+ "memory_scenes", "entity_profiles", "bm25_tokens",
173
+ "atomic_facts", "memories", "canonical_entities",
174
+ ]
175
+ for table in tables:
176
+ rows = self._db.execute(
177
+ f"SELECT COUNT(*) AS c FROM {table} WHERE profile_id = ?",
178
+ (profile_id,),
179
+ )
180
+ counts[table] = int(dict(rows[0])["c"]) if rows else 0
181
+ self._db.execute(
182
+ f"DELETE FROM {table} WHERE profile_id = ?", (profile_id,)
183
+ )
184
+
185
+ # Delete entity aliases (orphan PII via entity relationships)
186
+ self._db.execute(
187
+ "DELETE FROM entity_aliases WHERE entity_id IN "
188
+ "(SELECT entity_id FROM canonical_entities WHERE profile_id = ?)",
189
+ (profile_id,),
190
+ )
191
+
192
+ # Delete profile itself
193
+ self._db.execute(
194
+ "DELETE FROM profiles WHERE profile_id = ?", (profile_id,)
195
+ )
196
+ counts["profiles"] = 1
197
+
198
+ # Erase learning database (separate DB file)
199
+ try:
200
+ from superlocalmemory.learning.database import LearningDatabase
201
+ from superlocalmemory.core.config import DEFAULT_BASE_DIR
202
+ learning_db = LearningDatabase(DEFAULT_BASE_DIR / "learning.db")
203
+ learning_db.reset(profile_id)
204
+ counts["learning_db"] = 1
205
+ except Exception:
206
+ pass
207
+
208
+ # VACUUM to remove deleted data from physical file
209
+ try:
210
+ self._db.execute("VACUUM")
211
+ except Exception:
212
+ pass
213
+
214
+ logger.info("GDPR erasure for '%s': %s", profile_id, counts)
215
+ return counts
216
+
217
+ def forget_entity(self, entity_name: str, profile_id: str) -> dict:
218
+ """Delete all data related to a specific entity.
219
+
220
+ Removes facts mentioning the entity, edges, temporal events,
221
+ and the entity itself. For targeted erasure requests.
222
+ """
223
+ self._audit("delete", "entity", entity_name,
224
+ f"GDPR entity erasure in profile {profile_id}",
225
+ profile_id=profile_id)
226
+
227
+ entity = self._db.get_entity_by_name(entity_name, profile_id)
228
+ if entity is None:
229
+ return {"deleted": 0, "entity": entity_name, "found": False}
230
+
231
+ eid = entity.entity_id
232
+ counts: dict[str, int] = {}
233
+
234
+ # Delete facts mentioning this entity
235
+ rows = self._db.execute(
236
+ "SELECT fact_id FROM atomic_facts WHERE profile_id = ? "
237
+ "AND canonical_entities_json LIKE ?",
238
+ (profile_id, f'%"{eid}"%'),
239
+ )
240
+ fact_ids = [dict(r)["fact_id"] for r in rows]
241
+ for fid in fact_ids:
242
+ self._db.delete_fact(fid)
243
+ counts["facts"] = len(fact_ids)
244
+
245
+ # Delete temporal events
246
+ self._db.execute(
247
+ "DELETE FROM temporal_events WHERE entity_id = ? AND profile_id = ?",
248
+ (eid, profile_id),
249
+ )
250
+
251
+ # Delete entity profile
252
+ self._db.execute(
253
+ "DELETE FROM entity_profiles WHERE entity_id = ? AND profile_id = ?",
254
+ (eid, profile_id),
255
+ )
256
+
257
+ # Delete aliases + entity
258
+ self._db.execute("DELETE FROM entity_aliases WHERE entity_id = ?", (eid,))
259
+ self._db.execute("DELETE FROM canonical_entities WHERE entity_id = ?", (eid,))
260
+ counts["entity"] = 1
261
+
262
+ logger.info("Entity erasure '%s' in '%s': %s", entity_name, profile_id, counts)
263
+ return counts
264
+
265
+ # -- Audit Trail -------------------------------------------------------
266
+
267
+ def get_audit_trail(
268
+ self, profile_id: str, limit: int = 100
269
+ ) -> list[dict]:
270
+ """Get compliance audit trail for a profile."""
271
+ rows = self._db.execute(
272
+ "SELECT * FROM compliance_audit WHERE profile_id = ? "
273
+ "ORDER BY timestamp DESC LIMIT ?",
274
+ (profile_id, limit),
275
+ )
276
+ return [dict(r) for r in rows]
277
+
278
+ def _audit(
279
+ self, action: str, target_type: str, target_id: str, details: str,
280
+ profile_id: str | None = None,
281
+ ) -> None:
282
+ """Log a compliance action."""
283
+ from superlocalmemory.storage.models import _new_id
284
+ pid = profile_id if profile_id is not None else target_id
285
+ self._db.execute(
286
+ "INSERT INTO compliance_audit "
287
+ "(audit_id, profile_id, action, target_type, target_id, details, timestamp) "
288
+ "VALUES (?,?,?,?,?,?,?)",
289
+ (_new_id(), pid, action, target_type, target_id, details, _now()),
290
+ )
291
+
292
+
293
+ def _now() -> str:
294
+ return datetime.now(UTC).isoformat()
@@ -0,0 +1,158 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """SuperLocalMemory V3 — Memory Lifecycle Management.
6
+
7
+ Implements Active → Warm → Cold → Archived state machine.
8
+ Coupled with Langevin dynamics: positions naturally create lifecycle states.
9
+
10
+ Ported from V2.8 with Langevin coupling (Innovation's unique feature).
11
+
12
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from datetime import UTC, datetime, timedelta
19
+
20
+ from superlocalmemory.storage.models import AtomicFact, MemoryLifecycle
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Lifecycle transition thresholds (days since last access)
25
+ _ACTIVE_MAX_DAYS = 7 # Active for 7 days after last access
26
+ _WARM_MAX_DAYS = 30 # Warm for 30 days
27
+ _COLD_MAX_DAYS = 90 # Cold for 90 days, then archived
28
+
29
+ # Langevin weight thresholds (if Langevin is available)
30
+ _ACTIVE_WEIGHT_MIN = 0.7
31
+ _WARM_WEIGHT_MIN = 0.4
32
+ _COLD_WEIGHT_MIN = 0.1
33
+
34
+
35
+ class LifecycleManager:
36
+ """Manage memory lifecycle states.
37
+
38
+ Two complementary strategies:
39
+ 1. Time-based: days since last access → state transition
40
+ 2. Langevin-based: position on Poincaré ball → lifecycle weight → state
41
+
42
+ When Langevin is available, it takes precedence (more nuanced).
43
+ Time-based is the fallback for Mode A (no dynamics).
44
+ """
45
+
46
+ def __init__(self, db, langevin=None) -> None:
47
+ self._db = db
48
+ self._langevin = langevin
49
+
50
+ def get_lifecycle_state(self, fact: AtomicFact) -> MemoryLifecycle:
51
+ """Determine current lifecycle state for a fact."""
52
+ # Strategy 1: Langevin-based (if available and position exists)
53
+ if self._langevin is not None and fact.langevin_position:
54
+ weight = self._langevin.compute_lifecycle_weight(fact.langevin_position)
55
+ return self._langevin.get_lifecycle_state(weight)
56
+
57
+ # Strategy 2: Time-based fallback
58
+ return self._time_based_state(fact)
59
+
60
+ def update_lifecycle(self, fact_id: str, profile_id: str) -> MemoryLifecycle:
61
+ """Recompute and persist lifecycle state for a fact."""
62
+ rows = self._db.execute(
63
+ "SELECT lifecycle, access_count, created_at FROM atomic_facts "
64
+ "WHERE fact_id = ? AND profile_id = ?",
65
+ (fact_id, profile_id),
66
+ )
67
+ if not rows:
68
+ return MemoryLifecycle.ACTIVE
69
+
70
+ d = dict(rows[0])
71
+ days = self._days_since(d.get("created_at", ""))
72
+ access = d.get("access_count", 0)
73
+
74
+ if access > 10 or days < _ACTIVE_MAX_DAYS:
75
+ state = MemoryLifecycle.ACTIVE
76
+ elif days < _WARM_MAX_DAYS:
77
+ state = MemoryLifecycle.WARM
78
+ elif days < _COLD_MAX_DAYS:
79
+ state = MemoryLifecycle.COLD
80
+ else:
81
+ state = MemoryLifecycle.ARCHIVED
82
+
83
+ current = d.get("lifecycle", "active")
84
+ if current != state.value:
85
+ self._db.update_fact(fact_id, {"lifecycle": state})
86
+ logger.debug("Fact %s: %s → %s", fact_id, current, state.value)
87
+
88
+ return state
89
+
90
+ def run_maintenance(self, profile_id: str) -> dict[str, int]:
91
+ """Run lifecycle maintenance on all facts in a profile.
92
+
93
+ If Langevin is available, evolve positions and update states.
94
+ Otherwise, use time-based transitions.
95
+ """
96
+ facts = self._db.get_all_facts(profile_id)
97
+ counts: dict[str, int] = {s.value: 0 for s in MemoryLifecycle}
98
+ transitions = 0
99
+
100
+ for fact in facts:
101
+ old_state = fact.lifecycle
102
+
103
+ if self._langevin is not None and fact.langevin_position:
104
+ # Langevin step
105
+ age = self._days_since(fact.created_at)
106
+ new_pos, weight = self._langevin.step(
107
+ fact.langevin_position, fact.access_count, age, fact.importance
108
+ )
109
+ new_state = self._langevin.get_lifecycle_state(weight)
110
+ # Persist position + state
111
+ self._db.update_fact(fact.fact_id, {
112
+ "langevin_position": new_pos,
113
+ "lifecycle": new_state,
114
+ })
115
+ else:
116
+ new_state = self._time_based_state(fact)
117
+ if new_state != old_state:
118
+ self._db.update_fact(fact.fact_id, {"lifecycle": new_state})
119
+
120
+ counts[new_state.value] = counts.get(new_state.value, 0) + 1
121
+ if new_state != old_state:
122
+ transitions += 1
123
+
124
+ counts["transitions"] = transitions
125
+ logger.info("Lifecycle maintenance for '%s': %s", profile_id, counts)
126
+ return counts
127
+
128
+ def get_archived_facts(self, profile_id: str) -> list[AtomicFact]:
129
+ """Get all archived facts (candidates for deletion/export)."""
130
+ rows = self._db.execute(
131
+ "SELECT * FROM atomic_facts WHERE profile_id = ? AND lifecycle = 'archived'",
132
+ (profile_id,),
133
+ )
134
+ return [self._db._row_to_fact(r) for r in rows]
135
+
136
+ # -- Internal ----------------------------------------------------------
137
+
138
+ def _time_based_state(self, fact: AtomicFact) -> MemoryLifecycle:
139
+ """Determine lifecycle state from time since creation + access count."""
140
+ days = self._days_since(fact.created_at)
141
+ if fact.access_count > 10 or days < _ACTIVE_MAX_DAYS:
142
+ return MemoryLifecycle.ACTIVE
143
+ if days < _WARM_MAX_DAYS:
144
+ return MemoryLifecycle.WARM
145
+ if days < _COLD_MAX_DAYS:
146
+ return MemoryLifecycle.COLD
147
+ return MemoryLifecycle.ARCHIVED
148
+
149
+ @staticmethod
150
+ def _days_since(iso_date: str) -> float:
151
+ """Days since an ISO date string."""
152
+ if not iso_date:
153
+ return 0.0
154
+ try:
155
+ dt = datetime.fromisoformat(iso_date)
156
+ return (datetime.now(UTC) - dt).total_seconds() / 86400.0
157
+ except (ValueError, TypeError):
158
+ return 0.0
@@ -0,0 +1,232 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Named retention rules engine for compliance (GDPR, HIPAA, custom).
6
+
7
+ Rules are bound to profiles. Each rule specifies a retention period
8
+ in days. The engine can identify expired facts and enforce deletion.
9
+
10
+ Retention rules are stored in a dedicated SQLite table and operate
11
+ independently of the main memory lifecycle system.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import sqlite3
18
+ from datetime import datetime, timezone
19
+ from typing import Any, Optional
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _RETENTION_RULES_TABLE = """
24
+ CREATE TABLE IF NOT EXISTS retention_rules (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ profile_id TEXT NOT NULL,
27
+ rule_name TEXT NOT NULL,
28
+ days INTEGER NOT NULL,
29
+ description TEXT DEFAULT '',
30
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
31
+ UNIQUE(profile_id, rule_name)
32
+ )
33
+ """
34
+
35
+ _FACTS_TABLE_CHECK = """
36
+ SELECT name FROM sqlite_master
37
+ WHERE type='table' AND name='atomic_facts'
38
+ """
39
+
40
+
41
+ class RetentionEngine:
42
+ """Named retention rules for compliance (GDPR, HIPAA, custom).
43
+
44
+ Rules are bound to profiles. Each rule specifies a retention period.
45
+ The engine can identify expired facts and enforce deletion.
46
+ """
47
+
48
+ def __init__(self, db: sqlite3.Connection) -> None:
49
+ self._db = db
50
+ self._ensure_table()
51
+
52
+ # ------------------------------------------------------------------
53
+ # Internal helpers
54
+ # ------------------------------------------------------------------
55
+
56
+ def _ensure_table(self) -> None:
57
+ """Create the retention_rules table if it does not exist."""
58
+ self._db.execute(_RETENTION_RULES_TABLE)
59
+ self._db.commit()
60
+
61
+ def _has_facts_table(self) -> bool:
62
+ """Check if atomic_facts table exists in the database."""
63
+ row = self._db.execute(_FACTS_TABLE_CHECK).fetchone()
64
+ return row is not None
65
+
66
+ @staticmethod
67
+ def _age_in_days(created_at_str: str) -> float:
68
+ """Calculate age from an ISO timestamp string to now, in days."""
69
+ try:
70
+ created = datetime.fromisoformat(created_at_str)
71
+ if created.tzinfo is None:
72
+ created = created.replace(tzinfo=timezone.utc)
73
+ now = datetime.now(timezone.utc)
74
+ return (now - created).total_seconds() / 86400.0
75
+ except (ValueError, TypeError):
76
+ return 0.0
77
+
78
+ # ------------------------------------------------------------------
79
+ # Rule management
80
+ # ------------------------------------------------------------------
81
+
82
+ def add_rule(
83
+ self,
84
+ profile_id: str,
85
+ rule_name: str,
86
+ days: int,
87
+ description: str = "",
88
+ ) -> None:
89
+ """Add a retention rule to a profile.
90
+
91
+ Args:
92
+ profile_id: Profile this rule applies to.
93
+ rule_name: Human-readable name (e.g. 'GDPR-30d').
94
+ days: Retention period in days.
95
+ description: Optional description of the rule.
96
+
97
+ Raises:
98
+ sqlite3.IntegrityError: If rule_name already exists for profile.
99
+ """
100
+ self._db.execute(
101
+ "INSERT OR REPLACE INTO retention_rules "
102
+ "(profile_id, rule_name, days, description) "
103
+ "VALUES (?, ?, ?, ?)",
104
+ (profile_id, rule_name, days, description),
105
+ )
106
+ self._db.commit()
107
+ logger.info(
108
+ "Added retention rule '%s' (%d days) to profile '%s'",
109
+ rule_name, days, profile_id,
110
+ )
111
+
112
+ def remove_rule(self, profile_id: str, rule_name: str) -> None:
113
+ """Remove a retention rule.
114
+
115
+ Args:
116
+ profile_id: Profile the rule belongs to.
117
+ rule_name: Name of the rule to remove.
118
+ """
119
+ self._db.execute(
120
+ "DELETE FROM retention_rules "
121
+ "WHERE profile_id = ? AND rule_name = ?",
122
+ (profile_id, rule_name),
123
+ )
124
+ self._db.commit()
125
+ logger.info(
126
+ "Removed retention rule '%s' from profile '%s'",
127
+ rule_name, profile_id,
128
+ )
129
+
130
+ def get_rules(self, profile_id: str) -> list[dict[str, Any]]:
131
+ """Get all retention rules for a profile.
132
+
133
+ Args:
134
+ profile_id: Profile to query rules for.
135
+
136
+ Returns:
137
+ List of rule dicts with keys: rule_name, days, description.
138
+ """
139
+ rows = self._db.execute(
140
+ "SELECT rule_name, days, description, created_at "
141
+ "FROM retention_rules WHERE profile_id = ? "
142
+ "ORDER BY id",
143
+ (profile_id,),
144
+ ).fetchall()
145
+ return [
146
+ {
147
+ "rule_name": r[0],
148
+ "days": r[1],
149
+ "description": r[2],
150
+ "created_at": r[3],
151
+ }
152
+ for r in rows
153
+ ]
154
+
155
+ # ------------------------------------------------------------------
156
+ # Expiration detection
157
+ # ------------------------------------------------------------------
158
+
159
+ def get_expired_facts(self, profile_id: str) -> list[str]:
160
+ """Get fact IDs that have exceeded their retention period.
161
+
162
+ Checks each fact's created_at against the profile's shortest
163
+ retention rule. A fact is expired if its age exceeds the
164
+ minimum retention days across all rules for the profile.
165
+
166
+ Args:
167
+ profile_id: Profile to check facts for.
168
+
169
+ Returns:
170
+ List of expired fact IDs (as strings).
171
+ """
172
+ rules = self.get_rules(profile_id)
173
+ if not rules:
174
+ return []
175
+
176
+ # Use the shortest retention period (most restrictive)
177
+ min_days = min(r["days"] for r in rules)
178
+
179
+ if not self._has_facts_table():
180
+ return []
181
+
182
+ rows = self._db.execute(
183
+ "SELECT id, created_at FROM atomic_facts "
184
+ "WHERE profile_id = ?",
185
+ (profile_id,),
186
+ ).fetchall()
187
+
188
+ expired: list[str] = []
189
+ for row in rows:
190
+ fact_id = str(row[0])
191
+ created_at = row[1] if len(row) > 1 else None
192
+ if created_at and self._age_in_days(created_at) > min_days:
193
+ expired.append(fact_id)
194
+
195
+ return expired
196
+
197
+ # ------------------------------------------------------------------
198
+ # Enforcement
199
+ # ------------------------------------------------------------------
200
+
201
+ def enforce(self, profile_id: str) -> dict[str, Any]:
202
+ """Enforce retention rules — delete expired facts.
203
+
204
+ Finds all expired facts for the profile and deletes them.
205
+
206
+ Args:
207
+ profile_id: Profile to enforce rules on.
208
+
209
+ Returns:
210
+ Dict with keys: deleted_count, expired_ids, profile_id.
211
+ """
212
+ expired_ids = self.get_expired_facts(profile_id)
213
+ deleted_count = 0
214
+
215
+ if expired_ids and self._has_facts_table():
216
+ placeholders = ",".join("?" for _ in expired_ids)
217
+ self._db.execute(
218
+ f"DELETE FROM atomic_facts WHERE id IN ({placeholders})",
219
+ [int(fid) for fid in expired_ids],
220
+ )
221
+ self._db.commit()
222
+ deleted_count = len(expired_ids)
223
+ logger.info(
224
+ "Retention enforcement: deleted %d facts from profile '%s'",
225
+ deleted_count, profile_id,
226
+ )
227
+
228
+ return {
229
+ "profile_id": profile_id,
230
+ "deleted_count": deleted_count,
231
+ "expired_ids": expired_ids,
232
+ }