@semiont/content 0.5.27 → 0.5.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -187,13 +187,6 @@ declare function verifyChecksum(content: string | Buffer, checksum: string): boo
187
187
  * "no entry under the current checksum" as work (P0's third drift class).
188
188
  */
189
189
 
190
- /** The two halves of the wire record, split for storage. */
191
- type SuccessOutcome = Exclude<ExtractionOutcome, {
192
- declined: string;
193
- }>;
194
- type DeclineOutcome = Extract<ExtractionOutcome, {
195
- declined: string;
196
- }>;
197
190
  /**
198
191
  * One line of recognized text: the geometry every word on it shares, plus the
199
192
  * per-word parts that differ.
@@ -244,10 +237,14 @@ type CachedAnchoredText = ({
244
237
  stamp: string;
245
238
  text: string;
246
239
  lines: CachedLine[];
247
- } & Omit<SuccessOutcome, 'text' | 'items'>) | ({
240
+ } & Omit<Extract<ExtractionOutcome, {
241
+ kind: 'extracted';
242
+ }>, 'kind' | 'text' | 'items'>) | ({
248
243
  v: 2;
249
244
  stamp: string;
250
- } & DeclineOutcome);
245
+ } & Omit<Extract<ExtractionOutcome, {
246
+ kind: 'declined';
247
+ }>, 'kind'>);
251
248
  interface AnchoredTextStore {
252
249
  /**
253
250
  * The stored map for this key, or null for any miss. Never throws.
@@ -307,6 +304,8 @@ declare function createAnchoredTextStore(dir: string, logger?: Logger): Anchored
307
304
  */
308
305
 
309
306
  interface ExtractedText {
307
+ /** Discriminant — mirrors the wire member (WIRE-UNION-DISCRIMINANTS P5c/D6). */
308
+ kind: 'extracted';
310
309
  /** Reading-order plain text, ready for the chunker. */
311
310
  text: string;
312
311
  /**
@@ -351,6 +350,8 @@ interface ExtractedText {
351
350
  * could not name its class; SMELTER-MEDIA-TYPES Phase 0 log, note a).
352
351
  */
353
352
  interface ExtractionDecline {
353
+ /** Discriminant — mirrors the wire member (WIRE-UNION-DISCRIMINANTS P5c/D6). */
354
+ kind: 'declined';
354
355
  declined: 'no-text-layer' | 'encrypted' | 'corrupt' | 'too-large';
355
356
  }
356
357
  /**
package/dist/index.js CHANGED
@@ -725,7 +725,7 @@ function foldFormFields(layer) {
725
725
  height: field.height
726
726
  });
727
727
  }
728
- return { text, items, method: "form", pdfClass: "E" };
728
+ return { kind: "extracted", text, items, method: "form", pdfClass: "E" };
729
729
  }
730
730
  function shapeTables(layer) {
731
731
  const pages = layer.pages.map((page) => {
@@ -748,7 +748,7 @@ function shapeTables(layer) {
748
748
  }
749
749
  }
750
750
  }
751
- return { text, items, method: "table", pdfClass: "D" };
751
+ return { kind: "extracted", text, items, method: "table", pdfClass: "D" };
752
752
  }
753
753
  var pdfExtractor = {
754
754
  // Every non-declined PDF extraction carries positioned runs — native text
@@ -759,25 +759,26 @@ var pdfExtractor = {
759
759
  if (hit) return hit;
760
760
  const outcome = await extractPdf(content);
761
761
  if (cache) {
762
- if ("declined" in outcome) await cache.store.write(cache.key, outcome);
762
+ if (outcome.kind === "declined") await cache.store.write(cache.key, outcome);
763
763
  else if (outcome.items) await cache.store.write(cache.key, { ...outcome, items: outcome.items });
764
764
  }
765
765
  return outcome;
766
766
  }
767
767
  };
768
768
  async function extractPdf(content) {
769
- if (!withinByteBudget(content.length)) return { declined: "too-large" };
769
+ if (!withinByteBudget(content.length)) return { kind: "declined", declined: "too-large" };
770
770
  let layer;
771
771
  try {
772
772
  layer = await extractPdfTextLayer(content);
773
773
  } catch (error) {
774
- return { declined: classifyPdfError(error) };
774
+ return { kind: "declined", declined: classifyPdfError(error) };
775
775
  }
776
776
  if (!layer) {
777
777
  const ocr2 = await ocrPages(content);
778
- if (!ocr2.text) return { declined: "no-text-layer" };
778
+ if (!ocr2.text) return { kind: "declined", declined: "no-text-layer" };
779
779
  const confidence2 = summarize(ocr2.confidences);
780
780
  return {
781
+ kind: "extracted",
781
782
  text: ocr2.text,
782
783
  items: ocr2.items,
783
784
  method: "ocr",
@@ -785,7 +786,7 @@ async function extractPdf(content) {
785
786
  ...confidence2 ? { ocrConfidence: confidence2 } : {}
786
787
  };
787
788
  }
788
- const shaped = layer.fields.length > 0 ? foldFormFields(layer) : shapeTables(layer) ?? { text: layer.text, items: layer.items, method: "pdf-text-layer", pdfClass: "A" };
789
+ const shaped = layer.fields.length > 0 ? foldFormFields(layer) : shapeTables(layer) ?? { kind: "extracted", text: layer.text, items: layer.items, method: "pdf-text-layer", pdfClass: "A" };
789
790
  const unreadPages = layer.pages.filter((page) => !page.hasTextLayer).map((page) => page.pageNumber);
790
791
  if (unreadPages.length === 0) return shaped;
791
792
  const recovered = await ocrPages(content, unreadPages);
@@ -818,7 +819,7 @@ async function extractPdf(content) {
818
819
  var passthroughExtractor = {
819
820
  yieldsGeometry: false,
820
821
  async extract(content, mediaType) {
821
- return { text: decodeRepresentation(content, mediaType), method: "text-passthrough" };
822
+ return { kind: "extracted", text: decodeRepresentation(content, mediaType), method: "text-passthrough" };
822
823
  }
823
824
  };
824
825
  var EXTRACTORS = {
@@ -896,9 +897,9 @@ function createAnchoredTextStore(dir, logger) {
896
897
  ...hit ? "declined" in hit ? { declined: hit.declined } : { lines: hit.lines.length } : {}
897
898
  });
898
899
  if (!hit) return null;
899
- if ("declined" in hit) return { declined: hit.declined };
900
+ if ("declined" in hit) return { kind: "declined", declined: hit.declined };
900
901
  const { v: _v, stamp: _stamp, lines, text, ...provenance } = hit;
901
- return { text, items: decodeLines(lines), ...provenance };
902
+ return { kind: "extracted", text, items: decodeLines(lines), ...provenance };
902
903
  },
903
904
  async write(key, outcome) {
904
905
  const target = fileFor(key);
@@ -906,8 +907,8 @@ function createAnchoredTextStore(dir, logger) {
906
907
  logger?.debug("Anchored-text cache: refusing invalid key", { key });
907
908
  return;
908
909
  }
909
- const entry = "declined" in outcome ? { v: 2, stamp: STAMP, declined: outcome.declined } : (() => {
910
- const { text, items, ...provenance } = outcome;
910
+ const entry = outcome.kind === "declined" ? { v: 2, stamp: STAMP, declined: outcome.declined } : (() => {
911
+ const { kind: _kind, text, items, ...provenance } = outcome;
911
912
  return { v: 2, stamp: STAMP, text, lines: encodeLines(items), ...provenance };
912
913
  })();
913
914
  const temp = `${target}.${process.pid}.tmp`;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/working-tree-store.ts","../src/checksum.ts","../src/storage-uri.ts","../src/content-extractor.ts","../src/pdf-extractor.ts","../src/extract-pdf-text-layer.ts","../src/pdfjs-assets.ts","../src/pdf-tables.ts","../src/pdf-page-images.ts","../src/png-encode.ts","../src/ocr.ts","../src/ocr-geometry.ts","../src/anchored-text-store.ts","../src/anchored-text-store-adapter.ts"],"sourcesContent":["/**\n * WorkingTreeStore - Manages files in the project working tree\n *\n * Unlike the old content-addressed RepresentationStore, this store treats\n * the working tree (project root) as the source of truth for file content.\n * Resources are identified by their file:// URI, which is stable across\n * content changes and moves (tracked by events).\n *\n * Two write paths:\n * - store(content, storageUri): Write bytes to disk (API/GUI/AI path).\n * Used when the file does not yet exist and the caller provides content.\n * - register(storageUri, expectedChecksum?): Read an existing file and\n * return its metadata (CLI path). The file is already on disk; we just\n * verify and record it. If expectedChecksum is provided, throws on mismatch.\n *\n * Storage layout:\n * {projectRoot}/{path-from-uri}\n *\n * For example, storageUri \"file://docs/overview.md\" resolves to\n * {projectRoot}/docs/overview.md\n */\n\nimport { promises as fs } from 'fs';\nimport { execFileSync } from 'child_process';\nimport path from 'path';\nimport type { SemiontProject } from '@semiont/core/node';\nimport type { Logger } from '@semiont/core';\nimport { calculateChecksum, verifyChecksum } from './checksum';\n\n/**\n * Result of store() or register()\n */\nexport interface StoredResource {\n storageUri: string; // file:// URI (e.g. \"file://docs/overview.md\")\n checksum: string; // SHA-256 hex of content\n byteSize: number; // Size in bytes\n created: string; // ISO 8601 timestamp\n}\n\n/**\n * Manages files in the project working tree\n */\nexport class WorkingTreeStore {\n private projectRoot: string;\n private gitSync: boolean;\n private logger?: Logger;\n\n constructor(project: SemiontProject, logger?: Logger) {\n this.projectRoot = project.root;\n this.gitSync = project.gitSync;\n this.logger = logger;\n }\n\n private shouldRunGit(noGit?: boolean): boolean {\n return this.gitSync && !noGit;\n }\n\n /**\n * Write content to disk at the location indicated by storageUri.\n *\n * API/GUI/AI path: caller provides bytes; file may not yet exist.\n *\n * @param content - Raw bytes to write\n * @param storageUri - file:// URI (e.g. \"file://docs/overview.md\")\n * @returns Stored resource metadata\n */\n async store(content: Buffer, storageUri: string, options?: { noGit?: boolean }): Promise<StoredResource> {\n const filePath = this.resolveUri(storageUri);\n const checksum = calculateChecksum(content);\n\n this.logger?.debug('Storing resource', { storageUri, byteSize: content.length });\n\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n await fs.writeFile(filePath, content);\n\n if (this.shouldRunGit(options?.noGit)) {\n execFileSync('git', ['add', filePath], { cwd: this.projectRoot });\n }\n\n this.logger?.info('Resource stored', { storageUri, checksum, byteSize: content.length });\n\n return {\n storageUri,\n checksum,\n byteSize: content.length,\n created: new Date().toISOString(),\n };\n }\n\n /**\n * Read an existing file and return its metadata.\n *\n * CLI path: the file is already on disk. We read it to compute the checksum.\n * If expectedChecksum is provided, throws ChecksumMismatchError on mismatch.\n *\n * @param storageUri - file:// URI (e.g. \"file://docs/overview.md\")\n * @param expectedChecksum - Optional SHA-256 to verify against\n * @returns Stored resource metadata\n * @throws ChecksumMismatchError if expectedChecksum is provided and does not match\n * @throws Error if file does not exist\n */\n async register(storageUri: string, expectedChecksum?: string, options?: { noGit?: boolean }): Promise<StoredResource> {\n const filePath = this.resolveUri(storageUri);\n\n this.logger?.debug('Registering resource', { storageUri });\n\n const content = await fs.readFile(filePath);\n const checksum = calculateChecksum(content);\n\n if (expectedChecksum !== undefined && !verifyChecksum(content, expectedChecksum)) {\n throw new ChecksumMismatchError(storageUri, expectedChecksum, checksum);\n }\n\n if (this.shouldRunGit(options?.noGit)) {\n execFileSync('git', ['add', filePath], { cwd: this.projectRoot });\n }\n\n this.logger?.info('Resource registered', { storageUri, checksum, byteSize: content.length });\n\n return {\n storageUri,\n checksum,\n byteSize: content.length,\n created: new Date().toISOString(),\n };\n }\n\n /**\n * Read file content by URI.\n *\n * @param storageUri - file:// URI\n * @returns Raw bytes\n */\n async retrieve(storageUri: string): Promise<Buffer> {\n const filePath = this.resolveUri(storageUri);\n try {\n return await fs.readFile(filePath);\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n throw new Error(`Resource not found: ${storageUri}`);\n }\n throw error;\n }\n }\n\n /**\n * Move a file from one URI to another.\n *\n * If .git/ exists in the project root and noGit is not set, runs `git mv`.\n * Otherwise (no .git/ or noGit: true), runs fs.rename.\n *\n * @param fromUri - Current file:// URI\n * @param toUri - New file:// URI\n * @param options.noGit - Skip git mv even if .git/ is present\n */\n async move(fromUri: string, toUri: string, options?: { noGit?: boolean }): Promise<void> {\n const fromPath = this.resolveUri(fromUri);\n const toPath = this.resolveUri(toUri);\n\n this.logger?.debug('Moving resource', { fromUri, toUri });\n\n await fs.mkdir(path.dirname(toPath), { recursive: true });\n\n if (this.shouldRunGit(options?.noGit)) {\n // git mv handles both the filesystem rename and the index update\n execFileSync('git', ['mv', fromPath, toPath], { cwd: this.projectRoot });\n } else {\n await fs.rename(fromPath, toPath);\n }\n\n this.logger?.info('Resource moved', { fromUri, toUri });\n }\n\n /**\n * Remove a file from the working tree.\n *\n * If .git/ exists and noGit is not set:\n * - keepFile false (default): runs `git rm` (removes from index and disk)\n * - keepFile true: runs `git rm --cached` (removes from index only, file stays on disk)\n * If no .git/ or noGit: true:\n * - keepFile false: runs fs.unlink\n * - keepFile true: no-op on filesystem\n *\n * @param storageUri - file:// URI\n * @param options.noGit - Skip git rm even if .git/ is present\n * @param options.keepFile - Remove from git index only; leave file on disk\n */\n async remove(storageUri: string, options?: { noGit?: boolean; keepFile?: boolean }): Promise<void> {\n const filePath = this.resolveUri(storageUri);\n const keepFile = options?.keepFile ?? false;\n\n this.logger?.debug('Removing resource', { storageUri, keepFile });\n\n const useGit = this.shouldRunGit(options?.noGit);\n\n if (useGit) {\n const gitArgs = keepFile\n ? ['rm', '--cached', filePath]\n : ['rm', filePath];\n execFileSync('git', gitArgs, { cwd: this.projectRoot });\n this.logger?.info('Resource removed', { storageUri, keepFile, git: true });\n return;\n }\n\n if (keepFile) {\n this.logger?.info('Resource removed from index (file kept on disk)', { storageUri });\n return;\n }\n\n try {\n await fs.unlink(filePath);\n this.logger?.info('Resource removed', { storageUri });\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n this.logger?.warn('Resource file already absent', { storageUri });\n return;\n }\n throw error;\n }\n }\n\n /**\n * Convert a file:// URI to an absolute filesystem path.\n *\n * \"file://docs/overview.md\" → \"{projectRoot}/docs/overview.md\"\n *\n * @param storageUri - file:// URI\n * @returns Absolute path\n */\n resolveUri(storageUri: string): string {\n if (!storageUri.startsWith('file://')) {\n throw new Error(`Invalid storage URI (must start with file://): ${storageUri}`);\n }\n const relativePath = storageUri.slice('file://'.length);\n return path.join(this.projectRoot, relativePath);\n }\n}\n\n/**\n * Thrown when a registered file's checksum does not match the expected value.\n * This indicates the file on disk differs from what was recorded (e.g. modified\n * after staging, or wrong file path provided).\n */\nexport class ChecksumMismatchError extends Error {\n constructor(\n readonly storageUri: string,\n readonly expected: string,\n readonly actual: string,\n ) {\n super(\n `Checksum mismatch for ${storageUri}: expected ${expected.slice(0, 8)}... but got ${actual.slice(0, 8)}...\\n` +\n `The file on disk differs from the recorded checksum. Has it been modified since staging?`\n );\n this.name = 'ChecksumMismatchError';\n }\n}\n","/**\n * Checksum utilities for content verification\n */\n\nimport { createHash } from 'crypto';\n\n/**\n * Calculate SHA-256 checksum of content\n * @param content The content to hash\n * @returns Hex-encoded SHA-256 hash\n */\nexport function calculateChecksum(content: string | Buffer): string {\n const hash = createHash('sha256');\n hash.update(content);\n return hash.digest('hex');\n}\n\n/**\n * Verify content against a checksum\n * @param content The content to verify\n * @param checksum The expected checksum\n * @returns True if content matches checksum\n */\nexport function verifyChecksum(content: string | Buffer, checksum: string): boolean {\n return calculateChecksum(content) === checksum;\n}\n","/**\n * Storage URI Derivation\n *\n * Builds the file:// URI a resource lives at in the working tree from its\n * name and validated media type. Extensions come from the media-type\n * registry in @semiont/core; formats are validated upstream at the\n * create/yield boundary, so the lookup is strict — no fallback.\n */\n\nimport { MEDIA_TYPES, type SupportedMediaType } from '@semiont/core';\n\n/**\n * Derive a file:// storage URI from a resource name and media type.\n *\n * The name is lowercased, runs of non-alphanumeric characters collapse to\n * single hyphens, and leading/trailing hyphens are stripped.\n *\n * @example\n * deriveStorageUri(\"My Document\", \"text/markdown\") // => \"file://my-document.md\"\n */\nexport function deriveStorageUri(name: string, format: SupportedMediaType): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n return `file://${slug}${MEDIA_TYPES[format].extension}`;\n}\n","/**\n * ContentExtractor — strategy-keyed text extraction for embedding.\n *\n * The registry is keyed by `TextExtraction` from `@semiont/core` — the\n * media-type registry's dispatch vocabulary — never by a second media-type\n * list (SMELTER-MEDIA-TYPES.md, Design §1): there is exactly one media-type\n * table in the system, and this registry consumes it. The Smelter resolves\n * `textExtractionOf(contentType)` and looks the extractor up by strategy; a\n * `null` slot means decline (settle skipped, reason 'no-extractor').\n *\n * Extraction is ephemeral: `extract` runs at read time, its output feeds the\n * chunker, and is discarded — no stored derived representation. Annotations\n * anchor to native geometry (`items`), never to extracted-text offsets, so\n * re-extraction can never break an anchor.\n */\n\nimport { decodeRepresentation, type TextExtraction, type PdfTextItem } from '@semiont/core';\nimport type { AnchoredTextStore } from './anchored-text-store';\nimport { pdfExtractor } from './pdf-extractor';\n\nexport interface ExtractedText {\n /** Reading-order plain text, ready for the chunker. */\n text: string;\n /**\n * Positioned text runs indexing `text`, for callers that anchor; absent for\n * pure text, where character offsets are the anchor. Named `items` to match\n * `AnchoredText`/`PdfTextLayer` — one concept, one name, and no collision\n * with the OCR engine's own \"blocks\" (which are page regions, not runs).\n */\n items?: PdfTextItem[];\n method: 'text-passthrough' | 'pdf-text-layer' | 'table' | 'form' | 'ocr';\n pdfClass?: 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G';\n /**\n * How well the engine read the pixels, when any of this text came from OCR.\n *\n * Extraction quality, deliberately NOT anchor confidence: the two answer\n * different questions. `AnchorConfidence` asks whether the renderer\n * relocated a stored span in the current text, and for a PDF the answer is\n * always \"exactly\" — the viewrect is absolute. This asks whether the glyphs\n * under that box were read correctly, which no client can recompute.\n * Reported for operators rather than stored on annotations, following the\n * existing rule that anchor-audit detail belongs in logs.\n */\n ocrConfidence?: {\n /** Mean per-word confidence, 0–100. */\n mean: number;\n /** Words the engine was unsure of — the number worth acting on. */\n lowConfidenceWords: number;\n totalWords: number;\n };\n /**\n * 1-indexed pages this extraction could not read — present only when a\n * document is partially covered (class C). Naming the gap is the point:\n * without it a hybrid document embeds its native pages and says nothing\n * about the rest, so coverage silently overstates what search can see.\n * This is the work list OCR consumes.\n */\n unreadPages?: number[];\n}\n\n/**\n * A named decline — an extractor that ran and decided it cannot yield text\n * says why, so the settled signal can carry the class reason (a bare null\n * could not name its class; SMELTER-MEDIA-TYPES Phase 0 log, note a).\n */\nexport interface ExtractionDecline {\n declined: 'no-text-layer' | 'encrypted' | 'corrupt' | 'too-large';\n}\n\n/**\n * Where a strategy may reuse an earlier recognition, and under what key.\n *\n * The caller supplies the key, and derives it from the bytes it actually\n * holds — `calculateChecksum` over the same Buffer it passes to `extract()` —\n * never from a descriptor's claim. A catalog-derived key can race a byte\n * change (bytes fetched at one moment, descriptor read at another) and file\n * or read geometry under an identity that does not describe the bytes being\n * extracted. The write path made recompute-over-claim the rule\n * (PERSIST-ANCHORS P1b); readers mirror it (P1c). One SHA-256 over bytes\n * already in memory is noise against the engine pass a hit avoids.\n *\n * Optional throughout: a caller that passes nothing extracts uncached and is\n * unaffected. The seam is `extract()` itself (PERSIST-ANCHORS D1/P2b): a hit\n * returns the FINISHED outcome — classification, geometry, provenance, or a\n * named decline — so neither the native parse nor the engine runs. Every\n * geometry-yielding extraction produces an entry, native documents included;\n * the 'decode' strategy ignores the cache (no geometry, nothing expensive).\n */\nexport interface ExtractionCache {\n key: string;\n store: AnchoredTextStore;\n}\n\nexport interface ContentExtractor {\n /**\n * Whether this strategy's extractions carry positioned runs (`items`) — the\n * geometry an anchored-text artifact is made of. Declared, not probed:\n * the reconcile planner must know \"should an artifact exist?\" without\n * running the extractor (PERSIST-ANCHORS P0, the third drift class), and\n * the declaration keeps the planner's gate and the live fetch's behavior\n * twins by construction. Text strategies anchor by character offset and\n * declare false.\n */\n yieldsGeometry: boolean;\n\n /**\n * Extract embeddable/annotatable text, or decline with the class reason\n * (scanned-without-OCR, encrypted, corrupt). The caller skips embedding\n * and settles skipped with that reason.\n */\n extract(content: Buffer, mediaType: string, cache?: ExtractionCache): Promise<ExtractedText | ExtractionDecline>;\n}\n\n/** Charset-aware decode of textual bytes — the pre-registry behavior, now\n * scoped as the 'decode' strategy's extractor. Never declines: any byte\n * sequence decodes to *some* string; emptiness is the caller's call. */\nconst passthroughExtractor: ContentExtractor = {\n yieldsGeometry: false,\n async extract(content, mediaType) {\n return { text: decodeRepresentation(content, mediaType), method: 'text-passthrough' };\n },\n};\n\n/**\n * Strategy → extractor. A `null` slot is a decline: the strategy names a\n * capability nothing currently provides ('none' permanently).\n */\nexport const EXTRACTORS: Record<TextExtraction, ContentExtractor | null> = {\n 'decode': passthroughExtractor,\n 'pdf-text-layer': pdfExtractor,\n 'none': null,\n};\n","/**\n * PDF extractor — the 'pdf-text-layer' strategy (SMELTER-MEDIA-TYPES).\n *\n * Wraps the shared `extractPdfTextLayer` reader (detection's other consumer)\n * and turns a PDF into text plus the geometry that indexes it, by class:\n *\n * A native text layer → read directly\n * B scanned → read the page pixels by OCR\n * C hybrid → both, with any page still unread reported\n * D tables → grid pages rewritten as markdown rows\n * E forms → AcroForm values folded in, anchored to widgets\n * F/G encrypted, corrupt → declined by name, from the parser error\n *\n * Everything runs inline. OCR was originally planned off the hot path, but\n * the Smelter's lanes are per-resource and concurrent, so a slow page delays\n * only its own resource — see SMELTER-MEDIA-TYPES Design §4 (revised).\n */\n\nimport { isObject, type PdfTextItem } from '@semiont/core';\nimport { extractPdfTextLayer } from './extract-pdf-text-layer';\nimport type { ContentExtractor, ExtractedText, ExtractionDecline } from './content-extractor';\nimport type { PdfTextLayer } from './pdf-text-layer';\nimport { detectTable, renderTable } from './pdf-tables';\nimport { extractPageImages } from './pdf-page-images';\nimport { recognizeImages } from './ocr';\nimport { mapWordsToItems } from './ocr-geometry';\n\n\n/** One OCR'd page: its text, and word geometry with page-local offsets. */\ninterface OcrPageResult {\n text: string;\n items: PdfTextItem[];\n /** Per-word confidences, kept only long enough to summarize. */\n confidences: number[];\n}\n\n/**\n * Largest PDF this will attempt, in bytes.\n *\n * A PDF is a compressed container, so input size bounds nothing on its own —\n * but it is the one number available before the parser touches the file, and\n * refusing here means a hostile or pathological document never gets to expand\n * inside pdf.js. Chosen to sit above real corpora (a few hundred pages of\n * scanned FOIA material runs tens of megabytes) while still being a ceiling.\n *\n * A starting point, not a measured optimum — revisit against a real corpus\n * (SMELTER-MEDIA-TYPES, live-testing follow-up). The per-image budget in\n * `pdf-page-images` guards the decoded side, which is where the unbounded\n * growth actually lives.\n */\nexport const MAX_PDF_BYTES = 200 * 1024 * 1024;\n\n/** Whether a document is small enough to attempt. Exported because the\n * threshold is a judgement, and judgements deserve tests that do not have to\n * materialize two hundred megabytes to ask the question. */\nexport function withinByteBudget(bytes: number): boolean {\n return Number.isFinite(bytes) && bytes >= 0 && bytes <= MAX_PDF_BYTES;\n}\n\n/** Words below this are worth an operator's attention. Tesseract reports\n * 0–100; readable text on a clean scan sits well above this. */\nconst LOW_CONFIDENCE = 60;\n\nfunction summarize(confidences: number[]): ExtractedText['ocrConfidence'] {\n if (confidences.length === 0) return undefined;\n const total = confidences.reduce((sum, c) => sum + c, 0);\n return {\n mean: Math.round((total / confidences.length) * 10) / 10,\n lowConfidenceWords: confidences.filter((c) => c < LOW_CONFIDENCE).length,\n totalWords: confidences.length,\n };\n}\n\n/**\n * Read the pages that have no text layer by OCR'ing their pixels. Returns\n * results only for pages that yielded text; a page absent from the map stayed\n * unread. Pages with no extractable image never reach the engine.\n *\n * Each word is anchored through the matrix that placed its image, so a scanned\n * page ends up carrying the same kind of geometry a native one does.\n */\nasync function ocrPages(\n content: Buffer,\n pageNumbers?: number[],\n): Promise<OcrPageResult> {\n // Pure recognition since PERSIST-ANCHORS P2b: the caching seam lives at\n // `extract()`, which stores and serves the FINISHED outcome. This function\n // neither consults nor writes the store — it reads pixels.\n const imagesByPage = await extractPageImages(content, pageNumbers);\n if (imagesByPage.size === 0) return { text: '', items: [], confidences: [] };\n\n // One batch for the whole document: worker startup dominates per-page cost.\n const pages = [...imagesByPage.keys()].sort((a, b) => a - b);\n const batch = pages.flatMap((page) => imagesByPage.get(page)!.map((image) => image.png));\n const recognized = await recognizeImages(batch);\n\n const byPage = new Map<number, OcrPageResult>();\n let cursor = 0;\n for (const page of pages) {\n const images = imagesByPage.get(page)!;\n let text = '';\n const items: PdfTextItem[] = [];\n const confidences: number[] = [];\n for (const image of images) {\n const result = recognized[cursor++];\n if (!result?.text.trim()) continue;\n if (text) text += '\\n';\n items.push(...mapWordsToItems(result.words, image, page, text.length));\n confidences.push(...result.words.map((word) => word.confidence));\n text += result.text;\n }\n if (text) byPage.set(page, { text, items, confidences });\n }\n\n // Joined at base 0 — the document's own coordinates. Class C shifts by the\n // native text length at its call site.\n return joinPages(byPage, 0);\n}\n\n/**\n * Recovered pages in page order as one block of text, with every word's\n * offsets shifted to where its page actually lands. `baseOffset` is where this\n * block begins in the document being assembled.\n */\nfunction joinPages(byPage: Map<number, OcrPageResult>, baseOffset: number): OcrPageResult {\n let text = '';\n const items: PdfTextItem[] = [];\n const confidences: number[] = [];\n for (const [, page] of [...byPage.entries()].sort((a, b) => a[0] - b[0])) {\n if (text) text += '\\n\\n';\n const shift = baseOffset + text.length;\n for (const item of page.items) {\n items.push({ ...item, start: item.start + shift, end: item.end + shift });\n }\n confidences.push(...page.confidences);\n text += page.text;\n }\n return { text, items, confidences };\n}\n\n/**\n * pdf.js signals a password-protected document with PasswordException.\n * Matched by name, not instanceof — pdf.js exception classes descend from\n * its own BaseException, not Error. Everything else the parser throws is\n * class G.\n */\nexport function classifyPdfError(error: unknown): 'encrypted' | 'corrupt' {\n return isObject(error) && error.name === 'PasswordException' ? 'encrypted' : 'corrupt';\n}\n\n/**\n * Class E — fold filled AcroForm values into the embedding text.\n *\n * A form's answers live in the form dictionary, not the drawn page, so a\n * naive text-layer read returns the blank labels and loses every value.\n * Each value is appended as a `name: value` line and anchored by its widget\n * rectangle, so `items` stays a complete geometry index of `text`.\n */\nfunction foldFormFields(layer: PdfTextLayer): ExtractedText {\n let text = layer.text;\n const items: PdfTextItem[] = [...layer.items];\n for (const field of layer.fields) {\n const start = text.length + `${field.name}: `.length;\n text += `${field.name}: ${field.value}\\n`;\n items.push({\n start,\n end: start + field.value.length,\n page: field.page,\n x: field.x,\n y: field.y,\n width: field.width,\n height: field.height,\n });\n }\n return { text, items, method: 'form', pdfClass: 'E' };\n}\n\n/**\n * Class D — rewrite grid pages as markdown, keep every other page verbatim.\n *\n * Returns null when no page is a table, so the caller falls back to class A.\n * Pages are shaped independently: the common report — prose sections around\n * an outcome table — gets row-coherent tables without disturbing its prose.\n */\nfunction shapeTables(layer: PdfTextLayer): ExtractedText | null {\n const pages = layer.pages.map((page) => {\n const pageItems = layer.items.filter((item) => item.page === page.pageNumber);\n return { page, pageItems, table: detectTable(pageItems, layer.text) };\n });\n if (!pages.some((p) => p.table)) return null;\n\n let text = '';\n const items: PdfTextItem[] = [];\n for (const { page, pageItems, table } of pages) {\n if (table) {\n const rendered = renderTable(table, page.pageNumber, text.length);\n text += rendered.text;\n items.push(...rendered.items);\n } else {\n // Verbatim page: copy its slice and shift its runs' offsets to match.\n const shift = text.length - page.textStart;\n text += layer.text.slice(page.textStart, page.textEnd);\n for (const item of pageItems) {\n items.push({ ...item, start: item.start + shift, end: item.end + shift });\n }\n }\n }\n return { text, items, method: 'table', pdfClass: 'D' };\n}\n\nexport const pdfExtractor: ContentExtractor = {\n // Every non-declined PDF extraction carries positioned runs — native text\n // layers and OCR both anchor by page geometry.\n yieldsGeometry: true,\n async extract(content, _mediaType, cache) {\n // The seam (PERSIST-ANCHORS D1/P2b): consult the store for the FINISHED\n // outcome before anything runs — byte gate, native parse, image decode\n // and OCR are all part of the stored answer, classification included.\n // The pre-P2b seam skipped only Tesseract, on the argument that the\n // text-layer parse \"has to run either way\" — true on a miss, false on a\n // hit. A hit is returned WHOLE, which is sound because the outcome is a\n // pure function of the bytes, the key IS the bytes' identity (the\n // caller's producer-supplied checksum — P1b/P1c), and STAMP covers the\n // code that did the deriving. Declines are first-class hits: \"we read\n // this and there was nothing\" costs a full recognition pass to discover,\n // so the negative is precisely the result worth keeping.\n const hit = await cache?.store.read(cache.key);\n if (hit) return hit;\n\n const outcome = await extractPdf(content);\n\n // Store failures stay silent — the store may make things faster, never\n // make them fail. The path that must insist on a write is the smelter's\n // re-anchor publish (P0), not this seam.\n if (cache) {\n if ('declined' in outcome) await cache.store.write(cache.key, outcome);\n else if (outcome.items) await cache.store.write(cache.key, { ...outcome, items: outcome.items });\n }\n return outcome;\n },\n};\n\n/** The uncached pipeline: classify, shape, and read the document. */\nasync function extractPdf(content: Buffer): Promise<ExtractedText | ExtractionDecline> {\n // Before the parser sees it: everything downstream — parse, image decode,\n // OCR — expands from these bytes, so this is the only gate that costs\n // nothing to enforce.\n if (!withinByteBudget(content.length)) return { declined: 'too-large' };\n\n let layer;\n try {\n layer = await extractPdfTextLayer(content);\n } catch (error) {\n return { declined: classifyPdfError(error) };\n }\n // Class B — no text operators anywhere: the characters exist only as\n // pixels, so read them. 'no-text-layer' now means OCR genuinely came up\n // empty, not that we never tried.\n if (!layer) {\n const ocr = await ocrPages(content);\n if (!ocr.text) return { declined: 'no-text-layer' };\n const confidence = summarize(ocr.confidences);\n return {\n text: ocr.text,\n items: ocr.items,\n method: 'ocr',\n pdfClass: 'B',\n ...(confidence ? { ocrConfidence: confidence } : {}),\n };\n }\n\n // One class per document, so a filled form outranks a grid: its values\n // are content that exists nowhere else, while a table's cells are at\n // worst reordered.\n const shaped = layer.fields.length > 0\n ? foldFormFields(layer)\n : shapeTables(layer)\n ?? { text: layer.text, items: layer.items, method: 'pdf-text-layer' as const, pdfClass: 'A' as const };\n\n // A page with no text-showing operators is scanned: its characters exist\n // only as pixels. Report those pages rather than dropping them silently —\n // the document embeds what it can now, and this is the list OCR works\n // from. 'C' (hybrid) replaces the plain-prose label only; a form or table\n // keeps its own class, and carries the gap just the same.\n const unreadPages = layer.pages.filter((page) => !page.hasTextLayer).map((page) => page.pageNumber);\n if (unreadPages.length === 0) return shaped;\n\n // Class C — read the scanned pages and append what OCR recovers. Appended\n // rather than spliced into reading order, so the items already computed\n // for the native pages keep pointing at the right characters; OCR text\n // carries no geometry of its own this phase (mapping pixel boxes back to\n // page points needs the image's placement transform — #739's critical\n // path, not embedding's).\n const recovered = await ocrPages(content, unreadPages);\n const readPages = new Set(recovered.items.map((item) => item.page));\n const stillUnread = unreadPages.filter((page) => !readPages.has(page));\n const hybridClass = shaped.pdfClass === 'A' ? 'C' as const : shaped.pdfClass;\n if (!recovered.text) {\n return { ...shaped, unreadPages: stillUnread, pdfClass: hybridClass };\n }\n // Appended, so the native pages' items keep pointing at the right\n // characters; the OCR'd words are offset to where they actually land.\n const shift = shaped.text.length;\n const ocr: OcrPageResult = {\n text: recovered.text,\n items: recovered.items.map((item) => ({ ...item, start: item.start + shift, end: item.end + shift })),\n confidences: recovered.confidences,\n };\n const confidence = summarize(ocr.confidences);\n return {\n ...shaped,\n text: `${shaped.text}${ocr.text}\\n`,\n items: [...(shaped.items ?? []), ...ocr.items],\n method: 'ocr',\n pdfClass: hybridClass,\n ...(confidence ? { ocrConfidence: confidence } : {}),\n ...(stillUnread.length > 0 ? { unreadPages: stillUnread } : {}),\n };\n}\n","/**\n * PDF Text Layer Extraction\n *\n * Extracts positioned text from native, non-scanned PDFs using pdfjs-dist.\n * Returns null for scanned/image-only PDFs (no text items).\n *\n * Coordinates are in PDF point space, originating from the bottom-left.\n * The Y-flip to canvas pixels happens downstream.\n */\n\nimport * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs';\nimport { STANDARD_FONT_DATA_URL } from './pdfjs-assets';\nimport { isObject, isString, isNumber, isArray, anchorRuns, isTextRun, type PdfTextItem } from '@semiont/core';\nimport type { PdfTextLayer, PdfPageInfo, PdfFormField } from './pdf-text-layer';\n\n/**\n * One entry from pdf.js's `getFieldObjects()` map, narrowed to a filled\n * field. The API types entries as bare `Object`, so every field is checked:\n * group entries (a parent with `kidIds`) carry `page: -1` and no value and\n * are rejected here, leaving the widgets that actually hold content.\n */\nfunction toFormField(entry: unknown): PdfFormField | null {\n if (!isObject(entry)) return null;\n const { name, value, page, rect } = entry;\n if (!isString(name) || !isString(value) || !value.trim()) return null;\n if (!isNumber(page) || page < 0) return null;\n if (!isArray(rect) || rect.length < 4 || !rect.every(isNumber)) return null;\n const [x1, y1, x2, y2] = rect as [number, number, number, number];\n return {\n name,\n value: value.trim(),\n page: page + 1, // pdf.js reports 0-indexed; PdfTextItem is 1-indexed\n x: Math.min(x1, x2),\n y: Math.min(y1, y2),\n width: Math.abs(x2 - x1),\n height: Math.abs(y2 - y1),\n };\n}\n\n/**\n * Filled AcroForm values, one per field name (first filled widget wins, so a\n * radio group contributes a single answer). Returns [] for a document with\n * no form. XFA forms are out of scope: whatever their AcroForm shell exposes\n * is read the same way, and anything else simply yields no fields.\n */\nasync function readFormFields(doc: pdfjs.PDFDocumentProxy): Promise<PdfFormField[]> {\n const fieldObjects = await doc.getFieldObjects();\n if (!fieldObjects) return [];\n const byName = new Map<string, PdfFormField>();\n for (const entries of Object.values(fieldObjects)) {\n if (!isArray(entries)) continue;\n for (const entry of entries) {\n const field = toFormField(entry);\n if (field && !byName.has(field.name)) byName.set(field.name, field);\n }\n }\n return [...byName.values()];\n}\n\nexport async function extractPdfTextLayer(\n bytes: Uint8Array | Buffer\n): Promise<PdfTextLayer | null> {\n // A private copy, for two pdf.js contracts at once: it refuses Node\n // Buffers outright (\"provide binary data as Uint8Array\"), and it CONSUMES\n // the array it is given — the underlying ArrayBuffer is transferred and\n // detached, which would silently zero the caller's bytes. Callers keep\n // their bytes; pdf.js gets its own.\n const data = new Uint8Array(bytes);\n // pdf.js v5 removed the isEvalSupported option; this path only calls\n // getTextContent (no rendering / no PDF functions).\n const loadingTask = pdfjs.getDocument({ data, standardFontDataUrl: STANDARD_FONT_DATA_URL });\n\n try {\n // Inside the try so the finally's destroy() also runs when the\n // parse rejects (encrypted/corrupt input — the extractor's decline\n // path classifies that throw).\n const doc = await loadingTask.promise;\n const pages: PdfPageInfo[] = [];\n const items: PdfTextItem[] = [];\n let text = '';\n\n for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {\n const page = await doc.getPage(pageNum);\n const viewport = page.getViewport({ scale: 1.0 });\n const content = await page.getTextContent(); // all text items on the page\n const pageTextStart = text.length;\n\n // `anchorRuns` owns the offset and separator convention; the\n // browser canvas builds its page the same way, so a rectangle\n // quotes identically whichever side captured it. Marked-content\n // items (no `str`) are filtered here, at the pdf.js boundary —\n // core stays free of pdfjs-dist.\n const page1 = anchorRuns(content.items.filter(isTextRun), pageNum);\n\n // Offsets come back page-local; shift them into the document text.\n for (const item of page1.items) {\n items.push({ ...item, start: item.start + pageTextStart, end: item.end + pageTextStart });\n }\n text += page1.text;\n text += '\\n'; // page break\n\n pages.push({\n pageNumber: pageNum,\n widthPt: viewport.width,\n heightPt: viewport.height,\n textStart: pageTextStart,\n textEnd: text.length,\n hasTextLayer: page1.items.length > 0,\n });\n }\n\n // A document with no drawn text is a scanned page (class B) even when\n // it carries an AcroForm — form values augment a text layer, they do\n // not substitute for one. Keeping this condition on text items alone\n // also keeps the reader's null contract stable for detection.\n if (!pages.some((page) => page.hasTextLayer)) return null;\n\n return { pages, text, items, fields: await readFormFields(doc) };\n } finally {\n // Release the pdf.js document — Phase 2 runs this in a long-lived worker\n // pool. pdf.js 6.0 removed PDFDocumentProxy.destroy(); teardown moved to\n // PDFDocumentLoadingTask.destroy().\n await loadingTask.destroy();\n }\n}\n","/**\n * Where pdf.js finds the asset bundles it does not carry in its main build.\n *\n * pdf.js ships the Standard 14 font programs (Foxit substitutes for Helvetica,\n * Times, Courier, Symbol, ZapfDingbats) as separate `.pfb` files rather than in\n * `pdf.mjs`. Without a `standardFontDataUrl` it cannot load them, and every\n * document that references a standard font logs\n *\n * Warning: UnknownErrorException: Ensure that the `standardFontDataUrl` API\n * parameter is provided.\n *\n * once per font per document — 66 lines on a single 28-page PDF, drowning real\n * output. That noise is the reason to fix it; text extraction itself was never\n * affected, because `getTextContent()` reads the content stream and the font's\n * encoding, not its glyph outlines.\n *\n * Resolved through `require.resolve` rather than a path relative to this file:\n * the built `dist/` sits at a different depth than `src/`, and npm may hoist\n * `pdfjs-dist` to the workspace root or nest it under this package. Asking the\n * resolver is the only form that is correct in all of those, including inside\n * the service images where the tree is installed fresh.\n *\n * The trailing slash is required — pdf.js concatenates the filename onto this\n * string.\n */\n\nimport { createRequire } from 'module';\nimport path from 'path';\n\nconst require = createRequire(import.meta.url);\n\nexport const STANDARD_FONT_DATA_URL =\n `${path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'standard_fonts')}${path.sep}`;\n","/**\n * Table reconstruction from PDF text-layer geometry (SMELTER-MEDIA-TYPES\n * class D).\n *\n * A PDF has no table structure — only positioned text runs. Read in reading\n * order a grid's cells interleave, so a row's values scatter across chunks\n * and semantic recall over an outcome table returns nothing useful. This\n * module recovers the grid from the geometry the reader already carries\n * (`PdfTextItem.x/y/width/height`), then renders markdown rows so a row's\n * cells stay adjacent for the shared chunker. No new dependency: the\n * clustering is the same arithmetic a table library would do, over data we\n * already have.\n *\n * PRECISION OVER RECALL. A false positive scrambles prose into a fake table;\n * a false negative merely falls back to class A, which is Phase 1 behavior.\n * So detection demands a strict, regular grid — every row the same cell\n * count, every column aligned — and declines everything else.\n */\n\nimport type { PdfTextItem } from '@semiont/core';\n\n/** A reconstructed cell: its text plus the bounding box of its runs. */\nexport interface TableCell {\n text: string;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** A header row plus at least two data rows — below this, prose in columns\n * is indistinguishable from a table. */\nconst MIN_ROWS = 3;\nconst MIN_COLUMNS = 2;\n\n/** Row grouping tolerance, as a fraction of text height: runs whose\n * baselines differ by less than half a line belong to one row. */\nconst ROW_TOLERANCE = 0.5;\n/** Horizontal gap that separates cells, as a fraction of text height. Word\n * spaces are far narrower; column gutters are far wider. */\nconst CELL_GAP = 0.8;\n\nfunction median(values: number[]): number {\n const sorted = [...values].sort((a, b) => a - b);\n return sorted[Math.floor(sorted.length / 2)] ?? 0;\n}\n\n/** Group runs into visual rows, top of page first. */\nfunction groupRows(items: PdfTextItem[], tolerance: number): PdfTextItem[][] {\n const rows: PdfTextItem[][] = [];\n for (const item of [...items].sort((a, b) => b.y - a.y)) {\n const row = rows[rows.length - 1];\n if (row && Math.abs(row[0]!.y - item.y) <= tolerance) row.push(item);\n else rows.push([item]);\n }\n return rows;\n}\n\nfunction toCell(runs: PdfTextItem[], text: string): TableCell {\n const x = Math.min(...runs.map((r) => r.x));\n const y = Math.min(...runs.map((r) => r.y));\n const right = Math.max(...runs.map((r) => r.x + r.width));\n const top = Math.max(...runs.map((r) => r.y + r.height));\n return {\n text: runs.map((r) => text.slice(r.start, r.end)).join(' ').trim(),\n x,\n y,\n width: right - x,\n height: top - y,\n };\n}\n\n/** Split a row into cells: runs closer than a gutter belong to one cell. */\nfunction toCells(row: PdfTextItem[], gap: number, text: string): TableCell[] {\n const cells: TableCell[] = [];\n let current: PdfTextItem[] = [];\n for (const item of [...row].sort((a, b) => a.x - b.x)) {\n const previous = current[current.length - 1];\n if (previous && item.x - (previous.x + previous.width) > gap) {\n cells.push(toCell(current, text));\n current = [];\n }\n current.push(item);\n }\n if (current.length > 0) cells.push(toCell(current, text));\n return cells;\n}\n\n/**\n * Recover a grid from one page's runs, or null when the page is not a\n * regular table.\n */\nexport function detectTable(items: PdfTextItem[], text: string): TableCell[][] | null {\n if (items.length === 0) return null;\n const unit = median(items.map((i) => i.height).filter((h) => h > 0)) || 12;\n\n const rows = groupRows(items, unit * ROW_TOLERANCE).map((row) => toCells(row, unit * CELL_GAP, text));\n if (rows.length < MIN_ROWS) return null;\n\n const columnCount = rows[0]!.length;\n if (columnCount < MIN_COLUMNS) return null;\n if (!rows.every((row) => row.length === columnCount)) return null;\n\n // Every column must start at the same offset down the page; ragged left\n // edges mean prose that happens to wrap into columns, not a grid.\n for (let column = 0; column < columnCount; column++) {\n const lefts = rows.map((row) => row[column]!.x);\n if (Math.max(...lefts) - Math.min(...lefts) > unit) return null;\n }\n if (rows.some((row) => row.some((cell) => cell.text.length === 0))) return null;\n\n return rows;\n}\n\n/**\n * Render a grid as markdown rows, anchoring every cell to the geometry it\n * came from. `offset` is where this text lands in the assembled document, so\n * the returned items index the final string.\n */\nexport function renderTable(\n rows: TableCell[][],\n page: number,\n offset: number,\n): { text: string; items: PdfTextItem[] } {\n let text = '';\n const items: PdfTextItem[] = [];\n rows.forEach((row, rowIndex) => {\n text += '|';\n for (const cell of row) {\n text += ' ';\n const start = offset + text.length;\n text += cell.text;\n items.push({\n start,\n end: offset + text.length,\n page,\n x: cell.x,\n y: cell.y,\n width: cell.width,\n height: cell.height,\n });\n text += ' |';\n }\n text += '\\n';\n // Markdown needs the delimiter row for the header to read as a table.\n if (rowIndex === 0) text += `|${' --- |'.repeat(row.length)}\\n`;\n });\n return { text, items };\n}\n","/**\n * Embedded page images from a PDF — the pixels OCR reads.\n *\n * A scanned page holds its characters only as pixels inside an image object,\n * so reading it means getting that image out. We do NOT rasterize: pdf.js\n * decodes the embedded image in its worker (pure JS — JPEG, CCITT and JBIG2\n * decoders all live there) and hands back raw pixel planes, which means no\n * canvas backend and no native dependency. Measured, not assumed — see\n * `.plans/SMELTER-MEDIA-TYPES.md` Resolved decision 10.\n *\n * Two consequences of extracting rather than rendering: we get the scan's own\n * resolution rather than choosing a render DPI (for a real scan that IS the\n * page, so it is what we want), and a page composed of vector overlays or\n * tiled strips yields more than one image, or none we can use. Anything we\n * cannot turn into pixels simply stays unread — never an error.\n */\n\nimport * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs';\nimport { STANDARD_FONT_DATA_URL } from './pdfjs-assets';\nimport { isObject, isNumber, isString, isArray } from '@semiont/core';\nimport { encodePng } from './png-encode';\n\n/** pdf.js image kinds (`ImageKind` in its API). */\nconst GRAYSCALE_1BPP = 1;\nconst RGB_24BPP = 2;\nconst RGBA_32BPP = 3;\n\nconst IDENTITY: readonly number[] = [1, 0, 0, 1, 0, 0];\n\n/**\n * Largest image this will read, in pixels.\n *\n * Sizing this needs the WHOLE allocation chain, not just the decoded raster —\n * reading one image can hold several copies at once:\n *\n * pdf.js decoded samples 4 bytes/px worst case (RGBA; RGB is 3)\n * + `toRgb` conversion 3 bytes/px (RGBA and 1-bit both allocate a copy;\n * plain RGB is passed through, no copy)\n * + `encodePng` scanlines 3 bytes/px (`raw`, plus a filter byte per row)\n * + deflate output smaller, but live alongside the above\n * ────────────────────────────────────────────────────────────────────\n * ≈ 10 bytes/px transient peak for a single image\n *\n * So the budget below implies roughly half a gigabyte of transient peak for\n * one pathological page — the number to size a worker against. Stating three\n * bytes per pixel here (as an earlier revision did) understated it by ~3× and\n * gave a false sense of safety.\n *\n * Chosen to admit the legitimate large cases with headroom: US Letter at\n * 600dpi is ~34 MP and A0 at 300dpi is ~35 MP, against an ordinary US Letter\n * at 300dpi of ~8 MP.\n *\n * A starting point, not a measured optimum: revisit against a real scanned\n * corpus (SMELTER-MEDIA-TYPES, live-testing follow-up). Lowering the peak\n * itself means removing copies from the chain — passing the decoded samples\n * straight to the encoder — which is a refactor, not a smaller constant.\n */\nexport const MAX_IMAGE_PIXELS = 48_000_000;\n\n/** Worst-case bytes held per pixel while reading one image — the chain above.\n * Exported so the budget's real cost is asserted rather than assumed. */\nexport const PEAK_BYTES_PER_PIXEL = 10;\n\n/** Whether an image's dimensions are sane and inside the budget. Exported\n * because the threshold is a judgement, and judgements deserve tests. */\nexport function withinPixelBudget(width: number, height: number): boolean {\n if (!Number.isFinite(width) || !Number.isFinite(height)) return false;\n if (width <= 0 || height <= 0) return false;\n return width * height <= MAX_IMAGE_PIXELS;\n}\n\n/** An image painted on a page, with the matrix that placed it. */\nexport interface PlacedImage {\n ref: string;\n /** Natural pixel dimensions, as reported by the paint operator itself. */\n width: number;\n height: number;\n /** Maps the image's unit square onto the page, in PDF points. */\n ctm: number[];\n}\n\n/**\n * Walk an operator list and report every painted image with the matrix in\n * effect when it was painted.\n *\n * Exported for its own tests: the composition ORDER cannot be checked with a\n * generated fixture, because pdf-lib emits a single combined matrix per image\n * and identity × M equals M × identity. It is checked directly instead, with\n * two non-identity transforms.\n *\n * Order convention: `ctm = Util.transform(ctm, m)` puts each new matrix on the\n * right, so it applies to a point FIRST and the enclosing matrices after —\n * which is what PDF nesting means. `save`/`restore` bracket the stack.\n */\nexport function findPlacedImages(fnArray: number[], argsArray: unknown[][]): PlacedImage[] {\n const placed: PlacedImage[] = [];\n const stack: number[][] = [];\n let ctm: number[] = [...IDENTITY];\n\n for (let i = 0; i < fnArray.length; i++) {\n const op = fnArray[i];\n const args = argsArray[i];\n if (op === pdfjs.OPS.save) {\n stack.push([...ctm]);\n } else if (op === pdfjs.OPS.restore) {\n ctm = stack.pop() ?? [...IDENTITY];\n } else if (op === pdfjs.OPS.transform) {\n if (isArray(args) && args.length >= 6 && args.every(isNumber)) {\n ctm = pdfjs.Util.transform(ctm, args as number[]);\n }\n } else if (op === pdfjs.OPS.paintImageXObject) {\n const ref = args?.[0];\n const width = args?.[1];\n const height = args?.[2];\n if (isString(ref) && isNumber(width) && isNumber(height)) {\n placed.push({ ref, width, height, ctm: [...ctm] });\n }\n }\n }\n return placed;\n}\n\n/**\n * The decoded samples, whichever byte view pdf.js chose.\n *\n * `/FlateDecode` images arrive as a `Uint8Array`; `/DCTDecode` (JPEG) — what\n * essentially every real scanned PDF uses — arrives as a `Uint8ClampedArray`,\n * which is NOT an instance of `Uint8Array`. Testing only for the latter\n * discarded every real scan while accepting every fixture in this repo, all\n * of which are Flate. Both index bytes identically, so both are read; the\n * clamped view is re-wrapped without copying its 12 MB buffer.\n */\nfunction asBytes(data: unknown): Uint8Array | null {\n if (data instanceof Uint8Array) return data;\n if (data instanceof Uint8ClampedArray) return new Uint8Array(data.buffer, data.byteOffset, data.length);\n return null;\n}\n\n/**\n * Normalize a decoded pdf.js image to 8-bit RGB, or null for a kind we do\n * not read. Unknown kinds leave the page unread rather than risk feeding an\n * OCR engine garbled pixels.\n */\nexport function toRgb(image: unknown): { width: number; height: number; rgb: Uint8Array } | null {\n if (!isObject(image)) return null;\n const { width, height, kind } = image;\n const data = asBytes(image.data);\n if (!isNumber(width) || !isNumber(height) || !data) return null;\n if (width <= 0 || height <= 0) return null;\n\n if (kind === RGB_24BPP) {\n return data.length >= width * height * 3 ? { width, height, rgb: data } : null;\n }\n\n if (kind === RGBA_32BPP) {\n if (data.length < width * height * 4) return null;\n const rgb = new Uint8Array(width * height * 3);\n for (let i = 0, o = 0; o < rgb.length; i += 4, o += 3) {\n rgb[o] = data[i]!;\n rgb[o + 1] = data[i + 1]!;\n rgb[o + 2] = data[i + 2]!;\n }\n return { width, height, rgb };\n }\n\n if (kind === GRAYSCALE_1BPP) {\n // Packed bilevel, rows padded to a byte boundary — the shape fax-encoded\n // scans arrive in. A set bit is white, matching pdf.js's own rendering.\n // If a real CCITT scan ever comes out inverted, this is the line to fix;\n // the failure mode is a page that OCRs to nothing, not corrupt output.\n const rowBytes = Math.ceil(width / 8);\n if (data.length < rowBytes * height) return null;\n const rgb = new Uint8Array(width * height * 3);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const bit = data[y * rowBytes + (x >> 3)]! & (0x80 >> (x & 7));\n const value = bit ? 0xFF : 0x00;\n const o = (y * width + x) * 3;\n rgb[o] = value;\n rgb[o + 1] = value;\n rgb[o + 2] = value;\n }\n }\n return { width, height, rgb };\n }\n\n return null;\n}\n\n/**\n * How long to wait for pdf.js to deliver one image before giving up on the\n * page. Generous: this is not a performance budget but a liveness backstop —\n * see `resolveImage`.\n */\nconst IMAGE_RESOLVE_TIMEOUT_MS = 30_000;\n\n/**\n * Resolve one image object; pdf.js delivers it asynchronously, so the callback\n * form is required — the synchronous getter throws.\n *\n * Two scopes, and asking the wrong one never answers. An image used by a single\n * page lives in `page.objs` as `img_p0_1`; an image used by MORE than one page\n * — a letterhead, a watermark, a scan pipeline that dedupes identical page\n * rasters — is promoted to pdf.js's global scope, renamed `g_d1_img_p1_1`, and\n * lives in `page.commonObjs`. `objs.get` on a global ref simply registers a\n * callback that is never invoked.\n *\n * The timeout is the second half, and it is about liveness rather than speed:\n * the smelter and the detection worker both `await` this, and a worker will not\n * claim another job while one is active — so a promise that never settles wedges\n * that worker permanently, on one bad document. Timing out yields `null`, which\n * leaves the page unread and reported, the same as an unreadable image kind.\n */\nfunction resolveImage(page: pdfjs.PDFPageProxy, ref: string): Promise<unknown> {\n // pdf.js marks globally-scoped objects with a `g_` prefix.\n const scope = ref.startsWith('g_') ? page.commonObjs : page.objs;\n return new Promise((resolve) => {\n const timer = setTimeout(() => resolve(null), IMAGE_RESOLVE_TIMEOUT_MS);\n const settle = (value: unknown) => {\n clearTimeout(timer);\n resolve(value);\n };\n try {\n scope.get(ref, settle);\n } catch {\n settle(null);\n }\n });\n}\n\n/** A page image ready for OCR, with everything needed to map results back. */\nexport interface PageImage {\n png: Buffer;\n /** Pixel dimensions of the decoded raster (may differ from the paint\n * operator's declared size if the image was resampled). */\n width: number;\n height: number;\n /** Maps the image's unit square onto the page, in PDF points. */\n ctm: number[];\n}\n\n/**\n * PNG-encoded images for the given pages (all pages when omitted), keyed by\n * 1-indexed page number, each with the matrix that placed it. Pages with no\n * usable image are absent from the map.\n */\nexport async function extractPageImages(\n bytes: Uint8Array | Buffer,\n pageNumbers?: number[],\n): Promise<Map<number, PageImage[]>> {\n const wanted = pageNumbers ? new Set(pageNumbers) : null;\n const loadingTask = pdfjs.getDocument({ data: new Uint8Array(bytes), standardFontDataUrl: STANDARD_FONT_DATA_URL });\n const byPage = new Map<number, PageImage[]>();\n\n try {\n const doc = await loadingTask.promise;\n for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {\n if (wanted && !wanted.has(pageNum)) continue;\n const page = await doc.getPage(pageNum);\n const ops = await page.getOperatorList();\n\n const images: PageImage[] = [];\n for (const placement of findPlacedImages(ops.fnArray, ops.argsArray)) {\n // Checked from the paint operator's own dimensions, BEFORE the\n // image is resolved — refusing after decoding would already\n // have paid the allocation this guards against.\n if (!withinPixelBudget(placement.width, placement.height)) continue;\n const rgb = toRgb(await resolveImage(page, placement.ref));\n if (!rgb) continue;\n images.push({\n png: encodePng(rgb.width, rgb.height, rgb.rgb),\n width: rgb.width,\n height: rgb.height,\n ctm: placement.ctm,\n });\n }\n if (images.length > 0) byPage.set(pageNum, images);\n }\n return byPage;\n } finally {\n await loadingTask.destroy();\n }\n}\n","/**\n * Minimal PNG encoder.\n *\n * OCR engines take an encoded image, while pdf.js hands back raw pixel\n * planes — this bridges the two. Deterministic, built on node's zlib, so a\n * package that deliberately carries no image dependency still does not.\n */\n\nimport zlib from 'zlib';\n\nconst CRC_TABLE = (() => {\n const table = new Int32Array(256);\n for (let n = 0; n < 256; n++) {\n let c = n;\n for (let k = 0; k < 8; k++) c = (c & 1) ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;\n table[n] = c;\n }\n return table;\n})();\n\nfunction crc32(buf: Buffer): number {\n let c = -1;\n for (const byte of buf) c = CRC_TABLE[(c ^ byte) & 0xFF]! ^ (c >>> 8);\n return (c ^ -1) >>> 0;\n}\n\nfunction chunk(type: string, data: Buffer): Buffer {\n const length = Buffer.alloc(4);\n length.writeUInt32BE(data.length);\n const body = Buffer.concat([Buffer.from(type, 'ascii'), data]);\n const crc = Buffer.alloc(4);\n crc.writeUInt32BE(crc32(body));\n return Buffer.concat([length, body, crc]);\n}\n\n/** Encode 8-bit RGB pixels (length must be width × height × 3) as a PNG. */\nexport function encodePng(width: number, height: number, rgb: Uint8Array): Buffer {\n const stride = width * 3 + 1; // one filter byte per scanline\n const raw = Buffer.alloc(stride * height);\n for (let y = 0; y < height; y++) {\n raw[y * stride] = 0; // filter type: none\n Buffer.from(rgb.buffer, rgb.byteOffset + y * width * 3, width * 3)\n .copy(raw, y * stride + 1);\n }\n const ihdr = Buffer.alloc(13);\n ihdr.writeUInt32BE(width, 0);\n ihdr.writeUInt32BE(height, 4);\n ihdr[8] = 8; // bit depth\n ihdr[9] = 2; // color type: truecolor\n return Buffer.concat([\n Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),\n chunk('IHDR', ihdr),\n chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),\n chunk('IEND', Buffer.alloc(0)),\n ]);\n}\n","/**\n * OCR — reading text out of page pixels (tesseract.js).\n *\n * Runs inline, on the caller's thread of control, deliberately: the Smelter's\n * lanes are per-resource and concurrent (`groupBy` + `mergeMap`), so a slow\n * page delays only its own resource, never the fast text resources it shares\n * a worker with. Extraction stays ephemeral — nothing is cached, and a\n * rebuild re-reads the pixels (SMELTER-MEDIA-TYPES Design §3/§5).\n *\n * Deterministic for a pinned engine: the same bytes yield the same text, so\n * re-running costs time and nothing else.\n */\n\nimport { createRequire } from 'node:module';\nimport { createWorker } from 'tesseract.js';\nimport { isObject, isString } from '@semiont/core';\n\n/**\n * The vendored language data — `@tesseract.js-data/eng` ships the same\n * `eng.traineddata.gz` tesseract.js would otherwise fetch from a CDN, and\n * exports the directory holding it.\n *\n * OCR is core (SMELTER-MEDIA-TYPES decision 8), so it must never reach the\n * network at runtime: an air-gapped worker has to be able to read a scan, and\n * a CDN outage must not silently turn scanned documents unreadable. Because\n * this is an ordinary dependency, `npm install` vendors it into the smelter\n * and worker images — no Dockerfile fetch step, and the lockfile pins it.\n *\n * Resolved lazily so importing this module has no side effects.\n */\nlet cachedLangPath: string | undefined;\nfunction langPath(): string {\n if (cachedLangPath) return cachedLangPath;\n const data: unknown = createRequire(import.meta.url)('@tesseract.js-data/eng');\n if (!isObject(data) || !isString(data.langPath)) {\n throw new Error(\n 'Vendored OCR language data is missing or malformed: @tesseract.js-data/eng did not export a langPath',\n );\n }\n cachedLangPath = data.langPath;\n return cachedLangPath;\n}\n\n/**\n * The slice of tesseract's recognition tree this module reads. Declared\n * structurally rather than importing `Tesseract.Block`, so tests can build a\n * tree without satisfying a dozen fields nothing here looks at; a real\n * `Block[]` still satisfies it.\n */\nexport interface OcrBbox { x0: number; y0: number; x1: number; y1: number }\nexport interface OcrLine {\n /** The line's own box — the vertical extent shared by its words. */\n bbox: OcrBbox;\n words: { text: string; confidence: number; bbox: OcrBbox }[];\n}\nexport interface OcrBlock {\n paragraphs: { lines: OcrLine[] }[];\n}\n\n/** A recognized word, with the range it occupies in the assembled page text. */\nexport interface OcrWord {\n text: string;\n /** Offsets into `OcrPage.text` — `text.slice(start, end) === word.text`. */\n start: number;\n end: number;\n /** Image pixel space, top-left origin — mapped to PDF points downstream. */\n bbox: OcrBbox;\n confidence: number;\n}\n\nexport interface OcrPage {\n text: string;\n words: OcrWord[];\n}\n\n/**\n * Assemble a page's text from its recognition tree, recording where each word\n * lands as it is written.\n *\n * The text is built here rather than taken from tesseract's own `data.text`\n * precisely so the offsets are exact **by construction** — deriving offsets by\n * searching for words in a separately-produced string is where this kind of\n * code goes wrong. Words join with a space, lines with a newline, paragraphs\n * with a blank line.\n */\nexport function assemblePage(blocks: OcrBlock[] | null): OcrPage {\n let text = '';\n const words: OcrWord[] = [];\n\n for (const block of blocks ?? []) {\n for (const paragraph of block.paragraphs ?? []) {\n for (const line of paragraph.lines ?? []) {\n let wroteWord = false;\n for (const word of line.words ?? []) {\n const value = word.text.trim();\n if (!value) continue; // an empty box is not a word\n if (wroteWord) text += ' ';\n const start = text.length;\n text += value;\n words.push({\n text: value,\n start,\n end: text.length,\n // Horizontal extent from the word, vertical from the\n // line. OCR boxes hug their glyphs, so a descender\n // ('page') sits lower than its neighbours — and\n // `locate()` groups items into lines by comparing `y`\n // within a couple of points, a threshold that holds\n // because NATIVE runs take y from the shared baseline.\n // Passing per-word descenders through would split one\n // visual line into several rects and draw a highlight\n // as stacked fragments. Nothing is lost: `locate()`\n // bounds each line anyway, so per-word vertical extent\n // never reaches an annotation.\n bbox: {\n x0: word.bbox.x0,\n x1: word.bbox.x1,\n y0: line.bbox.y0,\n y1: line.bbox.y1,\n },\n confidence: word.confidence,\n });\n wroteWord = true;\n }\n if (wroteWord) text += '\\n';\n }\n text += '\\n';\n }\n }\n\n // Only trailing separators are removed, so no recorded offset moves.\n return { text: text.trimEnd(), words };\n}\n\n/**\n * Recognize a batch of PNG images, returning one result per image (empty\n * where nothing legible was found). One worker serves the whole batch —\n * startup is the expensive part, not the pages.\n */\nexport async function recognizeImages(images: Buffer[]): Promise<OcrPage[]> {\n if (images.length === 0) return [];\n // `cacheMethod: 'none'` — the data is already local, so there is nothing to\n // cache and no reason to write a copy into the working directory.\n const worker = await createWorker('eng', undefined, {\n langPath: langPath(),\n cacheMethod: 'none',\n });\n try {\n const results: OcrPage[] = [];\n for (const image of images) {\n // `blocks: true` is what carries the per-word geometry; without it\n // tesseract returns text only and `data.blocks` is null.\n const { data } = await worker.recognize(image, {}, { blocks: true, text: false });\n results.push(assemblePage(data.blocks));\n }\n return results;\n } finally {\n await worker.terminate();\n }\n}\n","/**\n * OCR word boxes → PDF-point geometry (#739).\n *\n * OCR reports boxes in the image's own pixel space, top-left origin. Anchoring\n * them means going through the matrix that placed the image on the page:\n *\n * pixel (px, py) → unit square (px/W, 1 − py/H) → CTM → PDF points\n *\n * Rotation and non-uniform scale fall out of the matrix, so there are no\n * special cases for them — the only explicit work is normalizing the result,\n * since a mirrored placement can invert an axis and consumers bound\n * rectangles rather than orienting them.\n */\n\nimport * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs';\nimport type { OcrWord } from './ocr';\nimport type { PdfTextItem } from '@semiont/core';\n\n/** The placement of one image: its pixel size and its matrix onto the page. */\nexport interface ImagePlacement {\n width: number;\n height: number;\n ctm: number[];\n}\n\nfunction toPagePoint(px: number, py: number, placement: ImagePlacement): [number, number] {\n // Unit square, Y flipped: pixel rows run down, PDF space runs up.\n const point: [number, number] = [px / placement.width, 1 - py / placement.height];\n pdfjs.Util.applyTransform(point, placement.ctm); // mutates in place\n return point;\n}\n\n/**\n * Map recognized words onto the page, shifting their character offsets by\n * `textOffset` — where this page's text begins in the assembled document.\n */\nexport function mapWordsToItems(\n words: OcrWord[],\n placement: ImagePlacement,\n page: number,\n textOffset: number,\n): PdfTextItem[] {\n if (placement.width <= 0 || placement.height <= 0) return [];\n\n return words.map((word) => {\n // Both corners through the matrix, then bound them — a flipped or\n // rotated placement can put either one first.\n const [ax, ay] = toPagePoint(word.bbox.x0, word.bbox.y0, placement);\n const [bx, by] = toPagePoint(word.bbox.x1, word.bbox.y1, placement);\n const x = Math.min(ax, bx);\n const y = Math.min(ay, by);\n return {\n start: word.start + textOffset,\n end: word.end + textOffset,\n page,\n x,\n y,\n width: Math.abs(bx - ax),\n height: Math.abs(by - ay),\n };\n });\n}\n","/**\n * Anchored-text cache — the persistent half of ANCHORED-TEXT-CACHE.md Lane 2.\n *\n * OCR costs ~2.9 s per scanned page, and six passes read the same document (five\n * detection motivations plus the smelter's embed), each its own job in its own\n * process. This stores what the engine produced so only the first pass pays.\n *\n * **Derived values only.** Everything here is reproducible from the source\n * bytes, which is what makes a stamp miss safe. An authored coordinate map is\n * embedded in the PDF Semiont generated, not stored alongside one — see\n * `PDF-GENERATION.md`, which owns that decision and states the negative:\n * never this store.\n *\n * The seam is `extract()` (PERSIST-ANCHORS D1/P2b): the record is the FINISHED\n * extraction outcome — classification, geometry, provenance, or a named\n * decline — so a hit skips the native parse and the engine both, and every\n * geometry-yielding extraction stores an entry, native documents included.\n * That is what makes the anchored-text endpoint answer for every resource\n * whose extraction yields geometry, and what lets the reconcile planner treat\n * \"no entry under the current checksum\" as work (P0's third drift class).\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport { createRequire } from 'module';\nimport { getShardPath, isObject, isString, isNumber, isArray, type ExtractionOutcome, type Logger, type PdfTextItem } from '@semiont/core';\n\n/** The two halves of the wire record, split for storage. */\ntype SuccessOutcome = Exclude<ExtractionOutcome, { declined: string }>;\ntype DeclineOutcome = Extract<ExtractionOutcome, { declined: string }>;\n\n/**\n * One line of recognized text: the geometry every word on it shares, plus the\n * per-word parts that differ.\n *\n * Grouping is by *contiguous runs* of equal `(y, h)`, never by scanning for all\n * items at a given y. That makes the codec lossless and order-preserving for\n * any input — compression is the only thing that depends on words actually\n * arriving in reading order, and correctness never is.\n *\n * Sharing `y`/`h` is measured-safe rather than assumed: within-line word-height\n * spread is 0.0pt in both native and OCR'd output, because the engine already\n * normalizes word boxes to the line. Per-word `x` and `width` are stored\n * explicitly and NOT derived from neighbouring split positions — deriving width\n * from the gap to the next word would widen every box to touch its neighbour,\n * which would silently change the coverage arithmetic `textUnder` is calibrated\n * on (RUN_COVERAGE_THRESHOLD, tuned against ink-tight boxes).\n */\nexport interface CachedLine {\n /** 1-indexed page. */\n p: number;\n /** PDF points, bottom-left origin — shared by every word on the line. */\n y: number;\n h: number;\n /** `[x, width, start, end]` per word; offsets index `CachedAnchoredText.text`. */\n words: [number, number, number, number][];\n}\n\n/**\n * The stored record: one extraction OUTCOME for the whole resource\n * (PERSIST-ANCHORS decision D1) — the anchored text with its provenance\n * (`method`, `pdfClass`, `ocrConfidence`, `unreadPages`), or a named decline.\n *\n * Whole-resource on every side, deliberately. The producer's own shape is a\n * per-page map, but that is an artifact of how `ocrPages` iterates, and letting\n * it reach storage would have forced every consumer — the transport, the\n * browser, a headless client — to reassemble pages it never asked to see.\n *\n * The `ocrConfidence` SUMMARY is stored (v2) — this repairs the regression\n * OCR-CONFIDENCE-LOST.md records, where a hit answered with no confidence at\n * all. Per-word confidences remain unstored: the summary is the record's\n * quality provenance; the word list is operator log detail.\n *\n * v1 records (bare `{ text, lines }`, no provenance) read as misses under the\n * v2 prefix; the reconcile planner's third drift class re-derives them.\n */\nexport type CachedAnchoredText =\n | ({\n v: 2;\n /** Engine + traineddata + our assembly code. A mismatch is a clean miss. */\n stamp: string;\n text: string;\n lines: CachedLine[];\n } & Omit<SuccessOutcome, 'text' | 'items'>)\n | ({\n v: 2;\n stamp: string;\n } & DeclineOutcome);\n\n/**\n * What the cached value must be recomputed against.\n *\n * Derived, never hand-maintained. A hand-bumped counter fails in the one\n * direction that matters: forgetting to bump it does not cost a recomputation,\n * it silently serves geometry built by different code. Over-invalidating costs\n * seconds of the work this cache exists to avoid; under-invalidating is\n * corruption, so the stamp is deliberately over-eager — a release of this\n * package busts the cache whether or not assembly actually changed.\n *\n * `@semiont/content`'s own version covers our assembly code (`anchorRuns`,\n * `assemblePage`, `mapWordsToItems` — the offset construction IS part of what\n * the cached value means). The engine and its traineddata are read separately\n * because both are pinned with carets and can move without a release here —\n * and different traineddata means different recognized text, which is a\n * difference in the value itself, not merely in how fast it was produced.\n *\n * pdf.js joined at P2b, because the seam did: the record is the finished\n * extraction outcome, so it depends on the native parse — classification,\n * text-layer read, table/form shaping — not just the engine. A parser upgrade\n * is a change in the value, and the entry must miss.\n */\nfunction buildStamp(): string {\n const require = createRequire(import.meta.url);\n const version = (specifier: string): string => {\n try {\n const pkg: unknown = require(specifier);\n return isObject(pkg) && isString(pkg.version) ? pkg.version : 'unknown';\n } catch {\n return 'unknown';\n }\n };\n return `content-${version('../package.json')}`\n + `+pdfjs-${version('pdfjs-dist/package.json')}`\n + `+tesseract-${version('tesseract.js/package.json')}`\n + `+eng-${version('@tesseract.js-data/eng/package.json')}`;\n}\n\nconst STAMP = buildStamp();\n\n/** Pack items into line records. Lossless and order-preserving for any input. */\nexport function encodeLines(items: PdfTextItem[]): CachedLine[] {\n const lines: CachedLine[] = [];\n for (const item of items) {\n const last = lines[lines.length - 1];\n if (last && last.p === item.page && last.y === item.y && last.h === item.height) {\n last.words.push([item.x, item.width, item.start, item.end]);\n } else {\n lines.push({ p: item.page, y: item.y, h: item.height, words: [[item.x, item.width, item.start, item.end]] });\n }\n }\n return lines;\n}\n\n/** The inverse of `encodeLines`. */\nexport function decodeLines(lines: CachedLine[]): PdfTextItem[] {\n const items: PdfTextItem[] = [];\n for (const line of lines) {\n for (const [x, width, start, end] of line.words) {\n items.push({ start, end, page: line.p, x, y: line.y, width, height: line.h });\n }\n }\n return items;\n}\n\nexport interface AnchoredTextStore {\n /**\n * The stored map for this key, or null for any miss. Never throws.\n *\n * The key is the **content checksum of the bytes the map derives from**\n * (PERSIST-ANCHORS decision A): a representation is its bytes, so the\n * checksum is its identity, and geometry derived from one revision of the\n * bytes is unreachable by a reader holding a different revision — by\n * construction, not by invalidation. Callers holding some other handle\n * (a resource id) reach the artifact through an index, not by a second\n * key scheme here.\n */\n read(key: string): Promise<ExtractionOutcome | null>;\n /** Record an extraction outcome under the content checksum of its source\n * bytes. A store that cannot write is still a store. */\n write(key: string, outcome: ExtractionOutcome): Promise<void>;\n /**\n * Every key `read()` would currently HIT — entries under a stale stamp or\n * unreadable files are excluded, exactly as `read()` would exclude them.\n * That equivalence is load-bearing: the reconcile planner treats a listed\n * key as \"artifact present\" and plans re-derivation for the rest\n * (PERSIST-ANCHORS P0, the third drift class), so a key listed here but\n * missed by `read()` would be a permanent loss the diff can never see —\n * the exact shape of the post-engine-upgrade hole this filter closes.\n * One bulk call per reconcile, never a probe per resource. Never throws.\n */\n list(): Promise<string[]>;\n}\n\n/** Narrow a parsed entry, so a truncated or foreign file is a miss, not a crash. */\nfunction isCached(value: unknown): value is CachedAnchoredText {\n if (!isObject(value) || value.v !== 2 || !isString(value.stamp)) return false;\n if (isString(value.declined)) return true;\n if (!isString(value.text) || !isString(value.method) || !isArray(value.lines)) return false;\n return value.lines.every((line) =>\n isObject(line) && isNumber(line.p) && isNumber(line.y) && isNumber(line.h) && isArray(line.words)\n && line.words.every((w) => isArray(w) && w.length === 4 && w.every(isNumber)));\n}\n\n/** A key that could not have come from a checksum (or a legacy hex handle) is\n * refused outright rather than sanitized: a silently stripped key could share\n * a file with a different entry. Rejection replaces the old strip\n * (PERSIST-ANCHORS, *Smaller things*). */\nconst VALID_KEY = /^[A-Za-z0-9_-]+$/;\n\n/**\n * A file-backed store under `dir` — one file per content key, sharded as\n * `{ab}/{cd}/{key}.json` via the same `getShardPath` the event log uses\n * (PERSIST-ANCHORS decision E). Same convention, separate tree: `.semiont/`\n * is the KB's committed system of record; everything here is derived,\n * reclaimable, and never a source of truth.\n *\n * `dir` is the caller's, out of `Project.anchoredTextDir`: this package has no idea\n * which project it is serving. Every failure path is a miss rather than an\n * error, matching the rule extraction already follows for unreadable pages —\n * the cache may make things faster, never make them fail.\n */\nexport function createAnchoredTextStore(dir: string, logger?: Logger): AnchoredTextStore {\n const fileFor = (key: string): string | null => {\n if (!VALID_KEY.test(key)) return null;\n const [ab, cd] = getShardPath(key);\n return path.join(dir, ab, cd, `${key}.json`);\n };\n\n return {\n async read(key) {\n let hit: CachedAnchoredText | null = null;\n try {\n const file = fileFor(key);\n if (file === null) throw new Error('invalid key'); // refused → a miss like any other\n const parsed: unknown = JSON.parse(await fs.promises.readFile(file, 'utf8'));\n if (isCached(parsed) && parsed.stamp === STAMP) hit = parsed;\n } catch {\n hit = null; // absent, unreadable, truncated, or not ours\n }\n // Logged here rather than at the call sites: `prepare-detection` and\n // the smelter both extract, so each would see only its own share of\n // the traffic and the policy would be stated twice. Hit rate is what\n // keeps the Lane 0 decision auditable after the fact.\n logger?.debug('Anchored-text cache', {\n outcome: hit ? 'hit' : 'miss',\n key,\n ...(hit ? ('declined' in hit ? { declined: hit.declined } : { lines: hit.lines.length }) : {}),\n });\n if (!hit) return null;\n if ('declined' in hit) return { declined: hit.declined };\n const { v: _v, stamp: _stamp, lines, text, ...provenance } = hit;\n return { text, items: decodeLines(lines), ...provenance };\n },\n\n async write(key, outcome) {\n const target = fileFor(key);\n if (target === null) {\n logger?.debug('Anchored-text cache: refusing invalid key', { key });\n return; // a store that cannot write is still a store\n }\n // Key order (`v`, `stamp`, first) is load-bearing: `list()` below\n // reads only a prefix of each file and matches the stamp there.\n const entry: CachedAnchoredText = 'declined' in outcome\n ? { v: 2, stamp: STAMP, declined: outcome.declined }\n : (() => {\n const { text, items, ...provenance } = outcome;\n return { v: 2, stamp: STAMP, text, lines: encodeLines(items), ...provenance };\n })();\n // Write-then-rename: a reader never observes a half-written entry,\n // and two writers racing on the same key both produce the same bytes.\n const temp = `${target}.${process.pid}.tmp`;\n try {\n await fs.promises.mkdir(path.dirname(target), { recursive: true });\n await fs.promises.writeFile(temp, JSON.stringify(entry), 'utf8');\n await fs.promises.rename(temp, target);\n } catch {\n await fs.promises.rm(temp, { force: true }).catch(() => {});\n }\n },\n\n async list() {\n // Would-hit keys only (see the interface doc). The stamp check\n // reads a bounded prefix rather than parsing whole entries — an\n // artifact is ~32 KB per scanned page and this runs over every\n // entry at every reconcile. Sound because `write()` above puts\n // `v` and `stamp` first, so the current stamp appears within the\n // first bytes of every entry this store has ever written; a file\n // whose prefix doesn't match is either stale or not ours, and\n // both are misses for `read()` too. Keys round-trip through\n // filenames unchanged because every real key is hex — the same\n // fact that makes `fileFor`'s guard a no-op for them.\n const prefix = JSON.stringify({ v: 2, stamp: STAMP }).slice(0, -1) + ',';\n let rootNames: string[];\n try {\n rootNames = await fs.promises.readdir(dir);\n } catch {\n return []; // no directory yet: nothing has been written\n }\n\n // One-generation sweep (PERSIST-ANCHORS P1): a `.json` at the root\n // is a pre-P1 entry — flat layout, resource-id key, a dead scheme.\n // The rebuild path (P0's third drift class) re-derives anything\n // still needed, which is what makes this delete safe; leaving a\n // generation behind is how the store's size becomes unexplainable.\n // Done here because list() is the one bulk call every reconcile\n // already makes, so the sweep runs exactly when the planner is\n // about to notice what is missing. Best-effort, never throws.\n let swept = 0;\n for (const name of rootNames) {\n if (!name.endsWith('.json')) continue;\n await fs.promises.rm(path.join(dir, name), { force: true }).then(() => { swept += 1; }, () => {});\n }\n if (swept > 0) logger?.info('Anchored-text cache: swept pre-P1 flat entries', { swept });\n\n const keys: string[] = [];\n let sweptInterim = 0;\n for (const ab of rootNames) {\n if (!/^[0-9a-f]{2}$/.test(ab)) continue;\n let cdNames: string[];\n try {\n cdNames = await fs.promises.readdir(path.join(dir, ab));\n } catch {\n continue;\n }\n for (const cd of cdNames) {\n if (!/^[0-9a-f]{2}$/.test(cd)) continue;\n let names: string[];\n try {\n names = await fs.promises.readdir(path.join(dir, ab, cd));\n } catch {\n continue;\n }\n for (const name of names) {\n if (!name.endsWith('.json')) continue;\n // Interim-generation sweep (PERSIST-ANCHORS P1b): a\n // 32-hex basename is a resource-id key — writes that\n // landed sharded between P1a's rekey and P1b's\n // call-site switch. Checksums are 64-hex (SHA-256),\n // so the two generations are disjoint by length.\n // Reaped here for the same reason the flat sweep\n // lives here: one bulk call per reconcile, and never\n // a third scheme lingering silently.\n const base = name.slice(0, -'.json'.length);\n if (/^[0-9a-f]{32}$/.test(base)) {\n await fs.promises.rm(path.join(dir, ab, cd, name), { force: true }).then(() => { sweptInterim += 1; }, () => {});\n continue;\n }\n let handle: fs.promises.FileHandle | null = null;\n try {\n handle = await fs.promises.open(path.join(dir, ab, cd, name), 'r');\n const buf = Buffer.alloc(prefix.length);\n const { bytesRead } = await handle.read(buf, 0, prefix.length, 0);\n if (bytesRead === prefix.length && buf.toString('utf8') === prefix) {\n keys.push(base);\n }\n } catch {\n // unreadable is a miss, matching read()\n } finally {\n await handle?.close().catch(() => {});\n }\n }\n }\n }\n if (sweptInterim > 0) logger?.info('Anchored-text cache: swept interim resource-id entries', { swept: sweptInterim });\n return keys;\n },\n };\n}\n","/**\n * `AnchoredTextStore` over `IContentTransport` — how an out-of-process\n * extraction seam reaches the one real store (PERSIST-ANCHORS P2c).\n *\n * Every cache consumer runs outside the backend — the smelter worker and the\n * detection workers — while the KnowledgeSystem owns the storage. This\n * adapter maps the store contract onto the transport's three\n * checksum-addressed calls, so `ExtractionCache { key, store }` works\n * identically in-process (LocalContentTransport → the store directly) and\n * over the wire (HttpContentTransport → the /anchored-text routes).\n *\n * It honors the store contract's failure rule — the cache may make things\n * faster, never make them fail: a read or list failure is a miss, a write\n * failure is swallowed (debug-logged). Callers that need a write to be LOUD\n * — the re-anchor path, whose artifact IS the job — use the transport's\n * `putAnchoredText` directly, not this adapter.\n */\n\nimport type { IContentTransport, Logger } from '@semiont/core';\nimport type { AnchoredTextStore } from './anchored-text-store';\n\nexport function anchoredTextStoreOverTransport(\n content: IContentTransport,\n logger?: Logger,\n): AnchoredTextStore {\n return {\n async read(key) {\n try {\n return await content.getAnchoredTextByChecksum(key);\n } catch (error) {\n logger?.debug('Anchored-text cache: transport read failed — treating as miss', {\n key,\n reason: error instanceof Error ? error.message : String(error),\n });\n return null;\n }\n },\n\n async write(key, outcome) {\n try {\n await content.putAnchoredText(key, outcome);\n } catch (error) {\n logger?.debug('Anchored-text cache: transport write failed — entry not stored', {\n key,\n reason: error instanceof Error ? error.message : String(error),\n });\n }\n },\n\n async list() {\n try {\n return await content.listAnchoredTextKeys();\n } catch (error) {\n logger?.debug('Anchored-text cache: transport list failed — treating as empty', {\n reason: error instanceof Error ? error.message : String(error),\n });\n return [];\n }\n },\n };\n}\n"],"mappings":"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","names":["isObject","path","require","pdfjs","isObject","isNumber","isString","isArray","isArray","isNumber","isString","isObject","createRequire","isObject","isString","pdfjs","isObject","ocr","confidence","fs","path","createRequire","isObject","isString","isNumber","isArray","require"]}
1
+ {"version":3,"sources":["../src/working-tree-store.ts","../src/checksum.ts","../src/storage-uri.ts","../src/content-extractor.ts","../src/pdf-extractor.ts","../src/extract-pdf-text-layer.ts","../src/pdfjs-assets.ts","../src/pdf-tables.ts","../src/pdf-page-images.ts","../src/png-encode.ts","../src/ocr.ts","../src/ocr-geometry.ts","../src/anchored-text-store.ts","../src/anchored-text-store-adapter.ts"],"sourcesContent":["/**\n * WorkingTreeStore - Manages files in the project working tree\n *\n * Unlike the old content-addressed RepresentationStore, this store treats\n * the working tree (project root) as the source of truth for file content.\n * Resources are identified by their file:// URI, which is stable across\n * content changes and moves (tracked by events).\n *\n * Two write paths:\n * - store(content, storageUri): Write bytes to disk (API/GUI/AI path).\n * Used when the file does not yet exist and the caller provides content.\n * - register(storageUri, expectedChecksum?): Read an existing file and\n * return its metadata (CLI path). The file is already on disk; we just\n * verify and record it. If expectedChecksum is provided, throws on mismatch.\n *\n * Storage layout:\n * {projectRoot}/{path-from-uri}\n *\n * For example, storageUri \"file://docs/overview.md\" resolves to\n * {projectRoot}/docs/overview.md\n */\n\nimport { promises as fs } from 'fs';\nimport { execFileSync } from 'child_process';\nimport path from 'path';\nimport type { SemiontProject } from '@semiont/core/node';\nimport type { Logger } from '@semiont/core';\nimport { calculateChecksum, verifyChecksum } from './checksum';\n\n/**\n * Result of store() or register()\n */\nexport interface StoredResource {\n storageUri: string; // file:// URI (e.g. \"file://docs/overview.md\")\n checksum: string; // SHA-256 hex of content\n byteSize: number; // Size in bytes\n created: string; // ISO 8601 timestamp\n}\n\n/**\n * Manages files in the project working tree\n */\nexport class WorkingTreeStore {\n private projectRoot: string;\n private gitSync: boolean;\n private logger?: Logger;\n\n constructor(project: SemiontProject, logger?: Logger) {\n this.projectRoot = project.root;\n this.gitSync = project.gitSync;\n this.logger = logger;\n }\n\n private shouldRunGit(noGit?: boolean): boolean {\n return this.gitSync && !noGit;\n }\n\n /**\n * Write content to disk at the location indicated by storageUri.\n *\n * API/GUI/AI path: caller provides bytes; file may not yet exist.\n *\n * @param content - Raw bytes to write\n * @param storageUri - file:// URI (e.g. \"file://docs/overview.md\")\n * @returns Stored resource metadata\n */\n async store(content: Buffer, storageUri: string, options?: { noGit?: boolean }): Promise<StoredResource> {\n const filePath = this.resolveUri(storageUri);\n const checksum = calculateChecksum(content);\n\n this.logger?.debug('Storing resource', { storageUri, byteSize: content.length });\n\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n await fs.writeFile(filePath, content);\n\n if (this.shouldRunGit(options?.noGit)) {\n execFileSync('git', ['add', filePath], { cwd: this.projectRoot });\n }\n\n this.logger?.info('Resource stored', { storageUri, checksum, byteSize: content.length });\n\n return {\n storageUri,\n checksum,\n byteSize: content.length,\n created: new Date().toISOString(),\n };\n }\n\n /**\n * Read an existing file and return its metadata.\n *\n * CLI path: the file is already on disk. We read it to compute the checksum.\n * If expectedChecksum is provided, throws ChecksumMismatchError on mismatch.\n *\n * @param storageUri - file:// URI (e.g. \"file://docs/overview.md\")\n * @param expectedChecksum - Optional SHA-256 to verify against\n * @returns Stored resource metadata\n * @throws ChecksumMismatchError if expectedChecksum is provided and does not match\n * @throws Error if file does not exist\n */\n async register(storageUri: string, expectedChecksum?: string, options?: { noGit?: boolean }): Promise<StoredResource> {\n const filePath = this.resolveUri(storageUri);\n\n this.logger?.debug('Registering resource', { storageUri });\n\n const content = await fs.readFile(filePath);\n const checksum = calculateChecksum(content);\n\n if (expectedChecksum !== undefined && !verifyChecksum(content, expectedChecksum)) {\n throw new ChecksumMismatchError(storageUri, expectedChecksum, checksum);\n }\n\n if (this.shouldRunGit(options?.noGit)) {\n execFileSync('git', ['add', filePath], { cwd: this.projectRoot });\n }\n\n this.logger?.info('Resource registered', { storageUri, checksum, byteSize: content.length });\n\n return {\n storageUri,\n checksum,\n byteSize: content.length,\n created: new Date().toISOString(),\n };\n }\n\n /**\n * Read file content by URI.\n *\n * @param storageUri - file:// URI\n * @returns Raw bytes\n */\n async retrieve(storageUri: string): Promise<Buffer> {\n const filePath = this.resolveUri(storageUri);\n try {\n return await fs.readFile(filePath);\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n throw new Error(`Resource not found: ${storageUri}`);\n }\n throw error;\n }\n }\n\n /**\n * Move a file from one URI to another.\n *\n * If .git/ exists in the project root and noGit is not set, runs `git mv`.\n * Otherwise (no .git/ or noGit: true), runs fs.rename.\n *\n * @param fromUri - Current file:// URI\n * @param toUri - New file:// URI\n * @param options.noGit - Skip git mv even if .git/ is present\n */\n async move(fromUri: string, toUri: string, options?: { noGit?: boolean }): Promise<void> {\n const fromPath = this.resolveUri(fromUri);\n const toPath = this.resolveUri(toUri);\n\n this.logger?.debug('Moving resource', { fromUri, toUri });\n\n await fs.mkdir(path.dirname(toPath), { recursive: true });\n\n if (this.shouldRunGit(options?.noGit)) {\n // git mv handles both the filesystem rename and the index update\n execFileSync('git', ['mv', fromPath, toPath], { cwd: this.projectRoot });\n } else {\n await fs.rename(fromPath, toPath);\n }\n\n this.logger?.info('Resource moved', { fromUri, toUri });\n }\n\n /**\n * Remove a file from the working tree.\n *\n * If .git/ exists and noGit is not set:\n * - keepFile false (default): runs `git rm` (removes from index and disk)\n * - keepFile true: runs `git rm --cached` (removes from index only, file stays on disk)\n * If no .git/ or noGit: true:\n * - keepFile false: runs fs.unlink\n * - keepFile true: no-op on filesystem\n *\n * @param storageUri - file:// URI\n * @param options.noGit - Skip git rm even if .git/ is present\n * @param options.keepFile - Remove from git index only; leave file on disk\n */\n async remove(storageUri: string, options?: { noGit?: boolean; keepFile?: boolean }): Promise<void> {\n const filePath = this.resolveUri(storageUri);\n const keepFile = options?.keepFile ?? false;\n\n this.logger?.debug('Removing resource', { storageUri, keepFile });\n\n const useGit = this.shouldRunGit(options?.noGit);\n\n if (useGit) {\n const gitArgs = keepFile\n ? ['rm', '--cached', filePath]\n : ['rm', filePath];\n execFileSync('git', gitArgs, { cwd: this.projectRoot });\n this.logger?.info('Resource removed', { storageUri, keepFile, git: true });\n return;\n }\n\n if (keepFile) {\n this.logger?.info('Resource removed from index (file kept on disk)', { storageUri });\n return;\n }\n\n try {\n await fs.unlink(filePath);\n this.logger?.info('Resource removed', { storageUri });\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n this.logger?.warn('Resource file already absent', { storageUri });\n return;\n }\n throw error;\n }\n }\n\n /**\n * Convert a file:// URI to an absolute filesystem path.\n *\n * \"file://docs/overview.md\" → \"{projectRoot}/docs/overview.md\"\n *\n * @param storageUri - file:// URI\n * @returns Absolute path\n */\n resolveUri(storageUri: string): string {\n if (!storageUri.startsWith('file://')) {\n throw new Error(`Invalid storage URI (must start with file://): ${storageUri}`);\n }\n const relativePath = storageUri.slice('file://'.length);\n return path.join(this.projectRoot, relativePath);\n }\n}\n\n/**\n * Thrown when a registered file's checksum does not match the expected value.\n * This indicates the file on disk differs from what was recorded (e.g. modified\n * after staging, or wrong file path provided).\n */\nexport class ChecksumMismatchError extends Error {\n constructor(\n readonly storageUri: string,\n readonly expected: string,\n readonly actual: string,\n ) {\n super(\n `Checksum mismatch for ${storageUri}: expected ${expected.slice(0, 8)}... but got ${actual.slice(0, 8)}...\\n` +\n `The file on disk differs from the recorded checksum. Has it been modified since staging?`\n );\n this.name = 'ChecksumMismatchError';\n }\n}\n","/**\n * Checksum utilities for content verification\n */\n\nimport { createHash } from 'crypto';\n\n/**\n * Calculate SHA-256 checksum of content\n * @param content The content to hash\n * @returns Hex-encoded SHA-256 hash\n */\nexport function calculateChecksum(content: string | Buffer): string {\n const hash = createHash('sha256');\n hash.update(content);\n return hash.digest('hex');\n}\n\n/**\n * Verify content against a checksum\n * @param content The content to verify\n * @param checksum The expected checksum\n * @returns True if content matches checksum\n */\nexport function verifyChecksum(content: string | Buffer, checksum: string): boolean {\n return calculateChecksum(content) === checksum;\n}\n","/**\n * Storage URI Derivation\n *\n * Builds the file:// URI a resource lives at in the working tree from its\n * name and validated media type. Extensions come from the media-type\n * registry in @semiont/core; formats are validated upstream at the\n * create/yield boundary, so the lookup is strict — no fallback.\n */\n\nimport { MEDIA_TYPES, type SupportedMediaType } from '@semiont/core';\n\n/**\n * Derive a file:// storage URI from a resource name and media type.\n *\n * The name is lowercased, runs of non-alphanumeric characters collapse to\n * single hyphens, and leading/trailing hyphens are stripped.\n *\n * @example\n * deriveStorageUri(\"My Document\", \"text/markdown\") // => \"file://my-document.md\"\n */\nexport function deriveStorageUri(name: string, format: SupportedMediaType): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n return `file://${slug}${MEDIA_TYPES[format].extension}`;\n}\n","/**\n * ContentExtractor — strategy-keyed text extraction for embedding.\n *\n * The registry is keyed by `TextExtraction` from `@semiont/core` — the\n * media-type registry's dispatch vocabulary — never by a second media-type\n * list (SMELTER-MEDIA-TYPES.md, Design §1): there is exactly one media-type\n * table in the system, and this registry consumes it. The Smelter resolves\n * `textExtractionOf(contentType)` and looks the extractor up by strategy; a\n * `null` slot means decline (settle skipped, reason 'no-extractor').\n *\n * Extraction is ephemeral: `extract` runs at read time, its output feeds the\n * chunker, and is discarded — no stored derived representation. Annotations\n * anchor to native geometry (`items`), never to extracted-text offsets, so\n * re-extraction can never break an anchor.\n */\n\nimport { decodeRepresentation, type TextExtraction, type PdfTextItem } from '@semiont/core';\nimport type { AnchoredTextStore } from './anchored-text-store';\nimport { pdfExtractor } from './pdf-extractor';\n\nexport interface ExtractedText {\n /** Discriminant — mirrors the wire member (WIRE-UNION-DISCRIMINANTS P5c/D6). */\n kind: 'extracted';\n /** Reading-order plain text, ready for the chunker. */\n text: string;\n /**\n * Positioned text runs indexing `text`, for callers that anchor; absent for\n * pure text, where character offsets are the anchor. Named `items` to match\n * `AnchoredText`/`PdfTextLayer` — one concept, one name, and no collision\n * with the OCR engine's own \"blocks\" (which are page regions, not runs).\n */\n items?: PdfTextItem[];\n method: 'text-passthrough' | 'pdf-text-layer' | 'table' | 'form' | 'ocr';\n pdfClass?: 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G';\n /**\n * How well the engine read the pixels, when any of this text came from OCR.\n *\n * Extraction quality, deliberately NOT anchor confidence: the two answer\n * different questions. `AnchorConfidence` asks whether the renderer\n * relocated a stored span in the current text, and for a PDF the answer is\n * always \"exactly\" — the viewrect is absolute. This asks whether the glyphs\n * under that box were read correctly, which no client can recompute.\n * Reported for operators rather than stored on annotations, following the\n * existing rule that anchor-audit detail belongs in logs.\n */\n ocrConfidence?: {\n /** Mean per-word confidence, 0–100. */\n mean: number;\n /** Words the engine was unsure of — the number worth acting on. */\n lowConfidenceWords: number;\n totalWords: number;\n };\n /**\n * 1-indexed pages this extraction could not read — present only when a\n * document is partially covered (class C). Naming the gap is the point:\n * without it a hybrid document embeds its native pages and says nothing\n * about the rest, so coverage silently overstates what search can see.\n * This is the work list OCR consumes.\n */\n unreadPages?: number[];\n}\n\n/**\n * A named decline — an extractor that ran and decided it cannot yield text\n * says why, so the settled signal can carry the class reason (a bare null\n * could not name its class; SMELTER-MEDIA-TYPES Phase 0 log, note a).\n */\nexport interface ExtractionDecline {\n /** Discriminant — mirrors the wire member (WIRE-UNION-DISCRIMINANTS P5c/D6). */\n kind: 'declined';\n declined: 'no-text-layer' | 'encrypted' | 'corrupt' | 'too-large';\n}\n\n/**\n * Where a strategy may reuse an earlier recognition, and under what key.\n *\n * The caller supplies the key, and derives it from the bytes it actually\n * holds — `calculateChecksum` over the same Buffer it passes to `extract()` —\n * never from a descriptor's claim. A catalog-derived key can race a byte\n * change (bytes fetched at one moment, descriptor read at another) and file\n * or read geometry under an identity that does not describe the bytes being\n * extracted. The write path made recompute-over-claim the rule\n * (PERSIST-ANCHORS P1b); readers mirror it (P1c). One SHA-256 over bytes\n * already in memory is noise against the engine pass a hit avoids.\n *\n * Optional throughout: a caller that passes nothing extracts uncached and is\n * unaffected. The seam is `extract()` itself (PERSIST-ANCHORS D1/P2b): a hit\n * returns the FINISHED outcome — classification, geometry, provenance, or a\n * named decline — so neither the native parse nor the engine runs. Every\n * geometry-yielding extraction produces an entry, native documents included;\n * the 'decode' strategy ignores the cache (no geometry, nothing expensive).\n */\nexport interface ExtractionCache {\n key: string;\n store: AnchoredTextStore;\n}\n\nexport interface ContentExtractor {\n /**\n * Whether this strategy's extractions carry positioned runs (`items`) — the\n * geometry an anchored-text artifact is made of. Declared, not probed:\n * the reconcile planner must know \"should an artifact exist?\" without\n * running the extractor (PERSIST-ANCHORS P0, the third drift class), and\n * the declaration keeps the planner's gate and the live fetch's behavior\n * twins by construction. Text strategies anchor by character offset and\n * declare false.\n */\n yieldsGeometry: boolean;\n\n /**\n * Extract embeddable/annotatable text, or decline with the class reason\n * (scanned-without-OCR, encrypted, corrupt). The caller skips embedding\n * and settles skipped with that reason.\n */\n extract(content: Buffer, mediaType: string, cache?: ExtractionCache): Promise<ExtractedText | ExtractionDecline>;\n}\n\n/** Charset-aware decode of textual bytes — the pre-registry behavior, now\n * scoped as the 'decode' strategy's extractor. Never declines: any byte\n * sequence decodes to *some* string; emptiness is the caller's call. */\nconst passthroughExtractor: ContentExtractor = {\n yieldsGeometry: false,\n async extract(content, mediaType) {\n return { kind: 'extracted', text: decodeRepresentation(content, mediaType), method: 'text-passthrough' };\n },\n};\n\n/**\n * Strategy → extractor. A `null` slot is a decline: the strategy names a\n * capability nothing currently provides ('none' permanently).\n */\nexport const EXTRACTORS: Record<TextExtraction, ContentExtractor | null> = {\n 'decode': passthroughExtractor,\n 'pdf-text-layer': pdfExtractor,\n 'none': null,\n};\n","/**\n * PDF extractor — the 'pdf-text-layer' strategy (SMELTER-MEDIA-TYPES).\n *\n * Wraps the shared `extractPdfTextLayer` reader (detection's other consumer)\n * and turns a PDF into text plus the geometry that indexes it, by class:\n *\n * A native text layer → read directly\n * B scanned → read the page pixels by OCR\n * C hybrid → both, with any page still unread reported\n * D tables → grid pages rewritten as markdown rows\n * E forms → AcroForm values folded in, anchored to widgets\n * F/G encrypted, corrupt → declined by name, from the parser error\n *\n * Everything runs inline. OCR was originally planned off the hot path, but\n * the Smelter's lanes are per-resource and concurrent, so a slow page delays\n * only its own resource — see SMELTER-MEDIA-TYPES Design §4 (revised).\n */\n\nimport { isObject, type PdfTextItem } from '@semiont/core';\nimport { extractPdfTextLayer } from './extract-pdf-text-layer';\nimport type { ContentExtractor, ExtractedText, ExtractionDecline } from './content-extractor';\nimport type { PdfTextLayer } from './pdf-text-layer';\nimport { detectTable, renderTable } from './pdf-tables';\nimport { extractPageImages } from './pdf-page-images';\nimport { recognizeImages } from './ocr';\nimport { mapWordsToItems } from './ocr-geometry';\n\n\n/** One OCR'd page: its text, and word geometry with page-local offsets. */\ninterface OcrPageResult {\n text: string;\n items: PdfTextItem[];\n /** Per-word confidences, kept only long enough to summarize. */\n confidences: number[];\n}\n\n/**\n * Largest PDF this will attempt, in bytes.\n *\n * A PDF is a compressed container, so input size bounds nothing on its own —\n * but it is the one number available before the parser touches the file, and\n * refusing here means a hostile or pathological document never gets to expand\n * inside pdf.js. Chosen to sit above real corpora (a few hundred pages of\n * scanned FOIA material runs tens of megabytes) while still being a ceiling.\n *\n * A starting point, not a measured optimum — revisit against a real corpus\n * (SMELTER-MEDIA-TYPES, live-testing follow-up). The per-image budget in\n * `pdf-page-images` guards the decoded side, which is where the unbounded\n * growth actually lives.\n */\nexport const MAX_PDF_BYTES = 200 * 1024 * 1024;\n\n/** Whether a document is small enough to attempt. Exported because the\n * threshold is a judgement, and judgements deserve tests that do not have to\n * materialize two hundred megabytes to ask the question. */\nexport function withinByteBudget(bytes: number): boolean {\n return Number.isFinite(bytes) && bytes >= 0 && bytes <= MAX_PDF_BYTES;\n}\n\n/** Words below this are worth an operator's attention. Tesseract reports\n * 0–100; readable text on a clean scan sits well above this. */\nconst LOW_CONFIDENCE = 60;\n\nfunction summarize(confidences: number[]): ExtractedText['ocrConfidence'] {\n if (confidences.length === 0) return undefined;\n const total = confidences.reduce((sum, c) => sum + c, 0);\n return {\n mean: Math.round((total / confidences.length) * 10) / 10,\n lowConfidenceWords: confidences.filter((c) => c < LOW_CONFIDENCE).length,\n totalWords: confidences.length,\n };\n}\n\n/**\n * Read the pages that have no text layer by OCR'ing their pixels. Returns\n * results only for pages that yielded text; a page absent from the map stayed\n * unread. Pages with no extractable image never reach the engine.\n *\n * Each word is anchored through the matrix that placed its image, so a scanned\n * page ends up carrying the same kind of geometry a native one does.\n */\nasync function ocrPages(\n content: Buffer,\n pageNumbers?: number[],\n): Promise<OcrPageResult> {\n // Pure recognition since PERSIST-ANCHORS P2b: the caching seam lives at\n // `extract()`, which stores and serves the FINISHED outcome. This function\n // neither consults nor writes the store — it reads pixels.\n const imagesByPage = await extractPageImages(content, pageNumbers);\n if (imagesByPage.size === 0) return { text: '', items: [], confidences: [] };\n\n // One batch for the whole document: worker startup dominates per-page cost.\n const pages = [...imagesByPage.keys()].sort((a, b) => a - b);\n const batch = pages.flatMap((page) => imagesByPage.get(page)!.map((image) => image.png));\n const recognized = await recognizeImages(batch);\n\n const byPage = new Map<number, OcrPageResult>();\n let cursor = 0;\n for (const page of pages) {\n const images = imagesByPage.get(page)!;\n let text = '';\n const items: PdfTextItem[] = [];\n const confidences: number[] = [];\n for (const image of images) {\n const result = recognized[cursor++];\n if (!result?.text.trim()) continue;\n if (text) text += '\\n';\n items.push(...mapWordsToItems(result.words, image, page, text.length));\n confidences.push(...result.words.map((word) => word.confidence));\n text += result.text;\n }\n if (text) byPage.set(page, { text, items, confidences });\n }\n\n // Joined at base 0 — the document's own coordinates. Class C shifts by the\n // native text length at its call site.\n return joinPages(byPage, 0);\n}\n\n/**\n * Recovered pages in page order as one block of text, with every word's\n * offsets shifted to where its page actually lands. `baseOffset` is where this\n * block begins in the document being assembled.\n */\nfunction joinPages(byPage: Map<number, OcrPageResult>, baseOffset: number): OcrPageResult {\n let text = '';\n const items: PdfTextItem[] = [];\n const confidences: number[] = [];\n for (const [, page] of [...byPage.entries()].sort((a, b) => a[0] - b[0])) {\n if (text) text += '\\n\\n';\n const shift = baseOffset + text.length;\n for (const item of page.items) {\n items.push({ ...item, start: item.start + shift, end: item.end + shift });\n }\n confidences.push(...page.confidences);\n text += page.text;\n }\n return { text, items, confidences };\n}\n\n/**\n * pdf.js signals a password-protected document with PasswordException.\n * Matched by name, not instanceof — pdf.js exception classes descend from\n * its own BaseException, not Error. Everything else the parser throws is\n * class G.\n */\nexport function classifyPdfError(error: unknown): 'encrypted' | 'corrupt' {\n return isObject(error) && error.name === 'PasswordException' ? 'encrypted' : 'corrupt';\n}\n\n/**\n * Class E — fold filled AcroForm values into the embedding text.\n *\n * A form's answers live in the form dictionary, not the drawn page, so a\n * naive text-layer read returns the blank labels and loses every value.\n * Each value is appended as a `name: value` line and anchored by its widget\n * rectangle, so `items` stays a complete geometry index of `text`.\n */\nfunction foldFormFields(layer: PdfTextLayer): ExtractedText {\n let text = layer.text;\n const items: PdfTextItem[] = [...layer.items];\n for (const field of layer.fields) {\n const start = text.length + `${field.name}: `.length;\n text += `${field.name}: ${field.value}\\n`;\n items.push({\n start,\n end: start + field.value.length,\n page: field.page,\n x: field.x,\n y: field.y,\n width: field.width,\n height: field.height,\n });\n }\n return { kind: 'extracted', text, items, method: 'form', pdfClass: 'E' };\n}\n\n/**\n * Class D — rewrite grid pages as markdown, keep every other page verbatim.\n *\n * Returns null when no page is a table, so the caller falls back to class A.\n * Pages are shaped independently: the common report — prose sections around\n * an outcome table — gets row-coherent tables without disturbing its prose.\n */\nfunction shapeTables(layer: PdfTextLayer): ExtractedText | null {\n const pages = layer.pages.map((page) => {\n const pageItems = layer.items.filter((item) => item.page === page.pageNumber);\n return { page, pageItems, table: detectTable(pageItems, layer.text) };\n });\n if (!pages.some((p) => p.table)) return null;\n\n let text = '';\n const items: PdfTextItem[] = [];\n for (const { page, pageItems, table } of pages) {\n if (table) {\n const rendered = renderTable(table, page.pageNumber, text.length);\n text += rendered.text;\n items.push(...rendered.items);\n } else {\n // Verbatim page: copy its slice and shift its runs' offsets to match.\n const shift = text.length - page.textStart;\n text += layer.text.slice(page.textStart, page.textEnd);\n for (const item of pageItems) {\n items.push({ ...item, start: item.start + shift, end: item.end + shift });\n }\n }\n }\n return { kind: 'extracted', text, items, method: 'table', pdfClass: 'D' };\n}\n\nexport const pdfExtractor: ContentExtractor = {\n // Every non-declined PDF extraction carries positioned runs — native text\n // layers and OCR both anchor by page geometry.\n yieldsGeometry: true,\n async extract(content, _mediaType, cache) {\n // The seam (PERSIST-ANCHORS D1/P2b): consult the store for the FINISHED\n // outcome before anything runs — byte gate, native parse, image decode\n // and OCR are all part of the stored answer, classification included.\n // The pre-P2b seam skipped only Tesseract, on the argument that the\n // text-layer parse \"has to run either way\" — true on a miss, false on a\n // hit. A hit is returned WHOLE, which is sound because the outcome is a\n // pure function of the bytes, the key IS the bytes' identity (the\n // caller's producer-supplied checksum — P1b/P1c), and STAMP covers the\n // code that did the deriving. Declines are first-class hits: \"we read\n // this and there was nothing\" costs a full recognition pass to discover,\n // so the negative is precisely the result worth keeping.\n const hit = await cache?.store.read(cache.key);\n if (hit) return hit;\n\n const outcome = await extractPdf(content);\n\n // Store failures stay silent — the store may make things faster, never\n // make them fail. The path that must insist on a write is the smelter's\n // re-anchor publish (P0), not this seam.\n if (cache) {\n if (outcome.kind === 'declined') await cache.store.write(cache.key, outcome);\n else if (outcome.items) await cache.store.write(cache.key, { ...outcome, items: outcome.items });\n }\n return outcome;\n },\n};\n\n/** The uncached pipeline: classify, shape, and read the document. */\nasync function extractPdf(content: Buffer): Promise<ExtractedText | ExtractionDecline> {\n // Before the parser sees it: everything downstream — parse, image decode,\n // OCR — expands from these bytes, so this is the only gate that costs\n // nothing to enforce.\n if (!withinByteBudget(content.length)) return { kind: 'declined', declined: 'too-large' };\n\n let layer;\n try {\n layer = await extractPdfTextLayer(content);\n } catch (error) {\n return { kind: 'declined', declined: classifyPdfError(error) };\n }\n // Class B — no text operators anywhere: the characters exist only as\n // pixels, so read them. 'no-text-layer' now means OCR genuinely came up\n // empty, not that we never tried.\n if (!layer) {\n const ocr = await ocrPages(content);\n if (!ocr.text) return { kind: 'declined', declined: 'no-text-layer' };\n const confidence = summarize(ocr.confidences);\n return {\n kind: 'extracted',\n text: ocr.text,\n items: ocr.items,\n method: 'ocr',\n pdfClass: 'B',\n ...(confidence ? { ocrConfidence: confidence } : {}),\n };\n }\n\n // One class per document, so a filled form outranks a grid: its values\n // are content that exists nowhere else, while a table's cells are at\n // worst reordered.\n const shaped = layer.fields.length > 0\n ? foldFormFields(layer)\n : shapeTables(layer)\n ?? { kind: 'extracted' as const, text: layer.text, items: layer.items, method: 'pdf-text-layer' as const, pdfClass: 'A' as const };\n\n // A page with no text-showing operators is scanned: its characters exist\n // only as pixels. Report those pages rather than dropping them silently —\n // the document embeds what it can now, and this is the list OCR works\n // from. 'C' (hybrid) replaces the plain-prose label only; a form or table\n // keeps its own class, and carries the gap just the same.\n const unreadPages = layer.pages.filter((page) => !page.hasTextLayer).map((page) => page.pageNumber);\n if (unreadPages.length === 0) return shaped;\n\n // Class C — read the scanned pages and append what OCR recovers. Appended\n // rather than spliced into reading order, so the items already computed\n // for the native pages keep pointing at the right characters; OCR text\n // carries no geometry of its own this phase (mapping pixel boxes back to\n // page points needs the image's placement transform — #739's critical\n // path, not embedding's).\n const recovered = await ocrPages(content, unreadPages);\n const readPages = new Set(recovered.items.map((item) => item.page));\n const stillUnread = unreadPages.filter((page) => !readPages.has(page));\n const hybridClass = shaped.pdfClass === 'A' ? 'C' as const : shaped.pdfClass;\n if (!recovered.text) {\n return { ...shaped, unreadPages: stillUnread, pdfClass: hybridClass };\n }\n // Appended, so the native pages' items keep pointing at the right\n // characters; the OCR'd words are offset to where they actually land.\n const shift = shaped.text.length;\n const ocr: OcrPageResult = {\n text: recovered.text,\n items: recovered.items.map((item) => ({ ...item, start: item.start + shift, end: item.end + shift })),\n confidences: recovered.confidences,\n };\n const confidence = summarize(ocr.confidences);\n return {\n ...shaped,\n text: `${shaped.text}${ocr.text}\\n`,\n items: [...(shaped.items ?? []), ...ocr.items],\n method: 'ocr',\n pdfClass: hybridClass,\n ...(confidence ? { ocrConfidence: confidence } : {}),\n ...(stillUnread.length > 0 ? { unreadPages: stillUnread } : {}),\n };\n}\n","/**\n * PDF Text Layer Extraction\n *\n * Extracts positioned text from native, non-scanned PDFs using pdfjs-dist.\n * Returns null for scanned/image-only PDFs (no text items).\n *\n * Coordinates are in PDF point space, originating from the bottom-left.\n * The Y-flip to canvas pixels happens downstream.\n */\n\nimport * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs';\nimport { STANDARD_FONT_DATA_URL } from './pdfjs-assets';\nimport { isObject, isString, isNumber, isArray, anchorRuns, isTextRun, type PdfTextItem } from '@semiont/core';\nimport type { PdfTextLayer, PdfPageInfo, PdfFormField } from './pdf-text-layer';\n\n/**\n * One entry from pdf.js's `getFieldObjects()` map, narrowed to a filled\n * field. The API types entries as bare `Object`, so every field is checked:\n * group entries (a parent with `kidIds`) carry `page: -1` and no value and\n * are rejected here, leaving the widgets that actually hold content.\n */\nfunction toFormField(entry: unknown): PdfFormField | null {\n if (!isObject(entry)) return null;\n const { name, value, page, rect } = entry;\n if (!isString(name) || !isString(value) || !value.trim()) return null;\n if (!isNumber(page) || page < 0) return null;\n if (!isArray(rect) || rect.length < 4 || !rect.every(isNumber)) return null;\n const [x1, y1, x2, y2] = rect as [number, number, number, number];\n return {\n name,\n value: value.trim(),\n page: page + 1, // pdf.js reports 0-indexed; PdfTextItem is 1-indexed\n x: Math.min(x1, x2),\n y: Math.min(y1, y2),\n width: Math.abs(x2 - x1),\n height: Math.abs(y2 - y1),\n };\n}\n\n/**\n * Filled AcroForm values, one per field name (first filled widget wins, so a\n * radio group contributes a single answer). Returns [] for a document with\n * no form. XFA forms are out of scope: whatever their AcroForm shell exposes\n * is read the same way, and anything else simply yields no fields.\n */\nasync function readFormFields(doc: pdfjs.PDFDocumentProxy): Promise<PdfFormField[]> {\n const fieldObjects = await doc.getFieldObjects();\n if (!fieldObjects) return [];\n const byName = new Map<string, PdfFormField>();\n for (const entries of Object.values(fieldObjects)) {\n if (!isArray(entries)) continue;\n for (const entry of entries) {\n const field = toFormField(entry);\n if (field && !byName.has(field.name)) byName.set(field.name, field);\n }\n }\n return [...byName.values()];\n}\n\nexport async function extractPdfTextLayer(\n bytes: Uint8Array | Buffer\n): Promise<PdfTextLayer | null> {\n // A private copy, for two pdf.js contracts at once: it refuses Node\n // Buffers outright (\"provide binary data as Uint8Array\"), and it CONSUMES\n // the array it is given — the underlying ArrayBuffer is transferred and\n // detached, which would silently zero the caller's bytes. Callers keep\n // their bytes; pdf.js gets its own.\n const data = new Uint8Array(bytes);\n // pdf.js v5 removed the isEvalSupported option; this path only calls\n // getTextContent (no rendering / no PDF functions).\n const loadingTask = pdfjs.getDocument({ data, standardFontDataUrl: STANDARD_FONT_DATA_URL });\n\n try {\n // Inside the try so the finally's destroy() also runs when the\n // parse rejects (encrypted/corrupt input — the extractor's decline\n // path classifies that throw).\n const doc = await loadingTask.promise;\n const pages: PdfPageInfo[] = [];\n const items: PdfTextItem[] = [];\n let text = '';\n\n for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {\n const page = await doc.getPage(pageNum);\n const viewport = page.getViewport({ scale: 1.0 });\n const content = await page.getTextContent(); // all text items on the page\n const pageTextStart = text.length;\n\n // `anchorRuns` owns the offset and separator convention; the\n // browser canvas builds its page the same way, so a rectangle\n // quotes identically whichever side captured it. Marked-content\n // items (no `str`) are filtered here, at the pdf.js boundary —\n // core stays free of pdfjs-dist.\n const page1 = anchorRuns(content.items.filter(isTextRun), pageNum);\n\n // Offsets come back page-local; shift them into the document text.\n for (const item of page1.items) {\n items.push({ ...item, start: item.start + pageTextStart, end: item.end + pageTextStart });\n }\n text += page1.text;\n text += '\\n'; // page break\n\n pages.push({\n pageNumber: pageNum,\n widthPt: viewport.width,\n heightPt: viewport.height,\n textStart: pageTextStart,\n textEnd: text.length,\n hasTextLayer: page1.items.length > 0,\n });\n }\n\n // A document with no drawn text is a scanned page (class B) even when\n // it carries an AcroForm — form values augment a text layer, they do\n // not substitute for one. Keeping this condition on text items alone\n // also keeps the reader's null contract stable for detection.\n if (!pages.some((page) => page.hasTextLayer)) return null;\n\n return { pages, text, items, fields: await readFormFields(doc) };\n } finally {\n // Release the pdf.js document — Phase 2 runs this in a long-lived worker\n // pool. pdf.js 6.0 removed PDFDocumentProxy.destroy(); teardown moved to\n // PDFDocumentLoadingTask.destroy().\n await loadingTask.destroy();\n }\n}\n","/**\n * Where pdf.js finds the asset bundles it does not carry in its main build.\n *\n * pdf.js ships the Standard 14 font programs (Foxit substitutes for Helvetica,\n * Times, Courier, Symbol, ZapfDingbats) as separate `.pfb` files rather than in\n * `pdf.mjs`. Without a `standardFontDataUrl` it cannot load them, and every\n * document that references a standard font logs\n *\n * Warning: UnknownErrorException: Ensure that the `standardFontDataUrl` API\n * parameter is provided.\n *\n * once per font per document — 66 lines on a single 28-page PDF, drowning real\n * output. That noise is the reason to fix it; text extraction itself was never\n * affected, because `getTextContent()` reads the content stream and the font's\n * encoding, not its glyph outlines.\n *\n * Resolved through `require.resolve` rather than a path relative to this file:\n * the built `dist/` sits at a different depth than `src/`, and npm may hoist\n * `pdfjs-dist` to the workspace root or nest it under this package. Asking the\n * resolver is the only form that is correct in all of those, including inside\n * the service images where the tree is installed fresh.\n *\n * The trailing slash is required — pdf.js concatenates the filename onto this\n * string.\n */\n\nimport { createRequire } from 'module';\nimport path from 'path';\n\nconst require = createRequire(import.meta.url);\n\nexport const STANDARD_FONT_DATA_URL =\n `${path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'standard_fonts')}${path.sep}`;\n","/**\n * Table reconstruction from PDF text-layer geometry (SMELTER-MEDIA-TYPES\n * class D).\n *\n * A PDF has no table structure — only positioned text runs. Read in reading\n * order a grid's cells interleave, so a row's values scatter across chunks\n * and semantic recall over an outcome table returns nothing useful. This\n * module recovers the grid from the geometry the reader already carries\n * (`PdfTextItem.x/y/width/height`), then renders markdown rows so a row's\n * cells stay adjacent for the shared chunker. No new dependency: the\n * clustering is the same arithmetic a table library would do, over data we\n * already have.\n *\n * PRECISION OVER RECALL. A false positive scrambles prose into a fake table;\n * a false negative merely falls back to class A, which is Phase 1 behavior.\n * So detection demands a strict, regular grid — every row the same cell\n * count, every column aligned — and declines everything else.\n */\n\nimport type { PdfTextItem } from '@semiont/core';\n\n/** A reconstructed cell: its text plus the bounding box of its runs. */\nexport interface TableCell {\n text: string;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** A header row plus at least two data rows — below this, prose in columns\n * is indistinguishable from a table. */\nconst MIN_ROWS = 3;\nconst MIN_COLUMNS = 2;\n\n/** Row grouping tolerance, as a fraction of text height: runs whose\n * baselines differ by less than half a line belong to one row. */\nconst ROW_TOLERANCE = 0.5;\n/** Horizontal gap that separates cells, as a fraction of text height. Word\n * spaces are far narrower; column gutters are far wider. */\nconst CELL_GAP = 0.8;\n\nfunction median(values: number[]): number {\n const sorted = [...values].sort((a, b) => a - b);\n return sorted[Math.floor(sorted.length / 2)] ?? 0;\n}\n\n/** Group runs into visual rows, top of page first. */\nfunction groupRows(items: PdfTextItem[], tolerance: number): PdfTextItem[][] {\n const rows: PdfTextItem[][] = [];\n for (const item of [...items].sort((a, b) => b.y - a.y)) {\n const row = rows[rows.length - 1];\n if (row && Math.abs(row[0]!.y - item.y) <= tolerance) row.push(item);\n else rows.push([item]);\n }\n return rows;\n}\n\nfunction toCell(runs: PdfTextItem[], text: string): TableCell {\n const x = Math.min(...runs.map((r) => r.x));\n const y = Math.min(...runs.map((r) => r.y));\n const right = Math.max(...runs.map((r) => r.x + r.width));\n const top = Math.max(...runs.map((r) => r.y + r.height));\n return {\n text: runs.map((r) => text.slice(r.start, r.end)).join(' ').trim(),\n x,\n y,\n width: right - x,\n height: top - y,\n };\n}\n\n/** Split a row into cells: runs closer than a gutter belong to one cell. */\nfunction toCells(row: PdfTextItem[], gap: number, text: string): TableCell[] {\n const cells: TableCell[] = [];\n let current: PdfTextItem[] = [];\n for (const item of [...row].sort((a, b) => a.x - b.x)) {\n const previous = current[current.length - 1];\n if (previous && item.x - (previous.x + previous.width) > gap) {\n cells.push(toCell(current, text));\n current = [];\n }\n current.push(item);\n }\n if (current.length > 0) cells.push(toCell(current, text));\n return cells;\n}\n\n/**\n * Recover a grid from one page's runs, or null when the page is not a\n * regular table.\n */\nexport function detectTable(items: PdfTextItem[], text: string): TableCell[][] | null {\n if (items.length === 0) return null;\n const unit = median(items.map((i) => i.height).filter((h) => h > 0)) || 12;\n\n const rows = groupRows(items, unit * ROW_TOLERANCE).map((row) => toCells(row, unit * CELL_GAP, text));\n if (rows.length < MIN_ROWS) return null;\n\n const columnCount = rows[0]!.length;\n if (columnCount < MIN_COLUMNS) return null;\n if (!rows.every((row) => row.length === columnCount)) return null;\n\n // Every column must start at the same offset down the page; ragged left\n // edges mean prose that happens to wrap into columns, not a grid.\n for (let column = 0; column < columnCount; column++) {\n const lefts = rows.map((row) => row[column]!.x);\n if (Math.max(...lefts) - Math.min(...lefts) > unit) return null;\n }\n if (rows.some((row) => row.some((cell) => cell.text.length === 0))) return null;\n\n return rows;\n}\n\n/**\n * Render a grid as markdown rows, anchoring every cell to the geometry it\n * came from. `offset` is where this text lands in the assembled document, so\n * the returned items index the final string.\n */\nexport function renderTable(\n rows: TableCell[][],\n page: number,\n offset: number,\n): { text: string; items: PdfTextItem[] } {\n let text = '';\n const items: PdfTextItem[] = [];\n rows.forEach((row, rowIndex) => {\n text += '|';\n for (const cell of row) {\n text += ' ';\n const start = offset + text.length;\n text += cell.text;\n items.push({\n start,\n end: offset + text.length,\n page,\n x: cell.x,\n y: cell.y,\n width: cell.width,\n height: cell.height,\n });\n text += ' |';\n }\n text += '\\n';\n // Markdown needs the delimiter row for the header to read as a table.\n if (rowIndex === 0) text += `|${' --- |'.repeat(row.length)}\\n`;\n });\n return { text, items };\n}\n","/**\n * Embedded page images from a PDF — the pixels OCR reads.\n *\n * A scanned page holds its characters only as pixels inside an image object,\n * so reading it means getting that image out. We do NOT rasterize: pdf.js\n * decodes the embedded image in its worker (pure JS — JPEG, CCITT and JBIG2\n * decoders all live there) and hands back raw pixel planes, which means no\n * canvas backend and no native dependency. Measured, not assumed — see\n * `.plans/SMELTER-MEDIA-TYPES.md` Resolved decision 10.\n *\n * Two consequences of extracting rather than rendering: we get the scan's own\n * resolution rather than choosing a render DPI (for a real scan that IS the\n * page, so it is what we want), and a page composed of vector overlays or\n * tiled strips yields more than one image, or none we can use. Anything we\n * cannot turn into pixels simply stays unread — never an error.\n */\n\nimport * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs';\nimport { STANDARD_FONT_DATA_URL } from './pdfjs-assets';\nimport { isObject, isNumber, isString, isArray } from '@semiont/core';\nimport { encodePng } from './png-encode';\n\n/** pdf.js image kinds (`ImageKind` in its API). */\nconst GRAYSCALE_1BPP = 1;\nconst RGB_24BPP = 2;\nconst RGBA_32BPP = 3;\n\nconst IDENTITY: readonly number[] = [1, 0, 0, 1, 0, 0];\n\n/**\n * Largest image this will read, in pixels.\n *\n * Sizing this needs the WHOLE allocation chain, not just the decoded raster —\n * reading one image can hold several copies at once:\n *\n * pdf.js decoded samples 4 bytes/px worst case (RGBA; RGB is 3)\n * + `toRgb` conversion 3 bytes/px (RGBA and 1-bit both allocate a copy;\n * plain RGB is passed through, no copy)\n * + `encodePng` scanlines 3 bytes/px (`raw`, plus a filter byte per row)\n * + deflate output smaller, but live alongside the above\n * ────────────────────────────────────────────────────────────────────\n * ≈ 10 bytes/px transient peak for a single image\n *\n * So the budget below implies roughly half a gigabyte of transient peak for\n * one pathological page — the number to size a worker against. Stating three\n * bytes per pixel here (as an earlier revision did) understated it by ~3× and\n * gave a false sense of safety.\n *\n * Chosen to admit the legitimate large cases with headroom: US Letter at\n * 600dpi is ~34 MP and A0 at 300dpi is ~35 MP, against an ordinary US Letter\n * at 300dpi of ~8 MP.\n *\n * A starting point, not a measured optimum: revisit against a real scanned\n * corpus (SMELTER-MEDIA-TYPES, live-testing follow-up). Lowering the peak\n * itself means removing copies from the chain — passing the decoded samples\n * straight to the encoder — which is a refactor, not a smaller constant.\n */\nexport const MAX_IMAGE_PIXELS = 48_000_000;\n\n/** Worst-case bytes held per pixel while reading one image — the chain above.\n * Exported so the budget's real cost is asserted rather than assumed. */\nexport const PEAK_BYTES_PER_PIXEL = 10;\n\n/** Whether an image's dimensions are sane and inside the budget. Exported\n * because the threshold is a judgement, and judgements deserve tests. */\nexport function withinPixelBudget(width: number, height: number): boolean {\n if (!Number.isFinite(width) || !Number.isFinite(height)) return false;\n if (width <= 0 || height <= 0) return false;\n return width * height <= MAX_IMAGE_PIXELS;\n}\n\n/** An image painted on a page, with the matrix that placed it. */\nexport interface PlacedImage {\n ref: string;\n /** Natural pixel dimensions, as reported by the paint operator itself. */\n width: number;\n height: number;\n /** Maps the image's unit square onto the page, in PDF points. */\n ctm: number[];\n}\n\n/**\n * Walk an operator list and report every painted image with the matrix in\n * effect when it was painted.\n *\n * Exported for its own tests: the composition ORDER cannot be checked with a\n * generated fixture, because pdf-lib emits a single combined matrix per image\n * and identity × M equals M × identity. It is checked directly instead, with\n * two non-identity transforms.\n *\n * Order convention: `ctm = Util.transform(ctm, m)` puts each new matrix on the\n * right, so it applies to a point FIRST and the enclosing matrices after —\n * which is what PDF nesting means. `save`/`restore` bracket the stack.\n */\nexport function findPlacedImages(fnArray: number[], argsArray: unknown[][]): PlacedImage[] {\n const placed: PlacedImage[] = [];\n const stack: number[][] = [];\n let ctm: number[] = [...IDENTITY];\n\n for (let i = 0; i < fnArray.length; i++) {\n const op = fnArray[i];\n const args = argsArray[i];\n if (op === pdfjs.OPS.save) {\n stack.push([...ctm]);\n } else if (op === pdfjs.OPS.restore) {\n ctm = stack.pop() ?? [...IDENTITY];\n } else if (op === pdfjs.OPS.transform) {\n if (isArray(args) && args.length >= 6 && args.every(isNumber)) {\n ctm = pdfjs.Util.transform(ctm, args as number[]);\n }\n } else if (op === pdfjs.OPS.paintImageXObject) {\n const ref = args?.[0];\n const width = args?.[1];\n const height = args?.[2];\n if (isString(ref) && isNumber(width) && isNumber(height)) {\n placed.push({ ref, width, height, ctm: [...ctm] });\n }\n }\n }\n return placed;\n}\n\n/**\n * The decoded samples, whichever byte view pdf.js chose.\n *\n * `/FlateDecode` images arrive as a `Uint8Array`; `/DCTDecode` (JPEG) — what\n * essentially every real scanned PDF uses — arrives as a `Uint8ClampedArray`,\n * which is NOT an instance of `Uint8Array`. Testing only for the latter\n * discarded every real scan while accepting every fixture in this repo, all\n * of which are Flate. Both index bytes identically, so both are read; the\n * clamped view is re-wrapped without copying its 12 MB buffer.\n */\nfunction asBytes(data: unknown): Uint8Array | null {\n if (data instanceof Uint8Array) return data;\n if (data instanceof Uint8ClampedArray) return new Uint8Array(data.buffer, data.byteOffset, data.length);\n return null;\n}\n\n/**\n * Normalize a decoded pdf.js image to 8-bit RGB, or null for a kind we do\n * not read. Unknown kinds leave the page unread rather than risk feeding an\n * OCR engine garbled pixels.\n */\nexport function toRgb(image: unknown): { width: number; height: number; rgb: Uint8Array } | null {\n if (!isObject(image)) return null;\n const { width, height, kind } = image;\n const data = asBytes(image.data);\n if (!isNumber(width) || !isNumber(height) || !data) return null;\n if (width <= 0 || height <= 0) return null;\n\n if (kind === RGB_24BPP) {\n return data.length >= width * height * 3 ? { width, height, rgb: data } : null;\n }\n\n if (kind === RGBA_32BPP) {\n if (data.length < width * height * 4) return null;\n const rgb = new Uint8Array(width * height * 3);\n for (let i = 0, o = 0; o < rgb.length; i += 4, o += 3) {\n rgb[o] = data[i]!;\n rgb[o + 1] = data[i + 1]!;\n rgb[o + 2] = data[i + 2]!;\n }\n return { width, height, rgb };\n }\n\n if (kind === GRAYSCALE_1BPP) {\n // Packed bilevel, rows padded to a byte boundary — the shape fax-encoded\n // scans arrive in. A set bit is white, matching pdf.js's own rendering.\n // If a real CCITT scan ever comes out inverted, this is the line to fix;\n // the failure mode is a page that OCRs to nothing, not corrupt output.\n const rowBytes = Math.ceil(width / 8);\n if (data.length < rowBytes * height) return null;\n const rgb = new Uint8Array(width * height * 3);\n for (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const bit = data[y * rowBytes + (x >> 3)]! & (0x80 >> (x & 7));\n const value = bit ? 0xFF : 0x00;\n const o = (y * width + x) * 3;\n rgb[o] = value;\n rgb[o + 1] = value;\n rgb[o + 2] = value;\n }\n }\n return { width, height, rgb };\n }\n\n return null;\n}\n\n/**\n * How long to wait for pdf.js to deliver one image before giving up on the\n * page. Generous: this is not a performance budget but a liveness backstop —\n * see `resolveImage`.\n */\nconst IMAGE_RESOLVE_TIMEOUT_MS = 30_000;\n\n/**\n * Resolve one image object; pdf.js delivers it asynchronously, so the callback\n * form is required — the synchronous getter throws.\n *\n * Two scopes, and asking the wrong one never answers. An image used by a single\n * page lives in `page.objs` as `img_p0_1`; an image used by MORE than one page\n * — a letterhead, a watermark, a scan pipeline that dedupes identical page\n * rasters — is promoted to pdf.js's global scope, renamed `g_d1_img_p1_1`, and\n * lives in `page.commonObjs`. `objs.get` on a global ref simply registers a\n * callback that is never invoked.\n *\n * The timeout is the second half, and it is about liveness rather than speed:\n * the smelter and the detection worker both `await` this, and a worker will not\n * claim another job while one is active — so a promise that never settles wedges\n * that worker permanently, on one bad document. Timing out yields `null`, which\n * leaves the page unread and reported, the same as an unreadable image kind.\n */\nfunction resolveImage(page: pdfjs.PDFPageProxy, ref: string): Promise<unknown> {\n // pdf.js marks globally-scoped objects with a `g_` prefix.\n const scope = ref.startsWith('g_') ? page.commonObjs : page.objs;\n return new Promise((resolve) => {\n const timer = setTimeout(() => resolve(null), IMAGE_RESOLVE_TIMEOUT_MS);\n const settle = (value: unknown) => {\n clearTimeout(timer);\n resolve(value);\n };\n try {\n scope.get(ref, settle);\n } catch {\n settle(null);\n }\n });\n}\n\n/** A page image ready for OCR, with everything needed to map results back. */\nexport interface PageImage {\n png: Buffer;\n /** Pixel dimensions of the decoded raster (may differ from the paint\n * operator's declared size if the image was resampled). */\n width: number;\n height: number;\n /** Maps the image's unit square onto the page, in PDF points. */\n ctm: number[];\n}\n\n/**\n * PNG-encoded images for the given pages (all pages when omitted), keyed by\n * 1-indexed page number, each with the matrix that placed it. Pages with no\n * usable image are absent from the map.\n */\nexport async function extractPageImages(\n bytes: Uint8Array | Buffer,\n pageNumbers?: number[],\n): Promise<Map<number, PageImage[]>> {\n const wanted = pageNumbers ? new Set(pageNumbers) : null;\n const loadingTask = pdfjs.getDocument({ data: new Uint8Array(bytes), standardFontDataUrl: STANDARD_FONT_DATA_URL });\n const byPage = new Map<number, PageImage[]>();\n\n try {\n const doc = await loadingTask.promise;\n for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {\n if (wanted && !wanted.has(pageNum)) continue;\n const page = await doc.getPage(pageNum);\n const ops = await page.getOperatorList();\n\n const images: PageImage[] = [];\n for (const placement of findPlacedImages(ops.fnArray, ops.argsArray)) {\n // Checked from the paint operator's own dimensions, BEFORE the\n // image is resolved — refusing after decoding would already\n // have paid the allocation this guards against.\n if (!withinPixelBudget(placement.width, placement.height)) continue;\n const rgb = toRgb(await resolveImage(page, placement.ref));\n if (!rgb) continue;\n images.push({\n png: encodePng(rgb.width, rgb.height, rgb.rgb),\n width: rgb.width,\n height: rgb.height,\n ctm: placement.ctm,\n });\n }\n if (images.length > 0) byPage.set(pageNum, images);\n }\n return byPage;\n } finally {\n await loadingTask.destroy();\n }\n}\n","/**\n * Minimal PNG encoder.\n *\n * OCR engines take an encoded image, while pdf.js hands back raw pixel\n * planes — this bridges the two. Deterministic, built on node's zlib, so a\n * package that deliberately carries no image dependency still does not.\n */\n\nimport zlib from 'zlib';\n\nconst CRC_TABLE = (() => {\n const table = new Int32Array(256);\n for (let n = 0; n < 256; n++) {\n let c = n;\n for (let k = 0; k < 8; k++) c = (c & 1) ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;\n table[n] = c;\n }\n return table;\n})();\n\nfunction crc32(buf: Buffer): number {\n let c = -1;\n for (const byte of buf) c = CRC_TABLE[(c ^ byte) & 0xFF]! ^ (c >>> 8);\n return (c ^ -1) >>> 0;\n}\n\nfunction chunk(type: string, data: Buffer): Buffer {\n const length = Buffer.alloc(4);\n length.writeUInt32BE(data.length);\n const body = Buffer.concat([Buffer.from(type, 'ascii'), data]);\n const crc = Buffer.alloc(4);\n crc.writeUInt32BE(crc32(body));\n return Buffer.concat([length, body, crc]);\n}\n\n/** Encode 8-bit RGB pixels (length must be width × height × 3) as a PNG. */\nexport function encodePng(width: number, height: number, rgb: Uint8Array): Buffer {\n const stride = width * 3 + 1; // one filter byte per scanline\n const raw = Buffer.alloc(stride * height);\n for (let y = 0; y < height; y++) {\n raw[y * stride] = 0; // filter type: none\n Buffer.from(rgb.buffer, rgb.byteOffset + y * width * 3, width * 3)\n .copy(raw, y * stride + 1);\n }\n const ihdr = Buffer.alloc(13);\n ihdr.writeUInt32BE(width, 0);\n ihdr.writeUInt32BE(height, 4);\n ihdr[8] = 8; // bit depth\n ihdr[9] = 2; // color type: truecolor\n return Buffer.concat([\n Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),\n chunk('IHDR', ihdr),\n chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),\n chunk('IEND', Buffer.alloc(0)),\n ]);\n}\n","/**\n * OCR — reading text out of page pixels (tesseract.js).\n *\n * Runs inline, on the caller's thread of control, deliberately: the Smelter's\n * lanes are per-resource and concurrent (`groupBy` + `mergeMap`), so a slow\n * page delays only its own resource, never the fast text resources it shares\n * a worker with. Extraction stays ephemeral — nothing is cached, and a\n * rebuild re-reads the pixels (SMELTER-MEDIA-TYPES Design §3/§5).\n *\n * Deterministic for a pinned engine: the same bytes yield the same text, so\n * re-running costs time and nothing else.\n */\n\nimport { createRequire } from 'node:module';\nimport { createWorker } from 'tesseract.js';\nimport { isObject, isString } from '@semiont/core';\n\n/**\n * The vendored language data — `@tesseract.js-data/eng` ships the same\n * `eng.traineddata.gz` tesseract.js would otherwise fetch from a CDN, and\n * exports the directory holding it.\n *\n * OCR is core (SMELTER-MEDIA-TYPES decision 8), so it must never reach the\n * network at runtime: an air-gapped worker has to be able to read a scan, and\n * a CDN outage must not silently turn scanned documents unreadable. Because\n * this is an ordinary dependency, `npm install` vendors it into the smelter\n * and worker images — no Dockerfile fetch step, and the lockfile pins it.\n *\n * Resolved lazily so importing this module has no side effects.\n */\nlet cachedLangPath: string | undefined;\nfunction langPath(): string {\n if (cachedLangPath) return cachedLangPath;\n const data: unknown = createRequire(import.meta.url)('@tesseract.js-data/eng');\n if (!isObject(data) || !isString(data.langPath)) {\n throw new Error(\n 'Vendored OCR language data is missing or malformed: @tesseract.js-data/eng did not export a langPath',\n );\n }\n cachedLangPath = data.langPath;\n return cachedLangPath;\n}\n\n/**\n * The slice of tesseract's recognition tree this module reads. Declared\n * structurally rather than importing `Tesseract.Block`, so tests can build a\n * tree without satisfying a dozen fields nothing here looks at; a real\n * `Block[]` still satisfies it.\n */\nexport interface OcrBbox { x0: number; y0: number; x1: number; y1: number }\nexport interface OcrLine {\n /** The line's own box — the vertical extent shared by its words. */\n bbox: OcrBbox;\n words: { text: string; confidence: number; bbox: OcrBbox }[];\n}\nexport interface OcrBlock {\n paragraphs: { lines: OcrLine[] }[];\n}\n\n/** A recognized word, with the range it occupies in the assembled page text. */\nexport interface OcrWord {\n text: string;\n /** Offsets into `OcrPage.text` — `text.slice(start, end) === word.text`. */\n start: number;\n end: number;\n /** Image pixel space, top-left origin — mapped to PDF points downstream. */\n bbox: OcrBbox;\n confidence: number;\n}\n\nexport interface OcrPage {\n text: string;\n words: OcrWord[];\n}\n\n/**\n * Assemble a page's text from its recognition tree, recording where each word\n * lands as it is written.\n *\n * The text is built here rather than taken from tesseract's own `data.text`\n * precisely so the offsets are exact **by construction** — deriving offsets by\n * searching for words in a separately-produced string is where this kind of\n * code goes wrong. Words join with a space, lines with a newline, paragraphs\n * with a blank line.\n */\nexport function assemblePage(blocks: OcrBlock[] | null): OcrPage {\n let text = '';\n const words: OcrWord[] = [];\n\n for (const block of blocks ?? []) {\n for (const paragraph of block.paragraphs ?? []) {\n for (const line of paragraph.lines ?? []) {\n let wroteWord = false;\n for (const word of line.words ?? []) {\n const value = word.text.trim();\n if (!value) continue; // an empty box is not a word\n if (wroteWord) text += ' ';\n const start = text.length;\n text += value;\n words.push({\n text: value,\n start,\n end: text.length,\n // Horizontal extent from the word, vertical from the\n // line. OCR boxes hug their glyphs, so a descender\n // ('page') sits lower than its neighbours — and\n // `locate()` groups items into lines by comparing `y`\n // within a couple of points, a threshold that holds\n // because NATIVE runs take y from the shared baseline.\n // Passing per-word descenders through would split one\n // visual line into several rects and draw a highlight\n // as stacked fragments. Nothing is lost: `locate()`\n // bounds each line anyway, so per-word vertical extent\n // never reaches an annotation.\n bbox: {\n x0: word.bbox.x0,\n x1: word.bbox.x1,\n y0: line.bbox.y0,\n y1: line.bbox.y1,\n },\n confidence: word.confidence,\n });\n wroteWord = true;\n }\n if (wroteWord) text += '\\n';\n }\n text += '\\n';\n }\n }\n\n // Only trailing separators are removed, so no recorded offset moves.\n return { text: text.trimEnd(), words };\n}\n\n/**\n * Recognize a batch of PNG images, returning one result per image (empty\n * where nothing legible was found). One worker serves the whole batch —\n * startup is the expensive part, not the pages.\n */\nexport async function recognizeImages(images: Buffer[]): Promise<OcrPage[]> {\n if (images.length === 0) return [];\n // `cacheMethod: 'none'` — the data is already local, so there is nothing to\n // cache and no reason to write a copy into the working directory.\n const worker = await createWorker('eng', undefined, {\n langPath: langPath(),\n cacheMethod: 'none',\n });\n try {\n const results: OcrPage[] = [];\n for (const image of images) {\n // `blocks: true` is what carries the per-word geometry; without it\n // tesseract returns text only and `data.blocks` is null.\n const { data } = await worker.recognize(image, {}, { blocks: true, text: false });\n results.push(assemblePage(data.blocks));\n }\n return results;\n } finally {\n await worker.terminate();\n }\n}\n","/**\n * OCR word boxes → PDF-point geometry (#739).\n *\n * OCR reports boxes in the image's own pixel space, top-left origin. Anchoring\n * them means going through the matrix that placed the image on the page:\n *\n * pixel (px, py) → unit square (px/W, 1 − py/H) → CTM → PDF points\n *\n * Rotation and non-uniform scale fall out of the matrix, so there are no\n * special cases for them — the only explicit work is normalizing the result,\n * since a mirrored placement can invert an axis and consumers bound\n * rectangles rather than orienting them.\n */\n\nimport * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs';\nimport type { OcrWord } from './ocr';\nimport type { PdfTextItem } from '@semiont/core';\n\n/** The placement of one image: its pixel size and its matrix onto the page. */\nexport interface ImagePlacement {\n width: number;\n height: number;\n ctm: number[];\n}\n\nfunction toPagePoint(px: number, py: number, placement: ImagePlacement): [number, number] {\n // Unit square, Y flipped: pixel rows run down, PDF space runs up.\n const point: [number, number] = [px / placement.width, 1 - py / placement.height];\n pdfjs.Util.applyTransform(point, placement.ctm); // mutates in place\n return point;\n}\n\n/**\n * Map recognized words onto the page, shifting their character offsets by\n * `textOffset` — where this page's text begins in the assembled document.\n */\nexport function mapWordsToItems(\n words: OcrWord[],\n placement: ImagePlacement,\n page: number,\n textOffset: number,\n): PdfTextItem[] {\n if (placement.width <= 0 || placement.height <= 0) return [];\n\n return words.map((word) => {\n // Both corners through the matrix, then bound them — a flipped or\n // rotated placement can put either one first.\n const [ax, ay] = toPagePoint(word.bbox.x0, word.bbox.y0, placement);\n const [bx, by] = toPagePoint(word.bbox.x1, word.bbox.y1, placement);\n const x = Math.min(ax, bx);\n const y = Math.min(ay, by);\n return {\n start: word.start + textOffset,\n end: word.end + textOffset,\n page,\n x,\n y,\n width: Math.abs(bx - ax),\n height: Math.abs(by - ay),\n };\n });\n}\n","/**\n * Anchored-text cache — the persistent half of ANCHORED-TEXT-CACHE.md Lane 2.\n *\n * OCR costs ~2.9 s per scanned page, and six passes read the same document (five\n * detection motivations plus the smelter's embed), each its own job in its own\n * process. This stores what the engine produced so only the first pass pays.\n *\n * **Derived values only.** Everything here is reproducible from the source\n * bytes, which is what makes a stamp miss safe. An authored coordinate map is\n * embedded in the PDF Semiont generated, not stored alongside one — see\n * `PDF-GENERATION.md`, which owns that decision and states the negative:\n * never this store.\n *\n * The seam is `extract()` (PERSIST-ANCHORS D1/P2b): the record is the FINISHED\n * extraction outcome — classification, geometry, provenance, or a named\n * decline — so a hit skips the native parse and the engine both, and every\n * geometry-yielding extraction stores an entry, native documents included.\n * That is what makes the anchored-text endpoint answer for every resource\n * whose extraction yields geometry, and what lets the reconcile planner treat\n * \"no entry under the current checksum\" as work (P0's third drift class).\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport { createRequire } from 'module';\nimport { getShardPath, isObject, isString, isNumber, isArray, type ExtractionOutcome, type Logger, type PdfTextItem } from '@semiont/core';\n\n\n/**\n * One line of recognized text: the geometry every word on it shares, plus the\n * per-word parts that differ.\n *\n * Grouping is by *contiguous runs* of equal `(y, h)`, never by scanning for all\n * items at a given y. That makes the codec lossless and order-preserving for\n * any input — compression is the only thing that depends on words actually\n * arriving in reading order, and correctness never is.\n *\n * Sharing `y`/`h` is measured-safe rather than assumed: within-line word-height\n * spread is 0.0pt in both native and OCR'd output, because the engine already\n * normalizes word boxes to the line. Per-word `x` and `width` are stored\n * explicitly and NOT derived from neighbouring split positions — deriving width\n * from the gap to the next word would widen every box to touch its neighbour,\n * which would silently change the coverage arithmetic `textUnder` is calibrated\n * on (RUN_COVERAGE_THRESHOLD, tuned against ink-tight boxes).\n */\nexport interface CachedLine {\n /** 1-indexed page. */\n p: number;\n /** PDF points, bottom-left origin — shared by every word on the line. */\n y: number;\n h: number;\n /** `[x, width, start, end]` per word; offsets index `CachedAnchoredText.text`. */\n words: [number, number, number, number][];\n}\n\n/**\n * The stored record: one extraction OUTCOME for the whole resource\n * (PERSIST-ANCHORS decision D1) — the anchored text with its provenance\n * (`method`, `pdfClass`, `ocrConfidence`, `unreadPages`), or a named decline.\n *\n * Whole-resource on every side, deliberately. The producer's own shape is a\n * per-page map, but that is an artifact of how `ocrPages` iterates, and letting\n * it reach storage would have forced every consumer — the transport, the\n * browser, a headless client — to reassemble pages it never asked to see.\n *\n * The `ocrConfidence` SUMMARY is stored (v2) — this repairs the regression\n * OCR-CONFIDENCE-LOST.md records, where a hit answered with no confidence at\n * all. Per-word confidences remain unstored: the summary is the record's\n * quality provenance; the word list is operator log detail.\n *\n * v1 records (bare `{ text, lines }`, no provenance) read as misses under the\n * v2 prefix; the reconcile planner's third drift class re-derives them.\n */\nexport type CachedAnchoredText =\n | ({\n v: 2;\n /** Engine + traineddata + our assembly code. A mismatch is a clean miss. */\n stamp: string;\n text: string;\n lines: CachedLine[];\n } & Omit<Extract<ExtractionOutcome, { kind: 'extracted' }>, 'kind' | 'text' | 'items'>)\n | ({\n v: 2;\n stamp: string;\n } & Omit<Extract<ExtractionOutcome, { kind: 'declined' }>, 'kind'>);\n\n/**\n * What the cached value must be recomputed against.\n *\n * Derived, never hand-maintained. A hand-bumped counter fails in the one\n * direction that matters: forgetting to bump it does not cost a recomputation,\n * it silently serves geometry built by different code. Over-invalidating costs\n * seconds of the work this cache exists to avoid; under-invalidating is\n * corruption, so the stamp is deliberately over-eager — a release of this\n * package busts the cache whether or not assembly actually changed.\n *\n * `@semiont/content`'s own version covers our assembly code (`anchorRuns`,\n * `assemblePage`, `mapWordsToItems` — the offset construction IS part of what\n * the cached value means). The engine and its traineddata are read separately\n * because both are pinned with carets and can move without a release here —\n * and different traineddata means different recognized text, which is a\n * difference in the value itself, not merely in how fast it was produced.\n *\n * pdf.js joined at P2b, because the seam did: the record is the finished\n * extraction outcome, so it depends on the native parse — classification,\n * text-layer read, table/form shaping — not just the engine. A parser upgrade\n * is a change in the value, and the entry must miss.\n */\nfunction buildStamp(): string {\n const require = createRequire(import.meta.url);\n const version = (specifier: string): string => {\n try {\n const pkg: unknown = require(specifier);\n return isObject(pkg) && isString(pkg.version) ? pkg.version : 'unknown';\n } catch {\n return 'unknown';\n }\n };\n return `content-${version('../package.json')}`\n + `+pdfjs-${version('pdfjs-dist/package.json')}`\n + `+tesseract-${version('tesseract.js/package.json')}`\n + `+eng-${version('@tesseract.js-data/eng/package.json')}`;\n}\n\nconst STAMP = buildStamp();\n\n/** Pack items into line records. Lossless and order-preserving for any input. */\nexport function encodeLines(items: PdfTextItem[]): CachedLine[] {\n const lines: CachedLine[] = [];\n for (const item of items) {\n const last = lines[lines.length - 1];\n if (last && last.p === item.page && last.y === item.y && last.h === item.height) {\n last.words.push([item.x, item.width, item.start, item.end]);\n } else {\n lines.push({ p: item.page, y: item.y, h: item.height, words: [[item.x, item.width, item.start, item.end]] });\n }\n }\n return lines;\n}\n\n/** The inverse of `encodeLines`. */\nexport function decodeLines(lines: CachedLine[]): PdfTextItem[] {\n const items: PdfTextItem[] = [];\n for (const line of lines) {\n for (const [x, width, start, end] of line.words) {\n items.push({ start, end, page: line.p, x, y: line.y, width, height: line.h });\n }\n }\n return items;\n}\n\nexport interface AnchoredTextStore {\n /**\n * The stored map for this key, or null for any miss. Never throws.\n *\n * The key is the **content checksum of the bytes the map derives from**\n * (PERSIST-ANCHORS decision A): a representation is its bytes, so the\n * checksum is its identity, and geometry derived from one revision of the\n * bytes is unreachable by a reader holding a different revision — by\n * construction, not by invalidation. Callers holding some other handle\n * (a resource id) reach the artifact through an index, not by a second\n * key scheme here.\n */\n read(key: string): Promise<ExtractionOutcome | null>;\n /** Record an extraction outcome under the content checksum of its source\n * bytes. A store that cannot write is still a store. */\n write(key: string, outcome: ExtractionOutcome): Promise<void>;\n /**\n * Every key `read()` would currently HIT — entries under a stale stamp or\n * unreadable files are excluded, exactly as `read()` would exclude them.\n * That equivalence is load-bearing: the reconcile planner treats a listed\n * key as \"artifact present\" and plans re-derivation for the rest\n * (PERSIST-ANCHORS P0, the third drift class), so a key listed here but\n * missed by `read()` would be a permanent loss the diff can never see —\n * the exact shape of the post-engine-upgrade hole this filter closes.\n * One bulk call per reconcile, never a probe per resource. Never throws.\n */\n list(): Promise<string[]>;\n}\n\n/** Narrow a parsed entry, so a truncated or foreign file is a miss, not a crash. */\nfunction isCached(value: unknown): value is CachedAnchoredText {\n if (!isObject(value) || value.v !== 2 || !isString(value.stamp)) return false;\n if (isString(value.declined)) return true;\n if (!isString(value.text) || !isString(value.method) || !isArray(value.lines)) return false;\n return value.lines.every((line) =>\n isObject(line) && isNumber(line.p) && isNumber(line.y) && isNumber(line.h) && isArray(line.words)\n && line.words.every((w) => isArray(w) && w.length === 4 && w.every(isNumber)));\n}\n\n/** A key that could not have come from a checksum (or a legacy hex handle) is\n * refused outright rather than sanitized: a silently stripped key could share\n * a file with a different entry. Rejection replaces the old strip\n * (PERSIST-ANCHORS, *Smaller things*). */\nconst VALID_KEY = /^[A-Za-z0-9_-]+$/;\n\n/**\n * A file-backed store under `dir` — one file per content key, sharded as\n * `{ab}/{cd}/{key}.json` via the same `getShardPath` the event log uses\n * (PERSIST-ANCHORS decision E). Same convention, separate tree: `.semiont/`\n * is the KB's committed system of record; everything here is derived,\n * reclaimable, and never a source of truth.\n *\n * `dir` is the caller's, out of `Project.anchoredTextDir`: this package has no idea\n * which project it is serving. Every failure path is a miss rather than an\n * error, matching the rule extraction already follows for unreadable pages —\n * the cache may make things faster, never make them fail.\n */\nexport function createAnchoredTextStore(dir: string, logger?: Logger): AnchoredTextStore {\n const fileFor = (key: string): string | null => {\n if (!VALID_KEY.test(key)) return null;\n const [ab, cd] = getShardPath(key);\n return path.join(dir, ab, cd, `${key}.json`);\n };\n\n return {\n async read(key) {\n let hit: CachedAnchoredText | null = null;\n try {\n const file = fileFor(key);\n if (file === null) throw new Error('invalid key'); // refused → a miss like any other\n const parsed: unknown = JSON.parse(await fs.promises.readFile(file, 'utf8'));\n if (isCached(parsed) && parsed.stamp === STAMP) hit = parsed;\n } catch {\n hit = null; // absent, unreadable, truncated, or not ours\n }\n // Logged here rather than at the call sites: `prepare-detection` and\n // the smelter both extract, so each would see only its own share of\n // the traffic and the policy would be stated twice. Hit rate is what\n // keeps the Lane 0 decision auditable after the fact.\n logger?.debug('Anchored-text cache', {\n outcome: hit ? 'hit' : 'miss',\n key,\n ...(hit ? ('declined' in hit ? { declined: hit.declined } : { lines: hit.lines.length }) : {}),\n });\n if (!hit) return null;\n // `kind` is not persisted — the branch is implied by the record's\n // own shape, and re-added here so readers get the discriminated\n // wire union (WIRE-UNION-DISCRIMINANTS P5c).\n if ('declined' in hit) return { kind: 'declined', declined: hit.declined };\n const { v: _v, stamp: _stamp, lines, text, ...provenance } = hit;\n return { kind: 'extracted', text, items: decodeLines(lines), ...provenance };\n },\n\n async write(key, outcome) {\n const target = fileFor(key);\n if (target === null) {\n logger?.debug('Anchored-text cache: refusing invalid key', { key });\n return; // a store that cannot write is still a store\n }\n // Key order (`v`, `stamp`, first) is load-bearing: `list()` below\n // reads only a prefix of each file and matches the stamp there.\n const entry: CachedAnchoredText = outcome.kind === 'declined'\n ? { v: 2, stamp: STAMP, declined: outcome.declined }\n : (() => {\n // `kind` is deliberately destructured OUT: persisting it\n // would store a byte the branch already implies, and a\n // stored-shape change here would outrun the release-derived\n // STAMP (WIRE-UNION-DISCRIMINANTS P5c).\n const { kind: _kind, text, items, ...provenance } = outcome;\n return { v: 2, stamp: STAMP, text, lines: encodeLines(items), ...provenance };\n })();\n // Write-then-rename: a reader never observes a half-written entry,\n // and two writers racing on the same key both produce the same bytes.\n const temp = `${target}.${process.pid}.tmp`;\n try {\n await fs.promises.mkdir(path.dirname(target), { recursive: true });\n await fs.promises.writeFile(temp, JSON.stringify(entry), 'utf8');\n await fs.promises.rename(temp, target);\n } catch {\n await fs.promises.rm(temp, { force: true }).catch(() => {});\n }\n },\n\n async list() {\n // Would-hit keys only (see the interface doc). The stamp check\n // reads a bounded prefix rather than parsing whole entries — an\n // artifact is ~32 KB per scanned page and this runs over every\n // entry at every reconcile. Sound because `write()` above puts\n // `v` and `stamp` first, so the current stamp appears within the\n // first bytes of every entry this store has ever written; a file\n // whose prefix doesn't match is either stale or not ours, and\n // both are misses for `read()` too. Keys round-trip through\n // filenames unchanged because every real key is hex — the same\n // fact that makes `fileFor`'s guard a no-op for them.\n const prefix = JSON.stringify({ v: 2, stamp: STAMP }).slice(0, -1) + ',';\n let rootNames: string[];\n try {\n rootNames = await fs.promises.readdir(dir);\n } catch {\n return []; // no directory yet: nothing has been written\n }\n\n // One-generation sweep (PERSIST-ANCHORS P1): a `.json` at the root\n // is a pre-P1 entry — flat layout, resource-id key, a dead scheme.\n // The rebuild path (P0's third drift class) re-derives anything\n // still needed, which is what makes this delete safe; leaving a\n // generation behind is how the store's size becomes unexplainable.\n // Done here because list() is the one bulk call every reconcile\n // already makes, so the sweep runs exactly when the planner is\n // about to notice what is missing. Best-effort, never throws.\n let swept = 0;\n for (const name of rootNames) {\n if (!name.endsWith('.json')) continue;\n await fs.promises.rm(path.join(dir, name), { force: true }).then(() => { swept += 1; }, () => {});\n }\n if (swept > 0) logger?.info('Anchored-text cache: swept pre-P1 flat entries', { swept });\n\n const keys: string[] = [];\n let sweptInterim = 0;\n for (const ab of rootNames) {\n if (!/^[0-9a-f]{2}$/.test(ab)) continue;\n let cdNames: string[];\n try {\n cdNames = await fs.promises.readdir(path.join(dir, ab));\n } catch {\n continue;\n }\n for (const cd of cdNames) {\n if (!/^[0-9a-f]{2}$/.test(cd)) continue;\n let names: string[];\n try {\n names = await fs.promises.readdir(path.join(dir, ab, cd));\n } catch {\n continue;\n }\n for (const name of names) {\n if (!name.endsWith('.json')) continue;\n // Interim-generation sweep (PERSIST-ANCHORS P1b): a\n // 32-hex basename is a resource-id key — writes that\n // landed sharded between P1a's rekey and P1b's\n // call-site switch. Checksums are 64-hex (SHA-256),\n // so the two generations are disjoint by length.\n // Reaped here for the same reason the flat sweep\n // lives here: one bulk call per reconcile, and never\n // a third scheme lingering silently.\n const base = name.slice(0, -'.json'.length);\n if (/^[0-9a-f]{32}$/.test(base)) {\n await fs.promises.rm(path.join(dir, ab, cd, name), { force: true }).then(() => { sweptInterim += 1; }, () => {});\n continue;\n }\n let handle: fs.promises.FileHandle | null = null;\n try {\n handle = await fs.promises.open(path.join(dir, ab, cd, name), 'r');\n const buf = Buffer.alloc(prefix.length);\n const { bytesRead } = await handle.read(buf, 0, prefix.length, 0);\n if (bytesRead === prefix.length && buf.toString('utf8') === prefix) {\n keys.push(base);\n }\n } catch {\n // unreadable is a miss, matching read()\n } finally {\n await handle?.close().catch(() => {});\n }\n }\n }\n }\n if (sweptInterim > 0) logger?.info('Anchored-text cache: swept interim resource-id entries', { swept: sweptInterim });\n return keys;\n },\n };\n}\n","/**\n * `AnchoredTextStore` over `IContentTransport` — how an out-of-process\n * extraction seam reaches the one real store (PERSIST-ANCHORS P2c).\n *\n * Every cache consumer runs outside the backend — the smelter worker and the\n * detection workers — while the KnowledgeSystem owns the storage. This\n * adapter maps the store contract onto the transport's three\n * checksum-addressed calls, so `ExtractionCache { key, store }` works\n * identically in-process (LocalContentTransport → the store directly) and\n * over the wire (HttpContentTransport → the /anchored-text routes).\n *\n * It honors the store contract's failure rule — the cache may make things\n * faster, never make them fail: a read or list failure is a miss, a write\n * failure is swallowed (debug-logged). Callers that need a write to be LOUD\n * — the re-anchor path, whose artifact IS the job — use the transport's\n * `putAnchoredText` directly, not this adapter.\n */\n\nimport type { IContentTransport, Logger } from '@semiont/core';\nimport type { AnchoredTextStore } from './anchored-text-store';\n\nexport function anchoredTextStoreOverTransport(\n content: IContentTransport,\n logger?: Logger,\n): AnchoredTextStore {\n return {\n async read(key) {\n try {\n return await content.getAnchoredTextByChecksum(key);\n } catch (error) {\n logger?.debug('Anchored-text cache: transport read failed — treating as miss', {\n key,\n reason: error instanceof Error ? error.message : String(error),\n });\n return null;\n }\n },\n\n async write(key, outcome) {\n try {\n await content.putAnchoredText(key, outcome);\n } catch (error) {\n logger?.debug('Anchored-text cache: transport write failed — entry not stored', {\n key,\n reason: error instanceof Error ? error.message : String(error),\n });\n }\n },\n\n async list() {\n try {\n return await content.listAnchoredTextKeys();\n } catch (error) {\n logger?.debug('Anchored-text cache: transport list failed — treating as empty', {\n reason: error instanceof Error ? error.message : String(error),\n });\n return [];\n }\n },\n };\n}\n"],"mappings":"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","names":["isObject","path","require","pdfjs","isObject","isNumber","isString","isArray","isArray","isNumber","isString","isObject","createRequire","isObject","isString","pdfjs","isObject","ocr","confidence","fs","path","createRequire","isObject","isString","isNumber","isArray","require"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/content",
3
- "version": "0.5.27",
3
+ "version": "0.5.28",
4
4
  "engines": {
5
5
  "node": ">=24.0.0"
6
6
  },
@@ -27,7 +27,7 @@
27
27
  "test:coverage": "vitest run --coverage"
28
28
  },
29
29
  "dependencies": {
30
- "@semiont/core": "0.5.27",
30
+ "@semiont/core": "0.5.28",
31
31
  "@tesseract.js-data/eng": "^1.0.0",
32
32
  "pdfjs-dist": "^6.2.108",
33
33
  "tesseract.js": "^7.0.0"
@@ -53,7 +53,7 @@
53
53
  "license": "Apache-2.0",
54
54
  "repository": {
55
55
  "type": "git",
56
- "url": "https://github.com/The-AI-Alliance/semiont.git",
56
+ "url": "git+https://github.com/The-AI-Alliance/semiont.git",
57
57
  "directory": "packages/content"
58
58
  }
59
59
  }