@hraness/wordcell 0.26.2 → 0.27.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 (79) hide show
  1. package/README.md +36 -21
  2. package/dist/capture.js +15 -7
  3. package/dist/cli-bin.js +2 -2
  4. package/dist/cli.js +21 -19
  5. package/dist/clip/acquire.js +4 -3
  6. package/dist/clip/bundle-reader.js +3 -2
  7. package/dist/clip/cli.js +8 -7
  8. package/dist/clip/defuddle-worker.js +4 -1
  9. package/dist/clip/doctor.js +5 -4
  10. package/dist/clip/jobs.js +2 -1
  11. package/dist/clip/persist.js +2 -1
  12. package/dist/clip/refresh.js +4 -3
  13. package/dist/evaluation-builder-bin.js +2 -2
  14. package/dist/evaluation-builder.js +3 -3
  15. package/dist/evaluation-kb.js +3 -3
  16. package/dist/{index-mp5y3ty3.js → index-30v9w4cy.js} +36 -20
  17. package/dist/{index-9paxge84.js → index-3hts6yy9.js} +1 -1
  18. package/dist/{index-h9vyd03z.js → index-58azscg5.js} +7 -6
  19. package/dist/{index-9zkba8hr.js → index-7bqwzjba.js} +7 -6
  20. package/dist/{index-j4zgmzjr.js → index-8djdwjjd.js} +1 -1
  21. package/dist/{index-5h5awwh4.js → index-b40e2dkx.js} +2 -2
  22. package/dist/index-e2wstc7t.js +323 -0
  23. package/dist/{index-hacpnaew.js → index-fd8j527z.js} +1 -1
  24. package/dist/{index-mxxxytys.js → index-g63d3937.js} +4 -0
  25. package/dist/{index-k86wepd8.js → index-h2w7syy2.js} +5 -1
  26. package/dist/{index-9fywek7g.js → index-ht14xne3.js} +1 -1
  27. package/dist/{index-82r6g4py.js → index-k9yzc4vn.js} +1 -1
  28. package/dist/{index-m9p2a4mj.js → index-my50xsa3.js} +221 -36
  29. package/dist/{index-ncwzmsge.js → index-nwdzhj1h.js} +2 -2
  30. package/dist/{index-9rf81m0p.js → index-pbz3qw1s.js} +35 -20
  31. package/dist/index-pn14a2m5.js +59 -0
  32. package/dist/{index-nd8v7r0z.js → index-sp43012q.js} +1 -1
  33. package/dist/{index-8pabwzqg.js → index-t4hjm1kk.js} +2 -1
  34. package/dist/{index-npg9z1a4.js → index-xn5q4kne.js} +5 -1
  35. package/dist/{index-j2jt49gr.js → index-yatn62qq.js} +1 -1
  36. package/dist/index.js +14 -4
  37. package/dist/pdf.js +438 -4
  38. package/dist/portfolio.js +3 -3
  39. package/dist/rerank-clef.js +16 -0
  40. package/dist/rerank-typesafe.js +3 -1
  41. package/dist/sdk.js +2 -2
  42. package/dist/{support-9qdr4hb8.js → support-s059eytv.js} +20 -18
  43. package/dist/url-intelligence.js +4 -3
  44. package/dist/workflows/decision-context.js +3 -3
  45. package/dist/workflows/index.js +3 -3
  46. package/package.json +23 -9
  47. package/skills/wordcell/SKILL.md +1 -1
  48. package/skills/wordcell/references/query.md +11 -6
  49. package/src/capture.ts +5 -0
  50. package/src/cli-help.ts +2 -1
  51. package/src/cli-program.ts +23 -8
  52. package/src/clip/browser-profiles.ts +1 -1
  53. package/src/clip/bundle-reader.ts +3 -0
  54. package/src/clip/capture.ts +1 -0
  55. package/src/clip/extract.ts +2 -0
  56. package/src/clip/lib.ts +3 -0
  57. package/src/clip/metadata-search-tool/Cargo.lock +6 -6
  58. package/src/clip/metadata-search.ts +1 -1
  59. package/src/clip/metadata.ts +232 -0
  60. package/src/clip/persist.ts +3 -0
  61. package/src/index.ts +2 -1
  62. package/src/pdf/bundle-reader.ts +457 -0
  63. package/src/pdf/capture.ts +6 -2
  64. package/src/pdf/cli.ts +2 -1
  65. package/src/pdf/errors.ts +11 -0
  66. package/src/pdf/extract.ts +16 -13
  67. package/src/pdf/layout.ts +136 -3
  68. package/src/pdf/markdown.ts +7 -1
  69. package/src/pdf/model.ts +9 -0
  70. package/src/pdf/persist.ts +8 -2
  71. package/src/pdf/source.ts +5 -4
  72. package/src/pdf/tools.ts +8 -6
  73. package/src/pdf.ts +10 -0
  74. package/src/rerank-clef.ts +52 -0
  75. package/src/rerank-credentials.ts +11 -2
  76. package/src/rerank-typesafe.ts +44 -20
  77. package/src/rerank.ts +1 -1
  78. package/src/sdk.ts +4 -2
  79. /package/dist/{index-cd75vky9.js → index-jbtyp6q5.js} +0 -0
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  countWords,
4
4
  sniffImage
5
- } from "./index-k86wepd8.js";
5
+ } from "./index-h2w7syy2.js";
6
6
  import {
7
7
  renderFailure,
8
8
  sentence,
@@ -10,11 +10,11 @@ import {
10
10
  stderrStyle,
11
11
  terminalOutput,
12
12
  yamlString
13
- } from "./index-cd75vky9.js";
13
+ } from "./index-jbtyp6q5.js";
14
14
  import {
15
15
  redactSensitiveText,
16
16
  sanitizeArtifactUrl
17
- } from "./index-mxxxytys.js";
17
+ } from "./index-g63d3937.js";
18
18
  import {
19
19
  safeFetch
20
20
  } from "./index-e5fbsywq.js";
@@ -26,6 +26,16 @@ import {
26
26
  sanitizeTerminalText
27
27
  } from "./index-1xxnjn0d.js";
28
28
 
29
+ // src/pdf/errors.ts
30
+ class PdfCaptureError extends Error {
31
+ code;
32
+ constructor(code, message, options) {
33
+ super(message, options);
34
+ this.code = code;
35
+ this.name = "PdfCaptureError";
36
+ }
37
+ }
38
+
29
39
  // src/pdf/args.ts
30
40
  var pdfUsage = `wordcell pdf \u2014 save a local or public remote PDF as an auditable Markdown bundle
31
41
 
@@ -521,7 +531,7 @@ function layoutBlocks(pages) {
521
531
  const repeatedMargins = repeatedMarginLines(pages, linesByPage);
522
532
  const output = [];
523
533
  for (const page of pages) {
524
- const lines = (linesByPage.get(page.page) ?? []).filter((line) => !repeatedMargins.has(normalizedMarginText(line.text)));
534
+ const lines = (linesByPage.get(page.page) ?? []).filter((line) => !(line.top <= page.height * 0.06 || line.top + line.height >= page.height * 0.94) || !repeatedMargins.has(normalizedMarginText(line.text)));
525
535
  const steps = lines.slice(1).map((line, index) => Math.max(0, line.top - (lines[index]?.top ?? line.top)));
526
536
  const normalStep = median(steps.filter((step) => step > 0), Math.max(1, bodySize * 1.4));
527
537
  const events = [
@@ -548,7 +558,7 @@ function layoutBlocks(pages) {
548
558
  const { line, index } = event;
549
559
  const previous = lines[index - 1];
550
560
  const gapBefore = previous === undefined ? Number.POSITIVE_INFINITY : line.top - previous.top;
551
- const fontHeading = line.fontSize > bodySize * 1.08;
561
+ const fontHeading = line.fontSize > bodySize * 1.08 && !/[=\u222B\u2211\u221A\u2070\u00B9\u00B2\u00B3\u2074\u2075\u2076\u2077\u2078\u2079]/u.test(line.text);
552
562
  const boldHeading = line.boldRatio >= 0.78 && line.text.length <= 180 && gapBefore >= normalStep * 1.45;
553
563
  if (line.text.length <= 240 && (fontHeading || boldHeading)) {
554
564
  flushParagraph();
@@ -572,7 +582,7 @@ function layoutBlocks(pages) {
572
582
  }
573
583
  flushParagraph();
574
584
  }
575
- return output.filter((block) => block.kind === "image" || block.text.trim() !== "");
585
+ return output.filter((block) => block.kind === "image" || block.kind === "table" || block.text.trim() !== "");
576
586
  }
577
587
  function parsePdfImageInterpretations(value) {
578
588
  if (!Array.isArray(value))
@@ -673,6 +683,160 @@ function parsePdfImageInterpretations(value) {
673
683
  }
674
684
  return output;
675
685
  }
686
+ function analyzePdfLayout(pages) {
687
+ const issues = [];
688
+ const blocks = [];
689
+ const marginLines = new Map(pages.filter((page) => page.text.length <= 4000).map((page) => [page.page, pageLines(page)]));
690
+ const margins = repeatedMarginLines(pages, marginLines);
691
+ for (const originalInput of pages) {
692
+ const original = { ...originalInput, text: originalInput.text.filter((fragment) => !(fragment.top <= originalInput.height * 0.06 || fragment.top + fragment.height >= originalInput.height * 0.94) || !margins.has(normalizedMarginText(fragment.text))) };
693
+ const warn = (code) => {
694
+ if (!issues.some((issue) => issue.page === original.page && issue.code === code))
695
+ issues.push({ page: original.page, code });
696
+ };
697
+ if (original.text.length > 4000) {
698
+ warn("reading-order-uncertain");
699
+ blocks.push(...layoutBlocks([original]));
700
+ continue;
701
+ }
702
+ if (original.text.some((fragment) => /[\u222B\u2211\u221A]/u.test(fragment.text)))
703
+ warn("math-structure-uncertain");
704
+ const text = [...original.text].sort((a, b) => a.top - b.top || a.left - b.left);
705
+ const consumed = new Set;
706
+ const replacements = new Map;
707
+ const superscripts = { "0": "\u2070", "1": "\xB9", "2": "\xB2", "3": "\xB3", "4": "\u2074", "5": "\u2075", "6": "\u2076", "7": "\u2077", "8": "\u2078", "9": "\u2079", "+": "\u207A", "-": "\u207B", "=": "\u207C", "(": "\u207D", ")": "\u207E", n: "\u207F", i: "\u2071" };
708
+ for (const small of text) {
709
+ const bases = text.filter((base2) => base2 !== small && base2.fontSize >= small.fontSize * 1.3 && small.top < base2.top && small.top + small.height > base2.top - base2.height * 0.25 && Math.abs(small.left - (base2.left + base2.width)) < base2.fontSize * 0.6);
710
+ if (bases.length === 0)
711
+ continue;
712
+ const base = bases[0];
713
+ const glyphs = [...small.text.trim()];
714
+ if (bases.length === 1 && glyphs.length > 0 && glyphs.length <= 8 && glyphs.every((glyph) => superscripts[glyph] !== undefined) && !replacements.has(base)) {
715
+ replacements.set(base, { ...base, text: base.text + glyphs.map((glyph) => superscripts[glyph]).join(""), width: small.left + small.width - base.left });
716
+ consumed.add(small);
717
+ } else
718
+ warn("math-structure-uncertain");
719
+ }
720
+ const fragments = text.filter((fragment) => !consumed.has(fragment)).map((fragment) => replacements.get(fragment) ?? fragment);
721
+ const page = { ...original, text: fragments };
722
+ const rows = [];
723
+ for (const fragment of fragments) {
724
+ const row = rows.at(-1);
725
+ if (row === undefined || Math.abs(fragment.top - row[0].top) > Math.max(2, fragment.height * 0.2))
726
+ rows.push([fragment]);
727
+ else
728
+ row.push(fragment);
729
+ }
730
+ const cellRows = rows.map((row) => {
731
+ const groups = [];
732
+ for (const fragment of [...row].sort((a, b) => a.left - b.left)) {
733
+ const prior = groups.at(-1), last = prior?.at(-1);
734
+ if (last === undefined || fragment.left - (last.left + last.width) > Math.max(12, fragment.fontSize * 1.5))
735
+ groups.push([fragment]);
736
+ else
737
+ prior.push(fragment);
738
+ }
739
+ return groups;
740
+ });
741
+ const tables = [];
742
+ for (let index = 0;index < cellRows.length; index++) {
743
+ const first = cellRows[index];
744
+ if (first.length < 2 || first.length > 12)
745
+ continue;
746
+ const group = [first];
747
+ let next = index + 1;
748
+ for (;next < cellRows.length; next++) {
749
+ const candidate = cellRows[next];
750
+ if (candidate.length !== first.length || candidate.some((cell, column) => Math.abs(cell[0].left - first[column][0].left) > 4))
751
+ break;
752
+ if (candidate[0][0].top - group.at(-1)[0][0].top > Math.max(40, candidate[0][0].height * 3))
753
+ break;
754
+ group.push(candidate);
755
+ }
756
+ const cells = group.map((row) => row.map((cell) => joinFragments(cell)));
757
+ const header = first.every((cell) => cell.every((fragment) => fragment.bold));
758
+ const numeric = cells.slice(1).some((row) => row.slice(1).some((cell) => /^[-+\d$\u20AC\u00A3]/u.test(cell.trim())));
759
+ const compact = cells.every((row) => row.every((cell) => cell.length <= 100));
760
+ if (group.length >= 3 && compact && header && numeric) {
761
+ const members = group.flat(2);
762
+ tables.push({ top: Math.min(...members.map((fragment) => fragment.top)), bottom: Math.max(...members.map((fragment) => fragment.top + fragment.height)), block: { kind: "table", page: page.page, rows: cells }, fragments: members });
763
+ index = next - 1;
764
+ } else if (group.length >= 3 && first.length >= 3)
765
+ warn("table-structure-uncertain");
766
+ }
767
+ const tableFragments = new Set(tables.flatMap((table) => table.fragments));
768
+ const remaining = fragments.filter((fragment) => !tableFragments.has(fragment));
769
+ const emitBand = (band, images) => {
770
+ if (band.length === 0 && images.length === 0)
771
+ return;
772
+ const candidates = new Set;
773
+ for (const row of cellRows) {
774
+ for (let i = 1;i < row.length; i++) {
775
+ const left = row[i - 1].at(-1), right = row[i][0];
776
+ const gap = right.left - (left.left + left.width);
777
+ if (gap >= Math.max(page.width * 0.025, left.fontSize * 1.5))
778
+ candidates.add(Math.round((left.left + left.width + right.left) / 2));
779
+ }
780
+ }
781
+ const valid = [...candidates].filter((gutter) => {
782
+ const left = band.filter((fragment) => fragment.left + fragment.width <= gutter);
783
+ const right = band.filter((fragment) => fragment.left >= gutter);
784
+ if (left.length + right.length !== band.length || left.length < 3 || right.length < 3)
785
+ return false;
786
+ if (left.filter((fragment) => fragment.width > page.width * 0.18).length < 2 || right.filter((fragment) => fragment.width > page.width * 0.18).length < 2)
787
+ return false;
788
+ const overlap = Math.min(Math.max(...left.map((fragment) => fragment.top)), Math.max(...right.map((fragment) => fragment.top))) - Math.max(Math.min(...left.map((fragment) => fragment.top)), Math.min(...right.map((fragment) => fragment.top)));
789
+ return overlap > Math.max(...band.map((fragment) => fragment.height)) * 1.5;
790
+ });
791
+ if (valid.length > 0) {
792
+ const gutter = valid[Math.floor(valid.length / 2)];
793
+ if (images.some((image) => image.left < gutter && image.left + image.width > gutter)) {
794
+ warn("reading-order-uncertain");
795
+ blocks.push(...layoutBlocks([{ ...page, text: band, images }]));
796
+ return;
797
+ }
798
+ for (const left of [true, false])
799
+ blocks.push(...layoutBlocks([{ ...page, text: band.filter((fragment) => left ? fragment.left + fragment.width <= gutter : fragment.left >= gutter), images: images.filter((image) => left ? image.left + image.width <= gutter : image.left >= gutter) }]));
800
+ } else {
801
+ if (band.length >= 6 && cellRows.filter((row) => row.length >= 2 && row.some((cell) => band.includes(cell[0]))).length >= 3)
802
+ warn("reading-order-uncertain");
803
+ blocks.push(...layoutBlocks([{ ...page, text: band, images }]));
804
+ }
805
+ };
806
+ const bodyFont = weightedBodyFontSize(pageLines(page));
807
+ const gutterVotes = new Map;
808
+ for (const row of cellRows)
809
+ for (let i = 1;i < row.length; i++) {
810
+ const left = row[i - 1].at(-1), right = row[i][0];
811
+ if (right.left - (left.left + left.width) < Math.max(12, left.fontSize * 1.5))
812
+ continue;
813
+ const gutter = Math.round((left.left + left.width + right.left) / 20) * 10;
814
+ gutterVotes.set(gutter, (gutterVotes.get(gutter) ?? 0) + 1);
815
+ }
816
+ const repeatedGutters = [...gutterVotes].filter(([, votes]) => votes >= 3).map(([gutter]) => gutter);
817
+ const spans = remaining.filter((fragment) => fragment.width > page.width * 0.65 || fragment.fontSize > bodyFont * 1.08 && repeatedGutters.some((gutter) => fragment.left < gutter && fragment.left + fragment.width > gutter));
818
+ const boundaries = [...tables.map((table) => ({ top: table.top, bottom: table.bottom, table, span: null })), ...spans.map((span) => ({ top: span.top, bottom: span.top + span.height, table: null, span }))].sort((a, b) => a.top - b.top);
819
+ const emitted = new Set, emittedImages = new Set;
820
+ for (const boundary of boundaries) {
821
+ const band = remaining.filter((fragment) => !emitted.has(fragment) && fragment !== boundary.span && fragment.top < boundary.top);
822
+ const images = page.images.filter((image) => !emittedImages.has(image) && image.top < boundary.top);
823
+ emitBand(band, images);
824
+ for (const fragment of band)
825
+ emitted.add(fragment);
826
+ for (const image of images)
827
+ emittedImages.add(image);
828
+ if (boundary.table !== null)
829
+ blocks.push(boundary.table.block);
830
+ if (boundary.span !== null) {
831
+ const span = boundary.span;
832
+ blocks.push(span.fontSize > bodyFont * 1.08 ? { kind: "heading", page: page.page, level: 2, text: span.text } : { kind: "paragraph", page: page.page, text: span.text });
833
+ emitted.add(span);
834
+ }
835
+ }
836
+ emitBand(remaining.filter((fragment) => !emitted.has(fragment)), page.images.filter((image) => !emittedImages.has(image)));
837
+ }
838
+ return { blocks, issues };
839
+ }
676
840
 
677
841
  // src/pdf/tools.ts
678
842
  import { spawn } from "child_process";
@@ -696,7 +860,7 @@ async function readBoundedStream(stream, maxBytes) {
696
860
  else
697
861
  throw new Error("PDF tool returned an unsupported output chunk");
698
862
  if (!bytes.append(chunk))
699
- throw new Error(`PDF tool output exceeded ${maxBytes} bytes`);
863
+ throw new PdfCaptureError("limit-exceeded", `PDF tool output exceeded ${maxBytes} bytes`);
700
864
  }
701
865
  return new TextDecoder().decode(bytes.toUint8Array());
702
866
  }
@@ -704,7 +868,9 @@ var runPdfToolCommand = async (specification) => {
704
868
  const executable = specification.command[0];
705
869
  if (executable === undefined)
706
870
  throw new Error("PDF tool command is empty");
707
- const useProcessGroup = process.platform !== "win32";
871
+ if (specification.processGroup !== undefined && !["new", "inherit"].includes(specification.processGroup))
872
+ throw new PdfCaptureError("invalid-input", "Invalid PDF process group policy");
873
+ const useProcessGroup = process.platform !== "win32" && specification.processGroup !== "inherit";
708
874
  const child = spawn(executable, specification.command.slice(1), {
709
875
  cwd: specification.cwd,
710
876
  detached: useProcessGroup,
@@ -718,7 +884,7 @@ var runPdfToolCommand = async (specification) => {
718
884
  windowsHide: true
719
885
  });
720
886
  const exited = new Promise((resolveExit, rejectExit) => {
721
- child.once("error", rejectExit);
887
+ child.once("error", (error) => rejectExit(error.code === "ENOENT" ? new PdfCaptureError("tool-unavailable", "PDF tool executable is unavailable", { cause: error }) : error));
722
888
  child.once("close", (code) => resolveExit(code ?? 1));
723
889
  });
724
890
  const signalProcessTree = (signal) => {
@@ -742,7 +908,7 @@ var runPdfToolCommand = async (specification) => {
742
908
  forceKillTimer = setTimeout(() => signalProcessTree("SIGKILL"), 1000);
743
909
  };
744
910
  const timer = setTimeout(() => {
745
- requestStop(new Error(`PDF tool timed out after ${specification.timeoutMs}ms`));
911
+ requestStop(new PdfCaptureError("timeout", `PDF tool timed out after ${specification.timeoutMs}ms`));
746
912
  }, specification.timeoutMs);
747
913
  try {
748
914
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -791,10 +957,10 @@ function resolvePdfTools(dependencies = {}) {
791
957
  const pdftohtml = dependencies.tools?.pdftohtml ?? discoverExecutable("pdftohtml", dependencies);
792
958
  const tesseract = dependencies.tools?.tesseract === undefined ? discoverExecutable("tesseract", dependencies) : dependencies.tools.tesseract;
793
959
  if (pdfinfo === null) {
794
- throw new Error("pdfinfo is required for PDF ingestion; install the Poppler command-line tools");
960
+ throw new PdfCaptureError("tool-unavailable", "pdfinfo is required for PDF ingestion; install the Poppler command-line tools");
795
961
  }
796
962
  if (pdftohtml === null) {
797
- throw new Error("pdftohtml is required for PDF ingestion; install the Poppler command-line tools");
963
+ throw new PdfCaptureError("tool-unavailable", "pdftohtml is required for PDF ingestion; install the Poppler command-line tools");
798
964
  }
799
965
  return { pdfinfo, pdftohtml, tesseract };
800
966
  }
@@ -828,7 +994,7 @@ var pdfCaptureDefaults = {
828
994
  function positiveBound(value, fallback, maximum, label) {
829
995
  const selected = value ?? fallback;
830
996
  if (!Number.isSafeInteger(selected) || selected < 1 || selected > maximum) {
831
- throw new Error(`${label} must be an integer from 1 through ${maximum}`);
997
+ throw new PdfCaptureError("invalid-input", `${label} must be an integer from 1 through ${maximum}`);
832
998
  }
833
999
  return selected;
834
1000
  }
@@ -841,10 +1007,10 @@ function prepareWorkspace(path) {
841
1007
  if (existsSync2(absolute)) {
842
1008
  const stats = lstatSync(absolute);
843
1009
  if (!stats.isDirectory() || stats.isSymbolicLink()) {
844
- throw new Error("PDF inspection workspace must be a real directory");
1010
+ throw new PdfCaptureError("invalid-input", "PDF inspection workspace must be a real directory");
845
1011
  }
846
1012
  if (readdirSync(absolute).length !== 0) {
847
- throw new Error("PDF inspection workspace must be empty");
1013
+ throw new PdfCaptureError("invalid-input", "PDF inspection workspace must be empty");
848
1014
  }
849
1015
  } else {
850
1016
  mkdirSync(absolute, { recursive: false, mode: 448 });
@@ -857,17 +1023,17 @@ function sourceIdentity(path, maxPdfBytes) {
857
1023
  const canonical = realpathSync(resolve(path));
858
1024
  const stats = statSync(canonical);
859
1025
  if (!stats.isFile())
860
- throw new Error("PDF input must be a regular file");
1026
+ throw new PdfCaptureError("invalid-input", "PDF input must be a regular file");
861
1027
  if (stats.size < 5)
862
- throw new Error("PDF input is too small to contain a PDF header");
1028
+ throw new PdfCaptureError("invalid-input", "PDF input is too small to contain a PDF header");
863
1029
  if (stats.size > maxPdfBytes)
864
- throw new Error(`PDF input exceeds the ${maxPdfBytes}-byte limit`);
1030
+ throw new PdfCaptureError("limit-exceeded", `PDF input exceeds the ${maxPdfBytes}-byte limit`);
865
1031
  const descriptor = openSync(canonical, "r");
866
1032
  try {
867
1033
  const signature = Buffer.alloc(5);
868
1034
  const count = readSync(descriptor, signature, 0, signature.length, 0);
869
1035
  if (count !== signature.length || signature.toString("ascii") !== "%PDF-") {
870
- throw new Error("PDF input does not have a valid PDF signature");
1036
+ throw new PdfCaptureError("invalid-input", "PDF input does not have a valid PDF signature");
871
1037
  }
872
1038
  } finally {
873
1039
  closeSync(descriptor);
@@ -923,18 +1089,18 @@ function safeGeneratedImage(image, workspace, index, limits) {
923
1089
  function generatedTreeSize(workspace, maximumFiles, maximumBytes) {
924
1090
  const entries = readdirSync(workspace, { withFileTypes: true });
925
1091
  if (entries.length > maximumFiles) {
926
- throw new Error(`PDF extraction created more than ${maximumFiles} files`);
1092
+ throw new PdfCaptureError("limit-exceeded", `PDF extraction created more than ${maximumFiles} files`);
927
1093
  }
928
1094
  let totalBytes = 0;
929
1095
  for (const entry of entries) {
930
1096
  const path = join2(workspace, entry.name);
931
1097
  const stats = lstatSync(path);
932
1098
  if (!entry.isFile() || stats.isSymbolicLink()) {
933
- throw new Error("PDF extraction created an unexpected non-file output");
1099
+ throw new PdfCaptureError("extraction-failed", "PDF extraction created an unexpected non-file output");
934
1100
  }
935
1101
  totalBytes += stats.size;
936
1102
  if (totalBytes > maximumBytes) {
937
- throw new Error(`PDF extraction exceeded the ${maximumBytes}-byte workspace limit`);
1103
+ throw new PdfCaptureError("limit-exceeded", `PDF extraction exceeded the ${maximumBytes}-byte workspace limit`);
938
1104
  }
939
1105
  }
940
1106
  }
@@ -957,7 +1123,8 @@ async function inspectPdf(options, dependencies = {}) {
957
1123
  cwd: workspaceDirectory
958
1124
  });
959
1125
  if (info.exitCode !== 0) {
960
- throw new Error("pdfinfo could not inspect the input PDF");
1126
+ const encrypted = info.stderr.split(/\r?\n/u).some((line) => line.trim() === "Command Line Error: Incorrect password");
1127
+ throw new PdfCaptureError(encrypted ? "encrypted" : "extraction-failed", "pdfinfo could not inspect the input PDF");
961
1128
  }
962
1129
  const metadata = parsePdfInfo(info.stdout);
963
1130
  const processedPageLimit = Math.min(metadata.pageCount, maxPages);
@@ -982,12 +1149,12 @@ async function inspectPdf(options, dependencies = {}) {
982
1149
  cwd: workspaceDirectory
983
1150
  });
984
1151
  if (extracted.exitCode !== 0) {
985
- throw new Error("pdftohtml could not extract the input PDF");
1152
+ throw new PdfCaptureError("extraction-failed", "pdftohtml could not extract the input PDF");
986
1153
  }
987
1154
  generatedTreeSize(workspaceDirectory, 10002, Math.min(Number.MAX_SAFE_INTEGER, 8 * 1024 ** 3 + pdfCaptureDefaults.maxLayoutBytes));
988
1155
  const layoutStats = statSync(layoutPath);
989
1156
  if (!layoutStats.isFile() || layoutStats.size > pdfCaptureDefaults.maxLayoutBytes) {
990
- throw new Error(`Poppler layout XML exceeds the ${pdfCaptureDefaults.maxLayoutBytes}-byte limit`);
1157
+ throw new PdfCaptureError("limit-exceeded", `Poppler layout XML exceeds the ${pdfCaptureDefaults.maxLayoutBytes}-byte limit`);
991
1158
  }
992
1159
  const parsed = parsePopplerXml(readFileSync(layoutPath, "utf8"), {
993
1160
  maxPages: processedPageLimit,
@@ -1214,8 +1381,14 @@ function persistPdfCapture(input) {
1214
1381
  copyFileSync(image.sourcePath, destination);
1215
1382
  chmodSync2(destination, 420);
1216
1383
  }
1217
- writeFileSync(join3(stagingDirectory, `${slug}.md`), `${redactSensitiveText(input.markdown).trimEnd()}
1218
- `, { encoding: "utf8", flag: "wx", mode: 420 });
1384
+ const markdownBytes = `${redactSensitiveText(input.markdown).trimEnd()}
1385
+ `;
1386
+ const markdownDescriptor = { path: `${slug}.md`, bytes: Buffer.byteLength(markdownBytes), sha256: createHash2("sha256").update(markdownBytes).digest("hex") };
1387
+ if (input.manifest.markdown !== undefined && (input.manifest.markdown.path !== markdownDescriptor.path || input.manifest.markdown.bytes !== markdownDescriptor.bytes || input.manifest.markdown.sha256 !== markdownDescriptor.sha256)) {
1388
+ throw new Error("PDF Markdown does not match its manifest provenance");
1389
+ }
1390
+ const manifest = { ...input.manifest, markdown: markdownDescriptor };
1391
+ writeFileSync(join3(stagingDirectory, `${slug}.md`), markdownBytes, { encoding: "utf8", flag: "wx", mode: 420 });
1219
1392
  if (input.manifest.annotations === null) {
1220
1393
  if (input.annotationsJson !== undefined) {
1221
1394
  throw new Error("PDF annotations were supplied without manifest provenance");
@@ -1239,7 +1412,7 @@ function persistPdfCapture(input) {
1239
1412
  }
1240
1413
  writeFileSync(join3(stagingDirectory, PDF_CAPTURE_ANNOTATIONS_FILENAME), annotationsJson, { encoding: "utf8", flag: "wx", mode: 420 });
1241
1414
  }
1242
- writeFileSync(join3(stagingDirectory, PDF_CAPTURE_MANIFEST_FILENAME), `${JSON.stringify(input.manifest, null, 2)}
1415
+ writeFileSync(join3(stagingDirectory, PDF_CAPTURE_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}
1243
1416
  `, { encoding: "utf8", flag: "wx", mode: 420 });
1244
1417
  assertStagingTreeSafe(stagingDirectory);
1245
1418
  if (existsSync3(targetDirectory) !== (expectedIdentity !== null)) {
@@ -1297,7 +1470,7 @@ function pdfMarkdownFilename(slug) {
1297
1470
 
1298
1471
  // src/pdf/markdown.ts
1299
1472
  function escapeInline(value) {
1300
- return sanitizeTerminalText(value).replace(/\\/gu, "\\\\").replace(/([`*_[\]{}<>#])/gu, "\\$1").replace(/\s+/gu, " ").trim();
1473
+ return sanitizeTerminalText(value).replace(/\\/gu, "\\\\").replace(/([`*_[\]{}<>#|])/gu, "\\$1").replace(/\s+/gu, " ").trim();
1301
1474
  }
1302
1475
  function metadataLine(metadata) {
1303
1476
  if (metadata === null)
@@ -1347,6 +1520,13 @@ function markdownContent(blocks, images) {
1347
1520
  lines.push(imageMarkdown(resolved), "");
1348
1521
  continue;
1349
1522
  }
1523
+ if (block.kind === "table") {
1524
+ textBlockCount += 1;
1525
+ const rows = block.rows.map((row) => `| ${row.map((cell) => escapeInline(cell)).join(" | ")} |`);
1526
+ if (rows.length > 0)
1527
+ lines.push(rows[0], `| ${block.rows[0].map(() => "---").join(" | ")} |`, ...rows.slice(1), "");
1528
+ continue;
1529
+ }
1350
1530
  if (block.kind === "heading") {
1351
1531
  headingCount += 1;
1352
1532
  lines.push(`${"#".repeat(Math.max(2, Math.min(6, block.level)))} ${escapeInline(block.text)}`, "");
@@ -1662,13 +1842,16 @@ async function runPdfCapture(options, dependencies = {}) {
1662
1842
  });
1663
1843
  }
1664
1844
  const platforms = embeddedPlatforms(resolvedImages);
1845
+ const layout = analyzePdfLayout(inspection.pages);
1846
+ for (const issue of layout.issues)
1847
+ warnings.push(`Page ${issue.page}: ${issue.code}; consult the retained source PDF.`);
1665
1848
  const status = warnings.length > 0 || unclassifiedImages > 0 || ocrWarningCount > 0 ? "partial" : "complete";
1666
1849
  const filenameTitle = inspection.originalFilename.replace(/\.pdf$/iu, "").trim();
1667
1850
  const requestedSlug = options.slug ?? inspection.metadata.title ?? filenameTitle;
1668
1851
  const slug = slugify(sanitizeTerminalText(requestedSlug));
1669
1852
  if (slug === "")
1670
1853
  throw new Error("could not derive a safe PDF capture slug; pass an explicit slug");
1671
- const blocks = layoutBlocks(inspection.pages);
1854
+ const blocks = layout.blocks;
1672
1855
  const byId = new Map(resolvedImages.map((image) => [image.image.id, image]));
1673
1856
  const capturedAt = (dependencies.now ?? (() => new Date))().toISOString();
1674
1857
  const built = buildPdfMarkdown({
@@ -1701,6 +1884,8 @@ async function runPdfCapture(options, dependencies = {}) {
1701
1884
  const manifest = {
1702
1885
  schemaVersion: PDF_CAPTURE_MANIFEST_SCHEMA_VERSION,
1703
1886
  kind: "pdf",
1887
+ quality: { schemaVersion: 1, issues: layout.issues },
1888
+ markdown: { path: pdfMarkdownFilename(slug), bytes: Buffer.byteLength(built.markdown), sha256: createHash3("sha256").update(built.markdown).digest("hex") },
1704
1889
  capturedAt,
1705
1890
  status,
1706
1891
  source: {
@@ -1785,13 +1970,13 @@ function parseRemoteUrl(input) {
1785
1970
  try {
1786
1971
  url = new URL(input);
1787
1972
  } catch {
1788
- throw new Error("PDF URL is invalid");
1973
+ throw new PdfCaptureError("invalid-input", "PDF URL is invalid");
1789
1974
  }
1790
1975
  if (url.protocol !== "http:" && url.protocol !== "https:") {
1791
- throw new Error("PDF URL must use HTTP or HTTPS");
1976
+ throw new PdfCaptureError("invalid-input", "PDF URL must use HTTP or HTTPS");
1792
1977
  }
1793
1978
  if (url.username !== "" || url.password !== "") {
1794
- throw new Error("PDF URL must not contain embedded credentials");
1979
+ throw new PdfCaptureError("invalid-input", "PDF URL must not contain embedded credentials");
1795
1980
  }
1796
1981
  return url;
1797
1982
  }
@@ -1808,7 +1993,7 @@ function remoteFilename(url) {
1808
1993
  }
1809
1994
  function assertPdfSignature(bytes) {
1810
1995
  if (bytes.byteLength < 5 || new TextDecoder().decode(bytes.subarray(0, 5)) !== "%PDF-") {
1811
- throw new Error("remote PDF input does not have a valid PDF signature");
1996
+ throw new PdfCaptureError("invalid-input", "remote PDF input does not have a valid PDF signature");
1812
1997
  }
1813
1998
  }
1814
1999
  async function preparePdfSource(input, options = {}, dependencies = {}) {
@@ -1961,7 +2146,7 @@ async function main(rawArguments = process.argv.slice(2), environment = process.
1961
2146
  } catch (error) {
1962
2147
  const message = safe(error instanceof Error ? error.message : String(error));
1963
2148
  if (arguments_.json)
1964
- output.stdout(terminalSafeJson({ ok: false, error: message }));
2149
+ output.stdout(terminalSafeJson({ ok: false, error: message, code: error instanceof PdfCaptureError ? error.code : "unknown" }));
1965
2150
  else
1966
2151
  output.stderr(renderFailure(message, "wordcell doctor", stderrStyle(environment, output)));
1967
2152
  return 1;
@@ -1972,4 +2157,4 @@ async function main(rawArguments = process.argv.slice(2), environment = process.
1972
2157
  if (false)
1973
2158
  ;
1974
2159
 
1975
- export { pdfUsage, parsePdfArguments, decodePopplerText, parsePopplerXml, parsePdfInfo, layoutBlocks, parsePdfImageInterpretations, runPdfToolCommand, resolvePdfTools, pdfCaptureDefaults, inspectPdf, PDF_CAPTURE_MANIFEST_SCHEMA_VERSION, PDF_CAPTURE_MANIFEST_FILENAME, PDF_CAPTURE_SOURCE_FILENAME, PDF_CAPTURE_ANNOTATIONS_FILENAME, pdfImageAssetPath, persistPdfCapture, pdfMarkdownFilename, buildPdfMarkdown, parseTesseractTsv, ocrPdfImage, runPdfCapture, pdfCaptureSummary, main };
2160
+ export { PdfCaptureError, pdfUsage, parsePdfArguments, decodePopplerText, parsePopplerXml, parsePdfInfo, layoutBlocks, parsePdfImageInterpretations, runPdfToolCommand, resolvePdfTools, pdfCaptureDefaults, inspectPdf, PDF_CAPTURE_MANIFEST_SCHEMA_VERSION, PDF_CAPTURE_MANIFEST_FILENAME, PDF_CAPTURE_SOURCE_FILENAME, PDF_CAPTURE_ANNOTATIONS_FILENAME, pdfImageAssetPath, persistPdfCapture, pdfMarkdownFilename, buildPdfMarkdown, parseTesseractTsv, ocrPdfImage, runPdfCapture, pdfCaptureSummary, main };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  classifyPlatformUrl,
4
4
  symbol
5
- } from "./index-cd75vky9.js";
5
+ } from "./index-jbtyp6q5.js";
6
6
  import {
7
7
  filterCookieProviderResult,
8
8
  readCookieFile,
@@ -10,7 +10,7 @@ import {
10
10
  } from "./index-2gv8y733.js";
11
11
  import {
12
12
  sanitizeArtifactUrl
13
- } from "./index-mxxxytys.js";
13
+ } from "./index-g63d3937.js";
14
14
  import {
15
15
  startNetworkProxy
16
16
  } from "./index-w2zc0vwa.js";
@@ -105,10 +105,10 @@ function hasExactKeys(value, expected) {
105
105
  function tokenCount(value) {
106
106
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 1e9 ? value : null;
107
107
  }
108
- function acceptedResponseModel(value) {
109
- return value === DEFAULT_SYSTEMONE_MODEL;
108
+ function acceptedResponseModel(value, model) {
109
+ return value === model;
110
110
  }
111
- function parseSystemOneResponse(value) {
111
+ function parseSystemOneResponse(value, model) {
112
112
  const root = recordValue(value);
113
113
  if (root === null || !hasExactKeys(root, ["model", "answers", "usage"]))
114
114
  return null;
@@ -126,7 +126,7 @@ function parseSystemOneResponse(value) {
126
126
  return null;
127
127
  const inputTokens = tokenCount(usage["input_tokens"]);
128
128
  const outputTokens = tokenCount(usage["output_tokens"]);
129
- if (!acceptedResponseModel(root["model"]) || inputTokens === null || outputTokens === null)
129
+ if (!acceptedResponseModel(root["model"], model) || inputTokens === null || outputTokens === null)
130
130
  return null;
131
131
  return { noul, model: root["model"], inputTokens, outputTokens };
132
132
  }
@@ -144,7 +144,7 @@ async function mapWithConcurrency(items, concurrency, operation) {
144
144
  return results;
145
145
  }
146
146
  function validApiKey(value) {
147
- return value !== undefined && /^[\x21-\x7e]{1,512}$/u.test(value);
147
+ return typeof value === "string" && /^[\x21-\x7e]{1,512}$/u.test(value);
148
148
  }
149
149
  function boundedPositiveInteger(value, fallback, maximum) {
150
150
  const resolved = value ?? fallback;
@@ -164,14 +164,25 @@ function validRerankRequest(value) {
164
164
  return true;
165
165
  }
166
166
  function createTypeSafeReranker(options = {}) {
167
+ const environment = options.environment ?? process.env;
168
+ return createHostedReranker(options, {
169
+ id: "typesafe",
170
+ label: "TypeSafe",
171
+ model: DEFAULT_SYSTEMONE_MODEL,
172
+ endpoint: DEFAULT_SYSTEMONE_ENDPOINT,
173
+ apiKey: Object.hasOwn(environment, "TYPESAFE_API_KEY") ? environment["TYPESAFE_API_KEY"] : undefined,
174
+ unavailableMessage: "TYPESAFE_API_KEY is missing or invalid; the TypeSafe rerank lane is unavailable.",
175
+ unwrap: (value) => value
176
+ });
177
+ }
178
+ function createHostedReranker(options, provider) {
167
179
  const transport = options.transport ?? fetchSystemOneTransport;
168
180
  const timeoutMs = boundedPositiveInteger(options.timeoutMs, DEFAULT_RERANK_TIMEOUT_MS, MAX_RERANK_TIMEOUT_MS);
169
181
  const maxResponseBytes = boundedPositiveInteger(options.maxResponseBytes, DEFAULT_RERANK_MAX_RESPONSE_BYTES, MAX_RERANK_RESPONSE_BYTES);
170
182
  const concurrency = boundedPositiveInteger(options.concurrency, DEFAULT_RERANK_CONCURRENCY, MAX_RERANK_CONCURRENCY);
171
- const environment = options.environment ?? process.env;
172
- const apiKey = Object.hasOwn(environment, "TYPESAFE_API_KEY") ? environment["TYPESAFE_API_KEY"] : undefined;
183
+ const apiKey = provider.apiKey;
173
184
  return {
174
- id: "typesafe",
185
+ id: provider.id,
175
186
  rerank: async (request) => {
176
187
  const started = performance.now();
177
188
  const controller = new AbortController;
@@ -209,7 +220,7 @@ function createTypeSafeReranker(options = {}) {
209
220
  };
210
221
  const failed = (message) => ({
211
222
  status: "failed",
212
- message,
223
+ message: message.replaceAll("TypeSafe", provider.label),
213
224
  ...details()
214
225
  });
215
226
  try {
@@ -224,8 +235,8 @@ function createTypeSafeReranker(options = {}) {
224
235
  if (callerSignal?.aborted === true) {
225
236
  return failed("TypeSafe rerank request was aborted.");
226
237
  }
227
- if (!validApiKey(apiKey)) {
228
- return { status: "unavailable", message: "TYPESAFE_API_KEY is missing or invalid; the TypeSafe rerank lane is unavailable." };
238
+ if (!validApiKey(apiKey) || provider.endpoint === undefined) {
239
+ return { status: "unavailable", message: provider.unavailableMessage };
229
240
  }
230
241
  if (candidateCount === 0)
231
242
  return { status: "ready", ordering: [], probabilities: {} };
@@ -242,15 +253,18 @@ function createTypeSafeReranker(options = {}) {
242
253
  if (encoder.encode(JSON.stringify(state)).byteLength > MAX_RERANK_STATE_BYTES) {
243
254
  return failed(`TypeSafe rerank state exceeds the ${MAX_RERANK_STATE_BYTES}-byte limit.`);
244
255
  }
245
- bodies.push(encoder.encode(JSON.stringify({
246
- model: DEFAULT_SYSTEMONE_MODEL,
256
+ const body = encoder.encode(JSON.stringify({
257
+ model: provider.model,
247
258
  state,
248
259
  questions: { [RELEVANCE_QUESTION]: {
249
260
  type: "noul",
250
261
  instructions: RELEVANCE_INSTRUCTIONS,
251
262
  criteria: RELEVANCE_CRITERIA
252
263
  } }
253
- })));
264
+ }));
265
+ if (body.byteLength > 32 * 1024)
266
+ return failed("TypeSafe rerank body exceeds the 32768-byte limit.");
267
+ bodies.push(body);
254
268
  }
255
269
  const deadline = started + timeoutMs;
256
270
  const timeoutMessage = "TypeSafe rerank window exceeded its deadline.";
@@ -271,7 +285,7 @@ function createTypeSafeReranker(options = {}) {
271
285
  let response;
272
286
  try {
273
287
  response = await transport({
274
- url: DEFAULT_SYSTEMONE_ENDPOINT,
288
+ url: provider.endpoint,
275
289
  body,
276
290
  headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
277
291
  timeoutMs: Math.max(1, Math.ceil(deadline - performance.now())),
@@ -302,7 +316,8 @@ function createTypeSafeReranker(options = {}) {
302
316
  stop(response.status === 200 ? "TypeSafe rerank response was not valid UTF-8 JSON." : `TypeSafe rerank request returned HTTP ${response.status}.`);
303
317
  return { ok: false, message: firstFailure };
304
318
  }
305
- const root = recordValue(parsed);
319
+ const result = provider.unwrap(parsed);
320
+ const root = recordValue(result);
306
321
  const usage = recordValue(root?.["usage"]);
307
322
  const input = tokenCount(usage?.["input_tokens"]);
308
323
  const output = tokenCount(usage?.["output_tokens"]);
@@ -315,13 +330,13 @@ function createTypeSafeReranker(options = {}) {
315
330
  outputTokens += output;
316
331
  usageReceipts += 1;
317
332
  }
318
- if (acceptedResponseModel(root?.["model"]))
319
- observedModel = DEFAULT_SYSTEMONE_MODEL;
333
+ if (acceptedResponseModel(root?.["model"], provider.model))
334
+ observedModel = provider.model;
320
335
  if (response.status !== 200) {
321
336
  stop(`TypeSafe rerank request returned HTTP ${response.status}.`);
322
337
  return { ok: false, message: firstFailure };
323
338
  }
324
- const answer = parseSystemOneResponse(parsed);
339
+ const answer = parseSystemOneResponse(result, provider.model);
325
340
  if (answer === null) {
326
341
  stop("TypeSafe rerank response was malformed.");
327
342
  return { ok: false, message: firstFailure };
@@ -365,4 +380,4 @@ function createTypeSafeReranker(options = {}) {
365
380
  };
366
381
  }
367
382
 
368
- export { DEFAULT_SYSTEMONE_ENDPOINT, DEFAULT_SYSTEMONE_MODEL, createTypeSafeReranker };
383
+ export { DEFAULT_SYSTEMONE_ENDPOINT, DEFAULT_SYSTEMONE_MODEL, createTypeSafeReranker, createHostedReranker };