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.
- package/CHANGELOG.md +52 -0
- package/README.md +275 -72
- package/bin/slm-npm +43 -89
- package/docs/pi-dev-integration.md +43 -0
- package/ide/configs/antigravity-mcp.json +2 -2
- package/ide/configs/chatgpt-desktop-mcp.json +1 -1
- package/ide/configs/claude-desktop-mcp.json +2 -2
- package/ide/configs/windsurf-mcp.json +2 -2
- package/ide/hooks/context-hook.js +6 -2
- package/ide/hooks/post-recall-hook.js +7 -3
- package/ide/hooks/tool-event-hook.sh +2 -1
- package/package.json +19 -10
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/_GENERATED.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/pyproject.toml +40 -8
- package/scripts/postinstall-interactive.js +17 -94
- package/scripts/postinstall.js +185 -258
- package/scripts/preuninstall.js +9 -50
- package/src/superlocalmemory/__init__.py +2 -2
- package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
- package/src/superlocalmemory/attribution/signer.py +34 -19
- package/src/superlocalmemory/attribution/watermark.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +3 -5
- package/src/superlocalmemory/cli/commands.py +490 -195
- package/src/superlocalmemory/cli/context_commands.py +5 -4
- package/src/superlocalmemory/cli/daemon.py +282 -187
- package/src/superlocalmemory/cli/db_migrate.py +3 -1
- package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
- package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
- package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
- package/src/superlocalmemory/cli/main.py +128 -31
- package/src/superlocalmemory/cli/pending_store.py +54 -38
- package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
- package/src/superlocalmemory/cli/service_installer.py +57 -52
- package/src/superlocalmemory/cli/setup_wizard.py +142 -88
- package/src/superlocalmemory/cli/version_banner.py +2 -1
- package/src/superlocalmemory/code_graph/config.py +3 -1
- package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
- package/src/superlocalmemory/core/config.py +65 -20
- package/src/superlocalmemory/core/consolidation_engine.py +9 -7
- package/src/superlocalmemory/core/context_cache.py +56 -8
- package/src/superlocalmemory/core/derivation_lineage.py +246 -0
- package/src/superlocalmemory/core/embedding_worker.py +32 -20
- package/src/superlocalmemory/core/embeddings.py +54 -18
- package/src/superlocalmemory/core/engine.py +150 -104
- package/src/superlocalmemory/core/engine_ingestion.py +513 -0
- package/src/superlocalmemory/core/engine_wiring.py +2 -0
- package/src/superlocalmemory/core/evidence_bundle.py +526 -0
- package/src/superlocalmemory/core/fact_consolidator.py +5 -11
- package/src/superlocalmemory/core/graph_analyzer.py +2 -2
- package/src/superlocalmemory/core/health_monitor.py +4 -2
- package/src/superlocalmemory/core/ingestion_command.py +636 -0
- package/src/superlocalmemory/core/injection.py +69 -18
- package/src/superlocalmemory/core/lifecycle_state.py +153 -0
- package/src/superlocalmemory/core/maintenance.py +23 -22
- package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
- package/src/superlocalmemory/core/mutations.py +143 -0
- package/src/superlocalmemory/core/platform_utils.py +7 -4
- package/src/superlocalmemory/core/ram_lock.py +16 -5
- package/src/superlocalmemory/core/rate_limit.py +1 -1
- package/src/superlocalmemory/core/recall_pipeline.py +60 -101
- package/src/superlocalmemory/core/recall_worker.py +76 -59
- package/src/superlocalmemory/core/registry.py +1 -1
- package/src/superlocalmemory/core/scale_engine.py +293 -0
- package/src/superlocalmemory/core/score_contract.py +62 -0
- package/src/superlocalmemory/core/security_primitives.py +3 -1
- package/src/superlocalmemory/core/slm_disabled.py +3 -5
- package/src/superlocalmemory/core/store_pipeline.py +172 -40
- package/src/superlocalmemory/core/tier_manager.py +32 -20
- package/src/superlocalmemory/core/worker_pool.py +13 -4
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
- package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
- package/src/superlocalmemory/encoding/auto_linker.py +1 -1
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
- package/src/superlocalmemory/encoding/consolidator.py +22 -5
- package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
- package/src/superlocalmemory/encoding/foresight.py +2 -0
- package/src/superlocalmemory/encoding/graph_builder.py +1 -1
- package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
- package/src/superlocalmemory/evaluation/__init__.py +13 -0
- package/src/superlocalmemory/evaluation/calibration.py +308 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
- package/src/superlocalmemory/graph/cozo_backend.py +256 -23
- package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
- package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
- package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
- package/src/superlocalmemory/hooks/auto_recall.py +31 -6
- package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
- package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
- package/src/superlocalmemory/hooks/claude_code_hooks.py +126 -39
- package/src/superlocalmemory/hooks/codex_assets.py +59 -0
- package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
- package/src/superlocalmemory/hooks/context_payload.py +1 -1
- package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
- package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
- package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
- package/src/superlocalmemory/hooks/hook_handlers.py +241 -55
- package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
- package/src/superlocalmemory/hooks/session_registry.py +15 -8
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
- package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
- package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
- package/src/superlocalmemory/infra/auth_middleware.py +38 -5
- package/src/superlocalmemory/infra/backup.py +7 -5
- package/src/superlocalmemory/infra/cloud_backup.py +18 -8
- package/src/superlocalmemory/infra/daemon_identity.py +248 -0
- package/src/superlocalmemory/infra/data_root.py +199 -0
- package/src/superlocalmemory/infra/event_bus.py +3 -1
- package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
- package/src/superlocalmemory/infra/process_reaper.py +23 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
- package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
- package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
- package/src/superlocalmemory/ingestion/credentials.py +14 -7
- package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
- package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
- package/src/superlocalmemory/learning/ensemble.py +11 -0
- package/src/superlocalmemory/learning/entity_compiler.py +1 -1
- package/src/superlocalmemory/learning/feedback.py +1 -1
- package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
- package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
- package/src/superlocalmemory/learning/ranker.py +4 -1
- package/src/superlocalmemory/learning/source_quality.py +1 -1
- package/src/superlocalmemory/learning/trigram_index.py +3 -2
- package/src/superlocalmemory/llm/backbone.py +13 -8
- package/src/superlocalmemory/math/ebbinghaus.py +1 -1
- package/src/superlocalmemory/math/fisher.py +1 -1
- package/src/superlocalmemory/math/fisher_quantized.py +1 -1
- package/src/superlocalmemory/math/hopfield.py +1 -1
- package/src/superlocalmemory/math/langevin.py +1 -1
- package/src/superlocalmemory/math/polar_quant.py +3 -4
- package/src/superlocalmemory/math/qjl.py +1 -1
- package/src/superlocalmemory/math/sheaf.py +1 -1
- package/src/superlocalmemory/math/turbo_quant.py +3 -2
- package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
- package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
- package/src/superlocalmemory/mcp/http_transport.py +53 -0
- package/src/superlocalmemory/mcp/server.py +39 -13
- package/src/superlocalmemory/mcp/shared.py +69 -3
- package/src/superlocalmemory/mcp/tools_active.py +141 -31
- package/src/superlocalmemory/mcp/tools_core.py +128 -29
- package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
- package/src/superlocalmemory/mcp/tools_learning.py +42 -2
- package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
- package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
- package/src/superlocalmemory/mcp/tools_v28.py +23 -2
- package/src/superlocalmemory/mcp/tools_v3.py +26 -1
- package/src/superlocalmemory/mcp/tools_v33.py +56 -17
- package/src/superlocalmemory/mesh/broker.py +2 -0
- package/src/superlocalmemory/mesh/remote_sync.py +50 -12
- package/src/superlocalmemory/optimize/cache/manager.py +77 -1
- package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
- package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
- package/src/superlocalmemory/optimize/compress/router.py +6 -1
- package/src/superlocalmemory/optimize/config/__init__.py +5 -0
- package/src/superlocalmemory/optimize/config/store.py +6 -4
- package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
- package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
- package/src/superlocalmemory/optimize/proxy/server.py +2 -2
- package/src/superlocalmemory/optimize/storage/db.py +14 -13
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/ann_index.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
- package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
- package/src/superlocalmemory/retrieval/engine.py +169 -79
- package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
- package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
- package/src/superlocalmemory/retrieval/fusion.py +1 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
- package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
- package/src/superlocalmemory/retrieval/reranker.py +56 -20
- package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
- package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
- package/src/superlocalmemory/retrieval/strategy.py +1 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
- package/src/superlocalmemory/retrieval/vector_store.py +1 -1
- package/src/superlocalmemory/server/api.py +10 -7
- package/src/superlocalmemory/server/bandit_loops.py +4 -2
- package/src/superlocalmemory/server/recall_serializer.py +24 -0
- package/src/superlocalmemory/server/route_mutations.py +84 -0
- package/src/superlocalmemory/server/routes/agents.py +8 -6
- package/src/superlocalmemory/server/routes/brain.py +14 -12
- package/src/superlocalmemory/server/routes/chat.py +29 -12
- package/src/superlocalmemory/server/routes/data_io.py +55 -24
- package/src/superlocalmemory/server/routes/helpers.py +29 -4
- package/src/superlocalmemory/server/routes/ingest.py +53 -36
- package/src/superlocalmemory/server/routes/memories.py +104 -43
- package/src/superlocalmemory/server/routes/mesh.py +31 -0
- package/src/superlocalmemory/server/routes/profiles.py +26 -4
- package/src/superlocalmemory/server/routes/tiers.py +43 -11
- package/src/superlocalmemory/server/routes/timeline.py +5 -1
- package/src/superlocalmemory/server/routes/v3_api.py +76 -21
- package/src/superlocalmemory/server/security_middleware.py +1 -1
- package/src/superlocalmemory/server/ui.py +6 -3
- package/src/superlocalmemory/server/unified_daemon.py +680 -293
- package/src/superlocalmemory/server/write_identity.py +147 -0
- package/src/superlocalmemory/storage/access_log.py +4 -3
- package/src/superlocalmemory/storage/database.py +118 -25
- package/src/superlocalmemory/storage/migration_runner.py +84 -1
- package/src/superlocalmemory/storage/migration_v33.py +1 -1
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
- package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
- package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
- package/src/superlocalmemory/storage/models.py +16 -0
- package/src/superlocalmemory/storage/quantized_store.py +20 -3
- package/src/superlocalmemory/storage/v2_migrator.py +5 -3
- package/src/superlocalmemory/ui/favicon.svg +5 -0
- package/src/superlocalmemory/ui/index.html +1 -0
- package/src/superlocalmemory/ui/js/compliance.js +1 -1
- package/src/superlocalmemory/ui/js/core.js +49 -8
- package/src/superlocalmemory/ui/js/dashboard.js +23 -2
- package/src/superlocalmemory/ui/js/feedback.js +1 -1
- package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
- package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
- package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
- package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
- package/src/superlocalmemory/ui/js/settings.js +4 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
- package/bin/slm +0 -59
- package/bin/slm.bat +0 -77
- package/bin/slm.cmd +0 -5
- package/ide/integrations/langchain/README.md +0 -106
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
- package/ide/integrations/langchain/pyproject.toml +0 -38
- package/ide/integrations/langchain/tests/__init__.py +0 -3
- package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
- package/ide/integrations/langchain/tests/test_security.py +0 -117
- package/ide/integrations/llamaindex/README.md +0 -81
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
- package/ide/integrations/llamaindex/pyproject.toml +0 -43
- package/ide/integrations/llamaindex/tests/__init__.py +0 -3
- package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
- package/ide/integrations/llamaindex/tests/test_security.py +0 -241
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -15
- package/plugin-src/scripts/slm-launch.bat +0 -17
- package/plugin-src/settings.json +0 -16
- package/plugin-src/skills/slm-cache/SKILL.md +0 -140
- package/plugin-src/skills/slm-compress/SKILL.md +0 -143
- package/plugin-src/skills/slm-graph/SKILL.md +0 -300
- package/plugin-src/skills/slm-recall/SKILL.md +0 -204
- package/plugin-src/skills/slm-remember/SKILL.md +0 -194
- package/plugin-src/skills/slm-session/SKILL.md +0 -207
- package/plugin-src/skills/slm-status/SKILL.md +0 -149
- package/scripts/__tests__/build-plugin.test.mjs +0 -613
- package/scripts/_savings_math.py +0 -270
- package/scripts/build-dmg.sh +0 -417
- package/scripts/build-plugin.js +0 -742
- package/scripts/build-slm-hook.ps1 +0 -40
- package/scripts/build-slm-hook.sh +0 -45
- package/scripts/build_entry.py +0 -452
- package/scripts/ci/stage5b_gate.sh +0 -50
- package/scripts/dogfood_savings.py +0 -490
- package/scripts/generate-thumbnails.py +0 -218
- package/scripts/install-skills.ps1 +0 -4
- package/scripts/install-skills.sh +0 -5
- package/scripts/install.ps1 +0 -701
- package/scripts/install.sh +0 -1015
- package/scripts/postinstall_binary.js +0 -287
- package/scripts/prepack.js +0 -33
- package/scripts/release_manifest.py +0 -273
- package/scripts/slm-hook.spec +0 -56
- package/scripts/start-dashboard.ps1 +0 -52
- package/scripts/start-dashboard.sh +0 -41
- package/scripts/sync-wiki.ps1 +0 -127
- package/scripts/sync-wiki.sh +0 -82
- package/scripts/test-dmg.sh +0 -161
- package/scripts/test-npm-package.ps1 +0 -252
- package/scripts/test-npm-package.sh +0 -207
- package/scripts/verify-install.ps1 +0 -294
- package/scripts/verify-install.sh +0 -266
- package/scripts/verify-v27.ps1 +0 -301
- package/scripts/verify-v27.sh +0 -233
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -513
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -71
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
|
@@ -19,22 +19,20 @@ Key features:
|
|
|
19
19
|
- Returns [] on any error (HR-06)
|
|
20
20
|
|
|
21
21
|
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
22
|
-
License:
|
|
22
|
+
License: AGPL-3.0-or-later
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
27
|
import logging
|
|
28
|
+
import threading
|
|
28
29
|
import time
|
|
29
|
-
from typing import
|
|
30
|
+
from typing import Any
|
|
30
31
|
|
|
31
32
|
import numpy as np
|
|
32
33
|
|
|
33
34
|
from superlocalmemory.math.hopfield import HopfieldConfig, ModernHopfieldNetwork
|
|
34
|
-
|
|
35
|
-
if TYPE_CHECKING:
|
|
36
|
-
from superlocalmemory.retrieval.vector_store import VectorStore
|
|
37
|
-
from superlocalmemory.storage.database import DatabaseManager
|
|
35
|
+
from superlocalmemory.retrieval.scope_policy import filter_authorized_results
|
|
38
36
|
|
|
39
37
|
logger = logging.getLogger(__name__)
|
|
40
38
|
|
|
@@ -73,11 +71,13 @@ class HopfieldChannel:
|
|
|
73
71
|
self._vector_store = vector_store
|
|
74
72
|
self._config = config or HopfieldConfig()
|
|
75
73
|
self._hopfield = ModernHopfieldNetwork(self._config)
|
|
74
|
+
self._cache_lock = threading.RLock()
|
|
76
75
|
|
|
77
76
|
# Memory matrix cache (per LLD Section 2.2, HR-09)
|
|
78
77
|
self._cached_matrix: np.ndarray | None = None
|
|
79
78
|
self._cached_fact_ids: list[str] = []
|
|
80
79
|
self._cached_profile: str = ""
|
|
80
|
+
self._cached_scope_key: tuple[str, bool, bool] | None = None
|
|
81
81
|
self._cached_count: int = 0
|
|
82
82
|
self._cache_timestamp: float = 0.0
|
|
83
83
|
|
|
@@ -104,8 +104,17 @@ class HopfieldChannel:
|
|
|
104
104
|
if not self._config.enabled:
|
|
105
105
|
return []
|
|
106
106
|
|
|
107
|
+
include_global = bool(getattr(self, "include_global", False))
|
|
108
|
+
include_shared = bool(getattr(self, "include_shared", False))
|
|
107
109
|
try:
|
|
108
|
-
|
|
110
|
+
with self._cache_lock:
|
|
111
|
+
return self._search_inner(
|
|
112
|
+
query,
|
|
113
|
+
profile_id,
|
|
114
|
+
top_k,
|
|
115
|
+
include_global=include_global,
|
|
116
|
+
include_shared=include_shared,
|
|
117
|
+
)
|
|
109
118
|
except Exception as exc:
|
|
110
119
|
# HR-06: Return [] on any error
|
|
111
120
|
logger.warning("Hopfield channel error: %s", exc)
|
|
@@ -118,6 +127,9 @@ class HopfieldChannel:
|
|
|
118
127
|
query: Any,
|
|
119
128
|
profile_id: str,
|
|
120
129
|
top_k: int,
|
|
130
|
+
*,
|
|
131
|
+
include_global: bool = False,
|
|
132
|
+
include_shared: bool = False,
|
|
121
133
|
) -> list[tuple[str, float]]:
|
|
122
134
|
"""Core search logic, separated for clean error handling."""
|
|
123
135
|
# Step 2: Convert query to numpy
|
|
@@ -132,11 +144,18 @@ class HopfieldChannel:
|
|
|
132
144
|
return []
|
|
133
145
|
|
|
134
146
|
# Step 3b (AUDIT FIX G-MEDIUM-02): Check skip_threshold BEFORE loading matrix
|
|
135
|
-
|
|
136
|
-
self.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
try:
|
|
148
|
+
total_count = self._db.get_fact_count(
|
|
149
|
+
profile_id,
|
|
150
|
+
include_global=include_global,
|
|
151
|
+
include_shared=include_shared,
|
|
152
|
+
)
|
|
153
|
+
except (AttributeError, TypeError):
|
|
154
|
+
total_count = (
|
|
155
|
+
self._vector_store.count(profile_id)
|
|
156
|
+
if self._vector_store and getattr(self._vector_store, "available", False)
|
|
157
|
+
else 0
|
|
158
|
+
)
|
|
140
159
|
# Step 3c: Skip for very large stores
|
|
141
160
|
if total_count > self._config.skip_threshold:
|
|
142
161
|
logger.debug(
|
|
@@ -162,17 +181,42 @@ class HopfieldChannel:
|
|
|
162
181
|
# VS exists. Routing on prefilter_candidates (not prefilter_threshold)
|
|
163
182
|
# ensures the matrix is always bounded to ~prefilter_candidates rows.
|
|
164
183
|
if vs_ok and total_count > self._config.prefilter_candidates:
|
|
165
|
-
return self._search_with_prefilter(
|
|
184
|
+
return self._search_with_prefilter(
|
|
185
|
+
q_vec,
|
|
186
|
+
profile_id,
|
|
187
|
+
[],
|
|
188
|
+
top_k,
|
|
189
|
+
include_global=include_global,
|
|
190
|
+
include_shared=include_shared,
|
|
191
|
+
)
|
|
166
192
|
|
|
167
193
|
# Tiny store (or no VS): build (cached) full matrix.
|
|
168
|
-
memory_matrix, fact_ids = self._get_memory_matrix(
|
|
194
|
+
memory_matrix, fact_ids = self._get_memory_matrix(
|
|
195
|
+
profile_id,
|
|
196
|
+
include_global=include_global,
|
|
197
|
+
include_shared=include_shared,
|
|
198
|
+
)
|
|
169
199
|
if memory_matrix is None or len(fact_ids) == 0:
|
|
170
200
|
return []
|
|
171
201
|
if vs_ok and len(fact_ids) > self._config.prefilter_candidates:
|
|
172
|
-
return self._search_with_prefilter(
|
|
173
|
-
|
|
202
|
+
return self._search_with_prefilter(
|
|
203
|
+
q_vec,
|
|
204
|
+
profile_id,
|
|
205
|
+
fact_ids,
|
|
206
|
+
top_k,
|
|
207
|
+
include_global=include_global,
|
|
208
|
+
include_shared=include_shared,
|
|
209
|
+
)
|
|
210
|
+
results = self._search_full_matrix(
|
|
174
211
|
q_vec, memory_matrix, fact_ids, top_k,
|
|
175
212
|
)
|
|
213
|
+
return filter_authorized_results(
|
|
214
|
+
self._db,
|
|
215
|
+
results,
|
|
216
|
+
profile_id,
|
|
217
|
+
include_global=include_global,
|
|
218
|
+
include_shared=include_shared,
|
|
219
|
+
)
|
|
176
220
|
|
|
177
221
|
def _search_full_matrix(
|
|
178
222
|
self,
|
|
@@ -220,6 +264,9 @@ class HopfieldChannel:
|
|
|
220
264
|
profile_id: str,
|
|
221
265
|
all_fact_ids: list[str],
|
|
222
266
|
top_k: int,
|
|
267
|
+
*,
|
|
268
|
+
include_global: bool = False,
|
|
269
|
+
include_shared: bool = False,
|
|
223
270
|
) -> list[tuple[str, float]]:
|
|
224
271
|
"""Two-stage retrieval for large stores (>prefilter_threshold facts).
|
|
225
272
|
|
|
@@ -243,15 +290,35 @@ class HopfieldChannel:
|
|
|
243
290
|
top_k=self._config.prefilter_candidates,
|
|
244
291
|
profile_id=profile_id,
|
|
245
292
|
)
|
|
246
|
-
|
|
293
|
+
# The ANN index is owner-profile partitioned. Supplement it with
|
|
294
|
+
# opted-in cross-profile facts, then authorize the combined candidates
|
|
295
|
+
# through the canonical DB predicate below.
|
|
296
|
+
external_facts = self._db.get_external_visible_facts(
|
|
297
|
+
profile_id,
|
|
298
|
+
include_global=include_global,
|
|
299
|
+
include_shared=include_shared,
|
|
300
|
+
)
|
|
301
|
+
combined = {fact_id: score for fact_id, score in knn_results}
|
|
302
|
+
query_norm = float(np.linalg.norm(query))
|
|
303
|
+
for fact in external_facts:
|
|
304
|
+
embedding = getattr(fact, "embedding", None)
|
|
305
|
+
if embedding is None or len(embedding) != self._config.dimension:
|
|
306
|
+
continue
|
|
307
|
+
vector = np.array(embedding, dtype=np.float32)
|
|
308
|
+
denominator = query_norm * float(np.linalg.norm(vector))
|
|
309
|
+
if denominator <= 1e-8:
|
|
310
|
+
continue
|
|
311
|
+
score = (float(np.dot(query, vector) / denominator) + 1.0) / 2.0
|
|
312
|
+
combined[fact.fact_id] = max(combined.get(fact.fact_id, 0.0), score)
|
|
313
|
+
if not combined:
|
|
247
314
|
return []
|
|
248
315
|
|
|
249
316
|
# Stage 2: Load candidate facts
|
|
250
|
-
candidate_ids =
|
|
317
|
+
candidate_ids = list(combined)
|
|
251
318
|
candidates = self._db.get_facts_by_ids(
|
|
252
319
|
candidate_ids, profile_id,
|
|
253
|
-
include_global=
|
|
254
|
-
include_shared=
|
|
320
|
+
include_global=include_global,
|
|
321
|
+
include_shared=include_shared,
|
|
255
322
|
)
|
|
256
323
|
if not candidates:
|
|
257
324
|
return []
|
|
@@ -276,10 +343,21 @@ class HopfieldChannel:
|
|
|
276
343
|
sub_matrix = sub_matrix / norms
|
|
277
344
|
|
|
278
345
|
# Stage 4: Hopfield on subset
|
|
279
|
-
|
|
346
|
+
results = self._search_full_matrix(query, sub_matrix, sub_ids, top_k)
|
|
347
|
+
return filter_authorized_results(
|
|
348
|
+
self._db,
|
|
349
|
+
results,
|
|
350
|
+
profile_id,
|
|
351
|
+
include_global=include_global,
|
|
352
|
+
include_shared=include_shared,
|
|
353
|
+
)
|
|
280
354
|
|
|
281
355
|
def _get_memory_matrix(
|
|
282
|
-
self,
|
|
356
|
+
self,
|
|
357
|
+
profile_id: str,
|
|
358
|
+
*,
|
|
359
|
+
include_global: bool = False,
|
|
360
|
+
include_shared: bool = False,
|
|
283
361
|
) -> tuple[np.ndarray | None, list[str]]:
|
|
284
362
|
"""Build or retrieve cached memory matrix X (n x d).
|
|
285
363
|
|
|
@@ -290,14 +368,22 @@ class HopfieldChannel:
|
|
|
290
368
|
(memory_matrix, fact_ids) or (None, []) if no valid facts.
|
|
291
369
|
"""
|
|
292
370
|
# Step 1: Check cache validity
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
371
|
+
scope_key = (profile_id, bool(include_global), bool(include_shared))
|
|
372
|
+
try:
|
|
373
|
+
current_count = self._db.get_fact_count(
|
|
374
|
+
profile_id,
|
|
375
|
+
include_global=include_global,
|
|
376
|
+
include_shared=include_shared,
|
|
377
|
+
)
|
|
378
|
+
except (AttributeError, TypeError):
|
|
379
|
+
current_count = (
|
|
380
|
+
self._vector_store.count(profile_id)
|
|
381
|
+
if self._vector_store and getattr(self._vector_store, "available", False)
|
|
382
|
+
else 0
|
|
383
|
+
)
|
|
298
384
|
|
|
299
385
|
if (
|
|
300
|
-
self.
|
|
386
|
+
self._cached_scope_key == scope_key
|
|
301
387
|
and self._cached_count == current_count
|
|
302
388
|
and self._cached_matrix is not None
|
|
303
389
|
and (time.monotonic() - self._cache_timestamp)
|
|
@@ -310,8 +396,8 @@ class HopfieldChannel:
|
|
|
310
396
|
# deserialize the whole table just to slice it.
|
|
311
397
|
facts = self._db.get_all_facts(
|
|
312
398
|
profile_id, limit=5000,
|
|
313
|
-
include_global=
|
|
314
|
-
include_shared=
|
|
399
|
+
include_global=include_global,
|
|
400
|
+
include_shared=include_shared,
|
|
315
401
|
)
|
|
316
402
|
if not facts:
|
|
317
403
|
return (None, [])
|
|
@@ -341,6 +427,7 @@ class HopfieldChannel:
|
|
|
341
427
|
self._cached_matrix = matrix
|
|
342
428
|
self._cached_fact_ids = fact_ids
|
|
343
429
|
self._cached_profile = profile_id
|
|
430
|
+
self._cached_scope_key = scope_key
|
|
344
431
|
self._cached_count = current_count
|
|
345
432
|
self._cache_timestamp = time.monotonic()
|
|
346
433
|
|
|
@@ -354,5 +441,6 @@ class HopfieldChannel:
|
|
|
354
441
|
"""
|
|
355
442
|
self._cached_matrix = None
|
|
356
443
|
self._cached_fact_ids = []
|
|
444
|
+
self._cached_scope_key = None
|
|
357
445
|
self._cached_count = 0
|
|
358
446
|
self._cache_timestamp = 0.0
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
Merges results from:
|
|
8
8
|
Tier 1: float32 (VectorStore.search -- exact cosine)
|
|
9
|
-
Tier 2: int8 (
|
|
10
|
-
Tier 3: polar (QuantizedEmbeddingStore
|
|
9
|
+
Tier 2: int8 (QuantizedEmbeddingStore -- 8-bit PolarQuant rows)
|
|
10
|
+
Tier 3: polar (QuantizedEmbeddingStore -- 2/4-bit PolarQuant rows)
|
|
11
11
|
|
|
12
12
|
Deduplicates by keeping the highest score per fact_id.
|
|
13
13
|
Applies precision-dependent score penalties:
|
|
@@ -16,7 +16,7 @@ Applies precision-dependent score penalties:
|
|
|
16
16
|
- polar: config.polar_search_penalty (default 0.95x)
|
|
17
17
|
|
|
18
18
|
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
19
|
-
License:
|
|
19
|
+
License: AGPL-3.0-or-later
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
@@ -110,16 +110,17 @@ class QuantizationAwareSearch:
|
|
|
110
110
|
def _search_int8(
|
|
111
111
|
self, query: NDArray, profile_id: str, top_k: int,
|
|
112
112
|
) -> list[tuple[str, float]]:
|
|
113
|
-
"""Tier 2:
|
|
113
|
+
"""Tier 2: persisted 8-bit quantized embeddings.
|
|
114
114
|
|
|
115
115
|
Applies 0.98x penalty to account for int8 quantization error.
|
|
116
|
-
Gracefully returns [] if VectorStore lacks search_int8 method.
|
|
117
116
|
"""
|
|
118
|
-
fn = getattr(self._vector_store, "search_int8", None)
|
|
119
|
-
if fn is None:
|
|
120
|
-
return []
|
|
121
117
|
try:
|
|
122
|
-
raw =
|
|
118
|
+
raw = self._quantized_store.search(
|
|
119
|
+
query,
|
|
120
|
+
profile_id,
|
|
121
|
+
top_k,
|
|
122
|
+
bit_widths=(8,),
|
|
123
|
+
)
|
|
123
124
|
return [(fid, score * _INT8_PENALTY) for fid, score in raw]
|
|
124
125
|
except Exception as exc:
|
|
125
126
|
logger.debug("int8 search failed: %s", exc)
|
|
@@ -133,7 +134,12 @@ class QuantizationAwareSearch:
|
|
|
133
134
|
Applies polar_search_penalty from config.
|
|
134
135
|
"""
|
|
135
136
|
try:
|
|
136
|
-
raw = self._quantized_store.search(
|
|
137
|
+
raw = self._quantized_store.search(
|
|
138
|
+
query,
|
|
139
|
+
profile_id,
|
|
140
|
+
top_k,
|
|
141
|
+
bit_widths=(2, 4),
|
|
142
|
+
)
|
|
137
143
|
penalty = self._config.polar_search_penalty
|
|
138
144
|
return [(fid, score * penalty) for fid, score in raw]
|
|
139
145
|
except Exception as exc:
|
|
@@ -11,7 +11,7 @@ at ~60 MB. Same isolation pattern as EmbeddingService.
|
|
|
11
11
|
The worker subprocess auto-kills after 2 minutes idle.
|
|
12
12
|
|
|
13
13
|
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
14
|
-
License:
|
|
14
|
+
License: AGPL-3.0-or-later
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
@@ -29,21 +29,27 @@ from typing import Any
|
|
|
29
29
|
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
|
|
32
|
+
from superlocalmemory.infra.data_root import state_path
|
|
32
33
|
from superlocalmemory.storage.models import AtomicFact
|
|
33
34
|
|
|
34
|
-
_RERANKER_PID_FILE =
|
|
35
|
+
_RERANKER_PID_FILE = None # test-only override
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _reranker_pid_file() -> Path:
|
|
39
|
+
return _RERANKER_PID_FILE or state_path(".reranker-worker.pid")
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
def _is_reranker_worker_alive() -> bool:
|
|
38
43
|
"""Check if a reranker worker PID is already alive (machine-wide singleton)."""
|
|
39
44
|
try:
|
|
40
|
-
|
|
45
|
+
pid_file = _reranker_pid_file()
|
|
46
|
+
if not pid_file.exists():
|
|
41
47
|
return False
|
|
42
|
-
pid = int(
|
|
48
|
+
pid = int(pid_file.read_text().strip())
|
|
43
49
|
os.kill(pid, 0)
|
|
44
50
|
return True
|
|
45
51
|
except (ValueError, OSError, ProcessLookupError):
|
|
46
|
-
|
|
52
|
+
_reranker_pid_file().unlink(missing_ok=True)
|
|
47
53
|
return False
|
|
48
54
|
|
|
49
55
|
# Track all live reranker instances for atexit cleanup
|
|
@@ -215,8 +221,9 @@ class CrossEncoderReranker:
|
|
|
215
221
|
**popen_platform_kwargs(),
|
|
216
222
|
)
|
|
217
223
|
# v3.4.13: Register PID for machine-wide singleton
|
|
218
|
-
|
|
219
|
-
|
|
224
|
+
pid_file = _reranker_pid_file()
|
|
225
|
+
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
pid_file.write_text(str(self._worker_proc.pid))
|
|
220
227
|
logger.info(
|
|
221
228
|
"Reranker worker spawned (PID %d)", self._worker_proc.pid,
|
|
222
229
|
)
|
|
@@ -321,22 +328,41 @@ class CrossEncoderReranker:
|
|
|
321
328
|
return result_container[0] if result_container else ""
|
|
322
329
|
|
|
323
330
|
def _kill_worker(self) -> None:
|
|
324
|
-
"""Terminate worker
|
|
331
|
+
"""Terminate the worker and close every owned pipe exactly once."""
|
|
325
332
|
if self._idle_timer is not None:
|
|
326
333
|
self._idle_timer.cancel()
|
|
327
334
|
self._idle_timer = None
|
|
328
|
-
|
|
335
|
+
|
|
336
|
+
proc = self._worker_proc
|
|
337
|
+
if proc is not None:
|
|
338
|
+
# Detach first so re-entrant/finalizer cleanup is idempotent.
|
|
339
|
+
self._worker_proc = None
|
|
340
|
+
self._worker_ready = False
|
|
329
341
|
try:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
342
|
+
proc.stdin.write('{"cmd":"quit"}\n')
|
|
343
|
+
proc.stdin.flush()
|
|
344
|
+
proc.wait(timeout=3)
|
|
333
345
|
except Exception:
|
|
334
346
|
try:
|
|
335
|
-
|
|
347
|
+
returncode = proc.poll()
|
|
336
348
|
except Exception:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
349
|
+
returncode = None
|
|
350
|
+
if returncode is None or not isinstance(returncode, int):
|
|
351
|
+
try:
|
|
352
|
+
proc.kill()
|
|
353
|
+
proc.wait(timeout=3)
|
|
354
|
+
except Exception:
|
|
355
|
+
pass
|
|
356
|
+
finally:
|
|
357
|
+
# Explicit close prevents TextIOWrapper from flushing a dead
|
|
358
|
+
# child's stdin later from an unraisable object finalizer.
|
|
359
|
+
for stream_name in ("stdin", "stdout", "stderr"):
|
|
360
|
+
stream = getattr(proc, stream_name, None)
|
|
361
|
+
if stream is not None:
|
|
362
|
+
try:
|
|
363
|
+
stream.close()
|
|
364
|
+
except (BrokenPipeError, OSError, ValueError):
|
|
365
|
+
pass
|
|
340
366
|
|
|
341
367
|
def _reset_idle_timer(self) -> None:
|
|
342
368
|
"""Reset idle timer — kills worker after 2 min inactivity."""
|
|
@@ -373,13 +399,23 @@ class CrossEncoderReranker:
|
|
|
373
399
|
results (without reranking), and MCP gets reranked results
|
|
374
400
|
(worker stays warm between calls).
|
|
375
401
|
"""
|
|
402
|
+
results, _, _ = self.rerank_with_status(query, candidates, top_k=top_k)
|
|
403
|
+
return results
|
|
404
|
+
|
|
405
|
+
def rerank_with_status(
|
|
406
|
+
self,
|
|
407
|
+
query: str,
|
|
408
|
+
candidates: list[tuple[AtomicFact, float]],
|
|
409
|
+
top_k: int = 10,
|
|
410
|
+
) -> tuple[list[tuple[AtomicFact, float]], bool, str]:
|
|
411
|
+
"""Return results plus whether cross-encoder inference actually ran."""
|
|
376
412
|
if not candidates:
|
|
377
|
-
return []
|
|
413
|
+
return [], False, "no_candidates"
|
|
378
414
|
|
|
379
415
|
# Non-blocking: if model isn't loaded yet, return fallback
|
|
380
416
|
if not self._model_loaded:
|
|
381
417
|
sorted_cands = sorted(candidates, key=lambda x: x[1], reverse=True)
|
|
382
|
-
return sorted_cands[:top_k]
|
|
418
|
+
return sorted_cands[:top_k], False, "fallback_not_ready"
|
|
383
419
|
|
|
384
420
|
documents = [fact.content for fact, _ in candidates]
|
|
385
421
|
|
|
@@ -397,7 +433,7 @@ class CrossEncoderReranker:
|
|
|
397
433
|
if resp is None or not resp.get("ok"):
|
|
398
434
|
# Fallback: return by existing score
|
|
399
435
|
sorted_cands = sorted(candidates, key=lambda x: x[1], reverse=True)
|
|
400
|
-
return sorted_cands[:top_k]
|
|
436
|
+
return sorted_cands[:top_k], False, "fallback_busy_or_unavailable"
|
|
401
437
|
|
|
402
438
|
scores = resp["scores"]
|
|
403
439
|
scored: list[tuple[AtomicFact, float]] = [
|
|
@@ -405,7 +441,7 @@ class CrossEncoderReranker:
|
|
|
405
441
|
for (fact, _), score in zip(candidates, scores)
|
|
406
442
|
]
|
|
407
443
|
scored.sort(key=lambda x: x[1], reverse=True)
|
|
408
|
-
return scored[:top_k]
|
|
444
|
+
return scored[:top_k], True, "applied"
|
|
409
445
|
|
|
410
446
|
def score_pair(self, query: str, document: str) -> float:
|
|
411
447
|
"""Score a single (query, document) pair."""
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Fail-closed authorization helpers for retrieval candidate paths.
|
|
5
|
+
|
|
6
|
+
Candidate generators may use caches, approximate indexes, or graph stores that
|
|
7
|
+
are not the authorization source of truth. Every such path must therefore
|
|
8
|
+
re-authorize fact IDs through ``DatabaseManager.get_facts_by_ids()``, whose SQL
|
|
9
|
+
is built by the canonical ``_scope_where`` predicate.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any, Iterable
|
|
15
|
+
|
|
16
|
+
from superlocalmemory.storage.database import _scope_where
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def authorized_fact_ids(
|
|
20
|
+
db: Any,
|
|
21
|
+
fact_ids: Iterable[str],
|
|
22
|
+
profile_id: str,
|
|
23
|
+
*,
|
|
24
|
+
include_global: bool = False,
|
|
25
|
+
include_shared: bool = False,
|
|
26
|
+
) -> set[str]:
|
|
27
|
+
"""Return only IDs visible under the canonical scope predicate.
|
|
28
|
+
|
|
29
|
+
Authorization errors fail closed. The stable de-duplication avoids SQLite
|
|
30
|
+
parameter waste without changing candidate order at the caller boundary.
|
|
31
|
+
"""
|
|
32
|
+
unique_ids = list(dict.fromkeys(fact_ids))
|
|
33
|
+
if not unique_ids:
|
|
34
|
+
return set()
|
|
35
|
+
try:
|
|
36
|
+
facts = db.get_facts_by_ids(
|
|
37
|
+
unique_ids,
|
|
38
|
+
profile_id,
|
|
39
|
+
include_global=bool(include_global),
|
|
40
|
+
include_shared=bool(include_shared),
|
|
41
|
+
)
|
|
42
|
+
if isinstance(facts, list):
|
|
43
|
+
return {fact.fact_id for fact in facts}
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
# Lightweight DB wrappers used by maintenance paths may expose execute()
|
|
48
|
+
# without the higher-level method. Keep the same canonical predicate.
|
|
49
|
+
try:
|
|
50
|
+
where, params = _scope_where(
|
|
51
|
+
profile_id,
|
|
52
|
+
include_global=include_global,
|
|
53
|
+
include_shared=include_shared,
|
|
54
|
+
)
|
|
55
|
+
placeholders = ",".join("?" for _ in unique_ids)
|
|
56
|
+
rows = db.execute(
|
|
57
|
+
f"SELECT fact_id FROM atomic_facts WHERE fact_id IN ({placeholders}) "
|
|
58
|
+
f"AND {where}",
|
|
59
|
+
(*unique_ids, *params),
|
|
60
|
+
)
|
|
61
|
+
if not isinstance(rows, list):
|
|
62
|
+
rows = list(rows)
|
|
63
|
+
return {dict(row)["fact_id"] for row in rows}
|
|
64
|
+
except Exception:
|
|
65
|
+
return set()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def filter_authorized_results(
|
|
69
|
+
db: Any,
|
|
70
|
+
results: Iterable[tuple[str, float]],
|
|
71
|
+
profile_id: str,
|
|
72
|
+
*,
|
|
73
|
+
include_global: bool = False,
|
|
74
|
+
include_shared: bool = False,
|
|
75
|
+
) -> list[tuple[str, float]]:
|
|
76
|
+
"""Preserve result order/scores while removing unauthorized fact IDs."""
|
|
77
|
+
materialized = list(results)
|
|
78
|
+
allowed = authorized_fact_ids(
|
|
79
|
+
db,
|
|
80
|
+
(fact_id for fact_id, _score in materialized),
|
|
81
|
+
profile_id,
|
|
82
|
+
include_global=include_global,
|
|
83
|
+
include_shared=include_shared,
|
|
84
|
+
)
|
|
85
|
+
return [item for item in materialized if item[0] in allowed]
|