@softerist/heuristic-mcp 2.1.47 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.agent/workflows/code-review.md +60 -0
  2. package/.prettierrc +7 -0
  3. package/ARCHITECTURE.md +105 -170
  4. package/CONTRIBUTING.md +32 -113
  5. package/GEMINI.md +73 -0
  6. package/LICENSE +21 -21
  7. package/README.md +161 -54
  8. package/config.json +876 -75
  9. package/debug-pids.js +27 -0
  10. package/eslint.config.js +36 -0
  11. package/features/ann-config.js +37 -26
  12. package/features/clear-cache.js +28 -19
  13. package/features/find-similar-code.js +142 -66
  14. package/features/hybrid-search.js +253 -93
  15. package/features/index-codebase.js +1455 -394
  16. package/features/lifecycle.js +813 -180
  17. package/features/register.js +58 -52
  18. package/index.js +450 -306
  19. package/lib/cache-ops.js +22 -0
  20. package/lib/cache-utils.js +68 -0
  21. package/lib/cache.js +1392 -587
  22. package/lib/call-graph.js +165 -50
  23. package/lib/cli.js +154 -0
  24. package/lib/config.js +462 -121
  25. package/lib/embedding-process.js +77 -0
  26. package/lib/embedding-worker.js +545 -30
  27. package/lib/ignore-patterns.js +61 -59
  28. package/lib/json-worker.js +14 -0
  29. package/lib/json-writer.js +344 -0
  30. package/lib/logging.js +88 -0
  31. package/lib/memory-logger.js +13 -0
  32. package/lib/project-detector.js +13 -17
  33. package/lib/server-lifecycle.js +38 -0
  34. package/lib/settings-editor.js +645 -0
  35. package/lib/tokenizer.js +207 -104
  36. package/lib/utils.js +273 -198
  37. package/lib/vector-store-binary.js +592 -0
  38. package/mcp_config.example.json +13 -0
  39. package/package.json +13 -2
  40. package/scripts/clear-cache.js +6 -17
  41. package/scripts/download-model.js +14 -9
  42. package/scripts/postinstall.js +5 -5
  43. package/search-configs.js +36 -0
  44. package/test/ann-config.test.js +179 -0
  45. package/test/ann-fallback.test.js +6 -6
  46. package/test/binary-store.test.js +69 -0
  47. package/test/cache-branches.test.js +120 -0
  48. package/test/cache-errors.test.js +264 -0
  49. package/test/cache-extra.test.js +300 -0
  50. package/test/cache-helpers.test.js +205 -0
  51. package/test/cache-hnsw-failure.test.js +40 -0
  52. package/test/cache-json-worker.test.js +190 -0
  53. package/test/cache-worker.test.js +102 -0
  54. package/test/cache.test.js +443 -0
  55. package/test/call-graph.test.js +103 -4
  56. package/test/clear-cache.test.js +69 -68
  57. package/test/code-review-workflow.test.js +50 -0
  58. package/test/config.test.js +418 -0
  59. package/test/coverage-gap.test.js +497 -0
  60. package/test/coverage-maximizer.test.js +236 -0
  61. package/test/debug-analysis.js +107 -0
  62. package/test/embedding-model.test.js +173 -103
  63. package/test/embedding-worker-extra.test.js +272 -0
  64. package/test/embedding-worker.test.js +158 -0
  65. package/test/features.test.js +139 -0
  66. package/test/final-boost.test.js +271 -0
  67. package/test/final-polish.test.js +183 -0
  68. package/test/final.test.js +95 -0
  69. package/test/find-similar-code.test.js +191 -0
  70. package/test/helpers.js +92 -11
  71. package/test/helpers.test.js +46 -0
  72. package/test/hybrid-search-basic.test.js +62 -0
  73. package/test/hybrid-search-branch.test.js +202 -0
  74. package/test/hybrid-search-callgraph.test.js +229 -0
  75. package/test/hybrid-search-extra.test.js +81 -0
  76. package/test/hybrid-search.test.js +484 -71
  77. package/test/index-cli.test.js +520 -0
  78. package/test/index-codebase-batch.test.js +119 -0
  79. package/test/index-codebase-branches.test.js +585 -0
  80. package/test/index-codebase-core.test.js +1032 -0
  81. package/test/index-codebase-edge-cases.test.js +254 -0
  82. package/test/index-codebase-errors.test.js +132 -0
  83. package/test/index-codebase-gap.test.js +239 -0
  84. package/test/index-codebase-lines.test.js +151 -0
  85. package/test/index-codebase-watcher.test.js +259 -0
  86. package/test/index-codebase-zone.test.js +259 -0
  87. package/test/index-codebase.test.js +371 -69
  88. package/test/index-memory.test.js +220 -0
  89. package/test/indexer-detailed.test.js +176 -0
  90. package/test/integration.test.js +148 -92
  91. package/test/json-worker.test.js +50 -0
  92. package/test/lifecycle.test.js +541 -0
  93. package/test/master.test.js +198 -0
  94. package/test/perfection.test.js +349 -0
  95. package/test/project-detector.test.js +65 -0
  96. package/test/register.test.js +262 -0
  97. package/test/tokenizer.test.js +55 -93
  98. package/test/ultra-maximizer.test.js +116 -0
  99. package/test/utils-branches.test.js +161 -0
  100. package/test/utils-extra.test.js +116 -0
  101. package/test/utils.test.js +131 -0
  102. package/test/verify_fixes.js +76 -0
  103. package/test/worker-errors.test.js +96 -0
  104. package/test/worker-init.test.js +102 -0
  105. package/test/worker_throttling.test.js +93 -0
  106. package/tools/scripts/benchmark-search.js +95 -0
  107. package/tools/scripts/cache-stats.js +71 -0
  108. package/tools/scripts/manual-search.js +34 -0
  109. package/vitest.config.js +19 -9
@@ -1,6 +1,5 @@
1
-
2
1
  import path from 'path';
3
- import os from 'os';
2
+
4
3
  import { getGlobalCacheDir } from '../lib/config.js';
5
4
 
6
5
  async function downloadModel() {
@@ -13,23 +12,29 @@ async function downloadModel() {
13
12
  // Force cache directory to global location
14
13
  env.cacheDir = globalCacheDir;
15
14
 
16
- console.log(`[Model Setup] Pre-caching model to: ${globalCacheDir}`);
15
+ console.info(`[Model Setup] Pre-caching model to: ${globalCacheDir}`);
17
16
  // Check if network is available by pinging HF (simple check)
18
17
  // Actually, pipeline() will fail fast if network is down
19
- console.log(`[Model Setup] Downloading 'Xenova/all-MiniLM-L6-v2'...`);
18
+ console.info(`[Model Setup] Downloading 'jinaai/jina-embeddings-v2-base-code'...`);
20
19
 
21
20
  // This will download the model to the cache directory
22
- await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
21
+ await pipeline('feature-extraction', 'jinaai/jina-embeddings-v2-base-code');
23
22
 
24
- console.log(`[Model Setup] ✅ Model cached successfully!`);
23
+ console.info(`[Model Setup] ✅ Model cached successfully!`);
25
24
  } catch (error) {
26
25
  if (error && error.code === 'ERR_MODULE_NOT_FOUND') {
27
- console.warn('[Model Setup] ⚠️ Transformers not available yet; skipping model pre-download.');
28
- console.warn('[Model Setup] This is okay! The server will attempt to download it when started.');
26
+ console.warn(
27
+ '[Model Setup] ⚠️ Transformers not available yet; skipping model pre-download.'
28
+ );
29
+ console.warn(
30
+ '[Model Setup] This is okay! The server will attempt to download it when started.'
31
+ );
29
32
  return;
30
33
  }
31
34
  console.warn(`[Model Setup] ⚠️ Constructive warning: Failed to pre-download model.`);
32
- console.warn('[Model Setup] This is okay! The server will attempt to download it when started.');
35
+ console.warn(
36
+ '[Model Setup] This is okay! The server will attempt to download it when started.'
37
+ );
33
38
  console.warn(`[Model Setup] Error details: ${error.message}`);
34
39
  // Don't fail the install, just warn
35
40
  }
@@ -1,12 +1,12 @@
1
1
  import { register } from '../features/register.js';
2
2
 
3
3
  // Run the registration process - MUST await to ensure file writes complete
4
- console.log('[PostInstall] Running Heuristic MCP registration...');
4
+ console.info('[PostInstall] Running Heuristic MCP registration...');
5
5
 
6
6
  try {
7
- await register();
8
- console.log('[PostInstall] Registration complete.');
7
+ await register();
8
+ console.info('[PostInstall] Registration complete.');
9
9
  } catch (err) {
10
- console.error('[PostInstall] Registration failed:', err.message);
11
- // Don't fail the install if registration fails, just warn
10
+ console.error('[PostInstall] Registration failed:', err.message);
11
+ // Don't fail the install if registration fails, just warn
12
12
  }
@@ -0,0 +1,36 @@
1
+
2
+ import { loadConfig } from './lib/config.js';
3
+ import { EmbeddingsCache } from './lib/cache.js';
4
+ import { HybridSearch } from './features/hybrid-search.js';
5
+ import { pipeline, env } from '@xenova/transformers';
6
+
7
+ // Force same thread config as server
8
+ if (env?.backends?.onnx) {
9
+ env.backends.onnx.numThreads = 2;
10
+ if (env.backends.onnx.wasm) {
11
+ env.backends.onnx.wasm.numThreads = 2;
12
+ }
13
+ }
14
+
15
+ async function searchConfigs() {
16
+ const config = await loadConfig(process.cwd());
17
+ const cache = new EmbeddingsCache(config);
18
+ await cache.load();
19
+
20
+ const embedder = async (text) => {
21
+ const pipe = await pipeline('feature-extraction', config.embeddingModel, {
22
+ session_options: { numThreads: 2 }
23
+ });
24
+ return pipe(text, { pooling: 'mean', normalize: true });
25
+ };
26
+
27
+ const searcher = new HybridSearch(embedder, cache, config);
28
+ const { results } = await searcher.search('configuration files, config, settings');
29
+
30
+ console.info(JSON.stringify(results, null, 2));
31
+ }
32
+
33
+ searchConfigs().catch(err => {
34
+ console.error(err);
35
+ process.exit(1);
36
+ });
@@ -0,0 +1,179 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { AnnConfigTool, getToolDefinition, handleToolCall } from '../features/ann-config.js';
3
+
4
+ describe('AnnConfigTool', () => {
5
+ let cache;
6
+ let config;
7
+
8
+ beforeEach(() => {
9
+ cache = {
10
+ getAnnStats: vi.fn(),
11
+ setEfSearch: vi.fn(),
12
+ invalidateAnnIndex: vi.fn(),
13
+ ensureAnnIndex: vi.fn(),
14
+ };
15
+ config = {};
16
+ });
17
+
18
+ it('returns stats by default', async () => {
19
+ const stats = {
20
+ enabled: true,
21
+ indexLoaded: false,
22
+ dirty: false,
23
+ vectorCount: 0,
24
+ minChunksForAnn: 10,
25
+ };
26
+ cache.getAnnStats.mockReturnValue(stats);
27
+ const tool = new AnnConfigTool(cache, config);
28
+
29
+ const result = await tool.execute({});
30
+
31
+ expect(result).toEqual(stats);
32
+ expect(cache.getAnnStats).toHaveBeenCalled();
33
+ });
34
+
35
+ it('validates set_ef_search arguments', async () => {
36
+ const tool = new AnnConfigTool(cache, config);
37
+
38
+ const result = await tool.execute({ action: 'set_ef_search' });
39
+
40
+ expect(result).toEqual({
41
+ success: false,
42
+ error: 'efSearch parameter is required for set_ef_search action',
43
+ });
44
+ });
45
+
46
+ it('sets efSearch when provided', async () => {
47
+ cache.setEfSearch.mockResolvedValue({ success: true });
48
+ const tool = new AnnConfigTool(cache, config);
49
+
50
+ const result = await tool.execute({
51
+ action: 'set_ef_search',
52
+ efSearch: 64,
53
+ });
54
+
55
+ expect(cache.setEfSearch).toHaveBeenCalledWith(64);
56
+ expect(result).toEqual({ success: true });
57
+ });
58
+
59
+ it('rebuilds ANN index and reports success', async () => {
60
+ cache.ensureAnnIndex.mockResolvedValue({ ok: true });
61
+ const tool = new AnnConfigTool(cache, config);
62
+
63
+ const result = await tool.execute({ action: 'rebuild' });
64
+
65
+ expect(cache.invalidateAnnIndex).toHaveBeenCalled();
66
+ expect(cache.ensureAnnIndex).toHaveBeenCalled();
67
+ expect(result).toEqual({
68
+ success: true,
69
+ message: 'ANN index rebuilt successfully',
70
+ });
71
+ });
72
+
73
+ it('reports rebuild failure when index is unavailable', async () => {
74
+ cache.ensureAnnIndex.mockResolvedValue(null);
75
+ const tool = new AnnConfigTool(cache, config);
76
+
77
+ const result = await tool.execute({ action: 'rebuild' });
78
+
79
+ expect(result).toEqual({
80
+ success: false,
81
+ message: 'ANN index rebuild failed or not available',
82
+ });
83
+ });
84
+
85
+ it('handles unknown actions', async () => {
86
+ const tool = new AnnConfigTool(cache, config);
87
+
88
+ const result = await tool.execute({ action: 'mystery' });
89
+
90
+ expect(result).toEqual({
91
+ success: false,
92
+ error: 'Unknown action: mystery. Valid actions: stats, set_ef_search, rebuild',
93
+ });
94
+ });
95
+
96
+ it('formats error results', () => {
97
+ const tool = new AnnConfigTool(cache, config);
98
+
99
+ const formatted = tool.formatResults({ success: false, error: 'boom' });
100
+
101
+ expect(formatted).toBe('Error: boom');
102
+ });
103
+
104
+ it('formats stats results with config', () => {
105
+ const tool = new AnnConfigTool(cache, config);
106
+ const formatted = tool.formatResults({
107
+ enabled: true,
108
+ indexLoaded: true,
109
+ dirty: false,
110
+ vectorCount: 2,
111
+ minChunksForAnn: 3,
112
+ config: {
113
+ metric: 'l2',
114
+ dim: 2,
115
+ count: 2,
116
+ m: 16,
117
+ efConstruction: 100,
118
+ efSearch: 64,
119
+ },
120
+ });
121
+
122
+ expect(formatted).toContain('ANN Index Statistics');
123
+ expect(formatted).toContain('Current Config');
124
+ expect(formatted).toContain('efSearch');
125
+ });
126
+
127
+ it('formats stats results without active config', () => {
128
+ const tool = new AnnConfigTool(cache, config);
129
+ const formatted = tool.formatResults({
130
+ enabled: true,
131
+ indexLoaded: false,
132
+ dirty: true,
133
+ vectorCount: 0,
134
+ minChunksForAnn: 1,
135
+ config: null,
136
+ });
137
+
138
+ expect(formatted).toContain('No active ANN index');
139
+ });
140
+
141
+ it('formats generic results as JSON', () => {
142
+ const tool = new AnnConfigTool(cache, config);
143
+ const formatted = tool.formatResults({ success: true, message: 'ok' });
144
+
145
+ expect(formatted).toBe(JSON.stringify({ success: true, message: 'ok' }, null, 2));
146
+ });
147
+
148
+ it('handles tool calls end-to-end', async () => {
149
+ cache.getAnnStats.mockReturnValue({
150
+ enabled: false,
151
+ indexLoaded: false,
152
+ dirty: false,
153
+ vectorCount: 0,
154
+ minChunksForAnn: 1,
155
+ });
156
+ const tool = new AnnConfigTool(cache, config);
157
+ const request = { params: { arguments: {} } };
158
+
159
+ const response = await handleToolCall(request, tool);
160
+
161
+ expect(response.content[0].text).toContain('ANN Index Statistics');
162
+ });
163
+
164
+ it('defaults missing tool arguments', async () => {
165
+ cache.getAnnStats.mockReturnValue({
166
+ enabled: true,
167
+ indexLoaded: false,
168
+ dirty: false,
169
+ vectorCount: 0,
170
+ minChunksForAnn: 1,
171
+ });
172
+ const tool = new AnnConfigTool(cache, config);
173
+ const request = { params: {} };
174
+
175
+ const response = await handleToolCall(request, tool);
176
+
177
+ expect(response.content[0].text).toContain('ANN Index Statistics');
178
+ });
179
+ });
@@ -18,12 +18,12 @@ describe('ANN Fallback (Missing hnswlib-node)', () => {
18
18
  annEnabled: true,
19
19
  annMinChunks: 5, // Low threshold for testing
20
20
  annIndexCache: false,
21
- embeddingModel: 'test-model'
21
+ embeddingModel: 'test-model',
22
22
  };
23
23
 
24
24
  // Mock embedder
25
25
  embedder = vi.fn().mockResolvedValue({
26
- data: new Float32Array([0.1, 0.2, 0.3])
26
+ data: new Float32Array([0.1, 0.2, 0.3]),
27
27
  });
28
28
 
29
29
  cache = new EmbeddingsCache(config);
@@ -36,7 +36,7 @@ describe('ANN Fallback (Missing hnswlib-node)', () => {
36
36
  content: `content ${i}`,
37
37
  startLine: 1,
38
38
  endLine: 5,
39
- vector: [0.1, 0.2, 0.3] // simple dummy vector
39
+ vector: [0.1, 0.2, 0.3], // simple dummy vector
40
40
  });
41
41
  }
42
42
  cache.setVectorStore(vectors);
@@ -45,7 +45,7 @@ describe('ANN Fallback (Missing hnswlib-node)', () => {
45
45
  });
46
46
 
47
47
  it('should fall back to linear search when ANN index is unavailable', async () => {
48
- const query = "test query";
48
+ const query = 'test query';
49
49
  const maxResults = 5;
50
50
 
51
51
  const result = await hybridSearch.search(query, maxResults);
@@ -55,7 +55,7 @@ describe('ANN Fallback (Missing hnswlib-node)', () => {
55
55
  expect(embedder).toHaveBeenCalledWith(query, expect.any(Object));
56
56
  // Verify it didn't throw and ANN attempt doesn't prevent results
57
57
  const annAttempt = await cache.queryAnn([0.1, 0.2, 0.3], 5);
58
- expect(annAttempt).toBeNull();
58
+ expect(annAttempt).toEqual([]);
59
59
  });
60
60
 
61
61
  it('should handle ANN loading failure gracefully', async () => {
@@ -63,6 +63,6 @@ describe('ANN Fallback (Missing hnswlib-node)', () => {
63
63
  expect(index).toBeNull();
64
64
 
65
65
  const annResults = await cache.queryAnn([0.1, 0.2, 0.3], 5);
66
- expect(annResults).toBeNull();
66
+ expect(annResults).toEqual([]);
67
67
  });
68
68
  });
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { BinaryVectorStore } from '../lib/vector-store-binary.js';
6
+
7
+ async function withTempDir(testFn) {
8
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'heuristic-binary-'));
9
+ try {
10
+ await testFn(dir);
11
+ } finally {
12
+ await fs.rm(dir, { recursive: true, force: true });
13
+ }
14
+ }
15
+
16
+ describe('BinaryVectorStore smoke', () => {
17
+ it('writes and loads a larger store', async () => {
18
+ await withTempDir(async (dir) => {
19
+ const count = 512;
20
+ const chunks = new Array(count).fill(null).map((_, i) => ({
21
+ file: path.join(dir, `file-${i % 8}.js`),
22
+ startLine: i + 1,
23
+ endLine: i + 2,
24
+ content: `line-${i}`,
25
+ vector: new Float32Array([i / 100, i / 200, i / 300]),
26
+ }));
27
+
28
+ const store = await BinaryVectorStore.write(dir, chunks, { contentCacheEntries: 4 });
29
+ expect(store.length).toBe(count);
30
+
31
+ const loaded = await BinaryVectorStore.load(dir, { contentCacheEntries: 4 });
32
+ expect(loaded.length).toBe(count);
33
+ expect(loaded.dim).toBe(3);
34
+ expect(await loaded.getContent(0)).toContain('line-0');
35
+
36
+ await store.close();
37
+ await loaded.close();
38
+ });
39
+ });
40
+
41
+ it('supports disk-backed vector reads', async () => {
42
+ await withTempDir(async (dir) => {
43
+ const chunks = [
44
+ {
45
+ file: path.join(dir, 'file-a.js'),
46
+ startLine: 1,
47
+ endLine: 2,
48
+ content: 'const x = 1;',
49
+ vector: new Float32Array([0.1, 0.2, 0.3]),
50
+ },
51
+ ];
52
+
53
+ const store = await BinaryVectorStore.write(dir, chunks, { contentCacheEntries: 2 });
54
+ await store.close();
55
+
56
+ const loaded = await BinaryVectorStore.load(dir, {
57
+ contentCacheEntries: 2,
58
+ vectorCacheEntries: 1,
59
+ vectorLoadMode: 'disk',
60
+ });
61
+
62
+ const vector = loaded.getVector(0);
63
+ expect(vector).toBeInstanceOf(Float32Array);
64
+ expect(vector.length).toBe(3);
65
+
66
+ await loaded.close();
67
+ });
68
+ });
69
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const makeConfig = (cacheDir) => ({
7
+ cacheDirectory: cacheDir,
8
+ searchDirectory: cacheDir,
9
+ enableCache: true,
10
+ callGraphEnabled: false,
11
+ embeddingModel: 'test-model',
12
+ fileExtensions: ['js'],
13
+ excludePatterns: [],
14
+ annEnabled: true,
15
+ annMinChunks: 1,
16
+ annMetric: 'cosine',
17
+ annM: 48,
18
+ annEfConstruction: 200,
19
+ annEfSearch: 10,
20
+ annIndexCache: true,
21
+ verbose: true,
22
+ });
23
+
24
+ async function withTempDir(testFn) {
25
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'heuristic-cache-branches-'));
26
+ try {
27
+ await testFn(dir);
28
+ } finally {
29
+ await fs.rm(dir, { recursive: true, force: true });
30
+ }
31
+ }
32
+
33
+ describe('EmbeddingsCache branch coverage', () => {
34
+ let consoleSpy;
35
+
36
+ beforeEach(() => {
37
+ consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
38
+ vi.resetModules();
39
+ });
40
+
41
+ afterEach(() => {
42
+ consoleSpy.mockRestore();
43
+ vi.clearAllMocks();
44
+ vi.resetModules();
45
+ });
46
+
47
+ it('logs when HierarchicalNSW export is missing', async () => {
48
+ await withTempDir(async (dir) => {
49
+ vi.doMock('hnswlib-node', () => ({ default: {} }));
50
+ const { EmbeddingsCache } = await import('../lib/cache.js');
51
+ const cache = new EmbeddingsCache(makeConfig(dir));
52
+ cache.vectorStore = [{ vector: [1, 2, 3] }];
53
+
54
+ const result = await cache.ensureAnnIndex();
55
+
56
+ expect(result).toBeNull();
57
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('hnswlib-node unavailable'));
58
+ });
59
+ });
60
+
61
+ it('returns null for empty ANN labels and preserves Float32Array input', async () => {
62
+ await withTempDir(async (dir) => {
63
+ const { EmbeddingsCache } = await import('../lib/cache.js');
64
+ const cache = new EmbeddingsCache(makeConfig(dir));
65
+ cache.vectorStore = [{ vector: [1, 2, 3] }];
66
+ const searchKnn = vi.fn().mockReturnValue({});
67
+ cache.annIndex = { searchKnn };
68
+ cache.annDirty = false;
69
+
70
+ const query = Float32Array.from([0.1, 0.2]);
71
+ const result = await cache.queryAnn(query, 2);
72
+
73
+ expect(result).toEqual([]);
74
+ expect(searchKnn).toHaveBeenCalledWith(query, 2);
75
+ });
76
+ });
77
+
78
+ it('logs embedding model and size mismatches when loading ANN metadata', async () => {
79
+ await withTempDir(async (dir) => {
80
+ const { EmbeddingsCache } = await import('../lib/cache.js');
81
+ const cache = new EmbeddingsCache(makeConfig(dir));
82
+ cache.vectorStore = [{ vector: [1, 2, 3] }];
83
+
84
+ const annMetaFile = path.join(dir, 'ann-meta.json');
85
+
86
+ await fs.writeFile(
87
+ annMetaFile,
88
+ JSON.stringify({
89
+ version: 1,
90
+ embeddingModel: 'other-model',
91
+ dim: 3,
92
+ count: 1,
93
+ metric: 'cosine',
94
+ m: 48,
95
+ efConstruction: 200,
96
+ })
97
+ );
98
+ await cache.loadAnnIndexFromDisk(class {}, 3);
99
+
100
+ await fs.writeFile(
101
+ annMetaFile,
102
+ JSON.stringify({
103
+ version: 1,
104
+ embeddingModel: 'test-model',
105
+ dim: 2,
106
+ count: 5,
107
+ metric: 'cosine',
108
+ m: 48,
109
+ efConstruction: 200,
110
+ })
111
+ );
112
+ await cache.loadAnnIndexFromDisk(class {}, 3);
113
+
114
+ expect(consoleSpy).toHaveBeenCalledWith(
115
+ expect.stringContaining('Embedding model changed for ANN index')
116
+ );
117
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('ANN index size mismatch'));
118
+ });
119
+ });
120
+ });