@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.
Files changed (102) hide show
  1. package/README.md +37 -172
  2. package/dist/access-log.d.ts +1 -0
  3. package/dist/access-log.d.ts.map +1 -1
  4. package/dist/access-log.js +57 -24
  5. package/dist/access-log.js.map +1 -1
  6. package/dist/cli.js +15 -1
  7. package/dist/cli.js.map +1 -1
  8. package/dist/client.d.ts +3 -3
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +4 -4
  11. package/dist/client.js.map +1 -1
  12. package/dist/config.js +6 -14
  13. package/dist/config.js.map +1 -1
  14. package/dist/context-resources.d.ts +3 -3
  15. package/dist/context-resources.d.ts.map +1 -1
  16. package/dist/context-resources.js +13 -4
  17. package/dist/context-resources.js.map +1 -1
  18. package/dist/destroy.d.ts.map +1 -1
  19. package/dist/destroy.js +10 -6
  20. package/dist/destroy.js.map +1 -1
  21. package/dist/doctor.d.ts +2 -2
  22. package/dist/doctor.d.ts.map +1 -1
  23. package/dist/doctor.js +11 -3
  24. package/dist/doctor.js.map +1 -1
  25. package/dist/embeddings.d.ts +4 -0
  26. package/dist/embeddings.d.ts.map +1 -1
  27. package/dist/embeddings.js +68 -9
  28. package/dist/embeddings.js.map +1 -1
  29. package/dist/evaluate.d.ts +5 -0
  30. package/dist/evaluate.d.ts.map +1 -1
  31. package/dist/evaluate.js +50 -9
  32. package/dist/evaluate.js.map +1 -1
  33. package/dist/files.d.ts +3 -10
  34. package/dist/files.d.ts.map +1 -1
  35. package/dist/files.js +49 -15
  36. package/dist/files.js.map +1 -1
  37. package/dist/index.d.ts +2 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/ingest.d.ts +2 -2
  42. package/dist/ingest.d.ts.map +1 -1
  43. package/dist/ingest.js +356 -163
  44. package/dist/ingest.js.map +1 -1
  45. package/dist/ingestion-state.d.ts +53 -0
  46. package/dist/ingestion-state.d.ts.map +1 -0
  47. package/dist/ingestion-state.js +257 -0
  48. package/dist/ingestion-state.js.map +1 -0
  49. package/dist/mcp-output.d.ts +14 -0
  50. package/dist/mcp-output.d.ts.map +1 -1
  51. package/dist/mcp-output.js +121 -0
  52. package/dist/mcp-output.js.map +1 -1
  53. package/dist/mcp.d.ts +8 -0
  54. package/dist/mcp.d.ts.map +1 -1
  55. package/dist/mcp.js +362 -116
  56. package/dist/mcp.js.map +1 -1
  57. package/dist/ocr.d.ts.map +1 -1
  58. package/dist/ocr.js +6 -12
  59. package/dist/ocr.js.map +1 -1
  60. package/dist/operation.d.ts.map +1 -1
  61. package/dist/operation.js +3 -2
  62. package/dist/operation.js.map +1 -1
  63. package/dist/parsing.d.ts +1 -0
  64. package/dist/parsing.d.ts.map +1 -1
  65. package/dist/parsing.js +86 -27
  66. package/dist/parsing.js.map +1 -1
  67. package/dist/project-config-file.d.ts +11 -0
  68. package/dist/project-config-file.d.ts.map +1 -0
  69. package/dist/project-config-file.js +55 -0
  70. package/dist/project-config-file.js.map +1 -0
  71. package/dist/query.d.ts.map +1 -1
  72. package/dist/query.js +12 -4
  73. package/dist/query.js.map +1 -1
  74. package/dist/research.d.ts.map +1 -1
  75. package/dist/research.js +3 -2
  76. package/dist/research.js.map +1 -1
  77. package/dist/security.d.ts +2 -2
  78. package/dist/security.d.ts.map +1 -1
  79. package/dist/security.js +57 -21
  80. package/dist/security.js.map +1 -1
  81. package/dist/semantic-config.d.ts.map +1 -1
  82. package/dist/semantic-config.js +16 -20
  83. package/dist/semantic-config.js.map +1 -1
  84. package/dist/skill.d.ts +2 -13
  85. package/dist/skill.d.ts.map +1 -1
  86. package/dist/skill.js.map +1 -1
  87. package/dist/sources.d.ts.map +1 -1
  88. package/dist/sources.js +24 -40
  89. package/dist/sources.js.map +1 -1
  90. package/dist/store.d.ts +5 -0
  91. package/dist/store.d.ts.map +1 -1
  92. package/dist/store.js +45 -13
  93. package/dist/store.js.map +1 -1
  94. package/dist/types.d.ts +39 -3
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/version.d.ts +1 -1
  97. package/dist/version.js +1 -1
  98. package/package.json +7 -6
  99. package/skills/ragmir/SKILL.md +5 -3
  100. package/skills/ragmir-audio-summary/listening-style.md +11 -11
  101. package/skills/ragmir-audio-summary/narration-templates.md +22 -22
  102. 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, updateRows, writeEmptyTextFiles, writeIndexManifest, writeRows, } from "./store.js";
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
- throwIfAborted(signal);
41
- const policyFingerprint = indexPolicyFingerprint(config);
42
- const existingManifest = await readIndexManifest(config);
43
- const manifestCompatible = !options.rebuild &&
44
- existingManifest?.schemaVersion === INDEX_SCHEMA_VERSION &&
45
- existingManifest.indexPolicyFingerprint === policyFingerprint &&
46
- existingManifest.indexedFiles !== undefined;
47
- const existingTable = manifestCompatible ? await openRowsTable(config, connection) : null;
48
- const storedEmptyFiles = manifestCompatible ? await readEmptyTextFiles(config) : [];
49
- const knownFiles = new Map([...(existingManifest?.indexedFiles ?? []), ...storedEmptyFiles].map((file) => [
50
- file.relativePath,
51
- file,
52
- ]));
53
- const inventory = await inventorySourceFiles(config, { knownFiles });
54
- const files = inventory.supportedFiles;
55
- const inventoryMetrics = sourceInventoryMetrics(files);
56
- const currentFiles = new Map(files.map((file) => [file.relativePath, file]));
57
- const policyRebuild = !options.rebuild &&
58
- existingManifest !== null &&
59
- (existingManifest.schemaVersion !== INDEX_SCHEMA_VERSION ||
60
- existingManifest.indexPolicyFingerprint !== policyFingerprint);
61
- const canReuse = manifestCompatible && existingTable !== null;
62
- const previousIndexedFiles = canReuse ? (existingManifest.indexedFiles ?? []) : [];
63
- const previousEmptyFiles = canReuse ? storedEmptyFiles : [];
64
- const reusableIndexedFiles = previousIndexedFiles.filter((file) => currentFiles.get(file.relativePath)?.checksum === file.checksum);
65
- const reusableEmptyFiles = previousEmptyFiles.filter((file) => currentFiles.get(file.relativePath)?.checksum === file.checksum);
66
- const reusableFiles = new Set([
67
- ...reusableIndexedFiles.map((file) => file.relativePath),
68
- ...reusableEmptyFiles.map((file) => file.relativePath),
69
- ]);
70
- const filesToIndex = files.filter((file) => !reusableFiles.has(file.relativePath));
71
- const allChunks = [];
72
- const errors = [];
73
- const redactionCounts = [];
74
- const emptyTextFiles = [];
75
- const results = await mapLimit(filesToIndex, config.ingestConcurrency, signal, async (file) => {
76
- try {
77
- const parsed = await parseFile(file, config);
78
- throwIfAborted(signal);
79
- const redacted = redactText(parsed.text, config);
80
- const chunks = chunkDocument({ ...parsed, text: redacted.text }, config.chunkSize, config.chunkOverlap);
81
- return { path: file.relativePath, chunks, redactions: redacted.counts, error: null };
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
- catch (error) {
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
- for (const result of results) {
97
- if (result.error) {
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
- redactionCounts.push(...result.redactions);
102
- if (result.chunks.length === 0) {
103
- emptyTextFiles.push(result.path);
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
- allChunks.push(...result.chunks);
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 (let i = 0; i < allChunks.length; i += config.embeddingBatchSize) {
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
- const rebuiltIndexedFiles = indexedFileRecords(rows);
128
- const indexedFiles = [...reusableIndexedFiles, ...rebuiltIndexedFiles].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
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 previousPaths = new Set(previousIndexedFiles.map((file) => file.relativePath));
131
- const currentPaths = new Set(files.map((file) => file.relativePath));
132
- const replacePaths = [
133
- ...filesToIndex.map((file) => file.relativePath),
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
- indexedFiles: indexedFiles.length,
186
- rebuiltFiles: new Set(rows.map((row) => row.relativePath)).size,
187
- reusedFiles: reusableFiles.size,
188
- policyRebuild,
189
- chunks: chunkCount,
190
- discoveredFiles: inventory.discoveredFiles,
191
- supportedFiles: files.length,
192
- supportedBytes: inventoryMetrics.supportedBytes,
193
- largestFileBytes: inventoryMetrics.largestFileBytes,
194
- skippedFiles: inventory.skippedFiles.length + emptyTextFiles.length,
195
- unsupportedFiles: countSkippedByReason(inventory.skippedFiles, "unsupported-extension"),
196
- oversizedFiles: countSkippedByReason(inventory.skippedFiles, "oversized"),
197
- sensitiveFiles: countSkippedByReason(inventory.skippedFiles, "sensitive-name"),
198
- emptyTextFiles: [
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 indexedFileRecords(rows) {
210
- const records = new Map();
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 current = records.get(row.relativePath);
213
- records.set(row.relativePath, {
214
- relativePath: row.relativePath,
215
- checksum: row.checksum,
216
- chunkCount: (current?.chunkCount ?? 0) + 1,
217
- bytes: row.bytes,
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
- async function firstStoredRow(config, connection) {
224
- const table = await openRowsTable(config, connection);
225
- if (!table) {
226
- return null;
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
- const [row] = (await table.query().limit(1).toArray());
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
- const inventory = await inventorySourceFiles(config);
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
- const emptyTextFiles = await currentEmptyTextFiles(config, files);
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
- for (const record of await readEmptyTextFiles(config)) {
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
  }