opencode-rag-plugin 1.15.1 → 1.17.2
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/ReadMe.md +52 -9
- package/dist/api.js +44 -24
- package/dist/chunker/base.d.ts +34 -3
- package/dist/chunker/base.js +70 -21
- package/dist/chunker/factory.d.ts +4 -1
- package/dist/chunker/factory.js +12 -1
- package/dist/chunker/grammar.js +3 -0
- package/dist/chunker/image.js +8 -8
- package/dist/chunker/pdf.js +11 -13
- package/dist/chunker/xml.d.ts +2 -0
- package/dist/chunker/xml.js +2 -0
- package/dist/cli/commands/index-command.js +0 -4
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/init-helpers.d.ts +12 -0
- package/dist/cli/commands/init-helpers.js +87 -2
- package/dist/cli/commands/init.js +20 -2
- package/dist/cli/commands/query.js +1 -0
- package/dist/cli/commands/setup.d.ts +2 -0
- package/dist/cli/commands/setup.js +113 -0
- package/dist/cli/commands/status.js +64 -2
- package/dist/cli/commands/update.d.ts +4 -6
- package/dist/cli/commands/update.js +44 -58
- package/dist/cli/index.js +2 -1
- package/dist/content/image.js +24 -3
- package/dist/content/reader.d.ts +6 -1
- package/dist/content/reader.js +49 -4
- package/dist/core/config.d.ts +29 -1
- package/dist/core/config.js +105 -8
- package/dist/core/desc-cache.d.ts +31 -0
- package/dist/core/desc-cache.js +124 -0
- package/dist/core/interfaces.d.ts +58 -3
- package/dist/core/manifest.d.ts +24 -1
- package/dist/core/manifest.js +34 -3
- package/dist/core/resolve-api-key.js +4 -2
- package/dist/core/runtime-overrides.js +12 -8
- package/dist/core/setup-runtime.d.ts +23 -0
- package/dist/core/setup-runtime.js +231 -0
- package/dist/core/version-check.d.ts +85 -0
- package/dist/core/version-check.js +160 -0
- package/dist/describer/anthropic.d.ts +2 -2
- package/dist/describer/anthropic.js +5 -7
- package/dist/describer/describer.d.ts +2 -2
- package/dist/describer/describer.js +6 -8
- package/dist/describer/gemini.d.ts +2 -2
- package/dist/describer/gemini.js +5 -7
- package/dist/embedder/cohere.d.ts +1 -1
- package/dist/embedder/cohere.js +2 -2
- package/dist/embedder/factory.d.ts +3 -1
- package/dist/embedder/factory.js +8 -2
- package/dist/embedder/health.js +5 -1
- package/dist/embedder/http.d.ts +1 -1
- package/dist/embedder/http.js +74 -43
- package/dist/embedder/ollama.d.ts +1 -1
- package/dist/embedder/ollama.js +2 -2
- package/dist/embedder/openai.d.ts +1 -1
- package/dist/embedder/openai.js +2 -2
- package/dist/eval/compare-merge.d.ts +10 -0
- package/dist/eval/compare-merge.js +537 -0
- package/dist/eval/compare-rankings.d.ts +10 -0
- package/dist/eval/compare-rankings.js +245 -0
- package/dist/eval/dump-descriptions.d.ts +7 -0
- package/dist/eval/dump-descriptions.js +58 -0
- package/dist/eval/fast-index.d.ts +8 -0
- package/dist/eval/fast-index.js +283 -0
- package/dist/eval/run-branch-compare.d.ts +7 -0
- package/dist/eval/run-branch-compare.js +220 -0
- package/dist/eval/run-token-test.js +1 -0
- package/dist/eval/test-kw.d.ts +1 -0
- package/dist/eval/test-kw.js +22 -0
- package/dist/eval/update-descriptions.d.ts +7 -0
- package/dist/eval/update-descriptions.js +84 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/indexer/embed-stage.js +2 -1
- package/dist/indexer/git-diff.js +21 -9
- package/dist/indexer/pipeline.d.ts +6 -0
- package/dist/indexer/pipeline.js +304 -40
- package/dist/indexer/watch.js +1 -3
- package/dist/indexer/worker.d.ts +15 -2
- package/dist/indexer/worker.js +25 -12
- package/dist/mcp/handlers.d.ts +9 -0
- package/dist/mcp/handlers.js +23 -6
- package/dist/mcp/server.js +2 -0
- package/dist/opencode/create-read-tool.d.ts +2 -0
- package/dist/opencode/create-read-tool.js +8 -2
- package/dist/opencode/read-fallback.d.ts +1 -5
- package/dist/opencode/read-fallback.js +1 -18
- package/dist/opencode/read-format.js +5 -3
- package/dist/opencode/tools.js +5 -7
- package/dist/plugin.js +223 -71
- package/dist/retriever/keyword-index.d.ts +3 -2
- package/dist/retriever/keyword-index.js +25 -1
- package/dist/retriever/retriever.d.ts +4 -1
- package/dist/retriever/retriever.js +34 -56
- package/dist/tui.js +18 -1
- package/dist/vectorstore/lancedb.d.ts +35 -4
- package/dist/vectorstore/lancedb.js +146 -23
- package/dist/vectorstore/memory.d.ts +6 -1
- package/dist/vectorstore/memory.js +58 -0
- package/dist/watcher.d.ts +2 -0
- package/dist/watcher.js +5 -1
- package/dist/web/api.js +10 -2
- package/dist/web/server.js +18 -3
- package/package.json +8 -9
- package/scripts/postinstall-setup.js +82 -0
- package/dist/api.js.map +0 -1
- package/dist/chunker/base.js.map +0 -1
- package/dist/chunker/bash.js.map +0 -1
- package/dist/chunker/c.js.map +0 -1
- package/dist/chunker/cpp.js.map +0 -1
- package/dist/chunker/csharp.js.map +0 -1
- package/dist/chunker/css.js.map +0 -1
- package/dist/chunker/doc.js.map +0 -1
- package/dist/chunker/dockerfile.js.map +0 -1
- package/dist/chunker/docx.js.map +0 -1
- package/dist/chunker/excel.js.map +0 -1
- package/dist/chunker/factory.js.map +0 -1
- package/dist/chunker/fallback.js.map +0 -1
- package/dist/chunker/go.js.map +0 -1
- package/dist/chunker/grammar.js.map +0 -1
- package/dist/chunker/html.js.map +0 -1
- package/dist/chunker/image.js.map +0 -1
- package/dist/chunker/ini.js.map +0 -1
- package/dist/chunker/java.js.map +0 -1
- package/dist/chunker/javascript.js.map +0 -1
- package/dist/chunker/json.js.map +0 -1
- package/dist/chunker/kotlin.js.map +0 -1
- package/dist/chunker/loader.js.map +0 -1
- package/dist/chunker/markdown.js.map +0 -1
- package/dist/chunker/pdf.js.map +0 -1
- package/dist/chunker/php.js.map +0 -1
- package/dist/chunker/powershell.js.map +0 -1
- package/dist/chunker/python.js.map +0 -1
- package/dist/chunker/razor.js.map +0 -1
- package/dist/chunker/ruby.js.map +0 -1
- package/dist/chunker/rust.js.map +0 -1
- package/dist/chunker/sln.js.map +0 -1
- package/dist/chunker/sql.js.map +0 -1
- package/dist/chunker/ssl.js.map +0 -1
- package/dist/chunker/swift.js.map +0 -1
- package/dist/chunker/tex.js.map +0 -1
- package/dist/chunker/toml.js.map +0 -1
- package/dist/chunker/typescript.js.map +0 -1
- package/dist/chunker/uuid.js.map +0 -1
- package/dist/chunker/xml.js.map +0 -1
- package/dist/chunker/yaml.js.map +0 -1
- package/dist/cli/commands/clear.js.map +0 -1
- package/dist/cli/commands/describe-image.js.map +0 -1
- package/dist/cli/commands/dump.js.map +0 -1
- package/dist/cli/commands/eval.js.map +0 -1
- package/dist/cli/commands/index-command.js.map +0 -1
- package/dist/cli/commands/index.js.map +0 -1
- package/dist/cli/commands/init-helpers.js.map +0 -1
- package/dist/cli/commands/init.js.map +0 -1
- package/dist/cli/commands/list.js.map +0 -1
- package/dist/cli/commands/mcp.js.map +0 -1
- package/dist/cli/commands/query.js.map +0 -1
- package/dist/cli/commands/show.js.map +0 -1
- package/dist/cli/commands/status.js.map +0 -1
- package/dist/cli/commands/ui.js.map +0 -1
- package/dist/cli/commands/update.js.map +0 -1
- package/dist/cli/format.js.map +0 -1
- package/dist/cli/helpers.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/progress.d.ts +0 -42
- package/dist/cli/progress.js +0 -137
- package/dist/cli/progress.js.map +0 -1
- package/dist/cli/types.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/content/doc.js.map +0 -1
- package/dist/content/docx.js.map +0 -1
- package/dist/content/excel.js.map +0 -1
- package/dist/content/image.js.map +0 -1
- package/dist/content/pdf.js.map +0 -1
- package/dist/content/reader.js.map +0 -1
- package/dist/content/types.js.map +0 -1
- package/dist/core/bootstrap.js.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/doc-progress.js.map +0 -1
- package/dist/core/fileLogger.js.map +0 -1
- package/dist/core/interfaces.js.map +0 -1
- package/dist/core/manifest.js.map +0 -1
- package/dist/core/provider-defaults.js.map +0 -1
- package/dist/core/rag-injection-flag.js.map +0 -1
- package/dist/core/resolve-api-key.js.map +0 -1
- package/dist/core/runtime-overrides.js.map +0 -1
- package/dist/describer/anthropic.js.map +0 -1
- package/dist/describer/describer.js.map +0 -1
- package/dist/describer/factory.js.map +0 -1
- package/dist/describer/gemini.js.map +0 -1
- package/dist/describer/shared.js.map +0 -1
- package/dist/embedder/cohere.js.map +0 -1
- package/dist/embedder/factory.js.map +0 -1
- package/dist/embedder/health.js.map +0 -1
- package/dist/embedder/http.js.map +0 -1
- package/dist/embedder/ollama.js.map +0 -1
- package/dist/embedder/openai.js.map +0 -1
- package/dist/eval/index.js.map +0 -1
- package/dist/eval/run-token-test.js.map +0 -1
- package/dist/eval/session-logger.js.map +0 -1
- package/dist/eval/storage.js.map +0 -1
- package/dist/eval/token-analysis.js.map +0 -1
- package/dist/eval/token-counter.js.map +0 -1
- package/dist/eval/types.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/indexer/description-stage.js.map +0 -1
- package/dist/indexer/embed-stage.js.map +0 -1
- package/dist/indexer/git-diff.js.map +0 -1
- package/dist/indexer/metadata.js.map +0 -1
- package/dist/indexer/pipeline.js.map +0 -1
- package/dist/indexer/stats.js.map +0 -1
- package/dist/indexer/watch.js.map +0 -1
- package/dist/indexer/worker.js.map +0 -1
- package/dist/indexer.js.map +0 -1
- package/dist/mcp/cli.js.map +0 -1
- package/dist/mcp/handlers.js.map +0 -1
- package/dist/mcp/server.js.map +0 -1
- package/dist/opencode/create-read-tool.js.map +0 -1
- package/dist/opencode/read-fallback.js.map +0 -1
- package/dist/opencode/read-format.js.map +0 -1
- package/dist/opencode/read-query.js.map +0 -1
- package/dist/opencode/tool-args.js.map +0 -1
- package/dist/opencode/tools.js.map +0 -1
- package/dist/plugin-entry.js.map +0 -1
- package/dist/plugin.js.map +0 -1
- package/dist/retriever/context-optimizer.js.map +0 -1
- package/dist/retriever/keyword-index.js.map +0 -1
- package/dist/retriever/retriever.js.map +0 -1
- package/dist/tui.js.map +0 -1
- package/dist/updater.d.ts +0 -45
- package/dist/updater.js +0 -175
- package/dist/updater.js.map +0 -1
- package/dist/vectorstore/factory.js.map +0 -1
- package/dist/vectorstore/lancedb.js.map +0 -1
- package/dist/vectorstore/memory.js.map +0 -1
- package/dist/watcher.js.map +0 -1
- package/dist/web/api.js.map +0 -1
- package/dist/web/server.js.map +0 -1
- package/dist/web/static.js.map +0 -1
package/dist/indexer/pipeline.js
CHANGED
|
@@ -5,12 +5,13 @@ import fs from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import pLimit from "p-limit";
|
|
7
7
|
import { scanWorkspaceFiles } from "../content/reader.js";
|
|
8
|
-
import { loadManifest, saveManifest, normalizeFilePath } from "../core/manifest.js";
|
|
8
|
+
import { loadManifest, saveManifest, normalizeFilePath, computeDescriptionConfigHash } from "../core/manifest.js";
|
|
9
|
+
import { DescriptionCache } from "../core/desc-cache.js";
|
|
9
10
|
import { embedBatch } from "../embedder/factory.js";
|
|
10
11
|
import { createVectorStore } from "../vectorstore/factory.js";
|
|
11
12
|
import { swapStoreDirectories } from "../vectorstore/lancedb.js";
|
|
12
13
|
import { createIndexStats } from "./stats.js";
|
|
13
|
-
import { prepareFile, buildTextsToEmbed
|
|
14
|
+
import { prepareFile, buildTextsToEmbed } from "./worker.js";
|
|
14
15
|
import { buildFallbackDescription } from "./description-stage.js";
|
|
15
16
|
import { getCurrentCommit, getChangedFilesSince, getUntrackedFiles, getRepoRoot } from "./git-diff.js";
|
|
16
17
|
export { createWatchPassScheduler, createWatchIgnore } from "./watch.js";
|
|
@@ -99,6 +100,11 @@ async function runIndexPassInner(options, logger) {
|
|
|
99
100
|
manifestStatus = "missing";
|
|
100
101
|
logger.debug("Force mode: ignoring manifest");
|
|
101
102
|
}
|
|
103
|
+
// Compute a hash of the current description config — used to skip re-description
|
|
104
|
+
// when neither the file content nor the description config has changed.
|
|
105
|
+
const descHash = options.descriptionProvider
|
|
106
|
+
? computeDescriptionConfigHash(options.config)
|
|
107
|
+
: undefined;
|
|
102
108
|
// Clear files that had description failures so they are fully re-indexed
|
|
103
109
|
const descFailedPaths = Object.keys(manifest.files).filter((p) => manifest.files[p]?.descriptionFailed);
|
|
104
110
|
if (descFailedPaths.length > 0) {
|
|
@@ -107,6 +113,39 @@ async function runIndexPassInner(options, logger) {
|
|
|
107
113
|
delete manifest.files[p];
|
|
108
114
|
}
|
|
109
115
|
}
|
|
116
|
+
// Persistent description cache — survives aborted runs so that descriptions
|
|
117
|
+
// generated before an abort are reused on the next run.
|
|
118
|
+
const descCachePath = options.storePath;
|
|
119
|
+
const descCache = new DescriptionCache(descCachePath);
|
|
120
|
+
await descCache.load();
|
|
121
|
+
// Resume-on-interrupt: detect manifest entries that are inconsistent with
|
|
122
|
+
// the vector store (e.g. indexing was aborted before all chunks were stored,
|
|
123
|
+
// or a full rebuild was aborted leaving a stale manifest pointing to old data).
|
|
124
|
+
// These entries are removed so they will be re-processed in this pass.
|
|
125
|
+
if (!options.force && manifestStatus === "ok" && Object.keys(manifest.files).length > 0) {
|
|
126
|
+
try {
|
|
127
|
+
const storedPaths = await options.store.getFilePaths();
|
|
128
|
+
const storedSet = new Set(storedPaths);
|
|
129
|
+
let removedCount = 0;
|
|
130
|
+
for (const [p, entry] of Object.entries(manifest.files)) {
|
|
131
|
+
// Path missing from store entirely (aborted before store finished)
|
|
132
|
+
if (entry.chunkCount > 0 && !storedSet.has(p)) {
|
|
133
|
+
delete manifest.files[p];
|
|
134
|
+
removedCount++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (removedCount > 0) {
|
|
138
|
+
logger.info(` ${removedCount} file(s) in manifest but missing from store — re-indexing (resume after interruption)`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
logger.debug(" Could not check store paths for resume — proceeding without");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Clear files where the stored descHash doesn't match current descHash and the
|
|
146
|
+
// file content IS different — these would be caught by hash comparison below but
|
|
147
|
+
// this pre-clear ensures the description cache is consulted during re-description.
|
|
148
|
+
// (Files with same hash but different descHash already fall through in prepareFile.)
|
|
110
149
|
let filterPaths;
|
|
111
150
|
let gitDeletedPaths = [];
|
|
112
151
|
if (!options.force && manifestStatus === "ok" && manifest.lastGitCommit) {
|
|
@@ -127,7 +166,7 @@ async function runIndexPassInner(options, logger) {
|
|
|
127
166
|
}
|
|
128
167
|
}
|
|
129
168
|
const scanStart = Date.now();
|
|
130
|
-
const workspaceFiles = await scanWorkspaceFiles(options.cwd, options.config, logger, options.force ? undefined : manifest, filterPaths);
|
|
169
|
+
const workspaceFiles = await scanWorkspaceFiles(options.cwd, options.config, logger, options.force ? undefined : manifest, filterPaths, options.imageVisionProvider, descCache);
|
|
131
170
|
const scanSec = ((Date.now() - scanStart) / 1000).toFixed(1);
|
|
132
171
|
logger.info(`Workspace scan complete: ${workspaceFiles.length} files in ${scanSec}s`);
|
|
133
172
|
const existingCount = await options.store.count();
|
|
@@ -171,10 +210,17 @@ async function runIndexPassInner(options, logger) {
|
|
|
171
210
|
effectiveStore = createVectorStore(options.config, tempStorePath, options.dimension);
|
|
172
211
|
logger.debug(`Rebuilding index in temporary store at ${tempStorePath}`);
|
|
173
212
|
}
|
|
213
|
+
else if (existingCount > 0) {
|
|
214
|
+
// NEVER destroy existing data when we can't do an atomic rebuild.
|
|
215
|
+
// Abort and ask the user to run 'opencode-rag index --force' manually.
|
|
216
|
+
logger.warn("Cannot rebuild safely without embedding dimension — aborting to protect existing data. " +
|
|
217
|
+
"Run 'opencode-rag index --force' manually to rebuild.");
|
|
218
|
+
// Restore manifest entries we just deleted so the next pass can retry incrementally
|
|
219
|
+
return createIndexStats(workspaceFiles.length, manifestStatus);
|
|
220
|
+
}
|
|
174
221
|
else {
|
|
175
|
-
//
|
|
176
|
-
logger.
|
|
177
|
-
await options.store.clear();
|
|
222
|
+
// No existing data — safe to proceed with in-place indexing (no clear needed)
|
|
223
|
+
logger.debug("No existing data; indexing from scratch.");
|
|
178
224
|
}
|
|
179
225
|
}
|
|
180
226
|
const stats = createIndexStats(workspaceFiles.length, manifestStatus);
|
|
@@ -237,12 +283,19 @@ async function runIndexPassInner(options, logger) {
|
|
|
237
283
|
if (isActive) {
|
|
238
284
|
options.progress?.startFile(fileLabel);
|
|
239
285
|
}
|
|
240
|
-
const prep = await prepareFile(file, options.cwd, manifest.files[file.normalizedPath], options.config, options.keywordIndex, options.descriptionProvider, logger, deferDescriptions);
|
|
286
|
+
const prep = await prepareFile(file, options.cwd, manifest.files[file.normalizedPath], options.config, options.keywordIndex, options.descriptionProvider, logger, deferDescriptions, descHash);
|
|
241
287
|
if (prep.earlyResult && isActive) {
|
|
242
288
|
options.progress?.finishFile(fileLabel);
|
|
243
289
|
}
|
|
244
290
|
return prep;
|
|
245
291
|
})));
|
|
292
|
+
const aborted = () => options.abortSignal?.aborted ?? false;
|
|
293
|
+
// Shared progress logger: "stage <file> (chunk i/n) — X/total remaining (P%)".
|
|
294
|
+
const logChunkProgress = (stage, fileLabel, index, count, completed, total) => {
|
|
295
|
+
const remaining = total - completed;
|
|
296
|
+
const pct = total > 0 ? ((remaining / total) * 100).toFixed(1) : "0.0";
|
|
297
|
+
logger.info(`${stage} ${fileLabel} (chunk ${index}/${count}) — ${remaining}/${total} remaining (${pct}%)`);
|
|
298
|
+
};
|
|
246
299
|
if (deferDescriptions) {
|
|
247
300
|
const deferredPreps = prepared.filter((p) => p.chunks && p.chunks.length > 0 && p.relPath !== undefined);
|
|
248
301
|
if (deferredPreps.length > 0) {
|
|
@@ -264,13 +317,139 @@ async function runIndexPassInner(options, logger) {
|
|
|
264
317
|
for (const chunk of oversizedChunks) {
|
|
265
318
|
chunk.description = buildFallbackDescription(chunk);
|
|
266
319
|
}
|
|
320
|
+
// Per-file chunk index + label for clear progress reporting, plus a shared
|
|
321
|
+
// progress logger emitting "stage <file> (chunk i/n) — X/total remaining (P%)".
|
|
322
|
+
const chunkToFileLabel = new Map();
|
|
323
|
+
for (const prep of deferredPreps) {
|
|
324
|
+
for (const chunk of prep.chunks ?? []) {
|
|
325
|
+
chunkToFileLabel.set(chunk.id, prep.fileLabel);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const chunkMeta = new Map();
|
|
329
|
+
{
|
|
330
|
+
const perFileCount = new Map();
|
|
331
|
+
for (const chunk of allChunks) {
|
|
332
|
+
perFileCount.set(chunk.metadata.filePath, (perFileCount.get(chunk.metadata.filePath) ?? 0) + 1);
|
|
333
|
+
}
|
|
334
|
+
const perFileSeen = new Map();
|
|
335
|
+
for (const chunk of allChunks) {
|
|
336
|
+
const seen = perFileSeen.get(chunk.metadata.filePath) ?? 0;
|
|
337
|
+
perFileSeen.set(chunk.metadata.filePath, seen + 1);
|
|
338
|
+
chunkMeta.set(chunk.id, {
|
|
339
|
+
fileLabel: chunkToFileLabel.get(chunk.id) ?? chunk.metadata.filePath,
|
|
340
|
+
index: seen + 1,
|
|
341
|
+
count: perFileCount.get(chunk.metadata.filePath) ?? 1,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
267
345
|
// Advance progress to Description stage before descriptions start
|
|
268
346
|
for (const prep of deferredPreps) {
|
|
269
347
|
options.progress?.finishStage(prep.fileLabel);
|
|
270
348
|
}
|
|
271
|
-
if (allChunks.length > 0) {
|
|
349
|
+
if (allChunks.length > 0 && descHash) {
|
|
350
|
+
// Check description cache first — reuse cached descriptions for unchanged chunks
|
|
351
|
+
const cacheHits = [];
|
|
352
|
+
const cacheMisses = [];
|
|
353
|
+
for (const chunk of allChunks) {
|
|
354
|
+
const cacheKey = DescriptionCache.codeKey(chunk.content, descHash);
|
|
355
|
+
const cached = descCache.get(cacheKey);
|
|
356
|
+
if (cached) {
|
|
357
|
+
cacheHits.push({ chunk, desc: cached });
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
cacheMisses.push(chunk);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (cacheHits.length > 0) {
|
|
364
|
+
logger.debug(` Using ${cacheHits.length} cached descriptions`);
|
|
365
|
+
for (const { chunk, desc } of cacheHits) {
|
|
366
|
+
chunk.description = desc;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Build chunkId → prep map for tracking description failures per-file
|
|
370
|
+
const chunkToPrep = new Map();
|
|
371
|
+
for (const prep of deferredPreps) {
|
|
372
|
+
for (const chunk of prep.chunks ?? []) {
|
|
373
|
+
chunkToPrep.set(chunk.id, prep);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Process cache misses in parallel waves with throttled cache saves.
|
|
377
|
+
// Parallel sub-batches reduce serial LLM turnaround time.
|
|
378
|
+
// Cache saves are throttled to avoid O(n^2) full-JSON serialization I/O.
|
|
379
|
+
if (cacheMisses.length > 0) {
|
|
380
|
+
const totalMisses = cacheMisses.length;
|
|
381
|
+
const descConcurrency = options.config.indexing.descriptionConcurrency ?? 4;
|
|
382
|
+
const SUB_BATCH = 10;
|
|
383
|
+
let describedDone = 0;
|
|
384
|
+
let describedSinceLastSave = 0;
|
|
385
|
+
const SAVE_INTERVAL = 200;
|
|
386
|
+
const descLimit = pLimit(descConcurrency);
|
|
387
|
+
const subBatchTasks = [];
|
|
388
|
+
for (let i = 0; i < totalMisses; i += SUB_BATCH) {
|
|
389
|
+
subBatchTasks.push({ i, chunks: cacheMisses.slice(i, i + SUB_BATCH) });
|
|
390
|
+
}
|
|
391
|
+
await Promise.all(subBatchTasks.map(({ i, chunks: subBatch }) => descLimit(async () => {
|
|
392
|
+
if (aborted())
|
|
393
|
+
return;
|
|
394
|
+
try {
|
|
395
|
+
const batchResult = await options.descriptionProvider.generateBatchDescriptions(subBatch, logger, {
|
|
396
|
+
total: totalMisses,
|
|
397
|
+
onProgress: (chunk) => {
|
|
398
|
+
describedDone++;
|
|
399
|
+
const meta = chunkMeta.get(chunk.id);
|
|
400
|
+
if (meta)
|
|
401
|
+
logChunkProgress("Describing", meta.fileLabel, meta.index, meta.count, describedDone, totalMisses);
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
const newCacheEntries = [];
|
|
405
|
+
for (const chunk of subBatch) {
|
|
406
|
+
const desc = batchResult.get(chunk.id);
|
|
407
|
+
if (desc && desc.trim().length > 0) {
|
|
408
|
+
chunk.description = desc;
|
|
409
|
+
newCacheEntries.push([DescriptionCache.codeKey(chunk.content, descHash), desc]);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (newCacheEntries.length > 0) {
|
|
413
|
+
descCache.setMany(newCacheEntries);
|
|
414
|
+
describedSinceLastSave += newCacheEntries.length;
|
|
415
|
+
if (describedSinceLastSave >= SAVE_INTERVAL) {
|
|
416
|
+
await descCache.save();
|
|
417
|
+
describedSinceLastSave = 0;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
logger.warn(` Description sub-batch failed (${i}-${i + subBatch.length}): ${err.message}`);
|
|
423
|
+
const failedPreps = new Set();
|
|
424
|
+
for (const chunk of subBatch) {
|
|
425
|
+
const prep = chunkToPrep.get(chunk.id);
|
|
426
|
+
if (prep)
|
|
427
|
+
failedPreps.add(prep);
|
|
428
|
+
}
|
|
429
|
+
for (const prep of failedPreps) {
|
|
430
|
+
prep.descriptionFailed = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
})));
|
|
434
|
+
if (describedSinceLastSave > 0) {
|
|
435
|
+
await descCache.save();
|
|
436
|
+
}
|
|
437
|
+
logger.debug(`Descriptions generated for ${totalMisses} chunks (concurrency: ${descConcurrency})`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (allChunks.length > 0) {
|
|
441
|
+
// No descHash available (no description provider) — still generate descriptions
|
|
442
|
+
let describedDone = 0;
|
|
272
443
|
try {
|
|
273
|
-
const batchResult = await options.descriptionProvider.generateBatchDescriptions(allChunks, logger
|
|
444
|
+
const batchResult = await options.descriptionProvider.generateBatchDescriptions(allChunks, logger, {
|
|
445
|
+
total: allChunks.length,
|
|
446
|
+
onProgress: (chunk) => {
|
|
447
|
+
describedDone++;
|
|
448
|
+
const meta = chunkMeta.get(chunk.id);
|
|
449
|
+
if (meta)
|
|
450
|
+
logChunkProgress("Describing", meta.fileLabel, meta.index, meta.count, describedDone, allChunks.length);
|
|
451
|
+
},
|
|
452
|
+
});
|
|
274
453
|
for (const chunk of allChunks) {
|
|
275
454
|
const desc = batchResult.get(chunk.id);
|
|
276
455
|
if (desc && desc.trim().length > 0) {
|
|
@@ -292,10 +471,9 @@ async function runIndexPassInner(options, logger) {
|
|
|
292
471
|
}
|
|
293
472
|
}
|
|
294
473
|
}
|
|
295
|
-
//
|
|
296
|
-
//
|
|
474
|
+
// Cross-file embedding batch: collect all texts into a single queue,
|
|
475
|
+
// embed in one batched call (with concurrency), then distribute back.
|
|
297
476
|
const isOllama = options.embedder.name === "ollama";
|
|
298
|
-
const ollamaMaxBatch = options.config.indexing.ollamaMaxBatchSize ?? 4000;
|
|
299
477
|
const defaultBatchSize = options.config.indexing.embedBatchSize;
|
|
300
478
|
const defaultConcurrency = options.config.indexing.embedConcurrency ?? 1;
|
|
301
479
|
// File metadata look-up for manifest entries
|
|
@@ -303,48 +481,102 @@ async function runIndexPassInner(options, logger) {
|
|
|
303
481
|
// Serialised manifest-save queue — prevents concurrent write races and acts
|
|
304
482
|
// as a checkpoint for Ctrl+C resilience. Each worker appends to this chain
|
|
305
483
|
// after a successful store, so previously completed files are never lost.
|
|
484
|
+
// During a temp-store rebuild, saves go to the temp path so the real
|
|
485
|
+
// manifest stays consistent with the real store if the process is aborted.
|
|
486
|
+
const manifestTargetPath = () => tempStorePath ?? options.storePath;
|
|
306
487
|
let manifestSaveChain = Promise.resolve(undefined);
|
|
307
488
|
function enqueueManifestSave() {
|
|
308
|
-
manifestSaveChain = manifestSaveChain.then(() => saveManifest(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
489
|
+
manifestSaveChain = manifestSaveChain.then(() => saveManifest(manifestTargetPath(), manifest).catch((err) => {
|
|
490
|
+
options.logger?.warn?.(`Failed to save manifest: ${err.message}`);
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
// ── Phase 1: Collect embed queue + handle early results ────────────────
|
|
494
|
+
const embedQueue = [];
|
|
495
|
+
let totalEmbedChunks = 0;
|
|
496
|
+
const earlyWorkerResults = new Map();
|
|
497
|
+
for (let fi = 0; fi < prepared.length; fi++) {
|
|
498
|
+
const prep = prepared[fi];
|
|
316
499
|
if (prep.earlyResult) {
|
|
317
|
-
// Remove stale manifest/store entries for files that no longer produce chunks
|
|
318
500
|
if (prep.earlyResult.isRemoved) {
|
|
319
501
|
await effectiveStore.deleteByFilePath(prep.normalizedPath);
|
|
320
502
|
options.keywordIndex?.removeByFilePath(prep.normalizedPath);
|
|
321
503
|
delete manifest.files[prep.normalizedPath];
|
|
322
504
|
enqueueManifestSave();
|
|
323
505
|
}
|
|
324
|
-
|
|
506
|
+
earlyWorkerResults.set(fi, prep.earlyResult);
|
|
507
|
+
continue;
|
|
325
508
|
}
|
|
326
509
|
if (!prep.chunks || !prep.textToEmbed || prep.textToEmbed.length === 0) {
|
|
327
510
|
options.progress?.finishFile(prep.fileLabel);
|
|
328
|
-
|
|
511
|
+
earlyWorkerResults.set(fi, {
|
|
329
512
|
normalizedPath: prep.normalizedPath, hash: prep.hash, chunkCount: 0,
|
|
330
513
|
fileLabel: prep.fileLabel,
|
|
331
514
|
isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
|
|
332
515
|
isTooSmall: false, isRemoved: true, hadChunks: false,
|
|
333
516
|
descriptionFailed: prep.descriptionFailed,
|
|
334
|
-
};
|
|
517
|
+
});
|
|
518
|
+
continue;
|
|
335
519
|
}
|
|
336
|
-
const batchSize = isOllama
|
|
337
|
-
? Math.min(prep.textToEmbed.length, ollamaMaxBatch)
|
|
338
|
-
: defaultBatchSize;
|
|
339
|
-
const concurrency = isOllama ? 1 : defaultConcurrency;
|
|
340
520
|
options.progress?.finishStage(prep.fileLabel);
|
|
341
|
-
let
|
|
521
|
+
for (let ci = 0; ci < prep.textToEmbed.length; ci++) {
|
|
522
|
+
embedQueue.push({ fileIdx: fi, chunkIdx: ci, text: prep.textToEmbed[ci] });
|
|
523
|
+
}
|
|
524
|
+
totalEmbedChunks += prep.textToEmbed.length;
|
|
525
|
+
}
|
|
526
|
+
// ── Phase 2: Embed all texts in a single batched call ──────────────────
|
|
527
|
+
const batchSize = isOllama
|
|
528
|
+
? Math.min(options.config.indexing.ollamaMaxBatchSize ?? 500, defaultBatchSize)
|
|
529
|
+
: defaultBatchSize;
|
|
530
|
+
let embeddedDone = 0;
|
|
531
|
+
const allTexts = embedQueue.map(item => item.text);
|
|
532
|
+
let allEmbeddings = [];
|
|
533
|
+
if (allTexts.length > 0) {
|
|
342
534
|
try {
|
|
343
|
-
|
|
535
|
+
allEmbeddings = await embedBatch(options.embedder, allTexts, batchSize, "document", defaultConcurrency, (completed, total) => {
|
|
536
|
+
embeddedDone = completed;
|
|
537
|
+
logChunkProgress("Embedding", "", completed, total, embeddedDone, totalEmbedChunks);
|
|
538
|
+
});
|
|
344
539
|
}
|
|
345
540
|
catch (err) {
|
|
346
|
-
logger.warn(`
|
|
347
|
-
|
|
541
|
+
logger.warn(` Global embedding failed: ${err.message}`);
|
|
542
|
+
for (const { fileIdx } of embedQueue) {
|
|
543
|
+
options.progress?.failFile(prepared[fileIdx].fileLabel);
|
|
544
|
+
earlyWorkerResults.set(fileIdx, {
|
|
545
|
+
normalizedPath: prepared[fileIdx].normalizedPath,
|
|
546
|
+
hash: prepared[fileIdx].hash,
|
|
547
|
+
chunkCount: 0, fileLabel: prepared[fileIdx].fileLabel,
|
|
548
|
+
isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
|
|
549
|
+
isTooSmall: false, isRemoved: true, hadChunks: false,
|
|
550
|
+
descriptionFailed: prepared[fileIdx].descriptionFailed,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
embedQueue.length = 0; // prevent double-processing in store phase
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// ── Distribute embeddings back to per-file chunks ─────────────────────
|
|
557
|
+
for (let i = 0; i < embedQueue.length; i++) {
|
|
558
|
+
const { fileIdx, chunkIdx } = embedQueue[i];
|
|
559
|
+
const emb = allEmbeddings[i];
|
|
560
|
+
const prep = prepared[fileIdx];
|
|
561
|
+
if (prep.chunks && prep.chunks[chunkIdx] && Array.isArray(emb) && emb.length > 0 && typeof emb[0] === "number") {
|
|
562
|
+
prep.chunks[chunkIdx].embedding = emb;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// ── Phase 3: Store + manifest update per file (parallel) ──────────────
|
|
566
|
+
const filesToStore = prepared.filter((p) => !earlyWorkerResults.has(prepared.indexOf(p)) && p.chunks && (p.textToEmbed?.length ?? 0) > 0).length;
|
|
567
|
+
let storedFiles = 0;
|
|
568
|
+
const storeLimit = pLimit(options.config.indexing.concurrency);
|
|
569
|
+
const storeResults = await Promise.all(prepared.map((prep, fi) => storeLimit(async () => {
|
|
570
|
+
if (aborted()) {
|
|
571
|
+
return { normalizedPath: prep.normalizedPath, skipped: true };
|
|
572
|
+
}
|
|
573
|
+
// Return early results from phase 1
|
|
574
|
+
const earlyResult = earlyWorkerResults.get(fi);
|
|
575
|
+
if (earlyResult)
|
|
576
|
+
return earlyResult;
|
|
577
|
+
// No-embed path (shouldn't reach here but guard anyway)
|
|
578
|
+
if (!prep.chunks || prep.textToEmbed?.length === 0) {
|
|
579
|
+
options.progress?.finishFile(prep.fileLabel);
|
|
348
580
|
return {
|
|
349
581
|
normalizedPath: prep.normalizedPath, hash: prep.hash, chunkCount: 0,
|
|
350
582
|
fileLabel: prep.fileLabel,
|
|
@@ -353,12 +585,30 @@ async function runIndexPassInner(options, logger) {
|
|
|
353
585
|
descriptionFailed: prep.descriptionFailed,
|
|
354
586
|
};
|
|
355
587
|
}
|
|
356
|
-
//
|
|
357
|
-
const
|
|
358
|
-
|
|
588
|
+
// Store chunks with pre-attached embeddings
|
|
589
|
+
const validChunks = (prep.chunks ?? []).filter((c) => c.embedding && c.embedding.length > 0);
|
|
590
|
+
if (validChunks.length > 0) {
|
|
591
|
+
await effectiveStore.addChunks(validChunks);
|
|
592
|
+
}
|
|
593
|
+
const result = {
|
|
594
|
+
normalizedPath: prep.normalizedPath,
|
|
595
|
+
hash: prep.hash,
|
|
596
|
+
chunkCount: prep.chunks?.length ?? 0,
|
|
597
|
+
fileLabel: prep.fileLabel,
|
|
598
|
+
isNew: !prep.isModified,
|
|
599
|
+
isModified: prep.isModified,
|
|
600
|
+
isUnchanged: false,
|
|
601
|
+
isEmpty: false,
|
|
602
|
+
isTooSmall: false,
|
|
603
|
+
isRemoved: validChunks.length === 0,
|
|
604
|
+
hadChunks: (prep.chunks?.length ?? 0) > 0,
|
|
605
|
+
descriptionFailed: prep.descriptionFailed,
|
|
606
|
+
descHash: prep.descHash,
|
|
607
|
+
};
|
|
608
|
+
// Update manifest
|
|
359
609
|
if (result.chunkCount > 0 && !result.isRemoved) {
|
|
360
610
|
const meta = fileMeta.get(result.normalizedPath);
|
|
361
|
-
|
|
611
|
+
const entry = {
|
|
362
612
|
hash: result.hash,
|
|
363
613
|
chunkCount: result.chunkCount,
|
|
364
614
|
indexedAt: Date.now(),
|
|
@@ -366,6 +616,10 @@ async function runIndexPassInner(options, logger) {
|
|
|
366
616
|
size: meta?.size,
|
|
367
617
|
descriptionFailed: result.descriptionFailed,
|
|
368
618
|
};
|
|
619
|
+
if (result.descHash) {
|
|
620
|
+
entry.descHash = result.descHash;
|
|
621
|
+
}
|
|
622
|
+
manifest.files[result.normalizedPath] = entry;
|
|
369
623
|
enqueueManifestSave();
|
|
370
624
|
}
|
|
371
625
|
else if (result.isRemoved) {
|
|
@@ -373,8 +627,11 @@ async function runIndexPassInner(options, logger) {
|
|
|
373
627
|
enqueueManifestSave();
|
|
374
628
|
}
|
|
375
629
|
options.progress?.finishFile(prep.fileLabel);
|
|
630
|
+
storedFiles++;
|
|
631
|
+
logChunkProgress("Storing", prep.fileLabel, storedFiles, filesToStore, storedFiles, filesToStore);
|
|
376
632
|
return result;
|
|
377
633
|
})));
|
|
634
|
+
const workerResults = storeResults;
|
|
378
635
|
const finalResults = [];
|
|
379
636
|
for (const r of workerResults) {
|
|
380
637
|
if (r.skipped)
|
|
@@ -406,9 +663,7 @@ async function runIndexPassInner(options, logger) {
|
|
|
406
663
|
// Swap the newly-built temp directory into the real path
|
|
407
664
|
await swapStoreDirectories(tempStorePath, options.storePath);
|
|
408
665
|
// Re-open the original store handle so callers can search the new data
|
|
409
|
-
|
|
410
|
-
await options.store.reopen(options.storePath);
|
|
411
|
-
}
|
|
666
|
+
await options.store.reopen?.(options.storePath);
|
|
412
667
|
logger.debug(`Promoted temporary store ${tempStorePath} → ${options.storePath}`);
|
|
413
668
|
}
|
|
414
669
|
catch (err) {
|
|
@@ -421,19 +676,28 @@ async function runIndexPassInner(options, logger) {
|
|
|
421
676
|
}
|
|
422
677
|
}
|
|
423
678
|
else {
|
|
424
|
-
// Aborted — discard temp, keep original data intact
|
|
679
|
+
// Aborted — discard temp, keep original data intact.
|
|
680
|
+
// Do NOT save the manifest to the real path — the in-memory manifest
|
|
681
|
+
// was cleared at rebuild start and only partially rebuilt. The old
|
|
682
|
+
// manifest on disk (at the real path) is still consistent with the
|
|
683
|
+
// old store data.
|
|
425
684
|
effectiveStore.close().catch(() => { });
|
|
426
685
|
try {
|
|
427
686
|
await fs.rm(tempStorePath, { recursive: true, force: true });
|
|
428
687
|
}
|
|
429
688
|
catch { }
|
|
430
689
|
logger.debug("Index pass cancelled; discarded temporary store.");
|
|
690
|
+
// Skip final manifest/keyword save since we want the old state preserved
|
|
691
|
+
await descCache.save();
|
|
692
|
+
return stats;
|
|
431
693
|
}
|
|
432
694
|
}
|
|
433
695
|
// Save manifest and keyword index (always to the real store path — after
|
|
434
696
|
// a successful swap this points to the new data; after an abort it's the old).
|
|
435
697
|
await saveManifest(options.storePath, manifest);
|
|
436
698
|
await options.keywordIndex?.save(options.storePath);
|
|
699
|
+
// Persist description cache
|
|
700
|
+
await descCache.save();
|
|
437
701
|
// Count from the store — after a successful swap, the original handle has
|
|
438
702
|
// been reopened pointing to the new directory.
|
|
439
703
|
try {
|
package/dist/indexer/watch.js
CHANGED
package/dist/indexer/worker.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface WorkerResult {
|
|
|
26
26
|
hadChunks: boolean;
|
|
27
27
|
/** Whether description generation failed for this file. */
|
|
28
28
|
descriptionFailed?: boolean;
|
|
29
|
+
/** Hash of the description config used when generating descriptions for this file. */
|
|
30
|
+
descHash?: string;
|
|
29
31
|
}
|
|
30
32
|
/** Intermediate result after chunking a file but before embedding/storing. */
|
|
31
33
|
export interface PreparedFile {
|
|
@@ -53,6 +55,8 @@ export interface PreparedFile {
|
|
|
53
55
|
docPrefix?: string;
|
|
54
56
|
/** Whether the source file is an image. */
|
|
55
57
|
isImageFile?: boolean;
|
|
58
|
+
/** Hash of the description config used when generating descriptions. */
|
|
59
|
+
descHash?: string;
|
|
56
60
|
}
|
|
57
61
|
/**
|
|
58
62
|
* Build the list of text strings that will be sent to the embedding provider.
|
|
@@ -76,6 +80,7 @@ interface ManifestFile {
|
|
|
76
80
|
hash: string;
|
|
77
81
|
chunkCount: number;
|
|
78
82
|
indexedAt?: number;
|
|
83
|
+
descHash?: string;
|
|
79
84
|
}
|
|
80
85
|
/**
|
|
81
86
|
* Prepare a single workspace file for indexing: chunk the content, build
|
|
@@ -92,6 +97,10 @@ interface ManifestFile {
|
|
|
92
97
|
* @param deferDescriptions - When true, skip description generation and build
|
|
93
98
|
* fallback descriptions instead; descriptions are
|
|
94
99
|
* expected to be generated in a later global pass.
|
|
100
|
+
* @param descHash - Hash of the current description config. When the file
|
|
101
|
+
* is unchanged and `previous.descHash === descHash`,
|
|
102
|
+
* descriptions are skipped entirely (they already exist
|
|
103
|
+
* in the vector store from a prior run).
|
|
95
104
|
* @returns A prepared file descriptor ready for embedding and storage.
|
|
96
105
|
*/
|
|
97
106
|
export declare function prepareFile(file: WorkspaceFile, cwd: string, previous: ManifestFile | undefined, config: {
|
|
@@ -104,7 +113,10 @@ export declare function prepareFile(file: WorkspaceFile, cwd: string, previous:
|
|
|
104
113
|
description?: {
|
|
105
114
|
maxContentChars?: number;
|
|
106
115
|
};
|
|
107
|
-
|
|
116
|
+
indexing?: {
|
|
117
|
+
maxSvgSizeBytes?: number;
|
|
118
|
+
};
|
|
119
|
+
}, keywordIndex: KeywordIndex | undefined, descriptionProvider: DescriptionProvider | undefined, logger: Logger, deferDescriptions?: boolean, descHash?: string): Promise<PreparedFile>;
|
|
108
120
|
/**
|
|
109
121
|
* Store the chunks of a prepared file into the vector store, attaching their
|
|
110
122
|
* computed embeddings.
|
|
@@ -139,6 +151,7 @@ export declare function processFile(file: WorkspaceFile, cwd: string, previous:
|
|
|
139
151
|
indexing: {
|
|
140
152
|
embedBatchSize: number;
|
|
141
153
|
embedConcurrency?: number;
|
|
154
|
+
maxSvgSizeBytes?: number;
|
|
142
155
|
};
|
|
143
156
|
chunking?: {
|
|
144
157
|
nodeTypes?: Record<string, string[]>;
|
|
@@ -146,5 +159,5 @@ export declare function processFile(file: WorkspaceFile, cwd: string, previous:
|
|
|
146
159
|
description?: {
|
|
147
160
|
maxContentChars?: number;
|
|
148
161
|
};
|
|
149
|
-
}, store: VectorStore, keywordIndex: KeywordIndex | undefined, embedder: EmbeddingProvider, descriptionProvider: DescriptionProvider | undefined, logger: Logger): Promise<WorkerResult>;
|
|
162
|
+
}, store: VectorStore, keywordIndex: KeywordIndex | undefined, embedder: EmbeddingProvider, descriptionProvider: DescriptionProvider | undefined, logger: Logger, descHash?: string): Promise<WorkerResult>;
|
|
150
163
|
export {};
|
package/dist/indexer/worker.js
CHANGED
|
@@ -52,9 +52,13 @@ export function buildTextsToEmbed(chunks, relPath, metaHeader, docPrefix, isImag
|
|
|
52
52
|
* @param deferDescriptions - When true, skip description generation and build
|
|
53
53
|
* fallback descriptions instead; descriptions are
|
|
54
54
|
* expected to be generated in a later global pass.
|
|
55
|
+
* @param descHash - Hash of the current description config. When the file
|
|
56
|
+
* is unchanged and `previous.descHash === descHash`,
|
|
57
|
+
* descriptions are skipped entirely (they already exist
|
|
58
|
+
* in the vector store from a prior run).
|
|
55
59
|
* @returns A prepared file descriptor ready for embedding and storage.
|
|
56
60
|
*/
|
|
57
|
-
export async function prepareFile(file, cwd, previous, config, keywordIndex, descriptionProvider, logger, deferDescriptions) {
|
|
61
|
+
export async function prepareFile(file, cwd, previous, config, keywordIndex, descriptionProvider, logger, deferDescriptions, descHash) {
|
|
58
62
|
const fileLabel = path.relative(cwd, file.filePath).replace(/\\/g, "/");
|
|
59
63
|
if (file.isEmpty) {
|
|
60
64
|
return {
|
|
@@ -77,14 +81,18 @@ export async function prepareFile(file, cwd, previous, config, keywordIndex, des
|
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
if (previous && previous.hash === file.hash) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
if (!descHash || previous.descHash === descHash) {
|
|
85
|
+
return {
|
|
86
|
+
normalizedPath: file.normalizedPath, hash: file.hash, fileLabel,
|
|
87
|
+
isModified: false,
|
|
88
|
+
earlyResult: {
|
|
89
|
+
normalizedPath: file.normalizedPath, hash: file.hash, chunkCount: 0, fileLabel,
|
|
90
|
+
isNew: false, isModified: false, isUnchanged: true, isEmpty: false, isTooSmall: false, isRemoved: false, hadChunks: false,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// File content is unchanged but description config changed — fall through to re-describe
|
|
95
|
+
logger.debug(` ${fileLabel} (unchanged but descHash differs — re-describing)`);
|
|
88
96
|
}
|
|
89
97
|
const isModified = !!previous;
|
|
90
98
|
let chunks;
|
|
@@ -102,7 +110,9 @@ export async function prepareFile(file, cwd, previous, config, keywordIndex, des
|
|
|
102
110
|
}];
|
|
103
111
|
}
|
|
104
112
|
else {
|
|
105
|
-
chunks = await chunkFile(file.filePath, file.content, config.chunking?.nodeTypes
|
|
113
|
+
chunks = await chunkFile(file.filePath, file.content, config.chunking?.nodeTypes, {
|
|
114
|
+
maxSvgSizeBytes: config.indexing?.maxSvgSizeBytes,
|
|
115
|
+
}).catch((err) => {
|
|
106
116
|
logger.warn(` ${fileLabel} (chunking failed: ${err.message})`);
|
|
107
117
|
return null;
|
|
108
118
|
});
|
|
@@ -152,6 +162,7 @@ export async function prepareFile(file, cwd, previous, config, keywordIndex, des
|
|
|
152
162
|
metaHeader,
|
|
153
163
|
docPrefix,
|
|
154
164
|
isImageFile: isImage,
|
|
165
|
+
descHash,
|
|
155
166
|
};
|
|
156
167
|
}
|
|
157
168
|
let descriptionFailed = false;
|
|
@@ -188,6 +199,7 @@ export async function prepareFile(file, cwd, previous, config, keywordIndex, des
|
|
|
188
199
|
chunks,
|
|
189
200
|
textToEmbed,
|
|
190
201
|
descriptionFailed,
|
|
202
|
+
descHash,
|
|
191
203
|
};
|
|
192
204
|
}
|
|
193
205
|
/**
|
|
@@ -235,6 +247,7 @@ export async function storeFileChunks(prep, embeddings, store, _logger) {
|
|
|
235
247
|
isRemoved: false,
|
|
236
248
|
hadChunks: prep.chunks.length > 0,
|
|
237
249
|
descriptionFailed: prep.descriptionFailed,
|
|
250
|
+
descHash: prep.descHash,
|
|
238
251
|
};
|
|
239
252
|
}
|
|
240
253
|
/**
|
|
@@ -253,8 +266,8 @@ export async function storeFileChunks(prep, embeddings, store, _logger) {
|
|
|
253
266
|
* @param logger - Logger for diagnostic messages.
|
|
254
267
|
* @returns A worker result describing the outcome.
|
|
255
268
|
*/
|
|
256
|
-
export async function processFile(file, cwd, previous, config, store, keywordIndex, embedder, descriptionProvider, logger) {
|
|
257
|
-
const prep = await prepareFile(file, cwd, previous, config, keywordIndex, descriptionProvider, logger);
|
|
269
|
+
export async function processFile(file, cwd, previous, config, store, keywordIndex, embedder, descriptionProvider, logger, descHash) {
|
|
270
|
+
const prep = await prepareFile(file, cwd, previous, config, keywordIndex, descriptionProvider, logger, false, descHash);
|
|
258
271
|
if (prep.earlyResult) {
|
|
259
272
|
if ((prep.earlyResult.isEmpty || prep.earlyResult.isTooSmall) && prep.earlyResult.isRemoved) {
|
|
260
273
|
await store.deleteByFilePath(prep.normalizedPath);
|