@jcode.labs/ragmir 2.17.2 → 2.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -43
- 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 +25 -63
- package/examples/document-evidence-benchmark/golden-queries.json +1 -1
- package/examples/library-api-demo/README.md +17 -57
- package/examples/sovereign-rag-demo/README.md +37 -97
- package/package.json +36 -11
package/dist/ingestion-state.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { open as openFile, readdir, readFile, rm } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { syncDirectory, writePrivateFileAtomic } from "./durable-file.js";
|
|
4
6
|
import { isRecord } from "./guards.js";
|
|
5
7
|
import { ensurePrivateDirectory, hardenPrivateFile } from "./permissions.js";
|
|
6
|
-
const INGESTION_STATE_VERSION =
|
|
8
|
+
const INGESTION_STATE_VERSION = 3;
|
|
9
|
+
const LEGACY_INGESTION_STATE_VERSION = 2;
|
|
7
10
|
const INGESTION_STATE_FILENAME = "ingestion-state.json";
|
|
11
|
+
const INGESTION_FILES_PREFIX = "ingestion-state.files.";
|
|
12
|
+
const INGESTION_FILES_SUFFIX = ".jsonl";
|
|
13
|
+
const INGESTION_JOURNAL_VERSION = 1;
|
|
14
|
+
const INGESTION_JOURNAL_FILENAME = "ingestion-state.journal.jsonl";
|
|
15
|
+
const STREAM_WRITE_BYTES = 64 * 1_024;
|
|
8
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;
|
|
9
17
|
const GENERATION_ID_PATTERN = /^[0-9a-f]{32}$/iu;
|
|
18
|
+
const ingestionRuntimes = new WeakMap();
|
|
10
19
|
export function createIngestionRunState(options) {
|
|
11
20
|
const now = new Date().toISOString();
|
|
12
|
-
|
|
21
|
+
const state = {
|
|
13
22
|
version: INGESTION_STATE_VERSION,
|
|
14
23
|
runId: randomUUID(),
|
|
15
24
|
mode: options.mode,
|
|
@@ -24,6 +33,9 @@ export function createIngestionRunState(options) {
|
|
|
24
33
|
resumed: false,
|
|
25
34
|
files: options.files.map((file) => {
|
|
26
35
|
const reused = options.reusablePaths.has(file.relativePath);
|
|
36
|
+
const previous = options.previousFiles?.get(file.relativePath);
|
|
37
|
+
const lastGoodChecksum = previous?.checksum ?? (reused ? file.checksum : null);
|
|
38
|
+
const lastGoodChunkCount = previous?.chunkCount ?? options.reusableChunkCounts.get(file.relativePath) ?? 0;
|
|
27
39
|
return {
|
|
28
40
|
relativePath: file.relativePath,
|
|
29
41
|
checksum: file.checksum,
|
|
@@ -31,7 +43,12 @@ export function createIngestionRunState(options) {
|
|
|
31
43
|
mtimeMs: file.mtimeMs,
|
|
32
44
|
policyFingerprint: options.policyFingerprint,
|
|
33
45
|
state: reused ? "indexed" : "pending",
|
|
34
|
-
chunkCount:
|
|
46
|
+
chunkCount: lastGoodChunkCount,
|
|
47
|
+
lastGoodChecksum,
|
|
48
|
+
lastGoodChunkCount,
|
|
49
|
+
lastGoodBytes: previous?.bytes ?? (reused ? file.bytes : null),
|
|
50
|
+
lastGoodMtimeMs: previous?.mtimeMs ?? (reused ? file.mtimeMs : null),
|
|
51
|
+
staleLastKnownGood: lastGoodChecksum !== null && lastGoodChecksum !== file.checksum,
|
|
35
52
|
redactions: 0,
|
|
36
53
|
error: null,
|
|
37
54
|
reused,
|
|
@@ -39,6 +56,8 @@ export function createIngestionRunState(options) {
|
|
|
39
56
|
};
|
|
40
57
|
}),
|
|
41
58
|
};
|
|
59
|
+
ingestionRuntimes.set(state, createRuntime(state, false));
|
|
60
|
+
return state;
|
|
42
61
|
}
|
|
43
62
|
export function canResumeIngestion(state, files, policyFingerprint, rebuildRequested) {
|
|
44
63
|
if (!["running", "interrupted", "failed"].includes(state.status) ||
|
|
@@ -54,38 +73,91 @@ export function canResumeIngestion(state, files, policyFingerprint, rebuildReque
|
|
|
54
73
|
});
|
|
55
74
|
}
|
|
56
75
|
export function resumeIngestionState(state) {
|
|
57
|
-
const now =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
const now = nextIsoTimestamp(state.updatedAt);
|
|
77
|
+
const runtime = runtimeFor(state);
|
|
78
|
+
for (const file of state.files) {
|
|
79
|
+
if (file.state === "indexed") {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const staleLastKnownGood = file.lastGoodChecksum !== null && file.lastGoodChecksum !== file.checksum;
|
|
83
|
+
if (file.state === "pending" &&
|
|
84
|
+
file.chunkCount === file.lastGoodChunkCount &&
|
|
85
|
+
file.error === null &&
|
|
86
|
+
file.staleLastKnownGood === staleLastKnownGood) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
applyIngestionFileUpdate(runtime, file, {
|
|
90
|
+
state: "pending",
|
|
91
|
+
chunkCount: file.lastGoodChunkCount,
|
|
92
|
+
error: null,
|
|
93
|
+
staleLastKnownGood,
|
|
94
|
+
}, now);
|
|
95
|
+
}
|
|
96
|
+
state.status = "running";
|
|
97
|
+
state.resumed = true;
|
|
98
|
+
state.updatedAt = now;
|
|
99
|
+
state.lastActivityAt = now;
|
|
100
|
+
runtime.headerDirty = true;
|
|
101
|
+
return state;
|
|
68
102
|
}
|
|
69
103
|
export function updateIngestionFile(state, relativePath, update) {
|
|
70
|
-
const now =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
const now = nextIsoTimestamp(state.updatedAt);
|
|
105
|
+
const runtime = runtimeFor(state);
|
|
106
|
+
const file = runtime.filesByPath.get(relativePath);
|
|
107
|
+
if (!file) {
|
|
108
|
+
return state;
|
|
109
|
+
}
|
|
110
|
+
applyIngestionFileUpdate(runtime, file, update, now);
|
|
111
|
+
state.updatedAt = now;
|
|
112
|
+
state.lastActivityAt = now;
|
|
113
|
+
runtime.headerDirty = true;
|
|
114
|
+
return state;
|
|
115
|
+
}
|
|
116
|
+
export function reconcileIngestionFile(state, relativePath, update) {
|
|
117
|
+
const now = nextIsoTimestamp(state.updatedAt);
|
|
118
|
+
const runtime = runtimeFor(state);
|
|
119
|
+
const file = runtime.filesByPath.get(relativePath);
|
|
120
|
+
if (!file) {
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
123
|
+
applyIngestionFileUpdate(runtime, file, update, file.updatedAt);
|
|
124
|
+
state.updatedAt = now;
|
|
125
|
+
state.lastActivityAt = now;
|
|
126
|
+
runtime.headerDirty = true;
|
|
127
|
+
return state;
|
|
77
128
|
}
|
|
78
129
|
export function finishIngestionState(state, status) {
|
|
79
|
-
const now =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
};
|
|
130
|
+
const now = nextIsoTimestamp(state.updatedAt);
|
|
131
|
+
state.status = status;
|
|
132
|
+
state.updatedAt = now;
|
|
133
|
+
state.lastActivityAt = now;
|
|
134
|
+
runtimeFor(state).headerDirty = true;
|
|
135
|
+
return state;
|
|
86
136
|
}
|
|
87
137
|
export async function writeIngestionState(state, config) {
|
|
88
|
-
|
|
138
|
+
const runtime = runtimeFor(state);
|
|
139
|
+
if (!runtime.snapshotPersisted) {
|
|
140
|
+
await compactIngestionState(state, config);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!runtime.headerDirty && runtime.dirtyFiles.size === 0) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await appendPrivateJournal(path.join(config.storageDir, INGESTION_JOURNAL_FILENAME), ingestionJournalRecords(state, runtime), config.storageDir);
|
|
147
|
+
runtime.headerDirty = false;
|
|
148
|
+
runtime.dirtyFiles.clear();
|
|
149
|
+
}
|
|
150
|
+
export async function compactIngestionState(state, config) {
|
|
151
|
+
state.version = INGESTION_STATE_VERSION;
|
|
152
|
+
const fileSnapshot = ingestionFileSnapshotFilename(state.runId);
|
|
153
|
+
await writePrivateJsonLinesAtomic(path.join(config.storageDir, fileSnapshot), state.files, config.storageDir);
|
|
154
|
+
await writePrivateJsonAtomic(path.join(config.storageDir, INGESTION_STATE_FILENAME), ingestionRunHeader(state, fileSnapshot), config.storageDir);
|
|
155
|
+
await rm(path.join(config.storageDir, INGESTION_JOURNAL_FILENAME), { force: true });
|
|
156
|
+
await removeStaleIngestionFileSnapshots(fileSnapshot, config);
|
|
157
|
+
const runtime = runtimeFor(state);
|
|
158
|
+
runtime.snapshotPersisted = true;
|
|
159
|
+
runtime.headerDirty = false;
|
|
160
|
+
runtime.dirtyFiles.clear();
|
|
89
161
|
}
|
|
90
162
|
export async function writeStagedIndexManifest(manifest, runId, config) {
|
|
91
163
|
await writePrivateJsonAtomic(stagedManifestPath(runId, config), manifest, config.storageDir);
|
|
@@ -96,7 +168,19 @@ export async function removeStagedIndexManifest(runId, config) {
|
|
|
96
168
|
export async function readIngestionState(config) {
|
|
97
169
|
try {
|
|
98
170
|
const value = JSON.parse(await readFile(path.join(config.storageDir, INGESTION_STATE_FILENAME), "utf8"));
|
|
99
|
-
|
|
171
|
+
const hydrated = await hydrateIngestionRunState(value, config);
|
|
172
|
+
if (!hydrated) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const { state, filesByPath } = hydrated;
|
|
176
|
+
const runtime = createRuntime(state, true, filesByPath);
|
|
177
|
+
ingestionRuntimes.set(state, runtime);
|
|
178
|
+
if (!(await applyIngestionJournal(state, config, runtime, state.updatedAt))) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
runtime.headerDirty = false;
|
|
182
|
+
runtime.dirtyFiles.clear();
|
|
183
|
+
return state;
|
|
100
184
|
}
|
|
101
185
|
catch (error) {
|
|
102
186
|
if (error instanceof SyntaxError || (isNodeError(error) && error.code === "ENOENT")) {
|
|
@@ -110,28 +194,241 @@ export async function getIngestionProgress(config) {
|
|
|
110
194
|
return state ? ingestionProgress(state) : null;
|
|
111
195
|
}
|
|
112
196
|
export function ingestionProgress(state) {
|
|
113
|
-
const
|
|
197
|
+
const runtime = runtimeFor(state);
|
|
114
198
|
return {
|
|
115
199
|
runId: state.runId,
|
|
116
200
|
mode: state.mode,
|
|
117
201
|
status: state.status,
|
|
118
202
|
resumed: state.resumed,
|
|
119
203
|
batchSize: state.batchSize,
|
|
120
|
-
totalFiles:
|
|
121
|
-
pendingFiles:
|
|
122
|
-
parsedFiles:
|
|
123
|
-
embeddedFiles:
|
|
124
|
-
indexedFiles:
|
|
125
|
-
errorFiles:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
.reduce((sum, file) => sum + file.chunkCount, 0),
|
|
204
|
+
totalFiles: runtime.filesByPath.size,
|
|
205
|
+
pendingFiles: runtime.stageCounts.pending,
|
|
206
|
+
parsedFiles: runtime.stageCounts.parsed,
|
|
207
|
+
embeddedFiles: runtime.stageCounts.embedded,
|
|
208
|
+
indexedFiles: runtime.stageCounts.indexed,
|
|
209
|
+
errorFiles: runtime.stageCounts.error,
|
|
210
|
+
staleFiles: runtime.staleFiles,
|
|
211
|
+
chunksIndexed: runtime.chunksIndexed,
|
|
129
212
|
lastActivityAt: state.lastActivityAt,
|
|
130
213
|
};
|
|
131
214
|
}
|
|
215
|
+
function runtimeFor(state) {
|
|
216
|
+
const existing = ingestionRuntimes.get(state);
|
|
217
|
+
if (existing) {
|
|
218
|
+
return existing;
|
|
219
|
+
}
|
|
220
|
+
const runtime = createRuntime(state, false);
|
|
221
|
+
ingestionRuntimes.set(state, runtime);
|
|
222
|
+
return runtime;
|
|
223
|
+
}
|
|
224
|
+
function createRuntime(state, snapshotPersisted, existingFilesByPath) {
|
|
225
|
+
const stageCounts = {
|
|
226
|
+
pending: 0,
|
|
227
|
+
parsed: 0,
|
|
228
|
+
embedded: 0,
|
|
229
|
+
indexed: 0,
|
|
230
|
+
error: 0,
|
|
231
|
+
};
|
|
232
|
+
const filesByPath = existingFilesByPath ?? new Map();
|
|
233
|
+
let staleFiles = 0;
|
|
234
|
+
let chunksIndexed = 0;
|
|
235
|
+
for (const file of state.files) {
|
|
236
|
+
if (!existingFilesByPath) {
|
|
237
|
+
filesByPath.set(file.relativePath, file);
|
|
238
|
+
}
|
|
239
|
+
stageCounts[file.state] += 1;
|
|
240
|
+
staleFiles += file.staleLastKnownGood ? 1 : 0;
|
|
241
|
+
chunksIndexed += indexedChunkContribution(file);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
filesByPath,
|
|
245
|
+
stageCounts,
|
|
246
|
+
staleFiles,
|
|
247
|
+
chunksIndexed,
|
|
248
|
+
dirtyFiles: new Set(),
|
|
249
|
+
headerDirty: false,
|
|
250
|
+
snapshotPersisted,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function applyIngestionFileUpdate(runtime, file, update, now) {
|
|
254
|
+
const previousStage = file.state;
|
|
255
|
+
const previousStale = file.staleLastKnownGood;
|
|
256
|
+
const previousChunks = indexedChunkContribution(file);
|
|
257
|
+
Object.assign(file, update, { updatedAt: now });
|
|
258
|
+
if (file.state !== previousStage) {
|
|
259
|
+
runtime.stageCounts[previousStage] -= 1;
|
|
260
|
+
runtime.stageCounts[file.state] += 1;
|
|
261
|
+
}
|
|
262
|
+
runtime.staleFiles += Number(file.staleLastKnownGood) - Number(previousStale);
|
|
263
|
+
runtime.chunksIndexed += indexedChunkContribution(file) - previousChunks;
|
|
264
|
+
runtime.dirtyFiles.add(file.relativePath);
|
|
265
|
+
runtime.filesByPath.set(file.relativePath, file);
|
|
266
|
+
}
|
|
267
|
+
function indexedChunkContribution(file) {
|
|
268
|
+
if (file.staleLastKnownGood) {
|
|
269
|
+
return file.lastGoodChunkCount;
|
|
270
|
+
}
|
|
271
|
+
return file.state === "indexed" ? file.chunkCount : 0;
|
|
272
|
+
}
|
|
273
|
+
async function applyIngestionJournal(state, config, runtime, snapshotUpdatedAt) {
|
|
274
|
+
const stream = createReadStream(path.join(config.storageDir, INGESTION_JOURNAL_FILENAME));
|
|
275
|
+
stream.setEncoding("utf8");
|
|
276
|
+
let buffered = "";
|
|
277
|
+
try {
|
|
278
|
+
for await (const chunk of stream) {
|
|
279
|
+
buffered += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
280
|
+
let lineEnd = buffered.indexOf("\n");
|
|
281
|
+
while (lineEnd >= 0) {
|
|
282
|
+
const line = buffered.slice(0, lineEnd);
|
|
283
|
+
buffered = buffered.slice(lineEnd + 1);
|
|
284
|
+
if (line && !applyIngestionJournalLine(state, runtime, snapshotUpdatedAt, line)) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
lineEnd = buffered.indexOf("\n");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function applyIngestionJournalLine(state, runtime, snapshotUpdatedAt, line) {
|
|
300
|
+
let value;
|
|
301
|
+
try {
|
|
302
|
+
value = JSON.parse(line);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (!isRecord(value) ||
|
|
308
|
+
value.version !== INGESTION_JOURNAL_VERSION ||
|
|
309
|
+
typeof value.runId !== "string") {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
if (value.runId !== state.runId) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (value.type === "run") {
|
|
316
|
+
if (!isIngestionRunStatus(value.status) ||
|
|
317
|
+
typeof value.resumed !== "boolean" ||
|
|
318
|
+
!isIsoTimestamp(value.updatedAt) ||
|
|
319
|
+
!isIsoTimestamp(value.lastActivityAt)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
if (value.updatedAt > snapshotUpdatedAt && value.updatedAt > state.updatedAt) {
|
|
323
|
+
state.status = value.status;
|
|
324
|
+
state.resumed = value.resumed;
|
|
325
|
+
state.updatedAt = value.updatedAt;
|
|
326
|
+
state.lastActivityAt = value.lastActivityAt;
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
if (value.type !== "file" ||
|
|
331
|
+
!isIsoTimestamp(value.writtenAt) ||
|
|
332
|
+
!isIngestionFileState(value.file, state.policyFingerprint)) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
const current = runtime.filesByPath.get(value.file.relativePath);
|
|
336
|
+
if (!current ||
|
|
337
|
+
current.checksum !== value.file.checksum ||
|
|
338
|
+
current.bytes !== value.file.bytes ||
|
|
339
|
+
current.mtimeMs !== value.file.mtimeMs) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
if (value.writtenAt > snapshotUpdatedAt) {
|
|
343
|
+
applyIngestionFileUpdate(runtime, current, value.file, value.file.updatedAt);
|
|
344
|
+
}
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
function nextIsoTimestamp(previous) {
|
|
348
|
+
const now = Date.now();
|
|
349
|
+
const previousTime = Date.parse(previous);
|
|
350
|
+
return new Date(Math.max(now, previousTime + 1)).toISOString();
|
|
351
|
+
}
|
|
352
|
+
async function hydrateIngestionRunState(value, config) {
|
|
353
|
+
if (isIngestionRunState(value, config)) {
|
|
354
|
+
return { state: value };
|
|
355
|
+
}
|
|
356
|
+
if (!isPersistedIngestionRunHeader(value, config)) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
const files = await readIngestionFileSnapshot(value, config);
|
|
360
|
+
if (!files) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
state: {
|
|
365
|
+
version: value.version,
|
|
366
|
+
runId: value.runId,
|
|
367
|
+
mode: value.mode,
|
|
368
|
+
status: value.status,
|
|
369
|
+
tableName: value.tableName,
|
|
370
|
+
previousTableName: value.previousTableName,
|
|
371
|
+
policyFingerprint: value.policyFingerprint,
|
|
372
|
+
batchSize: value.batchSize,
|
|
373
|
+
createdAt: value.createdAt,
|
|
374
|
+
updatedAt: value.updatedAt,
|
|
375
|
+
lastActivityAt: value.lastActivityAt,
|
|
376
|
+
resumed: value.resumed,
|
|
377
|
+
files: files.values,
|
|
378
|
+
},
|
|
379
|
+
filesByPath: files.byPath,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
async function readIngestionFileSnapshot(header, config) {
|
|
383
|
+
const values = [];
|
|
384
|
+
const byPath = new Map();
|
|
385
|
+
const stream = createReadStream(path.join(config.storageDir, header.fileSnapshot));
|
|
386
|
+
stream.setEncoding("utf8");
|
|
387
|
+
let buffered = "";
|
|
388
|
+
const applyLine = (line) => {
|
|
389
|
+
let value;
|
|
390
|
+
try {
|
|
391
|
+
value = JSON.parse(line);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
if (!isIngestionFileState(value, header.policyFingerprint) || byPath.has(value.relativePath)) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
values.push(value);
|
|
400
|
+
byPath.set(value.relativePath, value);
|
|
401
|
+
return true;
|
|
402
|
+
};
|
|
403
|
+
try {
|
|
404
|
+
for await (const chunk of stream) {
|
|
405
|
+
buffered += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
406
|
+
let lineEnd = buffered.indexOf("\n");
|
|
407
|
+
while (lineEnd >= 0) {
|
|
408
|
+
const line = buffered.slice(0, lineEnd);
|
|
409
|
+
buffered = buffered.slice(lineEnd + 1);
|
|
410
|
+
if (!line || !applyLine(line)) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
lineEnd = buffered.indexOf("\n");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (buffered && !applyLine(buffered)) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
return values.length === header.fileCount ? { values, byPath } : null;
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
132
428
|
function isIngestionRunState(value, config) {
|
|
133
429
|
if (!isRecord(value) ||
|
|
134
|
-
value.version !==
|
|
430
|
+
(value.version !== LEGACY_INGESTION_STATE_VERSION &&
|
|
431
|
+
value.version !== INGESTION_STATE_VERSION) ||
|
|
135
432
|
!isUuidV4(value.runId) ||
|
|
136
433
|
(value.mode !== "incremental" && value.mode !== "rebuild") ||
|
|
137
434
|
!isIngestionRunStatus(value.status) ||
|
|
@@ -163,6 +460,37 @@ function isIngestionRunState(value, config) {
|
|
|
163
460
|
value.previousTableName !== value.tableName &&
|
|
164
461
|
isManagedTableName(value.previousTableName, config));
|
|
165
462
|
}
|
|
463
|
+
function isPersistedIngestionRunHeader(value, config) {
|
|
464
|
+
return (isRecord(value) &&
|
|
465
|
+
value.version === INGESTION_STATE_VERSION &&
|
|
466
|
+
isIngestionRunMetadata(value, config) &&
|
|
467
|
+
value.fileSnapshot === ingestionFileSnapshotFilename(value.runId) &&
|
|
468
|
+
isNonNegativeSafeInteger(value.fileCount));
|
|
469
|
+
}
|
|
470
|
+
function isIngestionRunMetadata(value, config) {
|
|
471
|
+
if (!isUuidV4(value.runId) ||
|
|
472
|
+
(value.mode !== "incremental" && value.mode !== "rebuild") ||
|
|
473
|
+
!isIngestionRunStatus(value.status) ||
|
|
474
|
+
typeof value.tableName !== "string" ||
|
|
475
|
+
(value.previousTableName !== null && typeof value.previousTableName !== "string") ||
|
|
476
|
+
typeof value.policyFingerprint !== "string" ||
|
|
477
|
+
value.policyFingerprint.length === 0 ||
|
|
478
|
+
!isPositiveSafeInteger(value.batchSize) ||
|
|
479
|
+
!isIsoTimestamp(value.createdAt) ||
|
|
480
|
+
!isIsoTimestamp(value.updatedAt) ||
|
|
481
|
+
!isIsoTimestamp(value.lastActivityAt) ||
|
|
482
|
+
typeof value.resumed !== "boolean" ||
|
|
483
|
+
!isManagedTableName(value.tableName, config)) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
if (value.mode === "incremental") {
|
|
487
|
+
return value.previousTableName === null;
|
|
488
|
+
}
|
|
489
|
+
return (value.tableName === generationTableName(config.tableName, value.runId) &&
|
|
490
|
+
value.previousTableName !== null &&
|
|
491
|
+
value.previousTableName !== value.tableName &&
|
|
492
|
+
isManagedTableName(value.previousTableName, config));
|
|
493
|
+
}
|
|
166
494
|
function isIngestionRunStatus(value) {
|
|
167
495
|
return (value === "running" ||
|
|
168
496
|
value === "interrupted" ||
|
|
@@ -183,7 +511,7 @@ function stagedManifestPath(runId, config) {
|
|
|
183
511
|
return targetPath;
|
|
184
512
|
}
|
|
185
513
|
function isIngestionFileState(value, policyFingerprint) {
|
|
186
|
-
|
|
514
|
+
if (!(isRecord(value) &&
|
|
187
515
|
typeof value.relativePath === "string" &&
|
|
188
516
|
value.relativePath.length > 0 &&
|
|
189
517
|
!value.relativePath.includes("\0") &&
|
|
@@ -194,11 +522,34 @@ function isIngestionFileState(value, policyFingerprint) {
|
|
|
194
522
|
value.policyFingerprint === policyFingerprint &&
|
|
195
523
|
isIngestionFileStage(value.state) &&
|
|
196
524
|
isNonNegativeSafeInteger(value.chunkCount) &&
|
|
525
|
+
(value.lastGoodChecksum === null ||
|
|
526
|
+
(typeof value.lastGoodChecksum === "string" && value.lastGoodChecksum.length > 0)) &&
|
|
527
|
+
isNonNegativeSafeInteger(value.lastGoodChunkCount) &&
|
|
528
|
+
(value.lastGoodBytes === null || isNonNegativeSafeInteger(value.lastGoodBytes)) &&
|
|
529
|
+
(value.lastGoodMtimeMs === null || isNonNegativeFiniteNumber(value.lastGoodMtimeMs)) &&
|
|
530
|
+
typeof value.staleLastKnownGood === "boolean" &&
|
|
197
531
|
isNonNegativeSafeInteger(value.redactions) &&
|
|
198
532
|
(value.error === null || typeof value.error === "string") &&
|
|
199
533
|
typeof value.reused === "boolean" &&
|
|
200
534
|
isIsoTimestamp(value.updatedAt) &&
|
|
201
|
-
(!value.reused || value.state === "indexed"))
|
|
535
|
+
(!value.reused || value.state === "indexed"))) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
const hasLastGood = value.lastGoodChecksum !== null;
|
|
539
|
+
if ((!hasLastGood &&
|
|
540
|
+
(value.lastGoodChunkCount !== 0 ||
|
|
541
|
+
value.lastGoodBytes !== null ||
|
|
542
|
+
value.lastGoodMtimeMs !== null)) ||
|
|
543
|
+
(value.staleLastKnownGood &&
|
|
544
|
+
(!hasLastGood || value.lastGoodChecksum === value.checksum || value.state === "indexed"))) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
return (value.state !== "indexed" ||
|
|
548
|
+
(value.lastGoodChecksum === value.checksum &&
|
|
549
|
+
value.lastGoodChunkCount === value.chunkCount &&
|
|
550
|
+
value.lastGoodBytes === value.bytes &&
|
|
551
|
+
value.lastGoodMtimeMs === value.mtimeMs &&
|
|
552
|
+
!value.staleLastKnownGood));
|
|
202
553
|
}
|
|
203
554
|
export function generationTableName(baseName, runId) {
|
|
204
555
|
if (!isUuidV4(runId)) {
|
|
@@ -242,16 +593,96 @@ function isIngestionFileStage(value) {
|
|
|
242
593
|
function isNodeError(error) {
|
|
243
594
|
return error instanceof Error && "code" in error;
|
|
244
595
|
}
|
|
596
|
+
function ingestionRunHeader(state, fileSnapshot) {
|
|
597
|
+
return {
|
|
598
|
+
version: INGESTION_STATE_VERSION,
|
|
599
|
+
runId: state.runId,
|
|
600
|
+
mode: state.mode,
|
|
601
|
+
status: state.status,
|
|
602
|
+
tableName: state.tableName,
|
|
603
|
+
previousTableName: state.previousTableName,
|
|
604
|
+
policyFingerprint: state.policyFingerprint,
|
|
605
|
+
batchSize: state.batchSize,
|
|
606
|
+
createdAt: state.createdAt,
|
|
607
|
+
updatedAt: state.updatedAt,
|
|
608
|
+
lastActivityAt: state.lastActivityAt,
|
|
609
|
+
resumed: state.resumed,
|
|
610
|
+
fileSnapshot,
|
|
611
|
+
fileCount: state.files.length,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function ingestionFileSnapshotFilename(runId) {
|
|
615
|
+
if (!isUuidV4(runId)) {
|
|
616
|
+
throw new Error("Ingestion runId must be a valid UUID v4.");
|
|
617
|
+
}
|
|
618
|
+
return `${INGESTION_FILES_PREFIX}${runId}${INGESTION_FILES_SUFFIX}`;
|
|
619
|
+
}
|
|
620
|
+
function* ingestionJournalRecords(state, runtime) {
|
|
621
|
+
if (runtime.headerDirty) {
|
|
622
|
+
yield {
|
|
623
|
+
version: INGESTION_JOURNAL_VERSION,
|
|
624
|
+
runId: state.runId,
|
|
625
|
+
type: "run",
|
|
626
|
+
status: state.status,
|
|
627
|
+
resumed: state.resumed,
|
|
628
|
+
updatedAt: state.updatedAt,
|
|
629
|
+
lastActivityAt: state.lastActivityAt,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
for (const relativePath of runtime.dirtyFiles) {
|
|
633
|
+
const file = runtime.filesByPath.get(relativePath);
|
|
634
|
+
if (file) {
|
|
635
|
+
yield {
|
|
636
|
+
version: INGESTION_JOURNAL_VERSION,
|
|
637
|
+
runId: state.runId,
|
|
638
|
+
type: "file",
|
|
639
|
+
writtenAt: state.updatedAt,
|
|
640
|
+
file,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async function removeStaleIngestionFileSnapshots(currentFilename, config) {
|
|
646
|
+
const entries = await readdir(config.storageDir);
|
|
647
|
+
await Promise.all(entries
|
|
648
|
+
.filter((entry) => entry !== currentFilename &&
|
|
649
|
+
entry.startsWith(INGESTION_FILES_PREFIX) &&
|
|
650
|
+
entry.endsWith(INGESTION_FILES_SUFFIX) &&
|
|
651
|
+
isUuidV4(entry.slice(INGESTION_FILES_PREFIX.length, -INGESTION_FILES_SUFFIX.length)))
|
|
652
|
+
.map((entry) => rm(path.join(config.storageDir, entry), { force: true })));
|
|
653
|
+
}
|
|
245
654
|
async function writePrivateJsonAtomic(targetPath, value, directory) {
|
|
655
|
+
await writePrivateFileAtomic(targetPath, directory, async (handle) => {
|
|
656
|
+
await handle.writeFile(JSON.stringify(value, null, 2), "utf8");
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
async function writePrivateJsonLinesAtomic(targetPath, values, directory) {
|
|
660
|
+
await writePrivateFileAtomic(targetPath, directory, (handle) => writeJsonLines(handle, values));
|
|
661
|
+
}
|
|
662
|
+
async function appendPrivateJournal(targetPath, records, directory) {
|
|
246
663
|
await ensurePrivateDirectory(directory);
|
|
247
|
-
const
|
|
664
|
+
const handle = await openFile(targetPath, "a", 0o600);
|
|
248
665
|
try {
|
|
249
|
-
await
|
|
250
|
-
await
|
|
251
|
-
await
|
|
666
|
+
await hardenPrivateFile(targetPath);
|
|
667
|
+
await writeJsonLines(handle, records);
|
|
668
|
+
await handle.sync();
|
|
252
669
|
}
|
|
253
670
|
finally {
|
|
254
|
-
await
|
|
671
|
+
await handle.close();
|
|
672
|
+
}
|
|
673
|
+
await syncDirectory(directory);
|
|
674
|
+
}
|
|
675
|
+
async function writeJsonLines(handle, values) {
|
|
676
|
+
let buffered = "";
|
|
677
|
+
for (const value of values) {
|
|
678
|
+
buffered += `${JSON.stringify(value)}\n`;
|
|
679
|
+
if (Buffer.byteLength(buffered) >= STREAM_WRITE_BYTES) {
|
|
680
|
+
await handle.writeFile(buffered, "utf8");
|
|
681
|
+
buffered = "";
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (buffered) {
|
|
685
|
+
await handle.writeFile(buffered, "utf8");
|
|
255
686
|
}
|
|
256
687
|
}
|
|
257
688
|
//# sourceMappingURL=ingestion-state.js.map
|