@jcode.labs/ragmir 2.16.2 → 2.17.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/README.md +37 -172
- package/dist/access-log.d.ts +1 -0
- package/dist/access-log.d.ts.map +1 -1
- package/dist/access-log.js +57 -24
- package/dist/access-log.js.map +1 -1
- package/dist/cli.js +15 -1
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/config.js +6 -14
- package/dist/config.js.map +1 -1
- package/dist/context-resources.d.ts +3 -3
- package/dist/context-resources.d.ts.map +1 -1
- package/dist/context-resources.js +13 -4
- package/dist/context-resources.js.map +1 -1
- package/dist/destroy.d.ts.map +1 -1
- package/dist/destroy.js +10 -6
- package/dist/destroy.js.map +1 -1
- package/dist/doctor.d.ts +2 -2
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +11 -3
- package/dist/doctor.js.map +1 -1
- package/dist/embeddings.d.ts +4 -0
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +68 -9
- package/dist/embeddings.js.map +1 -1
- package/dist/evaluate.d.ts +5 -0
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +50 -9
- package/dist/evaluate.js.map +1 -1
- package/dist/files.d.ts +3 -10
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +49 -15
- package/dist/files.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +2 -2
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +356 -163
- package/dist/ingest.js.map +1 -1
- package/dist/ingestion-state.d.ts +53 -0
- package/dist/ingestion-state.d.ts.map +1 -0
- package/dist/ingestion-state.js +257 -0
- package/dist/ingestion-state.js.map +1 -0
- package/dist/mcp-output.d.ts +14 -0
- package/dist/mcp-output.d.ts.map +1 -1
- package/dist/mcp-output.js +121 -0
- package/dist/mcp-output.js.map +1 -1
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +362 -116
- package/dist/mcp.js.map +1 -1
- package/dist/ocr.d.ts.map +1 -1
- package/dist/ocr.js +6 -12
- package/dist/ocr.js.map +1 -1
- package/dist/operation.d.ts.map +1 -1
- package/dist/operation.js +3 -2
- package/dist/operation.js.map +1 -1
- package/dist/parsing.d.ts +1 -0
- package/dist/parsing.d.ts.map +1 -1
- package/dist/parsing.js +86 -27
- package/dist/parsing.js.map +1 -1
- package/dist/project-config-file.d.ts +11 -0
- package/dist/project-config-file.d.ts.map +1 -0
- package/dist/project-config-file.js +55 -0
- package/dist/project-config-file.js.map +1 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +12 -4
- package/dist/query.js.map +1 -1
- package/dist/research.d.ts.map +1 -1
- package/dist/research.js +3 -2
- package/dist/research.js.map +1 -1
- package/dist/security.d.ts +2 -2
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +57 -21
- package/dist/security.js.map +1 -1
- package/dist/semantic-config.d.ts.map +1 -1
- package/dist/semantic-config.js +16 -20
- package/dist/semantic-config.js.map +1 -1
- package/dist/skill.d.ts +2 -13
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js.map +1 -1
- package/dist/sources.d.ts.map +1 -1
- package/dist/sources.js +24 -40
- package/dist/sources.js.map +1 -1
- package/dist/store.d.ts +5 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +45 -13
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +39 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -6
- package/skills/ragmir/SKILL.md +5 -3
- package/skills/ragmir-audio-summary/listening-style.md +11 -11
- package/skills/ragmir-audio-summary/narration-templates.md +22 -22
- package/skills/ragmir-audio-summary/split-lines.py +2 -1
package/dist/ingest.js
CHANGED
|
@@ -8,12 +8,14 @@ import { countSkippedByReason, inventorySourceFiles, summarizeUnsupportedExtensi
|
|
|
8
8
|
import { INDEX_SCHEMA_VERSION } from "./index-diagnostics.js";
|
|
9
9
|
import { indexPolicyFingerprint } from "./index-policy.js";
|
|
10
10
|
import { withIndexWriteLock } from "./index-write-lock.js";
|
|
11
|
+
import { canResumeIngestion, createIngestionRunState, finishIngestionState, generationTableName, ingestionProgress, readIngestionState, removeStagedIndexManifest, resumeIngestionState, updateIngestionFile, writeIngestionState, writeStagedIndexManifest, } from "./ingestion-state.js";
|
|
11
12
|
import { operationSignal, throwIfAborted } from "./operation.js";
|
|
12
13
|
import { parseFile } from "./parsing.js";
|
|
13
14
|
import { redactText, totalRedactions } from "./redaction.js";
|
|
14
|
-
import { openRowsTable, readEmptyTextFiles, readIndexManifest,
|
|
15
|
+
import { activeIndexTableName, dropRowsTable, openRowsTable, openRowsTableByName, readEmptyTextFiles, readIndexManifest, updateRowsInTable, writeEmptyTextFiles, writeIndexManifest, } from "./store.js";
|
|
15
16
|
import { VERSION } from "./version.js";
|
|
16
17
|
const MAX_SOURCE_DIAGNOSTIC_ITEMS = 20;
|
|
18
|
+
const DEFAULT_INGEST_FILE_BATCH_SIZE = 25;
|
|
17
19
|
const ARCHIVE_PATH_PATTERNS = [
|
|
18
20
|
/(^|[/_-])archive(s)?([/_-]|$)/iu,
|
|
19
21
|
/(^|[/_-])backup(s)?([/_-]|$)/iu,
|
|
@@ -37,77 +39,250 @@ export async function ingestWithConfig(config, options = {}, connection) {
|
|
|
37
39
|
return withIndexWriteLock(config.storageDir, signal, () => ingestUnlocked(config, options, connection, signal));
|
|
38
40
|
}
|
|
39
41
|
async function ingestUnlocked(config, options, connection, signal) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
existingManifest
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
file,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
42
|
+
let state = null;
|
|
43
|
+
try {
|
|
44
|
+
throwIfAborted(signal);
|
|
45
|
+
const requestedBatchSize = ingestFileBatchSize(options.batchSize);
|
|
46
|
+
const policyFingerprint = indexPolicyFingerprint(config);
|
|
47
|
+
const existingManifest = await readIndexManifest(config);
|
|
48
|
+
const storedState = await readIngestionState(config);
|
|
49
|
+
const storedEmptyFiles = await readEmptyTextFiles(config);
|
|
50
|
+
const inventory = await inventorySourceFiles(config, signal ? { signal } : {});
|
|
51
|
+
const files = inventory.supportedFiles;
|
|
52
|
+
const inventoryMetrics = sourceInventoryMetrics(files);
|
|
53
|
+
const currentFiles = new Map(files.map((file) => [file.relativePath, file]));
|
|
54
|
+
const existingTable = await openRowsTable(config, connection);
|
|
55
|
+
const manifestCompatible = !options.rebuild &&
|
|
56
|
+
existingManifest?.schemaVersion === INDEX_SCHEMA_VERSION &&
|
|
57
|
+
existingManifest.indexPolicyFingerprint === policyFingerprint &&
|
|
58
|
+
existingManifest.indexedFiles !== undefined;
|
|
59
|
+
const policyRebuild = !options.rebuild &&
|
|
60
|
+
existingManifest !== null &&
|
|
61
|
+
(existingManifest.schemaVersion !== INDEX_SCHEMA_VERSION ||
|
|
62
|
+
existingManifest.indexPolicyFingerprint !== policyFingerprint);
|
|
63
|
+
const canReuse = manifestCompatible && existingTable !== null;
|
|
64
|
+
const previousIndexedFiles = canReuse ? (existingManifest.indexedFiles ?? []) : [];
|
|
65
|
+
const previousEmptyFiles = canReuse ? storedEmptyFiles : [];
|
|
66
|
+
const reusableIndexedFiles = previousIndexedFiles.filter((file) => currentFiles.get(file.relativePath)?.checksum === file.checksum);
|
|
67
|
+
const reusableEmptyFiles = previousEmptyFiles.filter((file) => currentFiles.get(file.relativePath)?.checksum === file.checksum);
|
|
68
|
+
const reusablePaths = new Set([
|
|
69
|
+
...reusableIndexedFiles.map((file) => file.relativePath),
|
|
70
|
+
...reusableEmptyFiles.map((file) => file.relativePath),
|
|
71
|
+
]);
|
|
72
|
+
const activeTableName = await activeIndexTableName(config);
|
|
73
|
+
const resumableTable = storedState
|
|
74
|
+
? await openRowsTableByName(storedState.tableName, config, connection)
|
|
75
|
+
: null;
|
|
76
|
+
const resumableTableAvailable = !storedState?.files.some((file) => file.state === "indexed" && file.chunkCount > 0) ||
|
|
77
|
+
resumableTable !== null;
|
|
78
|
+
if (storedState &&
|
|
79
|
+
resumableTableAvailable &&
|
|
80
|
+
canResumeIngestion(storedState, files, policyFingerprint, options.rebuild === true)) {
|
|
81
|
+
state = resumeIngestionState(await reconcileCommittedFiles(storedState, config, connection));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
if (storedState?.mode === "rebuild" &&
|
|
85
|
+
storedState.tableName !== activeTableName &&
|
|
86
|
+
storedState.status !== "completed" &&
|
|
87
|
+
storedState.status !== "completed_with_errors") {
|
|
88
|
+
await dropRowsTable(storedState.tableName, config, connection);
|
|
89
|
+
await removeStagedIndexManifest(storedState.runId, config);
|
|
90
|
+
}
|
|
91
|
+
const mode = options.rebuild || (existingTable !== null && !manifestCompatible)
|
|
92
|
+
? "rebuild"
|
|
93
|
+
: "incremental";
|
|
94
|
+
const reusableChunkCounts = new Map([
|
|
95
|
+
...reusableIndexedFiles.map((file) => [file.relativePath, file.chunkCount]),
|
|
96
|
+
...reusableEmptyFiles.map((file) => [file.relativePath, 0]),
|
|
97
|
+
]);
|
|
98
|
+
state = createIngestionRunState({
|
|
99
|
+
mode,
|
|
100
|
+
tableName: activeTableName,
|
|
101
|
+
previousTableName: mode === "rebuild" ? activeTableName : null,
|
|
102
|
+
policyFingerprint,
|
|
103
|
+
batchSize: requestedBatchSize,
|
|
104
|
+
files,
|
|
105
|
+
reusablePaths: mode === "incremental" ? reusablePaths : new Set(),
|
|
106
|
+
reusableChunkCounts,
|
|
107
|
+
});
|
|
108
|
+
if (mode === "rebuild") {
|
|
109
|
+
state = {
|
|
110
|
+
...state,
|
|
111
|
+
tableName: generationTableName(config.tableName, state.runId),
|
|
112
|
+
files: state.files.map((file) => ({
|
|
113
|
+
...file,
|
|
114
|
+
state: "pending",
|
|
115
|
+
chunkCount: 0,
|
|
116
|
+
reused: false,
|
|
117
|
+
})),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
82
120
|
}
|
|
83
|
-
|
|
121
|
+
await persistIngestionProgress(state, config, options);
|
|
122
|
+
throwIfAborted(signal);
|
|
123
|
+
const previousPaths = new Set(previousIndexedFiles.map((file) => file.relativePath));
|
|
124
|
+
const currentPaths = new Set(files.map((file) => file.relativePath));
|
|
125
|
+
const removedPaths = state.mode === "incremental"
|
|
126
|
+
? [...previousPaths].filter((relativePath) => !currentPaths.has(relativePath))
|
|
127
|
+
: [];
|
|
128
|
+
const pendingFiles = state.files
|
|
129
|
+
.filter((file) => file.state !== "indexed")
|
|
130
|
+
.flatMap((file) => {
|
|
131
|
+
const source = currentFiles.get(file.relativePath);
|
|
132
|
+
return source ? [source] : [];
|
|
133
|
+
});
|
|
134
|
+
let removalApplied = false;
|
|
135
|
+
let lexicalIndexWarning = null;
|
|
136
|
+
for (const fileBatch of valueBatches(pendingFiles, state.batchSize)) {
|
|
137
|
+
throwIfAborted(signal);
|
|
138
|
+
const parsedBatch = await mapLimit(fileBatch, config.ingestConcurrency, signal, async (file) => parseSourceFile(file, config, signal));
|
|
139
|
+
for (const parsed of parsedBatch) {
|
|
140
|
+
state = updateIngestionFile(state, parsed.file.relativePath, parsed.error
|
|
141
|
+
? { state: "error", chunkCount: 0, redactions: 0, error: parsed.error }
|
|
142
|
+
: {
|
|
143
|
+
state: "parsed",
|
|
144
|
+
chunkCount: parsed.chunks.length,
|
|
145
|
+
redactions: parsed.redactions,
|
|
146
|
+
error: null,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
await persistIngestionProgress(state, config, options);
|
|
150
|
+
throwIfAborted(signal);
|
|
151
|
+
const successfulFiles = parsedBatch.filter((parsed) => parsed.error === null);
|
|
152
|
+
const allChunks = successfulFiles.flatMap((parsed) => parsed.chunks);
|
|
153
|
+
const rows = await vectorRowsForChunks(allChunks, config, signal);
|
|
154
|
+
for (const parsed of successfulFiles) {
|
|
155
|
+
state = updateIngestionFile(state, parsed.file.relativePath, { state: "embedded" });
|
|
156
|
+
}
|
|
157
|
+
await persistIngestionProgress(state, config, options);
|
|
158
|
+
throwIfAborted(signal);
|
|
159
|
+
const replacePaths = [
|
|
160
|
+
...fileBatch.map((file) => file.relativePath),
|
|
161
|
+
...(!removalApplied ? removedPaths : []),
|
|
162
|
+
];
|
|
163
|
+
const writeResult = await updateRowsInTable(rows, replacePaths, state.tableName, config, connection);
|
|
164
|
+
removalApplied = true;
|
|
165
|
+
lexicalIndexWarning ??= writeResult.lexicalIndexWarning;
|
|
166
|
+
for (const parsed of successfulFiles) {
|
|
167
|
+
state = updateIngestionFile(state, parsed.file.relativePath, { state: "indexed" });
|
|
168
|
+
}
|
|
169
|
+
await writeProgressManifest(state, config, connection);
|
|
170
|
+
await persistIngestionProgress(state, config, options);
|
|
84
171
|
throwIfAborted(signal);
|
|
85
|
-
return {
|
|
86
|
-
path: file.relativePath,
|
|
87
|
-
chunks: [],
|
|
88
|
-
redactions: [],
|
|
89
|
-
error: {
|
|
90
|
-
path: file.relativePath,
|
|
91
|
-
message: error instanceof Error ? error.message : String(error),
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
172
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
errors.push(result.error);
|
|
99
|
-
continue;
|
|
173
|
+
if (!removalApplied && removedPaths.length > 0) {
|
|
174
|
+
const writeResult = await updateRowsInTable([], removedPaths, state.tableName, config, connection);
|
|
175
|
+
lexicalIndexWarning ??= writeResult.lexicalIndexWarning;
|
|
100
176
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
177
|
+
const manifest = await manifestForState(state, config, connection);
|
|
178
|
+
await validateIngestionTable(state, manifest, config, connection);
|
|
179
|
+
await writeEmptyTextFiles(emptyTextRecords(state), config);
|
|
180
|
+
await writeIndexManifest(manifest, config);
|
|
181
|
+
await removeStagedIndexManifest(state.runId, config);
|
|
182
|
+
const errors = ingestionErrors(state);
|
|
183
|
+
state = finishIngestionState(state, errors.length > 0 ? "completed_with_errors" : "completed");
|
|
184
|
+
await persistIngestionProgress(state, config, options);
|
|
185
|
+
const indexedFiles = manifest.indexedFiles ?? [];
|
|
186
|
+
const emptyTextFiles = emptyTextRecords(state).map((file) => file.relativePath);
|
|
187
|
+
const redactions = state.files.reduce((sum, file) => sum + file.redactions, 0);
|
|
188
|
+
await recordAccess(config, {
|
|
189
|
+
action: "ingest",
|
|
190
|
+
resultCount: manifest.chunkCount,
|
|
191
|
+
redactions,
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
runId: state.runId,
|
|
195
|
+
resumed: state.resumed,
|
|
196
|
+
batchSize: state.batchSize,
|
|
197
|
+
indexedFiles: indexedFiles.length,
|
|
198
|
+
rebuiltFiles: state.files.filter((file) => file.state === "indexed" && file.chunkCount > 0 && !file.reused).length,
|
|
199
|
+
reusedFiles: state.files.filter((file) => file.reused).length,
|
|
200
|
+
policyRebuild,
|
|
201
|
+
chunks: manifest.chunkCount,
|
|
202
|
+
discoveredFiles: inventory.discoveredFiles,
|
|
203
|
+
supportedFiles: files.length,
|
|
204
|
+
supportedBytes: inventoryMetrics.supportedBytes,
|
|
205
|
+
largestFileBytes: inventoryMetrics.largestFileBytes,
|
|
206
|
+
skippedFiles: inventory.skippedFiles.length + emptyTextFiles.length,
|
|
207
|
+
unsupportedFiles: countSkippedByReason(inventory.skippedFiles, "unsupported-extension"),
|
|
208
|
+
oversizedFiles: countSkippedByReason(inventory.skippedFiles, "oversized"),
|
|
209
|
+
sensitiveFiles: countSkippedByReason(inventory.skippedFiles, "sensitive-name"),
|
|
210
|
+
emptyTextFiles,
|
|
211
|
+
unsupportedExtensions: summarizeUnsupportedExtensions(inventory.skippedFiles),
|
|
212
|
+
redactions,
|
|
213
|
+
vectorIndexWarning: null,
|
|
214
|
+
lexicalIndexWarning,
|
|
215
|
+
errors,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (state?.status === "running") {
|
|
220
|
+
state = finishIngestionState(state, signal?.aborted ? "interrupted" : "failed");
|
|
221
|
+
try {
|
|
222
|
+
await writeIngestionState(state, config);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Preserve the original ingestion failure.
|
|
226
|
+
}
|
|
104
227
|
}
|
|
105
|
-
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function reconcileCommittedFiles(state, config, connection) {
|
|
232
|
+
const filesToReconcile = state.files.filter((file) => file.state !== "indexed" && file.state !== "error");
|
|
233
|
+
if (filesToReconcile.length === 0) {
|
|
234
|
+
return state;
|
|
235
|
+
}
|
|
236
|
+
const table = await openRowsTableByName(state.tableName, config, connection);
|
|
237
|
+
const rows = table
|
|
238
|
+
? (await table.query().select(["relativePath", "checksum"]).toArray())
|
|
239
|
+
: [];
|
|
240
|
+
const rowCounts = new Map();
|
|
241
|
+
for (const row of rows) {
|
|
242
|
+
const key = `${row.relativePath}\0${row.checksum}`;
|
|
243
|
+
rowCounts.set(key, (rowCounts.get(key) ?? 0) + 1);
|
|
244
|
+
}
|
|
245
|
+
const committedEmptyFiles = state.mode === "incremental"
|
|
246
|
+
? new Set((await readEmptyTextFiles(config)).map((file) => `${file.relativePath}\0${file.checksum}`))
|
|
247
|
+
: new Set();
|
|
248
|
+
return {
|
|
249
|
+
...state,
|
|
250
|
+
files: state.files.map((file) => {
|
|
251
|
+
if (file.state === "indexed" || file.state === "error") {
|
|
252
|
+
return file;
|
|
253
|
+
}
|
|
254
|
+
const key = `${file.relativePath}\0${file.checksum}`;
|
|
255
|
+
const committed = file.chunkCount > 0 ? rowCounts.get(key) === file.chunkCount : committedEmptyFiles.has(key);
|
|
256
|
+
return committed ? { ...file, state: "indexed", error: null } : file;
|
|
257
|
+
}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
async function parseSourceFile(file, config, signal) {
|
|
261
|
+
try {
|
|
262
|
+
const parsed = await parseFile(file, { ...config, ...(signal ? { signal } : {}) });
|
|
263
|
+
throwIfAborted(signal);
|
|
264
|
+
const redacted = redactText(parsed.text, config);
|
|
265
|
+
return {
|
|
266
|
+
file,
|
|
267
|
+
chunks: chunkDocument({ ...parsed, text: redacted.text }, config.chunkSize, config.chunkOverlap),
|
|
268
|
+
redactions: totalRedactions(redacted.counts),
|
|
269
|
+
error: null,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
throwIfAborted(signal);
|
|
274
|
+
return {
|
|
275
|
+
file,
|
|
276
|
+
chunks: [],
|
|
277
|
+
redactions: 0,
|
|
278
|
+
error: error instanceof Error ? error.message : String(error),
|
|
279
|
+
};
|
|
106
280
|
}
|
|
281
|
+
}
|
|
282
|
+
async function vectorRowsForChunks(chunks, config, signal) {
|
|
107
283
|
const rows = [];
|
|
108
|
-
for (
|
|
284
|
+
for (const batch of valueBatches(chunks, config.embeddingBatchSize)) {
|
|
109
285
|
throwIfAborted(signal);
|
|
110
|
-
const batch = allChunks.slice(i, i + config.embeddingBatchSize);
|
|
111
286
|
const embeddings = await embedTexts(batch.map(chunkSearchText), config);
|
|
112
287
|
throwIfAborted(signal);
|
|
113
288
|
for (const [index, chunk] of batch.entries()) {
|
|
@@ -124,118 +299,129 @@ async function ingestUnlocked(config, options, connection, signal) {
|
|
|
124
299
|
});
|
|
125
300
|
}
|
|
126
301
|
}
|
|
127
|
-
|
|
128
|
-
|
|
302
|
+
return rows;
|
|
303
|
+
}
|
|
304
|
+
async function persistIngestionProgress(state, config, options) {
|
|
305
|
+
await writeIngestionState(state, config);
|
|
306
|
+
await options.onProgress?.(ingestionProgress(state));
|
|
307
|
+
}
|
|
308
|
+
async function writeProgressManifest(state, config, connection) {
|
|
309
|
+
const manifest = await manifestForState(state, config, connection);
|
|
310
|
+
if (state.mode === "rebuild") {
|
|
311
|
+
await writeStagedIndexManifest(manifest, state.runId, config);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
await writeEmptyTextFiles(emptyTextRecords(state), config);
|
|
315
|
+
await writeIndexManifest(manifest, config);
|
|
316
|
+
}
|
|
317
|
+
async function manifestForState(state, config, connection) {
|
|
318
|
+
const indexedFiles = indexedFilesFromState(state);
|
|
129
319
|
const chunkCount = indexedFiles.reduce((sum, file) => sum + file.chunkCount, 0);
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
...[...previousPaths].filter((relativePath) => !currentPaths.has(relativePath)),
|
|
135
|
-
];
|
|
136
|
-
throwIfAborted(signal);
|
|
137
|
-
const writeResult = !canReuse || chunkCount === 0
|
|
138
|
-
? await writeRows(rows, config, connection)
|
|
139
|
-
: replacePaths.length > 0
|
|
140
|
-
? await updateRows(rows, replacePaths, config, connection)
|
|
141
|
-
: { vectorIndexWarning: null, lexicalIndexWarning: null };
|
|
142
|
-
if (chunkCount > 0) {
|
|
143
|
-
const firstRow = rows[0] ?? (await firstStoredRow(config, connection));
|
|
144
|
-
if (!firstRow) {
|
|
145
|
-
throw new Error("Cannot write an index manifest without indexed rows.");
|
|
146
|
-
}
|
|
147
|
-
await writeIndexManifest({
|
|
148
|
-
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
149
|
-
createdAt: new Date().toISOString(),
|
|
150
|
-
ragmirVersion: VERSION,
|
|
151
|
-
embeddingProvider: config.embeddingProvider,
|
|
152
|
-
embeddingModel: config.embeddingModel,
|
|
153
|
-
indexPolicyFingerprint: policyFingerprint,
|
|
154
|
-
vectorDimension: firstRow.vector.length,
|
|
155
|
-
vectorDistanceMetric: VECTOR_DISTANCE_METRIC,
|
|
156
|
-
chunkSize: config.chunkSize,
|
|
157
|
-
chunkOverlap: config.chunkOverlap,
|
|
158
|
-
fileCount: indexedFiles.length,
|
|
159
|
-
chunkCount,
|
|
160
|
-
indexedFiles,
|
|
161
|
-
}, config);
|
|
320
|
+
const table = await openRowsTableByName(state.tableName, config, connection);
|
|
321
|
+
const [firstRow] = table ? (await table.query().limit(1).toArray()) : [];
|
|
322
|
+
if (chunkCount > 0 && !firstRow) {
|
|
323
|
+
throw new Error("Cannot write an index manifest without indexed rows.");
|
|
162
324
|
}
|
|
163
|
-
await writeEmptyTextFiles([
|
|
164
|
-
...reusableEmptyFiles,
|
|
165
|
-
...emptyTextFiles.flatMap((relativePath) => {
|
|
166
|
-
const file = currentFiles.get(relativePath);
|
|
167
|
-
return file
|
|
168
|
-
? [
|
|
169
|
-
{
|
|
170
|
-
relativePath,
|
|
171
|
-
checksum: file.checksum,
|
|
172
|
-
bytes: file.bytes,
|
|
173
|
-
mtimeMs: file.mtimeMs,
|
|
174
|
-
},
|
|
175
|
-
]
|
|
176
|
-
: [];
|
|
177
|
-
}),
|
|
178
|
-
], config);
|
|
179
|
-
await recordAccess(config, {
|
|
180
|
-
action: "ingest",
|
|
181
|
-
resultCount: chunkCount,
|
|
182
|
-
redactions: totalRedactions(redactionCounts),
|
|
183
|
-
});
|
|
184
325
|
return {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
...reusableEmptyFiles.map((file) => file.relativePath),
|
|
200
|
-
...emptyTextFiles,
|
|
201
|
-
].sort(),
|
|
202
|
-
unsupportedExtensions: summarizeUnsupportedExtensions(inventory.skippedFiles),
|
|
203
|
-
redactions: totalRedactions(redactionCounts),
|
|
204
|
-
vectorIndexWarning: writeResult.vectorIndexWarning,
|
|
205
|
-
lexicalIndexWarning: writeResult.lexicalIndexWarning,
|
|
206
|
-
errors,
|
|
326
|
+
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
327
|
+
createdAt: new Date().toISOString(),
|
|
328
|
+
ragmirVersion: VERSION,
|
|
329
|
+
embeddingProvider: config.embeddingProvider,
|
|
330
|
+
embeddingModel: config.embeddingModel,
|
|
331
|
+
indexPolicyFingerprint: state.policyFingerprint,
|
|
332
|
+
...(firstRow ? { vectorDimension: firstRow.vector.length } : {}),
|
|
333
|
+
vectorDistanceMetric: VECTOR_DISTANCE_METRIC,
|
|
334
|
+
chunkSize: config.chunkSize,
|
|
335
|
+
chunkOverlap: config.chunkOverlap,
|
|
336
|
+
fileCount: indexedFiles.length,
|
|
337
|
+
chunkCount,
|
|
338
|
+
tableName: state.tableName,
|
|
339
|
+
indexedFiles,
|
|
207
340
|
};
|
|
208
341
|
}
|
|
209
|
-
function
|
|
210
|
-
|
|
342
|
+
function indexedFilesFromState(state) {
|
|
343
|
+
return state.files
|
|
344
|
+
.filter((file) => file.state === "indexed" && file.chunkCount > 0)
|
|
345
|
+
.map((file) => ({
|
|
346
|
+
relativePath: file.relativePath,
|
|
347
|
+
checksum: file.checksum,
|
|
348
|
+
chunkCount: file.chunkCount,
|
|
349
|
+
bytes: file.bytes,
|
|
350
|
+
mtimeMs: file.mtimeMs,
|
|
351
|
+
}))
|
|
352
|
+
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
353
|
+
}
|
|
354
|
+
function emptyTextRecords(state) {
|
|
355
|
+
return state.files
|
|
356
|
+
.filter((file) => file.state === "indexed" && file.chunkCount === 0)
|
|
357
|
+
.map((file) => ({
|
|
358
|
+
relativePath: file.relativePath,
|
|
359
|
+
checksum: file.checksum,
|
|
360
|
+
bytes: file.bytes,
|
|
361
|
+
mtimeMs: file.mtimeMs,
|
|
362
|
+
}))
|
|
363
|
+
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
364
|
+
}
|
|
365
|
+
function ingestionErrors(state) {
|
|
366
|
+
return state.files.flatMap((file) => file.state === "error" && file.error ? [{ path: file.relativePath, message: file.error }] : []);
|
|
367
|
+
}
|
|
368
|
+
async function validateIngestionTable(state, manifest, config, connection) {
|
|
369
|
+
const table = await openRowsTableByName(state.tableName, config, connection);
|
|
370
|
+
if (!table) {
|
|
371
|
+
if (manifest.chunkCount === 0) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
throw new Error("Ingestion validation failed because the generated table is missing.");
|
|
375
|
+
}
|
|
376
|
+
const rows = (await table.query().select(["id", "relativePath", "checksum"]).toArray());
|
|
377
|
+
if (rows.length !== manifest.chunkCount) {
|
|
378
|
+
throw new Error(`Ingestion validation expected ${manifest.chunkCount} rows but found ${rows.length}.`);
|
|
379
|
+
}
|
|
380
|
+
if (new Set(rows.map((row) => row.id)).size !== rows.length) {
|
|
381
|
+
throw new Error("Ingestion validation found duplicate chunk identifiers.");
|
|
382
|
+
}
|
|
383
|
+
const expectedFiles = new Map((manifest.indexedFiles ?? []).map((file) => [
|
|
384
|
+
`${file.relativePath}\0${file.checksum}`,
|
|
385
|
+
file.chunkCount,
|
|
386
|
+
]));
|
|
387
|
+
const actualFiles = new Map();
|
|
211
388
|
for (const row of rows) {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
mtimeMs: row.mtimeMs,
|
|
219
|
-
});
|
|
389
|
+
const key = `${row.relativePath}\0${row.checksum}`;
|
|
390
|
+
actualFiles.set(key, (actualFiles.get(key) ?? 0) + 1);
|
|
391
|
+
}
|
|
392
|
+
if (expectedFiles.size !== actualFiles.size ||
|
|
393
|
+
[...expectedFiles].some(([key, count]) => actualFiles.get(key) !== count)) {
|
|
394
|
+
throw new Error("Ingestion validation found rows that do not match the generated manifest.");
|
|
220
395
|
}
|
|
221
|
-
return [...records.values()];
|
|
222
396
|
}
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
if (!
|
|
226
|
-
|
|
397
|
+
function ingestFileBatchSize(value) {
|
|
398
|
+
const batchSize = value ?? DEFAULT_INGEST_FILE_BATCH_SIZE;
|
|
399
|
+
if (!Number.isInteger(batchSize) || batchSize <= 0) {
|
|
400
|
+
throw new Error("batchSize must be a positive integer.");
|
|
401
|
+
}
|
|
402
|
+
return batchSize;
|
|
403
|
+
}
|
|
404
|
+
function valueBatches(values, batchSize) {
|
|
405
|
+
const batches = [];
|
|
406
|
+
for (let index = 0; index < values.length; index += batchSize) {
|
|
407
|
+
batches.push(values.slice(index, index + batchSize));
|
|
227
408
|
}
|
|
228
|
-
|
|
229
|
-
return row ?? null;
|
|
409
|
+
return batches;
|
|
230
410
|
}
|
|
231
|
-
export async function audit(cwd = process.cwd()) {
|
|
411
|
+
export async function audit(cwd = process.cwd(), options = {}) {
|
|
412
|
+
const signal = operationSignal(options);
|
|
413
|
+
throwIfAborted(signal);
|
|
232
414
|
const config = await loadConfig(cwd);
|
|
233
|
-
|
|
415
|
+
throwIfAborted(signal);
|
|
416
|
+
const inventory = await inventorySourceFiles(config, signal ? { signal } : {});
|
|
417
|
+
throwIfAborted(signal);
|
|
234
418
|
const files = inventory.supportedFiles;
|
|
235
419
|
const inventoryMetrics = sourceInventoryMetrics(files);
|
|
236
420
|
const supportedFiles = files.map((file) => file.relativePath);
|
|
237
421
|
const table = await openRowsTable(config);
|
|
238
|
-
|
|
422
|
+
throwIfAborted(signal);
|
|
423
|
+
const emptyTextFiles = await currentEmptyTextFiles(config, files, signal);
|
|
424
|
+
throwIfAborted(signal);
|
|
239
425
|
if (!table) {
|
|
240
426
|
return {
|
|
241
427
|
discoveredFiles: inventory.discoveredFiles,
|
|
@@ -257,9 +443,11 @@ export async function audit(cwd = process.cwd()) {
|
|
|
257
443
|
.query()
|
|
258
444
|
.select(["relativePath", "checksum", "contextPath", "text"])
|
|
259
445
|
.toArray());
|
|
446
|
+
throwIfAborted(signal);
|
|
260
447
|
const counts = new Map();
|
|
261
448
|
const checksums = new Map();
|
|
262
449
|
for (const row of rows) {
|
|
450
|
+
throwIfAborted(signal);
|
|
263
451
|
counts.set(row.relativePath, (counts.get(row.relativePath) ?? 0) + 1);
|
|
264
452
|
if (row.checksum) {
|
|
265
453
|
const fileChecksums = checksums.get(row.relativePath) ?? new Set();
|
|
@@ -270,6 +458,7 @@ export async function audit(cwd = process.cwd()) {
|
|
|
270
458
|
const supportedSet = new Set(supportedFiles);
|
|
271
459
|
const indexedSet = new Set(counts.keys());
|
|
272
460
|
const currentChecksums = new Map(files.map((file) => [file.relativePath, file.checksum]));
|
|
461
|
+
throwIfAborted(signal);
|
|
273
462
|
return {
|
|
274
463
|
discoveredFiles: inventory.discoveredFiles,
|
|
275
464
|
supportedBytes: inventoryMetrics.supportedBytes,
|
|
@@ -343,10 +532,14 @@ function groupedDuplicates(groups) {
|
|
|
343
532
|
.filter(([, files]) => files.length > 1)
|
|
344
533
|
.map(([key, files]) => ({ key, files: [...new Set(files)].sort() }));
|
|
345
534
|
}
|
|
346
|
-
async function currentEmptyTextFiles(config, files) {
|
|
535
|
+
async function currentEmptyTextFiles(config, files, signal) {
|
|
536
|
+
throwIfAborted(signal);
|
|
347
537
|
const currentChecksums = new Map(files.map((file) => [file.relativePath, file.checksum]));
|
|
348
538
|
const emptyTextFiles = new Set();
|
|
349
|
-
|
|
539
|
+
const records = await readEmptyTextFiles(config);
|
|
540
|
+
throwIfAborted(signal);
|
|
541
|
+
for (const record of records) {
|
|
542
|
+
throwIfAborted(signal);
|
|
350
543
|
if (currentChecksums.get(record.relativePath) === record.checksum) {
|
|
351
544
|
emptyTextFiles.add(record.relativePath);
|
|
352
545
|
}
|