sweet-search 0.0.1 → 2.4.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 (161) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +23 -0
  3. package/core/cli.js +51 -0
  4. package/core/config.js +27 -0
  5. package/core/embedding/embedding-cache.js +467 -0
  6. package/core/embedding/embedding-local-model.js +845 -0
  7. package/core/embedding/embedding-remote.js +492 -0
  8. package/core/embedding/embedding-service.js +712 -0
  9. package/core/embedding/embedding-telemetry.js +219 -0
  10. package/core/embedding/index.js +40 -0
  11. package/core/graph/community-detector.js +294 -0
  12. package/core/graph/graph-expansion.js +839 -0
  13. package/core/graph/graph-extractor.js +2304 -0
  14. package/core/graph/graph-search.js +2148 -0
  15. package/core/graph/hcgs-generator.js +666 -0
  16. package/core/graph/index.js +16 -0
  17. package/core/graph/leiden-algorithm.js +547 -0
  18. package/core/graph/relationship-resolver.js +366 -0
  19. package/core/graph/repo-map.js +408 -0
  20. package/core/graph/summary-manager.js +549 -0
  21. package/core/indexing/artifact-builder.js +1054 -0
  22. package/core/indexing/ast-chunker.js +709 -0
  23. package/core/indexing/chunking/chunk-builder.js +170 -0
  24. package/core/indexing/chunking/markdown-chunker.js +503 -0
  25. package/core/indexing/chunking/plaintext-chunker.js +104 -0
  26. package/core/indexing/dedup/dedup-phase.js +159 -0
  27. package/core/indexing/dedup/exemplar-selector.js +65 -0
  28. package/core/indexing/document-chunker.js +56 -0
  29. package/core/indexing/incremental-parser.js +390 -0
  30. package/core/indexing/incremental-tracker.js +761 -0
  31. package/core/indexing/index-codebase-v21.js +472 -0
  32. package/core/indexing/index-maintainer.mjs +1674 -0
  33. package/core/indexing/index.js +90 -0
  34. package/core/indexing/indexer-ann.js +1077 -0
  35. package/core/indexing/indexer-build.js +742 -0
  36. package/core/indexing/indexer-phases.js +800 -0
  37. package/core/indexing/indexer-pool.js +764 -0
  38. package/core/indexing/indexer-sparse-gram.js +98 -0
  39. package/core/indexing/indexer-utils.js +536 -0
  40. package/core/indexing/indexer-worker.js +148 -0
  41. package/core/indexing/li-skip-policy.js +225 -0
  42. package/core/indexing/merkle-tracker.js +244 -0
  43. package/core/indexing/model-pool.js +166 -0
  44. package/core/infrastructure/code-graph-repository.js +120 -0
  45. package/core/infrastructure/codebase-repository.js +131 -0
  46. package/core/infrastructure/config/dedup.js +54 -0
  47. package/core/infrastructure/config/embedding.js +298 -0
  48. package/core/infrastructure/config/graph.js +80 -0
  49. package/core/infrastructure/config/index.js +82 -0
  50. package/core/infrastructure/config/indexing.js +8 -0
  51. package/core/infrastructure/config/platform.js +254 -0
  52. package/core/infrastructure/config/ranking.js +221 -0
  53. package/core/infrastructure/config/search.js +396 -0
  54. package/core/infrastructure/config/translation.js +89 -0
  55. package/core/infrastructure/config/vector-store.js +114 -0
  56. package/core/infrastructure/constants.js +86 -0
  57. package/core/infrastructure/coreml-cascade.js +909 -0
  58. package/core/infrastructure/coreml-cascade.json +46 -0
  59. package/core/infrastructure/coreml-provider.js +81 -0
  60. package/core/infrastructure/db-utils.js +69 -0
  61. package/core/infrastructure/dedup-hashing.js +83 -0
  62. package/core/infrastructure/hardware-capability.js +332 -0
  63. package/core/infrastructure/index.js +104 -0
  64. package/core/infrastructure/language-patterns/maps.js +121 -0
  65. package/core/infrastructure/language-patterns/registry-core.js +323 -0
  66. package/core/infrastructure/language-patterns/registry-data-query.js +155 -0
  67. package/core/infrastructure/language-patterns/registry-object-oriented.js +285 -0
  68. package/core/infrastructure/language-patterns/registry-tooling.js +240 -0
  69. package/core/infrastructure/language-patterns/registry-web-style.js +143 -0
  70. package/core/infrastructure/language-patterns/registry.js +19 -0
  71. package/core/infrastructure/language-patterns.js +141 -0
  72. package/core/infrastructure/llm-provider.js +733 -0
  73. package/core/infrastructure/manifest.json +46 -0
  74. package/core/infrastructure/maxsim.wasm +0 -0
  75. package/core/infrastructure/model-fetcher.js +423 -0
  76. package/core/infrastructure/model-registry.js +214 -0
  77. package/core/infrastructure/native-inference.js +598 -0
  78. package/core/infrastructure/native-resolver.js +187 -0
  79. package/core/infrastructure/native-sparse-gram.js +257 -0
  80. package/core/infrastructure/native-tokenizer.js +160 -0
  81. package/core/infrastructure/onnx-mutex.js +45 -0
  82. package/core/infrastructure/onnx-session-utils.js +261 -0
  83. package/core/infrastructure/ort-pipeline.js +111 -0
  84. package/core/infrastructure/project-detector.js +102 -0
  85. package/core/infrastructure/quantization.js +410 -0
  86. package/core/infrastructure/simd-distance.js +502 -0
  87. package/core/infrastructure/simd-distance.wasm +0 -0
  88. package/core/infrastructure/tree-sitter-provider.js +665 -0
  89. package/core/infrastructure/webgpu-maxsim.js +222 -0
  90. package/core/query/index.js +35 -0
  91. package/core/query/intent-detector.js +201 -0
  92. package/core/query/intent-router.js +156 -0
  93. package/core/query/query-router-catboost.js +222 -0
  94. package/core/query/query-router-ml.js +266 -0
  95. package/core/query/query-router.js +213 -0
  96. package/core/ranking/cascaded-scorer.js +379 -0
  97. package/core/ranking/flashrank.js +810 -0
  98. package/core/ranking/index.js +49 -0
  99. package/core/ranking/late-interaction-index.js +2383 -0
  100. package/core/ranking/late-interaction-model.js +812 -0
  101. package/core/ranking/local-reranker.js +374 -0
  102. package/core/ranking/mmr.js +379 -0
  103. package/core/ranking/quality-scorer.js +363 -0
  104. package/core/search/context-expander.js +1167 -0
  105. package/core/search/dedup/sibling-expander.js +327 -0
  106. package/core/search/index.js +16 -0
  107. package/core/search/search-boost.js +259 -0
  108. package/core/search/search-cli.js +544 -0
  109. package/core/search/search-format.js +282 -0
  110. package/core/search/search-fusion.js +327 -0
  111. package/core/search/search-hybrid.js +204 -0
  112. package/core/search/search-pattern-chunks.js +337 -0
  113. package/core/search/search-pattern-planner.js +439 -0
  114. package/core/search/search-pattern-prefilter.js +412 -0
  115. package/core/search/search-pattern-ripgrep.js +663 -0
  116. package/core/search/search-pattern.js +463 -0
  117. package/core/search/search-postprocess.js +452 -0
  118. package/core/search/search-semantic.js +706 -0
  119. package/core/search/search-server.js +554 -0
  120. package/core/search/session-daemon-prewarm.mjs +164 -0
  121. package/core/search/session-warmup.js +595 -0
  122. package/core/search/sweet-search.js +632 -0
  123. package/core/search/warmup-metrics.js +532 -0
  124. package/core/start-server.js +6 -0
  125. package/core/training/query-router/features/extractor.js +762 -0
  126. package/core/training/query-router/features/multilingual-patterns.js +431 -0
  127. package/core/training/query-router/features/text-segmenter.js +303 -0
  128. package/core/training/query-router/features/unicode-utils.js +383 -0
  129. package/core/training/query-router/output/v45_router_d4.js +11521 -0
  130. package/core/training/query-router/output/v46_router_d4.js +11498 -0
  131. package/core/vector-store/binary-heap.js +227 -0
  132. package/core/vector-store/binary-hnsw-index.js +1004 -0
  133. package/core/vector-store/float-vector-store.js +234 -0
  134. package/core/vector-store/hnsw-index.js +580 -0
  135. package/core/vector-store/index.js +39 -0
  136. package/core/vector-store/seismic-index.js +498 -0
  137. package/core/vocabulary/index.js +84 -0
  138. package/core/vocabulary/vocab-constants.js +20 -0
  139. package/core/vocabulary/vocab-miner-extractors.js +375 -0
  140. package/core/vocabulary/vocab-miner-nl.js +404 -0
  141. package/core/vocabulary/vocab-miner-utils.js +146 -0
  142. package/core/vocabulary/vocab-miner.js +574 -0
  143. package/core/vocabulary/vocab-prewarm-cli.js +110 -0
  144. package/core/vocabulary/vocab-ranker.js +492 -0
  145. package/core/vocabulary/vocab-warmer.js +523 -0
  146. package/core/vocabulary/vocab-warmup-orchestrator.js +425 -0
  147. package/core/vocabulary/vocabulary-utils.js +704 -0
  148. package/crates/wasm-router/pkg/package.json +13 -0
  149. package/crates/wasm-router/pkg/query_router_wasm.d.ts +36 -0
  150. package/crates/wasm-router/pkg/query_router_wasm.js +271 -0
  151. package/crates/wasm-router/pkg/query_router_wasm_bg.wasm +0 -0
  152. package/crates/wasm-router/pkg/query_router_wasm_bg.wasm.d.ts +19 -0
  153. package/mcp/config-gen.js +121 -0
  154. package/mcp/server.js +335 -0
  155. package/mcp/tool-handlers.js +476 -0
  156. package/package.json +131 -9
  157. package/scripts/benchmark-harness.js +794 -0
  158. package/scripts/init.js +1058 -0
  159. package/scripts/smoke-test.js +435 -0
  160. package/scripts/uninstall.js +478 -0
  161. package/scripts/verify-runtime.js +176 -0
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Embedding Cache - LRU cache, vocabulary, semantic cache, query deduplication.
3
+ * Extracted from embedding-service.js for file size compliance (<500 lines).
4
+ *
5
+ * Telemetry functions live in ./embedding-telemetry.js and are barrel-re-exported
6
+ * below so that existing import sites continue to work unchanged.
7
+ */
8
+
9
+ import fs from 'fs/promises';
10
+ import { existsSync } from 'fs';
11
+ import path from 'path';
12
+ import { join } from 'path';
13
+ import { EMBEDDING_CONFIG, DB_PATHS } from '../infrastructure/config/index.js';
14
+ import { fetchModel, getModelCacheDir } from '../infrastructure/model-fetcher.js';
15
+ import { createOrtPipeline, buildFeed } from '../infrastructure/ort-pipeline.js';
16
+ import { meanPoolWithAttentionMask } from './embedding-local-model.js';
17
+
18
+ // =============================================================================
19
+ // LRU CACHE
20
+ // =============================================================================
21
+
22
+ export class LRUCache {
23
+ constructor(maxSize = 1000) {
24
+ this.maxSize = maxSize;
25
+ this.cache = new Map();
26
+ this.hitCount = new Map();
27
+ }
28
+
29
+ get(key) {
30
+ if (!this.cache.has(key)) return null;
31
+ const value = this.cache.get(key);
32
+ this.cache.delete(key);
33
+ this.cache.set(key, value);
34
+ this.hitCount.set(key, (this.hitCount.get(key) || 0) + 1);
35
+ return value;
36
+ }
37
+
38
+ set(key, value) {
39
+ if (this.cache.has(key)) this.cache.delete(key);
40
+ if (this.cache.size >= this.maxSize) {
41
+ const oldestEntry = this.cache.entries().next().value;
42
+ if (oldestEntry) this.cache.delete(oldestEntry[0]);
43
+ }
44
+ this.cache.set(key, value);
45
+ }
46
+
47
+ has(key) { return this.cache.has(key); }
48
+ getHitCount(key) { return this.hitCount.get(key) || 0; }
49
+ size() { return this.cache.size; }
50
+ clear() { this.cache.clear(); this.hitCount.clear(); }
51
+
52
+ getFrequentQueries(threshold) {
53
+ const frequent = [];
54
+ for (const [key, count] of this.hitCount) {
55
+ if (count >= threshold && this.cache.has(key)) {
56
+ frequent.push({ query: key, count, embedding: this.cache.get(key) });
57
+ }
58
+ }
59
+ return frequent.sort((a, b) => b.count - a.count);
60
+ }
61
+ }
62
+
63
+ // =============================================================================
64
+ // QUERY STATS (Cross-session usage tracking)
65
+ // =============================================================================
66
+
67
+ export class QueryStats {
68
+ constructor(statsPath) {
69
+ this.statsPath = statsPath;
70
+ this.stats = new Map();
71
+ this.loaded = false;
72
+ this.dirty = false;
73
+ }
74
+
75
+ async load() {
76
+ if (this.loaded) return;
77
+ try {
78
+ if (existsSync(this.statsPath)) {
79
+ const data = JSON.parse(await fs.readFile(this.statsPath, 'utf-8'));
80
+ for (const [query, count] of Object.entries(data.queries || {})) {
81
+ this.stats.set(query, count);
82
+ }
83
+ }
84
+ } catch (err) {
85
+ if (process.env.DEBUG_CATCHES) process.stderr.write(`[non-fatal] ${err?.message || err}\n`);
86
+ }
87
+ this.loaded = true;
88
+ }
89
+
90
+ async save() {
91
+ if (!this.dirty) return;
92
+ const data = { queries: Object.fromEntries(this.stats), lastUpdated: new Date().toISOString() };
93
+ await fs.mkdir(path.dirname(this.statsPath), { recursive: true });
94
+ await fs.writeFile(this.statsPath, JSON.stringify(data));
95
+ this.dirty = false;
96
+ }
97
+
98
+ increment(query) {
99
+ const normalized = query.toLowerCase().trim();
100
+ const count = (this.stats.get(normalized) || 0) + 1;
101
+ this.stats.set(normalized, count);
102
+ this.dirty = true;
103
+ return count;
104
+ }
105
+
106
+ getCount(query) {
107
+ return this.stats.get(query.toLowerCase().trim()) || 0;
108
+ }
109
+ }
110
+
111
+ // =============================================================================
112
+ // VOCABULARY
113
+ // =============================================================================
114
+
115
+ export class Vocabulary {
116
+ constructor(vocabPath) {
117
+ this.vocabPath = vocabPath;
118
+ this.terms = new Map();
119
+ this.metadata = { created: null, lastUpdated: null, version: 2, provider: null };
120
+ this.loaded = false;
121
+ }
122
+
123
+ async load() {
124
+ if (this.loaded) return;
125
+ try {
126
+ if (existsSync(this.vocabPath)) {
127
+ const data = JSON.parse(await fs.readFile(this.vocabPath, 'utf-8'));
128
+ if (data.metadata?.provider && data.metadata.provider !== EMBEDDING_CONFIG.provider) {
129
+ console.log(`Vocabulary: Provider changed (${data.metadata.provider} → ${EMBEDDING_CONFIG.provider}), clearing cache`);
130
+ this.terms.clear();
131
+ } else {
132
+ this.metadata = data.metadata || this.metadata;
133
+ for (const [term, embedding] of Object.entries(data.terms || {})) {
134
+ this.terms.set(term, embedding);
135
+ }
136
+ console.log(`Vocabulary: Loaded ${this.terms.size} pre-computed embeddings`);
137
+ }
138
+ }
139
+ } catch (err) {
140
+ console.log(`Vocabulary: Starting fresh (${err.message})`);
141
+ }
142
+ this.loaded = true;
143
+ }
144
+
145
+ async save() {
146
+ this.metadata.lastUpdated = new Date().toISOString();
147
+ this.metadata.provider = EMBEDDING_CONFIG.provider;
148
+ if (!this.metadata.created) this.metadata.created = this.metadata.lastUpdated;
149
+ const data = { metadata: this.metadata, terms: Object.fromEntries(this.terms) };
150
+ await fs.mkdir(path.dirname(this.vocabPath), { recursive: true });
151
+ await fs.writeFile(this.vocabPath, JSON.stringify(data, null, 2));
152
+ }
153
+
154
+ get(term) { return this.terms.get(this.normalize(term)) || null; }
155
+ set(term, embedding) { this.terms.set(this.normalize(term), embedding); }
156
+ has(term) { return this.terms.has(this.normalize(term)); }
157
+ normalize(term) { return term.toLowerCase().trim(); }
158
+ size() { return this.terms.size; }
159
+
160
+ async addDefaultTerms(embedFn) {
161
+ const defaultTerms = [
162
+ 'AuthService', 'EmployeeService', 'LoginService', 'UserService',
163
+ 'ProcessService', 'EventService', 'ScreenshotService', 'SessionService',
164
+ 'AuthController', 'EmployeeController', 'LoginController', 'UserController',
165
+ 'authentication', 'authorization', 'login', 'logout', 'password',
166
+ 'JWT', 'token', 'session', 'employee', 'monitoring', 'tracking',
167
+ 'gRPC', 'REST', 'API', 'endpoint', 'request', 'response',
168
+ 'bot detection', 'heuristic', 'trajectory', 'mouse movement',
169
+ 'Spring Boot', 'React', 'Java', 'JavaScript', 'proto', 'protobuf',
170
+ ];
171
+
172
+ let added = 0;
173
+ for (const term of defaultTerms) {
174
+ if (!this.has(term)) {
175
+ const embedding = await embedFn(term);
176
+ this.set(term, embedding);
177
+ added++;
178
+ }
179
+ }
180
+ if (added > 0) {
181
+ await this.save();
182
+ console.log(`Vocabulary: Added ${added} default terms`);
183
+ }
184
+ }
185
+ }
186
+
187
+ // =============================================================================
188
+ // SEMANTIC CACHE (Local Model for Cache Keys)
189
+ // =============================================================================
190
+
191
+ export class SemanticCache {
192
+ constructor(options = {}) {
193
+ this.threshold = options.threshold ?? 0.85;
194
+ this.maxSize = options.maxSize ?? 500;
195
+ this.entries = [];
196
+ this.localModel = null;
197
+ this.loadingModel = false;
198
+ this.loadPromise = null;
199
+ this.stats = { hits: 0, misses: 0, localModelCalls: 0 };
200
+ }
201
+
202
+ async getLocalModel() {
203
+ if (this.localModel) return this.localModel;
204
+ if (this.loadingModel && this.loadPromise) return this.loadPromise;
205
+
206
+ this.loadingModel = true;
207
+ this.loadPromise = (async () => {
208
+ const start = Date.now();
209
+ console.log('[SemanticCache] Loading local model for cache keys...');
210
+ await fetchModel('all-minilm-l6-v2');
211
+ const cacheDir = getModelCacheDir('Xenova/all-MiniLM-L6-v2');
212
+ this.localModel = await createOrtPipeline(
213
+ join(cacheDir, 'onnx', 'model_quantized.onnx'),
214
+ join(cacheDir, 'tokenizer.json'),
215
+ { graphOptimizationLevel: 'all' },
216
+ );
217
+ console.log(`[SemanticCache] Local model loaded in ${Date.now() - start}ms`);
218
+ this.loadingModel = false;
219
+ return this.localModel;
220
+ })();
221
+
222
+ return this.loadPromise;
223
+ }
224
+
225
+ async computeLocalEmbedding(text) {
226
+ const { session, tokenizer } = await this.getLocalModel();
227
+ this.stats.localModelCalls++;
228
+ const tokenized = tokenizer(text, { padding: true, truncation: true, max_length: 256 });
229
+ const feed = buildFeed(tokenized, session.inputNames);
230
+ const output = await session.run(feed);
231
+ const tensor = output[session.outputNames[0]];
232
+ // Mean pooling with L2 normalization
233
+ const pooled = meanPoolWithAttentionMask(tensor, tokenized.attention_mask, true);
234
+ return Array.from(pooled.data);
235
+ }
236
+
237
+ cosineSimilarity(a, b) {
238
+ let dot = 0, normA = 0, normB = 0;
239
+ for (let i = 0; i < a.length; i++) {
240
+ dot += a[i] * b[i];
241
+ normA += a[i] * a[i];
242
+ normB += b[i] * b[i];
243
+ }
244
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
245
+ }
246
+
247
+ async findSimilar(text) {
248
+ const localEmb = await this.computeLocalEmbedding(text);
249
+
250
+ if (this.entries.length === 0) {
251
+ this.stats.misses++;
252
+ return { voyageEmb: null, localEmb };
253
+ }
254
+
255
+ let bestMatch = null;
256
+ let bestSimilarity = -1;
257
+
258
+ for (const entry of this.entries) {
259
+ const similarity = this.cosineSimilarity(localEmb, entry.localEmb);
260
+ if (similarity > this.threshold && similarity > bestSimilarity) {
261
+ bestSimilarity = similarity;
262
+ bestMatch = entry;
263
+ }
264
+ }
265
+
266
+ if (bestMatch) {
267
+ this.stats.hits++;
268
+ return {
269
+ voyageEmb: bestMatch.voyageEmb,
270
+ similarity: bestSimilarity,
271
+ matchedQuery: bestMatch.query,
272
+ localEmb,
273
+ };
274
+ }
275
+
276
+ this.stats.misses++;
277
+ return { voyageEmb: null, localEmb };
278
+ }
279
+
280
+ add(text, localEmb, voyageEmb) {
281
+ if (this.entries.length >= this.maxSize) {
282
+ this.entries.shift();
283
+ }
284
+ this.entries.push({
285
+ query: text,
286
+ localEmb,
287
+ voyageEmb,
288
+ addedAt: Date.now(),
289
+ });
290
+ }
291
+
292
+ getStats() {
293
+ const total = this.stats.hits + this.stats.misses;
294
+ const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(1) : 0;
295
+ return {
296
+ ...this.stats,
297
+ size: this.entries.length,
298
+ hitRate: `${hitRate}%`,
299
+ threshold: this.threshold,
300
+ };
301
+ }
302
+ }
303
+
304
+ // =============================================================================
305
+ // QUERY DEDUPLICATION
306
+ // =============================================================================
307
+
308
+ export class QueryDeduplicator {
309
+ constructor() {
310
+ this.inflight = new Map();
311
+ }
312
+
313
+ get(text) {
314
+ return this.inflight.get(text) || null;
315
+ }
316
+
317
+ set(text, promise) {
318
+ this.inflight.set(text, promise);
319
+ promise.finally(() => this.inflight.delete(text));
320
+ }
321
+
322
+ has(text) {
323
+ return this.inflight.has(text);
324
+ }
325
+ }
326
+
327
+ // =============================================================================
328
+ // SINGLETONS
329
+ // =============================================================================
330
+
331
+ export const queryCache = new LRUCache(EMBEDDING_CONFIG.cache?.maxSize || 1000);
332
+ export const vocabulary = new Vocabulary(DB_PATHS.vocabulary);
333
+ export const semanticCache = new SemanticCache({ threshold: 0.85, maxSize: 500 });
334
+ export const queryDeduplicator = new QueryDeduplicator();
335
+ export const queryStats = new QueryStats(DB_PATHS.vocabulary.replace('.json', '-stats.json'));
336
+ /** Mutable cache counters — access via getCacheStatsRef() to make singleton dependency explicit. */
337
+ const _cacheStats = { hits: 0, misses: 0, vocabularyHits: 0, apiCalls: 0 };
338
+
339
+ /**
340
+ * Returns a reference to the mutable stats object.
341
+ * Prefer this getter over a bare export so that the singleton
342
+ * dependency is explicit and mockable in tests.
343
+ */
344
+ export function getCacheStatsRef() {
345
+ return _cacheStats;
346
+ }
347
+
348
+ /** @deprecated Use getCacheStatsRef(). Kept for backward compatibility during migration. */
349
+ export const cacheStats = _cacheStats;
350
+ // Integration point: search modes (lexical/semantic/hybrid) should call
351
+ // recordQueryTelemetry(mode, hit, latencyMs) after each query to feed
352
+ // per-mode cache-hit telemetry for the vocabulary prewarm pipeline.
353
+
354
+ // =============================================================================
355
+ // TELEMETRY (barrel re-exports from ./embedding-telemetry.js)
356
+ // =============================================================================
357
+
358
+ export { telemetryStats, recordQueryTelemetry, getTelemetryReport, resetTelemetryStats, flushTelemetry } from './embedding-telemetry.js';
359
+
360
+ // =============================================================================
361
+ // CACHE MANAGEMENT
362
+ // =============================================================================
363
+
364
+ export function getCacheStats(circuitBreakerState) {
365
+ const total = cacheStats.hits + cacheStats.misses;
366
+ const hitRate = total > 0 ? ((cacheStats.hits + cacheStats.vocabularyHits) / total * 100).toFixed(1) : 0;
367
+ return {
368
+ ...cacheStats,
369
+ total,
370
+ hitRate: `${hitRate}%`,
371
+ cacheSize: queryCache.size(),
372
+ vocabularySize: vocabulary.size(),
373
+ semanticCache: semanticCache.getStats(),
374
+ circuitBreaker: circuitBreakerState || null,
375
+ provider: EMBEDDING_CONFIG.provider,
376
+ model: EMBEDDING_CONFIG.model,
377
+ };
378
+ }
379
+
380
+ export function getSemanticCacheStats() {
381
+ return semanticCache.getStats();
382
+ }
383
+
384
+ export function clearCache() {
385
+ queryCache.clear();
386
+ cacheStats.hits = 0;
387
+ cacheStats.misses = 0;
388
+ cacheStats.vocabularyHits = 0;
389
+ cacheStats.apiCalls = 0;
390
+ }
391
+
392
+ export function getFrequentQueries(threshold = 3) {
393
+ return queryCache.getFrequentQueries(threshold);
394
+ }
395
+
396
+ /**
397
+ * @param {string} term
398
+ * @param {Function} embedFn - Injected embed function (avoids circular dep with facade)
399
+ */
400
+ export async function addToVocabulary(term, embedFn) {
401
+ await vocabulary.load();
402
+ if (!vocabulary.has(term)) {
403
+ const embedding = await embedFn(term);
404
+ vocabulary.set(term, embedding);
405
+ await vocabulary.save();
406
+ return true;
407
+ }
408
+ return false;
409
+ }
410
+
411
+ /**
412
+ * @param {string[]} terms
413
+ * @param {Function} embedFn - Injected embed function (avoids circular dep with facade)
414
+ */
415
+ export async function expandVocabulary(terms, embedFn) {
416
+ await vocabulary.load();
417
+ let added = 0;
418
+ for (const term of terms) {
419
+ if (!vocabulary.has(term)) {
420
+ const embedding = await embedFn(term);
421
+ vocabulary.set(term, embedding);
422
+ added++;
423
+ }
424
+ }
425
+ if (added > 0) await vocabulary.save();
426
+ return added;
427
+ }
428
+
429
+ export async function autoPersistFrequentQueries(threshold = 2) {
430
+ const frequent = queryCache.getFrequentQueries(threshold);
431
+ if (frequent.length === 0) return { persisted: 0, total: 0 };
432
+
433
+ await vocabulary.load();
434
+ let persisted = 0;
435
+
436
+ for (const { query, embedding } of frequent) {
437
+ if (!vocabulary.has(query)) {
438
+ vocabulary.set(query, embedding);
439
+ persisted++;
440
+ }
441
+ }
442
+
443
+ if (persisted > 0) {
444
+ await vocabulary.save();
445
+ console.log(`Auto-persist: Saved ${persisted} frequent queries to vocabulary`);
446
+ }
447
+
448
+ return { persisted, total: frequent.length };
449
+ }
450
+
451
+ let exitHandlerRegistered = false;
452
+ export function registerAutoPersistOnExit(threshold = 2) {
453
+ if (exitHandlerRegistered) return;
454
+ exitHandlerRegistered = true;
455
+
456
+ const persist = async () => {
457
+ try {
458
+ await autoPersistFrequentQueries(threshold);
459
+ } catch (err) {
460
+ // Ignore errors on exit
461
+ }
462
+ };
463
+
464
+ process.on('beforeExit', persist);
465
+ process.on('SIGINT', async () => { await persist(); process.exit(0); });
466
+ process.on('SIGTERM', async () => { await persist(); process.exit(0); });
467
+ }