@jcode.labs/ragmir 2.17.1 → 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 +254 -47
- 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/store.js
CHANGED
|
@@ -1,16 +1,54 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { channel } from "node:diagnostics_channel";
|
|
3
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
4
|
+
import { readdir, readFile, rm, stat } from "node:fs/promises";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import * as lancedb from "@lancedb/lancedb";
|
|
5
7
|
import { INDEX_MANIFEST_FILENAME } from "./defaults.js";
|
|
8
|
+
import { writePrivateFileAtomic } from "./durable-file.js";
|
|
6
9
|
import { isRecord } from "./guards.js";
|
|
7
|
-
import { ensurePrivateDirectory
|
|
10
|
+
import { ensurePrivateDirectory } from "./permissions.js";
|
|
11
|
+
import { isIndexQualityReport } from "./quality-report.js";
|
|
8
12
|
const EMPTY_TEXT_FILES_MANIFEST = "empty-text-files.json";
|
|
13
|
+
const INDEX_MANIFEST_PREVIOUS_FILENAME = "index-manifest.previous.json";
|
|
14
|
+
const INDEX_FILES_PREFIX = "index-manifest.files.";
|
|
15
|
+
const INDEX_FILES_SUFFIX = ".jsonl";
|
|
16
|
+
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
17
|
+
const STREAM_WRITE_BYTES = 64 * 1_024;
|
|
18
|
+
export const INDEX_READ_DIAGNOSTICS_CHANNEL = "ragmir:index-read";
|
|
19
|
+
const indexReadDiagnostics = channel(INDEX_READ_DIAGNOSTICS_CHANNEL);
|
|
20
|
+
const manifestRecoveryDiagnostics = new WeakMap();
|
|
9
21
|
export async function connectStore(config) {
|
|
10
22
|
await ensurePrivateDirectory(config.storageDir);
|
|
11
|
-
|
|
23
|
+
const connection = await lancedb.connect(config.storageDir, {
|
|
12
24
|
readConsistencyInterval: 0,
|
|
13
25
|
});
|
|
26
|
+
publishIndexReadDiagnostics({ kind: "connection-open", projectRoot: config.projectRoot });
|
|
27
|
+
return connection;
|
|
28
|
+
}
|
|
29
|
+
export function closeStoreConnection(connection, config) {
|
|
30
|
+
if (!connection.isOpen()) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
connection.close();
|
|
34
|
+
publishIndexReadDiagnostics({ kind: "connection-close", projectRoot: config.projectRoot });
|
|
35
|
+
}
|
|
36
|
+
export function closeIndexReadSnapshot(snapshot, config) {
|
|
37
|
+
if (!snapshot.table) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
closeRowsTable(snapshot.table, config);
|
|
41
|
+
}
|
|
42
|
+
export function closeRowsTable(table, config) {
|
|
43
|
+
if (!table.isOpen()) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
table.close();
|
|
47
|
+
publishIndexReadDiagnostics({
|
|
48
|
+
kind: "table-close",
|
|
49
|
+
projectRoot: config.projectRoot,
|
|
50
|
+
tableName: table.name,
|
|
51
|
+
});
|
|
14
52
|
}
|
|
15
53
|
export async function writeRows(rows, config, connection) {
|
|
16
54
|
const tableName = await activeIndexTableName(config);
|
|
@@ -75,7 +113,7 @@ async function ensureLexicalIndex(table) {
|
|
|
75
113
|
}
|
|
76
114
|
try {
|
|
77
115
|
await table.createIndex("searchText", {
|
|
78
|
-
config: lancedb.Index.fts({ asciiFolding: true, lowercase: true }),
|
|
116
|
+
config: lancedb.Index.fts({ asciiFolding: true, lowercase: true, withPosition: true }),
|
|
79
117
|
});
|
|
80
118
|
return { created: true, warning: null };
|
|
81
119
|
}
|
|
@@ -87,25 +125,203 @@ async function ensureLexicalIndex(table) {
|
|
|
87
125
|
};
|
|
88
126
|
}
|
|
89
127
|
}
|
|
90
|
-
export async function writeIndexManifest(manifest, config) {
|
|
128
|
+
export async function writeIndexManifest(manifest, config, indexedFiles = manifest.indexedFiles) {
|
|
91
129
|
const manifestPath = path.join(config.storageDir, INDEX_MANIFEST_FILENAME);
|
|
92
|
-
|
|
130
|
+
const { indexedFiles: _indexedFiles, ...manifestHeader } = manifest;
|
|
131
|
+
let persistedManifest = manifestHeader;
|
|
132
|
+
let currentSnapshot = null;
|
|
133
|
+
if (indexedFiles) {
|
|
134
|
+
currentSnapshot = indexFilesSnapshotFilename(randomUUID());
|
|
135
|
+
const writtenFiles = await writePrivateJsonLinesAtomic(path.join(config.storageDir, currentSnapshot), indexedFiles, config.storageDir);
|
|
136
|
+
if (writtenFiles !== manifest.fileCount) {
|
|
137
|
+
await rm(path.join(config.storageDir, currentSnapshot), { force: true });
|
|
138
|
+
throw new Error(`Index manifest expected ${manifest.fileCount} files but received ${writtenFiles}.`);
|
|
139
|
+
}
|
|
140
|
+
persistedManifest = { ...manifestHeader, indexedFilesSnapshot: currentSnapshot };
|
|
141
|
+
}
|
|
142
|
+
const previousSelection = await resolveIndexManifest(config);
|
|
143
|
+
const previousManifest = previousSelection.manifest ?? persistedManifest;
|
|
144
|
+
await writePrivateJsonAtomic(path.join(config.storageDir, INDEX_MANIFEST_PREVIOUS_FILENAME), previousManifest, config.storageDir);
|
|
145
|
+
await writePrivateJsonAtomic(manifestPath, persistedManifest, config.storageDir);
|
|
146
|
+
manifestRecoveryDiagnostics.set(config, {
|
|
147
|
+
canonicalStatus: "valid",
|
|
148
|
+
previousStatus: "valid",
|
|
149
|
+
selected: "canonical",
|
|
150
|
+
warning: null,
|
|
151
|
+
});
|
|
152
|
+
await removeStaleIndexFileSnapshots(new Set([currentSnapshot, previousManifest.indexedFilesSnapshot].filter((filename) => filename !== null && filename !== undefined)), config);
|
|
93
153
|
}
|
|
94
154
|
export async function readIndexManifest(config) {
|
|
155
|
+
const raw = (await resolveIndexManifest(config)).manifest;
|
|
156
|
+
if (!raw) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const { indexedFilesSnapshot, qualityReport, ...manifest } = raw;
|
|
160
|
+
const indexedFiles = indexedFilesSnapshot
|
|
161
|
+
? await readIndexFileSnapshot(indexedFilesSnapshot, config)
|
|
162
|
+
: raw.indexedFiles;
|
|
163
|
+
if (indexedFilesSnapshot && (!indexedFiles || indexedFiles.length !== raw.fileCount)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const hydrated = indexedFiles ? { ...manifest, indexedFiles } : manifest;
|
|
167
|
+
return isIndexQualityReport(qualityReport) ? { ...hydrated, qualityReport } : hydrated;
|
|
168
|
+
}
|
|
169
|
+
export async function readIndexManifestHeader(config) {
|
|
170
|
+
publishIndexReadDiagnostics({ kind: "manifest-read", projectRoot: config.projectRoot });
|
|
171
|
+
const raw = (await resolveIndexManifest(config)).manifest;
|
|
172
|
+
if (!raw) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const { indexedFiles: _files, indexedFilesSnapshot: _snapshot, qualityReport, ...manifest } = raw;
|
|
176
|
+
return isIndexQualityReport(qualityReport) ? { ...manifest, qualityReport } : manifest;
|
|
177
|
+
}
|
|
178
|
+
export async function readIndexManifestFilePage(config, offset, limit) {
|
|
179
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
180
|
+
throw new Error("Index manifest file page offset must be a non-negative integer.");
|
|
181
|
+
}
|
|
182
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
183
|
+
throw new Error("Index manifest file page limit must be a positive integer.");
|
|
184
|
+
}
|
|
185
|
+
publishIndexReadDiagnostics({ kind: "manifest-read", projectRoot: config.projectRoot });
|
|
186
|
+
const raw = (await resolveIndexManifest(config)).manifest;
|
|
187
|
+
if (!raw) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const files = raw.indexedFilesSnapshot
|
|
191
|
+
? await readIndexFileSnapshotPage(raw.indexedFilesSnapshot, config, offset, limit)
|
|
192
|
+
: (raw.indexedFiles?.slice(offset, offset + limit) ?? null);
|
|
193
|
+
if (!files || offset > raw.fileCount || offset + files.length > raw.fileCount) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const nextOffset = offset + files.length < raw.fileCount ? offset + files.length : null;
|
|
197
|
+
return { files, total: raw.fileCount, offset, limit, nextOffset };
|
|
198
|
+
}
|
|
199
|
+
export async function loadIndexReadSnapshot(config, connection) {
|
|
200
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
201
|
+
const fingerprintBefore = await indexManifestFingerprint(config);
|
|
202
|
+
const manifest = await readIndexManifestHeader(config);
|
|
203
|
+
const fingerprintAfter = await indexManifestFingerprint(config);
|
|
204
|
+
if (fingerprintBefore !== fingerprintAfter) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const immutableManifest = manifest ? Object.freeze({ ...manifest }) : null;
|
|
208
|
+
const tableName = immutableManifest?.tableName ?? config.tableName;
|
|
209
|
+
const recoveryFailed = immutableManifest === null && indexManifestRecoveryWarning(config) !== null;
|
|
210
|
+
return {
|
|
211
|
+
manifest: immutableManifest,
|
|
212
|
+
manifestFingerprint: fingerprintAfter,
|
|
213
|
+
tableName,
|
|
214
|
+
table: recoveryFailed ? null : await openRowsTableByName(tableName, config, connection),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
throw new Error("Index generation changed repeatedly while opening a read snapshot. Retry the operation.");
|
|
218
|
+
}
|
|
219
|
+
export async function indexReadSnapshotCurrent(snapshot, config) {
|
|
220
|
+
return snapshot.manifestFingerprint === (await indexManifestFingerprint(config));
|
|
221
|
+
}
|
|
222
|
+
async function indexManifestFingerprint(config) {
|
|
223
|
+
const [canonical, previous] = await Promise.all([
|
|
224
|
+
fileFingerprint(path.join(config.storageDir, INDEX_MANIFEST_FILENAME)),
|
|
225
|
+
fileFingerprint(path.join(config.storageDir, INDEX_MANIFEST_PREVIOUS_FILENAME)),
|
|
226
|
+
]);
|
|
227
|
+
return canonical === null && previous === null
|
|
228
|
+
? null
|
|
229
|
+
: `${canonical ?? "missing"}|${previous ?? "missing"}`;
|
|
230
|
+
}
|
|
231
|
+
async function readRawIndexManifest(config, filename = INDEX_MANIFEST_FILENAME) {
|
|
232
|
+
return JSON.parse(await readFile(path.join(config.storageDir, filename), "utf8"));
|
|
233
|
+
}
|
|
234
|
+
export function indexManifestRecoveryDiagnostic(config) {
|
|
235
|
+
return manifestRecoveryDiagnostics.get(config) ?? null;
|
|
236
|
+
}
|
|
237
|
+
export function indexManifestRecoveryWarning(config) {
|
|
238
|
+
return indexManifestRecoveryDiagnostic(config)?.warning ?? null;
|
|
239
|
+
}
|
|
240
|
+
async function resolveIndexManifest(config) {
|
|
241
|
+
const canonical = await readManifestCandidate(INDEX_MANIFEST_FILENAME, config);
|
|
242
|
+
if (canonical.status === "valid") {
|
|
243
|
+
const recovery = {
|
|
244
|
+
canonicalStatus: "valid",
|
|
245
|
+
previousStatus: "not-checked",
|
|
246
|
+
selected: "canonical",
|
|
247
|
+
warning: null,
|
|
248
|
+
};
|
|
249
|
+
manifestRecoveryDiagnostics.set(config, recovery);
|
|
250
|
+
return { manifest: canonical.manifest, recovery };
|
|
251
|
+
}
|
|
252
|
+
const previous = await readManifestCandidate(INDEX_MANIFEST_PREVIOUS_FILENAME, config);
|
|
253
|
+
if (previous.status === "valid") {
|
|
254
|
+
const recovery = {
|
|
255
|
+
canonicalStatus: canonical.status,
|
|
256
|
+
previousStatus: "valid",
|
|
257
|
+
selected: "previous",
|
|
258
|
+
warning: canonical.status === "missing"
|
|
259
|
+
? "Canonical index manifest is missing. Ragmir recovered the last validated generation; run `rgr ingest --rebuild` to repair the canonical sidecar."
|
|
260
|
+
: "Canonical index manifest is invalid. Ragmir recovered the last validated generation; run `rgr ingest --rebuild` to repair the canonical sidecar.",
|
|
261
|
+
};
|
|
262
|
+
manifestRecoveryDiagnostics.set(config, recovery);
|
|
263
|
+
publishIndexReadDiagnostics({
|
|
264
|
+
kind: "manifest-recovery",
|
|
265
|
+
projectRoot: config.projectRoot,
|
|
266
|
+
recoveryReason: canonical.status === "missing" ? "canonical-missing" : "canonical-invalid",
|
|
267
|
+
...(previous.manifest?.tableName ? { tableName: previous.manifest.tableName } : {}),
|
|
268
|
+
});
|
|
269
|
+
return { manifest: previous.manifest, recovery };
|
|
270
|
+
}
|
|
271
|
+
const noSidecar = canonical.status === "missing" && previous.status === "missing";
|
|
272
|
+
const recovery = {
|
|
273
|
+
canonicalStatus: canonical.status,
|
|
274
|
+
previousStatus: previous.status,
|
|
275
|
+
selected: null,
|
|
276
|
+
warning: noSidecar
|
|
277
|
+
? null
|
|
278
|
+
: "Index manifest recovery failed because no valid canonical or previous sidecar is available. Run `rgr ingest --rebuild`; Ragmir will not select an unverified default table.",
|
|
279
|
+
};
|
|
280
|
+
manifestRecoveryDiagnostics.set(config, recovery);
|
|
281
|
+
return { manifest: null, recovery };
|
|
282
|
+
}
|
|
283
|
+
async function readManifestCandidate(filename, config) {
|
|
95
284
|
try {
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
98
|
-
return null;
|
|
285
|
+
const value = await readRawIndexManifest(config, filename);
|
|
286
|
+
if (!isIndexManifest(value) || !manifestReferencesManagedTable(value, config)) {
|
|
287
|
+
return { status: "invalid", manifest: null };
|
|
288
|
+
}
|
|
289
|
+
if (value.indexedFiles && value.indexedFiles.length !== value.fileCount) {
|
|
290
|
+
return { status: "invalid", manifest: null };
|
|
291
|
+
}
|
|
292
|
+
if (value.indexedFilesSnapshot) {
|
|
293
|
+
const snapshot = await stat(path.join(config.storageDir, value.indexedFilesSnapshot));
|
|
294
|
+
if (!snapshot.isFile()) {
|
|
295
|
+
return { status: "invalid", manifest: null };
|
|
296
|
+
}
|
|
99
297
|
}
|
|
100
|
-
return
|
|
298
|
+
return { status: "valid", manifest: value };
|
|
101
299
|
}
|
|
102
300
|
catch (error) {
|
|
103
|
-
// The manifest is purely diagnostic. A missing file (pre-manifest index) or
|
|
104
|
-
// a corrupt/unreadable file should surface as "no freshness info" rather
|
|
105
|
-
// than fail the caller.
|
|
106
301
|
if (error instanceof SyntaxError) {
|
|
107
|
-
return null;
|
|
302
|
+
return { status: "invalid", manifest: null };
|
|
303
|
+
}
|
|
304
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
305
|
+
return { status: "missing", manifest: null };
|
|
108
306
|
}
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function manifestReferencesManagedTable(manifest, config) {
|
|
311
|
+
const tableName = manifest.tableName ?? config.tableName;
|
|
312
|
+
if (tableName === config.tableName) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
const prefix = `${config.tableName}__generation_`;
|
|
316
|
+
const generationId = tableName.startsWith(prefix) ? tableName.slice(prefix.length) : "";
|
|
317
|
+
return /^[0-9a-f]{32}$/iu.test(generationId);
|
|
318
|
+
}
|
|
319
|
+
async function fileFingerprint(filePath) {
|
|
320
|
+
try {
|
|
321
|
+
const details = await stat(filePath);
|
|
322
|
+
return `${details.dev}:${details.ino}:${details.size}:${details.mtimeMs}`;
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
109
325
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
110
326
|
return null;
|
|
111
327
|
}
|
|
@@ -121,16 +337,133 @@ function isIndexManifest(value) {
|
|
|
121
337
|
typeof value.ragmirVersion === "string" &&
|
|
122
338
|
(value.embeddingProvider === "local-hash" || value.embeddingProvider === "transformers") &&
|
|
123
339
|
typeof value.embeddingModel === "string" &&
|
|
340
|
+
(!("embeddingModelRevision" in value) || typeof value.embeddingModelRevision === "string") &&
|
|
341
|
+
(!("embeddingModelDigest" in value) ||
|
|
342
|
+
value.embeddingModelDigest === null ||
|
|
343
|
+
typeof value.embeddingModelDigest === "string") &&
|
|
124
344
|
(!("indexPolicyFingerprint" in value) || typeof value.indexPolicyFingerprint === "string") &&
|
|
125
345
|
(!("vectorDimension" in value) || typeof value.vectorDimension === "number") &&
|
|
126
346
|
(!("vectorDistanceMetric" in value) || typeof value.vectorDistanceMetric === "string") &&
|
|
347
|
+
(!("vectorIndex" in value) || isVectorIndexManifest(value.vectorIndex)) &&
|
|
127
348
|
typeof value.chunkSize === "number" &&
|
|
128
349
|
typeof value.chunkOverlap === "number" &&
|
|
129
350
|
typeof value.fileCount === "number" &&
|
|
130
351
|
typeof value.chunkCount === "number" &&
|
|
352
|
+
(!("corpusFingerprint" in value) ||
|
|
353
|
+
(typeof value.corpusFingerprint === "string" &&
|
|
354
|
+
/^[0-9a-f]{64}$/u.test(value.corpusFingerprint))) &&
|
|
131
355
|
(!("tableName" in value) || typeof value.tableName === "string") &&
|
|
132
356
|
(!("indexedFiles" in value) ||
|
|
133
|
-
(Array.isArray(value.indexedFiles) && value.indexedFiles.every(isIndexManifestFile)))
|
|
357
|
+
(Array.isArray(value.indexedFiles) && value.indexedFiles.every(isIndexManifestFile))) &&
|
|
358
|
+
(!("indexedFilesSnapshot" in value) ||
|
|
359
|
+
isIndexFilesSnapshotFilename(value.indexedFilesSnapshot)) &&
|
|
360
|
+
!("indexedFiles" in value && "indexedFilesSnapshot" in value) &&
|
|
361
|
+
(!("staleFiles" in value) ||
|
|
362
|
+
(Array.isArray(value.staleFiles) && value.staleFiles.every(isIndexManifestStaleFile))) &&
|
|
363
|
+
(!("health" in value) || isIndexHealthSnapshot(value.health)) &&
|
|
364
|
+
(!("maintenance" in value) || isIndexMaintenanceSnapshot(value.maintenance)));
|
|
365
|
+
}
|
|
366
|
+
function isIndexHealthSnapshot(value) {
|
|
367
|
+
if (!isRecord(value) || !isRecord(value.previews) || !isRecord(value.previewOmitted)) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
return (value.schemaVersion === 1 &&
|
|
371
|
+
typeof value.checkedAt === "string" &&
|
|
372
|
+
isNonNegativeInteger(value.discoveredFiles) &&
|
|
373
|
+
isNonNegativeInteger(value.supportedFiles) &&
|
|
374
|
+
isNonNegativeNumber(value.supportedBytes) &&
|
|
375
|
+
isNonNegativeNumber(value.largestFileBytes) &&
|
|
376
|
+
isNonNegativeInteger(value.skippedFiles) &&
|
|
377
|
+
isNonNegativeInteger(value.unsupportedFiles) &&
|
|
378
|
+
isNonNegativeInteger(value.oversizedFiles) &&
|
|
379
|
+
isNonNegativeInteger(value.sensitiveFiles) &&
|
|
380
|
+
isNonNegativeInteger(value.emptyTextFiles) &&
|
|
381
|
+
isNonNegativeInteger(value.missingFromIndex) &&
|
|
382
|
+
isNonNegativeInteger(value.staleInIndex) &&
|
|
383
|
+
isStringArray(value.previews.missingFromIndex) &&
|
|
384
|
+
isStringArray(value.previews.staleInIndex) &&
|
|
385
|
+
isStringArray(value.previews.emptyTextFiles) &&
|
|
386
|
+
isNonNegativeInteger(value.previewOmitted.missingFromIndex) &&
|
|
387
|
+
isNonNegativeInteger(value.previewOmitted.staleInIndex) &&
|
|
388
|
+
isNonNegativeInteger(value.previewOmitted.emptyTextFiles) &&
|
|
389
|
+
isStringCountRecord(value.skippedByReason) &&
|
|
390
|
+
isSourceDiagnostics(value.sourceDiagnostics) &&
|
|
391
|
+
typeof value.securityCheckedAt === "string" &&
|
|
392
|
+
isStringArray(value.securityWarnings));
|
|
393
|
+
}
|
|
394
|
+
function isIndexMaintenanceSnapshot(value) {
|
|
395
|
+
if (!isRecord(value) || !isRecord(value.fragments) || !isRecord(value.fullTextIndex)) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
return (value.schemaVersion === 1 &&
|
|
399
|
+
typeof value.checkedAt === "string" &&
|
|
400
|
+
["missing", "healthy", "needed", "completed", "warning"].includes(String(value.status)) &&
|
|
401
|
+
(value.tableVersion === null || isNonNegativeInteger(value.tableVersion)) &&
|
|
402
|
+
isNonNegativeInteger(value.mutationsSinceOptimization) &&
|
|
403
|
+
isNonNegativeInteger(value.fragments.total) &&
|
|
404
|
+
isNonNegativeInteger(value.fragments.small) &&
|
|
405
|
+
isNonNegativeNumber(value.fragments.smallRatio) &&
|
|
406
|
+
typeof value.fullTextIndex.present === "boolean" &&
|
|
407
|
+
isNonNegativeInteger(value.fullTextIndex.indexedRows) &&
|
|
408
|
+
isNonNegativeInteger(value.fullTextIndex.unindexedRows) &&
|
|
409
|
+
typeof value.fullTextIndex.complete === "boolean" &&
|
|
410
|
+
(value.warning === null || typeof value.warning === "string"));
|
|
411
|
+
}
|
|
412
|
+
function isNonNegativeInteger(value) {
|
|
413
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
414
|
+
}
|
|
415
|
+
function isNonNegativeNumber(value) {
|
|
416
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
417
|
+
}
|
|
418
|
+
function isStringArray(value) {
|
|
419
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
420
|
+
}
|
|
421
|
+
function isStringCountRecord(value) {
|
|
422
|
+
return isRecord(value) && Object.values(value).every(isNonNegativeInteger);
|
|
423
|
+
}
|
|
424
|
+
function isSourceDiagnostics(value) {
|
|
425
|
+
if (!isRecord(value)) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
return (Array.isArray(value.duplicateCandidates) &&
|
|
429
|
+
value.duplicateCandidates.every((entry) => isRecord(entry) && typeof entry.key === "string" && isStringArray(entry.files)) &&
|
|
430
|
+
isSourcePathCandidates(value.archiveCandidates) &&
|
|
431
|
+
isSourcePathCandidates(value.mirrorCandidates));
|
|
432
|
+
}
|
|
433
|
+
function isSourcePathCandidates(value) {
|
|
434
|
+
return (Array.isArray(value) &&
|
|
435
|
+
value.every((entry) => isRecord(entry) &&
|
|
436
|
+
typeof entry.relativePath === "string" &&
|
|
437
|
+
typeof entry.reason === "string"));
|
|
438
|
+
}
|
|
439
|
+
function isVectorIndexManifest(value) {
|
|
440
|
+
if (!isRecord(value) || !isRecord(value.parameters)) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
const parameters = value.parameters;
|
|
444
|
+
return (value.policyVersion === 1 &&
|
|
445
|
+
(value.strategy === "exact" || value.strategy === "ivf-pq" || value.strategy === "hnsw-sq") &&
|
|
446
|
+
(value.indexName === null || typeof value.indexName === "string") &&
|
|
447
|
+
(value.indexType === null || typeof value.indexType === "string") &&
|
|
448
|
+
value.column === "vector" &&
|
|
449
|
+
typeof value.distanceMetric === "string" &&
|
|
450
|
+
typeof value.dimension === "number" &&
|
|
451
|
+
Number.isInteger(value.dimension) &&
|
|
452
|
+
value.dimension >= 0 &&
|
|
453
|
+
typeof value.modelFingerprint === "string" &&
|
|
454
|
+
typeof value.indexedRows === "number" &&
|
|
455
|
+
Number.isInteger(value.indexedRows) &&
|
|
456
|
+
value.indexedRows >= 0 &&
|
|
457
|
+
typeof value.unindexedRows === "number" &&
|
|
458
|
+
Number.isInteger(value.unindexedRows) &&
|
|
459
|
+
value.unindexedRows >= 0 &&
|
|
460
|
+
typeof value.coverage === "number" &&
|
|
461
|
+
value.coverage >= 0 &&
|
|
462
|
+
value.coverage <= 1 &&
|
|
463
|
+
["numPartitions", "numSubVectors", "nprobes", "refineFactor", "ef"].every((key) => !(key in parameters) || isPositiveInteger(parameters[key])));
|
|
464
|
+
}
|
|
465
|
+
function isPositiveInteger(value) {
|
|
466
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
134
467
|
}
|
|
135
468
|
function isIndexManifestFile(value) {
|
|
136
469
|
return (isRecord(value) &&
|
|
@@ -140,6 +473,14 @@ function isIndexManifestFile(value) {
|
|
|
140
473
|
(!("bytes" in value) || typeof value.bytes === "number") &&
|
|
141
474
|
(!("mtimeMs" in value) || typeof value.mtimeMs === "number"));
|
|
142
475
|
}
|
|
476
|
+
function isIndexManifestStaleFile(value) {
|
|
477
|
+
return (isRecord(value) &&
|
|
478
|
+
typeof value.relativePath === "string" &&
|
|
479
|
+
typeof value.currentChecksum === "string" &&
|
|
480
|
+
typeof value.lastGoodChecksum === "string" &&
|
|
481
|
+
typeof value.chunkCount === "number" &&
|
|
482
|
+
typeof value.error === "string");
|
|
483
|
+
}
|
|
143
484
|
export async function writeEmptyTextFiles(records, config) {
|
|
144
485
|
const manifestPath = path.join(config.storageDir, EMPTY_TEXT_FILES_MANIFEST);
|
|
145
486
|
if (records.length === 0) {
|
|
@@ -168,16 +509,29 @@ export async function openRowsTable(config, connection) {
|
|
|
168
509
|
return openRowsTableByName(await activeIndexTableName(config), config, connection);
|
|
169
510
|
}
|
|
170
511
|
export async function openRowsTableByName(tableName, config, connection) {
|
|
512
|
+
if (!connection && !existsSync(config.storageDir)) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
171
515
|
return withConnection(config, connection, async (db) => {
|
|
172
516
|
const tableNames = await db.tableNames();
|
|
173
517
|
if (!tableNames.includes(tableName)) {
|
|
174
518
|
return null;
|
|
175
519
|
}
|
|
520
|
+
publishIndexReadDiagnostics({
|
|
521
|
+
kind: "table-open",
|
|
522
|
+
projectRoot: config.projectRoot,
|
|
523
|
+
tableName,
|
|
524
|
+
});
|
|
176
525
|
return db.openTable(tableName);
|
|
177
526
|
});
|
|
178
527
|
}
|
|
528
|
+
function publishIndexReadDiagnostics(event) {
|
|
529
|
+
if (indexReadDiagnostics.hasSubscribers) {
|
|
530
|
+
indexReadDiagnostics.publish(event);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
179
533
|
export async function activeIndexTableName(config) {
|
|
180
|
-
return (await
|
|
534
|
+
return (await readIndexManifestHeader(config))?.tableName ?? config.tableName;
|
|
181
535
|
}
|
|
182
536
|
export async function dropRowsTable(tableName, config, connection) {
|
|
183
537
|
await withConnection(config, connection, async (db) => {
|
|
@@ -200,19 +554,44 @@ export async function countRows(config) {
|
|
|
200
554
|
function storedRows(rows) {
|
|
201
555
|
return rows.map((row) => ({
|
|
202
556
|
...row,
|
|
557
|
+
lineStart: row.lineStart ?? 0,
|
|
558
|
+
lineEnd: row.lineEnd ?? 0,
|
|
203
559
|
pageStart: row.pageStart ?? 0,
|
|
204
560
|
pageEnd: row.pageEnd ?? 0,
|
|
561
|
+
locationKind: row.locationKind ?? "",
|
|
562
|
+
locationStart: row.locationStart ?? 0,
|
|
563
|
+
locationEnd: row.locationEnd ?? 0,
|
|
564
|
+
locationLabel: row.locationLabel ?? "",
|
|
565
|
+
cellStart: row.cellStart ?? "",
|
|
566
|
+
cellEnd: row.cellEnd ?? "",
|
|
205
567
|
}));
|
|
206
568
|
}
|
|
207
569
|
function vectorRowFromStored(row) {
|
|
208
|
-
const { pageStart, pageEnd, ...rest } = row;
|
|
570
|
+
const { lineStart, lineEnd, pageStart, pageEnd, locationKind, locationStart, locationEnd, locationLabel, cellStart, cellEnd, ...rest } = row;
|
|
209
571
|
return {
|
|
210
572
|
...rest,
|
|
211
573
|
vector: normalizeVector(row.vector),
|
|
574
|
+
...(positiveStoredInteger(lineStart) ? { lineStart } : {}),
|
|
575
|
+
...(positiveStoredInteger(lineEnd) ? { lineEnd } : {}),
|
|
212
576
|
...(typeof pageStart === "number" && pageStart > 0 ? { pageStart } : {}),
|
|
213
577
|
...(typeof pageEnd === "number" && pageEnd > 0 ? { pageEnd } : {}),
|
|
578
|
+
...(isSourceLocationKind(locationKind) ? { locationKind } : {}),
|
|
579
|
+
...(positiveStoredInteger(locationStart) ? { locationStart } : {}),
|
|
580
|
+
...(positiveStoredInteger(locationEnd) ? { locationEnd } : {}),
|
|
581
|
+
...(nonEmptyStoredString(locationLabel) ? { locationLabel } : {}),
|
|
582
|
+
...(nonEmptyStoredString(cellStart) ? { cellStart } : {}),
|
|
583
|
+
...(nonEmptyStoredString(cellEnd) ? { cellEnd } : {}),
|
|
214
584
|
};
|
|
215
585
|
}
|
|
586
|
+
function positiveStoredInteger(value) {
|
|
587
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
588
|
+
}
|
|
589
|
+
function nonEmptyStoredString(value) {
|
|
590
|
+
return typeof value === "string" && value.length > 0;
|
|
591
|
+
}
|
|
592
|
+
function isSourceLocationKind(value) {
|
|
593
|
+
return value === "page" || value === "slide" || value === "sheet" || value === "epub";
|
|
594
|
+
}
|
|
216
595
|
function sqlString(value) {
|
|
217
596
|
return `'${value.replaceAll("'", "''")}'`;
|
|
218
597
|
}
|
|
@@ -260,20 +639,176 @@ async function withConnection(config, connection, operation) {
|
|
|
260
639
|
}
|
|
261
640
|
finally {
|
|
262
641
|
if (!connection) {
|
|
263
|
-
activeConnection
|
|
642
|
+
closeStoreConnection(activeConnection, config);
|
|
264
643
|
}
|
|
265
644
|
}
|
|
266
645
|
}
|
|
267
646
|
async function writePrivateJsonAtomic(targetPath, value, directory) {
|
|
268
|
-
await
|
|
269
|
-
|
|
647
|
+
await writePrivateFileAtomic(targetPath, directory, async (handle) => {
|
|
648
|
+
await handle.writeFile(JSON.stringify(value, null, 2), "utf8");
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
async function writePrivateJsonLinesAtomic(targetPath, values, directory) {
|
|
652
|
+
let count = 0;
|
|
653
|
+
await writePrivateFileAtomic(targetPath, directory, async (handle) => {
|
|
654
|
+
let previousKey = null;
|
|
655
|
+
let buffered = "";
|
|
656
|
+
for (const value of values) {
|
|
657
|
+
if (!isIndexManifestFile(value)) {
|
|
658
|
+
throw new Error("Index manifest contains invalid file metadata.");
|
|
659
|
+
}
|
|
660
|
+
const key = `${value.relativePath}\0${value.checksum}`;
|
|
661
|
+
if (previousKey !== null && key <= previousKey) {
|
|
662
|
+
throw new Error("Index manifest file metadata must be unique and sorted.");
|
|
663
|
+
}
|
|
664
|
+
previousKey = key;
|
|
665
|
+
count += 1;
|
|
666
|
+
buffered += `${JSON.stringify(value)}\n`;
|
|
667
|
+
if (Buffer.byteLength(buffered) >= STREAM_WRITE_BYTES) {
|
|
668
|
+
await handle.writeFile(buffered, "utf8");
|
|
669
|
+
buffered = "";
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (buffered) {
|
|
673
|
+
await handle.writeFile(buffered, "utf8");
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
return count;
|
|
677
|
+
}
|
|
678
|
+
async function readIndexFileSnapshot(filename, config) {
|
|
679
|
+
if (!isIndexFilesSnapshotFilename(filename)) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
const values = [];
|
|
683
|
+
const stream = createReadStream(path.join(config.storageDir, filename));
|
|
684
|
+
stream.setEncoding("utf8");
|
|
685
|
+
let buffered = "";
|
|
686
|
+
let previousKey = null;
|
|
687
|
+
const applyLine = (line) => {
|
|
688
|
+
let value;
|
|
689
|
+
try {
|
|
690
|
+
value = JSON.parse(line);
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
if (!isIndexManifestFile(value)) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
const key = `${value.relativePath}\0${value.checksum}`;
|
|
699
|
+
if (previousKey !== null && key <= previousKey) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
previousKey = key;
|
|
703
|
+
values.push(value);
|
|
704
|
+
return true;
|
|
705
|
+
};
|
|
706
|
+
try {
|
|
707
|
+
for await (const chunk of stream) {
|
|
708
|
+
buffered += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
709
|
+
let lineEnd = buffered.indexOf("\n");
|
|
710
|
+
while (lineEnd >= 0) {
|
|
711
|
+
const line = buffered.slice(0, lineEnd);
|
|
712
|
+
buffered = buffered.slice(lineEnd + 1);
|
|
713
|
+
if (!line || !applyLine(line)) {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
lineEnd = buffered.indexOf("\n");
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (buffered && !applyLine(buffered)) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
return values;
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async function readIndexFileSnapshotPage(filename, config, offset, limit) {
|
|
732
|
+
if (!isIndexFilesSnapshotFilename(filename)) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const values = [];
|
|
736
|
+
const stream = createReadStream(path.join(config.storageDir, filename));
|
|
737
|
+
stream.setEncoding("utf8");
|
|
738
|
+
let buffered = "";
|
|
739
|
+
let lineNumber = 0;
|
|
740
|
+
let previousKey = null;
|
|
741
|
+
const applyLine = (line) => {
|
|
742
|
+
let value;
|
|
743
|
+
try {
|
|
744
|
+
value = JSON.parse(line);
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
return "invalid";
|
|
748
|
+
}
|
|
749
|
+
if (!isIndexManifestFile(value)) {
|
|
750
|
+
return "invalid";
|
|
751
|
+
}
|
|
752
|
+
const key = `${value.relativePath}\0${value.checksum}`;
|
|
753
|
+
if (previousKey !== null && key <= previousKey) {
|
|
754
|
+
return "invalid";
|
|
755
|
+
}
|
|
756
|
+
previousKey = key;
|
|
757
|
+
if (lineNumber >= offset) {
|
|
758
|
+
values.push(value);
|
|
759
|
+
}
|
|
760
|
+
lineNumber += 1;
|
|
761
|
+
return values.length >= limit ? "done" : "continue";
|
|
762
|
+
};
|
|
270
763
|
try {
|
|
271
|
-
await
|
|
272
|
-
|
|
273
|
-
|
|
764
|
+
for await (const chunk of stream) {
|
|
765
|
+
buffered += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
766
|
+
let lineEnd = buffered.indexOf("\n");
|
|
767
|
+
while (lineEnd >= 0) {
|
|
768
|
+
const line = buffered.slice(0, lineEnd);
|
|
769
|
+
buffered = buffered.slice(lineEnd + 1);
|
|
770
|
+
if (!line) {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
const outcome = applyLine(line);
|
|
774
|
+
if (outcome === "invalid") {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
if (outcome === "done") {
|
|
778
|
+
stream.destroy();
|
|
779
|
+
return values;
|
|
780
|
+
}
|
|
781
|
+
lineEnd = buffered.indexOf("\n");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (buffered) {
|
|
785
|
+
const outcome = applyLine(buffered);
|
|
786
|
+
if (outcome === "invalid") {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return lineNumber >= offset ? values : null;
|
|
274
791
|
}
|
|
275
792
|
finally {
|
|
276
|
-
|
|
793
|
+
stream.destroy();
|
|
277
794
|
}
|
|
278
795
|
}
|
|
796
|
+
function indexFilesSnapshotFilename(id) {
|
|
797
|
+
if (!UUID_V4_PATTERN.test(id)) {
|
|
798
|
+
throw new Error("Index file snapshot id must be a valid UUID v4.");
|
|
799
|
+
}
|
|
800
|
+
return `${INDEX_FILES_PREFIX}${id}${INDEX_FILES_SUFFIX}`;
|
|
801
|
+
}
|
|
802
|
+
function isIndexFilesSnapshotFilename(value) {
|
|
803
|
+
return (typeof value === "string" &&
|
|
804
|
+
value.startsWith(INDEX_FILES_PREFIX) &&
|
|
805
|
+
value.endsWith(INDEX_FILES_SUFFIX) &&
|
|
806
|
+
UUID_V4_PATTERN.test(value.slice(INDEX_FILES_PREFIX.length, -INDEX_FILES_SUFFIX.length)));
|
|
807
|
+
}
|
|
808
|
+
async function removeStaleIndexFileSnapshots(retainedFilenames, config) {
|
|
809
|
+
const entries = await readdir(config.storageDir);
|
|
810
|
+
await Promise.all(entries
|
|
811
|
+
.filter((entry) => !retainedFilenames.has(entry) && isIndexFilesSnapshotFilename(entry))
|
|
812
|
+
.map((entry) => rm(path.join(config.storageDir, entry), { force: true })));
|
|
813
|
+
}
|
|
279
814
|
//# sourceMappingURL=store.js.map
|