superlocalmemory 2.8.5 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +9 -1
  3. package/NOTICE +63 -0
  4. package/README.md +165 -480
  5. package/bin/slm +17 -449
  6. package/bin/slm-npm +2 -2
  7. package/bin/slm.bat +4 -2
  8. package/conftest.py +5 -0
  9. package/docs/api-reference.md +284 -0
  10. package/docs/architecture.md +149 -0
  11. package/docs/auto-memory.md +150 -0
  12. package/docs/cli-reference.md +276 -0
  13. package/docs/compliance.md +191 -0
  14. package/docs/configuration.md +182 -0
  15. package/docs/getting-started.md +102 -0
  16. package/docs/ide-setup.md +261 -0
  17. package/docs/mcp-tools.md +220 -0
  18. package/docs/migration-from-v2.md +170 -0
  19. package/docs/profiles.md +173 -0
  20. package/docs/troubleshooting.md +310 -0
  21. package/{configs → ide/configs}/antigravity-mcp.json +3 -3
  22. package/ide/configs/chatgpt-desktop-mcp.json +16 -0
  23. package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
  24. package/{configs → ide/configs}/codex-mcp.toml +4 -4
  25. package/{configs → ide/configs}/continue-mcp.yaml +4 -3
  26. package/{configs → ide/configs}/continue-skills.yaml +6 -6
  27. package/ide/configs/cursor-mcp.json +15 -0
  28. package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
  29. package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
  30. package/{configs → ide/configs}/opencode-mcp.json +2 -2
  31. package/{configs → ide/configs}/perplexity-mcp.json +2 -2
  32. package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
  33. package/{configs → ide/configs}/windsurf-mcp.json +3 -3
  34. package/{configs → ide/configs}/zed-mcp.json +2 -2
  35. package/{hooks → ide/hooks}/context-hook.js +9 -20
  36. package/ide/hooks/memory-list-skill.js +70 -0
  37. package/ide/hooks/memory-profile-skill.js +101 -0
  38. package/ide/hooks/memory-recall-skill.js +62 -0
  39. package/ide/hooks/memory-remember-skill.js +68 -0
  40. package/ide/hooks/memory-reset-skill.js +160 -0
  41. package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
  42. package/ide/integrations/langchain/README.md +106 -0
  43. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
  44. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
  45. package/ide/integrations/langchain/pyproject.toml +38 -0
  46. package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
  47. package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
  48. package/ide/integrations/langchain/tests/test_security.py +117 -0
  49. package/ide/integrations/llamaindex/README.md +81 -0
  50. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
  51. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
  52. package/ide/integrations/llamaindex/pyproject.toml +43 -0
  53. package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
  54. package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
  55. package/ide/integrations/llamaindex/tests/test_security.py +241 -0
  56. package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
  57. package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
  58. package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
  59. package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
  60. package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
  61. package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
  62. package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
  63. package/package.json +13 -22
  64. package/pyproject.toml +85 -0
  65. package/scripts/build-dmg.sh +417 -0
  66. package/scripts/install-skills.ps1 +334 -0
  67. package/{install.ps1 → scripts/install.ps1} +36 -4
  68. package/{install.sh → scripts/install.sh} +14 -13
  69. package/scripts/postinstall.js +2 -2
  70. package/scripts/start-dashboard.ps1 +52 -0
  71. package/scripts/start-dashboard.sh +41 -0
  72. package/scripts/sync-wiki.ps1 +127 -0
  73. package/scripts/sync-wiki.sh +82 -0
  74. package/scripts/test-dmg.sh +161 -0
  75. package/scripts/test-npm-package.ps1 +252 -0
  76. package/scripts/test-npm-package.sh +207 -0
  77. package/scripts/verify-install.ps1 +294 -0
  78. package/scripts/verify-install.sh +266 -0
  79. package/src/superlocalmemory/__init__.py +0 -0
  80. package/src/superlocalmemory/attribution/__init__.py +9 -0
  81. package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
  82. package/src/superlocalmemory/attribution/signer.py +153 -0
  83. package/src/superlocalmemory/attribution/watermark.py +189 -0
  84. package/src/superlocalmemory/cli/__init__.py +5 -0
  85. package/src/superlocalmemory/cli/commands.py +245 -0
  86. package/src/superlocalmemory/cli/main.py +89 -0
  87. package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
  88. package/src/superlocalmemory/cli/post_install.py +99 -0
  89. package/src/superlocalmemory/cli/setup_wizard.py +129 -0
  90. package/src/superlocalmemory/compliance/__init__.py +0 -0
  91. package/src/superlocalmemory/compliance/abac.py +204 -0
  92. package/src/superlocalmemory/compliance/audit.py +314 -0
  93. package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
  94. package/src/superlocalmemory/compliance/gdpr.py +294 -0
  95. package/src/superlocalmemory/compliance/lifecycle.py +158 -0
  96. package/src/superlocalmemory/compliance/retention.py +232 -0
  97. package/src/superlocalmemory/compliance/scheduler.py +148 -0
  98. package/src/superlocalmemory/core/__init__.py +0 -0
  99. package/src/superlocalmemory/core/config.py +391 -0
  100. package/src/superlocalmemory/core/embeddings.py +293 -0
  101. package/src/superlocalmemory/core/engine.py +701 -0
  102. package/src/superlocalmemory/core/hooks.py +65 -0
  103. package/src/superlocalmemory/core/maintenance.py +172 -0
  104. package/src/superlocalmemory/core/modes.py +140 -0
  105. package/src/superlocalmemory/core/profiles.py +234 -0
  106. package/src/superlocalmemory/core/registry.py +117 -0
  107. package/src/superlocalmemory/dynamics/__init__.py +0 -0
  108. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
  109. package/src/superlocalmemory/encoding/__init__.py +0 -0
  110. package/src/superlocalmemory/encoding/consolidator.py +485 -0
  111. package/src/superlocalmemory/encoding/emotional.py +125 -0
  112. package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
  113. package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
  114. package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
  115. package/src/superlocalmemory/encoding/foresight.py +91 -0
  116. package/src/superlocalmemory/encoding/graph_builder.py +302 -0
  117. package/src/superlocalmemory/encoding/observation_builder.py +160 -0
  118. package/src/superlocalmemory/encoding/scene_builder.py +183 -0
  119. package/src/superlocalmemory/encoding/signal_inference.py +90 -0
  120. package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
  121. package/src/superlocalmemory/encoding/type_router.py +235 -0
  122. package/src/superlocalmemory/hooks/__init__.py +3 -0
  123. package/src/superlocalmemory/hooks/auto_capture.py +111 -0
  124. package/src/superlocalmemory/hooks/auto_recall.py +93 -0
  125. package/src/superlocalmemory/hooks/ide_connector.py +204 -0
  126. package/src/superlocalmemory/hooks/rules_engine.py +99 -0
  127. package/src/superlocalmemory/infra/__init__.py +3 -0
  128. package/src/superlocalmemory/infra/auth_middleware.py +82 -0
  129. package/src/superlocalmemory/infra/backup.py +317 -0
  130. package/src/superlocalmemory/infra/cache_manager.py +267 -0
  131. package/src/superlocalmemory/infra/event_bus.py +381 -0
  132. package/src/superlocalmemory/infra/rate_limiter.py +135 -0
  133. package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
  134. package/src/superlocalmemory/learning/__init__.py +0 -0
  135. package/src/superlocalmemory/learning/adaptive.py +172 -0
  136. package/src/superlocalmemory/learning/behavioral.py +490 -0
  137. package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
  138. package/src/superlocalmemory/learning/bootstrap.py +298 -0
  139. package/src/superlocalmemory/learning/cross_project.py +399 -0
  140. package/src/superlocalmemory/learning/database.py +376 -0
  141. package/src/superlocalmemory/learning/engagement.py +323 -0
  142. package/src/superlocalmemory/learning/features.py +138 -0
  143. package/src/superlocalmemory/learning/feedback.py +316 -0
  144. package/src/superlocalmemory/learning/outcomes.py +255 -0
  145. package/src/superlocalmemory/learning/project_context.py +366 -0
  146. package/src/superlocalmemory/learning/ranker.py +155 -0
  147. package/src/superlocalmemory/learning/source_quality.py +303 -0
  148. package/src/superlocalmemory/learning/workflows.py +309 -0
  149. package/src/superlocalmemory/llm/__init__.py +0 -0
  150. package/src/superlocalmemory/llm/backbone.py +316 -0
  151. package/src/superlocalmemory/math/__init__.py +0 -0
  152. package/src/superlocalmemory/math/fisher.py +356 -0
  153. package/src/superlocalmemory/math/langevin.py +398 -0
  154. package/src/superlocalmemory/math/sheaf.py +257 -0
  155. package/src/superlocalmemory/mcp/__init__.py +0 -0
  156. package/src/superlocalmemory/mcp/resources.py +245 -0
  157. package/src/superlocalmemory/mcp/server.py +61 -0
  158. package/src/superlocalmemory/mcp/tools.py +18 -0
  159. package/src/superlocalmemory/mcp/tools_core.py +305 -0
  160. package/src/superlocalmemory/mcp/tools_v28.py +223 -0
  161. package/src/superlocalmemory/mcp/tools_v3.py +286 -0
  162. package/src/superlocalmemory/retrieval/__init__.py +0 -0
  163. package/src/superlocalmemory/retrieval/agentic.py +295 -0
  164. package/src/superlocalmemory/retrieval/ann_index.py +223 -0
  165. package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
  166. package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
  167. package/src/superlocalmemory/retrieval/engine.py +390 -0
  168. package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
  169. package/src/superlocalmemory/retrieval/fusion.py +78 -0
  170. package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
  171. package/src/superlocalmemory/retrieval/reranker.py +154 -0
  172. package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
  173. package/src/superlocalmemory/retrieval/strategy.py +96 -0
  174. package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
  175. package/src/superlocalmemory/server/__init__.py +1 -0
  176. package/src/superlocalmemory/server/api.py +248 -0
  177. package/src/superlocalmemory/server/routes/__init__.py +4 -0
  178. package/src/superlocalmemory/server/routes/agents.py +107 -0
  179. package/src/superlocalmemory/server/routes/backup.py +91 -0
  180. package/src/superlocalmemory/server/routes/behavioral.py +127 -0
  181. package/src/superlocalmemory/server/routes/compliance.py +160 -0
  182. package/src/superlocalmemory/server/routes/data_io.py +188 -0
  183. package/src/superlocalmemory/server/routes/events.py +183 -0
  184. package/src/superlocalmemory/server/routes/helpers.py +85 -0
  185. package/src/superlocalmemory/server/routes/learning.py +273 -0
  186. package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
  187. package/src/superlocalmemory/server/routes/memories.py +399 -0
  188. package/src/superlocalmemory/server/routes/profiles.py +219 -0
  189. package/src/superlocalmemory/server/routes/stats.py +346 -0
  190. package/src/superlocalmemory/server/routes/v3_api.py +365 -0
  191. package/src/superlocalmemory/server/routes/ws.py +82 -0
  192. package/src/superlocalmemory/server/security_middleware.py +57 -0
  193. package/src/superlocalmemory/server/ui.py +245 -0
  194. package/src/superlocalmemory/storage/__init__.py +0 -0
  195. package/src/superlocalmemory/storage/access_control.py +182 -0
  196. package/src/superlocalmemory/storage/database.py +594 -0
  197. package/src/superlocalmemory/storage/migrations.py +303 -0
  198. package/src/superlocalmemory/storage/models.py +406 -0
  199. package/src/superlocalmemory/storage/schema.py +726 -0
  200. package/src/superlocalmemory/storage/v2_migrator.py +317 -0
  201. package/src/superlocalmemory/trust/__init__.py +0 -0
  202. package/src/superlocalmemory/trust/gate.py +130 -0
  203. package/src/superlocalmemory/trust/provenance.py +124 -0
  204. package/src/superlocalmemory/trust/scorer.py +347 -0
  205. package/src/superlocalmemory/trust/signals.py +153 -0
  206. package/ui/index.html +278 -5
  207. package/ui/js/auto-settings.js +70 -0
  208. package/ui/js/dashboard.js +90 -0
  209. package/ui/js/fact-detail.js +92 -0
  210. package/ui/js/feedback.js +2 -2
  211. package/ui/js/ide-status.js +102 -0
  212. package/ui/js/math-health.js +98 -0
  213. package/ui/js/recall-lab.js +127 -0
  214. package/ui/js/settings.js +2 -2
  215. package/ui/js/trust-dashboard.js +73 -0
  216. package/api_server.py +0 -724
  217. package/bin/aider-smart +0 -72
  218. package/bin/superlocalmemoryv2-learning +0 -4
  219. package/bin/superlocalmemoryv2-list +0 -3
  220. package/bin/superlocalmemoryv2-patterns +0 -4
  221. package/bin/superlocalmemoryv2-profile +0 -3
  222. package/bin/superlocalmemoryv2-recall +0 -3
  223. package/bin/superlocalmemoryv2-remember +0 -3
  224. package/bin/superlocalmemoryv2-reset +0 -3
  225. package/bin/superlocalmemoryv2-status +0 -3
  226. package/configs/chatgpt-desktop-mcp.json +0 -16
  227. package/configs/cursor-mcp.json +0 -15
  228. package/docs/SECURITY-QUICK-REFERENCE.md +0 -214
  229. package/hooks/memory-list-skill.js +0 -139
  230. package/hooks/memory-profile-skill.js +0 -273
  231. package/hooks/memory-recall-skill.js +0 -114
  232. package/hooks/memory-remember-skill.js +0 -127
  233. package/hooks/memory-reset-skill.js +0 -274
  234. package/mcp_server.py +0 -1800
  235. package/requirements-core.txt +0 -22
  236. package/requirements-learning.txt +0 -12
  237. package/requirements.txt +0 -12
  238. package/src/agent_registry.py +0 -411
  239. package/src/auth_middleware.py +0 -61
  240. package/src/auto_backup.py +0 -459
  241. package/src/behavioral/__init__.py +0 -49
  242. package/src/behavioral/behavioral_listener.py +0 -203
  243. package/src/behavioral/behavioral_patterns.py +0 -275
  244. package/src/behavioral/cross_project_transfer.py +0 -206
  245. package/src/behavioral/outcome_inference.py +0 -194
  246. package/src/behavioral/outcome_tracker.py +0 -193
  247. package/src/behavioral/tests/__init__.py +0 -4
  248. package/src/behavioral/tests/test_behavioral_integration.py +0 -108
  249. package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
  250. package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
  251. package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
  252. package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
  253. package/src/behavioral/tests/test_outcome_inference.py +0 -107
  254. package/src/behavioral/tests/test_outcome_tracker.py +0 -96
  255. package/src/cache_manager.py +0 -518
  256. package/src/compliance/__init__.py +0 -48
  257. package/src/compliance/abac_engine.py +0 -149
  258. package/src/compliance/abac_middleware.py +0 -116
  259. package/src/compliance/audit_db.py +0 -215
  260. package/src/compliance/audit_logger.py +0 -148
  261. package/src/compliance/retention_manager.py +0 -289
  262. package/src/compliance/retention_scheduler.py +0 -186
  263. package/src/compliance/tests/__init__.py +0 -4
  264. package/src/compliance/tests/test_abac_enforcement.py +0 -95
  265. package/src/compliance/tests/test_abac_engine.py +0 -124
  266. package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
  267. package/src/compliance/tests/test_audit_db.py +0 -123
  268. package/src/compliance/tests/test_audit_logger.py +0 -98
  269. package/src/compliance/tests/test_mcp_audit.py +0 -128
  270. package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
  271. package/src/compliance/tests/test_retention_manager.py +0 -131
  272. package/src/compliance/tests/test_retention_scheduler.py +0 -99
  273. package/src/compression/__init__.py +0 -25
  274. package/src/compression/cli.py +0 -150
  275. package/src/compression/cold_storage.py +0 -217
  276. package/src/compression/config.py +0 -72
  277. package/src/compression/orchestrator.py +0 -133
  278. package/src/compression/tier2_compressor.py +0 -228
  279. package/src/compression/tier3_compressor.py +0 -153
  280. package/src/compression/tier_classifier.py +0 -148
  281. package/src/db_connection_manager.py +0 -536
  282. package/src/embedding_engine.py +0 -63
  283. package/src/embeddings/__init__.py +0 -47
  284. package/src/embeddings/cache.py +0 -70
  285. package/src/embeddings/cli.py +0 -113
  286. package/src/embeddings/constants.py +0 -47
  287. package/src/embeddings/database.py +0 -91
  288. package/src/embeddings/engine.py +0 -247
  289. package/src/embeddings/model_loader.py +0 -145
  290. package/src/event_bus.py +0 -562
  291. package/src/graph/__init__.py +0 -36
  292. package/src/graph/build_helpers.py +0 -74
  293. package/src/graph/cli.py +0 -87
  294. package/src/graph/cluster_builder.py +0 -188
  295. package/src/graph/cluster_summary.py +0 -148
  296. package/src/graph/constants.py +0 -47
  297. package/src/graph/edge_builder.py +0 -162
  298. package/src/graph/entity_extractor.py +0 -95
  299. package/src/graph/graph_core.py +0 -226
  300. package/src/graph/graph_search.py +0 -231
  301. package/src/graph/hierarchical.py +0 -207
  302. package/src/graph/schema.py +0 -99
  303. package/src/graph_engine.py +0 -52
  304. package/src/hnsw_index.py +0 -628
  305. package/src/hybrid_search.py +0 -46
  306. package/src/learning/__init__.py +0 -217
  307. package/src/learning/adaptive_ranker.py +0 -682
  308. package/src/learning/bootstrap/__init__.py +0 -69
  309. package/src/learning/bootstrap/constants.py +0 -93
  310. package/src/learning/bootstrap/db_queries.py +0 -316
  311. package/src/learning/bootstrap/sampling.py +0 -82
  312. package/src/learning/bootstrap/text_utils.py +0 -71
  313. package/src/learning/cross_project_aggregator.py +0 -857
  314. package/src/learning/db/__init__.py +0 -40
  315. package/src/learning/db/constants.py +0 -44
  316. package/src/learning/db/schema.py +0 -279
  317. package/src/learning/engagement_tracker.py +0 -628
  318. package/src/learning/feature_extractor.py +0 -708
  319. package/src/learning/feedback_collector.py +0 -806
  320. package/src/learning/learning_db.py +0 -915
  321. package/src/learning/project_context_manager.py +0 -572
  322. package/src/learning/ranking/__init__.py +0 -33
  323. package/src/learning/ranking/constants.py +0 -84
  324. package/src/learning/ranking/helpers.py +0 -278
  325. package/src/learning/source_quality_scorer.py +0 -676
  326. package/src/learning/synthetic_bootstrap.py +0 -755
  327. package/src/learning/tests/test_adaptive_ranker.py +0 -325
  328. package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
  329. package/src/learning/tests/test_aggregator.py +0 -306
  330. package/src/learning/tests/test_auto_retrain_v28.py +0 -35
  331. package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
  332. package/src/learning/tests/test_feature_extractor_v28.py +0 -93
  333. package/src/learning/tests/test_feedback_collector.py +0 -294
  334. package/src/learning/tests/test_learning_db.py +0 -602
  335. package/src/learning/tests/test_learning_db_v28.py +0 -110
  336. package/src/learning/tests/test_learning_init_v28.py +0 -48
  337. package/src/learning/tests/test_outcome_signals.py +0 -48
  338. package/src/learning/tests/test_project_context.py +0 -292
  339. package/src/learning/tests/test_schema_migration.py +0 -319
  340. package/src/learning/tests/test_signal_inference.py +0 -397
  341. package/src/learning/tests/test_source_quality.py +0 -351
  342. package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
  343. package/src/learning/tests/test_workflow_miner.py +0 -318
  344. package/src/learning/workflow_pattern_miner.py +0 -655
  345. package/src/lifecycle/__init__.py +0 -54
  346. package/src/lifecycle/bounded_growth.py +0 -239
  347. package/src/lifecycle/compaction_engine.py +0 -226
  348. package/src/lifecycle/lifecycle_engine.py +0 -355
  349. package/src/lifecycle/lifecycle_evaluator.py +0 -257
  350. package/src/lifecycle/lifecycle_scheduler.py +0 -130
  351. package/src/lifecycle/retention_policy.py +0 -285
  352. package/src/lifecycle/tests/test_bounded_growth.py +0 -193
  353. package/src/lifecycle/tests/test_compaction.py +0 -179
  354. package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
  355. package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
  356. package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
  357. package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
  358. package/src/lifecycle/tests/test_mcp_compact.py +0 -149
  359. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
  360. package/src/lifecycle/tests/test_retention_policy.py +0 -162
  361. package/src/mcp_tools_v28.py +0 -281
  362. package/src/memory/__init__.py +0 -36
  363. package/src/memory/cli.py +0 -205
  364. package/src/memory/constants.py +0 -39
  365. package/src/memory/helpers.py +0 -28
  366. package/src/memory/schema.py +0 -166
  367. package/src/memory-profiles.py +0 -595
  368. package/src/memory-reset.py +0 -491
  369. package/src/memory_compression.py +0 -989
  370. package/src/memory_store_v2.py +0 -1155
  371. package/src/migrate_v1_to_v2.py +0 -629
  372. package/src/pattern_learner.py +0 -34
  373. package/src/patterns/__init__.py +0 -24
  374. package/src/patterns/analyzers.py +0 -251
  375. package/src/patterns/learner.py +0 -271
  376. package/src/patterns/scoring.py +0 -171
  377. package/src/patterns/store.py +0 -225
  378. package/src/patterns/terminology.py +0 -140
  379. package/src/provenance_tracker.py +0 -312
  380. package/src/qualixar_attribution.py +0 -139
  381. package/src/qualixar_watermark.py +0 -78
  382. package/src/query_optimizer.py +0 -511
  383. package/src/rate_limiter.py +0 -83
  384. package/src/search/__init__.py +0 -20
  385. package/src/search/cli.py +0 -77
  386. package/src/search/constants.py +0 -26
  387. package/src/search/engine.py +0 -241
  388. package/src/search/fusion.py +0 -122
  389. package/src/search/index_loader.py +0 -114
  390. package/src/search/methods.py +0 -162
  391. package/src/search_engine_v2.py +0 -401
  392. package/src/setup_validator.py +0 -482
  393. package/src/subscription_manager.py +0 -391
  394. package/src/tree/__init__.py +0 -59
  395. package/src/tree/builder.py +0 -185
  396. package/src/tree/nodes.py +0 -202
  397. package/src/tree/queries.py +0 -257
  398. package/src/tree/schema.py +0 -80
  399. package/src/tree_manager.py +0 -19
  400. package/src/trust/__init__.py +0 -45
  401. package/src/trust/constants.py +0 -66
  402. package/src/trust/queries.py +0 -157
  403. package/src/trust/schema.py +0 -95
  404. package/src/trust/scorer.py +0 -299
  405. package/src/trust/signals.py +0 -95
  406. package/src/trust_scorer.py +0 -44
  407. package/ui/app.js +0 -1588
  408. package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
  409. package/ui/js/graph-cytoscape.js +0 -1168
  410. package/ui/js/graph-d3-backup.js +0 -32
  411. package/ui/js/graph.js +0 -32
  412. package/ui_server.py +0 -266
  413. /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
  414. /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
  415. /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
  416. /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
  417. /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
  418. /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
  419. /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
  420. /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
  421. /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
  422. /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
  423. /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
  424. /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
  425. /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
  426. /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
  427. /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
  428. /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
  429. /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
  430. /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
  431. /package/{completions → ide/completions}/slm.bash +0 -0
  432. /package/{completions → ide/completions}/slm.zsh +0 -0
  433. /package/{configs → ide/configs}/cody-commands.json +0 -0
  434. /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
@@ -0,0 +1,183 @@
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 — Scene Builder (Memory Clustering).
6
+
7
+ Groups related facts into thematic scenes (EverMemOS MemScene pattern).
8
+ Scenes provide contextual retrieval — related facts come together.
9
+
10
+ V1 had this module but NEVER CALLED it. Now wired into the encoding pipeline.
11
+
12
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import logging
19
+ from datetime import UTC, datetime
20
+
21
+ from superlocalmemory.storage.models import AtomicFact, MemoryScene
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Similarity threshold for assigning fact to existing scene
26
+ _ASSIGN_THRESHOLD = 0.6
27
+
28
+
29
+ class SceneBuilder:
30
+ """Cluster related facts into thematic scenes.
31
+
32
+ When a new fact arrives:
33
+ 1. Compute similarity to existing scenes (via scene theme embedding)
34
+ 2. If above threshold: assign to nearest scene, update scene
35
+ 3. If below threshold: create new scene
36
+ """
37
+
38
+ def __init__(self, db, embedder=None) -> None:
39
+ self._db = db
40
+ self._embedder = embedder
41
+ self._scene_embeddings_cache: dict[str, list[float]] = {}
42
+
43
+ def assign_to_scene(
44
+ self,
45
+ new_fact: AtomicFact,
46
+ profile_id: str,
47
+ ) -> MemoryScene:
48
+ """Assign a fact to an existing scene or create a new one.
49
+
50
+ Always embeds the incoming fact content (when embedder is available)
51
+ so that the embedding is ready for comparison against existing scenes.
52
+ """
53
+ if self._embedder is None:
54
+ return self._create_scene(new_fact, profile_id)
55
+
56
+ # Always compute fact embedding first — needed for comparisons
57
+ fact_emb = self._embedder.embed(new_fact.content)
58
+
59
+ scenes = self._get_scenes(profile_id)
60
+ if not scenes:
61
+ return self._create_scene(new_fact, profile_id)
62
+
63
+ # Find best matching scene
64
+ best_scene: MemoryScene | None = None
65
+ best_sim = -1.0
66
+
67
+ for scene in scenes:
68
+ # Use cached embedding if available, otherwise compute fresh
69
+ if scene.theme in self._scene_embeddings_cache:
70
+ theme_emb = self._scene_embeddings_cache[scene.theme]
71
+ else:
72
+ theme_emb = self._embedder.embed(scene.theme)
73
+ self._scene_embeddings_cache[scene.theme] = theme_emb
74
+ sim = _cosine(fact_emb, theme_emb)
75
+ if sim > best_sim:
76
+ best_sim = sim
77
+ best_scene = scene
78
+
79
+ if best_scene is not None and best_sim >= _ASSIGN_THRESHOLD:
80
+ return self._add_to_scene(best_scene, new_fact, profile_id)
81
+
82
+ return self._create_scene(new_fact, profile_id)
83
+
84
+ def get_scene_for_fact(self, fact_id: str, profile_id: str) -> MemoryScene | None:
85
+ """Get the scene containing a specific fact."""
86
+ rows = self._db.execute(
87
+ "SELECT * FROM memory_scenes WHERE profile_id = ?", (profile_id,)
88
+ )
89
+ for row in rows:
90
+ d = dict(row)
91
+ fids = json.loads(d.get("fact_ids_json", "[]"))
92
+ if fact_id in fids:
93
+ return self._row_to_scene(d)
94
+ return None
95
+
96
+ def get_all_scenes(self, profile_id: str) -> list[MemoryScene]:
97
+ """Get all scenes for a profile."""
98
+ return self._get_scenes(profile_id)
99
+
100
+ # -- Internal ----------------------------------------------------------
101
+
102
+ def _create_scene(self, fact: AtomicFact, profile_id: str) -> MemoryScene:
103
+ """Create a new scene from a single fact.
104
+
105
+ Pre-computes and caches the theme embedding for efficient later
106
+ comparisons in assign_to_scene.
107
+ """
108
+ theme = fact.content[:200]
109
+ # Pre-compute theme embedding for future comparisons
110
+ if self._embedder is not None:
111
+ self._scene_embeddings_cache[theme] = self._embedder.embed(theme)
112
+
113
+ scene = MemoryScene(
114
+ profile_id=profile_id,
115
+ theme=theme,
116
+ fact_ids=[fact.fact_id],
117
+ entity_ids=list(fact.canonical_entities),
118
+ created_at=datetime.now(UTC).isoformat(),
119
+ last_updated=datetime.now(UTC).isoformat(),
120
+ )
121
+ self._save_scene(scene)
122
+ return scene
123
+
124
+ def _add_to_scene(
125
+ self, scene: MemoryScene, fact: AtomicFact, profile_id: str
126
+ ) -> MemoryScene:
127
+ """Add a fact to an existing scene."""
128
+ new_fact_ids = [*scene.fact_ids, fact.fact_id]
129
+ new_entity_ids = list(set(scene.entity_ids) | set(fact.canonical_entities))
130
+ updated = MemoryScene(
131
+ scene_id=scene.scene_id,
132
+ profile_id=profile_id,
133
+ theme=scene.theme,
134
+ fact_ids=new_fact_ids,
135
+ entity_ids=new_entity_ids,
136
+ created_at=scene.created_at,
137
+ last_updated=datetime.now(UTC).isoformat(),
138
+ )
139
+ self._save_scene(updated)
140
+ return updated
141
+
142
+ def _get_scenes(self, profile_id: str) -> list[MemoryScene]:
143
+ """Load all scenes from DB."""
144
+ rows = self._db.execute(
145
+ "SELECT * FROM memory_scenes WHERE profile_id = ? ORDER BY last_updated DESC",
146
+ (profile_id,),
147
+ )
148
+ return [self._row_to_scene(dict(r)) for r in rows]
149
+
150
+ def _save_scene(self, scene: MemoryScene) -> None:
151
+ """Upsert scene to DB."""
152
+ self._db.execute(
153
+ """INSERT OR REPLACE INTO memory_scenes
154
+ (scene_id, profile_id, theme, fact_ids_json, entity_ids_json,
155
+ created_at, last_updated)
156
+ VALUES (?, ?, ?, ?, ?, ?, ?)""",
157
+ (
158
+ scene.scene_id, scene.profile_id, scene.theme,
159
+ json.dumps(scene.fact_ids), json.dumps(scene.entity_ids),
160
+ scene.created_at, scene.last_updated,
161
+ ),
162
+ )
163
+
164
+ @staticmethod
165
+ def _row_to_scene(d: dict) -> MemoryScene:
166
+ return MemoryScene(
167
+ scene_id=d["scene_id"],
168
+ profile_id=d["profile_id"],
169
+ theme=d.get("theme", ""),
170
+ fact_ids=json.loads(d.get("fact_ids_json", "[]")),
171
+ entity_ids=json.loads(d.get("entity_ids_json", "[]")),
172
+ created_at=d.get("created_at", ""),
173
+ last_updated=d.get("last_updated", ""),
174
+ )
175
+
176
+
177
+ def _cosine(a: list[float], b: list[float]) -> float:
178
+ dot = sum(x * y for x, y in zip(a, b))
179
+ na = sum(x * x for x in a) ** 0.5
180
+ nb = sum(x * x for x in b) ** 0.5
181
+ if na == 0 or nb == 0:
182
+ return 0.0
183
+ return dot / (na * nb)
@@ -0,0 +1,90 @@
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 — Signal Inference (6 Types).
6
+
7
+ Infers the signal type of a memory/fact from its content.
8
+ Ported from V2.8 — used to adjust retrieval channel weights.
9
+
10
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+
17
+ from superlocalmemory.storage.models import SignalType
18
+
19
+ # Compiled patterns for each signal type
20
+ _PATTERNS: dict[SignalType, re.Pattern] = {
21
+ SignalType.EMOTIONAL: re.compile(
22
+ r"\b(happy|sad|angry|frustrated|excited|worried|anxious|"
23
+ r"love|hate|afraid|grateful|disappointed|thrilled|upset|"
24
+ r"feeling|emotion|mood)\b",
25
+ re.IGNORECASE,
26
+ ),
27
+ SignalType.TEMPORAL: re.compile(
28
+ r"\b(when|date|time|schedule|deadline|tomorrow|yesterday|"
29
+ r"last week|next month|ago|soon|later|earlier|"
30
+ r"january|february|march|april|may|june|july|"
31
+ r"august|september|october|november|december|"
32
+ r"monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b",
33
+ re.IGNORECASE,
34
+ ),
35
+ SignalType.OPINION: re.compile(
36
+ r"\b(think|believe|prefer|opinion|recommend|suggest|"
37
+ r"should|better|worse|best|worst|favorite|"
38
+ r"in my view|personally|i feel|i guess)\b",
39
+ re.IGNORECASE,
40
+ ),
41
+ SignalType.REQUEST: re.compile(
42
+ r"\b(please|could you|would you|can you|help|need|"
43
+ r"want|looking for|searching|find|tell me|show me|"
44
+ r"remind me|remember)\b",
45
+ re.IGNORECASE,
46
+ ),
47
+ SignalType.SOCIAL: re.compile(
48
+ r"\b(friend|family|colleague|partner|boss|team|"
49
+ r"relationship|together|married|dating|met with|"
50
+ r"call|message|email|chat)\b",
51
+ re.IGNORECASE,
52
+ ),
53
+ }
54
+
55
+ # Priority order: more specific types checked first.
56
+ # SOCIAL before TEMPORAL because social signals often co-occur with
57
+ # temporal markers ("met yesterday") and the social context is primary.
58
+ _PRIORITY = [
59
+ SignalType.REQUEST,
60
+ SignalType.SOCIAL,
61
+ SignalType.TEMPORAL,
62
+ SignalType.EMOTIONAL,
63
+ SignalType.OPINION,
64
+ ]
65
+
66
+
67
+ def infer_signal(text: str) -> SignalType:
68
+ """Infer the signal type of a text.
69
+
70
+ Returns the most specific matching type, or FACTUAL as default.
71
+ """
72
+ for stype in _PRIORITY:
73
+ pattern = _PATTERNS.get(stype)
74
+ if pattern and pattern.search(text):
75
+ return stype
76
+ return SignalType.FACTUAL
77
+
78
+
79
+ def infer_signal_scores(text: str) -> dict[SignalType, float]:
80
+ """Compute a signal score for each type (0.0-1.0).
81
+
82
+ Useful for multi-signal content that mixes types.
83
+ Score = number of pattern matches / 5 (capped at 1.0).
84
+ """
85
+ scores: dict[SignalType, float] = {}
86
+ for stype, pattern in _PATTERNS.items():
87
+ matches = pattern.findall(text)
88
+ scores[stype] = min(1.0, len(matches) / 5.0)
89
+ scores[SignalType.FACTUAL] = 1.0 - max(scores.values()) if scores else 1.0
90
+ return scores
@@ -0,0 +1,426 @@
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 — Temporal Parser (3-Date Model).
6
+
7
+ Parses and enriches temporal information for every memory:
8
+ 1. observation_date — when the conversation happened
9
+ 2. referenced_date — date mentioned in content ("last Tuesday")
10
+ 3. temporal_interval — [start, end] for duration events
11
+
12
+ Mastra achieved 95.5% temporal reasoning with this pattern.
13
+ V1 stored session_date as raw strings and ignored content dates entirely.
14
+
15
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import re
22
+ from datetime import UTC, datetime, timedelta
23
+ from typing import Any
24
+
25
+ from dateutil.parser import parse as dateutil_parse, ParserError
26
+ from dateutil.relativedelta import relativedelta
27
+
28
+ from superlocalmemory.storage.models import AtomicFact, TemporalEvent
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Compiled regex patterns (compiled once, reused)
34
+ # ---------------------------------------------------------------------------
35
+
36
+ _ISO_DATE = re.compile(r"\b\d{4}-\d{2}-\d{2}\b")
37
+
38
+ _US_DATE = re.compile(r"\b\d{1,2}/\d{1,2}/\d{2,4}\b")
39
+
40
+ _WRITTEN_DATE = re.compile(
41
+ r"\b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|"
42
+ r"Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)"
43
+ r"\s+\d{1,2},?\s*\d{4}\b",
44
+ re.IGNORECASE,
45
+ )
46
+
47
+ _WRITTEN_DATE_DMY = re.compile(
48
+ r"\b\d{1,2}\s+(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|"
49
+ r"Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)"
50
+ r",?\s*\d{4}\b",
51
+ re.IGNORECASE,
52
+ )
53
+
54
+ # "January 2023", "March 2024" — month + year without day
55
+ _MONTH_YEAR = re.compile(
56
+ r"\b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|"
57
+ r"Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)"
58
+ r"\s+\d{4}\b",
59
+ re.IGNORECASE,
60
+ )
61
+
62
+ _WEEKDAYS = r"(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)"
63
+ _TIME_UNITS = r"(?:days?|weeks?|months?|years?)"
64
+
65
+ _RELATIVE = re.compile(
66
+ rf"\b(?:last|next|this)\s+(?:{_WEEKDAYS}|week|month|year|spring|summer|autumn|fall|winter)\b",
67
+ re.IGNORECASE,
68
+ )
69
+
70
+ _AGO = re.compile(
71
+ rf"\b(\d+)\s+{_TIME_UNITS}\s+ago\b",
72
+ re.IGNORECASE,
73
+ )
74
+
75
+ _IN_FUTURE = re.compile(
76
+ rf"\bin\s+(\d+)\s+{_TIME_UNITS}\b",
77
+ re.IGNORECASE,
78
+ )
79
+
80
+ _DURATION = re.compile(
81
+ r"\bfrom\s+(.+?)\s+to\s+(.+?)(?:\.|,|;|$)",
82
+ re.IGNORECASE,
83
+ )
84
+
85
+ _FOR_DURATION = re.compile(
86
+ rf"\bfor\s+(\d+)\s+{_TIME_UNITS}\b",
87
+ re.IGNORECASE,
88
+ )
89
+
90
+ _VAGUE_TERMS: dict[str, int] = {
91
+ "yesterday": -1,
92
+ "today": 0,
93
+ "tomorrow": 1,
94
+ "recently": -7,
95
+ "a while ago": -30,
96
+ "soon": 7,
97
+ "the other day": -3,
98
+ "last night": -1,
99
+ }
100
+
101
+ _SEASON_MONTHS: dict[str, tuple[int, int]] = {
102
+ "spring": (3, 5),
103
+ "summer": (6, 8),
104
+ "autumn": (9, 11),
105
+ "fall": (9, 11),
106
+ "winter": (12, 2),
107
+ }
108
+
109
+ _WEEKDAY_MAP: dict[str, int] = {
110
+ "monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3,
111
+ "friday": 4, "saturday": 5, "sunday": 6,
112
+ }
113
+
114
+
115
+ def _safe_iso(dt: datetime | None) -> str | None:
116
+ """Convert datetime to ISO-8601 string, or None."""
117
+ if dt is None:
118
+ return None
119
+ return dt.strftime("%Y-%m-%dT%H:%M:%S")
120
+
121
+
122
+ def _extract_unit(text: str) -> str:
123
+ """Extract time unit from a matched span (days, weeks, months, years)."""
124
+ lower = text.lower()
125
+ for unit in ("year", "month", "week", "day"):
126
+ if unit in lower:
127
+ return unit
128
+ return "day"
129
+
130
+
131
+ class TemporalParser:
132
+ """3-date temporal parser for memory enrichment.
133
+
134
+ Extracts observation_date, referenced_date, and temporal intervals
135
+ from session dates and fact content. Handles absolute, relative,
136
+ duration, and vague temporal expressions.
137
+ """
138
+
139
+ def __init__(self, reference_date: str | None = None) -> None:
140
+ """Initialize with an optional reference date for relative computation.
141
+
142
+ Args:
143
+ reference_date: ISO-8601 string used as "today" for relative dates.
144
+ Defaults to current UTC time if None.
145
+ """
146
+ if reference_date is not None:
147
+ try:
148
+ parsed = dateutil_parse(reference_date)
149
+ self._ref = parsed.replace(tzinfo=UTC) if parsed.tzinfo is None else parsed
150
+ except (ParserError, ValueError):
151
+ logger.warning("Unparseable reference_date %r, using UTC now", reference_date)
152
+ self._ref = datetime.now(UTC)
153
+ else:
154
+ self._ref = datetime.now(UTC)
155
+
156
+ # ------------------------------------------------------------------
157
+ # Public API
158
+ # ------------------------------------------------------------------
159
+
160
+ def parse_session_date(self, raw_date: str) -> str | None:
161
+ """Parse LoCoMo-style session dates into ISO-8601.
162
+
163
+ Handles formats like:
164
+ - "1:56 pm on 8 May, 2023"
165
+ - "May 8, 2023"
166
+ - "2023-05-08"
167
+ - "8:56 pm on 20 July, 2023"
168
+
169
+ Returns:
170
+ ISO-8601 string or None if unparseable.
171
+ """
172
+ if not raw_date or not raw_date.strip():
173
+ return None
174
+ try:
175
+ dt = dateutil_parse(raw_date, fuzzy=True)
176
+ return _safe_iso(dt)
177
+ except (ParserError, ValueError, OverflowError):
178
+ logger.debug("Could not parse session_date: %r", raw_date)
179
+ return None
180
+
181
+ def extract_dates_from_text(self, text: str) -> dict[str, str | None]:
182
+ """Extract dates mentioned in fact content.
183
+
184
+ Strategy:
185
+ 1. Regex pass for structured date patterns
186
+ 2. Relative date resolution against reference_date
187
+ 3. Vague term mapping to approximate offsets
188
+ 4. Duration extraction for intervals
189
+
190
+ Returns:
191
+ dict with keys: referenced_date, interval_start, interval_end
192
+ All values are ISO-8601 strings or None.
193
+ """
194
+ if not text:
195
+ return {"referenced_date": None, "interval_start": None, "interval_end": None}
196
+
197
+ dates_found: list[datetime] = []
198
+ interval_start: datetime | None = None
199
+ interval_end: datetime | None = None
200
+
201
+ # --- Pass 1: Duration expressions ("from X to Y") ---
202
+ dur_match = _DURATION.search(text)
203
+ if dur_match:
204
+ start_dt = self._try_parse(dur_match.group(1).strip())
205
+ end_dt = self._try_parse(dur_match.group(2).strip())
206
+ if start_dt is not None and end_dt is not None:
207
+ interval_start = start_dt
208
+ interval_end = end_dt
209
+
210
+ # --- Pass 2: "for N units" duration ---
211
+ if interval_start is None:
212
+ for_match = _FOR_DURATION.search(text)
213
+ if for_match:
214
+ count = int(for_match.group(1))
215
+ unit = _extract_unit(for_match.group(0))
216
+ delta = self._unit_delta(count, unit)
217
+ interval_start = self._ref
218
+ interval_end = self._ref + delta
219
+
220
+ # --- Pass 3: Absolute date patterns ---
221
+ for pattern in (_ISO_DATE, _WRITTEN_DATE, _WRITTEN_DATE_DMY, _MONTH_YEAR, _US_DATE):
222
+ for match in pattern.finditer(text):
223
+ dt = self._try_parse(match.group(0))
224
+ if dt is not None:
225
+ dates_found.append(dt)
226
+
227
+ # --- Pass 4: Relative expressions ---
228
+ for match in _RELATIVE.finditer(text):
229
+ dt = self._resolve_relative(match.group(0))
230
+ if dt is not None:
231
+ dates_found.append(dt)
232
+
233
+ # --- Pass 5: "N units ago" ---
234
+ for match in _AGO.finditer(text):
235
+ count = int(match.group(1))
236
+ unit = _extract_unit(match.group(0))
237
+ dt = self._ref - self._unit_delta(count, unit)
238
+ dates_found.append(dt)
239
+
240
+ # --- Pass 6: "in N units" ---
241
+ for match in _IN_FUTURE.finditer(text):
242
+ count = int(match.group(1))
243
+ unit = _extract_unit(match.group(0))
244
+ dt = self._ref + self._unit_delta(count, unit)
245
+ dates_found.append(dt)
246
+
247
+ # --- Pass 7: Vague terms ---
248
+ text_lower = text.lower()
249
+ for term, offset_days in _VAGUE_TERMS.items():
250
+ if term in text_lower:
251
+ dates_found.append(self._ref + timedelta(days=offset_days))
252
+ break # Take first vague match only
253
+
254
+ # --- Assemble result ---
255
+ referenced: datetime | None = None
256
+ if dates_found:
257
+ referenced = dates_found[0]
258
+ # Two distinct dates without explicit "from...to" -> interval
259
+ if len(dates_found) >= 2 and interval_start is None:
260
+ # Normalize tz-awareness before comparing
261
+ _normed = [d.replace(tzinfo=UTC) if d.tzinfo is None else d for d in dates_found[:2]]
262
+ sorted_dates = sorted(_normed)
263
+ interval_start = sorted_dates[0]
264
+ interval_end = sorted_dates[1]
265
+
266
+ return {
267
+ "referenced_date": _safe_iso(referenced),
268
+ "interval_start": _safe_iso(interval_start),
269
+ "interval_end": _safe_iso(interval_end),
270
+ }
271
+
272
+ def build_temporal_event(
273
+ self,
274
+ fact: AtomicFact,
275
+ session_date: str | None,
276
+ entity_id: str,
277
+ ) -> TemporalEvent | None:
278
+ """Create a TemporalEvent for a fact-entity pair.
279
+
280
+ Combines the parsed session_date (observation) with dates
281
+ extracted from fact content (referenced + interval).
282
+
283
+ Returns:
284
+ TemporalEvent if any temporal info exists, else None.
285
+ """
286
+ obs_date = self.parse_session_date(session_date) if session_date else None
287
+ content_dates = self.extract_dates_from_text(fact.content)
288
+
289
+ ref_date = content_dates["referenced_date"]
290
+ int_start = content_dates["interval_start"]
291
+ int_end = content_dates["interval_end"]
292
+
293
+ # Use fact-level fields if already populated (from upstream)
294
+ if fact.observation_date and not obs_date:
295
+ obs_date = fact.observation_date
296
+ if fact.referenced_date and not ref_date:
297
+ ref_date = fact.referenced_date
298
+ if fact.interval_start and not int_start:
299
+ int_start = fact.interval_start
300
+ if fact.interval_end and not int_end:
301
+ int_end = fact.interval_end
302
+
303
+ # Only create event if we have at least one temporal anchor
304
+ if not any([obs_date, ref_date, int_start, int_end]):
305
+ return None
306
+
307
+ return TemporalEvent(
308
+ profile_id=fact.profile_id,
309
+ entity_id=entity_id,
310
+ fact_id=fact.fact_id,
311
+ observation_date=obs_date,
312
+ referenced_date=ref_date,
313
+ interval_start=int_start,
314
+ interval_end=int_end,
315
+ description=fact.content[:200],
316
+ )
317
+
318
+ def build_entity_timeline(
319
+ self,
320
+ entity_id: str,
321
+ facts: list[AtomicFact],
322
+ session_date: str | None = None,
323
+ ) -> list[TemporalEvent]:
324
+ """Build a chronological timeline for an entity from its facts.
325
+
326
+ Returns:
327
+ List of TemporalEvents sorted by earliest available date.
328
+ """
329
+ events: list[TemporalEvent] = []
330
+ for fact in facts:
331
+ s_date = session_date or fact.observation_date
332
+ event = self.build_temporal_event(fact, s_date, entity_id)
333
+ if event is not None:
334
+ events.append(event)
335
+
336
+ # Sort by the earliest available date
337
+ def _sort_key(ev: TemporalEvent) -> str:
338
+ for field in (ev.referenced_date, ev.observation_date, ev.interval_start):
339
+ if field:
340
+ return field
341
+ return "9999-12-31T23:59:59"
342
+
343
+ return sorted(events, key=_sort_key)
344
+
345
+ # ------------------------------------------------------------------
346
+ # Internal helpers
347
+ # ------------------------------------------------------------------
348
+
349
+ def _try_parse(self, text: str) -> datetime | None:
350
+ """Attempt to parse a text fragment as a date."""
351
+ if not text or len(text.strip()) < 3:
352
+ return None
353
+ try:
354
+ return dateutil_parse(text, fuzzy=True)
355
+ except (ParserError, ValueError, OverflowError):
356
+ return None
357
+
358
+ def _resolve_relative(self, expr: str) -> datetime | None:
359
+ """Resolve a relative expression like 'last Tuesday' or 'next month'."""
360
+ lower = expr.lower().strip()
361
+ parts = lower.split()
362
+ if len(parts) < 2:
363
+ return None
364
+
365
+ modifier = parts[0] # last / next / this
366
+ target = parts[1]
367
+
368
+ # Weekday resolution
369
+ if target in _WEEKDAY_MAP:
370
+ target_day = _WEEKDAY_MAP[target]
371
+ current_day = self._ref.weekday()
372
+ if modifier == "last":
373
+ diff = (current_day - target_day) % 7
374
+ diff = diff if diff > 0 else 7
375
+ return self._ref - timedelta(days=diff)
376
+ elif modifier == "next":
377
+ diff = (target_day - current_day) % 7
378
+ diff = diff if diff > 0 else 7
379
+ return self._ref + timedelta(days=diff)
380
+ else: # this
381
+ diff = (target_day - current_day) % 7
382
+ return self._ref + timedelta(days=diff)
383
+
384
+ # Week / month / year
385
+ if target == "week":
386
+ offset = {"last": -7, "next": 7, "this": 0}
387
+ return self._ref + timedelta(days=offset.get(modifier, 0))
388
+ if target == "month":
389
+ offset = {"last": -1, "next": 1, "this": 0}
390
+ return self._ref + relativedelta(months=offset.get(modifier, 0))
391
+ if target == "year":
392
+ offset = {"last": -1, "next": 1, "this": 0}
393
+ return self._ref + relativedelta(years=offset.get(modifier, 0))
394
+
395
+ # Seasons
396
+ if target in _SEASON_MONTHS:
397
+ start_m, end_m = _SEASON_MONTHS[target]
398
+ year = self._ref.year
399
+ if modifier == "last":
400
+ year -= 1
401
+ elif modifier == "next":
402
+ year += 1
403
+ # Return midpoint of season
404
+ if start_m <= end_m:
405
+ mid_m = (start_m + end_m) // 2
406
+ else:
407
+ mid_m = 1 # winter spans Dec-Feb, midpoint ~ Jan
408
+ try:
409
+ return datetime(year, mid_m, 15)
410
+ except ValueError:
411
+ return None
412
+
413
+ return None
414
+
415
+ @staticmethod
416
+ def _unit_delta(count: int, unit: str) -> timedelta | relativedelta:
417
+ """Build a timedelta/relativedelta from count + unit string."""
418
+ if unit == "day":
419
+ return timedelta(days=count)
420
+ if unit == "week":
421
+ return timedelta(weeks=count)
422
+ if unit == "month":
423
+ return relativedelta(months=count)
424
+ if unit == "year":
425
+ return relativedelta(years=count)
426
+ return timedelta(days=count)