@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.
- package/.agent/workflows/code-review.md +60 -0
- package/.prettierrc +7 -0
- package/ARCHITECTURE.md +105 -170
- package/CONTRIBUTING.md +32 -113
- package/GEMINI.md +73 -0
- package/LICENSE +21 -21
- package/README.md +161 -54
- package/config.json +876 -75
- package/debug-pids.js +27 -0
- package/eslint.config.js +36 -0
- package/features/ann-config.js +37 -26
- package/features/clear-cache.js +28 -19
- package/features/find-similar-code.js +142 -66
- package/features/hybrid-search.js +253 -93
- package/features/index-codebase.js +1455 -394
- package/features/lifecycle.js +813 -180
- package/features/register.js +58 -52
- package/index.js +450 -306
- package/lib/cache-ops.js +22 -0
- package/lib/cache-utils.js +68 -0
- package/lib/cache.js +1392 -587
- package/lib/call-graph.js +165 -50
- package/lib/cli.js +154 -0
- package/lib/config.js +462 -121
- package/lib/embedding-process.js +77 -0
- package/lib/embedding-worker.js +545 -30
- package/lib/ignore-patterns.js +61 -59
- package/lib/json-worker.js +14 -0
- package/lib/json-writer.js +344 -0
- package/lib/logging.js +88 -0
- package/lib/memory-logger.js +13 -0
- package/lib/project-detector.js +13 -17
- package/lib/server-lifecycle.js +38 -0
- package/lib/settings-editor.js +645 -0
- package/lib/tokenizer.js +207 -104
- package/lib/utils.js +273 -198
- package/lib/vector-store-binary.js +592 -0
- package/mcp_config.example.json +13 -0
- package/package.json +13 -2
- package/scripts/clear-cache.js +6 -17
- package/scripts/download-model.js +14 -9
- package/scripts/postinstall.js +5 -5
- package/search-configs.js +36 -0
- package/test/ann-config.test.js +179 -0
- package/test/ann-fallback.test.js +6 -6
- package/test/binary-store.test.js +69 -0
- package/test/cache-branches.test.js +120 -0
- package/test/cache-errors.test.js +264 -0
- package/test/cache-extra.test.js +300 -0
- package/test/cache-helpers.test.js +205 -0
- package/test/cache-hnsw-failure.test.js +40 -0
- package/test/cache-json-worker.test.js +190 -0
- package/test/cache-worker.test.js +102 -0
- package/test/cache.test.js +443 -0
- package/test/call-graph.test.js +103 -4
- package/test/clear-cache.test.js +69 -68
- package/test/code-review-workflow.test.js +50 -0
- package/test/config.test.js +418 -0
- package/test/coverage-gap.test.js +497 -0
- package/test/coverage-maximizer.test.js +236 -0
- package/test/debug-analysis.js +107 -0
- package/test/embedding-model.test.js +173 -103
- package/test/embedding-worker-extra.test.js +272 -0
- package/test/embedding-worker.test.js +158 -0
- package/test/features.test.js +139 -0
- package/test/final-boost.test.js +271 -0
- package/test/final-polish.test.js +183 -0
- package/test/final.test.js +95 -0
- package/test/find-similar-code.test.js +191 -0
- package/test/helpers.js +92 -11
- package/test/helpers.test.js +46 -0
- package/test/hybrid-search-basic.test.js +62 -0
- package/test/hybrid-search-branch.test.js +202 -0
- package/test/hybrid-search-callgraph.test.js +229 -0
- package/test/hybrid-search-extra.test.js +81 -0
- package/test/hybrid-search.test.js +484 -71
- package/test/index-cli.test.js +520 -0
- package/test/index-codebase-batch.test.js +119 -0
- package/test/index-codebase-branches.test.js +585 -0
- package/test/index-codebase-core.test.js +1032 -0
- package/test/index-codebase-edge-cases.test.js +254 -0
- package/test/index-codebase-errors.test.js +132 -0
- package/test/index-codebase-gap.test.js +239 -0
- package/test/index-codebase-lines.test.js +151 -0
- package/test/index-codebase-watcher.test.js +259 -0
- package/test/index-codebase-zone.test.js +259 -0
- package/test/index-codebase.test.js +371 -69
- package/test/index-memory.test.js +220 -0
- package/test/indexer-detailed.test.js +176 -0
- package/test/integration.test.js +148 -92
- package/test/json-worker.test.js +50 -0
- package/test/lifecycle.test.js +541 -0
- package/test/master.test.js +198 -0
- package/test/perfection.test.js +349 -0
- package/test/project-detector.test.js +65 -0
- package/test/register.test.js +262 -0
- package/test/tokenizer.test.js +55 -93
- package/test/ultra-maximizer.test.js +116 -0
- package/test/utils-branches.test.js +161 -0
- package/test/utils-extra.test.js +116 -0
- package/test/utils.test.js +131 -0
- package/test/verify_fixes.js +76 -0
- package/test/worker-errors.test.js +96 -0
- package/test/worker-init.test.js +102 -0
- package/test/worker_throttling.test.js +93 -0
- package/tools/scripts/benchmark-search.js +95 -0
- package/tools/scripts/cache-stats.js +71 -0
- package/tools/scripts/manual-search.js +34 -0
- package/vitest.config.js +19 -9
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
1
|
import path from 'path';
|
|
3
|
-
|
|
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.
|
|
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.
|
|
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', '
|
|
21
|
+
await pipeline('feature-extraction', 'jinaai/jina-embeddings-v2-base-code');
|
|
23
22
|
|
|
24
|
-
console.
|
|
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(
|
|
28
|
-
|
|
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(
|
|
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
|
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -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.
|
|
4
|
+
console.info('[PostInstall] Running Heuristic MCP registration...');
|
|
5
5
|
|
|
6
6
|
try {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
await register();
|
|
8
|
+
console.info('[PostInstall] Registration complete.');
|
|
9
9
|
} catch (err) {
|
|
10
|
-
|
|
11
|
-
|
|
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 =
|
|
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).
|
|
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).
|
|
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
|
+
});
|