graph-indexer 1.1.0 → 2.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/CHANGELOG.md +412 -0
- package/README.md +228 -350
- package/SECURITY.md +68 -0
- package/TRADEMARK.md +31 -0
- package/cli-ui.mjs +78 -0
- package/config.mjs +269 -0
- package/daemon-ctl.mjs +175 -0
- package/daemon-lock.mjs +69 -0
- package/embedders/python/mlx_embed_server.py +61 -0
- package/embedders/python/requirements-mlx.txt +5 -0
- package/embedders/setup-mlx.mjs +110 -0
- package/embeddings.mjs +489 -0
- package/engine/binary.mjs +399 -0
- package/{core-engine.mjs → engine/memory.mjs} +359 -249
- package/engine/sqlite.mjs +863 -0
- package/enrichment.mjs +426 -0
- package/git-signals.mjs +248 -0
- package/indexer.mjs +216 -77
- package/init.mjs +1604 -170
- package/layout.mjs +123 -0
- package/mcp/format.mjs +61 -0
- package/mcp/tools.mjs +1157 -0
- package/mcp/topology.mjs +551 -0
- package/mcp-server.mjs +118 -573
- package/package.json +56 -16
- package/parse/extractor.mjs +617 -0
- package/parse/imports.mjs +365 -0
- package/parse/languages.mjs +89 -0
- package/parse/metadata.mjs +655 -0
- package/parse/routes.mjs +327 -0
- package/prompts/CORE.md +86 -0
- package/prompts/DOMAIN_TEMPLATE.md +46 -0
- package/prompts/INTEGRATION.md +123 -0
- package/prompts/frameworks/ANDROID.md +17 -0
- package/prompts/frameworks/ASPNET_CORE.md +19 -0
- package/prompts/frameworks/FASTAPI_DJANGO.md +19 -0
- package/prompts/frameworks/LARAVEL_SYMFONY.md +22 -0
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +18 -0
- package/prompts/frameworks/RAILS.md +20 -0
- package/prompts/frameworks/REACT.md +20 -0
- package/prompts/frameworks/SPRING_BOOT.md +19 -0
- package/prompts/languages/BASH.md +22 -0
- package/prompts/languages/C.md +21 -0
- package/prompts/languages/CSHARP.md +24 -0
- package/prompts/languages/CSS_SCSS.md +22 -0
- package/prompts/languages/GO.md +23 -0
- package/prompts/languages/JAVA.md +23 -0
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +24 -0
- package/prompts/languages/KOTLIN.md +24 -0
- package/prompts/languages/PHP.md +23 -0
- package/prompts/languages/PYTHON.md +23 -0
- package/prompts/languages/RUBY.md +23 -0
- package/prompts/languages/RUST.md +21 -0
- package/prompts/languages/SWIFT.md +21 -0
- package/search-core.mjs +627 -0
- package/storage.mjs +68 -0
- package/watch-daemon.mjs +204 -69
- package/PROMPT.md +0 -143
- package/parser-utils.mjs +0 -861
package/indexer.mjs
CHANGED
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* @file indexer.mjs
|
|
4
|
-
* @description
|
|
4
|
+
* @description Bootstrap indexer. Walks a repository, extracts Tree-sitter AST
|
|
5
|
+
* chunks + cross-file topology, generates local embeddings (Ollama),
|
|
6
|
+
* optionally enriches the most central chunks with an LLM, and writes
|
|
7
|
+
* the index to the configured backend — the default in-memory JSON
|
|
8
|
+
* artifacts, or a disk-backed SQLite database (--use-sqlite).
|
|
5
9
|
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
6
10
|
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
7
11
|
* @license MIT
|
|
8
|
-
* Copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
9
|
-
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
11
|
-
* in the Software without restriction, including without limitation the rights
|
|
12
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
14
|
-
* furnished to do so, subject to the following conditions: The above copyright
|
|
15
|
-
* notice and this permission notice shall be included in all copies or
|
|
16
|
-
* substantial portions of the Software.
|
|
17
12
|
*/
|
|
18
13
|
import fs from 'fs';
|
|
19
14
|
import path from 'path';
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from './
|
|
24
|
-
import {
|
|
15
|
+
import { MAX_FILE_SIZE_BYTES, buildIgnoreFilter, extractImportsFromAST, extractSemanticChunks } from './parse/extractor.mjs';
|
|
16
|
+
import { EXTENSIONS, getParserForFile } from './parse/languages.mjs';
|
|
17
|
+
import { extractRoutes } from './parse/routes.mjs';
|
|
18
|
+
import { resolveLocalImports, buildEmbeddingPayload, fullBodyForEmbedding } from './parse/imports.mjs';
|
|
19
|
+
import { readEmbeddingBinary, writeEmbeddingBinary } from './engine/binary.mjs';
|
|
20
|
+
import { embeddingKeyFor, summaryEmbeddingText, SUMMARY_VEC_SUFFIX, WINDOW_VEC_SUFFIX, embeddingWindows } from './search-core.mjs';
|
|
21
|
+
import { createEmbedder, describeEmbedder, readEmbedMeta, writeEmbedMeta, _resetSubprocesses } from './embeddings.mjs';
|
|
22
|
+
import { resolveConfig, describeConfig, configNotices } from './config.mjs';
|
|
23
|
+
import { AUTO_SQLITE_CHUNK_THRESHOLD } from './storage.mjs';
|
|
24
|
+
import { ensureDataDir, migrateLegacyLayout } from './layout.mjs';
|
|
25
|
+
import { enrichCoreChunks } from './enrichment.mjs';
|
|
26
|
+
import { collectGitSignals, writeGitSignals } from './git-signals.mjs';
|
|
25
27
|
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
28
|
+
const config = resolveConfig();
|
|
29
|
+
const PROJECT_ROOT = config.projectRoot;
|
|
30
|
+
const INDEX_PATH = config.indexPath;
|
|
31
|
+
const EMBEDDINGS_PATH = config.embeddingPath;
|
|
32
|
+
|
|
33
|
+
// Artifacts are written under <root>/.graph-indexer/ — make sure it exists and
|
|
34
|
+
// relocate any artifacts left at the root by a pre-v1.4 install.
|
|
35
|
+
ensureDataDir(PROJECT_ROOT);
|
|
36
|
+
migrateLegacyLayout(PROJECT_ROOT);
|
|
30
37
|
|
|
31
38
|
function walkRepo(dir, root, ig, files = []) {
|
|
32
39
|
for (const entry of fs.readdirSync(dir)) {
|
|
@@ -44,18 +51,46 @@ function walkRepo(dir, root, ig, files = []) {
|
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
async function main() {
|
|
47
|
-
console.log(`\n🚀 Starting Optimized Indexer\n📂 Directory: ${PROJECT_ROOT}
|
|
54
|
+
console.log(`\n🚀 Starting Optimized Indexer\n📂 Directory: ${PROJECT_ROOT}`);
|
|
55
|
+
console.log('⚙️ Effective configuration:');
|
|
56
|
+
for (const line of describeConfig(config)) console.log(` ${line}`);
|
|
57
|
+
for (const notice of configNotices(config)) console.log(`⚠️ ${notice}`);
|
|
58
|
+
console.log('');
|
|
48
59
|
|
|
49
60
|
const ig = buildIgnoreFilter(PROJECT_ROOT);
|
|
50
61
|
const files = walkRepo(PROJECT_ROOT, PROJECT_ROOT, ig);
|
|
51
62
|
console.log(`Found ${files.length} files to analyse.\n`);
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
64
|
+
// Resolve the embedding provider for this run (Ollama → in-process local →
|
|
65
|
+
// lexical, unless forced). Index time and query time must share the model.
|
|
66
|
+
const embedder = await createEmbedder(config);
|
|
67
|
+
console.log(`🔎 Embeddings: ${describeEmbedder(embedder)}`);
|
|
68
|
+
// Make the `auto` fallback ladder observable — never silently lexical-only.
|
|
69
|
+
if (config.embedProvider === 'auto') {
|
|
70
|
+
if (embedder.provider === 'local') {
|
|
71
|
+
console.log(` ↪ Ollama not reachable at ${config.ollamaHost}; using the bundled in-process model (no daemon required).`);
|
|
72
|
+
} else if (embedder.provider === 'off') {
|
|
73
|
+
console.log(` ↪ No Ollama and no in-process model installed (\`npm i @huggingface/transformers\`) — indexing lexical-only.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
console.log('');
|
|
77
|
+
|
|
78
|
+
let existingCache = readEmbeddingBinary(EMBEDDINGS_PATH);
|
|
79
|
+
// A model switch invalidates the cached vectors — vectors of different models
|
|
80
|
+
// (and dims) must never be mixed in one bin. Re-embed from scratch.
|
|
81
|
+
const prevMeta = readEmbedMeta(EMBEDDINGS_PATH);
|
|
82
|
+
if (prevMeta?.model && embedder.model && prevMeta.model !== embedder.model) {
|
|
83
|
+
console.log(`⚠️ Embedding model changed (${prevMeta.model} → ${embedder.model}); re-embedding from scratch.`);
|
|
84
|
+
existingCache = new Map();
|
|
85
|
+
}
|
|
56
86
|
console.log(`📦 Loaded ${existingCache.size} cached embeddings from previous runs.\n`);
|
|
57
87
|
|
|
58
|
-
const indexData = { chunks: [], graph: { dependencies: {}, importedBy: {} }, embeddingCache: {} };
|
|
88
|
+
const indexData = { chunks: [], graph: { dependencies: {}, importedBy: {}, routes: [] }, embeddingCache: {} };
|
|
89
|
+
const pendingChunks = [];
|
|
90
|
+
// Full bodies of oversized chunks (snippet was truncated by the 3000-char cap),
|
|
91
|
+
// captured here while we still hold the source so the dense channel can window the
|
|
92
|
+
// WHOLE definition. Kept out of the chunk/JSON — transient, dropped after embedding.
|
|
93
|
+
const fullBodies = new Map();
|
|
59
94
|
let totalCheckedFiles = 0;
|
|
60
95
|
|
|
61
96
|
for (const absolutePath of files) {
|
|
@@ -69,73 +104,124 @@ async function main() {
|
|
|
69
104
|
const stats = await fs.promises.stat(absolutePath);
|
|
70
105
|
if (stats.size > MAX_FILE_SIZE_BYTES) continue;
|
|
71
106
|
|
|
72
|
-
|
|
107
|
+
const content = await fs.promises.readFile(absolutePath, 'utf-8');
|
|
73
108
|
if (!content.trim()) continue;
|
|
74
109
|
|
|
75
110
|
const ext = path.extname(absolutePath);
|
|
76
111
|
const parser = getParserForFile(ext);
|
|
77
112
|
if (!parser) continue;
|
|
78
113
|
|
|
79
|
-
|
|
114
|
+
const tree = parser.parse((offset) => offset < content.length ? content.slice(offset, offset + 4096) : null);
|
|
80
115
|
const rawImports = extractImportsFromAST(tree.rootNode, ext);
|
|
81
116
|
const imports = resolveLocalImports(rawImports, relPath, PROJECT_ROOT);
|
|
82
117
|
indexData.graph.dependencies[relPath] = imports;
|
|
83
118
|
|
|
84
|
-
|
|
85
|
-
for (const chunk of
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
119
|
+
const fileChunks = extractSemanticChunks(tree.rootNode, relPath, content, ext);
|
|
120
|
+
for (const chunk of fileChunks) {
|
|
121
|
+
const fullBody = fullBodyForEmbedding(chunk, content);
|
|
122
|
+
if (fullBody) fullBodies.set(chunk.id, fullBody);
|
|
123
|
+
pendingChunks.push(chunk);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const route of extractRoutes(tree.rootNode, relPath, fileChunks, ext)) {
|
|
127
|
+
indexData.graph.routes.push(route);
|
|
93
128
|
}
|
|
94
129
|
} catch (err) {
|
|
95
130
|
console.error(`\n💥 Error in ${relPath}: ${err.message}`);
|
|
96
131
|
}
|
|
97
132
|
}
|
|
98
133
|
|
|
99
|
-
|
|
100
|
-
|
|
134
|
+
// Enrichment runs before embedding so hypothetical questions share the same vector space.
|
|
135
|
+
if (config.enrichment.enabled) {
|
|
136
|
+
await enrichCoreChunks(pendingChunks, indexData.graph, config);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Embedding generation (cache-aware) ────────────────────────────────────────
|
|
140
|
+
// Vectors are keyed by embeddingKeyFor(chunk), which includes an enrichment
|
|
141
|
+
// digest so enriched chunks hit the cache on re-runs (previously they were
|
|
142
|
+
// re-embedded on every run).
|
|
143
|
+
const toEmbed = [];
|
|
144
|
+
for (const chunk of pendingChunks) {
|
|
145
|
+
const vecKey = embeddingKeyFor(chunk);
|
|
146
|
+
const sKey = vecKey + SUMMARY_VEC_SUFFIX;
|
|
147
|
+
// Compute the payload once here and reuse it in the worker to avoid a second build.
|
|
148
|
+
const payload = buildEmbeddingPayload(chunk, indexData.graph.dependencies[chunk.file_path] || [], fullBodies.get(chunk.id) || null);
|
|
149
|
+
const windows = embeddingWindows(payload);
|
|
150
|
+
// Indexes built before dual/window vectors only have the base key, so we must
|
|
151
|
+
// check that summary and window keys are also cached before skipping embedding.
|
|
152
|
+
const summaryMissing = summaryEmbeddingText(chunk) && !existingCache.has(sKey);
|
|
153
|
+
let windowsMissing = false;
|
|
154
|
+
for (let i = 1; i < windows.length; i++) if (!existingCache.has(vecKey + WINDOW_VEC_SUFFIX + i)) { windowsMissing = true; break; }
|
|
155
|
+
if (existingCache.has(vecKey) && !summaryMissing && !windowsMissing) {
|
|
156
|
+
indexData.embeddingCache[vecKey] = Array.from(existingCache.get(vecKey));
|
|
157
|
+
if (existingCache.has(sKey)) indexData.embeddingCache[sKey] = Array.from(existingCache.get(sKey));
|
|
158
|
+
for (let i = 1; i < windows.length; i++) {
|
|
159
|
+
const wk = vecKey + WINDOW_VEC_SUFFIX + i;
|
|
160
|
+
if (existingCache.has(wk)) indexData.embeddingCache[wk] = Array.from(existingCache.get(wk));
|
|
161
|
+
}
|
|
162
|
+
indexData.chunks.push(chunk);
|
|
163
|
+
} else {
|
|
164
|
+
toEmbed.push({ chunk, payload, windows });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(`\n\n🧠 Embedding Generation — ${describeEmbedder(embedder)}`);
|
|
101
169
|
console.log(`Chunks reused from cache: ${indexData.chunks.length}`);
|
|
102
|
-
console.log(`New chunks to process: ${
|
|
170
|
+
console.log(`New chunks to process: ${toEmbed.length}`);
|
|
103
171
|
|
|
104
|
-
if (
|
|
172
|
+
if (toEmbed.length > 0) {
|
|
105
173
|
const BATCH_SIZE = 64;
|
|
106
|
-
|
|
174
|
+
// Embedding concurrency. A single local Ollama model serves requests
|
|
175
|
+
// serially, so a high fan-out just queues batches and risks per-request
|
|
176
|
+
// timeouts (a large model like qwen3-embedding:4b made every queued batch
|
|
177
|
+
// breach the old cap). Default 4 (fast on small models / when
|
|
178
|
+
// OLLAMA_NUM_PARALLEL is set); lower it (e.g. 1) for big models on modest
|
|
179
|
+
// hardware via INDEXER_EMBED_CONCURRENCY.
|
|
180
|
+
const CONCURRENCY = Number(process.env.INDEXER_EMBED_CONCURRENCY) > 0
|
|
181
|
+
? Number(process.env.INDEXER_EMBED_CONCURRENCY) : 4;
|
|
107
182
|
const batches = [];
|
|
108
|
-
for (let i = 0; i <
|
|
109
|
-
batches.push(pendingChunks.slice(i, i + BATCH_SIZE));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
let completedChunksCount = 0;
|
|
113
|
-
console.time("Embedding Generation Duration");
|
|
183
|
+
for (let i = 0; i < toEmbed.length; i += BATCH_SIZE) batches.push(toEmbed.slice(i, i + BATCH_SIZE));
|
|
114
184
|
|
|
185
|
+
let completed = 0, vectorBatches = 0, failedBatches = 0;
|
|
186
|
+
console.time('Embedding Generation Duration');
|
|
115
187
|
const worker = async (batch) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
188
|
+
// Oversized chunks need window vectors so semantic search can reach
|
|
189
|
+
// their tail (the embedder truncates the base text to window 0 only).
|
|
190
|
+
// Enriched chunks also get a compact summary vector whose vocabulary
|
|
191
|
+
// matches natural-language queries.
|
|
192
|
+
const entries = [];
|
|
193
|
+
for (const { chunk: c, payload, windows } of batch) {
|
|
194
|
+
const baseKey = embeddingKeyFor(c);
|
|
195
|
+
entries.push({ key: baseKey, text: payload });
|
|
196
|
+
for (let i = 1; i < windows.length; i++) entries.push({ key: baseKey + WINDOW_VEC_SUFFIX + i, text: windows[i] });
|
|
197
|
+
const sText = summaryEmbeddingText(c);
|
|
198
|
+
if (sText) entries.push({ key: baseKey + SUMMARY_VEC_SUFFIX, text: sText });
|
|
199
|
+
}
|
|
200
|
+
// Per-batch graceful degradation: a slow/failed embedding batch (e.g. a
|
|
201
|
+
// timeout on a big model) must NOT abort the whole index. Those chunks
|
|
202
|
+
// are still indexed lexically — they just miss their vector until the
|
|
203
|
+
// next run re-tries them (the cache only stores what succeeded).
|
|
204
|
+
let matrix = null;
|
|
205
|
+
try { matrix = await embedder.embedDocuments(entries.map(e => e.text)); }
|
|
206
|
+
catch (e) { failedBatches++; process.stderr.write(`\n⚠️ embedding batch failed (${e.message}); indexing ${batch.length} chunks lexical-only this run.\n`); }
|
|
207
|
+
if (matrix && matrix.length === entries.length) {
|
|
208
|
+
for (let j = 0; j < entries.length; j++) {
|
|
209
|
+
indexData.embeddingCache[entries[j].key] = matrix[j];
|
|
126
210
|
}
|
|
127
|
-
|
|
128
|
-
for (const chunk of batch) indexData.chunks.push(chunk);
|
|
211
|
+
vectorBatches++;
|
|
129
212
|
}
|
|
130
|
-
|
|
131
|
-
|
|
213
|
+
for (const { chunk } of batch) indexData.chunks.push(chunk);
|
|
214
|
+
completed += batch.length;
|
|
215
|
+
process.stdout.write(`\r🤖 Embedding Progress: [${completed}/${toEmbed.length}] Chunks processed...`);
|
|
132
216
|
};
|
|
133
|
-
|
|
134
217
|
for (let i = 0; i < batches.length; i += CONCURRENCY) {
|
|
135
|
-
|
|
136
|
-
|
|
218
|
+
await Promise.all(batches.slice(i, i + CONCURRENCY).map(worker));
|
|
219
|
+
}
|
|
220
|
+
console.timeEnd('Embedding Generation Duration');
|
|
221
|
+
if (failedBatches > 0) {
|
|
222
|
+
console.log(`⚠️ ${failedBatches}/${batches.length} embedding batches failed (lexical-only for those chunks); re-run to fill them in. `
|
|
223
|
+
+ `Tip: a large embed model on modest hardware wants INDEXER_EMBED_CONCURRENCY=1 and/or a higher INDEXER_EMBED_TIMEOUT_MS.`);
|
|
137
224
|
}
|
|
138
|
-
console.timeEnd("Embedding Generation Duration");
|
|
139
225
|
}
|
|
140
226
|
|
|
141
227
|
for (const [filePath, imports] of Object.entries(indexData.graph.dependencies)) {
|
|
@@ -145,18 +231,71 @@ async function main() {
|
|
|
145
231
|
}
|
|
146
232
|
}
|
|
147
233
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
234
|
+
// Resolve 'auto' now that the true chunk count is known — the threshold is only
|
|
235
|
+
// meaningful after all chunks have been extracted.
|
|
236
|
+
const backend = config.storage === 'auto'
|
|
237
|
+
? (indexData.chunks.length >= AUTO_SQLITE_CHUNK_THRESHOLD ? 'sqlite' : 'memory')
|
|
238
|
+
: config.storage;
|
|
239
|
+
if (config.storage === 'auto') {
|
|
240
|
+
console.log(`🗄 Storage: auto → ${backend} (${indexData.chunks.length} chunks, threshold ${AUTO_SQLITE_CHUNK_THRESHOLD}).`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (backend === 'sqlite') {
|
|
244
|
+
const { SqliteGraphStore } = await import('./engine/sqlite.mjs');
|
|
245
|
+
const store = new SqliteGraphStore(config.sqlitePath, { embeddingPath: EMBEDDINGS_PATH });
|
|
246
|
+
const res = store.buildFrom({
|
|
247
|
+
chunks: indexData.chunks, graph: indexData.graph, embeddingCache: indexData.embeddingCache,
|
|
248
|
+
});
|
|
249
|
+
store.close?.();
|
|
250
|
+
for (const p of [INDEX_PATH, `${INDEX_PATH}.tmp`]) { try { fs.unlinkSync(p); } catch { /* none */ } }
|
|
251
|
+
console.log(`\n🎉 SQLite index built: ${res.chunks} chunks · ${res.terms} terms · dim ${res.dim}`);
|
|
252
|
+
console.log(` → ${config.sqlitePath}\n`);
|
|
253
|
+
} else {
|
|
254
|
+
const tmpPath = `${INDEX_PATH}.tmp`;
|
|
255
|
+
const tmpBinPath = `${EMBEDDINGS_PATH}.tmp`;
|
|
256
|
+
await Promise.all([
|
|
257
|
+
fs.promises.writeFile(tmpPath, JSON.stringify({ chunks: indexData.chunks, graph: indexData.graph })),
|
|
258
|
+
fs.promises.writeFile(tmpBinPath, writeEmbeddingBinary(indexData.embeddingCache)),
|
|
259
|
+
]);
|
|
260
|
+
await Promise.all([
|
|
261
|
+
fs.promises.rename(tmpPath, INDEX_PATH),
|
|
262
|
+
fs.promises.rename(tmpBinPath, EMBEDDINGS_PATH),
|
|
263
|
+
]);
|
|
264
|
+
for (const p of [config.sqlitePath, `${config.sqlitePath}-wal`, `${config.sqlitePath}-shm`]) {
|
|
265
|
+
try { fs.unlinkSync(p); } catch { /* none */ }
|
|
266
|
+
}
|
|
267
|
+
if (indexData.chunks.length === 0) {
|
|
268
|
+
console.log(`\n⚠️ Indexing finished but produced 0 chunks — no indexable definitions found under ${PROJECT_ROOT}.`);
|
|
269
|
+
console.log(` Check the --repo path is correct and contains supported languages (see README), and note that trivial one-line definitions are intentionally skipped.\n`);
|
|
270
|
+
} else {
|
|
271
|
+
console.log(`\n🎉 Indexing complete. Total fragments: ${indexData.chunks.length}\n`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Stamp which model produced the shared embeddings bin, so the server queries
|
|
276
|
+
// with the same provider and the next index run detects a model switch.
|
|
277
|
+
writeEmbedMeta(EMBEDDINGS_PATH, {
|
|
278
|
+
provider: embedder.provider,
|
|
279
|
+
model: embedder.model,
|
|
280
|
+
dim: embedder.dim ?? prevMeta?.dim ?? null,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Git signals are a sidecar kept out of the main index so they don't affect
|
|
284
|
+
// measured retrieval ranking — only opt-in tools consume them.
|
|
285
|
+
if (config.gitSignals) {
|
|
286
|
+
const signals = collectGitSignals(PROJECT_ROOT);
|
|
287
|
+
if (signals) {
|
|
288
|
+
writeGitSignals(config.gitSignalsPath, signals);
|
|
289
|
+
console.log(`🔄 Git signals: ${signals.commits} commits · ${Object.keys(signals.churn).length} files (churn/recency/co-change).\n`);
|
|
290
|
+
} else {
|
|
291
|
+
// Drop any stale sidecar so signals from a previous run don't linger.
|
|
292
|
+
try { fs.unlinkSync(config.gitSignalsPath); } catch { /* none to remove */ }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Kill any subprocess-based embedder (MLX) so the event loop can drain.
|
|
297
|
+
// Without this the readline interface on proc.stdout keeps Node alive indefinitely.
|
|
298
|
+
_resetSubprocesses();
|
|
160
299
|
}
|
|
161
300
|
|
|
162
|
-
main().catch(console.error);
|
|
301
|
+
main().catch(console.error);
|