@jcode.labs/ragmir 2.17.2 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -6
- package/dist/access-log.d.ts +10 -0
- package/dist/access-log.d.ts.map +1 -1
- package/dist/access-log.js +119 -49
- package/dist/access-log.js.map +1 -1
- package/dist/chunking.d.ts +5 -1
- package/dist/chunking.d.ts.map +1 -1
- package/dist/chunking.js +102 -16
- package/dist/chunking.js.map +1 -1
- package/dist/citation.d.ts +18 -0
- package/dist/citation.d.ts.map +1 -0
- package/dist/citation.js +54 -0
- package/dist/citation.js.map +1 -0
- package/dist/cli-entry.d.ts +3 -0
- package/dist/cli-entry.d.ts.map +1 -0
- package/dist/cli-entry.js +16 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli-fast.d.ts +13 -0
- package/dist/cli-fast.d.ts.map +1 -0
- package/dist/cli-fast.js +145 -0
- package/dist/cli-fast.js.map +1 -0
- package/dist/cli-options.d.ts +3 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +7 -0
- package/dist/cli-options.js.map +1 -1
- package/dist/cli-stdin.d.ts +3 -0
- package/dist/cli-stdin.d.ts.map +1 -0
- package/dist/cli-stdin.js +15 -0
- package/dist/cli-stdin.js.map +1 -0
- package/dist/cli.js +251 -44
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +13 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +155 -10
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +227 -49
- package/dist/config.js.map +1 -1
- package/dist/context-resources.d.ts +4 -2
- package/dist/context-resources.d.ts.map +1 -1
- package/dist/context-resources.js +55 -24
- package/dist/context-resources.js.map +1 -1
- package/dist/defaults.d.ts +27 -0
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +42 -2
- package/dist/defaults.js.map +1 -1
- package/dist/destroy.d.ts +2 -2
- package/dist/destroy.d.ts.map +1 -1
- package/dist/destroy.js +34 -20
- package/dist/destroy.js.map +1 -1
- package/dist/doctor.d.ts +3 -2
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +86 -41
- package/dist/doctor.js.map +1 -1
- package/dist/durable-file.d.ts +13 -0
- package/dist/durable-file.d.ts.map +1 -0
- package/dist/durable-file.js +56 -0
- package/dist/durable-file.js.map +1 -0
- package/dist/embeddings.d.ts +14 -3
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +233 -95
- package/dist/embeddings.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evaluate.d.ts +4 -3
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +611 -110
- package/dist/evaluate.js.map +1 -1
- package/dist/files.d.ts +4 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +119 -33
- package/dist/files.js.map +1 -1
- package/dist/generation-retention.d.ts +52 -0
- package/dist/generation-retention.d.ts.map +1 -0
- package/dist/generation-retention.js +318 -0
- package/dist/generation-retention.js.map +1 -0
- package/dist/index-diagnostics.d.ts +3 -2
- package/dist/index-diagnostics.d.ts.map +1 -1
- package/dist/index-diagnostics.js +32 -3
- package/dist/index-diagnostics.js.map +1 -1
- package/dist/index-policy.d.ts.map +1 -1
- package/dist/index-policy.js +3 -0
- package/dist/index-policy.js.map +1 -1
- package/dist/index-write-lock.d.ts +16 -1
- package/dist/index-write-lock.d.ts.map +1 -1
- package/dist/index-write-lock.js +392 -13
- package/dist/index-write-lock.js.map +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +11 -2
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +680 -165
- package/dist/ingest.js.map +1 -1
- package/dist/ingestion-metrics.d.ts +58 -0
- package/dist/ingestion-metrics.d.ts.map +1 -0
- package/dist/ingestion-metrics.js +216 -0
- package/dist/ingestion-metrics.js.map +1 -0
- package/dist/ingestion-state.d.ts +11 -2
- package/dist/ingestion-state.d.ts.map +1 -1
- package/dist/ingestion-state.js +480 -49
- package/dist/ingestion-state.js.map +1 -1
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +11 -1
- package/dist/limits.js.map +1 -1
- package/dist/mcp-output.d.ts +44 -4
- package/dist/mcp-output.d.ts.map +1 -1
- package/dist/mcp-output.js +132 -159
- package/dist/mcp-output.js.map +1 -1
- package/dist/mcp-summaries.d.ts +32 -0
- package/dist/mcp-summaries.d.ts.map +1 -0
- package/dist/mcp-summaries.js +227 -0
- package/dist/mcp-summaries.js.map +1 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +85 -46
- package/dist/mcp.js.map +1 -1
- package/dist/ocr-cache.d.ts +15 -0
- package/dist/ocr-cache.d.ts.map +1 -0
- package/dist/ocr-cache.js +90 -0
- package/dist/ocr-cache.js.map +1 -0
- package/dist/ocr.d.ts +31 -0
- package/dist/ocr.d.ts.map +1 -1
- package/dist/ocr.js +193 -17
- package/dist/ocr.js.map +1 -1
- package/dist/parsing.d.ts.map +1 -1
- package/dist/parsing.js +482 -85
- package/dist/parsing.js.map +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +10 -11
- package/dist/preview.js.map +1 -1
- package/dist/quality-report.d.ts +12 -0
- package/dist/quality-report.d.ts.map +1 -0
- package/dist/quality-report.js +146 -0
- package/dist/quality-report.js.map +1 -0
- package/dist/query.d.ts +7 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +371 -284
- package/dist/query.js.map +1 -1
- package/dist/ranking.d.ts +38 -0
- package/dist/ranking.d.ts.map +1 -0
- package/dist/ranking.js +278 -0
- package/dist/ranking.js.map +1 -0
- package/dist/redaction.d.ts +6 -1
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +118 -4
- package/dist/redaction.js.map +1 -1
- package/dist/research.d.ts +6 -1
- package/dist/research.d.ts.map +1 -1
- package/dist/research.js +330 -130
- package/dist/research.js.map +1 -1
- package/dist/runtime-info.d.ts +3 -0
- package/dist/runtime-info.d.ts.map +1 -0
- package/dist/runtime-info.js +81 -0
- package/dist/runtime-info.js.map +1 -0
- package/dist/security.d.ts +3 -2
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +242 -45
- package/dist/security.js.map +1 -1
- package/dist/semantic-config.d.ts +7 -1
- package/dist/semantic-config.d.ts.map +1 -1
- package/dist/semantic-config.js +30 -4
- package/dist/semantic-config.js.map +1 -1
- package/dist/setup.js +6 -3
- package/dist/setup.js.map +1 -1
- package/dist/skill.js +34 -12
- package/dist/skill.js.map +1 -1
- package/dist/source-fingerprint-cache.d.ts +20 -0
- package/dist/source-fingerprint-cache.d.ts.map +1 -0
- package/dist/source-fingerprint-cache.js +133 -0
- package/dist/source-fingerprint-cache.js.map +1 -0
- package/dist/storage-maintenance.d.ts +46 -0
- package/dist/storage-maintenance.d.ts.map +1 -0
- package/dist/storage-maintenance.js +329 -0
- package/dist/storage-maintenance.js.map +1 -0
- package/dist/store.d.ts +39 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +559 -24
- package/dist/store.js.map +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +12 -2
- package/dist/text.js.map +1 -1
- package/dist/types.d.ts +414 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-index.d.ts +42 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +249 -0
- package/dist/vector-index.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workload.d.ts +22 -0
- package/dist/workload.d.ts.map +1 -0
- package/dist/workload.js +181 -0
- package/dist/workload.js.map +1 -0
- package/examples/document-evidence-benchmark/README.md +3 -2
- package/examples/document-evidence-benchmark/golden-queries.json +1 -1
- package/package.json +36 -11
package/dist/ingest.js
CHANGED
|
@@ -2,19 +2,26 @@ import { recordAccess } from "./access-log.js";
|
|
|
2
2
|
import { summarizeChunkStats } from "./chunk-stats.js";
|
|
3
3
|
import { chunkDocument, chunkSearchText } from "./chunking.js";
|
|
4
4
|
import { loadConfig } from "./config.js";
|
|
5
|
-
import { VECTOR_DISTANCE_METRIC } from "./defaults.js";
|
|
6
|
-
import { embedTexts } from "./embeddings.js";
|
|
5
|
+
import { MAX_INGEST_CHUNK_WINDOW, MAX_INGEST_CHUNKS_PER_FILE, MAX_INGEST_FILE_BATCH_SIZE, MAX_INGEST_SOURCE_WINDOW_BYTES, MAX_INGEST_VECTOR_BYTES_PER_FILE, VECTOR_DISTANCE_METRIC, } from "./defaults.js";
|
|
6
|
+
import { embeddingModelState, embedTexts } from "./embeddings.js";
|
|
7
7
|
import { countSkippedByReason, inventorySourceFiles, summarizeUnsupportedExtensions, } from "./files.js";
|
|
8
|
+
import { collectGenerationGarbageUnlocked } from "./generation-retention.js";
|
|
8
9
|
import { INDEX_SCHEMA_VERSION } from "./index-diagnostics.js";
|
|
9
10
|
import { indexPolicyFingerprint } from "./index-policy.js";
|
|
10
11
|
import { withIndexWriteLock } from "./index-write-lock.js";
|
|
11
|
-
import {
|
|
12
|
+
import { activeIngestionMetrics, createIngestionMetricsCollector, runWithIngestionMetrics, } from "./ingestion-metrics.js";
|
|
13
|
+
import { canResumeIngestion, compactIngestionState, createIngestionRunState, finishIngestionState, generationTableName, ingestionProgress, readIngestionState, reconcileIngestionFile, removeStagedIndexManifest, resumeIngestionState, updateIngestionFile, writeIngestionState, } from "./ingestion-state.js";
|
|
12
14
|
import { operationSignal, throwIfAborted } from "./operation.js";
|
|
13
15
|
import { parseFile } from "./parsing.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
+
import { summarizeIndexedCorpus } from "./quality-report.js";
|
|
17
|
+
import { redactDocument, totalRedactions } from "./redaction.js";
|
|
18
|
+
import { securityAuditWithConfig } from "./security.js";
|
|
19
|
+
import { maintainStorageTable } from "./storage-maintenance.js";
|
|
20
|
+
import { activeIndexTableName, closeRowsTable, dropRowsTable, openRowsTable, openRowsTableByName, readEmptyTextFiles, readIndexManifest, updateRowsInTable, writeEmptyTextFiles, writeIndexManifest, } from "./store.js";
|
|
16
21
|
import { VERSION } from "./version.js";
|
|
22
|
+
import { runWorkload } from "./workload.js";
|
|
17
23
|
const MAX_SOURCE_DIAGNOSTIC_ITEMS = 20;
|
|
24
|
+
const MAX_HEALTH_PREVIEW_ITEMS = 50;
|
|
18
25
|
const DEFAULT_INGEST_FILE_BATCH_SIZE = 25;
|
|
19
26
|
const ARCHIVE_PATH_PATTERNS = [
|
|
20
27
|
/(^|[/_-])archive(s)?([/_-]|$)/iu,
|
|
@@ -36,10 +43,49 @@ export async function ingest(options = {}) {
|
|
|
36
43
|
}
|
|
37
44
|
export async function ingestWithConfig(config, options = {}, connection) {
|
|
38
45
|
const signal = operationSignal(options);
|
|
39
|
-
return
|
|
46
|
+
return runWorkload(config, "ingestion", signal, async ({ queueTimeMs }) => {
|
|
47
|
+
const collector = createIngestionMetricsCollector(options.collectMetrics === true, queueTimeMs, config.embeddingProvider);
|
|
48
|
+
if (!collector) {
|
|
49
|
+
return withIndexWriteLock(config.storageDir, signal, () => ingestUnlocked(config, options, connection, signal));
|
|
50
|
+
}
|
|
51
|
+
return runWithIngestionMetrics(collector, async () => {
|
|
52
|
+
collector.beginPhase("writeLockWait");
|
|
53
|
+
try {
|
|
54
|
+
const result = await withIndexWriteLock(config.storageDir, signal, () => {
|
|
55
|
+
collector.endPhase("writeLockWait");
|
|
56
|
+
return ingestUnlocked(config, options, connection, signal);
|
|
57
|
+
});
|
|
58
|
+
const metrics = collector.complete({
|
|
59
|
+
runId: result.runId,
|
|
60
|
+
status: result.errors.length > 0 ? "completed_with_errors" : "completed",
|
|
61
|
+
candidateFiles: result.supportedFiles,
|
|
62
|
+
processedFiles: result.rebuiltFiles + result.emptyTextFiles.length + result.errors.length,
|
|
63
|
+
fallbackFiles: result.staleLastKnownGood.length,
|
|
64
|
+
errorCount: result.errors.length,
|
|
65
|
+
ocrSubprocesses: result.ocr.subprocesses,
|
|
66
|
+
});
|
|
67
|
+
return options.collectMetrics === true ? { ...result, metrics } : result;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
collector.endPhase("writeLockWait");
|
|
71
|
+
collector.recordFailure(error);
|
|
72
|
+
collector.complete({
|
|
73
|
+
runId: null,
|
|
74
|
+
status: signal?.aborted ? "interrupted" : "failed",
|
|
75
|
+
candidateFiles: 0,
|
|
76
|
+
processedFiles: 0,
|
|
77
|
+
fallbackFiles: 0,
|
|
78
|
+
errorCount: 1,
|
|
79
|
+
ocrSubprocesses: 0,
|
|
80
|
+
});
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
40
85
|
}
|
|
41
86
|
async function ingestUnlocked(config, options, connection, signal) {
|
|
42
87
|
let state = null;
|
|
88
|
+
const metrics = activeIngestionMetrics();
|
|
43
89
|
try {
|
|
44
90
|
throwIfAborted(signal);
|
|
45
91
|
const requestedBatchSize = ingestFileBatchSize(options.batchSize);
|
|
@@ -47,7 +93,10 @@ async function ingestUnlocked(config, options, connection, signal) {
|
|
|
47
93
|
const existingManifest = await readIndexManifest(config);
|
|
48
94
|
const storedState = await readIngestionState(config);
|
|
49
95
|
const storedEmptyFiles = await readEmptyTextFiles(config);
|
|
50
|
-
const inventory =
|
|
96
|
+
const inventory = metrics
|
|
97
|
+
? await metrics.measureAsync("inventory", () => inventorySourceFiles(config, signal ? { signal } : {}))
|
|
98
|
+
: await inventorySourceFiles(config, signal ? { signal } : {});
|
|
99
|
+
metrics?.recordSourceBytes(inventory.contentBytesRead);
|
|
51
100
|
const files = inventory.supportedFiles;
|
|
52
101
|
const inventoryMetrics = sourceInventoryMetrics(files);
|
|
53
102
|
const currentFiles = new Map(files.map((file) => [file.relativePath, file]));
|
|
@@ -63,6 +112,10 @@ async function ingestUnlocked(config, options, connection, signal) {
|
|
|
63
112
|
const canReuse = manifestCompatible && existingTable !== null;
|
|
64
113
|
const previousIndexedFiles = canReuse ? (existingManifest.indexedFiles ?? []) : [];
|
|
65
114
|
const previousEmptyFiles = canReuse ? storedEmptyFiles : [];
|
|
115
|
+
const previousFiles = new Map([
|
|
116
|
+
...previousIndexedFiles.map((file) => [file.relativePath, file]),
|
|
117
|
+
...previousEmptyFiles.map((file) => [file.relativePath, { ...file, chunkCount: 0 }]),
|
|
118
|
+
]);
|
|
66
119
|
const reusableIndexedFiles = previousIndexedFiles.filter((file) => currentFiles.get(file.relativePath)?.checksum === file.checksum);
|
|
67
120
|
const reusableEmptyFiles = previousEmptyFiles.filter((file) => currentFiles.get(file.relativePath)?.checksum === file.checksum);
|
|
68
121
|
const reusablePaths = new Set([
|
|
@@ -104,22 +157,15 @@ async function ingestUnlocked(config, options, connection, signal) {
|
|
|
104
157
|
files,
|
|
105
158
|
reusablePaths: mode === "incremental" ? reusablePaths : new Set(),
|
|
106
159
|
reusableChunkCounts,
|
|
160
|
+
...(mode === "incremental" ? { previousFiles } : {}),
|
|
107
161
|
});
|
|
108
162
|
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
|
-
};
|
|
163
|
+
state.tableName = generationTableName(config.tableName, state.runId);
|
|
119
164
|
}
|
|
120
165
|
}
|
|
121
166
|
await persistIngestionProgress(state, config, options);
|
|
122
167
|
throwIfAborted(signal);
|
|
168
|
+
const tableName = state.tableName;
|
|
123
169
|
const previousPaths = new Set(previousIndexedFiles.map((file) => file.relativePath));
|
|
124
170
|
const currentPaths = new Set(files.map((file) => file.relativePath));
|
|
125
171
|
const removedPaths = state.mode === "incremental"
|
|
@@ -133,12 +179,17 @@ async function ingestUnlocked(config, options, connection, signal) {
|
|
|
133
179
|
});
|
|
134
180
|
let removalApplied = false;
|
|
135
181
|
let lexicalIndexWarning = null;
|
|
136
|
-
|
|
182
|
+
let storageWarning = null;
|
|
183
|
+
let storageMutations = 0;
|
|
184
|
+
const ocr = emptyPdfOcrMetrics();
|
|
185
|
+
const failurePolicy = incrementalFailurePolicy(options, config);
|
|
186
|
+
for (const fileWindow of ingestionWindows(pendingFiles, state.batchSize, config)) {
|
|
137
187
|
throwIfAborted(signal);
|
|
138
|
-
const
|
|
139
|
-
for (const parsed of
|
|
188
|
+
const parsedWindow = await mapLimit(fileWindow, config.ingestConcurrency, signal, async (file) => parseSourceFile(file, config, signal));
|
|
189
|
+
for (const parsed of parsedWindow) {
|
|
190
|
+
mergePdfOcrMetrics(ocr, parsed.ocr);
|
|
140
191
|
state = updateIngestionFile(state, parsed.file.relativePath, parsed.error
|
|
141
|
-
? { state: "error",
|
|
192
|
+
? { state: "error", redactions: 0, error: parsed.error }
|
|
142
193
|
: {
|
|
143
194
|
state: "parsed",
|
|
144
195
|
chunkCount: parsed.chunks.length,
|
|
@@ -148,41 +199,117 @@ async function ingestUnlocked(config, options, connection, signal) {
|
|
|
148
199
|
}
|
|
149
200
|
await persistIngestionProgress(state, config, options);
|
|
150
201
|
throwIfAborted(signal);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
202
|
+
for (const parsed of parsedWindow) {
|
|
203
|
+
throwIfAborted(signal);
|
|
204
|
+
const successful = parsed.error === null;
|
|
205
|
+
const chunkCount = parsed.chunks.length;
|
|
206
|
+
const rows = successful ? await vectorRowsForChunks(parsed.chunks, config, signal) : [];
|
|
207
|
+
if (successful) {
|
|
208
|
+
state = updateIngestionFile(state, parsed.file.relativePath, { state: "embedded" });
|
|
209
|
+
await persistIngestionProgress(state, config, options);
|
|
210
|
+
throwIfAborted(signal);
|
|
211
|
+
}
|
|
212
|
+
const replacePaths = [
|
|
213
|
+
...(successful || (state.mode === "incremental" && failurePolicy === "remove-stale")
|
|
214
|
+
? [parsed.file.relativePath]
|
|
215
|
+
: []),
|
|
216
|
+
...(!removalApplied ? removedPaths : []),
|
|
217
|
+
];
|
|
218
|
+
if (rows.length > 0 || replacePaths.length > 0) {
|
|
219
|
+
const writeResult = metrics
|
|
220
|
+
? await metrics.measureAsync("storageWrite", () => updateRowsInTable(rows, replacePaths, tableName, config, connection))
|
|
221
|
+
: await updateRowsInTable(rows, replacePaths, tableName, config, connection);
|
|
222
|
+
metrics?.recordStoragePayload(vectorRowsPayloadBytes(rows));
|
|
223
|
+
removalApplied = true;
|
|
224
|
+
storageMutations += 1;
|
|
225
|
+
lexicalIndexWarning ??= writeResult.lexicalIndexWarning;
|
|
226
|
+
}
|
|
227
|
+
if (successful) {
|
|
228
|
+
state = updateIngestionFile(state, parsed.file.relativePath, {
|
|
229
|
+
state: "indexed",
|
|
230
|
+
lastGoodChecksum: parsed.file.checksum,
|
|
231
|
+
lastGoodChunkCount: chunkCount,
|
|
232
|
+
lastGoodBytes: parsed.file.bytes,
|
|
233
|
+
lastGoodMtimeMs: parsed.file.mtimeMs,
|
|
234
|
+
staleLastKnownGood: false,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
else if (state.mode === "incremental" && failurePolicy === "remove-stale") {
|
|
238
|
+
state = updateIngestionFile(state, parsed.file.relativePath, {
|
|
239
|
+
chunkCount: 0,
|
|
240
|
+
lastGoodChecksum: null,
|
|
241
|
+
lastGoodChunkCount: 0,
|
|
242
|
+
lastGoodBytes: null,
|
|
243
|
+
lastGoodMtimeMs: null,
|
|
244
|
+
staleLastKnownGood: false,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
await persistIngestionProgress(state, config, options);
|
|
248
|
+
parsed.chunks.length = 0;
|
|
249
|
+
throwIfAborted(signal);
|
|
168
250
|
}
|
|
169
|
-
await writeProgressManifest(state, config, connection);
|
|
170
|
-
await persistIngestionProgress(state, config, options);
|
|
171
|
-
throwIfAborted(signal);
|
|
172
251
|
}
|
|
173
252
|
if (!removalApplied && removedPaths.length > 0) {
|
|
174
|
-
const writeResult =
|
|
253
|
+
const writeResult = metrics
|
|
254
|
+
? await metrics.measureAsync("storageWrite", () => updateRowsInTable([], removedPaths, tableName, config, connection))
|
|
255
|
+
: await updateRowsInTable([], removedPaths, tableName, config, connection);
|
|
256
|
+
storageMutations += 1;
|
|
175
257
|
lexicalIndexWarning ??= writeResult.lexicalIndexWarning;
|
|
176
258
|
}
|
|
177
|
-
|
|
259
|
+
sortIngestionFilesForStorage(state);
|
|
260
|
+
let manifest = await manifestForState(state, config, connection);
|
|
261
|
+
const maintain = () => maintainStorageTable(tableName, config, connection, {
|
|
262
|
+
additionalMutations: storageMutations,
|
|
263
|
+
...(manifest.vectorDimension === undefined
|
|
264
|
+
? {}
|
|
265
|
+
: { vectorDimension: manifest.vectorDimension }),
|
|
266
|
+
});
|
|
267
|
+
metrics?.recordMaintenanceOperation();
|
|
268
|
+
const maintenance = metrics
|
|
269
|
+
? await metrics.measureAsync("maintenance", maintain)
|
|
270
|
+
: await maintain();
|
|
271
|
+
if (manifest.chunkCount > 0 && !maintenance.adaptiveIndices) {
|
|
272
|
+
throw new Error("Cannot activate an index without adaptive vector index diagnostics.");
|
|
273
|
+
}
|
|
274
|
+
if (maintenance.adaptiveIndices) {
|
|
275
|
+
manifest = { ...manifest, vectorIndex: maintenance.adaptiveIndices.vectorIndex };
|
|
276
|
+
}
|
|
277
|
+
const securityReport = await securityAuditWithConfig(config, {
|
|
278
|
+
deep: false,
|
|
279
|
+
...(signal ? { signal } : {}),
|
|
280
|
+
});
|
|
281
|
+
const checkedAt = new Date().toISOString();
|
|
282
|
+
manifest = {
|
|
283
|
+
...manifest,
|
|
284
|
+
health: indexHealthSnapshot({
|
|
285
|
+
checkedAt,
|
|
286
|
+
inventory,
|
|
287
|
+
files,
|
|
288
|
+
state,
|
|
289
|
+
manifest,
|
|
290
|
+
securityWarnings: securityReport.warnings,
|
|
291
|
+
}),
|
|
292
|
+
maintenance: indexMaintenanceSnapshot(maintenance, checkedAt),
|
|
293
|
+
};
|
|
294
|
+
storageWarning = combineWarnings(storageWarning, maintenance.warning);
|
|
178
295
|
await validateIngestionTable(state, manifest, config, connection);
|
|
179
296
|
await writeEmptyTextFiles(emptyTextRecords(state), config);
|
|
180
|
-
await writeIndexManifest(manifest, config);
|
|
297
|
+
await writeIndexManifest(manifest, config, indexedFilesFromState(state));
|
|
298
|
+
try {
|
|
299
|
+
const collectGarbage = () => collectGenerationGarbageUnlocked(config, connection, { state });
|
|
300
|
+
metrics?.recordMaintenanceOperation();
|
|
301
|
+
const garbageCollection = metrics
|
|
302
|
+
? await metrics.measureAsync("maintenance", collectGarbage)
|
|
303
|
+
: await collectGarbage();
|
|
304
|
+
storageWarning = combineWarnings(storageWarning, garbageCollection.warning);
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
storageWarning = combineWarnings(storageWarning, `Generation cleanup failed (${error instanceof Error ? error.message : String(error)}). The active index remains available.`);
|
|
308
|
+
}
|
|
181
309
|
await removeStagedIndexManifest(state.runId, config);
|
|
182
310
|
const errors = ingestionErrors(state);
|
|
183
311
|
state = finishIngestionState(state, errors.length > 0 ? "completed_with_errors" : "completed");
|
|
184
|
-
await persistIngestionProgress(state, config, options);
|
|
185
|
-
const indexedFiles = manifest.indexedFiles ?? [];
|
|
312
|
+
await persistIngestionProgress(state, config, options, true);
|
|
186
313
|
const emptyTextFiles = emptyTextRecords(state).map((file) => file.relativePath);
|
|
187
314
|
const redactions = state.files.reduce((sum, file) => sum + file.redactions, 0);
|
|
188
315
|
await recordAccess(config, {
|
|
@@ -194,9 +321,10 @@ async function ingestUnlocked(config, options, connection, signal) {
|
|
|
194
321
|
runId: state.runId,
|
|
195
322
|
resumed: state.resumed,
|
|
196
323
|
batchSize: state.batchSize,
|
|
197
|
-
indexedFiles:
|
|
324
|
+
indexedFiles: manifest.fileCount,
|
|
198
325
|
rebuiltFiles: state.files.filter((file) => file.state === "indexed" && file.chunkCount > 0 && !file.reused).length,
|
|
199
326
|
reusedFiles: state.files.filter((file) => file.reused).length,
|
|
327
|
+
staleLastKnownGood: manifest.staleFiles?.map((file) => file.relativePath) ?? [],
|
|
200
328
|
policyRebuild,
|
|
201
329
|
chunks: manifest.chunkCount,
|
|
202
330
|
discoveredFiles: inventory.discoveredFiles,
|
|
@@ -210,8 +338,10 @@ async function ingestUnlocked(config, options, connection, signal) {
|
|
|
210
338
|
emptyTextFiles,
|
|
211
339
|
unsupportedExtensions: summarizeUnsupportedExtensions(inventory.skippedFiles),
|
|
212
340
|
redactions,
|
|
213
|
-
|
|
341
|
+
ocr,
|
|
342
|
+
vectorIndexWarning: maintenance.adaptiveIndices?.warning ?? null,
|
|
214
343
|
lexicalIndexWarning,
|
|
344
|
+
storageWarning,
|
|
215
345
|
errors,
|
|
216
346
|
};
|
|
217
347
|
}
|
|
@@ -228,68 +358,122 @@ async function ingestUnlocked(config, options, connection, signal) {
|
|
|
228
358
|
throw error;
|
|
229
359
|
}
|
|
230
360
|
}
|
|
361
|
+
function combineWarnings(...warnings) {
|
|
362
|
+
const present = warnings.filter((warning) => warning !== null);
|
|
363
|
+
return present.length > 0 ? present.join(" ") : null;
|
|
364
|
+
}
|
|
365
|
+
function emptyPdfOcrMetrics() {
|
|
366
|
+
return {
|
|
367
|
+
pages: 0,
|
|
368
|
+
cacheHits: 0,
|
|
369
|
+
cacheMisses: 0,
|
|
370
|
+
batches: 0,
|
|
371
|
+
subprocesses: 0,
|
|
372
|
+
durationMs: 0,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function mergePdfOcrMetrics(target, source) {
|
|
376
|
+
if (!source) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
target.pages += source.pages;
|
|
380
|
+
target.cacheHits += source.cacheHits;
|
|
381
|
+
target.cacheMisses += source.cacheMisses;
|
|
382
|
+
target.batches += source.batches;
|
|
383
|
+
target.subprocesses += source.subprocesses;
|
|
384
|
+
target.durationMs = Math.round((target.durationMs + source.durationMs) * 1_000) / 1_000;
|
|
385
|
+
}
|
|
231
386
|
async function reconcileCommittedFiles(state, config, connection) {
|
|
232
387
|
const filesToReconcile = state.files.filter((file) => file.state !== "indexed" && file.state !== "error");
|
|
233
388
|
if (filesToReconcile.length === 0) {
|
|
234
389
|
return state;
|
|
235
390
|
}
|
|
236
391
|
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
392
|
const committedEmptyFiles = state.mode === "incremental"
|
|
246
393
|
? new Set((await readEmptyTextFiles(config)).map((file) => `${file.relativePath}\0${file.checksum}`))
|
|
247
394
|
: new Set();
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
395
|
+
for (const file of filesToReconcile) {
|
|
396
|
+
const key = `${file.relativePath}\0${file.checksum}`;
|
|
397
|
+
const committedRows = file.chunkCount > 0 && table
|
|
398
|
+
? await table.countRows(`relativePath = ${sqlString(file.relativePath)} AND checksum = ${sqlString(file.checksum)}`)
|
|
399
|
+
: 0;
|
|
400
|
+
const committed = file.chunkCount > 0 ? committedRows === file.chunkCount : committedEmptyFiles.has(key);
|
|
401
|
+
if (committed) {
|
|
402
|
+
reconcileIngestionFile(state, file.relativePath, {
|
|
403
|
+
state: "indexed",
|
|
404
|
+
lastGoodChecksum: file.checksum,
|
|
405
|
+
lastGoodChunkCount: file.chunkCount,
|
|
406
|
+
lastGoodBytes: file.bytes,
|
|
407
|
+
lastGoodMtimeMs: file.mtimeMs,
|
|
408
|
+
staleLastKnownGood: false,
|
|
409
|
+
error: null,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return state;
|
|
259
414
|
}
|
|
260
415
|
async function parseSourceFile(file, config, signal) {
|
|
416
|
+
const metrics = activeIngestionMetrics();
|
|
417
|
+
metrics?.recordSourceBytes(file.bytes);
|
|
261
418
|
try {
|
|
262
|
-
const
|
|
419
|
+
const parse = () => parseFile(file, { ...config, ...(signal ? { signal } : {}) });
|
|
420
|
+
const parsed = metrics ? await metrics.measureAsync("parsing", parse) : await parse();
|
|
263
421
|
throwIfAborted(signal);
|
|
264
|
-
const redacted =
|
|
422
|
+
const redacted = metrics
|
|
423
|
+
? metrics.measure("redaction", () => redactDocument(parsed, config))
|
|
424
|
+
: redactDocument(parsed, config);
|
|
425
|
+
const chunks = metrics
|
|
426
|
+
? metrics.measure("chunking", () => chunkDocument(redacted.document, config.chunkSize, config.chunkOverlap, {
|
|
427
|
+
maxChunks: MAX_INGEST_CHUNKS_PER_FILE,
|
|
428
|
+
...(signal ? { signal } : {}),
|
|
429
|
+
}))
|
|
430
|
+
: chunkDocument(redacted.document, config.chunkSize, config.chunkOverlap, {
|
|
431
|
+
maxChunks: MAX_INGEST_CHUNKS_PER_FILE,
|
|
432
|
+
...(signal ? { signal } : {}),
|
|
433
|
+
});
|
|
265
434
|
return {
|
|
266
435
|
file,
|
|
267
|
-
chunks
|
|
436
|
+
chunks,
|
|
268
437
|
redactions: totalRedactions(redacted.counts),
|
|
438
|
+
ocr: parsed.ocr ?? null,
|
|
269
439
|
error: null,
|
|
270
440
|
};
|
|
271
441
|
}
|
|
272
442
|
catch (error) {
|
|
273
443
|
throwIfAborted(signal);
|
|
444
|
+
metrics?.recordFailure(error);
|
|
274
445
|
return {
|
|
275
446
|
file,
|
|
276
447
|
chunks: [],
|
|
277
448
|
redactions: 0,
|
|
449
|
+
ocr: null,
|
|
278
450
|
error: error instanceof Error ? error.message : String(error),
|
|
279
451
|
};
|
|
280
452
|
}
|
|
281
453
|
}
|
|
282
454
|
async function vectorRowsForChunks(chunks, config, signal) {
|
|
455
|
+
const metrics = activeIngestionMetrics();
|
|
283
456
|
const rows = [];
|
|
457
|
+
let vectorBytes = 0;
|
|
284
458
|
for (const batch of valueBatches(chunks, config.embeddingBatchSize)) {
|
|
285
459
|
throwIfAborted(signal);
|
|
286
|
-
|
|
460
|
+
metrics?.recordEmbeddingModelState(embeddingModelState(config));
|
|
461
|
+
const embed = () => embedTexts(batch.map(chunkSearchText), config, "document", signal, (admission) => {
|
|
462
|
+
metrics?.recordEmbeddingQueue(admission.queueTimeMs);
|
|
463
|
+
});
|
|
464
|
+
const embeddings = metrics ? await metrics.measureAsync("embedding", embed) : await embed();
|
|
465
|
+
metrics?.recordEmbeddings(embeddings.length);
|
|
287
466
|
throwIfAborted(signal);
|
|
288
467
|
for (const [index, chunk] of batch.entries()) {
|
|
289
468
|
const vector = embeddings[index];
|
|
290
469
|
if (!vector) {
|
|
291
470
|
throw new Error(`Missing embedding for chunk ${chunk.relativePath}#${chunk.chunkIndex}.`);
|
|
292
471
|
}
|
|
472
|
+
vectorBytes += vector.length * Float64Array.BYTES_PER_ELEMENT;
|
|
473
|
+
if (vectorBytes > MAX_INGEST_VECTOR_BYTES_PER_FILE) {
|
|
474
|
+
metrics?.recordTruncation();
|
|
475
|
+
throw new Error(`Vector memory limit of ${MAX_INGEST_VECTOR_BYTES_PER_FILE} bytes exceeded for ${chunk.relativePath}. Increase chunkSize, split the source file, or use a smaller embedding model.`);
|
|
476
|
+
}
|
|
293
477
|
rows.push({
|
|
294
478
|
...chunk,
|
|
295
479
|
searchText: chunkSearchText(chunk),
|
|
@@ -301,25 +485,45 @@ async function vectorRowsForChunks(chunks, config, signal) {
|
|
|
301
485
|
}
|
|
302
486
|
return rows;
|
|
303
487
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
488
|
+
function vectorRowsPayloadBytes(rows) {
|
|
489
|
+
return rows.reduce((total, row) => total +
|
|
490
|
+
Buffer.byteLength(row.id, "utf8") +
|
|
491
|
+
Buffer.byteLength(row.source, "utf8") +
|
|
492
|
+
Buffer.byteLength(row.relativePath, "utf8") +
|
|
493
|
+
Buffer.byteLength(row.contextPath, "utf8") +
|
|
494
|
+
Buffer.byteLength(row.text, "utf8") +
|
|
495
|
+
Buffer.byteLength(row.searchText, "utf8") +
|
|
496
|
+
Buffer.byteLength(row.embeddingProvider, "utf8") +
|
|
497
|
+
Buffer.byteLength(row.embeddingModel, "utf8") +
|
|
498
|
+
row.vector.length * Float64Array.BYTES_PER_ELEMENT, 0);
|
|
307
499
|
}
|
|
308
|
-
async function
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
500
|
+
async function persistIngestionProgress(state, config, options, compact = false) {
|
|
501
|
+
if (compact) {
|
|
502
|
+
await compactIngestionState(state, config);
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
await writeIngestionState(state, config);
|
|
313
506
|
}
|
|
314
|
-
await
|
|
315
|
-
await writeIndexManifest(manifest, config);
|
|
507
|
+
await options.onProgress?.(ingestionProgress(state));
|
|
316
508
|
}
|
|
317
509
|
async function manifestForState(state, config, connection) {
|
|
318
|
-
const
|
|
319
|
-
const
|
|
510
|
+
const corpus = summarizeIndexedCorpus(indexedFilesFromState(state));
|
|
511
|
+
const staleFiles = state.files
|
|
512
|
+
.flatMap((file) => file.staleLastKnownGood && file.lastGoodChecksum !== null && file.error !== null
|
|
513
|
+
? [
|
|
514
|
+
{
|
|
515
|
+
relativePath: file.relativePath,
|
|
516
|
+
currentChecksum: file.checksum,
|
|
517
|
+
lastGoodChecksum: file.lastGoodChecksum,
|
|
518
|
+
chunkCount: file.lastGoodChunkCount,
|
|
519
|
+
error: file.error,
|
|
520
|
+
},
|
|
521
|
+
]
|
|
522
|
+
: [])
|
|
523
|
+
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
320
524
|
const table = await openRowsTableByName(state.tableName, config, connection);
|
|
321
525
|
const [firstRow] = table ? (await table.query().limit(1).toArray()) : [];
|
|
322
|
-
if (chunkCount > 0 && !firstRow) {
|
|
526
|
+
if (corpus.chunkCount > 0 && !firstRow) {
|
|
323
527
|
throw new Error("Cannot write an index manifest without indexed rows.");
|
|
324
528
|
}
|
|
325
529
|
return {
|
|
@@ -328,38 +532,151 @@ async function manifestForState(state, config, connection) {
|
|
|
328
532
|
ragmirVersion: VERSION,
|
|
329
533
|
embeddingProvider: config.embeddingProvider,
|
|
330
534
|
embeddingModel: config.embeddingModel,
|
|
535
|
+
embeddingModelRevision: config.embeddingModelRevision,
|
|
536
|
+
embeddingModelDigest: config.embeddingModelDigest,
|
|
331
537
|
indexPolicyFingerprint: state.policyFingerprint,
|
|
332
538
|
...(firstRow ? { vectorDimension: firstRow.vector.length } : {}),
|
|
333
539
|
vectorDistanceMetric: VECTOR_DISTANCE_METRIC,
|
|
334
540
|
chunkSize: config.chunkSize,
|
|
335
541
|
chunkOverlap: config.chunkOverlap,
|
|
336
|
-
fileCount:
|
|
337
|
-
chunkCount,
|
|
542
|
+
fileCount: corpus.fileCount,
|
|
543
|
+
chunkCount: corpus.chunkCount,
|
|
544
|
+
corpusFingerprint: corpus.corpusFingerprint,
|
|
338
545
|
tableName: state.tableName,
|
|
339
|
-
|
|
546
|
+
...(staleFiles.length > 0 ? { staleFiles } : {}),
|
|
340
547
|
};
|
|
341
548
|
}
|
|
342
|
-
function
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
.
|
|
549
|
+
function indexHealthSnapshot(input) {
|
|
550
|
+
const inventoryMetrics = sourceInventoryMetrics(input.files);
|
|
551
|
+
const emptyTextFiles = emptyTextRecords(input.state).map((file) => file.relativePath);
|
|
552
|
+
const staleInIndex = (input.manifest.staleFiles ?? []).map((file) => file.relativePath);
|
|
553
|
+
const missingFromIndex = input.state.files
|
|
554
|
+
.filter((file) => file.state === "error" && !file.staleLastKnownGood)
|
|
555
|
+
.map((file) => file.relativePath)
|
|
556
|
+
.sort();
|
|
557
|
+
const previews = {
|
|
558
|
+
missingFromIndex: missingFromIndex.slice(0, MAX_HEALTH_PREVIEW_ITEMS),
|
|
559
|
+
staleInIndex: staleInIndex.slice(0, MAX_HEALTH_PREVIEW_ITEMS),
|
|
560
|
+
emptyTextFiles: emptyTextFiles.slice(0, MAX_HEALTH_PREVIEW_ITEMS),
|
|
561
|
+
};
|
|
562
|
+
return {
|
|
563
|
+
schemaVersion: 1,
|
|
564
|
+
checkedAt: input.checkedAt,
|
|
565
|
+
discoveredFiles: input.inventory.discoveredFiles,
|
|
566
|
+
supportedFiles: input.files.length,
|
|
567
|
+
supportedBytes: inventoryMetrics.supportedBytes,
|
|
568
|
+
largestFileBytes: inventoryMetrics.largestFileBytes,
|
|
569
|
+
skippedFiles: input.inventory.skippedFiles.length,
|
|
570
|
+
unsupportedFiles: countSkippedByReason(input.inventory.skippedFiles, "unsupported-extension"),
|
|
571
|
+
oversizedFiles: countSkippedByReason(input.inventory.skippedFiles, "oversized"),
|
|
572
|
+
sensitiveFiles: countSkippedByReason(input.inventory.skippedFiles, "sensitive-name"),
|
|
573
|
+
emptyTextFiles: emptyTextFiles.length,
|
|
574
|
+
missingFromIndex: missingFromIndex.length,
|
|
575
|
+
staleInIndex: staleInIndex.length,
|
|
576
|
+
previews,
|
|
577
|
+
previewOmitted: {
|
|
578
|
+
missingFromIndex: missingFromIndex.length - previews.missingFromIndex.length,
|
|
579
|
+
staleInIndex: staleInIndex.length - previews.staleInIndex.length,
|
|
580
|
+
emptyTextFiles: emptyTextFiles.length - previews.emptyTextFiles.length,
|
|
581
|
+
},
|
|
582
|
+
skippedByReason: skippedFileCounts(input.inventory.skippedFiles),
|
|
583
|
+
sourceDiagnostics: sourceDiagnostics(input.files, input.inventory.skippedFiles),
|
|
584
|
+
securityCheckedAt: input.checkedAt,
|
|
585
|
+
securityWarnings: [...input.securityWarnings].sort().slice(0, MAX_HEALTH_PREVIEW_ITEMS),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function indexMaintenanceSnapshot(report, checkedAt) {
|
|
589
|
+
return {
|
|
590
|
+
schemaVersion: 1,
|
|
591
|
+
checkedAt,
|
|
592
|
+
status: report.status,
|
|
593
|
+
tableVersion: report.tableVersion,
|
|
594
|
+
mutationsSinceOptimization: report.mutationsSinceOptimization,
|
|
595
|
+
fragments: report.fragments,
|
|
596
|
+
fullTextIndex: report.fullTextIndex,
|
|
597
|
+
warning: report.warning,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function skippedFileCounts(skippedFiles) {
|
|
601
|
+
const counts = new Map();
|
|
602
|
+
for (const file of skippedFiles) {
|
|
603
|
+
counts.set(file.reason, (counts.get(file.reason) ?? 0) + 1);
|
|
604
|
+
}
|
|
605
|
+
return Object.fromEntries([...counts.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
606
|
+
}
|
|
607
|
+
function* indexedFilesFromState(state) {
|
|
608
|
+
for (const file of state.files) {
|
|
609
|
+
if (file.state === "indexed" && file.chunkCount > 0) {
|
|
610
|
+
yield {
|
|
611
|
+
relativePath: file.relativePath,
|
|
612
|
+
checksum: file.checksum,
|
|
613
|
+
chunkCount: file.chunkCount,
|
|
614
|
+
bytes: file.bytes,
|
|
615
|
+
mtimeMs: file.mtimeMs,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
else if (file.staleLastKnownGood &&
|
|
619
|
+
file.lastGoodChecksum !== null &&
|
|
620
|
+
file.lastGoodChunkCount > 0) {
|
|
621
|
+
yield {
|
|
622
|
+
relativePath: file.relativePath,
|
|
623
|
+
checksum: file.lastGoodChecksum,
|
|
624
|
+
chunkCount: file.lastGoodChunkCount,
|
|
625
|
+
...(file.lastGoodBytes === null ? {} : { bytes: file.lastGoodBytes }),
|
|
626
|
+
...(file.lastGoodMtimeMs === null ? {} : { mtimeMs: file.lastGoodMtimeMs }),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function sortIngestionFilesForStorage(state) {
|
|
632
|
+
state.files.sort((left, right) => compareUnicodeScalarValues(left.relativePath, right.relativePath));
|
|
633
|
+
}
|
|
634
|
+
function compareUnicodeScalarValues(left, right) {
|
|
635
|
+
let leftIndex = 0;
|
|
636
|
+
let rightIndex = 0;
|
|
637
|
+
while (leftIndex < left.length && rightIndex < right.length) {
|
|
638
|
+
const leftCodePoint = left.codePointAt(leftIndex);
|
|
639
|
+
const rightCodePoint = right.codePointAt(rightIndex);
|
|
640
|
+
if (leftCodePoint === undefined || rightCodePoint === undefined) {
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
if (leftCodePoint !== rightCodePoint) {
|
|
644
|
+
return leftCodePoint - rightCodePoint;
|
|
645
|
+
}
|
|
646
|
+
leftIndex += leftCodePoint > 0xffff ? 2 : 1;
|
|
647
|
+
rightIndex += rightCodePoint > 0xffff ? 2 : 1;
|
|
648
|
+
}
|
|
649
|
+
return left.length - right.length;
|
|
353
650
|
}
|
|
354
651
|
function emptyTextRecords(state) {
|
|
355
652
|
return state.files
|
|
356
|
-
.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
653
|
+
.flatMap((file) => {
|
|
654
|
+
if (file.state === "indexed" && file.chunkCount === 0) {
|
|
655
|
+
return [
|
|
656
|
+
{
|
|
657
|
+
relativePath: file.relativePath,
|
|
658
|
+
checksum: file.checksum,
|
|
659
|
+
bytes: file.bytes,
|
|
660
|
+
mtimeMs: file.mtimeMs,
|
|
661
|
+
},
|
|
662
|
+
];
|
|
663
|
+
}
|
|
664
|
+
if (file.staleLastKnownGood &&
|
|
665
|
+
file.lastGoodChecksum !== null &&
|
|
666
|
+
file.lastGoodChunkCount === 0 &&
|
|
667
|
+
file.lastGoodBytes !== null &&
|
|
668
|
+
file.lastGoodMtimeMs !== null) {
|
|
669
|
+
return [
|
|
670
|
+
{
|
|
671
|
+
relativePath: file.relativePath,
|
|
672
|
+
checksum: file.lastGoodChecksum,
|
|
673
|
+
bytes: file.lastGoodBytes,
|
|
674
|
+
mtimeMs: file.lastGoodMtimeMs,
|
|
675
|
+
},
|
|
676
|
+
];
|
|
677
|
+
}
|
|
678
|
+
return [];
|
|
679
|
+
})
|
|
363
680
|
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
364
681
|
}
|
|
365
682
|
function ingestionErrors(state) {
|
|
@@ -373,34 +690,101 @@ async function validateIngestionTable(state, manifest, config, connection) {
|
|
|
373
690
|
}
|
|
374
691
|
throw new Error("Ingestion validation failed because the generated table is missing.");
|
|
375
692
|
}
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
693
|
+
const rowCount = await table.countRows();
|
|
694
|
+
await validateIngestionMetadata({
|
|
695
|
+
expectedChunkCount: manifest.chunkCount,
|
|
696
|
+
actualChunkCount: rowCount,
|
|
697
|
+
expectedFiles: indexedFilesFromState(state),
|
|
698
|
+
idRows: streamQueryRows(table.query().select(["id"]).orderBy({ columnName: "id", ascending: true })),
|
|
699
|
+
fileRows: streamQueryRows(table
|
|
700
|
+
.query()
|
|
701
|
+
.select(["relativePath", "checksum"])
|
|
702
|
+
.orderBy([
|
|
703
|
+
{ columnName: "relativePath", ascending: true },
|
|
704
|
+
{ columnName: "checksum", ascending: true },
|
|
705
|
+
])),
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
export async function validateIngestionMetadata(options) {
|
|
709
|
+
if (options.actualChunkCount !== options.expectedChunkCount) {
|
|
710
|
+
throw new Error(`Ingestion validation expected ${options.expectedChunkCount} rows but found ${options.actualChunkCount}.`);
|
|
379
711
|
}
|
|
380
|
-
|
|
381
|
-
|
|
712
|
+
let previousId = null;
|
|
713
|
+
for await (const row of options.idRows) {
|
|
714
|
+
const id = requiredStringField(row, "id", "Ingestion validation found an invalid chunk id.");
|
|
715
|
+
if (id === previousId) {
|
|
716
|
+
throw new Error("Ingestion validation found duplicate chunk identifiers.");
|
|
717
|
+
}
|
|
718
|
+
previousId = id;
|
|
382
719
|
}
|
|
383
|
-
const expectedFiles =
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
720
|
+
const expectedFiles = options.expectedFiles[Symbol.iterator]();
|
|
721
|
+
let expectedFile = expectedFiles.next();
|
|
722
|
+
let actualKey = null;
|
|
723
|
+
let actualCount = 0;
|
|
724
|
+
const validateActualFile = () => {
|
|
725
|
+
if (actualKey === null) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (expectedFile.done ||
|
|
729
|
+
`${expectedFile.value.relativePath}\0${expectedFile.value.checksum}` !== actualKey ||
|
|
730
|
+
expectedFile.value.chunkCount !== actualCount) {
|
|
731
|
+
throw new Error("Ingestion validation found rows that do not match the generated manifest.");
|
|
732
|
+
}
|
|
733
|
+
expectedFile = expectedFiles.next();
|
|
734
|
+
};
|
|
735
|
+
for await (const row of options.fileRows) {
|
|
736
|
+
const relativePath = requiredStringField(row, "relativePath", "Ingestion validation found an invalid source path.");
|
|
737
|
+
const checksum = requiredStringField(row, "checksum", "Ingestion validation found an invalid source checksum.");
|
|
738
|
+
const key = `${relativePath}\0${checksum}`;
|
|
739
|
+
if (actualKey !== key) {
|
|
740
|
+
validateActualFile();
|
|
741
|
+
actualKey = key;
|
|
742
|
+
actualCount = 0;
|
|
743
|
+
}
|
|
744
|
+
actualCount += 1;
|
|
391
745
|
}
|
|
392
|
-
|
|
393
|
-
|
|
746
|
+
validateActualFile();
|
|
747
|
+
if (!expectedFile.done) {
|
|
394
748
|
throw new Error("Ingestion validation found rows that do not match the generated manifest.");
|
|
395
749
|
}
|
|
396
750
|
}
|
|
751
|
+
async function* streamQueryRows(query) {
|
|
752
|
+
for await (const batch of query) {
|
|
753
|
+
for (let index = 0; index < batch.numRows; index += 1) {
|
|
754
|
+
yield batch.get(index);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function requiredStringField(row, key, message, allowEmpty = false) {
|
|
759
|
+
const value = tableRowField(row, key);
|
|
760
|
+
if (typeof value !== "string" || (!allowEmpty && value.length === 0)) {
|
|
761
|
+
throw new Error(message);
|
|
762
|
+
}
|
|
763
|
+
return value;
|
|
764
|
+
}
|
|
765
|
+
function tableRowField(row, key) {
|
|
766
|
+
return typeof row === "object" && row !== null ? Reflect.get(row, key) : undefined;
|
|
767
|
+
}
|
|
397
768
|
function ingestFileBatchSize(value) {
|
|
398
769
|
const batchSize = value ?? DEFAULT_INGEST_FILE_BATCH_SIZE;
|
|
399
770
|
if (!Number.isInteger(batchSize) || batchSize <= 0) {
|
|
400
771
|
throw new Error("batchSize must be a positive integer.");
|
|
401
772
|
}
|
|
773
|
+
if (batchSize > MAX_INGEST_FILE_BATCH_SIZE) {
|
|
774
|
+
throw new Error(`batchSize must be at most ${MAX_INGEST_FILE_BATCH_SIZE}.`);
|
|
775
|
+
}
|
|
402
776
|
return batchSize;
|
|
403
777
|
}
|
|
778
|
+
function sqlString(value) {
|
|
779
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
780
|
+
}
|
|
781
|
+
function incrementalFailurePolicy(options, config) {
|
|
782
|
+
const policy = options.incrementalFailurePolicy ?? config.incrementalFailurePolicy;
|
|
783
|
+
if (policy !== "preserve-last-good" && policy !== "remove-stale") {
|
|
784
|
+
throw new Error('incrementalFailurePolicy must be either "preserve-last-good" or "remove-stale".');
|
|
785
|
+
}
|
|
786
|
+
return policy;
|
|
787
|
+
}
|
|
404
788
|
function valueBatches(values, batchSize) {
|
|
405
789
|
const batches = [];
|
|
406
790
|
for (let index = 0; index < values.length; index += batchSize) {
|
|
@@ -408,12 +792,49 @@ function valueBatches(values, batchSize) {
|
|
|
408
792
|
}
|
|
409
793
|
return batches;
|
|
410
794
|
}
|
|
795
|
+
function ingestionWindows(files, maxFiles, config) {
|
|
796
|
+
const windows = [];
|
|
797
|
+
let current = [];
|
|
798
|
+
let currentBytes = 0;
|
|
799
|
+
let currentChunks = 0;
|
|
800
|
+
for (const file of files) {
|
|
801
|
+
const estimatedChunks = estimatedChunkCount(file, config);
|
|
802
|
+
const exceedsWindow = current.length > 0 &&
|
|
803
|
+
(current.length >= maxFiles ||
|
|
804
|
+
currentBytes + file.bytes > MAX_INGEST_SOURCE_WINDOW_BYTES ||
|
|
805
|
+
currentChunks + estimatedChunks > MAX_INGEST_CHUNK_WINDOW);
|
|
806
|
+
if (exceedsWindow) {
|
|
807
|
+
windows.push(current);
|
|
808
|
+
current = [];
|
|
809
|
+
currentBytes = 0;
|
|
810
|
+
currentChunks = 0;
|
|
811
|
+
}
|
|
812
|
+
current.push(file);
|
|
813
|
+
currentBytes += file.bytes;
|
|
814
|
+
currentChunks += estimatedChunks;
|
|
815
|
+
}
|
|
816
|
+
if (current.length > 0) {
|
|
817
|
+
windows.push(current);
|
|
818
|
+
}
|
|
819
|
+
return windows;
|
|
820
|
+
}
|
|
821
|
+
function estimatedChunkCount(file, config) {
|
|
822
|
+
const step = Math.max(1, config.chunkSize - config.chunkOverlap);
|
|
823
|
+
return Math.max(1, Math.ceil(file.bytes / step));
|
|
824
|
+
}
|
|
411
825
|
export async function audit(cwd = process.cwd(), options = {}) {
|
|
412
826
|
const signal = operationSignal(options);
|
|
413
827
|
throwIfAborted(signal);
|
|
414
828
|
const config = await loadConfig(cwd);
|
|
829
|
+
return auditWithConfig(config, options);
|
|
830
|
+
}
|
|
831
|
+
export async function auditWithConfig(config, options = {}) {
|
|
832
|
+
const signal = operationSignal(options);
|
|
415
833
|
throwIfAborted(signal);
|
|
416
|
-
const inventory = await inventorySourceFiles(config,
|
|
834
|
+
const inventory = await inventorySourceFiles(config, {
|
|
835
|
+
...(signal ? { signal } : {}),
|
|
836
|
+
writeFingerprintCache: false,
|
|
837
|
+
});
|
|
417
838
|
throwIfAborted(signal);
|
|
418
839
|
const files = inventory.supportedFiles;
|
|
419
840
|
const inventoryMetrics = sourceInventoryMetrics(files);
|
|
@@ -423,7 +844,10 @@ export async function audit(cwd = process.cwd(), options = {}) {
|
|
|
423
844
|
const emptyTextFiles = await currentEmptyTextFiles(config, files, signal);
|
|
424
845
|
throwIfAborted(signal);
|
|
425
846
|
if (!table) {
|
|
426
|
-
return {
|
|
847
|
+
return applyAuditPreviewLimit({
|
|
848
|
+
mode: "deep",
|
|
849
|
+
inventoryVerified: true,
|
|
850
|
+
cost: "O(corpus)",
|
|
427
851
|
discoveredFiles: inventory.discoveredFiles,
|
|
428
852
|
supportedBytes: inventoryMetrics.supportedBytes,
|
|
429
853
|
largestFileBytes: inventoryMetrics.largestFileBytes,
|
|
@@ -437,55 +861,146 @@ export async function audit(cwd = process.cwd(), options = {}) {
|
|
|
437
861
|
staleInIndex: [],
|
|
438
862
|
totalChunks: 0,
|
|
439
863
|
chunkStats: summarizeChunkStats([]),
|
|
440
|
-
};
|
|
864
|
+
}, options.previewLimit);
|
|
441
865
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
866
|
+
try {
|
|
867
|
+
const counts = new Map();
|
|
868
|
+
const checksums = new Map();
|
|
869
|
+
const chunkStats = createAuditChunkStatsAccumulator();
|
|
870
|
+
for await (const row of streamQueryRows(table.query().select(["relativePath", "checksum", "contextPath", "text"]))) {
|
|
871
|
+
throwIfAborted(signal);
|
|
872
|
+
const relativePath = requiredStringField(row, "relativePath", "Audit found an invalid source path.");
|
|
873
|
+
const checksum = tableRowField(row, "checksum");
|
|
874
|
+
const contextPath = requiredStringField(row, "contextPath", "Audit found an invalid chunk context.", true);
|
|
875
|
+
const text = requiredStringField(row, "text", "Audit found invalid chunk text.");
|
|
876
|
+
counts.set(relativePath, (counts.get(relativePath) ?? 0) + 1);
|
|
877
|
+
if (typeof checksum === "string" && checksum.length > 0) {
|
|
878
|
+
const fileChecksums = checksums.get(relativePath) ?? new Set();
|
|
879
|
+
fileChecksums.add(checksum);
|
|
880
|
+
checksums.set(relativePath, fileChecksums);
|
|
881
|
+
}
|
|
882
|
+
recordAuditChunkStats(chunkStats, text.length, contextPath.trim().length > 0);
|
|
456
883
|
}
|
|
884
|
+
const supportedSet = new Set(supportedFiles);
|
|
885
|
+
const indexedSet = new Set(counts.keys());
|
|
886
|
+
const currentChecksums = new Map(files.map((file) => [file.relativePath, file.checksum]));
|
|
887
|
+
throwIfAborted(signal);
|
|
888
|
+
return applyAuditPreviewLimit({
|
|
889
|
+
mode: "deep",
|
|
890
|
+
inventoryVerified: true,
|
|
891
|
+
cost: "O(corpus)",
|
|
892
|
+
discoveredFiles: inventory.discoveredFiles,
|
|
893
|
+
supportedBytes: inventoryMetrics.supportedBytes,
|
|
894
|
+
largestFileBytes: inventoryMetrics.largestFileBytes,
|
|
895
|
+
indexedFiles: [...counts.entries()]
|
|
896
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
897
|
+
.map(([source, chunks]) => ({ source, chunks })),
|
|
898
|
+
supportedFiles,
|
|
899
|
+
skippedFiles: inventory.skippedFiles,
|
|
900
|
+
emptyTextFiles: [...emptyTextFiles].sort(),
|
|
901
|
+
unsupportedExtensions: summarizeUnsupportedExtensions(inventory.skippedFiles),
|
|
902
|
+
sourceDiagnostics: sourceDiagnostics(files, inventory.skippedFiles),
|
|
903
|
+
missingFromIndex: supportedFiles.filter((file) => !indexedSet.has(file) && !emptyTextFiles.has(file)),
|
|
904
|
+
staleInIndex: [...indexedSet]
|
|
905
|
+
.filter((file) => {
|
|
906
|
+
if (!supportedSet.has(file)) {
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
const currentChecksum = currentChecksums.get(file);
|
|
910
|
+
const indexedChecksums = checksums.get(file);
|
|
911
|
+
return !currentChecksum || !indexedChecksums?.has(currentChecksum);
|
|
912
|
+
})
|
|
913
|
+
.sort(),
|
|
914
|
+
totalChunks: chunkStats.count,
|
|
915
|
+
chunkStats: finishAuditChunkStats(chunkStats),
|
|
916
|
+
}, options.previewLimit);
|
|
457
917
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
918
|
+
finally {
|
|
919
|
+
closeRowsTable(table, config);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function applyAuditPreviewLimit(report, previewLimit) {
|
|
923
|
+
if (previewLimit === undefined) {
|
|
924
|
+
return report;
|
|
925
|
+
}
|
|
926
|
+
if (!Number.isSafeInteger(previewLimit) || previewLimit < 0) {
|
|
927
|
+
throw new Error("Audit preview limit must be a non-negative integer.");
|
|
928
|
+
}
|
|
929
|
+
const limit = previewLimit;
|
|
462
930
|
return {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
.
|
|
485
|
-
|
|
486
|
-
|
|
931
|
+
...report,
|
|
932
|
+
indexedFiles: report.indexedFiles.slice(0, limit),
|
|
933
|
+
supportedFiles: report.supportedFiles.slice(0, limit),
|
|
934
|
+
skippedFiles: report.skippedFiles.slice(0, limit),
|
|
935
|
+
emptyTextFiles: report.emptyTextFiles.slice(0, limit),
|
|
936
|
+
sourceDiagnostics: {
|
|
937
|
+
duplicateCandidates: report.sourceDiagnostics.duplicateCandidates.slice(0, limit),
|
|
938
|
+
archiveCandidates: report.sourceDiagnostics.archiveCandidates.slice(0, limit),
|
|
939
|
+
mirrorCandidates: report.sourceDiagnostics.mirrorCandidates.slice(0, limit),
|
|
940
|
+
},
|
|
941
|
+
missingFromIndex: report.missingFromIndex.slice(0, limit),
|
|
942
|
+
staleInIndex: report.staleInIndex.slice(0, limit),
|
|
943
|
+
omitted: {
|
|
944
|
+
indexedFiles: omittedCount(report.indexedFiles.length, limit),
|
|
945
|
+
supportedFiles: omittedCount(report.supportedFiles.length, limit),
|
|
946
|
+
skippedFiles: omittedCount(report.skippedFiles.length, limit),
|
|
947
|
+
emptyTextFiles: omittedCount(report.emptyTextFiles.length, limit),
|
|
948
|
+
duplicateCandidates: omittedCount(report.sourceDiagnostics.duplicateCandidates.length, limit),
|
|
949
|
+
archiveCandidates: omittedCount(report.sourceDiagnostics.archiveCandidates.length, limit),
|
|
950
|
+
mirrorCandidates: omittedCount(report.sourceDiagnostics.mirrorCandidates.length, limit),
|
|
951
|
+
missingFromIndex: omittedCount(report.missingFromIndex.length, limit),
|
|
952
|
+
staleInIndex: omittedCount(report.staleInIndex.length, limit),
|
|
953
|
+
},
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
function omittedCount(total, retained) {
|
|
957
|
+
return Math.max(0, total - retained);
|
|
958
|
+
}
|
|
959
|
+
function createAuditChunkStatsAccumulator() {
|
|
960
|
+
return {
|
|
961
|
+
count: 0,
|
|
962
|
+
totalChars: 0,
|
|
963
|
+
minChars: Number.POSITIVE_INFINITY,
|
|
964
|
+
maxChars: 0,
|
|
965
|
+
contextualChunks: 0,
|
|
966
|
+
lengthHistogram: new Map(),
|
|
487
967
|
};
|
|
488
968
|
}
|
|
969
|
+
function recordAuditChunkStats(stats, characters, contextual) {
|
|
970
|
+
stats.count += 1;
|
|
971
|
+
stats.totalChars += characters;
|
|
972
|
+
stats.minChars = Math.min(stats.minChars, characters);
|
|
973
|
+
stats.maxChars = Math.max(stats.maxChars, characters);
|
|
974
|
+
stats.contextualChunks += contextual ? 1 : 0;
|
|
975
|
+
stats.lengthHistogram.set(characters, (stats.lengthHistogram.get(characters) ?? 0) + 1);
|
|
976
|
+
}
|
|
977
|
+
function finishAuditChunkStats(stats) {
|
|
978
|
+
if (stats.count === 0) {
|
|
979
|
+
return summarizeChunkStats([]);
|
|
980
|
+
}
|
|
981
|
+
const sortedHistogram = [...stats.lengthHistogram].sort(([left], [right]) => left - right);
|
|
982
|
+
return {
|
|
983
|
+
count: stats.count,
|
|
984
|
+
minChars: stats.minChars,
|
|
985
|
+
averageChars: stats.totalChars / stats.count,
|
|
986
|
+
p50Chars: histogramPercentile(sortedHistogram, stats.count, 0.5),
|
|
987
|
+
p95Chars: histogramPercentile(sortedHistogram, stats.count, 0.95),
|
|
988
|
+
maxChars: stats.maxChars,
|
|
989
|
+
contextualChunks: stats.contextualChunks,
|
|
990
|
+
contextualRatio: stats.contextualChunks / stats.count,
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function histogramPercentile(histogram, count, quantile) {
|
|
994
|
+
const target = Math.max(1, Math.ceil(count * quantile));
|
|
995
|
+
let cumulative = 0;
|
|
996
|
+
for (const [value, occurrences] of histogram) {
|
|
997
|
+
cumulative += occurrences;
|
|
998
|
+
if (cumulative >= target) {
|
|
999
|
+
return value;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return histogram.at(-1)?.[0] ?? 0;
|
|
1003
|
+
}
|
|
489
1004
|
function sourceInventoryMetrics(files) {
|
|
490
1005
|
let supportedBytes = 0;
|
|
491
1006
|
let largestFileBytes = 0;
|