@leu2m/semantic-search 0.2.0-beta.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.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/adapters/file-storage.d.ts +8 -0
- package/dist/adapters/file-storage.js +55 -0
- package/dist/adapters/file-storage.js.map +1 -0
- package/dist/adapters/filesystem.d.ts +23 -0
- package/dist/adapters/filesystem.js +119 -0
- package/dist/adapters/filesystem.js.map +1 -0
- package/dist/adapters/minilm.d.ts +50 -0
- package/dist/adapters/minilm.js +141 -0
- package/dist/adapters/minilm.js.map +1 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.js +26 -0
- package/dist/catalog.js.map +1 -0
- package/dist/chunking.d.ts +9 -0
- package/dist/chunking.js +54 -0
- package/dist/chunking.js.map +1 -0
- package/dist/contracts.d.ts +163 -0
- package/dist/contracts.js +4 -0
- package/dist/contracts.js.map +1 -0
- package/dist/embedding-input.d.ts +4 -0
- package/dist/embedding-input.js +13 -0
- package/dist/embedding-input.js.map +1 -0
- package/dist/engine.d.ts +63 -0
- package/dist/engine.js +257 -0
- package/dist/engine.js.map +1 -0
- package/dist/file-types/registry.d.ts +13 -0
- package/dist/file-types/registry.js +47 -0
- package/dist/file-types/registry.js.map +1 -0
- package/dist/file-types/types.d.ts +11 -0
- package/dist/file-types/types.js +2 -0
- package/dist/file-types/types.js.map +1 -0
- package/dist/index-state.d.ts +17 -0
- package/dist/index-state.js +91 -0
- package/dist/index-state.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/index.d.ts +28 -0
- package/dist/parsers/index.js +113 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/retrieval.d.ts +20 -0
- package/dist/retrieval.js +108 -0
- package/dist/retrieval.js.map +1 -0
- package/docs/alpha5-hardening.md +86 -0
- package/docs/alpha6-answerability.md +131 -0
- package/docs/alpha6-c-validation.md +490 -0
- package/docs/alpha6-evidence-traces.md +486 -0
- package/docs/api.md +43 -0
- package/docs/architecture.md +121 -0
- package/docs/benchmarks/alpha5-retrieval.json +5233 -0
- package/docs/benchmarks/alpha5-scale.json +505 -0
- package/docs/benchmarks/alpha6-evidence.json +14185 -0
- package/docs/benchmarks/alpha6c-heldout-real.json +9389 -0
- package/docs/decisions/0001-minilm-loading.md +32 -0
- package/docs/decisions/0002-retrieval-modes.md +26 -0
- package/docs/decisions/0003-retrieval-evidence-boundary.md +19 -0
- package/docs/evaluation.md +315 -0
- package/docs/file-types.md +31 -0
- package/docs/integration.md +204 -0
- package/docs/next-slice.md +11 -0
- package/examples/README.md +38 -0
- package/examples/core.mjs +53 -0
- package/examples/evidence.mjs +20 -0
- package/examples/filesystem.mjs +9 -0
- package/examples/minilm.mjs +15 -0
- package/package.json +73 -0
package/dist/engine.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { documentKey } from './contracts.js';
|
|
2
|
+
import { ParserRegistry } from './parsers/index.js';
|
|
3
|
+
import { chunkDocument } from './chunking.js';
|
|
4
|
+
import { catalogEntry } from './catalog.js';
|
|
5
|
+
import { embeddingTextForChunk } from './embedding-input.js';
|
|
6
|
+
import { configuration, reusableSnapshot, snapshotOf, embedChunks } from './index-state.js';
|
|
7
|
+
import { collectCandidates, rankLexical, rankSemantic, fuseRrf, toSearchResults } from './retrieval.js';
|
|
8
|
+
/** Host-independent indexing and lexical/semantic retrieval lifecycle. */
|
|
9
|
+
export class SemanticSearchEngine {
|
|
10
|
+
sources = new Map();
|
|
11
|
+
records = new Map();
|
|
12
|
+
parsers;
|
|
13
|
+
chunking;
|
|
14
|
+
embedder;
|
|
15
|
+
storage;
|
|
16
|
+
config;
|
|
17
|
+
vectors = new Map();
|
|
18
|
+
loaded = false;
|
|
19
|
+
seed = null;
|
|
20
|
+
busy = false;
|
|
21
|
+
generation = 0;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.parsers = options.parsers ?? new ParserRegistry();
|
|
24
|
+
this.chunking = { ...options.chunking };
|
|
25
|
+
this.embedder = options.embedder;
|
|
26
|
+
this.storage = options.storage;
|
|
27
|
+
if (this.embedder && (!this.embedder.id.trim() || !Number.isSafeInteger(this.embedder.dimensions) || this.embedder.dimensions < 1))
|
|
28
|
+
throw new Error('Invalid embedder configuration');
|
|
29
|
+
this.config = configuration(this.parsers, this.chunking, this.embedder);
|
|
30
|
+
const { targetChars, overlapLines } = this.config.chunking;
|
|
31
|
+
if (!Number.isSafeInteger(targetChars) || targetChars < 64 || !Number.isSafeInteger(overlapLines) || overlapLines < 0)
|
|
32
|
+
throw new Error('Invalid chunk options');
|
|
33
|
+
}
|
|
34
|
+
/** Effective strategy when SearchRequest.mode is omitted. */
|
|
35
|
+
get defaultMode() { return this.embedder ? 'hybrid' : 'lexical'; }
|
|
36
|
+
get revision() { return this.generation; }
|
|
37
|
+
addSource(source) {
|
|
38
|
+
if (this.busy)
|
|
39
|
+
throw new Error('Index update is in progress');
|
|
40
|
+
if (!source.id.trim() || this.sources.has(source.id))
|
|
41
|
+
throw new Error(`Invalid or duplicate source: ${source.id}`);
|
|
42
|
+
this.sources.set(source.id, source);
|
|
43
|
+
}
|
|
44
|
+
removeSource(sourceId) {
|
|
45
|
+
if (this.busy)
|
|
46
|
+
throw new Error('Index update is in progress');
|
|
47
|
+
this.sources.delete(sourceId);
|
|
48
|
+
for (const record of this.records.values())
|
|
49
|
+
if (record.catalog.sourceId === sourceId)
|
|
50
|
+
for (const chunk of record.chunks)
|
|
51
|
+
this.vectors.delete(chunk.id);
|
|
52
|
+
this.discardSeed(sourceId);
|
|
53
|
+
for (const [key, record] of this.records)
|
|
54
|
+
if (record.catalog.sourceId === sourceId)
|
|
55
|
+
this.records.delete(key);
|
|
56
|
+
this.generation++;
|
|
57
|
+
}
|
|
58
|
+
async initialize(options = {}) {
|
|
59
|
+
options.signal?.throwIfAborted();
|
|
60
|
+
const reports = [];
|
|
61
|
+
for (const id of this.sources.keys())
|
|
62
|
+
reports.push(await this.refreshSource(id, options));
|
|
63
|
+
return reports;
|
|
64
|
+
}
|
|
65
|
+
/** Atomic per-source refresh. A failed or cancelled scan preserves that source's previous index. */
|
|
66
|
+
async refreshSource(sourceId, options = {}) {
|
|
67
|
+
if (this.busy)
|
|
68
|
+
throw new Error('Index update is in progress');
|
|
69
|
+
const source = this.sources.get(sourceId);
|
|
70
|
+
if (!source)
|
|
71
|
+
throw new Error(`Unknown source: ${sourceId}`);
|
|
72
|
+
this.busy = true;
|
|
73
|
+
try {
|
|
74
|
+
options.signal?.throwIfAborted();
|
|
75
|
+
if (JSON.stringify(configuration(this.parsers, this.chunking, this.embedder)) !== JSON.stringify(this.config))
|
|
76
|
+
throw new Error('Index configuration changed; create a new engine');
|
|
77
|
+
if (!this.loaded) {
|
|
78
|
+
const snapshot = this.storage ? await this.storage.load(options) : null;
|
|
79
|
+
options.signal?.throwIfAborted();
|
|
80
|
+
this.seed = reusableSnapshot(snapshot, this.config);
|
|
81
|
+
this.loaded = true;
|
|
82
|
+
}
|
|
83
|
+
const documents = await source.list(options);
|
|
84
|
+
const next = new Map();
|
|
85
|
+
let unchangedChunks = 0;
|
|
86
|
+
const seedChunks = new Map((this.seed?.chunks ?? []).map(({ vector: _vector, ...chunk }) => [chunk.id, chunk]));
|
|
87
|
+
const ids = new Set();
|
|
88
|
+
for (const doc of documents) {
|
|
89
|
+
options.signal?.throwIfAborted();
|
|
90
|
+
if (!doc.id || doc.sourceId !== sourceId || !doc.uri || ids.has(doc.id))
|
|
91
|
+
throw new Error('Invalid, duplicate or cross-source document identity');
|
|
92
|
+
ids.add(doc.id);
|
|
93
|
+
if (!this.parsers.fileTypes.get(doc.path ?? doc.name)?.indexable)
|
|
94
|
+
continue;
|
|
95
|
+
const parsed = await this.parsers.parse(structuredClone(doc), await source.read(doc.id, options));
|
|
96
|
+
let chunks = chunkDocument(parsed, this.chunking);
|
|
97
|
+
const key = documentKey(sourceId, doc.id);
|
|
98
|
+
const previous = this.records.get(key);
|
|
99
|
+
const oldChunks = new Map(previous?.chunks.map(chunk => [chunk.id, chunk]));
|
|
100
|
+
chunks = chunks.map(chunk => {
|
|
101
|
+
const old = oldChunks.get(chunk.id) ?? seedChunks.get(chunk.id);
|
|
102
|
+
if (old && JSON.stringify(old) === JSON.stringify(chunk)) {
|
|
103
|
+
unchangedChunks++;
|
|
104
|
+
return old;
|
|
105
|
+
}
|
|
106
|
+
return chunk;
|
|
107
|
+
});
|
|
108
|
+
next.set(key, { catalog: catalogEntry(parsed), chunks });
|
|
109
|
+
}
|
|
110
|
+
options.signal?.throwIfAborted();
|
|
111
|
+
const reusable = new Map();
|
|
112
|
+
if (this.embedder) {
|
|
113
|
+
for (const chunk of this.seed?.chunks ?? [])
|
|
114
|
+
if (chunk.vector)
|
|
115
|
+
reusable.set(embeddingTextForChunk(chunk), new Float32Array(chunk.vector));
|
|
116
|
+
for (const record of this.records.values())
|
|
117
|
+
for (const chunk of record.chunks) {
|
|
118
|
+
const vector = this.vectors.get(chunk.id);
|
|
119
|
+
if (vector)
|
|
120
|
+
reusable.set(embeddingTextForChunk(chunk), vector);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const chunks = [...next.values()].flatMap(record => record.chunks);
|
|
124
|
+
const embedded = this.embedder ? await embedChunks(chunks, reusable, this.embedder, options) : { vectors: new Map(), embeddedTexts: 0 };
|
|
125
|
+
const candidate = new Map(this.records);
|
|
126
|
+
const vectors = new Map(this.vectors);
|
|
127
|
+
for (const [key, record] of candidate)
|
|
128
|
+
if (record.catalog.sourceId === sourceId) {
|
|
129
|
+
candidate.delete(key);
|
|
130
|
+
for (const chunk of record.chunks)
|
|
131
|
+
vectors.delete(chunk.id);
|
|
132
|
+
}
|
|
133
|
+
for (const [key, record] of next)
|
|
134
|
+
candidate.set(key, record);
|
|
135
|
+
for (const [id, vector] of embedded.vectors)
|
|
136
|
+
vectors.set(id, vector);
|
|
137
|
+
options.signal?.throwIfAborted();
|
|
138
|
+
if (JSON.stringify(configuration(this.parsers, this.chunking, this.embedder)) !== JSON.stringify(this.config))
|
|
139
|
+
throw new Error('Index configuration changed; create a new engine');
|
|
140
|
+
if (this.storage)
|
|
141
|
+
await this.storage.save(snapshotOf(candidate, vectors, this.config), options);
|
|
142
|
+
// Successful save is the commit point. Late cancellation cannot undo atomic publication.
|
|
143
|
+
this.records.clear();
|
|
144
|
+
for (const [key, record] of candidate)
|
|
145
|
+
this.records.set(key, record);
|
|
146
|
+
this.vectors = vectors;
|
|
147
|
+
this.discardSeed(sourceId);
|
|
148
|
+
this.generation++;
|
|
149
|
+
return { sourceId, documents: next.size, chunks: chunks.length, unchangedChunks,
|
|
150
|
+
embeddedTexts: embedded.embeddedTexts, reusedVectors: this.embedder ? chunks.length - embedded.embeddedTexts : 0 };
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
this.busy = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
discardSeed(sourceId) {
|
|
157
|
+
if (!this.seed)
|
|
158
|
+
return;
|
|
159
|
+
this.seed.documents = this.seed.documents.filter(doc => doc.sourceId !== sourceId);
|
|
160
|
+
this.seed.chunks = this.seed.chunks.filter(chunk => chunk.sourceId !== sourceId);
|
|
161
|
+
}
|
|
162
|
+
getCatalog() { return structuredClone([...this.records.values()].map(record => record.catalog)); }
|
|
163
|
+
/** Return ranked candidates only. Empty results may reflect query/mode/filter restrictions,
|
|
164
|
+
* not absence of sufficient evidence elsewhere. No answerability evaluation is performed. */
|
|
165
|
+
async search(request) {
|
|
166
|
+
const { signal, query } = request;
|
|
167
|
+
signal?.throwIfAborted();
|
|
168
|
+
const mode = request.mode ?? this.defaultMode;
|
|
169
|
+
if (!['lexical', 'semantic', 'hybrid'].includes(mode))
|
|
170
|
+
throw new Error('Invalid search mode');
|
|
171
|
+
if (mode !== 'lexical' && !this.embedder)
|
|
172
|
+
throw new Error(`${mode} search requires an Embedder`);
|
|
173
|
+
const limit = request.limit ?? 8;
|
|
174
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
175
|
+
throw new Error('limit must be an integer from 1 to 100');
|
|
176
|
+
if (!query.trim())
|
|
177
|
+
return [];
|
|
178
|
+
// Capture references before awaiting inference so a concurrent refresh cannot mix revisions.
|
|
179
|
+
const candidates = collectCandidates(this.records.values(), this.vectors, request);
|
|
180
|
+
if (!candidates.length)
|
|
181
|
+
return [];
|
|
182
|
+
const lexical = mode !== 'semantic' ? rankLexical(candidates, query, signal) : [];
|
|
183
|
+
let semantic = [];
|
|
184
|
+
if (mode !== 'lexical') {
|
|
185
|
+
this.checkEmbeddingConfiguration();
|
|
186
|
+
signal?.throwIfAborted();
|
|
187
|
+
const vector = await this.embedder.embedQuery(query, signal ? { signal } : {});
|
|
188
|
+
signal?.throwIfAborted();
|
|
189
|
+
this.checkEmbeddingConfiguration();
|
|
190
|
+
semantic = rankSemantic(candidates, vector, this.config.embedding.dimensions, signal);
|
|
191
|
+
}
|
|
192
|
+
const ranked = mode === 'hybrid' ? fuseRrf(lexical, semantic, signal)
|
|
193
|
+
: mode === 'lexical' ? lexical.map((candidate, i) => ({ candidate, lexicalRank: i + 1 }))
|
|
194
|
+
: semantic.map((candidate, i) => ({ candidate, semanticRank: i + 1 }));
|
|
195
|
+
signal?.throwIfAborted();
|
|
196
|
+
return toSearchResults(ranked, limit, signal);
|
|
197
|
+
}
|
|
198
|
+
checkEmbeddingConfiguration() {
|
|
199
|
+
if (this.embedder?.id !== this.config.embedding?.id || this.embedder?.dimensions !== this.config.embedding?.dimensions) {
|
|
200
|
+
throw new Error('Embedding configuration changed; create a new engine');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Host creates one candidate-result cache per run; bounded and invalidated by index revision.
|
|
205
|
+
* This caches retrieval only, not answerability, host decisions or current source verification.
|
|
206
|
+
*
|
|
207
|
+
* Cache key semantics:
|
|
208
|
+
* - Lexical queries normalize via trim() + toLowerCase().
|
|
209
|
+
* - Semantic/hybrid queries preserve the raw query string because arbitrary embedders may be case- or whitespace-sensitive.
|
|
210
|
+
* - Filters are canonicalized: source/tag/extension sets are sorted; extensions are lowercased with leading dot normalized. */
|
|
211
|
+
export function createSearchSession(engine, maxEntries = 64) {
|
|
212
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1)
|
|
213
|
+
throw new Error('Invalid cache size');
|
|
214
|
+
let revision = engine.revision;
|
|
215
|
+
let cleared = 0;
|
|
216
|
+
const cache = new Map();
|
|
217
|
+
const stats = { hits: 0, misses: 0 };
|
|
218
|
+
return {
|
|
219
|
+
get stats() { return { ...stats }; },
|
|
220
|
+
/** Cache candidate retrieval only; no host assessment is cached here. */
|
|
221
|
+
async search(request) {
|
|
222
|
+
request.signal?.throwIfAborted();
|
|
223
|
+
if (revision !== engine.revision) {
|
|
224
|
+
cache.clear();
|
|
225
|
+
revision = engine.revision;
|
|
226
|
+
}
|
|
227
|
+
const mode = request.mode ?? engine.defaultMode;
|
|
228
|
+
const query = mode === 'lexical' ? request.query.trim().toLowerCase() : request.query;
|
|
229
|
+
const startedRevision = engine.revision;
|
|
230
|
+
const startedClear = cleared;
|
|
231
|
+
const key = JSON.stringify([mode, query, request.sources ? [...new Set(request.sources)].sort() : null,
|
|
232
|
+
Object.entries(request.filters ?? {}).filter(([, value]) => value !== undefined).map(([key, value]) => {
|
|
233
|
+
// These filter lists are sets; extension matching also ignores case and a leading dot.
|
|
234
|
+
if (key === 'tags' || key === 'extensions') {
|
|
235
|
+
const items = value.map(item => key === 'extensions' ? '.' + item.replace(/^\./, '').toLowerCase() : item);
|
|
236
|
+
return [key, [...new Set(items)].sort()];
|
|
237
|
+
}
|
|
238
|
+
return [key, value];
|
|
239
|
+
}).sort(([a], [b]) => a.localeCompare(b)), request.limit ?? 8]);
|
|
240
|
+
const hit = cache.get(key);
|
|
241
|
+
if (hit) {
|
|
242
|
+
stats.hits++;
|
|
243
|
+
return structuredClone(hit);
|
|
244
|
+
}
|
|
245
|
+
stats.misses++;
|
|
246
|
+
const result = await engine.search(request);
|
|
247
|
+
if (engine.revision !== startedRevision || cleared !== startedClear)
|
|
248
|
+
return result;
|
|
249
|
+
if (cache.size >= maxEntries)
|
|
250
|
+
cache.delete(cache.keys().next().value);
|
|
251
|
+
cache.set(key, structuredClone(result));
|
|
252
|
+
return result;
|
|
253
|
+
},
|
|
254
|
+
clear() { cache.clear(); cleared++; },
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CatalogEntry, Embedder, IndexStorage, IndexSnapshot, OperationOptions, SearchMode, SearchRequest, SearchResult, SearchSource } from './contracts.js';\nimport { documentKey } from './contracts.js';\nimport { ParserRegistry } from './parsers/index.js';\nimport { chunkDocument, type ChunkOptions } from './chunking.js';\nimport { catalogEntry } from './catalog.js';\nimport { embeddingTextForChunk } from './embedding-input.js';\nimport { configuration, reusableSnapshot, snapshotOf, embedChunks, type IndexedDocument, type SnapshotConfiguration } from './index-state.js';\nimport { collectCandidates, rankLexical, rankSemantic, fuseRrf, toSearchResults } from './retrieval.js';\nexport interface RefreshReport { sourceId: string; documents: number; chunks: number; unchangedChunks: number; embeddedTexts: number; reusedVectors: number; }\nexport interface EngineOptions { parsers?: ParserRegistry; chunking?: ChunkOptions; embedder?: Embedder; storage?: IndexStorage; }\n/** Host-independent indexing and lexical/semantic retrieval lifecycle. */\nexport class SemanticSearchEngine {\n private readonly sources = new Map<string, SearchSource>();\n private readonly records = new Map<string, IndexedDocument>();\n private readonly parsers: ParserRegistry;\n private readonly chunking: ChunkOptions;\n private readonly embedder: Embedder | undefined;\n private readonly storage: IndexStorage | undefined;\n private readonly config: SnapshotConfiguration;\n private vectors = new Map<string, Float32Array>();\n private loaded = false;\n private seed: IndexSnapshot | null = null;\n private busy = false;\n private generation = 0;\n constructor(options: EngineOptions = {}) {\n this.parsers = options.parsers ?? new ParserRegistry(); this.chunking = { ...options.chunking };\n this.embedder = options.embedder; this.storage = options.storage;\n if (this.embedder && (!this.embedder.id.trim() || !Number.isSafeInteger(this.embedder.dimensions) || this.embedder.dimensions < 1)) throw new Error('Invalid embedder configuration');\n this.config = configuration(this.parsers, this.chunking, this.embedder);\n const { targetChars, overlapLines } = this.config.chunking!;\n if (!Number.isSafeInteger(targetChars) || targetChars < 64 || !Number.isSafeInteger(overlapLines) || overlapLines < 0) throw new Error('Invalid chunk options');\n }\n /** Effective strategy when SearchRequest.mode is omitted. */\n get defaultMode(): SearchMode { return this.embedder ? 'hybrid' : 'lexical'; }\n get revision(): number { return this.generation; }\n addSource(source: SearchSource): void {\n if (this.busy) throw new Error('Index update is in progress');\n if (!source.id.trim() || this.sources.has(source.id)) throw new Error(`Invalid or duplicate source: ${source.id}`);\n this.sources.set(source.id, source);\n }\n removeSource(sourceId: string): void {\n if (this.busy) throw new Error('Index update is in progress');\n this.sources.delete(sourceId);\n for (const record of this.records.values()) if (record.catalog.sourceId === sourceId) for (const chunk of record.chunks) this.vectors.delete(chunk.id);\n this.discardSeed(sourceId);\n for (const [key, record] of this.records) if (record.catalog.sourceId === sourceId) this.records.delete(key);\n this.generation++;\n }\n async initialize(options: OperationOptions = {}): Promise<RefreshReport[]> {\n options.signal?.throwIfAborted();\n const reports: RefreshReport[] = [];\n for (const id of this.sources.keys()) reports.push(await this.refreshSource(id, options));\n return reports;\n }\n /** Atomic per-source refresh. A failed or cancelled scan preserves that source's previous index. */\n async refreshSource(sourceId: string, options: OperationOptions = {}): Promise<RefreshReport> {\n if (this.busy) throw new Error('Index update is in progress');\n const source = this.sources.get(sourceId); if (!source) throw new Error(`Unknown source: ${sourceId}`);\n this.busy = true;\n try {\n options.signal?.throwIfAborted();\n if (JSON.stringify(configuration(this.parsers, this.chunking, this.embedder)) !== JSON.stringify(this.config)) throw new Error('Index configuration changed; create a new engine');\n if (!this.loaded) {\n const snapshot = this.storage ? await this.storage.load(options) : null;\n options.signal?.throwIfAborted();\n this.seed = reusableSnapshot(snapshot, this.config); this.loaded = true;\n }\n const documents = await source.list(options); const next = new Map<string, IndexedDocument>(); let unchangedChunks = 0;\n const seedChunks = new Map((this.seed?.chunks ?? []).map(({ vector: _vector, ...chunk }) => [chunk.id, chunk]));\n const ids = new Set<string>();\n for (const doc of documents) {\n options.signal?.throwIfAborted();\n if (!doc.id || doc.sourceId !== sourceId || !doc.uri || ids.has(doc.id)) throw new Error('Invalid, duplicate or cross-source document identity');\n ids.add(doc.id);\n if (!this.parsers.fileTypes.get(doc.path ?? doc.name)?.indexable) continue;\n const parsed = await this.parsers.parse(structuredClone(doc), await source.read(doc.id, options));\n let chunks = chunkDocument(parsed, this.chunking); const key = documentKey(sourceId, doc.id);\n const previous = this.records.get(key);\n const oldChunks = new Map(previous?.chunks.map(chunk => [chunk.id, chunk]));\n chunks = chunks.map(chunk => {\n const old = oldChunks.get(chunk.id) ?? seedChunks.get(chunk.id);\n if (old && JSON.stringify(old) === JSON.stringify(chunk)) { unchangedChunks++; return old; }\n return chunk;\n });\n next.set(key, { catalog: catalogEntry(parsed), chunks });\n }\n options.signal?.throwIfAborted();\n const reusable = new Map<string, Float32Array>();\n if (this.embedder) {\n for (const chunk of this.seed?.chunks ?? []) if (chunk.vector) reusable.set(embeddingTextForChunk(chunk), new Float32Array(chunk.vector));\n for (const record of this.records.values()) for (const chunk of record.chunks) {\n const vector = this.vectors.get(chunk.id); if (vector) reusable.set(embeddingTextForChunk(chunk), vector);\n }\n }\n const chunks = [...next.values()].flatMap(record => record.chunks);\n const embedded = this.embedder ? await embedChunks(chunks, reusable, this.embedder, options) : { vectors: new Map<string, Float32Array>(), embeddedTexts: 0 };\n const candidate = new Map(this.records); const vectors = new Map(this.vectors);\n for (const [key, record] of candidate) if (record.catalog.sourceId === sourceId) {\n candidate.delete(key); for (const chunk of record.chunks) vectors.delete(chunk.id);\n }\n for (const [key, record] of next) candidate.set(key, record);\n for (const [id, vector] of embedded.vectors) vectors.set(id, vector);\n options.signal?.throwIfAborted();\n if (JSON.stringify(configuration(this.parsers, this.chunking, this.embedder)) !== JSON.stringify(this.config)) throw new Error('Index configuration changed; create a new engine');\n if (this.storage) await this.storage.save(snapshotOf(candidate, vectors, this.config), options);\n // Successful save is the commit point. Late cancellation cannot undo atomic publication.\n this.records.clear(); for (const [key, record] of candidate) this.records.set(key, record);\n this.vectors = vectors; this.discardSeed(sourceId); this.generation++;\n return { sourceId, documents: next.size, chunks: chunks.length, unchangedChunks,\n embeddedTexts: embedded.embeddedTexts, reusedVectors: this.embedder ? chunks.length - embedded.embeddedTexts : 0 };\n } finally { this.busy = false; }\n }\n private discardSeed(sourceId: string): void {\n if (!this.seed) return;\n this.seed.documents = this.seed.documents.filter(doc => doc.sourceId !== sourceId);\n this.seed.chunks = this.seed.chunks.filter(chunk => chunk.sourceId !== sourceId);\n }\n getCatalog(): CatalogEntry[] { return structuredClone([...this.records.values()].map(record => record.catalog)); }\n /** Return ranked candidates only. Empty results may reflect query/mode/filter restrictions,\n * not absence of sufficient evidence elsewhere. No answerability evaluation is performed. */\n async search(request: SearchRequest): Promise<SearchResult[]> {\n const { signal, query } = request;\n signal?.throwIfAborted();\n const mode = request.mode ?? this.defaultMode;\n if (!['lexical', 'semantic', 'hybrid'].includes(mode)) throw new Error('Invalid search mode');\n if (mode !== 'lexical' && !this.embedder) throw new Error(`${mode} search requires an Embedder`);\n const limit = request.limit ?? 8;\n if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new Error('limit must be an integer from 1 to 100');\n if (!query.trim()) return [];\n // Capture references before awaiting inference so a concurrent refresh cannot mix revisions.\n const candidates = collectCandidates(this.records.values(), this.vectors, request);\n if (!candidates.length) return [];\n const lexical = mode !== 'semantic' ? rankLexical(candidates, query, signal) : [];\n let semantic: typeof candidates = [];\n if (mode !== 'lexical') {\n this.checkEmbeddingConfiguration(); signal?.throwIfAborted();\n const vector = await this.embedder!.embedQuery(query, signal ? { signal } : {});\n signal?.throwIfAborted(); this.checkEmbeddingConfiguration();\n semantic = rankSemantic(candidates, vector, this.config.embedding!.dimensions, signal);\n }\n const ranked = mode === 'hybrid' ? fuseRrf(lexical, semantic, signal)\n : mode === 'lexical' ? lexical.map((candidate, i) => ({ candidate, lexicalRank: i + 1 }))\n : semantic.map((candidate, i) => ({ candidate, semanticRank: i + 1 }));\n signal?.throwIfAborted();\n return toSearchResults(ranked, limit, signal);\n }\n private checkEmbeddingConfiguration(): void {\n if (this.embedder?.id !== this.config.embedding?.id || this.embedder?.dimensions !== this.config.embedding?.dimensions) {\n throw new Error('Embedding configuration changed; create a new engine');\n }\n }\n}\n/** Host creates one candidate-result cache per run; bounded and invalidated by index revision.\n * This caches retrieval only, not answerability, host decisions or current source verification.\n *\n * Cache key semantics:\n * - Lexical queries normalize via trim() + toLowerCase().\n * - Semantic/hybrid queries preserve the raw query string because arbitrary embedders may be case- or whitespace-sensitive.\n * - Filters are canonicalized: source/tag/extension sets are sorted; extensions are lowercased with leading dot normalized. */\nexport function createSearchSession(engine: SemanticSearchEngine, maxEntries = 64) {\n if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) throw new Error('Invalid cache size');\n let revision = engine.revision; let cleared = 0; const cache = new Map<string, SearchResult[]>();\n const stats = { hits: 0, misses: 0 };\n return {\n get stats() { return { ...stats }; },\n /** Cache candidate retrieval only; no host assessment is cached here. */\n async search(request: SearchRequest): Promise<SearchResult[]> {\n request.signal?.throwIfAborted();\n if (revision !== engine.revision) { cache.clear(); revision = engine.revision; }\n const mode = request.mode ?? engine.defaultMode;\n const query = mode === 'lexical' ? request.query.trim().toLowerCase() : request.query;\n const startedRevision = engine.revision; const startedClear = cleared;\n const key = JSON.stringify([mode, query, request.sources ? [...new Set(request.sources)].sort() : null,\n Object.entries(request.filters ?? {}).filter(([, value]) => value !== undefined).map(([key, value]) => {\n // These filter lists are sets; extension matching also ignores case and a leading dot.\n if (key === 'tags' || key === 'extensions') {\n const items = (value as string[]).map(item => key === 'extensions' ? '.' + item.replace(/^\\./, '').toLowerCase() : item);\n return [key, [...new Set(items)].sort()] as const;\n }\n return [key, value] as const;\n }).sort(([a], [b]) => a.localeCompare(b)), request.limit ?? 8]);\n const hit = cache.get(key); if (hit) { stats.hits++; return structuredClone(hit); }\n stats.misses++; const result = await engine.search(request);\n if (engine.revision !== startedRevision || cleared !== startedClear) return result;\n if (cache.size >= maxEntries) cache.delete(cache.keys().next().value!);\n cache.set(key, structuredClone(result)); return result;\n },\n clear() { cache.clear(); cleared++; },\n };\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FileTypeDefinition } from './types.js';
|
|
2
|
+
/** The only built-in extension list. Adapters, parsers and generated docs consult it. */
|
|
3
|
+
export declare const FILE_TYPES: readonly FileTypeDefinition[];
|
|
4
|
+
export declare function extensionOf(path: string): string;
|
|
5
|
+
export declare class FileTypeRegistry {
|
|
6
|
+
readonly definitions: readonly FileTypeDefinition[];
|
|
7
|
+
constructor(definitions?: readonly FileTypeDefinition[]);
|
|
8
|
+
get(path: string): FileTypeDefinition | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare const defaultFileTypes: FileTypeRegistry;
|
|
11
|
+
export declare function getFileTypeDefinition(path: string): FileTypeDefinition | undefined;
|
|
12
|
+
export declare function isSupportedFile(path: string): boolean;
|
|
13
|
+
export declare function getSupportedExtensions(): string[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const define = (extensions, kind, language, parserId, structureAware = false, mimeTypes = []) => Object.freeze({ extensions: Object.freeze(extensions), kind, language, parserId,
|
|
2
|
+
indexable: true, structureAware, textFallback: true, mimeTypes: Object.freeze(mimeTypes), maxBytes: 2 * 1024 * 1024 });
|
|
3
|
+
/** The only built-in extension list. Adapters, parsers and generated docs consult it. */
|
|
4
|
+
export const FILE_TYPES = Object.freeze([
|
|
5
|
+
define(['.md'], 'document', 'markdown', 'markdown', true, ['text/markdown']),
|
|
6
|
+
define(['.txt'], 'document', 'text', 'text', false, ['text/plain']),
|
|
7
|
+
define(['.js', '.jsx', '.mjs', '.cjs'], 'code', 'javascript', 'code'),
|
|
8
|
+
define(['.ts', '.tsx'], 'code', 'typescript', 'code'),
|
|
9
|
+
define(['.py'], 'code', 'python', 'code'), define(['.rs'], 'code', 'rust', 'code'),
|
|
10
|
+
define(['.go'], 'code', 'go', 'code'), define(['.java'], 'code', 'java', 'code'),
|
|
11
|
+
define(['.c', '.h'], 'code', 'c', 'code'), define(['.cpp', '.hpp'], 'code', 'cpp', 'code'),
|
|
12
|
+
define(['.html'], 'code', 'html', 'code'), define(['.css'], 'code', 'css', 'code'),
|
|
13
|
+
define(['.scss'], 'code', 'scss', 'code'),
|
|
14
|
+
define(['.json'], 'configuration', 'json', 'structured', false, ['application/json']),
|
|
15
|
+
define(['.yaml', '.yml'], 'configuration', 'yaml', 'structured'),
|
|
16
|
+
define(['.toml'], 'configuration', 'toml', 'structured'),
|
|
17
|
+
define(['.sh', '.bash'], 'code', 'bash', 'code'), define(['.fish'], 'code', 'fish', 'code'),
|
|
18
|
+
define(['.sql'], 'code', 'sql', 'code'),
|
|
19
|
+
]);
|
|
20
|
+
export function extensionOf(path) {
|
|
21
|
+
const name = path.replace(/\\/g, '/').split('/').pop() ?? '';
|
|
22
|
+
const dot = name.lastIndexOf('.');
|
|
23
|
+
return dot < 0 ? '' : name.slice(dot).toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
export class FileTypeRegistry {
|
|
26
|
+
definitions;
|
|
27
|
+
constructor(definitions = FILE_TYPES) {
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
this.definitions = Object.freeze(definitions.map(type => {
|
|
30
|
+
for (const extension of type.extensions) {
|
|
31
|
+
if (!/^\.[a-z0-9]+$/.test(extension) || seen.has(extension))
|
|
32
|
+
throw new Error(`Invalid or duplicate extension: ${extension}`);
|
|
33
|
+
seen.add(extension);
|
|
34
|
+
}
|
|
35
|
+
return Object.freeze({ ...type, extensions: Object.freeze([...type.extensions]), mimeTypes: Object.freeze([...type.mimeTypes]) });
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
get(path) {
|
|
39
|
+
const extension = extensionOf(path);
|
|
40
|
+
return this.definitions.find(type => type.extensions.includes(extension));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export const defaultFileTypes = new FileTypeRegistry();
|
|
44
|
+
export function getFileTypeDefinition(path) { return defaultFileTypes.get(path); }
|
|
45
|
+
export function isSupportedFile(path) { return getFileTypeDefinition(path)?.indexable === true; }
|
|
46
|
+
export function getSupportedExtensions() { return FILE_TYPES.flatMap(type => [...type.extensions]); }
|
|
47
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/file-types/registry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FileTypeDefinition } from './types.js';\nconst define = (extensions: string[], kind: FileTypeDefinition['kind'], language: string,\n parserId: FileTypeDefinition['parserId'], structureAware = false, mimeTypes: string[] = []): FileTypeDefinition =>\n Object.freeze({ extensions: Object.freeze(extensions), kind, language, parserId,\n indexable: true, structureAware, textFallback: true, mimeTypes: Object.freeze(mimeTypes), maxBytes: 2 * 1024 * 1024 });\n/** The only built-in extension list. Adapters, parsers and generated docs consult it. */\nexport const FILE_TYPES: readonly FileTypeDefinition[] = Object.freeze([\n define(['.md'], 'document', 'markdown', 'markdown', true, ['text/markdown']),\n define(['.txt'], 'document', 'text', 'text', false, ['text/plain']),\n define(['.js', '.jsx', '.mjs', '.cjs'], 'code', 'javascript', 'code'),\n define(['.ts', '.tsx'], 'code', 'typescript', 'code'),\n define(['.py'], 'code', 'python', 'code'), define(['.rs'], 'code', 'rust', 'code'),\n define(['.go'], 'code', 'go', 'code'), define(['.java'], 'code', 'java', 'code'),\n define(['.c', '.h'], 'code', 'c', 'code'), define(['.cpp', '.hpp'], 'code', 'cpp', 'code'),\n define(['.html'], 'code', 'html', 'code'), define(['.css'], 'code', 'css', 'code'),\n define(['.scss'], 'code', 'scss', 'code'),\n define(['.json'], 'configuration', 'json', 'structured', false, ['application/json']),\n define(['.yaml', '.yml'], 'configuration', 'yaml', 'structured'),\n define(['.toml'], 'configuration', 'toml', 'structured'),\n define(['.sh', '.bash'], 'code', 'bash', 'code'), define(['.fish'], 'code', 'fish', 'code'),\n define(['.sql'], 'code', 'sql', 'code'),\n]);\nexport function extensionOf(path: string): string {\n const name = path.replace(/\\\\/g, '/').split('/').pop() ?? '';\n const dot = name.lastIndexOf('.');\n return dot < 0 ? '' : name.slice(dot).toLowerCase();\n}\nexport class FileTypeRegistry {\n readonly definitions: readonly FileTypeDefinition[];\n constructor(definitions: readonly FileTypeDefinition[] = FILE_TYPES) {\n const seen = new Set<string>();\n this.definitions = Object.freeze(definitions.map(type => {\n for (const extension of type.extensions) {\n if (!/^\\.[a-z0-9]+$/.test(extension) || seen.has(extension)) throw new Error(`Invalid or duplicate extension: ${extension}`);\n seen.add(extension);\n }\n return Object.freeze({ ...type, extensions: Object.freeze([...type.extensions]), mimeTypes: Object.freeze([...type.mimeTypes]) });\n }));\n }\n get(path: string): FileTypeDefinition | undefined {\n const extension = extensionOf(path);\n return this.definitions.find(type => type.extensions.includes(extension));\n }\n}\nexport const defaultFileTypes = new FileTypeRegistry();\nexport function getFileTypeDefinition(path: string): FileTypeDefinition | undefined { return defaultFileTypes.get(path); }\nexport function isSupportedFile(path: string): boolean { return getFileTypeDefinition(path)?.indexable === true; }\nexport function getSupportedExtensions(): string[] { return FILE_TYPES.flatMap(type => [...type.extensions]); }\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface FileTypeDefinition {
|
|
2
|
+
readonly extensions: readonly string[];
|
|
3
|
+
readonly kind: 'document' | 'code' | 'configuration';
|
|
4
|
+
readonly language: string;
|
|
5
|
+
readonly parserId: string;
|
|
6
|
+
readonly indexable: boolean;
|
|
7
|
+
readonly structureAware: boolean;
|
|
8
|
+
readonly textFallback: boolean;
|
|
9
|
+
readonly mimeTypes: readonly string[];
|
|
10
|
+
readonly maxBytes: number;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/file-types/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface FileTypeDefinition {\n readonly extensions: readonly string[];\n readonly kind: 'document' | 'code' | 'configuration';\n readonly language: string;\n readonly parserId: string;\n readonly indexable: boolean;\n readonly structureAware: boolean;\n readonly textFallback: boolean;\n readonly mimeTypes: readonly string[];\n readonly maxBytes: number;\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CatalogEntry, Embedder, IndexedChunk, IndexSnapshot, OperationOptions } from './contracts.js';
|
|
2
|
+
import type { ChunkOptions } from './chunking.js';
|
|
3
|
+
import type { ParserRegistry } from './parsers/index.js';
|
|
4
|
+
export interface IndexedDocument {
|
|
5
|
+
catalog: CatalogEntry;
|
|
6
|
+
chunks: IndexedChunk[];
|
|
7
|
+
}
|
|
8
|
+
export type SnapshotConfiguration = Pick<IndexSnapshot, 'schemaVersion' | 'parserVersion' | 'chunkerVersion' | 'chunking' | 'embedding' | 'embeddingInputVersion'>;
|
|
9
|
+
export declare function configuration(parsers: ParserRegistry, chunking: ChunkOptions, embedder?: Embedder): SnapshotConfiguration;
|
|
10
|
+
/** Incompatible snapshots are ignored wholesale. Compatible but corrupt data is an explicit error. */
|
|
11
|
+
export declare function reusableSnapshot(snapshot: IndexSnapshot | null, config: SnapshotConfiguration): IndexSnapshot | null;
|
|
12
|
+
export declare function snapshotOf(records: Map<string, IndexedDocument>, vectors: Map<string, Float32Array>, config: SnapshotConfiguration): IndexSnapshot;
|
|
13
|
+
/** Exact embeddable text is the reuse key; provenance-sensitive chunk IDs remain unchanged. */
|
|
14
|
+
export declare function embedChunks(chunks: IndexedChunk[], reusable: Map<string, Float32Array>, embedder: Embedder, options: OperationOptions): Promise<{
|
|
15
|
+
vectors: Map<string, Float32Array>;
|
|
16
|
+
embeddedTexts: number;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { documentKey } from './contracts.js';
|
|
2
|
+
import { CHUNKER_VERSION, contentHash } from './chunking.js';
|
|
3
|
+
import { embeddingTextForChunk, EMBEDDING_INPUT_VERSION } from './embedding-input.js';
|
|
4
|
+
export function configuration(parsers, chunking, embedder) {
|
|
5
|
+
return {
|
|
6
|
+
schemaVersion: 3,
|
|
7
|
+
embeddingInputVersion: EMBEDDING_INPUT_VERSION,
|
|
8
|
+
parserVersion: parsers.version === undefined ? '' : JSON.stringify([parsers.version, parsers.fileTypes.definitions]),
|
|
9
|
+
chunkerVersion: CHUNKER_VERSION,
|
|
10
|
+
chunking: { targetChars: chunking.targetChars ?? 1600, overlapLines: chunking.overlapLines ?? 2 },
|
|
11
|
+
...(embedder ? { embedding: { id: embedder.id, dimensions: embedder.dimensions } } : {}),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const strings = (value) => Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
15
|
+
const optionalString = (value) => value === undefined || typeof value === 'string';
|
|
16
|
+
const optionalNumber = (value) => value === undefined || (typeof value === 'number' && Number.isFinite(value));
|
|
17
|
+
/** Incompatible snapshots are ignored wholesale. Compatible but corrupt data is an explicit error. */
|
|
18
|
+
export function reusableSnapshot(snapshot, config) {
|
|
19
|
+
if (!snapshot || !config.parserVersion || snapshot.schemaVersion !== config.schemaVersion ||
|
|
20
|
+
snapshot.embeddingInputVersion !== config.embeddingInputVersion || snapshot.parserVersion !== config.parserVersion || snapshot.chunkerVersion !== config.chunkerVersion ||
|
|
21
|
+
snapshot.chunking?.targetChars !== config.chunking?.targetChars || snapshot.chunking?.overlapLines !== config.chunking?.overlapLines ||
|
|
22
|
+
snapshot.embedding?.id !== config.embedding?.id || snapshot.embedding?.dimensions !== config.embedding?.dimensions)
|
|
23
|
+
return null;
|
|
24
|
+
const corrupt = () => { throw new Error('Corrupt index snapshot'); };
|
|
25
|
+
if (!Array.isArray(snapshot.documents) || !Array.isArray(snapshot.chunks))
|
|
26
|
+
return corrupt();
|
|
27
|
+
const documents = new Map();
|
|
28
|
+
for (const doc of snapshot.documents) {
|
|
29
|
+
if (!doc || ![doc.sourceId, doc.documentId, doc.uri].every(value => typeof value === 'string' && value.length > 0) ||
|
|
30
|
+
typeof doc.title !== 'string' || !strings(doc.tags) || !strings(doc.headings) || !strings(doc.outgoingLinks) ||
|
|
31
|
+
!optionalString(doc.path) || !optionalString(doc.fileType) || !optionalNumber(doc.createdAt) || !optionalNumber(doc.modifiedAt))
|
|
32
|
+
return corrupt();
|
|
33
|
+
const key = documentKey(doc.sourceId, doc.documentId);
|
|
34
|
+
if (documents.has(key))
|
|
35
|
+
return corrupt();
|
|
36
|
+
documents.set(key, doc);
|
|
37
|
+
}
|
|
38
|
+
const ids = new Set();
|
|
39
|
+
const occurrences = new Map();
|
|
40
|
+
for (const chunk of snapshot.chunks) {
|
|
41
|
+
if (!chunk || typeof chunk.text !== 'string' || typeof chunk.contentHash !== 'string' ||
|
|
42
|
+
!Number.isSafeInteger(chunk.startLine) || chunk.startLine < 1 || !Number.isSafeInteger(chunk.endLine) || chunk.endLine < chunk.startLine ||
|
|
43
|
+
chunk.text.split('\n').length !== chunk.endLine - chunk.startLine + 1 || !optionalString(chunk.heading) || !optionalString(chunk.language))
|
|
44
|
+
return corrupt();
|
|
45
|
+
const key = documentKey(chunk.sourceId, chunk.documentId);
|
|
46
|
+
const doc = documents.get(key);
|
|
47
|
+
const occurrenceKey = JSON.stringify([key, chunk.contentHash]);
|
|
48
|
+
const occurrence = occurrences.get(occurrenceKey) ?? 0;
|
|
49
|
+
occurrences.set(occurrenceKey, occurrence + 1);
|
|
50
|
+
if (!doc || chunk.uri !== doc.uri || chunk.path !== doc.path || chunk.contentHash !== contentHash(chunk.text) ||
|
|
51
|
+
chunk.id !== JSON.stringify([key, chunk.contentHash, occurrence]) || ids.has(chunk.id))
|
|
52
|
+
return corrupt();
|
|
53
|
+
ids.add(chunk.id);
|
|
54
|
+
if (config.embedding) {
|
|
55
|
+
if (!Array.isArray(chunk.vector) || chunk.vector.length !== config.embedding.dimensions ||
|
|
56
|
+
!chunk.vector.every(value => typeof value === 'number' && Number.isFinite(value) && Number.isFinite(Math.fround(value))))
|
|
57
|
+
return corrupt();
|
|
58
|
+
}
|
|
59
|
+
else if (chunk.vector !== undefined)
|
|
60
|
+
return corrupt();
|
|
61
|
+
}
|
|
62
|
+
return structuredClone(snapshot);
|
|
63
|
+
}
|
|
64
|
+
export function snapshotOf(records, vectors, config) {
|
|
65
|
+
return structuredClone({ ...config, documents: [...records.values()].map(record => record.catalog),
|
|
66
|
+
chunks: [...records.values()].flatMap(record => record.chunks.map(chunk => {
|
|
67
|
+
const vector = vectors.get(chunk.id);
|
|
68
|
+
return { ...chunk, ...(vector ? { vector: Array.from(vector) } : {}) };
|
|
69
|
+
})),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Exact embeddable text is the reuse key; provenance-sensitive chunk IDs remain unchanged. */
|
|
73
|
+
export async function embedChunks(chunks, reusable, embedder, options) {
|
|
74
|
+
const inputs = chunks.map(embeddingTextForChunk);
|
|
75
|
+
const missing = [...new Set(inputs.filter(input => !reusable.has(input)))];
|
|
76
|
+
options.signal?.throwIfAborted();
|
|
77
|
+
if (missing.length) {
|
|
78
|
+
// Runtime/model-specific batching belongs to the supplied embedder.
|
|
79
|
+
const result = await embedder.embedDocuments([...missing], options);
|
|
80
|
+
options.signal?.throwIfAborted();
|
|
81
|
+
if (!Array.isArray(result) || result.length !== missing.length)
|
|
82
|
+
throw new Error('Invalid embedding result length');
|
|
83
|
+
for (const [index, vector] of result.entries()) {
|
|
84
|
+
if (!(vector instanceof Float32Array) || vector.length !== embedder.dimensions || !vector.every(Number.isFinite))
|
|
85
|
+
throw new Error('Invalid embedding vector');
|
|
86
|
+
reusable.set(missing[index], vector.slice());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { vectors: new Map(chunks.map((chunk, index) => [chunk.id, reusable.get(inputs[index])])), embeddedTexts: missing.length };
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=index-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-state.js","sourceRoot":"","sources":["../src/index-state.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CatalogEntry, Embedder, IndexedChunk, IndexSnapshot, OperationOptions } from './contracts.js';\nimport { documentKey } from './contracts.js';\nimport { CHUNKER_VERSION, contentHash } from './chunking.js';\nimport type { ChunkOptions } from './chunking.js';\nimport { embeddingTextForChunk, EMBEDDING_INPUT_VERSION } from './embedding-input.js';\nimport type { ParserRegistry } from './parsers/index.js';\n\nexport interface IndexedDocument { catalog: CatalogEntry; chunks: IndexedChunk[]; }\nexport type SnapshotConfiguration = Pick<IndexSnapshot, 'schemaVersion' | 'parserVersion' | 'chunkerVersion' | 'chunking' | 'embedding' | 'embeddingInputVersion'>;\nexport function configuration(parsers: ParserRegistry, chunking: ChunkOptions, embedder?: Embedder): SnapshotConfiguration {\n return {\n schemaVersion: 3,\n embeddingInputVersion: EMBEDDING_INPUT_VERSION,\n parserVersion: parsers.version === undefined ? '' : JSON.stringify([parsers.version, parsers.fileTypes.definitions]),\n chunkerVersion: CHUNKER_VERSION,\n chunking: { targetChars: chunking.targetChars ?? 1600, overlapLines: chunking.overlapLines ?? 2 },\n ...(embedder ? { embedding: { id: embedder.id, dimensions: embedder.dimensions } } : {}),\n };\n}\nconst strings = (value: unknown): boolean => Array.isArray(value) && value.every(item => typeof item === 'string');\nconst optionalString = (value: unknown): boolean => value === undefined || typeof value === 'string';\nconst optionalNumber = (value: unknown): boolean => value === undefined || (typeof value === 'number' && Number.isFinite(value));\n/** Incompatible snapshots are ignored wholesale. Compatible but corrupt data is an explicit error. */\nexport function reusableSnapshot(snapshot: IndexSnapshot | null, config: SnapshotConfiguration): IndexSnapshot | null {\n if (!snapshot || !config.parserVersion || snapshot.schemaVersion !== config.schemaVersion ||\n snapshot.embeddingInputVersion !== config.embeddingInputVersion || snapshot.parserVersion !== config.parserVersion || snapshot.chunkerVersion !== config.chunkerVersion ||\n snapshot.chunking?.targetChars !== config.chunking?.targetChars || snapshot.chunking?.overlapLines !== config.chunking?.overlapLines ||\n snapshot.embedding?.id !== config.embedding?.id || snapshot.embedding?.dimensions !== config.embedding?.dimensions) return null;\n const corrupt = () => { throw new Error('Corrupt index snapshot'); };\n if (!Array.isArray(snapshot.documents) || !Array.isArray(snapshot.chunks)) return corrupt();\n const documents = new Map<string, CatalogEntry>();\n for (const doc of snapshot.documents) {\n if (!doc || ![doc.sourceId, doc.documentId, doc.uri].every(value => typeof value === 'string' && value.length > 0) ||\n typeof doc.title !== 'string' || !strings(doc.tags) || !strings(doc.headings) || !strings(doc.outgoingLinks) ||\n !optionalString(doc.path) || !optionalString(doc.fileType) || !optionalNumber(doc.createdAt) || !optionalNumber(doc.modifiedAt)) return corrupt();\n const key = documentKey(doc.sourceId, doc.documentId);\n if (documents.has(key)) return corrupt();\n documents.set(key, doc);\n }\n const ids = new Set<string>(); const occurrences = new Map<string, number>();\n for (const chunk of snapshot.chunks) {\n if (!chunk || typeof chunk.text !== 'string' || typeof chunk.contentHash !== 'string' ||\n !Number.isSafeInteger(chunk.startLine) || chunk.startLine < 1 || !Number.isSafeInteger(chunk.endLine) || chunk.endLine < chunk.startLine ||\n chunk.text.split('\\n').length !== chunk.endLine - chunk.startLine + 1 || !optionalString(chunk.heading) || !optionalString(chunk.language)) return corrupt();\n const key = documentKey(chunk.sourceId, chunk.documentId); const doc = documents.get(key);\n const occurrenceKey = JSON.stringify([key, chunk.contentHash]); const occurrence = occurrences.get(occurrenceKey) ?? 0;\n occurrences.set(occurrenceKey, occurrence + 1);\n if (!doc || chunk.uri !== doc.uri || chunk.path !== doc.path || chunk.contentHash !== contentHash(chunk.text) ||\n chunk.id !== JSON.stringify([key, chunk.contentHash, occurrence]) || ids.has(chunk.id)) return corrupt();\n ids.add(chunk.id);\n if (config.embedding) {\n if (!Array.isArray(chunk.vector) || chunk.vector.length !== config.embedding.dimensions ||\n !chunk.vector.every(value => typeof value === 'number' && Number.isFinite(value) && Number.isFinite(Math.fround(value)))) return corrupt();\n } else if (chunk.vector !== undefined) return corrupt();\n }\n return structuredClone(snapshot);\n}\nexport function snapshotOf(records: Map<string, IndexedDocument>, vectors: Map<string, Float32Array>, config: SnapshotConfiguration): IndexSnapshot {\n return structuredClone({ ...config, documents: [...records.values()].map(record => record.catalog),\n chunks: [...records.values()].flatMap(record => record.chunks.map(chunk => {\n const vector = vectors.get(chunk.id);\n return { ...chunk, ...(vector ? { vector: Array.from(vector) } : {}) };\n })),\n });\n}\n/** Exact embeddable text is the reuse key; provenance-sensitive chunk IDs remain unchanged. */\nexport async function embedChunks(chunks: IndexedChunk[], reusable: Map<string, Float32Array>, embedder: Embedder,\n options: OperationOptions): Promise<{ vectors: Map<string, Float32Array>; embeddedTexts: number }> {\n const inputs = chunks.map(embeddingTextForChunk);\n const missing = [...new Set(inputs.filter(input => !reusable.has(input)))];\n options.signal?.throwIfAborted();\n if (missing.length) {\n // Runtime/model-specific batching belongs to the supplied embedder.\n const result = await embedder.embedDocuments([...missing], options);\n options.signal?.throwIfAborted();\n if (!Array.isArray(result) || result.length !== missing.length) throw new Error('Invalid embedding result length');\n for (const [index, vector] of result.entries()) {\n if (!(vector instanceof Float32Array) || vector.length !== embedder.dimensions || !vector.every(Number.isFinite)) throw new Error('Invalid embedding vector');\n reusable.set(missing[index]!, vector.slice());\n }\n }\n return { vectors: new Map(chunks.map((chunk, index) => [chunk.id, reusable.get(inputs[index]!)!])), embeddedTexts: missing.length };\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type * from './contracts.js';
|
|
2
|
+
export { documentKey } from './contracts.js';
|
|
3
|
+
export type { FileTypeDefinition } from './file-types/types.js';
|
|
4
|
+
export { FileTypeRegistry, defaultFileTypes, FILE_TYPES, getFileTypeDefinition, getSupportedExtensions, isSupportedFile } from './file-types/registry.js';
|
|
5
|
+
export { ParserRegistry, MarkdownParser, PlainTextParser, CodeParser, StructuredTextParser, PARSER_VERSION } from './parsers/index.js';
|
|
6
|
+
export { chunkDocument, contentHash, CHUNKER_VERSION } from './chunking.js';
|
|
7
|
+
export type { ChunkOptions } from './chunking.js';
|
|
8
|
+
export { catalogEntry, matchesFilters } from './catalog.js';
|
|
9
|
+
export { SemanticSearchEngine, createSearchSession } from './engine.js';
|
|
10
|
+
export type { EngineOptions, RefreshReport } from './engine.js';
|
|
11
|
+
export { embeddingTextForChunk, EMBEDDING_INPUT_VERSION } from './embedding-input.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { documentKey } from './contracts.js';
|
|
2
|
+
export { FileTypeRegistry, defaultFileTypes, FILE_TYPES, getFileTypeDefinition, getSupportedExtensions, isSupportedFile } from './file-types/registry.js';
|
|
3
|
+
export { ParserRegistry, MarkdownParser, PlainTextParser, CodeParser, StructuredTextParser, PARSER_VERSION } from './parsers/index.js';
|
|
4
|
+
export { chunkDocument, contentHash, CHUNKER_VERSION } from './chunking.js';
|
|
5
|
+
export { catalogEntry, matchesFilters } from './catalog.js';
|
|
6
|
+
export { SemanticSearchEngine, createSearchSession } from './engine.js';
|
|
7
|
+
export { embeddingTextForChunk, EMBEDDING_INPUT_VERSION } from './embedding-input.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,UAAU,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC1J,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,UAAU,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACvI,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAE5E,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAExE,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC","sourcesContent":["export type * from './contracts.js';\nexport { documentKey } from './contracts.js';\nexport type { FileTypeDefinition } from './file-types/types.js';\nexport { FileTypeRegistry, defaultFileTypes, FILE_TYPES, getFileTypeDefinition, getSupportedExtensions, isSupportedFile } from './file-types/registry.js';\nexport { ParserRegistry, MarkdownParser, PlainTextParser, CodeParser, StructuredTextParser, PARSER_VERSION } from './parsers/index.js';\nexport { chunkDocument, contentHash, CHUNKER_VERSION } from './chunking.js';\nexport type { ChunkOptions } from './chunking.js';\nexport { catalogEntry, matchesFilters } from './catalog.js';\nexport { SemanticSearchEngine, createSearchSession } from './engine.js';\nexport type { EngineOptions, RefreshReport } from './engine.js';\nexport { embeddingTextForChunk, EMBEDDING_INPUT_VERSION } from './embedding-input.js';\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DocumentParser, ParsedDocument, SourceContent, SourceDocument } from '../contracts.js';
|
|
2
|
+
import { type FileTypeRegistry } from '../file-types/registry.js';
|
|
3
|
+
export declare const PARSER_VERSION = "1";
|
|
4
|
+
export declare class PlainTextParser implements DocumentParser {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
supports(doc: SourceDocument): boolean;
|
|
7
|
+
parse(doc: SourceDocument, content: SourceContent): ParsedDocument;
|
|
8
|
+
}
|
|
9
|
+
export declare class CodeParser extends PlainTextParser {
|
|
10
|
+
readonly id = "code";
|
|
11
|
+
}
|
|
12
|
+
/** Structured formats retain their exact line structure; JSON syntax is not required for indexing incomplete files. */
|
|
13
|
+
export declare class StructuredTextParser extends PlainTextParser {
|
|
14
|
+
readonly id = "structured";
|
|
15
|
+
}
|
|
16
|
+
export declare class MarkdownParser extends PlainTextParser {
|
|
17
|
+
readonly id = "markdown";
|
|
18
|
+
parse(doc: SourceDocument, content: SourceContent): ParsedDocument;
|
|
19
|
+
}
|
|
20
|
+
export declare class ParserRegistry {
|
|
21
|
+
readonly fileTypes: FileTypeRegistry;
|
|
22
|
+
private readonly parsers;
|
|
23
|
+
/** Custom parser behavior must have an explicit version to permit persisted reuse. */
|
|
24
|
+
readonly version: string | undefined;
|
|
25
|
+
constructor(parsers?: DocumentParser[], fileTypes?: FileTypeRegistry, version?: string);
|
|
26
|
+
select(document: SourceDocument): DocumentParser;
|
|
27
|
+
parse(document: SourceDocument, content: SourceContent): Promise<ParsedDocument>;
|
|
28
|
+
}
|