@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
package/dist/store.js CHANGED
@@ -1,16 +1,54 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { readFile, rename, rm, writeFile } from "node:fs/promises";
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, hardenPrivateFile } from "./permissions.js";
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
- return lancedb.connect(config.storageDir, {
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
- await writePrivateJsonAtomic(manifestPath, manifest, config.storageDir);
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 raw = JSON.parse(await readFile(path.join(config.storageDir, INDEX_MANIFEST_FILENAME), "utf8"));
97
- if (!isRecord(raw)) {
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 isIndexManifest(raw) ? raw : null;
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 readIndexManifest(config))?.tableName ?? config.tableName;
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.close();
642
+ closeStoreConnection(activeConnection, config);
264
643
  }
265
644
  }
266
645
  }
267
646
  async function writePrivateJsonAtomic(targetPath, value, directory) {
268
- await ensurePrivateDirectory(directory);
269
- const temporaryPath = `${targetPath}.${process.pid}.${randomUUID()}.tmp`;
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 writeFile(temporaryPath, JSON.stringify(value, null, 2), "utf8");
272
- await hardenPrivateFile(temporaryPath);
273
- await rename(temporaryPath, targetPath);
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
- await rm(temporaryPath, { force: true });
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