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
|
@@ -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:
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
|
137
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
166
|
-
(
|
|
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(
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
194
|
-
for
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
825
|
+
threshold=self._threshold,
|
|
826
|
+
top_k=top_k * 2,
|
|
626
827
|
)
|
|
627
828
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
#
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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:
|
|
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:
|
|
11
|
+
License: AGPL-3.0-or-later
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|