@renseiai/agentfactory-code-intelligence 0.8.8 → 0.8.10

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 (66) hide show
  1. package/dist/src/embedding/__tests__/embedding.test.d.ts +2 -0
  2. package/dist/src/embedding/__tests__/embedding.test.d.ts.map +1 -0
  3. package/dist/src/embedding/__tests__/embedding.test.js +339 -0
  4. package/dist/src/embedding/chunker.d.ts +40 -0
  5. package/dist/src/embedding/chunker.d.ts.map +1 -0
  6. package/dist/src/embedding/chunker.js +135 -0
  7. package/dist/src/embedding/embedding-provider.d.ts +15 -0
  8. package/dist/src/embedding/embedding-provider.d.ts.map +1 -0
  9. package/dist/src/embedding/embedding-provider.js +1 -0
  10. package/dist/src/embedding/voyage-provider.d.ts +39 -0
  11. package/dist/src/embedding/voyage-provider.d.ts.map +1 -0
  12. package/dist/src/embedding/voyage-provider.js +146 -0
  13. package/dist/src/index.d.ts +14 -2
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/index.js +10 -1
  16. package/dist/src/indexing/__tests__/vector-indexing.test.d.ts +2 -0
  17. package/dist/src/indexing/__tests__/vector-indexing.test.d.ts.map +1 -0
  18. package/dist/src/indexing/__tests__/vector-indexing.test.js +291 -0
  19. package/dist/src/indexing/incremental-indexer.d.ts +4 -0
  20. package/dist/src/indexing/incremental-indexer.d.ts.map +1 -1
  21. package/dist/src/indexing/incremental-indexer.js +45 -0
  22. package/dist/src/indexing/vector-indexer.d.ts +63 -0
  23. package/dist/src/indexing/vector-indexer.d.ts.map +1 -0
  24. package/dist/src/indexing/vector-indexer.js +197 -0
  25. package/dist/src/plugin/code-intelligence-plugin.d.ts.map +1 -1
  26. package/dist/src/plugin/code-intelligence-plugin.js +4 -2
  27. package/dist/src/reranking/__tests__/reranker.test.d.ts +2 -0
  28. package/dist/src/reranking/__tests__/reranker.test.d.ts.map +1 -0
  29. package/dist/src/reranking/__tests__/reranker.test.js +503 -0
  30. package/dist/src/reranking/cohere-reranker.d.ts +26 -0
  31. package/dist/src/reranking/cohere-reranker.d.ts.map +1 -0
  32. package/dist/src/reranking/cohere-reranker.js +110 -0
  33. package/dist/src/reranking/reranker-provider.d.ts +40 -0
  34. package/dist/src/reranking/reranker-provider.d.ts.map +1 -0
  35. package/dist/src/reranking/reranker-provider.js +6 -0
  36. package/dist/src/reranking/voyage-reranker.d.ts +27 -0
  37. package/dist/src/reranking/voyage-reranker.d.ts.map +1 -0
  38. package/dist/src/reranking/voyage-reranker.js +111 -0
  39. package/dist/src/search/__tests__/hybrid-search.test.d.ts +2 -0
  40. package/dist/src/search/__tests__/hybrid-search.test.d.ts.map +1 -0
  41. package/dist/src/search/__tests__/hybrid-search.test.js +437 -0
  42. package/dist/src/search/__tests__/query-classifier.test.d.ts +2 -0
  43. package/dist/src/search/__tests__/query-classifier.test.d.ts.map +1 -0
  44. package/dist/src/search/__tests__/query-classifier.test.js +136 -0
  45. package/dist/src/search/hybrid-search.d.ts +56 -0
  46. package/dist/src/search/hybrid-search.d.ts.map +1 -0
  47. package/dist/src/search/hybrid-search.js +299 -0
  48. package/dist/src/search/query-classifier.d.ts +20 -0
  49. package/dist/src/search/query-classifier.d.ts.map +1 -0
  50. package/dist/src/search/query-classifier.js +58 -0
  51. package/dist/src/search/score-normalizer.d.ts +16 -0
  52. package/dist/src/search/score-normalizer.d.ts.map +1 -0
  53. package/dist/src/search/score-normalizer.js +26 -0
  54. package/dist/src/types.d.ts +83 -0
  55. package/dist/src/types.d.ts.map +1 -1
  56. package/dist/src/types.js +36 -2
  57. package/dist/src/vector/__tests__/vector-store.test.d.ts +2 -0
  58. package/dist/src/vector/__tests__/vector-store.test.d.ts.map +1 -0
  59. package/dist/src/vector/__tests__/vector-store.test.js +278 -0
  60. package/dist/src/vector/hnsw-store.d.ts +48 -0
  61. package/dist/src/vector/hnsw-store.d.ts.map +1 -0
  62. package/dist/src/vector/hnsw-store.js +437 -0
  63. package/dist/src/vector/vector-store.d.ts +15 -0
  64. package/dist/src/vector/vector-store.d.ts.map +1 -0
  65. package/dist/src/vector/vector-store.js +1 -0
  66. package/package.json +1 -1
@@ -180,7 +180,12 @@ export declare const SearchResultSchema: z.ZodObject<{
180
180
  exact: "exact";
181
181
  fuzzy: "fuzzy";
182
182
  bm25: "bm25";
183
+ semantic: "semantic";
184
+ hybrid: "hybrid";
183
185
  }>;
186
+ bm25Score: z.ZodOptional<z.ZodNumber>;
187
+ vectorScore: z.ZodOptional<z.ZodNumber>;
188
+ rerankScore: z.ZodOptional<z.ZodNumber>;
184
189
  }, z.core.$strip>;
185
190
  export type SearchResult = z.infer<typeof SearchResultSchema>;
186
191
  export declare const MemoryEntrySchema: z.ZodObject<{
@@ -239,4 +244,82 @@ export declare const RepoMapEntrySchema: z.ZodObject<{
239
244
  }, z.core.$strip>>;
240
245
  }, z.core.$strip>;
241
246
  export type RepoMapEntry = z.infer<typeof RepoMapEntrySchema>;
247
+ export declare const EmbeddingChunkSchema: z.ZodObject<{
248
+ id: z.ZodString;
249
+ content: z.ZodString;
250
+ embedding: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
251
+ metadata: z.ZodObject<{
252
+ filePath: z.ZodString;
253
+ symbolName: z.ZodOptional<z.ZodString>;
254
+ symbolKind: z.ZodOptional<z.ZodEnum<{
255
+ function: "function";
256
+ class: "class";
257
+ interface: "interface";
258
+ type: "type";
259
+ variable: "variable";
260
+ method: "method";
261
+ property: "property";
262
+ import: "import";
263
+ export: "export";
264
+ enum: "enum";
265
+ struct: "struct";
266
+ trait: "trait";
267
+ impl: "impl";
268
+ macro: "macro";
269
+ decorator: "decorator";
270
+ module: "module";
271
+ }>>;
272
+ startLine: z.ZodNumber;
273
+ endLine: z.ZodNumber;
274
+ language: z.ZodString;
275
+ }, z.core.$strip>;
276
+ }, z.core.$strip>;
277
+ export type EmbeddingChunk = z.infer<typeof EmbeddingChunkSchema>;
278
+ export declare const EmbeddingProviderConfigSchema: z.ZodObject<{
279
+ model: z.ZodString;
280
+ dimensions: z.ZodOptional<z.ZodNumber>;
281
+ batchSize: z.ZodOptional<z.ZodNumber>;
282
+ maxRetries: z.ZodOptional<z.ZodNumber>;
283
+ }, z.core.$strip>;
284
+ export type EmbeddingProviderConfig = z.infer<typeof EmbeddingProviderConfigSchema>;
285
+ export declare const VectorSearchResultSchema: z.ZodObject<{
286
+ chunk: z.ZodObject<{
287
+ id: z.ZodString;
288
+ content: z.ZodString;
289
+ embedding: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
290
+ metadata: z.ZodObject<{
291
+ filePath: z.ZodString;
292
+ symbolName: z.ZodOptional<z.ZodString>;
293
+ symbolKind: z.ZodOptional<z.ZodEnum<{
294
+ function: "function";
295
+ class: "class";
296
+ interface: "interface";
297
+ type: "type";
298
+ variable: "variable";
299
+ method: "method";
300
+ property: "property";
301
+ import: "import";
302
+ export: "export";
303
+ enum: "enum";
304
+ struct: "struct";
305
+ trait: "trait";
306
+ impl: "impl";
307
+ macro: "macro";
308
+ decorator: "decorator";
309
+ module: "module";
310
+ }>>;
311
+ startLine: z.ZodNumber;
312
+ endLine: z.ZodNumber;
313
+ language: z.ZodString;
314
+ }, z.core.$strip>;
315
+ }, z.core.$strip>;
316
+ score: z.ZodNumber;
317
+ }, z.core.$strip>;
318
+ export type VectorSearchResult = z.infer<typeof VectorSearchResultSchema>;
319
+ export declare const VectorIndexConfigSchema: z.ZodObject<{
320
+ enabled: z.ZodBoolean;
321
+ batchSize: z.ZodOptional<z.ZodNumber>;
322
+ maxConcurrentBatches: z.ZodOptional<z.ZodNumber>;
323
+ }, z.core.$strip>;
324
+ export type VectorIndexConfig = z.infer<typeof VectorIndexConfigSchema>;
242
325
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;EAiB3B,CAAA;AACF,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AAIzD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAW3B,CAAA;AACF,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AAIzD,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAOxB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAA;AAInD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK1B,CAAA;AACF,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AAIvD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;iBAM5B,CAAA;AACF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAE3D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAI7B,CAAA;AACF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAI7D,eAAO,MAAM,iBAAiB;;;;;;;iBAO5B,CAAA;AACF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAE3D,eAAO,MAAM,iBAAiB;;;;;;;;;iBAK5B,CAAA;AACF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAI3D,eAAO,MAAM,mBAAmB;;;;;;;iBAO9B,CAAA;AACF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAI/D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;iBAQ7B,CAAA;AACF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;EAiB3B,CAAA;AACF,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AAIzD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAW3B,CAAA;AACF,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AAIzD,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAOxB,CAAA;AACF,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAA;AAInD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK1B,CAAA;AACF,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AAIvD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;iBAM5B,CAAA;AACF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAE3D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO7B,CAAA;AACF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAI7D,eAAO,MAAM,iBAAiB;;;;;;;iBAO5B,CAAA;AACF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAE3D,eAAO,MAAM,iBAAiB;;;;;;;;;iBAK5B,CAAA;AACF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAI3D,eAAO,MAAM,mBAAmB;;;;;;;iBAO9B,CAAA;AACF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAI/D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;iBAQ7B,CAAA;AACF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAI7D,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY/B,CAAA;AACF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AAEjE,eAAO,MAAM,6BAA6B;;;;;iBAKxC,CAAA;AACF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAA;AAInF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAGnC,CAAA;AACF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAA;AAIzE,eAAO,MAAM,uBAAuB;;;;iBAIlC,CAAA;AACF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAA"}
package/dist/src/types.js CHANGED
@@ -57,8 +57,11 @@ export const SearchQuerySchema = z.object({
57
57
  });
58
58
  export const SearchResultSchema = z.object({
59
59
  symbol: CodeSymbolSchema,
60
- score: z.number().nonnegative(),
61
- matchType: z.enum(['exact', 'fuzzy', 'bm25']),
60
+ score: z.number(),
61
+ matchType: z.enum(['exact', 'fuzzy', 'bm25', 'semantic', 'hybrid']),
62
+ bm25Score: z.number().optional(),
63
+ vectorScore: z.number().optional(),
64
+ rerankScore: z.number().optional(),
62
65
  });
63
66
  // ── Memory / Dedup ────────────────────────────────────────────────────
64
67
  export const MemoryEntrySchema = z.object({
@@ -94,3 +97,34 @@ export const RepoMapEntrySchema = z.object({
94
97
  line: z.number().int().nonnegative(),
95
98
  })),
96
99
  });
100
+ // ── Embedding ────────────────────────────────────────────────────────
101
+ export const EmbeddingChunkSchema = z.object({
102
+ id: z.string().min(1), // filePath:symbolName:startLine
103
+ content: z.string(), // Text sent to embedding model
104
+ embedding: z.array(z.number()).optional(), // Dense vector
105
+ metadata: z.object({
106
+ filePath: z.string().min(1),
107
+ symbolName: z.string().optional(),
108
+ symbolKind: SymbolKindSchema.optional(),
109
+ startLine: z.number().int().nonnegative(),
110
+ endLine: z.number().int().nonnegative(),
111
+ language: z.string(),
112
+ }),
113
+ });
114
+ export const EmbeddingProviderConfigSchema = z.object({
115
+ model: z.string().min(1),
116
+ dimensions: z.number().int().positive().optional(),
117
+ batchSize: z.number().int().positive().optional(),
118
+ maxRetries: z.number().int().nonnegative().optional(),
119
+ });
120
+ // ── Vector Store ─────────────────────────────────────────────────────
121
+ export const VectorSearchResultSchema = z.object({
122
+ chunk: EmbeddingChunkSchema,
123
+ score: z.number(),
124
+ });
125
+ // ── Vector Indexing ──────────────────────────────────────────────────
126
+ export const VectorIndexConfigSchema = z.object({
127
+ enabled: z.boolean(),
128
+ batchSize: z.number().int().positive().optional(),
129
+ maxConcurrentBatches: z.number().int().positive().optional(),
130
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=vector-store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vector-store.test.d.ts","sourceRoot":"","sources":["../../../../src/vector/__tests__/vector-store.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,278 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { InMemoryVectorStore } from '../hnsw-store.js';
6
+ // ── Helpers ──────────────────────────────────────────────────────────
7
+ function makeChunk(id, embedding) {
8
+ return {
9
+ id,
10
+ content: `content for ${id}`,
11
+ embedding,
12
+ metadata: {
13
+ filePath: `src/${id}.ts`,
14
+ symbolName: id,
15
+ symbolKind: 'function',
16
+ startLine: 0,
17
+ endLine: 10,
18
+ language: 'typescript',
19
+ },
20
+ };
21
+ }
22
+ /** Generate a random unit vector of given dimensions. */
23
+ function randomVector(dims) {
24
+ const v = Array.from({ length: dims }, () => Math.random() - 0.5);
25
+ const norm = Math.sqrt(v.reduce((sum, x) => sum + x * x, 0));
26
+ return v.map(x => x / norm);
27
+ }
28
+ /** Normalize a vector to unit length. */
29
+ function normalize(v) {
30
+ const norm = Math.sqrt(v.reduce((sum, x) => sum + x * x, 0));
31
+ if (norm === 0)
32
+ return v;
33
+ return v.map(x => x / norm);
34
+ }
35
+ /** Cosine similarity between two vectors. */
36
+ function cosine(a, b) {
37
+ let dot = 0, normA = 0, normB = 0;
38
+ for (let i = 0; i < a.length; i++) {
39
+ dot += a[i] * b[i];
40
+ normA += a[i] * a[i];
41
+ normB += b[i] * b[i];
42
+ }
43
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
44
+ return denom === 0 ? 0 : dot / denom;
45
+ }
46
+ // ── Tests ────────────────────────────────────────────────────────────
47
+ describe('InMemoryVectorStore', () => {
48
+ let store;
49
+ beforeEach(() => {
50
+ store = new InMemoryVectorStore();
51
+ });
52
+ describe('empty store', () => {
53
+ it('returns empty array for search on empty store', async () => {
54
+ const results = await store.search([1, 0, 0], 5);
55
+ expect(results).toEqual([]);
56
+ });
57
+ it('has size 0', () => {
58
+ expect(store.size()).toBe(0);
59
+ });
60
+ });
61
+ describe('insert and search (brute-force)', () => {
62
+ it('inserts and retrieves basic vectors', async () => {
63
+ const chunks = [
64
+ makeChunk('a', [1, 0, 0]),
65
+ makeChunk('b', [0, 1, 0]),
66
+ makeChunk('c', [0, 0, 1]),
67
+ ];
68
+ await store.insert(chunks);
69
+ expect(store.size()).toBe(3);
70
+ const results = await store.search([1, 0, 0], 3);
71
+ expect(results).toHaveLength(3);
72
+ expect(results[0].chunk.id).toBe('a');
73
+ expect(results[0].score).toBeCloseTo(1.0, 5);
74
+ });
75
+ it('respects topK limit', async () => {
76
+ const chunks = Array.from({ length: 10 }, (_, i) => makeChunk(`v${i}`, randomVector(8)));
77
+ await store.insert(chunks);
78
+ const results = await store.search(randomVector(8), 3);
79
+ expect(results).toHaveLength(3);
80
+ });
81
+ it('skips chunks without embeddings', async () => {
82
+ const withEmbedding = makeChunk('a', [1, 0, 0]);
83
+ const withoutEmbedding = {
84
+ id: 'b',
85
+ content: 'no embedding',
86
+ metadata: {
87
+ filePath: 'src/b.ts',
88
+ startLine: 0,
89
+ endLine: 5,
90
+ language: 'typescript',
91
+ },
92
+ };
93
+ await store.insert([withEmbedding, withoutEmbedding]);
94
+ expect(store.size()).toBe(1);
95
+ });
96
+ });
97
+ describe('cosine similarity correctness', () => {
98
+ it('identical vectors have similarity 1', async () => {
99
+ const v = normalize([3, 4, 0]);
100
+ await store.insert([makeChunk('a', v)]);
101
+ const results = await store.search(v, 1);
102
+ expect(results[0].score).toBeCloseTo(1.0, 5);
103
+ });
104
+ it('orthogonal vectors have similarity 0', async () => {
105
+ await store.insert([
106
+ makeChunk('x', [1, 0, 0]),
107
+ makeChunk('y', [0, 1, 0]),
108
+ ]);
109
+ const results = await store.search([1, 0, 0], 2);
110
+ const yResult = results.find(r => r.chunk.id === 'y');
111
+ expect(yResult.score).toBeCloseTo(0.0, 5);
112
+ });
113
+ it('opposite vectors have similarity -1', async () => {
114
+ await store.insert([makeChunk('neg', [-1, 0, 0])]);
115
+ const results = await store.search([1, 0, 0], 1);
116
+ expect(results[0].score).toBeCloseTo(-1.0, 5);
117
+ });
118
+ it('scores match manual cosine computation', async () => {
119
+ const a = [1, 2, 3];
120
+ const b = [4, 5, 6];
121
+ await store.insert([makeChunk('b', b)]);
122
+ const results = await store.search(a, 1);
123
+ expect(results[0].score).toBeCloseTo(cosine(a, b), 5);
124
+ });
125
+ });
126
+ describe('delete', () => {
127
+ it('removes chunks so they do not appear in search', async () => {
128
+ await store.insert([
129
+ makeChunk('a', [1, 0, 0]),
130
+ makeChunk('b', [0, 1, 0]),
131
+ makeChunk('c', [0, 0, 1]),
132
+ ]);
133
+ await store.delete(['b']);
134
+ expect(store.size()).toBe(2);
135
+ const results = await store.search([0, 1, 0], 3);
136
+ const ids = results.map(r => r.chunk.id);
137
+ expect(ids).not.toContain('b');
138
+ });
139
+ it('handles deleting non-existent ids gracefully', async () => {
140
+ await store.insert([makeChunk('a', [1, 0, 0])]);
141
+ await store.delete(['nonexistent']);
142
+ expect(store.size()).toBe(1);
143
+ });
144
+ });
145
+ describe('clear', () => {
146
+ it('empties the store completely', async () => {
147
+ await store.insert([
148
+ makeChunk('a', [1, 0, 0]),
149
+ makeChunk('b', [0, 1, 0]),
150
+ ]);
151
+ expect(store.size()).toBe(2);
152
+ await store.clear();
153
+ expect(store.size()).toBe(0);
154
+ const results = await store.search([1, 0, 0], 5);
155
+ expect(results).toEqual([]);
156
+ });
157
+ });
158
+ describe('HNSW promotion', () => {
159
+ it('activates HNSW when threshold is exceeded', async () => {
160
+ store.setHNSWThreshold(50);
161
+ expect(store.isHNSWActive).toBe(false);
162
+ const chunks = Array.from({ length: 60 }, (_, i) => makeChunk(`v${i}`, randomVector(16)));
163
+ await store.insert(chunks);
164
+ expect(store.isHNSWActive).toBe(true);
165
+ expect(store.size()).toBe(60);
166
+ });
167
+ it('returns correct results after HNSW promotion', async () => {
168
+ store.setHNSWThreshold(50);
169
+ // Create a distinguishable target vector
170
+ const dims = 16;
171
+ const target = Array(dims).fill(0);
172
+ target[0] = 1.0; // Points strongly in dimension 0
173
+ const chunks = [makeChunk('target', normalize(target))];
174
+ // Fill with random vectors
175
+ for (let i = 0; i < 59; i++) {
176
+ chunks.push(makeChunk(`rand${i}`, randomVector(dims)));
177
+ }
178
+ await store.insert(chunks);
179
+ expect(store.isHNSWActive).toBe(true);
180
+ // Search for the target direction
181
+ const query = [...target];
182
+ const results = await store.search(query, 5);
183
+ expect(results.length).toBeGreaterThan(0);
184
+ // The target chunk should be among top results (HNSW is approximate, but with
185
+ // such a distinctive vector it should find it)
186
+ const topIds = results.map(r => r.chunk.id);
187
+ expect(topIds).toContain('target');
188
+ });
189
+ it('delete works after HNSW promotion', async () => {
190
+ store.setHNSWThreshold(50);
191
+ const chunks = Array.from({ length: 55 }, (_, i) => makeChunk(`v${i}`, randomVector(8)));
192
+ await store.insert(chunks);
193
+ expect(store.isHNSWActive).toBe(true);
194
+ await store.delete(['v0', 'v1', 'v2']);
195
+ expect(store.size()).toBe(52);
196
+ const results = await store.search(randomVector(8), 55);
197
+ const ids = results.map(r => r.chunk.id);
198
+ expect(ids).not.toContain('v0');
199
+ expect(ids).not.toContain('v1');
200
+ expect(ids).not.toContain('v2');
201
+ });
202
+ });
203
+ describe('persistence (save/load)', () => {
204
+ let tempDir;
205
+ beforeEach(async () => {
206
+ tempDir = await mkdtemp(join(tmpdir(), 'vector-store-test-'));
207
+ });
208
+ afterEach(async () => {
209
+ await rm(tempDir, { recursive: true, force: true });
210
+ });
211
+ it('saves and loads, preserving data', async () => {
212
+ await store.insert([
213
+ makeChunk('a', [1, 0, 0]),
214
+ makeChunk('b', [0, 1, 0]),
215
+ makeChunk('c', [0, 0, 1]),
216
+ ]);
217
+ await store.save(tempDir);
218
+ const loaded = new InMemoryVectorStore();
219
+ await loaded.load(tempDir);
220
+ expect(loaded.size()).toBe(3);
221
+ const results = await loaded.search([1, 0, 0], 3);
222
+ expect(results).toHaveLength(3);
223
+ expect(results[0].chunk.id).toBe('a');
224
+ expect(results[0].score).toBeCloseTo(1.0, 5);
225
+ });
226
+ it('saves and loads HNSW index', async () => {
227
+ store.setHNSWThreshold(50);
228
+ const dims = 8;
229
+ const chunks = Array.from({ length: 55 }, (_, i) => makeChunk(`v${i}`, randomVector(dims)));
230
+ // Make one very distinct chunk
231
+ const distinctVec = Array(dims).fill(0);
232
+ distinctVec[0] = 1.0;
233
+ chunks.push(makeChunk('distinct', normalize(distinctVec)));
234
+ await store.insert(chunks);
235
+ expect(store.isHNSWActive).toBe(true);
236
+ await store.save(tempDir);
237
+ const loaded = new InMemoryVectorStore();
238
+ loaded.setHNSWThreshold(50);
239
+ await loaded.load(tempDir);
240
+ expect(loaded.size()).toBe(56);
241
+ const results = await loaded.search(distinctVec, 5);
242
+ const topIds = results.map(r => r.chunk.id);
243
+ expect(topIds).toContain('distinct');
244
+ });
245
+ });
246
+ describe('index invalidation', () => {
247
+ let tempDir;
248
+ beforeEach(async () => {
249
+ tempDir = await mkdtemp(join(tmpdir(), 'vector-store-invalid-'));
250
+ });
251
+ afterEach(async () => {
252
+ await rm(tempDir, { recursive: true, force: true });
253
+ });
254
+ it('rejects loading when model config hash mismatches', async () => {
255
+ const storeV1 = new InMemoryVectorStore({ modelConfigHash: 'hash-v1' });
256
+ await storeV1.insert([makeChunk('a', [1, 0, 0])]);
257
+ await storeV1.save(tempDir);
258
+ const storeV2 = new InMemoryVectorStore({ modelConfigHash: 'hash-v2' });
259
+ await expect(storeV2.load(tempDir)).rejects.toThrow(/model config hash mismatch/);
260
+ });
261
+ it('loads successfully when model config hash matches', async () => {
262
+ const storeV1 = new InMemoryVectorStore({ modelConfigHash: 'hash-v1' });
263
+ await storeV1.insert([makeChunk('a', [1, 0, 0])]);
264
+ await storeV1.save(tempDir);
265
+ const storeV1Copy = new InMemoryVectorStore({ modelConfigHash: 'hash-v1' });
266
+ await storeV1Copy.load(tempDir);
267
+ expect(storeV1Copy.size()).toBe(1);
268
+ });
269
+ it('loads successfully when no hash is configured on loader', async () => {
270
+ const storeV1 = new InMemoryVectorStore({ modelConfigHash: 'hash-v1' });
271
+ await storeV1.insert([makeChunk('a', [1, 0, 0])]);
272
+ await storeV1.save(tempDir);
273
+ const noHash = new InMemoryVectorStore();
274
+ await noHash.load(tempDir);
275
+ expect(noHash.size()).toBe(1);
276
+ });
277
+ });
278
+ });
@@ -0,0 +1,48 @@
1
+ import type { EmbeddingChunk } from '../types.js';
2
+ import type { VectorStore, VectorSearchResult } from './vector-store.js';
3
+ export interface HNSWStoreOptions {
4
+ M?: number;
5
+ efConstruction?: number;
6
+ efSearch?: number;
7
+ modelConfigHash?: string;
8
+ }
9
+ export declare class InMemoryVectorStore implements VectorStore {
10
+ private readonly M;
11
+ private readonly efConstruction;
12
+ private readonly efSearch;
13
+ private readonly modelConfigHash;
14
+ private hnswThreshold;
15
+ private chunks;
16
+ private vectors;
17
+ private nodes;
18
+ private entryPoint;
19
+ private maxLevel;
20
+ private hnswBuilt;
21
+ constructor(options?: HNSWStoreOptions);
22
+ /**
23
+ * Override the HNSW promotion threshold (useful for testing).
24
+ */
25
+ setHNSWThreshold(n: number): void;
26
+ insert(chunks: EmbeddingChunk[]): Promise<void>;
27
+ search(query: number[], topK: number): Promise<VectorSearchResult[]>;
28
+ delete(ids: string[]): Promise<void>;
29
+ size(): number;
30
+ save(dirPath: string): Promise<void>;
31
+ load(dirPath: string): Promise<void>;
32
+ clear(): Promise<void>;
33
+ get isHNSWActive(): boolean;
34
+ private bruteForceSearch;
35
+ private buildHNSW;
36
+ private randomLevel;
37
+ private hnswInsert;
38
+ private hnswRemove;
39
+ private hnswSearch;
40
+ private greedyClosest;
41
+ private searchLayer;
42
+ private selectNeighbors;
43
+ private pruneConnections;
44
+ private serializeIndex;
45
+ private deserializeIndex;
46
+ private getFirstDimensions;
47
+ }
48
+ //# sourceMappingURL=hnsw-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hnsw-store.d.ts","sourceRoot":"","sources":["../../../src/vector/hnsw-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAIxE,MAAM,WAAW,gBAAgB;IAC/B,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AA6DD,qBAAa,mBAAoB,YAAW,WAAW;IACrD,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAQ;IAC1B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,aAAa,CAAQ;IAG7B,OAAO,CAAC,MAAM,CAAyC;IACvD,OAAO,CAAC,OAAO,CAAmC;IAGlD,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,UAAU,CAAwB;IAC1C,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,SAAS,CAAQ;gBAEb,OAAO,GAAE,gBAAqB;IAQ1C;;OAEG;IACH,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAM3B,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB/C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IASpE,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAU1C,IAAI,IAAI,MAAM;IAIR,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBpC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAW5B,IAAI,YAAY,IAAI,OAAO,CAE1B;IAID,OAAO,CAAC,gBAAgB;IAexB,OAAO,CAAC,SAAS;IAcjB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,UAAU;IA6DlB,OAAO,CAAC,UAAU;IAoClB,OAAO,CAAC,UAAU;IAyBlB,OAAO,CAAC,aAAa;IAwBrB,OAAO,CAAC,WAAW;IA4DnB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,cAAc;IAsCtB,OAAO,CAAC,gBAAgB;IAgDxB,OAAO,CAAC,kBAAkB;CAM3B"}