superlocalmemory 2.8.6 → 3.0.1

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