superlocalmemory 3.6.22 → 3.7.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 (303) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +275 -72
  3. package/bin/slm-npm +43 -89
  4. package/docs/pi-dev-integration.md +43 -0
  5. package/ide/configs/antigravity-mcp.json +2 -2
  6. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  7. package/ide/configs/claude-desktop-mcp.json +2 -2
  8. package/ide/configs/windsurf-mcp.json +2 -2
  9. package/ide/hooks/context-hook.js +6 -2
  10. package/ide/hooks/post-recall-hook.js +7 -3
  11. package/ide/hooks/tool-event-hook.sh +2 -1
  12. package/package.json +19 -10
  13. package/plugin/.claude-plugin/plugin.json +1 -1
  14. package/plugin/_GENERATED.md +1 -1
  15. package/plugin/agents/slm-memory-advisor.md +1 -1
  16. package/plugin/requirements.txt +1 -1
  17. package/plugin/skills/slm-session/SKILL.md +1 -1
  18. package/plugin-src/rules/AGENTS.md +1 -1
  19. package/pyproject.toml +40 -8
  20. package/scripts/postinstall-interactive.js +17 -94
  21. package/scripts/postinstall.js +185 -258
  22. package/scripts/preuninstall.js +9 -50
  23. package/src/superlocalmemory/__init__.py +2 -2
  24. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  25. package/src/superlocalmemory/attribution/signer.py +34 -19
  26. package/src/superlocalmemory/attribution/watermark.py +1 -1
  27. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  28. package/src/superlocalmemory/cli/commands.py +490 -195
  29. package/src/superlocalmemory/cli/context_commands.py +5 -4
  30. package/src/superlocalmemory/cli/daemon.py +282 -187
  31. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  32. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  33. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  34. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  35. package/src/superlocalmemory/cli/main.py +128 -31
  36. package/src/superlocalmemory/cli/pending_store.py +54 -38
  37. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  38. package/src/superlocalmemory/cli/service_installer.py +57 -52
  39. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  40. package/src/superlocalmemory/cli/version_banner.py +2 -1
  41. package/src/superlocalmemory/code_graph/config.py +3 -1
  42. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  43. package/src/superlocalmemory/core/config.py +65 -20
  44. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  45. package/src/superlocalmemory/core/context_cache.py +56 -8
  46. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  47. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  48. package/src/superlocalmemory/core/embeddings.py +54 -18
  49. package/src/superlocalmemory/core/engine.py +150 -104
  50. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  51. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  52. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  53. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  54. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  55. package/src/superlocalmemory/core/health_monitor.py +4 -2
  56. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  57. package/src/superlocalmemory/core/injection.py +69 -18
  58. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  59. package/src/superlocalmemory/core/maintenance.py +23 -22
  60. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  61. package/src/superlocalmemory/core/mutations.py +143 -0
  62. package/src/superlocalmemory/core/platform_utils.py +7 -4
  63. package/src/superlocalmemory/core/ram_lock.py +16 -5
  64. package/src/superlocalmemory/core/rate_limit.py +1 -1
  65. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  66. package/src/superlocalmemory/core/recall_worker.py +76 -59
  67. package/src/superlocalmemory/core/registry.py +1 -1
  68. package/src/superlocalmemory/core/scale_engine.py +293 -0
  69. package/src/superlocalmemory/core/score_contract.py +62 -0
  70. package/src/superlocalmemory/core/security_primitives.py +3 -1
  71. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  72. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  73. package/src/superlocalmemory/core/tier_manager.py +32 -20
  74. package/src/superlocalmemory/core/worker_pool.py +13 -4
  75. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  76. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  77. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  79. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  80. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  81. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  82. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  83. package/src/superlocalmemory/encoding/foresight.py +2 -0
  84. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  85. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  86. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  87. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  88. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  89. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  90. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  91. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  92. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  93. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  94. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  95. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  96. package/src/superlocalmemory/hooks/claude_code_hooks.py +126 -39
  97. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  98. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  99. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  100. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  101. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  102. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  103. package/src/superlocalmemory/hooks/hook_handlers.py +241 -55
  104. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  105. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  106. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  107. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  108. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  109. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  110. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  111. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  112. package/src/superlocalmemory/infra/backup.py +7 -5
  113. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  114. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  115. package/src/superlocalmemory/infra/data_root.py +199 -0
  116. package/src/superlocalmemory/infra/event_bus.py +3 -1
  117. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  118. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  119. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  120. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  121. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  122. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  123. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  124. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  125. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  126. package/src/superlocalmemory/learning/ensemble.py +11 -0
  127. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  128. package/src/superlocalmemory/learning/feedback.py +1 -1
  129. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  130. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  131. package/src/superlocalmemory/learning/ranker.py +4 -1
  132. package/src/superlocalmemory/learning/source_quality.py +1 -1
  133. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  134. package/src/superlocalmemory/llm/backbone.py +13 -8
  135. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  136. package/src/superlocalmemory/math/fisher.py +1 -1
  137. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  138. package/src/superlocalmemory/math/hopfield.py +1 -1
  139. package/src/superlocalmemory/math/langevin.py +1 -1
  140. package/src/superlocalmemory/math/polar_quant.py +3 -4
  141. package/src/superlocalmemory/math/qjl.py +1 -1
  142. package/src/superlocalmemory/math/sheaf.py +1 -1
  143. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  144. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  145. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  146. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  147. package/src/superlocalmemory/mcp/server.py +39 -13
  148. package/src/superlocalmemory/mcp/shared.py +69 -3
  149. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  150. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  151. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  152. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  153. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  154. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  155. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  156. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  157. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  158. package/src/superlocalmemory/mesh/broker.py +2 -0
  159. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  160. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  161. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  162. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  163. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  164. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  165. package/src/superlocalmemory/optimize/config/store.py +6 -4
  166. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  167. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  168. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  169. package/src/superlocalmemory/optimize/storage/db.py +14 -13
  170. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  171. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  172. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  173. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  174. package/src/superlocalmemory/retrieval/engine.py +169 -79
  175. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  176. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  177. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  178. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  179. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  180. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  181. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  182. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  183. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  184. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  185. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  186. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  187. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  188. package/src/superlocalmemory/server/api.py +10 -7
  189. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  190. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  191. package/src/superlocalmemory/server/route_mutations.py +84 -0
  192. package/src/superlocalmemory/server/routes/agents.py +8 -6
  193. package/src/superlocalmemory/server/routes/brain.py +14 -12
  194. package/src/superlocalmemory/server/routes/chat.py +29 -12
  195. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  196. package/src/superlocalmemory/server/routes/helpers.py +29 -4
  197. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  198. package/src/superlocalmemory/server/routes/memories.py +104 -43
  199. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  200. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  201. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  202. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  203. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  204. package/src/superlocalmemory/server/security_middleware.py +1 -1
  205. package/src/superlocalmemory/server/ui.py +6 -3
  206. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  207. package/src/superlocalmemory/server/write_identity.py +147 -0
  208. package/src/superlocalmemory/storage/access_log.py +4 -3
  209. package/src/superlocalmemory/storage/database.py +118 -25
  210. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  211. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  212. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  213. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  214. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  215. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  216. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  217. package/src/superlocalmemory/storage/models.py +16 -0
  218. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  219. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  220. package/src/superlocalmemory/ui/favicon.svg +5 -0
  221. package/src/superlocalmemory/ui/index.html +1 -0
  222. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  223. package/src/superlocalmemory/ui/js/core.js +49 -8
  224. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  225. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  226. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  227. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  228. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  229. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  230. package/src/superlocalmemory/ui/js/settings.js +4 -2
  231. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  232. package/bin/slm +0 -59
  233. package/bin/slm.bat +0 -77
  234. package/bin/slm.cmd +0 -5
  235. package/ide/integrations/langchain/README.md +0 -106
  236. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  237. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  238. package/ide/integrations/langchain/pyproject.toml +0 -38
  239. package/ide/integrations/langchain/tests/__init__.py +0 -3
  240. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  241. package/ide/integrations/langchain/tests/test_security.py +0 -117
  242. package/ide/integrations/llamaindex/README.md +0 -81
  243. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  244. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  245. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  246. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  247. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  248. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  249. package/plugin-src/.mcp.json +0 -12
  250. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  251. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  252. package/plugin-src/hooks/.gitkeep +0 -0
  253. package/plugin-src/hooks/hooks.json +0 -23
  254. package/plugin-src/manifest.json +0 -25
  255. package/plugin-src/requirements.txt +0 -1
  256. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  257. package/plugin-src/scripts/ensure-venv.bat +0 -122
  258. package/plugin-src/scripts/ensure-venv.sh +0 -105
  259. package/plugin-src/scripts/slm-launch +0 -15
  260. package/plugin-src/scripts/slm-launch.bat +0 -17
  261. package/plugin-src/settings.json +0 -16
  262. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  263. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  264. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  265. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  266. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  267. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  268. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  269. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  270. package/scripts/_savings_math.py +0 -270
  271. package/scripts/build-dmg.sh +0 -417
  272. package/scripts/build-plugin.js +0 -742
  273. package/scripts/build-slm-hook.ps1 +0 -40
  274. package/scripts/build-slm-hook.sh +0 -45
  275. package/scripts/build_entry.py +0 -452
  276. package/scripts/ci/stage5b_gate.sh +0 -50
  277. package/scripts/dogfood_savings.py +0 -490
  278. package/scripts/generate-thumbnails.py +0 -218
  279. package/scripts/install-skills.ps1 +0 -4
  280. package/scripts/install-skills.sh +0 -5
  281. package/scripts/install.ps1 +0 -701
  282. package/scripts/install.sh +0 -1015
  283. package/scripts/postinstall_binary.js +0 -287
  284. package/scripts/prepack.js +0 -33
  285. package/scripts/release_manifest.py +0 -273
  286. package/scripts/slm-hook.spec +0 -56
  287. package/scripts/start-dashboard.ps1 +0 -52
  288. package/scripts/start-dashboard.sh +0 -41
  289. package/scripts/sync-wiki.ps1 +0 -127
  290. package/scripts/sync-wiki.sh +0 -82
  291. package/scripts/test-dmg.sh +0 -161
  292. package/scripts/test-npm-package.ps1 +0 -252
  293. package/scripts/test-npm-package.sh +0 -207
  294. package/scripts/verify-install.ps1 +0 -294
  295. package/scripts/verify-install.sh +0 -266
  296. package/scripts/verify-v27.ps1 +0 -301
  297. package/scripts/verify-v27.sh +0 -233
  298. package/src/superlocalmemory.egg-info/PKG-INFO +0 -513
  299. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  300. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  301. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  302. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  303. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -8,15 +8,22 @@ SA-RAG pattern: entities from query -> canonical lookup -> graph traversal
8
8
  with decay. Handles BOTH uppercase and lowercase entity mentions.
9
9
 
10
10
  Part of Qualixar | Author: Varun Pratap Bhardwaj
11
- License: Elastic-2.0
11
+ License: AGPL-3.0-or-later
12
12
  """
13
13
  from __future__ import annotations
14
14
 
15
15
  import json
16
16
  import logging
17
17
  import re
18
+ import threading
18
19
  from collections import defaultdict
19
- from typing import TYPE_CHECKING
20
+ from typing import TYPE_CHECKING, Any
21
+
22
+ from superlocalmemory.retrieval.scope_policy import (
23
+ authorized_fact_ids,
24
+ filter_authorized_results,
25
+ )
26
+ from superlocalmemory.storage.database import _scope_where
20
27
 
21
28
  if TYPE_CHECKING:
22
29
  from superlocalmemory.encoding.entity_resolver import EntityResolver
@@ -101,15 +108,27 @@ class EntityGraphChannel:
101
108
  self._max_hops = max_hops
102
109
  # v3.4.5: Optional CozoDB graph backend (Sprint 2)
103
110
  self._cozo = cozo_backend
111
+ self._cache_lock = threading.RLock()
104
112
  # In-memory adjacency: {node_id -> [(neighbor_id, weight), ...]}
105
113
  self._adj: dict[str, list[tuple[str, float]]] = {}
106
114
  self._adj_profile: str = "" # Track which profile is loaded
115
+ self._adj_scope_key: tuple[str, bool, bool] | None = None
107
116
  self._adj_edge_count: int = 0 # Track edge count for staleness detection
117
+ self._adj_fact_count: int = 0
118
+ self._entity_to_facts: dict[str, list[str]] = defaultdict(list)
119
+ self._fact_to_entities: dict[str, list[str]] = defaultdict(list)
120
+ self._visible_fact_ids: set[str] = set()
108
121
  # v3.4.1: Graph intelligence metrics (loaded from fact_importance)
109
122
  self._graph_metrics: dict[str, dict] = graph_metrics or {}
110
123
  self._graph_metrics_profile: str = ""
111
124
 
112
- def _ensure_adjacency(self, profile_id: str) -> None:
125
+ def _ensure_adjacency(
126
+ self,
127
+ profile_id: str,
128
+ *,
129
+ include_global: bool = False,
130
+ include_shared: bool = False,
131
+ ) -> None:
113
132
  """Load graph adjacency into memory for fast spreading activation.
114
133
 
115
134
  Loads ALL edges for a profile into a bidirectional dict.
@@ -117,7 +136,20 @@ class EntityGraphChannel:
117
136
  Cost: ~1s for 232K edges, ~18 MB RAM.
118
137
  """
119
138
  # Check staleness: profile changed or new edges added since last load
120
- current_count = self._get_edge_count(profile_id)
139
+ scope_key = (profile_id, bool(include_global), bool(include_shared))
140
+ current_count = self._get_edge_count(
141
+ profile_id,
142
+ include_global=include_global,
143
+ include_shared=include_shared,
144
+ )
145
+ try:
146
+ current_fact_count = self._db.get_fact_count(
147
+ profile_id,
148
+ include_global=include_global,
149
+ include_shared=include_shared,
150
+ )
151
+ except Exception:
152
+ current_fact_count = -1
121
153
  # memory-bounding-01: also reload if the cache is older than the TTL,
122
154
  # even when the edge COUNT is unchanged. Edge weights/pruning can mutate
123
155
  # the graph without changing the count (e.g. store_edge MAX-merge), and a
@@ -125,16 +157,23 @@ class EntityGraphChannel:
125
157
  import time as _t_ec
126
158
  _now_ec = _t_ec.monotonic()
127
159
  _fresh = (_now_ec - getattr(self, "_adj_loaded_at", 0.0)) < 300.0
128
- if (self._adj_profile == profile_id
129
- and self._adj
160
+ if (self._adj_scope_key == scope_key
161
+ and (self._adj or self._visible_fact_ids)
130
162
  and self._adj_edge_count == current_count
163
+ and self._adj_fact_count == current_fact_count
131
164
  and _fresh):
132
165
  return
133
166
  adj: dict[str, list[tuple[str, float]]] = defaultdict(list)
134
167
  try:
168
+ where, params = _scope_where(
169
+ profile_id,
170
+ include_global=include_global,
171
+ include_shared=include_shared,
172
+ )
135
173
  rows = self._db.execute(
136
- "SELECT source_id, target_id, weight FROM graph_edges WHERE profile_id = ?",
137
- (profile_id,),
174
+ "SELECT source_id, target_id, weight FROM graph_edges "
175
+ f"WHERE {where}",
176
+ (*params,),
138
177
  )
139
178
  except Exception:
140
179
  rows = []
@@ -143,12 +182,29 @@ class EntityGraphChannel:
143
182
  s, t, w = d["source_id"], d["target_id"], float(d["weight"])
144
183
  adj[s].append((t, w))
145
184
  adj[t].append((s, w))
146
- self._adj = dict(adj) # Convert defaultdict to regular dict (no accidental growth)
185
+ # Also load entity maps (same staleness lifecycle)
186
+ self._load_entity_maps(
187
+ profile_id,
188
+ include_global=include_global,
189
+ include_shared=include_shared,
190
+ )
191
+ # Edge scope alone cannot authorize an endpoint. Prune both endpoints
192
+ # against the visible fact corpus so denied facts cannot influence an
193
+ # allowed candidate indirectly through propagation.
194
+ self._adj = {
195
+ node_id: [
196
+ (neighbor_id, weight)
197
+ for neighbor_id, weight in neighbors
198
+ if neighbor_id in self._visible_fact_ids
199
+ ]
200
+ for node_id, neighbors in adj.items()
201
+ if node_id in self._visible_fact_ids
202
+ }
147
203
  self._adj_profile = profile_id
204
+ self._adj_scope_key = scope_key
148
205
  self._adj_edge_count = current_count
206
+ self._adj_fact_count = current_fact_count
149
207
  self._adj_loaded_at = _now_ec # memory-bounding-01: TTL reference
150
- # Also load entity maps (same staleness lifecycle)
151
- self._load_entity_maps(profile_id)
152
208
  # v3.4.1: Load graph intelligence metrics (P0)
153
209
  self._load_graph_metrics(profile_id)
154
210
 
@@ -158,12 +214,23 @@ class EntityGraphChannel:
158
214
  len(self._entity_to_facts), profile_id,
159
215
  )
160
216
 
161
- def _get_edge_count(self, profile_id: str) -> int:
217
+ def _get_edge_count(
218
+ self,
219
+ profile_id: str,
220
+ *,
221
+ include_global: bool = False,
222
+ include_shared: bool = False,
223
+ ) -> int:
162
224
  """Fast edge count for staleness check (~1ms)."""
163
225
  try:
226
+ where, params = _scope_where(
227
+ profile_id,
228
+ include_global=include_global,
229
+ include_shared=include_shared,
230
+ )
164
231
  rows = self._db.execute(
165
- "SELECT COUNT(*) as cnt FROM graph_edges WHERE profile_id = ?",
166
- (profile_id,),
232
+ f"SELECT COUNT(*) as cnt FROM graph_edges WHERE {where}",
233
+ (*params,),
167
234
  )
168
235
  if rows:
169
236
  return int(dict(rows[0]).get("cnt", 0))
@@ -171,7 +238,13 @@ class EntityGraphChannel:
171
238
  pass
172
239
  return 0
173
240
 
174
- def _load_entity_maps(self, profile_id: str) -> None:
241
+ def _load_entity_maps(
242
+ self,
243
+ profile_id: str,
244
+ *,
245
+ include_global: bool = False,
246
+ include_shared: bool = False,
247
+ ) -> None:
175
248
  """Pre-load entity→fact and fact→entity maps into memory.
176
249
 
177
250
  Eliminates per-entity and per-fact SQL in the spreading activation loop.
@@ -181,29 +254,21 @@ class EntityGraphChannel:
181
254
  self._entity_to_facts: dict[str, list[str]] = defaultdict(list)
182
255
  # fact_id -> [entity_id, ...]
183
256
  self._fact_to_entities: dict[str, list[str]] = defaultdict(list)
257
+ self._visible_fact_ids = set()
184
258
 
185
259
  try:
186
- rows = self._db.execute(
187
- "SELECT fact_id, canonical_entities_json FROM atomic_facts "
188
- "WHERE profile_id = ? AND canonical_entities_json IS NOT NULL "
189
- "AND canonical_entities_json != ''",
190
- (profile_id,),
260
+ facts = self._db.get_all_facts(
261
+ profile_id,
262
+ include_global=include_global,
263
+ include_shared=include_shared,
191
264
  )
192
265
  except Exception:
193
- rows = []
194
- for r in rows:
195
- d = dict(r)
196
- fid = d["fact_id"]
197
- raw = d.get("canonical_entities_json")
198
- if not raw:
199
- continue
200
- try:
201
- eids = json.loads(raw)
202
- for eid in eids:
203
- self._entity_to_facts[eid].append(fid)
204
- self._fact_to_entities[fid].append(eid)
205
- except (ValueError, TypeError):
206
- continue
266
+ facts = []
267
+ for fact in facts:
268
+ self._visible_fact_ids.add(fact.fact_id)
269
+ for eid in fact.canonical_entities:
270
+ self._entity_to_facts[eid].append(fact.fact_id)
271
+ self._fact_to_entities[fact.fact_id].append(eid)
207
272
 
208
273
  logger.info(
209
274
  "Loaded entity maps: %d entities, %d facts with entities",
@@ -245,33 +310,62 @@ class EntityGraphChannel:
245
310
  """Clear all caches. Call after adding/removing edges or facts."""
246
311
  self._adj.clear()
247
312
  self._adj_profile = ""
313
+ self._adj_scope_key = None
248
314
  self._adj_edge_count = 0
315
+ self._adj_fact_count = 0
249
316
  self._entity_to_facts = defaultdict(list)
250
317
  self._fact_to_entities = defaultdict(list)
318
+ self._visible_fact_ids.clear()
251
319
  self._graph_metrics.clear()
252
320
  self._graph_metrics_profile = ""
253
321
 
254
322
  def search(self, query: str, profile_id: str, top_k: int = 50) -> list[tuple[str, float]]:
323
+ """Serialize access to the scope-keyed graph/entity cache."""
324
+ with self._cache_lock:
325
+ return self._search_locked(query, profile_id, top_k)
326
+
327
+ def _search_locked(
328
+ self,
329
+ query: str,
330
+ profile_id: str,
331
+ top_k: int = 50,
332
+ ) -> list[tuple[str, float]]:
255
333
  """Search via entity graph with spreading activation.
256
334
 
257
335
  V3.3.9: Uses in-memory adjacency for O(1) edge lookups.
258
336
  V3.4.5: Routes to CozoDB if backend is active (Sprint 2).
259
337
  """
338
+ include_global = bool(getattr(self, "include_global", False))
339
+ include_shared = bool(getattr(self, "include_shared", False))
260
340
  raw_entities = extract_query_entities(query)
261
341
 
262
- # v3.4.5: Route to CozoDB if active
263
- if self._cozo is not None:
264
- return self._search_via_cozo(query, raw_entities, profile_id, top_k)
265
342
  if not raw_entities:
266
343
  return []
267
344
 
345
+ # Load the visible fact/entity map before resolution. Canonical entity
346
+ # IDs are profile-local UUIDs, so the same name may have a different ID
347
+ # in an opted-in global/shared owner's partition.
348
+ self._ensure_adjacency(
349
+ profile_id,
350
+ include_global=include_global,
351
+ include_shared=include_shared,
352
+ )
353
+
354
+ # v3.4.5: Route to CozoDB if active
355
+ if self._cozo is not None:
356
+ return self._search_via_cozo(
357
+ query,
358
+ raw_entities,
359
+ profile_id,
360
+ top_k,
361
+ include_global=include_global,
362
+ include_shared=include_shared,
363
+ )
364
+
268
365
  canonical_ids = self._resolve_entities(raw_entities, profile_id)
269
366
  if not canonical_ids:
270
367
  return []
271
368
 
272
- # Load adjacency cache (no-op if already loaded for this profile)
273
- self._ensure_adjacency(profile_id)
274
-
275
369
  # Seed activation from direct entity-linked facts
276
370
  # Use in-memory map when available, fall back to SQL for mock/test DBs
277
371
  activation: dict[str, float] = defaultdict(float)
@@ -283,7 +377,12 @@ class EntityGraphChannel:
283
377
  for fid in self._entity_to_facts.get(eid, ()):
284
378
  activation[fid] = max(activation[fid], 1.0)
285
379
  else:
286
- for fact in self._db.get_facts_by_entity(eid, profile_id, include_global=getattr(self, 'include_global', False), include_shared=getattr(self, 'include_shared', False)):
380
+ for fact in self._db.get_facts_by_entity(
381
+ eid,
382
+ profile_id,
383
+ include_global=include_global,
384
+ include_shared=include_shared,
385
+ ):
287
386
  activation[fact.fact_id] = max(activation[fact.fact_id], 1.0)
288
387
 
289
388
  # Spreading activation through graph edges (all in-memory O(1) lookups)
@@ -317,10 +416,18 @@ class EntityGraphChannel:
317
416
  # NOTE: SQL fallback path does NOT use graph intelligence (P1/P2/P3).
318
417
  # Graph intelligence is only available on the in-memory cache path.
319
418
  # This fallback exists for mock/test DBs. See Phase 7 LLD H-01.
320
- for edge in self._db.get_edges_for_node(fid, profile_id, include_global=getattr(self, 'include_global', False), include_shared=getattr(self, 'include_shared', False)):
419
+ for edge in self._db.get_edges_for_node(
420
+ fid,
421
+ profile_id,
422
+ include_global=include_global,
423
+ include_shared=include_shared,
424
+ ):
321
425
  neighbor = edge.target_id if edge.source_id == fid else edge.source_id
322
426
  propagated = activation[fid] * self._decay
323
- if propagated >= self._threshold and propagated > activation.get(neighbor, 0.0):
427
+ if (
428
+ propagated >= self._threshold
429
+ and propagated > activation.get(neighbor, 0.0)
430
+ ):
324
431
  activation[neighbor] = propagated
325
432
  next_frontier.add(neighbor)
326
433
 
@@ -342,7 +449,12 @@ class EntityGraphChannel:
342
449
  new_eids_sql = self._discover_entities(frontier, profile_id, visited_entities)
343
450
  for eid in new_eids_sql:
344
451
  visited_entities.add(eid)
345
- for fact in self._db.get_facts_by_entity(eid, profile_id, include_global=getattr(self, 'include_global', False), include_shared=getattr(self, 'include_shared', False)):
452
+ for fact in self._db.get_facts_by_entity(
453
+ eid,
454
+ profile_id,
455
+ include_global=include_global,
456
+ include_shared=include_shared,
457
+ ):
346
458
  if hop_decay > activation.get(fact.fact_id, 0.0):
347
459
  activation[fact.fact_id] = hop_decay
348
460
  next_frontier.add(fact.fact_id)
@@ -384,13 +496,41 @@ class EntityGraphChannel:
384
496
  if max_score > 0:
385
497
  results = [(fid, sc / max_score) for fid, sc in results]
386
498
  results.sort(key=lambda x: x[1], reverse=True)
387
- return results[:top_k]
499
+ return filter_authorized_results(
500
+ self._db,
501
+ results,
502
+ profile_id,
503
+ include_global=include_global,
504
+ include_shared=include_shared,
505
+ )[:top_k]
388
506
 
389
507
  def score_candidates(
390
508
  self,
391
509
  query: str,
392
510
  candidate_fact_ids: list[str],
393
511
  profile_id: str,
512
+ *,
513
+ include_global: bool | None = None,
514
+ include_shared: bool | None = None,
515
+ ) -> dict[str, float]:
516
+ """Serialize access to the scope-keyed graph/entity cache."""
517
+ with self._cache_lock:
518
+ return self._score_candidates_locked(
519
+ query,
520
+ candidate_fact_ids,
521
+ profile_id,
522
+ include_global=include_global,
523
+ include_shared=include_shared,
524
+ )
525
+
526
+ def _score_candidates_locked(
527
+ self,
528
+ query: str,
529
+ candidate_fact_ids: list[str],
530
+ profile_id: str,
531
+ *,
532
+ include_global: bool | None = None,
533
+ include_shared: bool | None = None,
394
534
  ) -> dict[str, float]:
395
535
  """Score candidate facts by their entity-graph proximity to query entities.
396
536
 
@@ -418,16 +558,33 @@ class EntityGraphChannel:
418
558
  if not candidate_fact_ids:
419
559
  return {}
420
560
 
561
+ if include_global is None:
562
+ include_global = bool(getattr(self, "include_global", False))
563
+ if include_shared is None:
564
+ include_shared = bool(getattr(self, "include_shared", False))
565
+ allowed_candidates = authorized_fact_ids(
566
+ self._db,
567
+ candidate_fact_ids,
568
+ profile_id,
569
+ include_global=include_global,
570
+ include_shared=include_shared,
571
+ )
572
+ if not allowed_candidates:
573
+ return {}
574
+
421
575
  raw_entities = extract_query_entities(query)
422
576
  if not raw_entities:
423
577
  return {}
424
578
 
579
+ self._ensure_adjacency(
580
+ profile_id,
581
+ include_global=include_global,
582
+ include_shared=include_shared,
583
+ )
425
584
  canonical_ids = self._resolve_entities(raw_entities, profile_id)
426
585
  if not canonical_ids:
427
586
  return {}
428
587
 
429
- self._ensure_adjacency(profile_id)
430
-
431
588
  # Run full spreading activation (same as search())
432
589
  activation: dict[str, float] = defaultdict(float)
433
590
  visited_entities: set[str] = set(canonical_ids)
@@ -438,7 +595,12 @@ class EntityGraphChannel:
438
595
  for fid in self._entity_to_facts.get(eid, ()):
439
596
  activation[fid] = max(activation[fid], 1.0)
440
597
  else:
441
- for fact in self._db.get_facts_by_entity(eid, profile_id, include_global=getattr(self, 'include_global', False), include_shared=getattr(self, 'include_shared', False)):
598
+ for fact in self._db.get_facts_by_entity(
599
+ eid,
600
+ profile_id,
601
+ include_global=include_global,
602
+ include_shared=include_shared,
603
+ ):
442
604
  activation[fact.fact_id] = max(activation[fact.fact_id], 1.0)
443
605
 
444
606
  frontier = set(activation.keys())
@@ -495,7 +657,7 @@ class EntityGraphChannel:
495
657
  activation[fid] *= boost
496
658
 
497
659
  # Extract scores ONLY for the candidate set, normalize to [0, 1]
498
- candidate_set = set(candidate_fact_ids)
660
+ candidate_set = allowed_candidates
499
661
  scored = {fid: activation.get(fid, 0.0) for fid in candidate_set}
500
662
 
501
663
  max_score = max(scored.values()) if scored else 0
@@ -558,7 +720,7 @@ class EntityGraphChannel:
558
720
  logger.debug("Contradiction suppression failed: %s", exc)
559
721
 
560
722
  def _resolve_entities(self, raw: list[str], profile_id: str) -> list[str]:
561
- """Resolve raw names to canonical entity IDs."""
723
+ """Resolve local and visible cross-profile canonical entity IDs."""
562
724
  ids: list[str] = []
563
725
  seen: set[str] = set()
564
726
  if self._resolver is not None:
@@ -572,6 +734,28 @@ class EntityGraphChannel:
572
734
  if ent and ent.entity_id not in seen:
573
735
  seen.add(ent.entity_id)
574
736
  ids.append(ent.entity_id)
737
+
738
+ # Entity UUIDs are profile-local. Supplement local resolution with
739
+ # same-name/alias IDs that are actually referenced by visible facts.
740
+ names = [name.strip().lower() for name in raw if name.strip()]
741
+ if names and self._visible_fact_ids and self._entity_to_facts:
742
+ placeholders = ",".join("?" for _ in names)
743
+ try:
744
+ rows = self._db.execute(
745
+ "SELECT entity_id FROM canonical_entities "
746
+ f"WHERE LOWER(canonical_name) IN ({placeholders}) "
747
+ "UNION SELECT entity_id FROM entity_aliases "
748
+ f"WHERE LOWER(alias) IN ({placeholders})",
749
+ (*names, *names),
750
+ )
751
+ except Exception:
752
+ rows = []
753
+ visible_entity_ids = set(self._entity_to_facts)
754
+ for row in rows:
755
+ entity_id = str(dict(row)["entity_id"])
756
+ if entity_id in visible_entity_ids and entity_id not in seen:
757
+ seen.add(entity_id)
758
+ ids.append(entity_id)
575
759
  return ids
576
760
 
577
761
  def _discover_entities(
@@ -580,7 +764,14 @@ class EntityGraphChannel:
580
764
  """Find new canonical entity IDs referenced by a set of facts."""
581
765
  new: list[str] = []
582
766
  seen = set(visited)
583
- for fid in fact_ids:
767
+ allowed_fact_ids = authorized_fact_ids(
768
+ self._db,
769
+ fact_ids,
770
+ profile_id,
771
+ include_global=bool(getattr(self, "include_global", False)),
772
+ include_shared=bool(getattr(self, "include_shared", False)),
773
+ )
774
+ for fid in allowed_fact_ids:
584
775
  rows = self._db.execute(
585
776
  "SELECT canonical_entities_json FROM atomic_facts WHERE fact_id = ?", (fid,),
586
777
  )
@@ -602,6 +793,9 @@ class EntityGraphChannel:
602
793
  def _search_via_cozo(
603
794
  self, query: str, raw_entities: list[str],
604
795
  profile_id: str, top_k: int,
796
+ *,
797
+ include_global: bool = False,
798
+ include_shared: bool = False,
605
799
  ) -> list[tuple[str, float]]:
606
800
  """Entity graph search routed through CozoDB.
607
801
 
@@ -616,26 +810,54 @@ class EntityGraphChannel:
616
810
  if not canonical_ids:
617
811
  return []
618
812
 
813
+ # Scoped/global recall has deliberately more complex authorization
814
+ # semantics than the promoted default-profile projection. Never let
815
+ # a projection broaden that boundary: SQLite remains authoritative.
816
+ if include_global or include_shared:
817
+ return self._search_without_cozo(query, profile_id, top_k)
818
+
619
819
  try:
620
- # Use CozoDB for spreading activation
621
- scored = self._cozo.spreading_activation(
820
+ scored = self._cozo.recall_facts(
622
821
  canonical_ids,
822
+ profile_id=profile_id,
623
823
  depth=self._max_hops,
624
824
  decay=self._decay,
625
- top_k=top_k * 2, # Fetch extra for filtering
825
+ threshold=self._threshold,
826
+ top_k=top_k * 2,
626
827
  )
627
828
 
628
- # Map entity scores to fact scores
629
- fact_scores: list[tuple[str, float]] = []
630
- for entity_id, score in scored:
631
- facts = self._db.get_facts_by_entity(entity_id, profile_id, include_global=getattr(self, 'include_global', False), include_shared=getattr(self, 'include_shared', False))
632
- for fact in facts:
633
- fact_scores.append((fact.fact_id, score))
634
-
635
- # Sort and return top_k
636
- fact_scores.sort(key=lambda x: x[1], reverse=True)
637
- return fact_scores[:top_k]
829
+ cozo_results = filter_authorized_results(
830
+ self._db,
831
+ scored,
832
+ profile_id,
833
+ include_global=include_global,
834
+ include_shared=include_shared,
835
+ )[:top_k]
836
+ # Shadow SQLite before accepting a projected answer. The graph
837
+ # channel has optional PageRank/community enrichments, so exact
838
+ # score equality is neither required nor useful; result ordering
839
+ # and membership are the correctness contract. Any divergence is
840
+ # recorded and fails closed to canonical SQLite.
841
+ sqlite_results = self._search_without_cozo(query, profile_id, top_k)
842
+ matches = [fact_id for fact_id, _ in cozo_results] == [
843
+ fact_id for fact_id, _ in sqlite_results
844
+ ]
845
+ record = getattr(self._cozo, "record_shadow_comparison", None)
846
+ if callable(record):
847
+ record(matches=matches, projected=cozo_results, canonical=sqlite_results)
848
+ return cozo_results if matches else sqlite_results
849
+ except Exception as exc:
850
+ record = getattr(self._cozo, "record_shadow_error", None)
851
+ if callable(record):
852
+ record(str(exc))
853
+ return self._search_without_cozo(query, profile_id, top_k)
638
854
 
639
- except Exception:
640
- # Fallback to in-memory adjacency (existing code path)
641
- return []
855
+ def _search_without_cozo(
856
+ self, query: str, profile_id: str, top_k: int,
857
+ ) -> list[tuple[str, float]]:
858
+ """Run canonical SQLite entity recall without recursive projection use."""
859
+ cozo, self._cozo = self._cozo, None
860
+ try:
861
+ return self._search_locked(query, profile_id, top_k)
862
+ finally:
863
+ self._cozo = cozo
@@ -15,7 +15,7 @@ signature: (all_results, profile_id, context) -> filtered_results.
15
15
  HR-06: When config.enabled=False, returns results unchanged.
16
16
 
17
17
  Part of Qualixar | Author: Varun Pratap Bhardwaj
18
- License: Elastic-2.0
18
+ License: AGPL-3.0-or-later
19
19
  """
20
20
 
21
21
  from __future__ import annotations
@@ -8,7 +8,7 @@ Single-pass RRF with k=15 for sharp rank discrimination on small candidate pools
8
8
  V1 had triple re-fusion which destroyed rankings — fixed in V2.
9
9
 
10
10
  Part of Qualixar | Author: Varun Pratap Bhardwaj
11
- License: Elastic-2.0
11
+ License: AGPL-3.0-or-later
12
12
  """
13
13
  from __future__ import annotations
14
14