superlocalmemory 3.2.3 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +106 -71
  3. package/package.json +1 -2
  4. package/pyproject.toml +16 -1
  5. package/src/superlocalmemory/cli/commands.py +419 -15
  6. package/src/superlocalmemory/cli/main.py +44 -0
  7. package/src/superlocalmemory/core/config.py +276 -4
  8. package/src/superlocalmemory/core/consolidation_engine.py +37 -0
  9. package/src/superlocalmemory/core/engine.py +21 -0
  10. package/src/superlocalmemory/core/engine_wiring.py +58 -8
  11. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +374 -0
  12. package/src/superlocalmemory/dynamics/eap_scheduler.py +276 -0
  13. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +171 -0
  14. package/src/superlocalmemory/encoding/cognitive_consolidator.py +804 -0
  15. package/src/superlocalmemory/hooks/auto_invoker.py +46 -8
  16. package/src/superlocalmemory/hooks/auto_parameterize.py +147 -0
  17. package/src/superlocalmemory/infra/heartbeat_monitor.py +140 -0
  18. package/src/superlocalmemory/infra/pid_manager.py +193 -0
  19. package/src/superlocalmemory/infra/process_reaper.py +572 -0
  20. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +115 -0
  21. package/src/superlocalmemory/learning/forgetting_scheduler.py +263 -0
  22. package/src/superlocalmemory/learning/quantization_scheduler.py +320 -0
  23. package/src/superlocalmemory/math/ebbinghaus.py +309 -0
  24. package/src/superlocalmemory/math/fisher_quantized.py +251 -0
  25. package/src/superlocalmemory/math/hopfield.py +279 -0
  26. package/src/superlocalmemory/math/polar_quant.py +379 -0
  27. package/src/superlocalmemory/math/qjl.py +115 -0
  28. package/src/superlocalmemory/mcp/server.py +2 -0
  29. package/src/superlocalmemory/mcp/tools_v3.py +10 -0
  30. package/src/superlocalmemory/mcp/tools_v33.py +351 -0
  31. package/src/superlocalmemory/parameterization/__init__.py +47 -0
  32. package/src/superlocalmemory/parameterization/pattern_extractor.py +534 -0
  33. package/src/superlocalmemory/parameterization/pii_filter.py +106 -0
  34. package/src/superlocalmemory/parameterization/prompt_injector.py +216 -0
  35. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +275 -0
  36. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +425 -0
  37. package/src/superlocalmemory/retrieval/engine.py +21 -3
  38. package/src/superlocalmemory/retrieval/forgetting_filter.py +145 -0
  39. package/src/superlocalmemory/retrieval/hopfield_channel.py +335 -0
  40. package/src/superlocalmemory/retrieval/quantization_aware_search.py +133 -0
  41. package/src/superlocalmemory/retrieval/strategy.py +16 -6
  42. package/src/superlocalmemory/server/routes/agents.py +68 -8
  43. package/src/superlocalmemory/server/routes/learning.py +18 -1
  44. package/src/superlocalmemory/server/routes/lifecycle.py +36 -17
  45. package/src/superlocalmemory/server/routes/v3_api.py +503 -1
  46. package/src/superlocalmemory/storage/database.py +206 -0
  47. package/src/superlocalmemory/storage/embedding_migrator.py +178 -0
  48. package/src/superlocalmemory/storage/migration_v33.py +140 -0
  49. package/src/superlocalmemory/storage/quantized_store.py +261 -0
  50. package/src/superlocalmemory/storage/schema_v32.py +137 -0
  51. package/conftest.py +0 -5
@@ -0,0 +1,309 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Ebbinghaus forgetting curve — memory strength and retention.
6
+
7
+ Implements the classic Ebbinghaus retention formula:
8
+
9
+ R(t) = e^(-t/S)
10
+
11
+ where:
12
+ R = retention score in [0, 1]
13
+ t = hours since last access
14
+ S = memory strength (composite of access, importance, confirmations, emotion)
15
+
16
+ Memory strength formula (ACT-R inspired):
17
+
18
+ S = alpha * log(1 + access_count)
19
+ + beta * importance
20
+ + gamma * confirmation_count
21
+ + delta * emotional_salience
22
+
23
+ Lifecycle zones are derived from retention score:
24
+ active (R > 0.8) -> weight 1.0
25
+ warm (R > 0.5) -> weight 0.7
26
+ cold (R > 0.2) -> weight 0.3
27
+ archive (R > 0.05) -> weight 0.0
28
+ forgotten (R <= 0.05) -> weight 0.0
29
+
30
+ References:
31
+ Ebbinghaus H (1885). Memory: A Contribution to Experimental Psychology.
32
+ Anderson J R & Lebiere C (1998). The Atomic Components of Thought. ACT-R.
33
+ Zhong W et al. (2024). MemoryBank: Enhancing Large Language Models with
34
+ Long-Term Memory. AAAI 2024.
35
+
36
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
37
+ License: MIT
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import logging
43
+ import math
44
+ from dataclasses import dataclass
45
+ from datetime import UTC, datetime
46
+ from typing import TYPE_CHECKING, TypedDict
47
+
48
+ if TYPE_CHECKING:
49
+ from superlocalmemory.storage.database import DatabaseManager
50
+
51
+ from superlocalmemory.core.config import ForgettingConfig
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Data types
58
+ # ---------------------------------------------------------------------------
59
+
60
+ @dataclass(frozen=True)
61
+ class MemoryStrength:
62
+ """Computed strength for a single memory."""
63
+
64
+ fact_id: str
65
+ strength: float # S(m) in [S_MIN, S_MAX]
66
+ access_component: float # alpha * log(1 + access_count)
67
+ importance_component: float # beta * importance
68
+ confirmation_component: float # gamma * confirmation_count
69
+ emotional_component: float # delta * emotional_salience
70
+
71
+
72
+ class FactRetentionInput(TypedDict):
73
+ """Input dict for batch retention computation. All keys are required."""
74
+
75
+ fact_id: str
76
+ access_count: int
77
+ importance: float # PageRank score from fact_importance
78
+ confirmation_count: int # Mapped from atomic_facts.evidence_count
79
+ emotional_salience: float # Mapped from atomic_facts.emotional_valence
80
+ last_accessed_at: str # ISO 8601 datetime string
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Lifecycle zone weights
85
+ # ---------------------------------------------------------------------------
86
+
87
+ _ZONE_WEIGHTS: dict[str, float] = {
88
+ "active": 1.0,
89
+ "warm": 0.7,
90
+ "cold": 0.3,
91
+ "archive": 0.0,
92
+ "forgotten": 0.0,
93
+ }
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # EbbinghausCurve
98
+ # ---------------------------------------------------------------------------
99
+
100
+ class EbbinghausCurve:
101
+ """Ebbinghaus forgetting curve with configurable strength formula.
102
+
103
+ Provides retention computation, memory strength calculation,
104
+ spaced repetition updates, lifecycle zone classification, and
105
+ batch processing for the forgetting scheduler.
106
+ """
107
+
108
+ __slots__ = ("_config",)
109
+
110
+ def __init__(self, config: ForgettingConfig) -> None:
111
+ self._config = config
112
+
113
+ def retention(self, hours_since_access: float, strength: float) -> float:
114
+ """Compute Ebbinghaus retention R(t) = e^(-t/S).
115
+
116
+ Args:
117
+ hours_since_access: Hours since last access. Negative treated as fresh.
118
+ strength: Memory strength S. Floored at min_strength.
119
+
120
+ Returns:
121
+ Retention score in [0.0, 1.0]. Never NaN, never Inf.
122
+ """
123
+ # HR-02: Validate negative time
124
+ if hours_since_access < 0:
125
+ return 1.0
126
+
127
+ # HR-03: Floor strength
128
+ s = max(self._config.min_strength, strength)
129
+
130
+ # Compute decay rate and retention
131
+ lambda_ = 1.0 / s
132
+ r = math.exp(-lambda_ * hours_since_access)
133
+
134
+ # NaN/Inf guard (A-MED-02) — MUST come before clamp
135
+ if math.isnan(r) or math.isinf(r):
136
+ logger.warning(
137
+ "retention(): NaN/Inf detected (lambda_=%f, t=%f), returning 0.0",
138
+ lambda_, hours_since_access,
139
+ )
140
+ return 0.0
141
+
142
+ # HR-02: Clamp to [0.0, 1.0]
143
+ return max(0.0, min(1.0, r))
144
+
145
+ def memory_strength(
146
+ self,
147
+ access_count: int,
148
+ importance: float,
149
+ confirmation_count: int,
150
+ emotional_salience: float,
151
+ ) -> float:
152
+ """Compute composite memory strength S(m).
153
+
154
+ S = alpha * log(1 + access_count)
155
+ + beta * importance
156
+ + gamma * confirmation_count
157
+ + delta * emotional_salience
158
+
159
+ Args:
160
+ access_count: Number of times memory was accessed.
161
+ importance: PageRank importance score.
162
+ confirmation_count: Number of confirmations/evidence.
163
+ emotional_salience: Emotional strength [-1, 1].
164
+
165
+ Returns:
166
+ Clamped strength in [min_strength, max_strength].
167
+ """
168
+ cfg = self._config
169
+ a = cfg.alpha * math.log(1.0 + access_count)
170
+ b = cfg.beta * importance
171
+ c = cfg.gamma * confirmation_count
172
+ d = cfg.delta * emotional_salience
173
+ s = a + b + c + d
174
+
175
+ # HR-03: Clamp to bounds
176
+ return max(cfg.min_strength, min(cfg.max_strength, s))
177
+
178
+ def compute_strength(
179
+ self,
180
+ fact_id: str,
181
+ access_count: int,
182
+ importance: float,
183
+ confirmation_count: int,
184
+ emotional_salience: float,
185
+ ) -> MemoryStrength:
186
+ """Compute MemoryStrength dataclass for a single fact.
187
+
188
+ Returns:
189
+ MemoryStrength with all component scores.
190
+ """
191
+ cfg = self._config
192
+ a = cfg.alpha * math.log(1.0 + access_count)
193
+ b = cfg.beta * importance
194
+ c = cfg.gamma * confirmation_count
195
+ d = cfg.delta * emotional_salience
196
+ s = a + b + c + d
197
+ s = max(cfg.min_strength, min(cfg.max_strength, s))
198
+
199
+ return MemoryStrength(
200
+ fact_id=fact_id,
201
+ strength=s,
202
+ access_component=a,
203
+ importance_component=b,
204
+ confirmation_component=c,
205
+ emotional_component=d,
206
+ )
207
+
208
+ def spaced_repetition_update(
209
+ self, current_strength: float, hours_since_last_access: float,
210
+ ) -> float:
211
+ """Boost strength on re-access (spaced repetition effect).
212
+
213
+ Longer gaps between accesses produce larger boosts — this is the
214
+ spacing effect from cognitive science.
215
+
216
+ HR-07: Only INCREASES strength, never decreases.
217
+
218
+ Args:
219
+ current_strength: Current memory strength.
220
+ hours_since_last_access: Hours since last access.
221
+
222
+ Returns:
223
+ Updated strength, clamped to [min_strength, max_strength].
224
+ """
225
+ cfg = self._config
226
+ interval = math.log(1.0 + hours_since_last_access / 24.0)
227
+ boost = cfg.learning_rate * interval
228
+ s_new = current_strength + boost
229
+
230
+ # HR-03: Clamp
231
+ return max(cfg.min_strength, min(cfg.max_strength, s_new))
232
+
233
+ def lifecycle_zone(self, retention_score: float) -> str:
234
+ """Classify retention score into lifecycle zone.
235
+
236
+ Args:
237
+ retention_score: R in [0, 1].
238
+
239
+ Returns:
240
+ One of: 'active', 'warm', 'cold', 'archive', 'forgotten'.
241
+ """
242
+ if retention_score > 0.8:
243
+ return "active"
244
+ if retention_score > 0.5:
245
+ return "warm"
246
+ if retention_score > self._config.archive_threshold:
247
+ return "cold"
248
+ if retention_score > self._config.forget_threshold:
249
+ return "archive"
250
+ return "forgotten"
251
+
252
+ def lifecycle_weight(self, zone: str) -> float:
253
+ """Get retrieval weight for a lifecycle zone.
254
+
255
+ Args:
256
+ zone: Lifecycle zone name.
257
+
258
+ Returns:
259
+ Weight in [0.0, 1.0].
260
+ """
261
+ return _ZONE_WEIGHTS.get(zone, 0.0)
262
+
263
+ def batch_compute_retention(
264
+ self, facts: list[FactRetentionInput],
265
+ ) -> list[dict]:
266
+ """Compute retention for a batch of facts.
267
+
268
+ Args:
269
+ facts: List of FactRetentionInput dicts.
270
+
271
+ Returns:
272
+ List of dicts with fact_id, retention, strength, zone.
273
+ """
274
+ now = datetime.now(UTC)
275
+ results: list[dict] = []
276
+
277
+ for fact in facts:
278
+ access_count = fact["access_count"]
279
+ importance = fact["importance"]
280
+ confirmation_count = fact["confirmation_count"]
281
+ emotional_salience = fact["emotional_salience"]
282
+ last_accessed_at = fact["last_accessed_at"]
283
+
284
+ # Compute hours since last access
285
+ try:
286
+ last_dt = datetime.fromisoformat(last_accessed_at)
287
+ # Ensure timezone-aware
288
+ if last_dt.tzinfo is None:
289
+ last_dt = last_dt.replace(tzinfo=UTC)
290
+ hours_since = (now - last_dt).total_seconds() / 3600.0
291
+ except (ValueError, TypeError):
292
+ hours_since = 0.0
293
+
294
+ strength = self.memory_strength(
295
+ access_count, importance, confirmation_count, emotional_salience,
296
+ )
297
+ ret = self.retention(hours_since, strength)
298
+ zone = self.lifecycle_zone(ret)
299
+
300
+ results.append({
301
+ "fact_id": fact["fact_id"],
302
+ "retention": ret,
303
+ "strength": strength,
304
+ "zone": zone,
305
+ "access_count": access_count,
306
+ "last_accessed_at": last_accessed_at,
307
+ })
308
+
309
+ return results
@@ -0,0 +1,251 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Fisher-Rao Quantization-Aware Distance (FRQAD).
6
+
7
+ Novel contribution (98% IP confidence): Fisher-Rao geodesic distance on
8
+ mixed-precision embeddings. Quantized memories naturally rank lower because
9
+ their variance is inflated to model quantization uncertainty.
10
+
11
+ sigma_quant = sigma_base * (32 / bit_width) ^ kappa
12
+
13
+ When both embeddings are float32, FRQAD degrades exactly to standard
14
+ Fisher-Rao --- no regression, pure extension.
15
+
16
+ Mathematical basis:
17
+ - Fisher-Rao geodesic (Atkinson & Mitchell 1981, Pinele et al. 2020)
18
+ - Variance inflation models quantization noise as additional uncertainty
19
+ on the statistical manifold of diagonal Gaussians
20
+
21
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
22
+ License: MIT
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ import math
29
+ from dataclasses import dataclass
30
+
31
+ import numpy as np
32
+ from numpy.typing import NDArray
33
+
34
+ from superlocalmemory.math.fisher import FisherRaoMetric
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # Valid bit-widths aligned with Phase B/D (no 16-bit format exists).
39
+ _VALID_BIT_WIDTHS: frozenset[int] = frozenset({2, 4, 8, 32})
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Configuration
44
+ # ---------------------------------------------------------------------------
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class FRQADConfig:
49
+ """Configuration for FRQAD metric.
50
+
51
+ Attributes:
52
+ kappa: Scaling exponent for variance inflation.
53
+ sigma_quant = sigma_base * (32/bw)^kappa.
54
+ temperature: Softmax temperature for similarity conversion.
55
+ enabled: When False, fall back to base Fisher-Rao.
56
+ variance_floor: Minimum per-dimension variance after inflation.
57
+ variance_ceiling: Maximum per-dimension variance after inflation.
58
+ """
59
+
60
+ kappa: float = 0.5
61
+ temperature: float = 15.0
62
+ enabled: bool = True
63
+ variance_floor: float = 0.05
64
+ variance_ceiling: float = 10.0
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # FRQAD Metric
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ class FRQADMetric:
73
+ """Fisher-Rao Quantization-Aware Distance metric.
74
+
75
+ Wraps an existing FisherRaoMetric and inflates variance vectors based
76
+ on each embedding's bit-width before computing geodesic distance.
77
+
78
+ Self-consistency property:
79
+ FRQAD(32bit, 32bit) == FisherRao(same pair) within float epsilon.
80
+ """
81
+
82
+ __slots__ = ("_base", "_config")
83
+
84
+ def __init__(
85
+ self,
86
+ base_metric: FisherRaoMetric,
87
+ config: FRQADConfig,
88
+ ) -> None:
89
+ """Construct FRQAD metric.
90
+
91
+ Args:
92
+ base_metric: Existing Fisher-Rao metric instance.
93
+ config: FRQAD configuration.
94
+
95
+ Raises:
96
+ ValueError: On invalid configuration values.
97
+ """
98
+ # Validate config ranges (HR-04)
99
+ if not 0.0 <= config.kappa <= 2.0:
100
+ raise ValueError(
101
+ f"kappa must be in [0.0, 2.0], got {config.kappa}"
102
+ )
103
+ if not 1.0 <= config.temperature <= 100.0:
104
+ raise ValueError(
105
+ f"temperature must be in [1.0, 100.0], got {config.temperature}"
106
+ )
107
+ if not 0.001 <= config.variance_floor <= 1.0:
108
+ raise ValueError(
109
+ f"variance_floor must be in [0.001, 1.0], got {config.variance_floor}"
110
+ )
111
+ if not 1.0 <= config.variance_ceiling <= 100.0:
112
+ raise ValueError(
113
+ f"variance_ceiling must be in [1.0, 100.0], got {config.variance_ceiling}"
114
+ )
115
+
116
+ self._base = base_metric
117
+ self._config = config
118
+
119
+ # ------------------------------------------------------------------
120
+ # Quantization variance inflation
121
+ # ------------------------------------------------------------------
122
+
123
+ def quantization_variance(
124
+ self,
125
+ base_variance: NDArray[np.floating],
126
+ bit_width: int,
127
+ ) -> NDArray[np.floating]:
128
+ """Inflate variance to model quantization uncertainty.
129
+
130
+ HR-02: NEVER decreases base variance (scale >= 1.0 since bw <= 32).
131
+
132
+ Args:
133
+ base_variance: Per-dimension variance vector (strictly positive).
134
+ bit_width: Embedding precision in bits.
135
+
136
+ Returns:
137
+ Quantization-aware variance, clamped to [floor, ceiling].
138
+ """
139
+ if bit_width not in _VALID_BIT_WIDTHS:
140
+ logger.warning(
141
+ "Unknown bit_width %d, treating as 32 (no penalty)", bit_width,
142
+ )
143
+ return np.array(base_variance, dtype=np.float64)
144
+
145
+ if bit_width >= 32:
146
+ return np.array(base_variance, dtype=np.float64)
147
+
148
+ scale = (32.0 / bit_width) ** self._config.kappa
149
+ sigma_q = np.asarray(base_variance, dtype=np.float64) * scale
150
+
151
+ return np.clip(sigma_q, self._config.variance_floor, self._config.variance_ceiling)
152
+
153
+ # ------------------------------------------------------------------
154
+ # Core distance (THE novel contribution)
155
+ # ------------------------------------------------------------------
156
+
157
+ def distance(
158
+ self,
159
+ mu_a: NDArray[np.floating],
160
+ var_a: NDArray[np.floating],
161
+ bw_a: int,
162
+ mu_b: NDArray[np.floating],
163
+ var_b: NDArray[np.floating],
164
+ bw_b: int,
165
+ ) -> float:
166
+ """FRQAD distance between mixed-precision embeddings.
167
+
168
+ Inflates variance based on bit-width, then delegates to the
169
+ standard Fisher-Rao geodesic.
170
+
171
+ Args:
172
+ mu_a: Mean of embedding A.
173
+ var_a: Per-dimension variance of A.
174
+ bw_a: Bit-width of A.
175
+ mu_b: Mean of embedding B.
176
+ var_b: Per-dimension variance of B.
177
+ bw_b: Bit-width of B.
178
+
179
+ Returns:
180
+ Non-negative geodesic distance.
181
+ """
182
+ if not self._config.enabled:
183
+ return self._base.distance(
184
+ np.asarray(mu_a, dtype=np.float64).tolist(),
185
+ np.asarray(var_a, dtype=np.float64).tolist(),
186
+ np.asarray(mu_b, dtype=np.float64).tolist(),
187
+ np.asarray(var_b, dtype=np.float64).tolist(),
188
+ )
189
+
190
+ var_a_q = self.quantization_variance(var_a, bw_a)
191
+ var_b_q = self.quantization_variance(var_b, bw_b)
192
+
193
+ return self._base.distance(
194
+ np.asarray(mu_a, dtype=np.float64).tolist(),
195
+ var_a_q.tolist(),
196
+ np.asarray(mu_b, dtype=np.float64).tolist(),
197
+ var_b_q.tolist(),
198
+ )
199
+
200
+ # ------------------------------------------------------------------
201
+ # Similarity (exponential kernel)
202
+ # ------------------------------------------------------------------
203
+
204
+ def similarity(
205
+ self,
206
+ mu_a: NDArray[np.floating],
207
+ var_a: NDArray[np.floating],
208
+ bw_a: int,
209
+ mu_b: NDArray[np.floating],
210
+ var_b: NDArray[np.floating],
211
+ bw_b: int,
212
+ ) -> float:
213
+ """Convert FRQAD distance to similarity in [0, 1].
214
+
215
+ sim = clamp(exp(-distance / temperature), 0, 1)
216
+
217
+ HR-03: No NaN, no negative, no > 1.
218
+ """
219
+ d = self.distance(mu_a, var_a, bw_a, mu_b, var_b, bw_b)
220
+ sim = math.exp(-d / self._config.temperature)
221
+ return max(0.0, min(1.0, sim))
222
+
223
+ # ------------------------------------------------------------------
224
+ # Batch similarity
225
+ # ------------------------------------------------------------------
226
+
227
+ def batch_similarity(
228
+ self,
229
+ query_mu: NDArray[np.floating],
230
+ query_var: NDArray[np.floating],
231
+ query_bw: int,
232
+ candidates: list[tuple[str, NDArray[np.floating], NDArray[np.floating], int]],
233
+ ) -> list[tuple[str, float]]:
234
+ """Score a batch of candidate memories against a query.
235
+
236
+ Args:
237
+ query_mu: Query embedding mean.
238
+ query_var: Query embedding variance.
239
+ query_bw: Query bit-width (typically 32).
240
+ candidates: List of (fact_id, mu, var, bit_width) tuples.
241
+
242
+ Returns:
243
+ List of (fact_id, similarity) sorted descending by score.
244
+ """
245
+ results: list[tuple[str, float]] = []
246
+ for fact_id, mu, var, bw in candidates:
247
+ sim = self.similarity(query_mu, query_var, query_bw, mu, var, bw)
248
+ results.append((fact_id, sim))
249
+
250
+ results.sort(key=lambda x: x[1], reverse=True)
251
+ return results