@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/parsing.js CHANGED
@@ -2,6 +2,7 @@ import { createRequire as _createRequire } from "module";
2
2
  const __require = _createRequire(import.meta.url);
3
3
  import { spawn } from "node:child_process";
4
4
  import { readFile } from "node:fs/promises";
5
+ import path from "node:path";
5
6
  import { strFromU8, unzipSync } from "fflate";
6
7
  import { htmlToText } from "html-to-text";
7
8
  import readExcelFile from "read-excel-file/node";
@@ -9,11 +10,15 @@ const mammoth = __require("mammoth");
9
10
  import { getDocumentProxy } from "unpdf";
10
11
  import YAML from "yaml";
11
12
  import { OCR_IMAGE_EXTENSIONS } from "./files.js";
13
+ import { isRecord } from "./guards.js";
12
14
  import { MAX_EXTERNAL_TEXT_STDIO_BYTES, MAX_OFFICE_TEXT_ENTRY_COUNT, MAX_OFFICE_XML_ENTRY_BYTES, MAX_OFFICE_XML_TOTAL_BYTES, MAX_PDF_PAGES, MAX_PDF_TEXT_CHARACTERS, } from "./limits.js";
15
+ import { MAX_OCR_BATCH_PAGES, pdfOcrCommandIdentity } from "./ocr.js";
16
+ import { PDF_OCR_PARSER_POLICY, readPdfOcrCache, writePdfOcrCache, } from "./ocr-cache.js";
13
17
  import { throwIfAborted } from "./operation.js";
14
18
  const EXTERNAL_COMMAND_KILL_GRACE_MS = 2_000;
15
19
  const LONG_BASE64_TEXT_PATTERN = /\b[A-Za-z0-9+/]{240,}={0,2}\b/gu;
16
- const OFFICE_TEXT_ENTRY_PATTERN = /\.(?:xml|rels|xhtml|html|htm)$/iu;
20
+ const OFFICE_TEXT_ENTRY_PATTERN = /\.(?:xml|rels|xhtml|html|htm|opf)$/iu;
21
+ const NATURAL_PATH_ORDER = new Intl.Collator("en", { numeric: true, sensitivity: "base" });
17
22
  const HTML_TO_TEXT_OPTIONS = {
18
23
  wordwrap: false,
19
24
  selectors: [
@@ -25,22 +30,27 @@ export async function parseFile(file, options = {}) {
25
30
  throwIfAborted(options.signal);
26
31
  let text;
27
32
  let pages;
33
+ let regions;
34
+ let ocr;
35
+ let sourceLineCoordinates = false;
28
36
  switch (file.extension) {
29
37
  case ".pdf":
30
38
  ;
31
- ({ text, pages } = await parsePdf(file.absolutePath, options));
39
+ ({ text, pages, regions, ocr } = await parsePdf(file, options));
32
40
  break;
33
41
  case ".doc":
34
42
  text = await parseLegacyWord(file.absolutePath, options);
35
43
  break;
36
44
  case ".docx":
37
- text = await parseDocx(file.absolutePath);
45
+ text = await parseDocx(file.absolutePath, options.signal);
38
46
  break;
39
47
  case ".pptx":
40
- text = await parsePptx(file.absolutePath);
48
+ ;
49
+ ({ text, regions } = await parsePptx(file.absolutePath, options.signal));
41
50
  break;
42
51
  case ".xlsx":
43
- text = await parseXlsx(file.absolutePath);
52
+ ;
53
+ ({ text, regions } = await parseXlsx(file.absolutePath, options.signal));
44
54
  break;
45
55
  case ".avif":
46
56
  case ".bmp":
@@ -58,61 +68,138 @@ export async function parseFile(file, options = {}) {
58
68
  case ".odt":
59
69
  case ".ods":
60
70
  case ".odp":
61
- text = await parseOpenDocument(file.absolutePath);
71
+ text = await parseOpenDocument(file.absolutePath, options.signal);
62
72
  break;
63
73
  case ".epub":
64
- text = await parseEpub(file.absolutePath);
74
+ ;
75
+ ({ text, regions } = await parseEpub(file.absolutePath, options.signal));
65
76
  break;
66
77
  case ".html":
67
78
  case ".htm":
68
79
  text = htmlToText(await readFile(file.absolutePath, "utf8"), HTML_TO_TEXT_OPTIONS);
69
80
  break;
70
81
  case ".json":
71
- case ".ipynb":
82
+ case ".ipynb": {
72
83
  text = JSON.stringify(JSON.parse(await readFile(file.absolutePath, "utf8")), null, 2);
73
84
  break;
85
+ }
74
86
  case ".yaml":
75
- case ".yml":
87
+ case ".yml": {
76
88
  text = YAML.stringify(YAML.parse(await readFile(file.absolutePath, "utf8")));
77
89
  break;
90
+ }
78
91
  case ".rtf":
79
92
  text = stripRtf(await readFile(file.absolutePath, "utf8"));
80
93
  break;
81
94
  default:
82
95
  text = await readFile(file.absolutePath, "utf8");
96
+ sourceLineCoordinates = true;
83
97
  }
84
98
  throwIfAborted(options.signal);
85
- const document = { file, text: normalizeText(text) };
99
+ const document = {
100
+ file,
101
+ text: regions ? text : normalizeText(text, sourceLineCoordinates),
102
+ sourceLineCoordinates,
103
+ };
86
104
  if (pages) {
87
105
  document.pages = pages;
88
106
  }
107
+ if (regions) {
108
+ document.regions = regions;
109
+ }
110
+ if (ocr) {
111
+ document.ocr = ocr;
112
+ }
89
113
  return document;
90
114
  }
91
- async function parseDocx(filePath) {
115
+ async function parseDocx(filePath, signal) {
92
116
  const buffer = await readFile(filePath);
93
- unzipOfficeFile(buffer);
117
+ throwIfAborted(signal);
118
+ validateOfficeArchive(buffer, signal);
94
119
  const result = await mammoth.extractRawText({ buffer });
120
+ throwIfAborted(signal);
95
121
  return result.value;
96
122
  }
97
- async function parsePptx(filePath) {
98
- const entries = unzipOfficeFile(await readFile(filePath));
99
- return xmlEntriesToText(entries, [
100
- /^ppt\/slides\/slide\d+\.xml$/u,
101
- /^ppt\/notesSlides\/notesSlide\d+\.xml$/u,
102
- ]);
123
+ async function parsePptx(filePath, signal) {
124
+ const entries = unzipOfficeFile(await readFile(filePath), signal);
125
+ const slides = orderedPresentationSlides(entries, signal);
126
+ const notes = new Map(numberedOfficeParts(entries, /^ppt\/notesSlides\/notesSlide(\d+)\.xml$/u, signal).map((part) => [part.number, part.text]));
127
+ const parts = [];
128
+ for (const [index, slide] of slides.entries()) {
129
+ throwIfAborted(signal);
130
+ const noteText = presentationSlideNotes(entries, slide.number) ?? notes.get(slide.number) ?? "";
131
+ const text = [slide.text, noteText].filter(Boolean).join("\n\n");
132
+ const slideNumber = index + 1;
133
+ if (text) {
134
+ parts.push({
135
+ text,
136
+ contextPath: `Slide ${slideNumber}`,
137
+ location: {
138
+ kind: "slide",
139
+ start: slideNumber,
140
+ end: slideNumber,
141
+ },
142
+ });
143
+ }
144
+ }
145
+ return joinLocatedParts(parts, signal);
146
+ }
147
+ function presentationSlideNotes(entries, slideNumber) {
148
+ const relationships = entries.get(`ppt/slides/_rels/slide${slideNumber}.xml.rels`);
149
+ if (!relationships) {
150
+ return null;
151
+ }
152
+ for (const relationship of xmlStartTags(relationships, "Relationship")) {
153
+ const target = xmlAttribute(relationship, "Target");
154
+ if (!target) {
155
+ continue;
156
+ }
157
+ const entry = path.posix.normalize(path.posix.join("ppt/slides", target));
158
+ if (!/^ppt\/notesSlides\/notesSlide\d+\.xml$/u.test(entry)) {
159
+ continue;
160
+ }
161
+ const xml = entries.get(entry);
162
+ if (xml) {
163
+ return xmlToText(xml);
164
+ }
165
+ }
166
+ return null;
103
167
  }
104
- async function parseXlsx(filePath) {
168
+ async function parseXlsx(filePath, signal) {
105
169
  const buffer = await readFile(filePath);
106
- unzipOfficeFile(buffer);
170
+ throwIfAborted(signal);
171
+ validateOfficeArchive(buffer, signal);
107
172
  const workbook = await readExcelFile(buffer, { trim: false });
108
- const sheets = [];
109
- for (const sheet of workbook) {
110
- const rows = sheet.data.map(spreadsheetRowToText).filter((row) => row.some(Boolean));
111
- if (rows.length > 0) {
112
- sheets.push(`# ${sheet.sheet}`, rows.map((row) => row.join("\t")).join("\n"));
173
+ throwIfAborted(signal);
174
+ const parts = [];
175
+ for (const [sheetIndex, sheet] of workbook.entries()) {
176
+ throwIfAborted(signal);
177
+ let firstRow = true;
178
+ for (const [rowIndex, rawRow] of sheet.data.entries()) {
179
+ throwIfAborted(signal);
180
+ const row = spreadsheetRowToText(rawRow);
181
+ const firstColumn = row.findIndex(Boolean);
182
+ if (firstColumn < 0) {
183
+ continue;
184
+ }
185
+ const lastColumn = lastNonEmptyIndex(row);
186
+ const rowNumber = rowIndex + 1;
187
+ parts.push({
188
+ text: `${firstRow ? `# ${sheet.sheet}\n` : ""}${row.join("\t")}`,
189
+ contextPath: `Sheet: ${sheet.sheet}`,
190
+ location: {
191
+ kind: "sheet",
192
+ start: sheetIndex + 1,
193
+ end: sheetIndex + 1,
194
+ label: sheet.sheet,
195
+ cellStart: `${spreadsheetColumnName(firstColumn + 1)}${rowNumber}`,
196
+ cellEnd: `${spreadsheetColumnName(lastColumn + 1)}${rowNumber}`,
197
+ },
198
+ });
199
+ firstRow = false;
113
200
  }
114
201
  }
115
- return sheets.join("\n\n");
202
+ return joinLocatedParts(parts, signal);
116
203
  }
117
204
  async function parseImage(filePath, extension, options) {
118
205
  if (!OCR_IMAGE_EXTENSIONS.has(extension)) {
@@ -143,59 +230,220 @@ async function parseLegacyWord(filePath, options) {
143
230
  ...(options.signal ? { signal: options.signal } : {}),
144
231
  });
145
232
  }
146
- async function parseOpenDocument(filePath) {
147
- const entries = unzipOfficeFile(await readFile(filePath));
148
- return xmlEntriesToText(entries, [/^content\.xml$/u, /^meta\.xml$/u]);
233
+ async function parseOpenDocument(filePath, signal) {
234
+ const entries = unzipOfficeFile(await readFile(filePath), signal);
235
+ return xmlEntriesToText(entries, [/^content\.xml$/u, /^meta\.xml$/u], signal);
149
236
  }
150
- async function parseEpub(filePath) {
151
- const entries = unzipOfficeFile(await readFile(filePath));
237
+ async function parseEpub(filePath, signal) {
238
+ const entries = unzipOfficeFile(await readFile(filePath), signal);
239
+ const orderedEntries = epubReadingOrder(entries, signal);
152
240
  const parts = [];
153
- for (const [name, content] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
154
- if (!/\.(?:xhtml|html|htm|xml)$/iu.test(name)) {
241
+ for (const [index, name] of orderedEntries.entries()) {
242
+ throwIfAborted(signal);
243
+ const content = entries.get(name);
244
+ if (content === undefined) {
155
245
  continue;
156
246
  }
157
247
  const text = htmlToText(content, HTML_TO_TEXT_OPTIONS);
158
248
  if (text.trim()) {
159
- parts.push(text);
249
+ parts.push({
250
+ text,
251
+ contextPath: `EPUB: ${name}`,
252
+ location: {
253
+ kind: "epub",
254
+ start: index + 1,
255
+ end: index + 1,
256
+ label: name,
257
+ },
258
+ });
160
259
  }
161
260
  }
162
- return parts.join("\n\n");
261
+ return joinLocatedParts(parts, signal);
262
+ }
263
+ function numberedOfficeParts(entries, pattern, signal) {
264
+ const parts = [];
265
+ for (const [name, xml] of entries) {
266
+ throwIfAborted(signal);
267
+ const match = pattern.exec(name);
268
+ const number = Number(match?.[1]);
269
+ const text = match && Number.isSafeInteger(number) && number > 0 ? xmlToText(xml) : "";
270
+ if (text) {
271
+ parts.push({ number, text });
272
+ }
273
+ }
274
+ return parts.sort((left, right) => left.number - right.number);
275
+ }
276
+ function orderedPresentationSlides(entries, signal) {
277
+ const natural = numberedOfficeParts(entries, /^ppt\/slides\/slide(\d+)\.xml$/u, signal);
278
+ const presentation = entries.get("ppt/presentation.xml");
279
+ const relationships = entries.get("ppt/_rels/presentation.xml.rels");
280
+ if (!presentation || !relationships) {
281
+ return natural;
282
+ }
283
+ const targets = new Map();
284
+ for (const relationship of xmlStartTags(relationships, "Relationship")) {
285
+ throwIfAborted(signal);
286
+ const id = xmlAttribute(relationship, "Id");
287
+ const target = xmlAttribute(relationship, "Target");
288
+ if (id && target) {
289
+ targets.set(id, path.posix.normalize(path.posix.join("ppt", target)));
290
+ }
291
+ }
292
+ const byNumber = new Map(natural.map((slide) => [slide.number, slide]));
293
+ const ordered = [];
294
+ for (const slide of xmlStartTags(presentation, "sldId")) {
295
+ throwIfAborted(signal);
296
+ const relationshipId = xmlAttribute(slide, "r:id");
297
+ const target = relationshipId ? targets.get(relationshipId) : undefined;
298
+ const match = target ? /^ppt\/slides\/slide(\d+)\.xml$/u.exec(target) : null;
299
+ const number = Number(match?.[1]);
300
+ const part = Number.isSafeInteger(number) ? byNumber.get(number) : undefined;
301
+ if (!part) {
302
+ continue;
303
+ }
304
+ byNumber.delete(number);
305
+ ordered.push(part);
306
+ }
307
+ return ordered.length > 0 ? [...ordered, ...byNumber.values()] : natural;
308
+ }
309
+ function joinLocatedParts(parts, signal) {
310
+ let text = "";
311
+ const regions = [];
312
+ for (const part of parts) {
313
+ throwIfAborted(signal);
314
+ const normalized = normalizeText(part.text);
315
+ if (!normalized) {
316
+ continue;
317
+ }
318
+ if (text) {
319
+ text += "\n\n";
320
+ }
321
+ const charStart = text.length;
322
+ text += normalized;
323
+ regions.push({
324
+ charStart,
325
+ charEnd: text.length,
326
+ contextPath: part.contextPath,
327
+ location: part.location,
328
+ });
329
+ }
330
+ return { text, regions };
331
+ }
332
+ function epubReadingOrder(entries, signal) {
333
+ const htmlEntries = [];
334
+ for (const name of entries.keys()) {
335
+ throwIfAborted(signal);
336
+ if (/\.(?:xhtml|html|htm)$/iu.test(name)) {
337
+ htmlEntries.push(name);
338
+ }
339
+ }
340
+ htmlEntries.sort((left, right) => NATURAL_PATH_ORDER.compare(left, right));
341
+ const containerPath = [...entries.keys()].find((name) => name.toLowerCase() === "meta-inf/container.xml");
342
+ const container = containerPath ? entries.get(containerPath) : undefined;
343
+ const packagePath = container ? firstXmlAttribute(container, "rootfile", "full-path") : null;
344
+ const packageXml = packagePath ? entries.get(packagePath) : undefined;
345
+ if (!packagePath || !packageXml) {
346
+ return htmlEntries;
347
+ }
348
+ const packageDirectory = path.posix.dirname(packagePath);
349
+ const manifest = new Map();
350
+ for (const element of xmlStartTags(packageXml, "item")) {
351
+ throwIfAborted(signal);
352
+ const id = xmlAttribute(element, "id");
353
+ const href = xmlAttribute(element, "href");
354
+ if (id && href) {
355
+ manifest.set(id, resolveEpubEntry(packageDirectory, href));
356
+ }
357
+ }
358
+ const spine = [];
359
+ for (const element of xmlStartTags(packageXml, "itemref")) {
360
+ throwIfAborted(signal);
361
+ const id = xmlAttribute(element, "idref");
362
+ const entry = id ? manifest.get(id) : undefined;
363
+ if (entry && entries.has(entry)) {
364
+ spine.push(entry);
365
+ }
366
+ }
367
+ return spine.length > 0 ? [...new Set(spine)] : htmlEntries;
368
+ }
369
+ function resolveEpubEntry(packageDirectory, href) {
370
+ const withoutFragment = href.split("#", 1)[0] ?? href;
371
+ let decoded = withoutFragment;
372
+ try {
373
+ decoded = decodeURIComponent(withoutFragment);
374
+ }
375
+ catch {
376
+ // Keep the literal manifest path when percent encoding is malformed.
377
+ }
378
+ return path.posix.normalize(path.posix.join(packageDirectory, decoded));
379
+ }
380
+ function xmlStartTags(xml, localName) {
381
+ return [...xml.matchAll(new RegExp(`<(?:[A-Za-z_][\\w.-]*:)?${localName}\\b[^>]*>`, "giu"))].map((match) => match[0]);
382
+ }
383
+ function firstXmlAttribute(xml, element, attribute) {
384
+ const tag = xmlStartTags(xml, element)[0];
385
+ return tag ? xmlAttribute(tag, attribute) : null;
163
386
  }
164
- function unzipOfficeFile(buffer) {
165
- let textEntryCount = 0;
166
- let totalTextBytes = 0;
167
- let rejectedForSafetyLimit = false;
387
+ function xmlAttribute(element, name) {
388
+ const match = new RegExp(`\\b${name}\\s*=\\s*(["'])(.*?)\\1`, "iu").exec(element);
389
+ return match?.[2] ? decodeXmlEntities(match[2]) : null;
390
+ }
391
+ function validateOfficeArchive(buffer, signal) {
392
+ const state = officeArchiveLimitsState();
393
+ unzipSync(new Uint8Array(buffer), {
394
+ filter: (file) => {
395
+ throwIfAborted(signal);
396
+ includeOfficeTextEntry(file, state);
397
+ return false;
398
+ },
399
+ });
400
+ assertOfficeArchiveLimits(state);
401
+ }
402
+ function unzipOfficeFile(buffer, signal) {
403
+ const state = officeArchiveLimitsState();
168
404
  const unzipped = unzipSync(new Uint8Array(buffer), {
169
405
  filter: (file) => {
170
- if (!OFFICE_TEXT_ENTRY_PATTERN.test(file.name)) {
171
- return false;
172
- }
173
- if (file.originalSize > MAX_OFFICE_XML_ENTRY_BYTES) {
174
- rejectedForSafetyLimit = true;
175
- return false;
176
- }
177
- textEntryCount += 1;
178
- totalTextBytes += file.originalSize;
179
- if (textEntryCount > MAX_OFFICE_TEXT_ENTRY_COUNT ||
180
- totalTextBytes > MAX_OFFICE_XML_TOTAL_BYTES) {
181
- rejectedForSafetyLimit = true;
182
- return false;
183
- }
184
- return true;
406
+ throwIfAborted(signal);
407
+ return includeOfficeTextEntry(file, state);
185
408
  },
186
409
  });
187
- if (rejectedForSafetyLimit) {
188
- throw new Error("Archive text payload exceeds Ragmir safety limits.");
189
- }
410
+ assertOfficeArchiveLimits(state);
190
411
  const entries = new Map();
191
412
  for (const [name, content] of Object.entries(unzipped)) {
413
+ throwIfAborted(signal);
192
414
  entries.set(name, strFromU8(content));
193
415
  }
194
416
  return entries;
195
417
  }
196
- function xmlEntriesToText(entries, patterns) {
418
+ function officeArchiveLimitsState() {
419
+ return { textEntryCount: 0, totalTextBytes: 0, rejected: false };
420
+ }
421
+ function includeOfficeTextEntry(file, state) {
422
+ if (!OFFICE_TEXT_ENTRY_PATTERN.test(file.name)) {
423
+ return false;
424
+ }
425
+ if (file.originalSize > MAX_OFFICE_XML_ENTRY_BYTES) {
426
+ state.rejected = true;
427
+ return false;
428
+ }
429
+ state.textEntryCount += 1;
430
+ state.totalTextBytes += file.originalSize;
431
+ if (state.textEntryCount > MAX_OFFICE_TEXT_ENTRY_COUNT ||
432
+ state.totalTextBytes > MAX_OFFICE_XML_TOTAL_BYTES) {
433
+ state.rejected = true;
434
+ return false;
435
+ }
436
+ return true;
437
+ }
438
+ function assertOfficeArchiveLimits(state) {
439
+ if (state.rejected) {
440
+ throw new Error("Archive text payload exceeds Ragmir safety limits.");
441
+ }
442
+ }
443
+ function xmlEntriesToText(entries, patterns, signal) {
197
444
  const parts = [];
198
445
  for (const [name, xml] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
446
+ throwIfAborted(signal);
199
447
  if (patterns.some((pattern) => pattern.test(name))) {
200
448
  const text = xmlToText(xml);
201
449
  if (text) {
@@ -227,6 +475,24 @@ function spreadsheetCellToText(value) {
227
475
  }
228
476
  return JSON.stringify(value);
229
477
  }
478
+ function spreadsheetColumnName(column) {
479
+ let current = column;
480
+ let name = "";
481
+ while (current > 0) {
482
+ current -= 1;
483
+ name = String.fromCharCode(65 + (current % 26)) + name;
484
+ current = Math.floor(current / 26);
485
+ }
486
+ return name;
487
+ }
488
+ function lastNonEmptyIndex(values) {
489
+ for (let index = values.length - 1; index >= 0; index -= 1) {
490
+ if (values[index]) {
491
+ return index;
492
+ }
493
+ }
494
+ return -1;
495
+ }
230
496
  function xmlToText(xml) {
231
497
  return normalizeText(decodeXmlEntities(xml
232
498
  .replace(/<w:tab\/>/gu, " ")
@@ -250,8 +516,8 @@ function decodeXmlEntities(input) {
250
516
  .replace(/&apos;/gu, "'")
251
517
  .replace(/&amp;/gu, "&");
252
518
  }
253
- async function parsePdf(filePath, options) {
254
- const buffer = await readFile(filePath);
519
+ async function parsePdf(file, options) {
520
+ const buffer = await readFile(file.absolutePath);
255
521
  let pdf;
256
522
  try {
257
523
  pdf = await getDocumentProxy(new Uint8Array(buffer));
@@ -263,12 +529,11 @@ async function parsePdf(filePath, options) {
263
529
  }
264
530
  throw error;
265
531
  }
532
+ const pageTexts = [];
266
533
  try {
267
534
  if (pdf.numPages > MAX_PDF_PAGES) {
268
535
  throw new Error(`PDF has ${pdf.numPages} pages; the safety limit is ${MAX_PDF_PAGES}.`);
269
536
  }
270
- const pageTexts = [];
271
- let totalCharacters = 0;
272
537
  for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber += 1) {
273
538
  const page = await pdf.getPage(pageNumber);
274
539
  let pageText = "";
@@ -284,28 +549,148 @@ async function parsePdf(filePath, options) {
284
549
  page.cleanup();
285
550
  }
286
551
  pageText = normalizePdfPageText(pageText);
287
- if (!pageText && options.pdfOcrCommand && options.pdfOcrCommand.length > 0) {
288
- pageText = normalizePdfPageText(await runExternalTextCommand(filePath, {
289
- command: options.pdfOcrCommand,
290
- cwd: options.projectRoot,
291
- label: `OCR command for PDF page ${pageNumber}`,
292
- timeoutMs: options.pdfOcrTimeoutMs ?? 120_000,
293
- pathEnvName: "RAGMIR_PDF_PATH",
294
- pageNumber,
295
- ...(options.signal ? { signal: options.signal } : {}),
296
- }));
297
- }
298
- totalCharacters += pageText.length;
299
- if (totalCharacters > MAX_PDF_TEXT_CHARACTERS) {
300
- throw new Error(`PDF text exceeds the ${MAX_PDF_TEXT_CHARACTERS} character safety limit.`);
301
- }
302
552
  pageTexts.push(pageText);
303
553
  }
304
- return joinPdfPages(pageTexts);
305
554
  }
306
555
  finally {
307
556
  await pdf.destroy();
308
557
  }
558
+ const blankPages = pageTexts.flatMap((text, index) => (text ? [] : [index + 1]));
559
+ const ocr = blankPages.length > 0 && options.pdfOcrCommand && options.pdfOcrCommand.length > 0
560
+ ? await fillPdfOcrPages(file, pageTexts, blankPages, options)
561
+ : undefined;
562
+ const totalCharacters = pageTexts.reduce((total, pageText) => total + pageText.length, 0);
563
+ if (totalCharacters > MAX_PDF_TEXT_CHARACTERS) {
564
+ throw new Error(`PDF text exceeds the ${MAX_PDF_TEXT_CHARACTERS} character safety limit.`);
565
+ }
566
+ return { ...joinPdfPages(pageTexts), ...(ocr ? { ocr } : {}) };
567
+ }
568
+ async function fillPdfOcrPages(file, pageTexts, blankPages, options) {
569
+ const startedAt = performance.now();
570
+ const command = options.pdfOcrCommand ?? [];
571
+ const projectRoot = options.projectRoot ?? path.dirname(file.absolutePath);
572
+ const commandIdentity = await pdfOcrCommandIdentity(command, projectRoot);
573
+ const metrics = {
574
+ pages: blankPages.length,
575
+ cacheHits: 0,
576
+ cacheMisses: 0,
577
+ batches: 0,
578
+ subprocesses: commandIdentity.subprocesses,
579
+ durationMs: 0,
580
+ };
581
+ const missingPages = [];
582
+ for (const pageNumber of blankPages) {
583
+ const cached = await readPdfOcrCache(projectRoot, pdfOcrCacheIdentity(file.checksum, pageNumber, commandIdentity));
584
+ if (cached === null) {
585
+ metrics.cacheMisses += 1;
586
+ missingPages.push(pageNumber);
587
+ }
588
+ else {
589
+ metrics.cacheHits += 1;
590
+ pageTexts[pageNumber - 1] = cached;
591
+ }
592
+ }
593
+ if (commandIdentity.supportsBatch) {
594
+ for (const batch of pdfOcrPageBatches(missingPages)) {
595
+ const output = await runExternalTextCommand(file.absolutePath, {
596
+ command,
597
+ cwd: options.projectRoot,
598
+ label: `OCR command for PDF pages ${batch.join(",")}`,
599
+ timeoutMs: options.pdfOcrTimeoutMs ?? 120_000,
600
+ pathEnvName: "RAGMIR_PDF_PATH",
601
+ pageNumbers: batch,
602
+ ...(options.signal ? { signal: options.signal } : {}),
603
+ });
604
+ const result = parsePdfOcrBatchOutput(output, batch);
605
+ metrics.batches += 1;
606
+ metrics.subprocesses += 1 + result.subprocesses;
607
+ for (const page of result.pages) {
608
+ const text = normalizePdfPageText(page.text);
609
+ pageTexts[page.page - 1] = text;
610
+ await writePdfOcrCache(projectRoot, pdfOcrCacheIdentity(file.checksum, page.page, commandIdentity), text);
611
+ }
612
+ }
613
+ }
614
+ else {
615
+ for (const pageNumber of missingPages) {
616
+ const text = normalizePdfPageText(await runExternalTextCommand(file.absolutePath, {
617
+ command,
618
+ cwd: options.projectRoot,
619
+ label: `OCR command for PDF page ${pageNumber}`,
620
+ timeoutMs: options.pdfOcrTimeoutMs ?? 120_000,
621
+ pathEnvName: "RAGMIR_PDF_PATH",
622
+ pageNumber,
623
+ ...(options.signal ? { signal: options.signal } : {}),
624
+ }));
625
+ metrics.batches += 1;
626
+ metrics.subprocesses += 1;
627
+ pageTexts[pageNumber - 1] = text;
628
+ await writePdfOcrCache(projectRoot, pdfOcrCacheIdentity(file.checksum, pageNumber, commandIdentity), text);
629
+ }
630
+ }
631
+ metrics.durationMs = Math.round((performance.now() - startedAt) * 1_000) / 1_000;
632
+ return metrics;
633
+ }
634
+ function pdfOcrCacheIdentity(sourceChecksum, page, command) {
635
+ return {
636
+ sourceChecksum,
637
+ page,
638
+ engine: command.engine,
639
+ engineVersion: command.engineVersion,
640
+ language: command.language,
641
+ dpi: command.dpi,
642
+ parserPolicy: PDF_OCR_PARSER_POLICY,
643
+ commandFingerprint: command.commandFingerprint,
644
+ };
645
+ }
646
+ function pdfOcrPageBatches(pages) {
647
+ const batches = [];
648
+ for (const page of pages) {
649
+ const current = batches.at(-1);
650
+ if (!current ||
651
+ current.length >= MAX_OCR_BATCH_PAGES ||
652
+ page - (current[0] ?? page) >= MAX_OCR_BATCH_PAGES) {
653
+ batches.push([page]);
654
+ }
655
+ else {
656
+ current.push(page);
657
+ }
658
+ }
659
+ return batches;
660
+ }
661
+ function parsePdfOcrBatchOutput(output, requestedPages) {
662
+ let value;
663
+ try {
664
+ value = JSON.parse(output);
665
+ }
666
+ catch {
667
+ throw new Error("Batched PDF OCR command returned invalid JSON.");
668
+ }
669
+ if (!isRecord(value)) {
670
+ throw new Error("Batched PDF OCR command returned an invalid result.");
671
+ }
672
+ if (!Number.isInteger(value.subprocesses) ||
673
+ Number(value.subprocesses) < 0 ||
674
+ Number(value.subprocesses) > 100 ||
675
+ !Array.isArray(value.pages)) {
676
+ throw new Error("Batched PDF OCR command returned invalid process diagnostics.");
677
+ }
678
+ const pages = value.pages.flatMap((entry) => {
679
+ if (isRecord(entry) && Number.isInteger(entry.page) && typeof entry.text === "string") {
680
+ return [
681
+ {
682
+ page: Number(entry.page),
683
+ text: entry.text,
684
+ },
685
+ ];
686
+ }
687
+ return [];
688
+ });
689
+ if (pages.length !== requestedPages.length ||
690
+ pages.some((page, index) => page.page !== requestedPages[index])) {
691
+ throw new Error("Batched PDF OCR command returned pages outside the requested ordered batch.");
692
+ }
693
+ return { pages, subprocesses: Number(value.subprocesses) };
309
694
  }
310
695
  function isPdfTextItem(value) {
311
696
  return (typeof value === "object" && value !== null && "str" in value && typeof value.str === "string");
@@ -316,15 +701,23 @@ function normalizePdfPageText(text) {
316
701
  function joinPdfPages(pageTexts) {
317
702
  let text = "";
318
703
  const pages = [];
704
+ const regions = [];
319
705
  for (const [index, pageText] of pageTexts.entries()) {
320
706
  if (index > 0) {
321
707
  text += "\n\n";
322
708
  }
323
709
  const charStart = text.length;
324
710
  text += pageText;
325
- pages.push({ pageNumber: index + 1, charStart, charEnd: text.length });
711
+ const pageNumber = index + 1;
712
+ pages.push({ pageNumber, charStart, charEnd: text.length });
713
+ regions.push({
714
+ charStart,
715
+ charEnd: text.length,
716
+ contextPath: `Page ${pageNumber}`,
717
+ location: { kind: "page", start: pageNumber, end: pageNumber },
718
+ });
326
719
  }
327
- return { text, pages };
720
+ return { text, pages, regions };
328
721
  }
329
722
  async function runExternalTextCommand(filePath, options) {
330
723
  throwIfAborted(options.signal);
@@ -336,8 +729,9 @@ async function runExternalTextCommand(filePath, options) {
336
729
  const hasInputPlaceholder = command.some((part) => part.includes("{input}"));
337
730
  const args = configuredArgs.map((part) => part.replaceAll("{input}", filePath));
338
731
  const pageNumber = options.pageNumber === undefined ? "" : String(options.pageNumber);
732
+ const pageNumbers = options.pageNumbers?.join(",") ?? "";
339
733
  for (const [index, arg] of args.entries()) {
340
- args[index] = arg.replaceAll("{page}", pageNumber);
734
+ args[index] = arg.replaceAll("{page}", pageNumber).replaceAll("{pages}", pageNumbers);
341
735
  }
342
736
  if (!hasInputPlaceholder) {
343
737
  args.push(filePath);
@@ -350,6 +744,7 @@ async function runExternalTextCommand(filePath, options) {
350
744
  ...externalCommandEnvironment(),
351
745
  [options.pathEnvName]: filePath,
352
746
  ...(options.pageNumber === undefined ? {} : { RAGMIR_PDF_PAGE: pageNumber }),
747
+ ...(options.pageNumbers === undefined ? {} : { RAGMIR_PDF_PAGES: pageNumbers }),
353
748
  },
354
749
  stdio: ["ignore", "pipe", "pipe"],
355
750
  });
@@ -478,10 +873,12 @@ function externalCommandEnvironment() {
478
873
  return value === undefined ? [] : [[name, value]];
479
874
  }));
480
875
  }
481
- function normalizeText(input) {
482
- return input
483
- .replace(LONG_BASE64_TEXT_PATTERN, " ")
484
- .replace(/\r\n/g, "\n")
876
+ function normalizeText(input, preserveSourceLines = false) {
877
+ const normalized = input.replace(LONG_BASE64_TEXT_PATTERN, " ").replace(/\r\n?/gu, "\n");
878
+ if (preserveSourceLines) {
879
+ return normalized.replace(/[ \t]+\n/gu, "\n").trimEnd();
880
+ }
881
+ return normalized
485
882
  .replace(/[ \t]+\n/g, "\n")
486
883
  .replace(/\n{4,}/g, "\n\n\n")
487
884
  .trim();