@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.
Files changed (203) hide show
  1. package/README.md +53 -6
  2. package/dist/access-log.d.ts +10 -0
  3. package/dist/access-log.d.ts.map +1 -1
  4. package/dist/access-log.js +119 -49
  5. package/dist/access-log.js.map +1 -1
  6. package/dist/chunking.d.ts +5 -1
  7. package/dist/chunking.d.ts.map +1 -1
  8. package/dist/chunking.js +102 -16
  9. package/dist/chunking.js.map +1 -1
  10. package/dist/citation.d.ts +18 -0
  11. package/dist/citation.d.ts.map +1 -0
  12. package/dist/citation.js +54 -0
  13. package/dist/citation.js.map +1 -0
  14. package/dist/cli-entry.d.ts +3 -0
  15. package/dist/cli-entry.d.ts.map +1 -0
  16. package/dist/cli-entry.js +16 -0
  17. package/dist/cli-entry.js.map +1 -0
  18. package/dist/cli-fast.d.ts +13 -0
  19. package/dist/cli-fast.d.ts.map +1 -0
  20. package/dist/cli-fast.js +145 -0
  21. package/dist/cli-fast.js.map +1 -0
  22. package/dist/cli-options.d.ts +3 -0
  23. package/dist/cli-options.d.ts.map +1 -1
  24. package/dist/cli-options.js +7 -0
  25. package/dist/cli-options.js.map +1 -1
  26. package/dist/cli-stdin.d.ts +3 -0
  27. package/dist/cli-stdin.d.ts.map +1 -0
  28. package/dist/cli-stdin.js +15 -0
  29. package/dist/cli-stdin.js.map +1 -0
  30. package/dist/cli.js +251 -44
  31. package/dist/cli.js.map +1 -1
  32. package/dist/client.d.ts +13 -2
  33. package/dist/client.d.ts.map +1 -1
  34. package/dist/client.js +155 -10
  35. package/dist/client.js.map +1 -1
  36. package/dist/config.d.ts +6 -0
  37. package/dist/config.d.ts.map +1 -1
  38. package/dist/config.js +227 -49
  39. package/dist/config.js.map +1 -1
  40. package/dist/context-resources.d.ts +4 -2
  41. package/dist/context-resources.d.ts.map +1 -1
  42. package/dist/context-resources.js +55 -24
  43. package/dist/context-resources.js.map +1 -1
  44. package/dist/defaults.d.ts +27 -0
  45. package/dist/defaults.d.ts.map +1 -1
  46. package/dist/defaults.js +42 -2
  47. package/dist/defaults.js.map +1 -1
  48. package/dist/destroy.d.ts +2 -2
  49. package/dist/destroy.d.ts.map +1 -1
  50. package/dist/destroy.js +34 -20
  51. package/dist/destroy.js.map +1 -1
  52. package/dist/doctor.d.ts +3 -2
  53. package/dist/doctor.d.ts.map +1 -1
  54. package/dist/doctor.js +86 -41
  55. package/dist/doctor.js.map +1 -1
  56. package/dist/durable-file.d.ts +13 -0
  57. package/dist/durable-file.d.ts.map +1 -0
  58. package/dist/durable-file.js +56 -0
  59. package/dist/durable-file.js.map +1 -0
  60. package/dist/embeddings.d.ts +14 -3
  61. package/dist/embeddings.d.ts.map +1 -1
  62. package/dist/embeddings.js +233 -95
  63. package/dist/embeddings.js.map +1 -1
  64. package/dist/errors.d.ts +1 -1
  65. package/dist/errors.d.ts.map +1 -1
  66. package/dist/errors.js.map +1 -1
  67. package/dist/evaluate.d.ts +4 -3
  68. package/dist/evaluate.d.ts.map +1 -1
  69. package/dist/evaluate.js +611 -110
  70. package/dist/evaluate.js.map +1 -1
  71. package/dist/files.d.ts +4 -1
  72. package/dist/files.d.ts.map +1 -1
  73. package/dist/files.js +119 -33
  74. package/dist/files.js.map +1 -1
  75. package/dist/generation-retention.d.ts +52 -0
  76. package/dist/generation-retention.d.ts.map +1 -0
  77. package/dist/generation-retention.js +318 -0
  78. package/dist/generation-retention.js.map +1 -0
  79. package/dist/index-diagnostics.d.ts +3 -2
  80. package/dist/index-diagnostics.d.ts.map +1 -1
  81. package/dist/index-diagnostics.js +32 -3
  82. package/dist/index-diagnostics.js.map +1 -1
  83. package/dist/index-policy.d.ts.map +1 -1
  84. package/dist/index-policy.js +3 -0
  85. package/dist/index-policy.js.map +1 -1
  86. package/dist/index-write-lock.d.ts +16 -1
  87. package/dist/index-write-lock.d.ts.map +1 -1
  88. package/dist/index-write-lock.js +392 -13
  89. package/dist/index-write-lock.js.map +1 -1
  90. package/dist/index.d.ts +12 -4
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +5 -2
  93. package/dist/index.js.map +1 -1
  94. package/dist/ingest.d.ts +11 -2
  95. package/dist/ingest.d.ts.map +1 -1
  96. package/dist/ingest.js +680 -165
  97. package/dist/ingest.js.map +1 -1
  98. package/dist/ingestion-metrics.d.ts +58 -0
  99. package/dist/ingestion-metrics.d.ts.map +1 -0
  100. package/dist/ingestion-metrics.js +216 -0
  101. package/dist/ingestion-metrics.js.map +1 -0
  102. package/dist/ingestion-state.d.ts +11 -2
  103. package/dist/ingestion-state.d.ts.map +1 -1
  104. package/dist/ingestion-state.js +480 -49
  105. package/dist/ingestion-state.js.map +1 -1
  106. package/dist/limits.d.ts.map +1 -1
  107. package/dist/limits.js +11 -1
  108. package/dist/limits.js.map +1 -1
  109. package/dist/mcp-output.d.ts +44 -4
  110. package/dist/mcp-output.d.ts.map +1 -1
  111. package/dist/mcp-output.js +132 -159
  112. package/dist/mcp-output.js.map +1 -1
  113. package/dist/mcp-summaries.d.ts +32 -0
  114. package/dist/mcp-summaries.d.ts.map +1 -0
  115. package/dist/mcp-summaries.js +227 -0
  116. package/dist/mcp-summaries.js.map +1 -0
  117. package/dist/mcp.d.ts +1 -1
  118. package/dist/mcp.d.ts.map +1 -1
  119. package/dist/mcp.js +85 -46
  120. package/dist/mcp.js.map +1 -1
  121. package/dist/ocr-cache.d.ts +15 -0
  122. package/dist/ocr-cache.d.ts.map +1 -0
  123. package/dist/ocr-cache.js +90 -0
  124. package/dist/ocr-cache.js.map +1 -0
  125. package/dist/ocr.d.ts +31 -0
  126. package/dist/ocr.d.ts.map +1 -1
  127. package/dist/ocr.js +193 -17
  128. package/dist/ocr.js.map +1 -1
  129. package/dist/parsing.d.ts.map +1 -1
  130. package/dist/parsing.js +482 -85
  131. package/dist/parsing.js.map +1 -1
  132. package/dist/permissions.js +2 -2
  133. package/dist/permissions.js.map +1 -1
  134. package/dist/preview.d.ts.map +1 -1
  135. package/dist/preview.js +10 -11
  136. package/dist/preview.js.map +1 -1
  137. package/dist/quality-report.d.ts +12 -0
  138. package/dist/quality-report.d.ts.map +1 -0
  139. package/dist/quality-report.js +146 -0
  140. package/dist/quality-report.js.map +1 -0
  141. package/dist/query.d.ts +7 -4
  142. package/dist/query.d.ts.map +1 -1
  143. package/dist/query.js +371 -284
  144. package/dist/query.js.map +1 -1
  145. package/dist/ranking.d.ts +38 -0
  146. package/dist/ranking.d.ts.map +1 -0
  147. package/dist/ranking.js +278 -0
  148. package/dist/ranking.js.map +1 -0
  149. package/dist/redaction.d.ts +6 -1
  150. package/dist/redaction.d.ts.map +1 -1
  151. package/dist/redaction.js +118 -4
  152. package/dist/redaction.js.map +1 -1
  153. package/dist/research.d.ts +6 -1
  154. package/dist/research.d.ts.map +1 -1
  155. package/dist/research.js +330 -130
  156. package/dist/research.js.map +1 -1
  157. package/dist/runtime-info.d.ts +3 -0
  158. package/dist/runtime-info.d.ts.map +1 -0
  159. package/dist/runtime-info.js +81 -0
  160. package/dist/runtime-info.js.map +1 -0
  161. package/dist/security.d.ts +3 -2
  162. package/dist/security.d.ts.map +1 -1
  163. package/dist/security.js +242 -45
  164. package/dist/security.js.map +1 -1
  165. package/dist/semantic-config.d.ts +7 -1
  166. package/dist/semantic-config.d.ts.map +1 -1
  167. package/dist/semantic-config.js +30 -4
  168. package/dist/semantic-config.js.map +1 -1
  169. package/dist/setup.js +6 -3
  170. package/dist/setup.js.map +1 -1
  171. package/dist/skill.js +34 -12
  172. package/dist/skill.js.map +1 -1
  173. package/dist/source-fingerprint-cache.d.ts +20 -0
  174. package/dist/source-fingerprint-cache.d.ts.map +1 -0
  175. package/dist/source-fingerprint-cache.js +133 -0
  176. package/dist/source-fingerprint-cache.js.map +1 -0
  177. package/dist/storage-maintenance.d.ts +46 -0
  178. package/dist/storage-maintenance.d.ts.map +1 -0
  179. package/dist/storage-maintenance.js +329 -0
  180. package/dist/storage-maintenance.js.map +1 -0
  181. package/dist/store.d.ts +39 -2
  182. package/dist/store.d.ts.map +1 -1
  183. package/dist/store.js +559 -24
  184. package/dist/store.js.map +1 -1
  185. package/dist/text.d.ts +1 -1
  186. package/dist/text.d.ts.map +1 -1
  187. package/dist/text.js +12 -2
  188. package/dist/text.js.map +1 -1
  189. package/dist/types.d.ts +414 -5
  190. package/dist/types.d.ts.map +1 -1
  191. package/dist/vector-index.d.ts +42 -0
  192. package/dist/vector-index.d.ts.map +1 -0
  193. package/dist/vector-index.js +249 -0
  194. package/dist/vector-index.js.map +1 -0
  195. package/dist/version.d.ts +1 -1
  196. package/dist/version.js +1 -1
  197. package/dist/workload.d.ts +22 -0
  198. package/dist/workload.d.ts.map +1 -0
  199. package/dist/workload.js +181 -0
  200. package/dist/workload.js.map +1 -0
  201. package/examples/document-evidence-benchmark/README.md +3 -2
  202. package/examples/document-evidence-benchmark/golden-queries.json +1 -1
  203. package/package.json +36 -11
@@ -1,15 +1,24 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { readFile, rename, rm, writeFile } from "node:fs/promises";
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 = 1;
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
- return {
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: options.reusableChunkCounts.get(file.relativePath) ?? 0,
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 = new Date().toISOString();
58
- return {
59
- ...state,
60
- status: "running",
61
- resumed: true,
62
- updatedAt: now,
63
- lastActivityAt: now,
64
- files: state.files.map((file) => file.state === "indexed"
65
- ? file
66
- : { ...file, state: "pending", chunkCount: 0, error: null, updatedAt: now }),
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 = new Date().toISOString();
71
- return {
72
- ...state,
73
- updatedAt: now,
74
- lastActivityAt: now,
75
- files: state.files.map((file) => file.relativePath === relativePath ? { ...file, ...update, updatedAt: now } : file),
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 = new Date().toISOString();
80
- return {
81
- ...state,
82
- status,
83
- updatedAt: now,
84
- lastActivityAt: now,
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
- await writePrivateJsonAtomic(path.join(config.storageDir, INGESTION_STATE_FILENAME), state, config.storageDir);
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
- return isIngestionRunState(value, config) ? value : null;
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 count = (fileState) => state.files.filter((file) => file.state === fileState).length;
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: state.files.length,
121
- pendingFiles: count("pending"),
122
- parsedFiles: count("parsed"),
123
- embeddedFiles: count("embedded"),
124
- indexedFiles: count("indexed"),
125
- errorFiles: count("error"),
126
- chunksIndexed: state.files
127
- .filter((file) => file.state === "indexed")
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 !== INGESTION_STATE_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
- return (isRecord(value) &&
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 temporaryPath = `${targetPath}.${process.pid}.${randomUUID()}.tmp`;
664
+ const handle = await openFile(targetPath, "a", 0o600);
248
665
  try {
249
- await writeFile(temporaryPath, JSON.stringify(value, null, 2), "utf8");
250
- await hardenPrivateFile(temporaryPath);
251
- await rename(temporaryPath, targetPath);
666
+ await hardenPrivateFile(targetPath);
667
+ await writeJsonLines(handle, records);
668
+ await handle.sync();
252
669
  }
253
670
  finally {
254
- await rm(temporaryPath, { force: true });
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