@semiont/content 0.5.28 → 0.5.29
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/README.md +4 -2
- package/dist/index.d.ts +98 -48
- package/dist/index.js +141 -73
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install @semiont/content
|
|
|
16
16
|
|
|
17
17
|
## Architecture Context
|
|
18
18
|
|
|
19
|
-
**Infrastructure Ownership**: In production applications, the working tree store is **created and managed by [@semiont/make-meaning](../make-meaning/)'s `startMakeMeaning()` function**, which serves as the single orchestration point for all infrastructure components.
|
|
19
|
+
**Infrastructure Ownership**: In production applications, the working tree store is **created and managed by [@semiont/make-meaning](../make-meaning/)'s `startMakeMeaning()` function**, which serves as the single orchestration point for all infrastructure components. Gateway code accesses it as `knowledgeBase.content`.
|
|
20
20
|
|
|
21
21
|
The quick start example below shows direct instantiation for **testing, CLI tools, or content management scripts**.
|
|
22
22
|
|
|
@@ -26,7 +26,9 @@ The quick start example below shows direct instantiation for **testing, CLI tool
|
|
|
26
26
|
import { WorkingTreeStore, deriveStorageUri } from '@semiont/content';
|
|
27
27
|
import { SemiontProject } from '@semiont/core/node';
|
|
28
28
|
|
|
29
|
-
const project = new SemiontProject('/path/to/project'
|
|
29
|
+
const project = new SemiontProject('/path/to/project', {
|
|
30
|
+
anchoredTextDir: process.env.SEMIONT_ANCHORED_TEXT_DIR!,
|
|
31
|
+
});
|
|
30
32
|
const store = new WorkingTreeStore(project);
|
|
31
33
|
|
|
32
34
|
// Derive a stable file:// URI from a resource name
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Readable } from 'stream';
|
|
2
|
+
import { SemiontProject, ArchivistAddressConfig } from '@semiont/core/node';
|
|
3
|
+
import { Logger, ExtractionOutcome, PdfTextItem, TextExtraction, IContentTransport, AnchoredText } from '@semiont/core';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* WorkingTreeStore - Manages files in the project working tree
|
|
@@ -12,9 +13,11 @@ import { Logger, SupportedMediaType, ExtractionOutcome, PdfTextItem, TextExtract
|
|
|
12
13
|
* Two write paths:
|
|
13
14
|
* - store(content, storageUri): Write bytes to disk (API/GUI/AI path).
|
|
14
15
|
* Used when the file does not yet exist and the caller provides content.
|
|
15
|
-
* - register(storageUri, expectedChecksum?):
|
|
16
|
-
* return its metadata
|
|
17
|
-
*
|
|
16
|
+
* - register(storageUri, expectedChecksum?): Adopt a file already on disk and
|
|
17
|
+
* return its metadata. The CLI path (the file arrived by other means) and
|
|
18
|
+
* the event-apply path (the Stower staging bytes an event names) both use
|
|
19
|
+
* it. Streams the file to hash it — never holds it. If expectedChecksum is
|
|
20
|
+
* provided, throws on mismatch.
|
|
18
21
|
*
|
|
19
22
|
* Storage layout:
|
|
20
23
|
* {projectRoot}/{path-from-uri}
|
|
@@ -44,20 +47,33 @@ declare class WorkingTreeStore {
|
|
|
44
47
|
/**
|
|
45
48
|
* Write content to disk at the location indicated by storageUri.
|
|
46
49
|
*
|
|
47
|
-
* API/GUI/AI path: caller provides bytes
|
|
50
|
+
* API/GUI/AI path: caller provides bytes — as a Buffer it already holds, or
|
|
51
|
+
* as a stream (the Archivist's write endpoint hands the request body
|
|
52
|
+
* straight through, SINGLE-KB-MOUNT P2/D7: memory stays bounded by the
|
|
53
|
+
* chunk, never the representation).
|
|
48
54
|
*
|
|
49
|
-
*
|
|
55
|
+
* Atomic either way: bytes stream into a temp file beside the target and
|
|
56
|
+
* are renamed into place only once complete — and only once
|
|
57
|
+
* `expectedChecksum`, when given, agrees with what actually arrived. A
|
|
58
|
+
* mismatch or a torn stream leaves the target untouched (a version being
|
|
59
|
+
* overwritten survives) and no temp file behind, so the Stower's `register`
|
|
60
|
+
* can never find partial bytes an event names.
|
|
61
|
+
*
|
|
62
|
+
* @param content - Raw bytes to write, whole or streamed
|
|
50
63
|
* @param storageUri - file:// URI (e.g. "file://docs/overview.md")
|
|
64
|
+
* @throws ChecksumMismatchError when expectedChecksum disagrees with the body
|
|
51
65
|
* @returns Stored resource metadata
|
|
52
66
|
*/
|
|
53
|
-
store(content: Buffer, storageUri: string, options?: {
|
|
67
|
+
store(content: Buffer | Readable, storageUri: string, options?: {
|
|
54
68
|
noGit?: boolean;
|
|
69
|
+
expectedChecksum?: string;
|
|
55
70
|
}): Promise<StoredResource>;
|
|
56
71
|
/**
|
|
57
72
|
* Read an existing file and return its metadata.
|
|
58
73
|
*
|
|
59
|
-
*
|
|
60
|
-
* If expectedChecksum is provided, throws
|
|
74
|
+
* The file is already on disk; this hashes it by streaming to confirm what
|
|
75
|
+
* it is, then stages it. If expectedChecksum is provided, throws
|
|
76
|
+
* ChecksumMismatchError on mismatch.
|
|
61
77
|
*
|
|
62
78
|
* @param storageUri - file:// URI (e.g. "file://docs/overview.md")
|
|
63
79
|
* @param expectedChecksum - Optional SHA-256 to verify against
|
|
@@ -74,6 +90,19 @@ declare class WorkingTreeStore {
|
|
|
74
90
|
* @param storageUri - file:// URI
|
|
75
91
|
* @returns Raw bytes
|
|
76
92
|
*/
|
|
93
|
+
/**
|
|
94
|
+
* The same bytes as `retrieve`, streamed — for the byte paths that must not
|
|
95
|
+
* hold a whole representation in memory (SINGLE-KB-MOUNT D7: the Archivist
|
|
96
|
+
* serves content for every reader now, so its memory cannot be bounded by
|
|
97
|
+
* the largest file anyone asks for).
|
|
98
|
+
*
|
|
99
|
+
* Lazy by construction: the stream is created here but nothing is read
|
|
100
|
+
* until the caller iterates, so a missing file surfaces as an `error` event
|
|
101
|
+
* on the stream rather than a rejected promise. Callers that need the
|
|
102
|
+
* distinction up front should resolve the descriptor first — which is what
|
|
103
|
+
* `resolveRepresentation` does.
|
|
104
|
+
*/
|
|
105
|
+
retrieveStream(storageUri: string): Readable;
|
|
77
106
|
retrieve(storageUri: string): Promise<Buffer>;
|
|
78
107
|
/**
|
|
79
108
|
* Move a file from one URI to another.
|
|
@@ -128,26 +157,6 @@ declare class ChecksumMismatchError extends Error {
|
|
|
128
157
|
constructor(storageUri: string, expected: string, actual: string);
|
|
129
158
|
}
|
|
130
159
|
|
|
131
|
-
/**
|
|
132
|
-
* Storage URI Derivation
|
|
133
|
-
*
|
|
134
|
-
* Builds the file:// URI a resource lives at in the working tree from its
|
|
135
|
-
* name and validated media type. Extensions come from the media-type
|
|
136
|
-
* registry in @semiont/core; formats are validated upstream at the
|
|
137
|
-
* create/yield boundary, so the lookup is strict — no fallback.
|
|
138
|
-
*/
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Derive a file:// storage URI from a resource name and media type.
|
|
142
|
-
*
|
|
143
|
-
* The name is lowercased, runs of non-alphanumeric characters collapse to
|
|
144
|
-
* single hyphens, and leading/trailing hyphens are stripped.
|
|
145
|
-
*
|
|
146
|
-
* @example
|
|
147
|
-
* deriveStorageUri("My Document", "text/markdown") // => "file://my-document.md"
|
|
148
|
-
*/
|
|
149
|
-
declare function deriveStorageUri(name: string, format: SupportedMediaType): string;
|
|
150
|
-
|
|
151
160
|
/**
|
|
152
161
|
* Checksum utilities for content verification
|
|
153
162
|
*/
|
|
@@ -258,8 +267,19 @@ interface AnchoredTextStore {
|
|
|
258
267
|
* key scheme here.
|
|
259
268
|
*/
|
|
260
269
|
read(key: string): Promise<ExtractionOutcome | null>;
|
|
261
|
-
/**
|
|
262
|
-
*
|
|
270
|
+
/**
|
|
271
|
+
* Record an extraction outcome under the content checksum of its source
|
|
272
|
+
* bytes. **THROWS on failure: a write that returns has written.**
|
|
273
|
+
*
|
|
274
|
+
* Asymmetric with `read` above, which never throws, and deliberately so —
|
|
275
|
+
* a miss is a normal answer, a failed write is not. The store used to
|
|
276
|
+
* swallow for everyone, which forced the one caller that needs a throw
|
|
277
|
+
* (the Smelter's re-anchor publish, whose `smelt:rebuild-anchors-failed`
|
|
278
|
+
* accounting rides on it) to route around the store entirely. Now the
|
|
279
|
+
* contract is honest and **leniency is the caller's**, stated where it is
|
|
280
|
+
* wanted: the read-through seam in `pdf-extractor` catches, because a
|
|
281
|
+
* store may make extraction faster but must never make it fail.
|
|
282
|
+
*/
|
|
263
283
|
write(key: string, outcome: ExtractionOutcome): Promise<void>;
|
|
264
284
|
/**
|
|
265
285
|
* Every key `read()` would currently HIT — entries under a stale stamp or
|
|
@@ -440,24 +460,54 @@ declare const MAX_PDF_BYTES: number;
|
|
|
440
460
|
declare function withinByteBudget(bytes: number): boolean;
|
|
441
461
|
|
|
442
462
|
/**
|
|
443
|
-
*
|
|
444
|
-
*
|
|
463
|
+
* Reading a resource's bytes: the contract, the way it fails, and the
|
|
464
|
+
* implementation that reaches the Archivist over HTTP.
|
|
465
|
+
*
|
|
466
|
+
* These live in `@semiont/content` because this package IS the byte layer —
|
|
467
|
+
* the Archivist's whole job — and because the readers span the dependency
|
|
468
|
+
* graph. `@semiont/make-meaning` holds the Archivist itself and satisfies
|
|
469
|
+
* `ContentReads` in-process from the working tree; `@semiont/jobs` holds the
|
|
470
|
+
* Worker and can only reach the record over the wire. make-meaning depends on
|
|
471
|
+
* jobs, so anything both need has to sit under both (SINGLE-KB-MOUNT P4).
|
|
445
472
|
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
* over the wire (HttpContentTransport → the /anchored-text routes).
|
|
473
|
+
* Where the Archivist IS lives in `@semiont/core/node` (`archivistEndpoint`),
|
|
474
|
+
* not here: an address is a config value plus an environment variable, and
|
|
475
|
+
* the gateway needs it without needing a byte reader. One resolution, shared
|
|
476
|
+
* with the gateway's own proxying — the address and the secret are deployment
|
|
477
|
+
* facts, and a second copy of either is a second thing to get wrong.
|
|
452
478
|
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
* — the re-anchor path, whose artifact IS the job — use the transport's
|
|
457
|
-
* `putAnchoredText` directly, not this adapter.
|
|
479
|
+
* Absence fails loudly. A missing host or secret is a misconfiguration, never
|
|
480
|
+
* a reason to fall back to reading a tree locally — the point of
|
|
481
|
+
* SINGLE-KB-MOUNT is that exactly one process touches it.
|
|
458
482
|
*/
|
|
459
483
|
|
|
460
|
-
|
|
484
|
+
/**
|
|
485
|
+
* The byte read, and nothing else — DERIVED from the transport contract so it
|
|
486
|
+
* cannot drift from it. Keyed by ResourceId because that is the transport's
|
|
487
|
+
* key and the Archivist's: no caller converts to a tree address only to have
|
|
488
|
+
* the far side convert back.
|
|
489
|
+
*/
|
|
490
|
+
type ContentReads = Pick<IContentTransport, 'getBinary'>;
|
|
491
|
+
/** Which half of the lookup failed — the gateway serves two different 404s. */
|
|
492
|
+
type MissingReason = 'resource' | 'representation';
|
|
493
|
+
declare class RepresentationMissing extends Error {
|
|
494
|
+
readonly resourceId: string;
|
|
495
|
+
readonly reason: MissingReason;
|
|
496
|
+
constructor(resourceId: string, reason: MissingReason);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* `ContentReads` against the Archivist — how a fleet process that holds no KB
|
|
500
|
+
* mount reads bytes (SINGLE-KB-MOUNT P4).
|
|
501
|
+
*
|
|
502
|
+
* The address resolves HERE, at construction, not per read: a process with no
|
|
503
|
+
* Archivist configured must die while an operator is watching it boot, rather
|
|
504
|
+
* than fail every resource for the life of the process.
|
|
505
|
+
*
|
|
506
|
+
* A miss arrives as `RepresentationMissing` — the same error the in-process
|
|
507
|
+
* face throws for the same fact, so no caller can tell whether the bytes were
|
|
508
|
+
* a hop away. `reason` rides the wire precisely so this side need not guess.
|
|
509
|
+
*/
|
|
510
|
+
declare function archivistContentReads(config: ArchivistAddressConfig): ContentReads;
|
|
461
511
|
|
|
462
512
|
/**
|
|
463
513
|
* PDF Text Layer Types
|
|
@@ -533,5 +583,5 @@ interface PdfTextLayer extends AnchoredText {
|
|
|
533
583
|
|
|
534
584
|
declare function extractPdfTextLayer(bytes: Uint8Array | Buffer): Promise<PdfTextLayer | null>;
|
|
535
585
|
|
|
536
|
-
export { ChecksumMismatchError, EXTRACTORS, MAX_PDF_BYTES, WorkingTreeStore,
|
|
537
|
-
export type { AnchoredTextStore, CachedAnchoredText, CachedLine, ContentExtractor, ExtractedText, ExtractionCache, ExtractionDecline, PdfFormField, PdfPageInfo, PdfTextLayer, StoredResource };
|
|
586
|
+
export { ChecksumMismatchError, EXTRACTORS, MAX_PDF_BYTES, RepresentationMissing, WorkingTreeStore, archivistContentReads, calculateChecksum, createAnchoredTextStore, extractPdfTextLayer, verifyChecksum, withinByteBudget };
|
|
587
|
+
export type { AnchoredTextStore, CachedAnchoredText, CachedLine, ContentExtractor, ContentReads, ExtractedText, ExtractionCache, ExtractionDecline, MissingReason, PdfFormField, PdfPageInfo, PdfTextLayer, StoredResource };
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
// src/working-tree-store.ts
|
|
2
|
-
import { promises as fs } from "fs";
|
|
2
|
+
import { promises as fs, createReadStream, createWriteStream } from "fs";
|
|
3
3
|
import { execFileSync } from "child_process";
|
|
4
|
+
import { createHash, randomUUID } from "crypto";
|
|
5
|
+
import { Readable } from "stream";
|
|
6
|
+
import { pipeline } from "stream/promises";
|
|
4
7
|
import path from "path";
|
|
5
|
-
|
|
6
|
-
// src/checksum.ts
|
|
7
|
-
import { createHash } from "crypto";
|
|
8
|
-
function calculateChecksum(content) {
|
|
8
|
+
function hashingTap() {
|
|
9
9
|
const hash = createHash("sha256");
|
|
10
|
-
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
let byteSize = 0;
|
|
11
|
+
return {
|
|
12
|
+
update(chunk2) {
|
|
13
|
+
hash.update(chunk2);
|
|
14
|
+
byteSize += chunk2.length;
|
|
15
|
+
},
|
|
16
|
+
get byteSize() {
|
|
17
|
+
return byteSize;
|
|
18
|
+
},
|
|
19
|
+
digest() {
|
|
20
|
+
return hash.digest("hex");
|
|
21
|
+
}
|
|
22
|
+
};
|
|
15
23
|
}
|
|
16
|
-
|
|
17
|
-
// src/working-tree-store.ts
|
|
18
24
|
var WorkingTreeStore = class {
|
|
19
25
|
projectRoot;
|
|
20
26
|
gitSync;
|
|
@@ -30,34 +36,68 @@ var WorkingTreeStore = class {
|
|
|
30
36
|
/**
|
|
31
37
|
* Write content to disk at the location indicated by storageUri.
|
|
32
38
|
*
|
|
33
|
-
* API/GUI/AI path: caller provides bytes
|
|
39
|
+
* API/GUI/AI path: caller provides bytes — as a Buffer it already holds, or
|
|
40
|
+
* as a stream (the Archivist's write endpoint hands the request body
|
|
41
|
+
* straight through, SINGLE-KB-MOUNT P2/D7: memory stays bounded by the
|
|
42
|
+
* chunk, never the representation).
|
|
34
43
|
*
|
|
35
|
-
*
|
|
44
|
+
* Atomic either way: bytes stream into a temp file beside the target and
|
|
45
|
+
* are renamed into place only once complete — and only once
|
|
46
|
+
* `expectedChecksum`, when given, agrees with what actually arrived. A
|
|
47
|
+
* mismatch or a torn stream leaves the target untouched (a version being
|
|
48
|
+
* overwritten survives) and no temp file behind, so the Stower's `register`
|
|
49
|
+
* can never find partial bytes an event names.
|
|
50
|
+
*
|
|
51
|
+
* @param content - Raw bytes to write, whole or streamed
|
|
36
52
|
* @param storageUri - file:// URI (e.g. "file://docs/overview.md")
|
|
53
|
+
* @throws ChecksumMismatchError when expectedChecksum disagrees with the body
|
|
37
54
|
* @returns Stored resource metadata
|
|
38
55
|
*/
|
|
39
56
|
async store(content, storageUri, options) {
|
|
40
57
|
const filePath = this.resolveUri(storageUri);
|
|
41
|
-
const
|
|
42
|
-
this.logger?.debug("Storing resource", { storageUri
|
|
58
|
+
const source = Buffer.isBuffer(content) ? Readable.from([content]) : content;
|
|
59
|
+
this.logger?.debug("Storing resource", { storageUri });
|
|
43
60
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
const tempPath = `${filePath}.${randomUUID()}.tmp`;
|
|
62
|
+
const tap = hashingTap();
|
|
63
|
+
try {
|
|
64
|
+
await pipeline(
|
|
65
|
+
source,
|
|
66
|
+
async function* (chunks) {
|
|
67
|
+
for await (const chunk2 of chunks) {
|
|
68
|
+
tap.update(chunk2);
|
|
69
|
+
yield chunk2;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
createWriteStream(tempPath)
|
|
73
|
+
);
|
|
74
|
+
const checksum = tap.digest();
|
|
75
|
+
const byteSize = tap.byteSize;
|
|
76
|
+
if (options?.expectedChecksum !== void 0 && options.expectedChecksum !== checksum) {
|
|
77
|
+
throw new ChecksumMismatchError(storageUri, options.expectedChecksum, checksum);
|
|
78
|
+
}
|
|
79
|
+
await fs.rename(tempPath, filePath);
|
|
80
|
+
if (this.shouldRunGit(options?.noGit)) {
|
|
81
|
+
execFileSync("git", ["add", filePath], { cwd: this.projectRoot });
|
|
82
|
+
}
|
|
83
|
+
this.logger?.info("Resource stored", { storageUri, checksum, byteSize });
|
|
84
|
+
return {
|
|
85
|
+
storageUri,
|
|
86
|
+
checksum,
|
|
87
|
+
byteSize,
|
|
88
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
89
|
+
};
|
|
90
|
+
} catch (error) {
|
|
91
|
+
await fs.rm(tempPath, { force: true });
|
|
92
|
+
throw error;
|
|
47
93
|
}
|
|
48
|
-
this.logger?.info("Resource stored", { storageUri, checksum, byteSize: content.length });
|
|
49
|
-
return {
|
|
50
|
-
storageUri,
|
|
51
|
-
checksum,
|
|
52
|
-
byteSize: content.length,
|
|
53
|
-
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
54
|
-
};
|
|
55
94
|
}
|
|
56
95
|
/**
|
|
57
96
|
* Read an existing file and return its metadata.
|
|
58
97
|
*
|
|
59
|
-
*
|
|
60
|
-
* If expectedChecksum is provided, throws
|
|
98
|
+
* The file is already on disk; this hashes it by streaming to confirm what
|
|
99
|
+
* it is, then stages it. If expectedChecksum is provided, throws
|
|
100
|
+
* ChecksumMismatchError on mismatch.
|
|
61
101
|
*
|
|
62
102
|
* @param storageUri - file:// URI (e.g. "file://docs/overview.md")
|
|
63
103
|
* @param expectedChecksum - Optional SHA-256 to verify against
|
|
@@ -68,19 +108,23 @@ var WorkingTreeStore = class {
|
|
|
68
108
|
async register(storageUri, expectedChecksum, options) {
|
|
69
109
|
const filePath = this.resolveUri(storageUri);
|
|
70
110
|
this.logger?.debug("Registering resource", { storageUri });
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
111
|
+
const tap = hashingTap();
|
|
112
|
+
for await (const chunk2 of createReadStream(filePath)) {
|
|
113
|
+
tap.update(chunk2);
|
|
114
|
+
}
|
|
115
|
+
const checksum = tap.digest();
|
|
116
|
+
if (expectedChecksum !== void 0 && checksum !== expectedChecksum) {
|
|
74
117
|
throw new ChecksumMismatchError(storageUri, expectedChecksum, checksum);
|
|
75
118
|
}
|
|
76
119
|
if (this.shouldRunGit(options?.noGit)) {
|
|
77
120
|
execFileSync("git", ["add", filePath], { cwd: this.projectRoot });
|
|
78
121
|
}
|
|
79
|
-
|
|
122
|
+
const byteSize = tap.byteSize;
|
|
123
|
+
this.logger?.info("Resource registered", { storageUri, checksum, byteSize });
|
|
80
124
|
return {
|
|
81
125
|
storageUri,
|
|
82
126
|
checksum,
|
|
83
|
-
byteSize
|
|
127
|
+
byteSize,
|
|
84
128
|
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
85
129
|
};
|
|
86
130
|
}
|
|
@@ -90,6 +134,21 @@ var WorkingTreeStore = class {
|
|
|
90
134
|
* @param storageUri - file:// URI
|
|
91
135
|
* @returns Raw bytes
|
|
92
136
|
*/
|
|
137
|
+
/**
|
|
138
|
+
* The same bytes as `retrieve`, streamed — for the byte paths that must not
|
|
139
|
+
* hold a whole representation in memory (SINGLE-KB-MOUNT D7: the Archivist
|
|
140
|
+
* serves content for every reader now, so its memory cannot be bounded by
|
|
141
|
+
* the largest file anyone asks for).
|
|
142
|
+
*
|
|
143
|
+
* Lazy by construction: the stream is created here but nothing is read
|
|
144
|
+
* until the caller iterates, so a missing file surfaces as an `error` event
|
|
145
|
+
* on the stream rather than a rejected promise. Callers that need the
|
|
146
|
+
* distinction up front should resolve the descriptor first — which is what
|
|
147
|
+
* `resolveRepresentation` does.
|
|
148
|
+
*/
|
|
149
|
+
retrieveStream(storageUri) {
|
|
150
|
+
return createReadStream(this.resolveUri(storageUri));
|
|
151
|
+
}
|
|
93
152
|
async retrieve(storageUri) {
|
|
94
153
|
const filePath = this.resolveUri(storageUri);
|
|
95
154
|
try {
|
|
@@ -195,11 +254,15 @@ The file on disk differs from the recorded checksum. Has it been modified since
|
|
|
195
254
|
actual;
|
|
196
255
|
};
|
|
197
256
|
|
|
198
|
-
// src/
|
|
199
|
-
import {
|
|
200
|
-
function
|
|
201
|
-
const
|
|
202
|
-
|
|
257
|
+
// src/checksum.ts
|
|
258
|
+
import { createHash as createHash2 } from "crypto";
|
|
259
|
+
function calculateChecksum(content) {
|
|
260
|
+
const hash = createHash2("sha256");
|
|
261
|
+
hash.update(content);
|
|
262
|
+
return hash.digest("hex");
|
|
263
|
+
}
|
|
264
|
+
function verifyChecksum(content, checksum) {
|
|
265
|
+
return calculateChecksum(content) === checksum;
|
|
203
266
|
}
|
|
204
267
|
|
|
205
268
|
// src/content-extractor.ts
|
|
@@ -759,8 +822,11 @@ var pdfExtractor = {
|
|
|
759
822
|
if (hit) return hit;
|
|
760
823
|
const outcome = await extractPdf(content);
|
|
761
824
|
if (cache) {
|
|
762
|
-
|
|
763
|
-
|
|
825
|
+
try {
|
|
826
|
+
if (outcome.kind === "declined") await cache.store.write(cache.key, outcome);
|
|
827
|
+
else if (outcome.items) await cache.store.write(cache.key, { ...outcome, items: outcome.items });
|
|
828
|
+
} catch {
|
|
829
|
+
}
|
|
764
830
|
}
|
|
765
831
|
return outcome;
|
|
766
832
|
}
|
|
@@ -916,9 +982,10 @@ function createAnchoredTextStore(dir, logger) {
|
|
|
916
982
|
await fs2.promises.mkdir(path3.dirname(target), { recursive: true });
|
|
917
983
|
await fs2.promises.writeFile(temp, JSON.stringify(entry), "utf8");
|
|
918
984
|
await fs2.promises.rename(temp, target);
|
|
919
|
-
} catch {
|
|
985
|
+
} catch (error) {
|
|
920
986
|
await fs2.promises.rm(temp, { force: true }).catch(() => {
|
|
921
987
|
});
|
|
988
|
+
throw error;
|
|
922
989
|
}
|
|
923
990
|
},
|
|
924
991
|
async list() {
|
|
@@ -988,39 +1055,40 @@ function createAnchoredTextStore(dir, logger) {
|
|
|
988
1055
|
};
|
|
989
1056
|
}
|
|
990
1057
|
|
|
991
|
-
// src/
|
|
992
|
-
|
|
1058
|
+
// src/representation-reads.ts
|
|
1059
|
+
import { archivistEndpoint } from "@semiont/core/node";
|
|
1060
|
+
var RepresentationMissing = class extends Error {
|
|
1061
|
+
constructor(resourceId, reason) {
|
|
1062
|
+
super(
|
|
1063
|
+
reason === "resource" ? `Resource not found: ${resourceId}` : `Resource representation not found: no storageUri for ${resourceId}`
|
|
1064
|
+
);
|
|
1065
|
+
this.resourceId = resourceId;
|
|
1066
|
+
this.reason = reason;
|
|
1067
|
+
this.name = "RepresentationMissing";
|
|
1068
|
+
}
|
|
1069
|
+
resourceId;
|
|
1070
|
+
reason;
|
|
1071
|
+
};
|
|
1072
|
+
function archivistContentReads(config) {
|
|
1073
|
+
const { base, headers } = archivistEndpoint(config);
|
|
993
1074
|
return {
|
|
994
|
-
async
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
}
|
|
1004
|
-
},
|
|
1005
|
-
async write(key, outcome) {
|
|
1006
|
-
try {
|
|
1007
|
-
await content.putAnchoredText(key, outcome);
|
|
1008
|
-
} catch (error) {
|
|
1009
|
-
logger?.debug("Anchored-text cache: transport write failed \u2014 entry not stored", {
|
|
1010
|
-
key,
|
|
1011
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
1012
|
-
});
|
|
1075
|
+
getBinary: async (resourceId) => {
|
|
1076
|
+
const url = `${base}/resources/${encodeURIComponent(String(resourceId))}/content`;
|
|
1077
|
+
const res = await fetch(url, { headers });
|
|
1078
|
+
if (res.status === 404) {
|
|
1079
|
+
const { reason } = await res.json().catch(() => ({}));
|
|
1080
|
+
throw new RepresentationMissing(
|
|
1081
|
+
String(resourceId),
|
|
1082
|
+
reason === "representation" ? "representation" : "resource"
|
|
1083
|
+
);
|
|
1013
1084
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
try {
|
|
1017
|
-
return await content.listAnchoredTextKeys();
|
|
1018
|
-
} catch (error) {
|
|
1019
|
-
logger?.debug("Anchored-text cache: transport list failed \u2014 treating as empty", {
|
|
1020
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
1021
|
-
});
|
|
1022
|
-
return [];
|
|
1085
|
+
if (!res.ok) {
|
|
1086
|
+
throw new Error(`Archivist content read failed for ${String(resourceId)}: ${res.status} ${res.statusText}`);
|
|
1023
1087
|
}
|
|
1088
|
+
return {
|
|
1089
|
+
data: await res.arrayBuffer(),
|
|
1090
|
+
contentType: res.headers.get("content-type") || "application/octet-stream"
|
|
1091
|
+
};
|
|
1024
1092
|
}
|
|
1025
1093
|
};
|
|
1026
1094
|
}
|
|
@@ -1028,11 +1096,11 @@ export {
|
|
|
1028
1096
|
ChecksumMismatchError,
|
|
1029
1097
|
EXTRACTORS,
|
|
1030
1098
|
MAX_PDF_BYTES,
|
|
1099
|
+
RepresentationMissing,
|
|
1031
1100
|
WorkingTreeStore,
|
|
1032
|
-
|
|
1101
|
+
archivistContentReads,
|
|
1033
1102
|
calculateChecksum,
|
|
1034
1103
|
createAnchoredTextStore,
|
|
1035
|
-
deriveStorageUri,
|
|
1036
1104
|
extractPdfTextLayer,
|
|
1037
1105
|
verifyChecksum,
|
|
1038
1106
|
withinByteBudget
|
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 /** 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"]}
|
|
1
|
+
{"version":3,"sources":["../src/working-tree-store.ts","../src/checksum.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/representation-reads.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?): Adopt a file already on disk and\n * return its metadata. The CLI path (the file arrived by other means) and\n * the event-apply path (the Stower staging bytes an event names) both use\n * it. Streams the file to hash it — never holds it. If expectedChecksum is\n * 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, createReadStream, createWriteStream } from 'fs';\nimport { execFileSync } from 'child_process';\nimport { createHash, randomUUID } from 'crypto';\nimport { Readable } from 'stream';\nimport { pipeline } from 'stream/promises';\nimport path from 'path';\nimport type { SemiontProject } from '@semiont/core/node';\nimport type { Logger } from '@semiont/core';\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 * sha256 + byte count over a chunk stream, held in one place because both\n * write paths need exactly this and neither may hold the file: `store` taps\n * bytes on their way to disk, `register` taps them on the way back off it.\n * Two copies would be two chances to disagree about what a checksum is.\n */\nfunction hashingTap() {\n const hash = createHash('sha256');\n let byteSize = 0;\n return {\n update(chunk: Buffer): void {\n hash.update(chunk);\n byteSize += chunk.length;\n },\n get byteSize(): number {\n return byteSize;\n },\n digest(): string {\n return hash.digest('hex');\n },\n };\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 — as a Buffer it already holds, or\n * as a stream (the Archivist's write endpoint hands the request body\n * straight through, SINGLE-KB-MOUNT P2/D7: memory stays bounded by the\n * chunk, never the representation).\n *\n * Atomic either way: bytes stream into a temp file beside the target and\n * are renamed into place only once complete — and only once\n * `expectedChecksum`, when given, agrees with what actually arrived. A\n * mismatch or a torn stream leaves the target untouched (a version being\n * overwritten survives) and no temp file behind, so the Stower's `register`\n * can never find partial bytes an event names.\n *\n * @param content - Raw bytes to write, whole or streamed\n * @param storageUri - file:// URI (e.g. \"file://docs/overview.md\")\n * @throws ChecksumMismatchError when expectedChecksum disagrees with the body\n * @returns Stored resource metadata\n */\n async store(\n content: Buffer | Readable,\n storageUri: string,\n options?: { noGit?: boolean; expectedChecksum?: string },\n ): Promise<StoredResource> {\n const filePath = this.resolveUri(storageUri);\n const source = Buffer.isBuffer(content) ? Readable.from([content]) : content;\n\n this.logger?.debug('Storing resource', { storageUri });\n\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n const tempPath = `${filePath}.${randomUUID()}.tmp`;\n const tap = hashingTap();\n\n try {\n await pipeline(\n source,\n async function* (chunks: AsyncIterable<Buffer>) {\n for await (const chunk of chunks) {\n tap.update(chunk);\n yield chunk;\n }\n },\n createWriteStream(tempPath),\n );\n\n const checksum = tap.digest();\n const byteSize = tap.byteSize;\n if (options?.expectedChecksum !== undefined && options.expectedChecksum !== checksum) {\n throw new ChecksumMismatchError(storageUri, options.expectedChecksum, checksum);\n }\n await fs.rename(tempPath, filePath);\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 });\n\n return {\n storageUri,\n checksum,\n byteSize,\n created: new Date().toISOString(),\n };\n } catch (error) {\n await fs.rm(tempPath, { force: true });\n throw error;\n }\n }\n\n /**\n * Read an existing file and return its metadata.\n *\n * The file is already on disk; this hashes it by streaming to confirm what\n * it is, then stages it. If expectedChecksum is provided, throws\n * 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 // Hashed by streaming, never read whole. This runs on the event-apply\n // path in the SAME process that streamed the upload in, so a `readFile`\n // here would re-materialize bytes the write path was careful to keep\n // chunk-bounded — the D7 memory bound would hold only until the event\n // applied. The second hash itself is kept deliberately: it is the moment\n // the record commits to \"these bytes are what this event says\", and the\n // CLI path writes files this process never saw.\n const tap = hashingTap();\n for await (const chunk of createReadStream(filePath)) {\n tap.update(chunk as Buffer);\n }\n const checksum = tap.digest();\n\n if (expectedChecksum !== undefined && checksum !== 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 const byteSize = tap.byteSize;\n this.logger?.info('Resource registered', { storageUri, checksum, byteSize });\n\n return {\n storageUri,\n checksum,\n byteSize,\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 /**\n * The same bytes as `retrieve`, streamed — for the byte paths that must not\n * hold a whole representation in memory (SINGLE-KB-MOUNT D7: the Archivist\n * serves content for every reader now, so its memory cannot be bounded by\n * the largest file anyone asks for).\n *\n * Lazy by construction: the stream is created here but nothing is read\n * until the caller iterates, so a missing file surfaces as an `error` event\n * on the stream rather than a rejected promise. Callers that need the\n * distinction up front should resolve the descriptor first — which is what\n * `resolveRepresentation` does.\n */\n retrieveStream(storageUri: string): Readable {\n return createReadStream(this.resolveUri(storageUri));\n }\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 * 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 //\n // The catch is HERE rather than inside the store: `write` throws, so this\n // is where \"best-effort\" is chosen, by the seam that wants it. Previously\n // the store swallowed for every caller and this comment described a\n // property it did not own.\n if (cache) {\n try {\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 } catch {\n // Cached nothing; the outcome below is still correct.\n }\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 gateway 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 /**\n * Record an extraction outcome under the content checksum of its source\n * bytes. **THROWS on failure: a write that returns has written.**\n *\n * Asymmetric with `read` above, which never throws, and deliberately so —\n * a miss is a normal answer, a failed write is not. The store used to\n * swallow for everyone, which forced the one caller that needs a throw\n * (the Smelter's re-anchor publish, whose `smelt:rebuild-anchors-failed`\n * accounting rides on it) to route around the store entirely. Now the\n * contract is honest and **leniency is the caller's**, stated where it is\n * wanted: the read-through seam in `pdf-extractor` catches, because a\n * store may make extraction faster but must never make it fail.\n */\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 (error) {\n // Clean up the partial temp, then RETHROW. A write that returns\n // has written — see the interface doc. Callers that want\n // best-effort say so at their own call site.\n await fs.promises.rm(temp, { force: true }).catch(() => {});\n throw error;\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 * Reading a resource's bytes: the contract, the way it fails, and the\n * implementation that reaches the Archivist over HTTP.\n *\n * These live in `@semiont/content` because this package IS the byte layer —\n * the Archivist's whole job — and because the readers span the dependency\n * graph. `@semiont/make-meaning` holds the Archivist itself and satisfies\n * `ContentReads` in-process from the working tree; `@semiont/jobs` holds the\n * Worker and can only reach the record over the wire. make-meaning depends on\n * jobs, so anything both need has to sit under both (SINGLE-KB-MOUNT P4).\n *\n * Where the Archivist IS lives in `@semiont/core/node` (`archivistEndpoint`),\n * not here: an address is a config value plus an environment variable, and\n * the gateway needs it without needing a byte reader. One resolution, shared\n * with the gateway's own proxying — the address and the secret are deployment\n * facts, and a second copy of either is a second thing to get wrong.\n *\n * Absence fails loudly. A missing host or secret is a misconfiguration, never\n * a reason to fall back to reading a tree locally — the point of\n * SINGLE-KB-MOUNT is that exactly one process touches it.\n */\n\nimport type { IContentTransport, ResourceId } from '@semiont/core';\nimport { archivistEndpoint, type ArchivistAddressConfig } from '@semiont/core/node';\n\n/**\n * The byte read, and nothing else — DERIVED from the transport contract so it\n * cannot drift from it. Keyed by ResourceId because that is the transport's\n * key and the Archivist's: no caller converts to a tree address only to have\n * the far side convert back.\n */\nexport type ContentReads = Pick<IContentTransport, 'getBinary'>;\n\n/** Which half of the lookup failed — the gateway serves two different 404s. */\nexport type MissingReason = 'resource' | 'representation';\n\nexport class RepresentationMissing extends Error {\n constructor(readonly resourceId: string, readonly reason: MissingReason) {\n // NAMES THE RESOURCE. The client-visible wording is the gateway's, built\n // from `reason` — so this message is free to be diagnostic, and must be:\n // an operator reading a log needs to know which resource, which is what\n // the pre-collapse message gave them.\n super(\n reason === 'resource'\n ? `Resource not found: ${resourceId}`\n : `Resource representation not found: no storageUri for ${resourceId}`,\n );\n this.name = 'RepresentationMissing';\n }\n}\n\n/**\n * `ContentReads` against the Archivist — how a fleet process that holds no KB\n * mount reads bytes (SINGLE-KB-MOUNT P4).\n *\n * The address resolves HERE, at construction, not per read: a process with no\n * Archivist configured must die while an operator is watching it boot, rather\n * than fail every resource for the life of the process.\n *\n * A miss arrives as `RepresentationMissing` — the same error the in-process\n * face throws for the same fact, so no caller can tell whether the bytes were\n * a hop away. `reason` rides the wire precisely so this side need not guess.\n */\nexport function archivistContentReads(config: ArchivistAddressConfig): ContentReads {\n const { base, headers } = archivistEndpoint(config);\n\n return {\n getBinary: async (resourceId: ResourceId) => {\n const url = `${base}/resources/${encodeURIComponent(String(resourceId))}/content`;\n const res = await fetch(url, { headers });\n\n if (res.status === 404) {\n const { reason } = await res.json().catch(() => ({})) as { reason?: string };\n throw new RepresentationMissing(\n String(resourceId),\n reason === 'representation' ? 'representation' : 'resource',\n );\n }\n if (!res.ok) {\n throw new Error(`Archivist content read failed for ${String(resourceId)}: ${res.status} ${res.statusText}`);\n }\n\n return {\n data: await res.arrayBuffer(),\n contentType: res.headers.get('content-type') || 'application/octet-stream',\n };\n },\n };\n}\n"],"mappings":";AAwBA,SAAS,YAAY,IAAI,kBAAkB,yBAAyB;AACpE,SAAS,oBAAoB;AAC7B,SAAS,YAAY,kBAAkB;AACvC,SAAS,gBAAgB;AACzB,SAAS,gBAAgB;AACzB,OAAO,UAAU;AAoBjB,SAAS,aAAa;AAClB,QAAM,OAAO,WAAW,QAAQ;AAChC,MAAI,WAAW;AACf,SAAO;AAAA,IACH,OAAOA,QAAqB;AACxB,WAAK,OAAOA,MAAK;AACjB,kBAAYA,OAAM;AAAA,IACtB;AAAA,IACA,IAAI,WAAmB;AACnB,aAAO;AAAA,IACX;AAAA,IACA,SAAiB;AACb,aAAO,KAAK,OAAO,KAAK;AAAA,IAC5B;AAAA,EACJ;AACJ;AAKO,IAAM,mBAAN,MAAuB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,SAAyB,QAAiB;AACpD,SAAK,cAAc,QAAQ;AAC3B,SAAK,UAAU,QAAQ;AACvB,SAAK,SAAS;AAAA,EAChB;AAAA,EAEQ,aAAa,OAA0B;AAC7C,WAAO,KAAK,WAAW,CAAC;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBA,MAAM,MACJ,SACA,YACA,SACyB;AACzB,UAAM,WAAW,KAAK,WAAW,UAAU;AAC3C,UAAM,SAAS,OAAO,SAAS,OAAO,IAAI,SAAS,KAAK,CAAC,OAAO,CAAC,IAAI;AAErE,SAAK,QAAQ,MAAM,oBAAoB,EAAE,WAAW,CAAC;AAErD,UAAM,GAAG,MAAM,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1D,UAAM,WAAW,GAAG,QAAQ,IAAI,WAAW,CAAC;AAC5C,UAAM,MAAM,WAAW;AAEvB,QAAI;AACF,YAAM;AAAA,QACJ;AAAA,QACA,iBAAiB,QAA+B;AAC9C,2BAAiBA,UAAS,QAAQ;AAChC,gBAAI,OAAOA,MAAK;AAChB,kBAAMA;AAAA,UACR;AAAA,QACF;AAAA,QACA,kBAAkB,QAAQ;AAAA,MAC5B;AAEA,YAAM,WAAW,IAAI,OAAO;AAC5B,YAAM,WAAW,IAAI;AACrB,UAAI,SAAS,qBAAqB,UAAa,QAAQ,qBAAqB,UAAU;AACpF,cAAM,IAAI,sBAAsB,YAAY,QAAQ,kBAAkB,QAAQ;AAAA,MAChF;AACA,YAAM,GAAG,OAAO,UAAU,QAAQ;AAElC,UAAI,KAAK,aAAa,SAAS,KAAK,GAAG;AACrC,qBAAa,OAAO,CAAC,OAAO,QAAQ,GAAG,EAAE,KAAK,KAAK,YAAY,CAAC;AAAA,MAClE;AAEA,WAAK,QAAQ,KAAK,mBAAmB,EAAE,YAAY,UAAU,SAAS,CAAC;AAEvE,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAS,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC;AAAA,IACF,SAAS,OAAO;AACd,YAAM,GAAG,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC;AACrC,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAM,SAAS,YAAoB,kBAA2B,SAAwD;AACpH,UAAM,WAAW,KAAK,WAAW,UAAU;AAE3C,SAAK,QAAQ,MAAM,wBAAwB,EAAE,WAAW,CAAC;AASzD,UAAM,MAAM,WAAW;AACvB,qBAAiBA,UAAS,iBAAiB,QAAQ,GAAG;AACpD,UAAI,OAAOA,MAAe;AAAA,IAC5B;AACA,UAAM,WAAW,IAAI,OAAO;AAE5B,QAAI,qBAAqB,UAAa,aAAa,kBAAkB;AACnE,YAAM,IAAI,sBAAsB,YAAY,kBAAkB,QAAQ;AAAA,IACxE;AAEA,QAAI,KAAK,aAAa,SAAS,KAAK,GAAG;AACrC,mBAAa,OAAO,CAAC,OAAO,QAAQ,GAAG,EAAE,KAAK,KAAK,YAAY,CAAC;AAAA,IAClE;AAEA,UAAM,WAAW,IAAI;AACrB,SAAK,QAAQ,KAAK,uBAAuB,EAAE,YAAY,UAAU,SAAS,CAAC;AAE3E,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAS,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,eAAe,YAA8B;AAC3C,WAAO,iBAAiB,KAAK,WAAW,UAAU,CAAC;AAAA,EACrD;AAAA,EAEA,MAAM,SAAS,YAAqC;AAClD,UAAM,WAAW,KAAK,WAAW,UAAU;AAC3C,QAAI;AACF,aAAO,MAAM,GAAG,SAAS,QAAQ;AAAA,IACnC,SAAS,OAAY;AACnB,UAAI,MAAM,SAAS,UAAU;AAC3B,cAAM,IAAI,MAAM,uBAAuB,UAAU,EAAE;AAAA,MACrD;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,KAAK,SAAiB,OAAe,SAA8C;AACvF,UAAM,WAAW,KAAK,WAAW,OAAO;AACxC,UAAM,SAAS,KAAK,WAAW,KAAK;AAEpC,SAAK,QAAQ,MAAM,mBAAmB,EAAE,SAAS,MAAM,CAAC;AAExD,UAAM,GAAG,MAAM,KAAK,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AAExD,QAAI,KAAK,aAAa,SAAS,KAAK,GAAG;AAErC,mBAAa,OAAO,CAAC,MAAM,UAAU,MAAM,GAAG,EAAE,KAAK,KAAK,YAAY,CAAC;AAAA,IACzE,OAAO;AACL,YAAM,GAAG,OAAO,UAAU,MAAM;AAAA,IAClC;AAEA,SAAK,QAAQ,KAAK,kBAAkB,EAAE,SAAS,MAAM,CAAC;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,MAAM,OAAO,YAAoB,SAAkE;AACjG,UAAM,WAAW,KAAK,WAAW,UAAU;AAC3C,UAAM,WAAW,SAAS,YAAY;AAEtC,SAAK,QAAQ,MAAM,qBAAqB,EAAE,YAAY,SAAS,CAAC;AAEhE,UAAM,SAAS,KAAK,aAAa,SAAS,KAAK;AAE/C,QAAI,QAAQ;AACV,YAAM,UAAU,WACZ,CAAC,MAAM,YAAY,QAAQ,IAC3B,CAAC,MAAM,QAAQ;AACnB,mBAAa,OAAO,SAAS,EAAE,KAAK,KAAK,YAAY,CAAC;AACtD,WAAK,QAAQ,KAAK,oBAAoB,EAAE,YAAY,UAAU,KAAK,KAAK,CAAC;AACzE;AAAA,IACF;AAEA,QAAI,UAAU;AACZ,WAAK,QAAQ,KAAK,mDAAmD,EAAE,WAAW,CAAC;AACnF;AAAA,IACF;AAEA,QAAI;AACF,YAAM,GAAG,OAAO,QAAQ;AACxB,WAAK,QAAQ,KAAK,oBAAoB,EAAE,WAAW,CAAC;AAAA,IACtD,SAAS,OAAY;AACnB,UAAI,MAAM,SAAS,UAAU;AAC3B,aAAK,QAAQ,KAAK,gCAAgC,EAAE,WAAW,CAAC;AAChE;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,WAAW,YAA4B;AACrC,QAAI,CAAC,WAAW,WAAW,SAAS,GAAG;AACrC,YAAM,IAAI,MAAM,kDAAkD,UAAU,EAAE;AAAA,IAChF;AACA,UAAM,eAAe,WAAW,MAAM,UAAU,MAAM;AACtD,WAAO,KAAK,KAAK,KAAK,aAAa,YAAY;AAAA,EACjD;AACF;AAOO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC/C,YACW,YACA,UACA,QACT;AACA;AAAA,MACE,yBAAyB,UAAU,cAAc,SAAS,MAAM,GAAG,CAAC,CAAC,eAAe,OAAO,MAAM,GAAG,CAAC,CAAC;AAAA;AAAA,IAExG;AAPS;AACA;AACA;AAMT,SAAK,OAAO;AAAA,EACd;AAAA,EATW;AAAA,EACA;AAAA,EACA;AAQb;;;ACzVA,SAAS,cAAAC,mBAAkB;AAOpB,SAAS,kBAAkB,SAAkC;AAClE,QAAM,OAAOA,YAAW,QAAQ;AAChC,OAAK,OAAO,OAAO;AACnB,SAAO,KAAK,OAAO,KAAK;AAC1B;AAQO,SAAS,eAAe,SAA0B,UAA2B;AAClF,SAAO,kBAAkB,OAAO,MAAM;AACxC;;;ACTA,SAAS,4BAAmE;;;ACE5E,SAAS,YAAAC,iBAAkC;;;ACR3C,YAAY,WAAW;;;ACgBvB,SAAS,qBAAqB;AAC9B,OAAOC,WAAU;AAEjB,IAAMC,WAAU,cAAc,YAAY,GAAG;AAEtC,IAAM,yBACX,GAAGD,MAAK,KAAKA,MAAK,QAAQC,SAAQ,QAAQ,yBAAyB,CAAC,GAAG,gBAAgB,CAAC,GAAGD,MAAK,GAAG;;;ADpBrG,SAAS,UAAU,UAAU,UAAU,SAAS,YAAY,iBAAmC;AAS/F,SAAS,YAAY,OAAqC;AACtD,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO;AAC7B,QAAM,EAAE,MAAM,OAAO,MAAM,KAAK,IAAI;AACpC,MAAI,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,MAAM,KAAK,EAAG,QAAO;AACjE,MAAI,CAAC,SAAS,IAAI,KAAK,OAAO,EAAG,QAAO;AACxC,MAAI,CAAC,QAAQ,IAAI,KAAK,KAAK,SAAS,KAAK,CAAC,KAAK,MAAM,QAAQ,EAAG,QAAO;AACvE,QAAM,CAAC,IAAI,IAAI,IAAI,EAAE,IAAI;AACzB,SAAO;AAAA,IACH;AAAA,IACA,OAAO,MAAM,KAAK;AAAA,IAClB,MAAM,OAAO;AAAA;AAAA,IACb,GAAG,KAAK,IAAI,IAAI,EAAE;AAAA,IAClB,GAAG,KAAK,IAAI,IAAI,EAAE;AAAA,IAClB,OAAO,KAAK,IAAI,KAAK,EAAE;AAAA,IACvB,QAAQ,KAAK,IAAI,KAAK,EAAE;AAAA,EAC5B;AACJ;AAQA,eAAe,eAAe,KAAsD;AAChF,QAAM,eAAe,MAAM,IAAI,gBAAgB;AAC/C,MAAI,CAAC,aAAc,QAAO,CAAC;AAC3B,QAAM,SAAS,oBAAI,IAA0B;AAC7C,aAAW,WAAW,OAAO,OAAO,YAAY,GAAG;AAC/C,QAAI,CAAC,QAAQ,OAAO,EAAG;AACvB,eAAW,SAAS,SAAS;AACzB,YAAM,QAAQ,YAAY,KAAK;AAC/B,UAAI,SAAS,CAAC,OAAO,IAAI,MAAM,IAAI,EAAG,QAAO,IAAI,MAAM,MAAM,KAAK;AAAA,IACtE;AAAA,EACJ;AACA,SAAO,CAAC,GAAG,OAAO,OAAO,CAAC;AAC9B;AAEA,eAAsB,oBAClB,OAC4B;AAM5B,QAAM,OAAO,IAAI,WAAW,KAAK;AAGjC,QAAM,cAAoB,kBAAY,EAAE,MAAM,qBAAqB,uBAAuB,CAAC;AAE3F,MAAI;AAIA,UAAM,MAAM,MAAM,YAAY;AAC9B,UAAM,QAAuB,CAAC;AAC9B,UAAM,QAAuB,CAAC;AAC9B,QAAI,OAAO;AAEX,aAAS,UAAU,GAAG,WAAW,IAAI,UAAU,WAAW;AACtD,YAAM,OAAO,MAAM,IAAI,QAAQ,OAAO;AACtC,YAAM,WAAW,KAAK,YAAY,EAAE,OAAO,EAAI,CAAC;AAChD,YAAM,UAAU,MAAM,KAAK,eAAe;AAC1C,YAAM,gBAAgB,KAAK;AAO3B,YAAM,QAAQ,WAAW,QAAQ,MAAM,OAAO,SAAS,GAAG,OAAO;AAGjE,iBAAW,QAAQ,MAAM,OAAO;AAC5B,cAAM,KAAK,EAAE,GAAG,MAAM,OAAO,KAAK,QAAQ,eAAe,KAAK,KAAK,MAAM,cAAc,CAAC;AAAA,MAC5F;AACA,cAAQ,MAAM;AACd,cAAQ;AAER,YAAM,KAAK;AAAA,QACP,YAAY;AAAA,QACZ,SAAS,SAAS;AAAA,QAClB,UAAU,SAAS;AAAA,QACnB,WAAW;AAAA,QACX,SAAS,KAAK;AAAA,QACd,cAAc,MAAM,MAAM,SAAS;AAAA,MACvC,CAAC;AAAA,IACL;AAMA,QAAI,CAAC,MAAM,KAAK,CAAC,SAAS,KAAK,YAAY,EAAG,QAAO;AAErD,WAAO,EAAE,OAAO,MAAM,OAAO,QAAQ,MAAM,eAAe,GAAG,EAAE;AAAA,EACnE,UAAE;AAIE,UAAM,YAAY,QAAQ;AAAA,EAC9B;AACJ;;;AE5FA,IAAM,WAAW;AACjB,IAAM,cAAc;AAIpB,IAAM,gBAAgB;AAGtB,IAAM,WAAW;AAEjB,SAAS,OAAO,QAA0B;AACxC,QAAM,SAAS,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAC/C,SAAO,OAAO,KAAK,MAAM,OAAO,SAAS,CAAC,CAAC,KAAK;AAClD;AAGA,SAAS,UAAU,OAAsB,WAAoC;AAC3E,QAAM,OAAwB,CAAC;AAC/B,aAAW,QAAQ,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG;AACvD,UAAM,MAAM,KAAK,KAAK,SAAS,CAAC;AAChC,QAAI,OAAO,KAAK,IAAI,IAAI,CAAC,EAAG,IAAI,KAAK,CAAC,KAAK,UAAW,KAAI,KAAK,IAAI;AAAA,QAC9D,MAAK,KAAK,CAAC,IAAI,CAAC;AAAA,EACvB;AACA,SAAO;AACT;AAEA,SAAS,OAAO,MAAqB,MAAyB;AAC5D,QAAM,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC1C,QAAM,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC1C,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC;AACxD,QAAM,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC;AACvD,SAAO;AAAA,IACL,MAAM,KAAK,IAAI,CAAC,MAAM,KAAK,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,KAAK,GAAG,EAAE,KAAK;AAAA,IACjE;AAAA,IACA;AAAA,IACA,OAAO,QAAQ;AAAA,IACf,QAAQ,MAAM;AAAA,EAChB;AACF;AAGA,SAAS,QAAQ,KAAoB,KAAa,MAA2B;AAC3E,QAAM,QAAqB,CAAC;AAC5B,MAAI,UAAyB,CAAC;AAC9B,aAAW,QAAQ,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG;AACrD,UAAM,WAAW,QAAQ,QAAQ,SAAS,CAAC;AAC3C,QAAI,YAAY,KAAK,KAAK,SAAS,IAAI,SAAS,SAAS,KAAK;AAC5D,YAAM,KAAK,OAAO,SAAS,IAAI,CAAC;AAChC,gBAAU,CAAC;AAAA,IACb;AACA,YAAQ,KAAK,IAAI;AAAA,EACnB;AACA,MAAI,QAAQ,SAAS,EAAG,OAAM,KAAK,OAAO,SAAS,IAAI,CAAC;AACxD,SAAO;AACT;AAMO,SAAS,YAAY,OAAsB,MAAoC;AACpF,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,OAAO,OAAO,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK;AAExE,QAAM,OAAO,UAAU,OAAO,OAAO,aAAa,EAAE,IAAI,CAAC,QAAQ,QAAQ,KAAK,OAAO,UAAU,IAAI,CAAC;AACpG,MAAI,KAAK,SAAS,SAAU,QAAO;AAEnC,QAAM,cAAc,KAAK,CAAC,EAAG;AAC7B,MAAI,cAAc,YAAa,QAAO;AACtC,MAAI,CAAC,KAAK,MAAM,CAAC,QAAQ,IAAI,WAAW,WAAW,EAAG,QAAO;AAI7D,WAAS,SAAS,GAAG,SAAS,aAAa,UAAU;AACnD,UAAM,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,MAAM,EAAG,CAAC;AAC9C,QAAI,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,QAAO;AAAA,EAC7D;AACA,MAAI,KAAK,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,SAAS,KAAK,KAAK,WAAW,CAAC,CAAC,EAAG,QAAO;AAE3E,SAAO;AACT;AAOO,SAAS,YACd,MACA,MACA,QACwC;AACxC,MAAI,OAAO;AACX,QAAM,QAAuB,CAAC;AAC9B,OAAK,QAAQ,CAAC,KAAK,aAAa;AAC9B,YAAQ;AACR,eAAW,QAAQ,KAAK;AACtB,cAAQ;AACR,YAAM,QAAQ,SAAS,KAAK;AAC5B,cAAQ,KAAK;AACb,YAAM,KAAK;AAAA,QACT;AAAA,QACA,KAAK,SAAS,KAAK;AAAA,QACnB;AAAA,QACA,GAAG,KAAK;AAAA,QACR,GAAG,KAAK;AAAA,QACR,OAAO,KAAK;AAAA,QACZ,QAAQ,KAAK;AAAA,MACf,CAAC;AACD,cAAQ;AAAA,IACV;AACA,YAAQ;AAER,QAAI,aAAa,EAAG,SAAQ,IAAI,SAAS,OAAO,IAAI,MAAM,CAAC;AAAA;AAAA,EAC7D,CAAC;AACD,SAAO,EAAE,MAAM,MAAM;AACvB;;;ACnIA,YAAYE,YAAW;AAEvB,SAAS,YAAAC,WAAU,YAAAC,WAAU,YAAAC,WAAU,WAAAC,gBAAe;;;ACXtD,OAAO,UAAU;AAEjB,IAAM,aAAa,MAAM;AACrB,QAAM,QAAQ,IAAI,WAAW,GAAG;AAChC,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC1B,QAAI,IAAI;AACR,aAAS,IAAI,GAAG,IAAI,GAAG,IAAK,KAAK,IAAI,IAAK,aAAc,MAAM,IAAK,MAAM;AACzE,UAAM,CAAC,IAAI;AAAA,EACf;AACA,SAAO;AACX,GAAG;AAEH,SAAS,MAAM,KAAqB;AAChC,MAAI,IAAI;AACR,aAAW,QAAQ,IAAK,KAAI,WAAW,IAAI,QAAQ,GAAI,IAAM,MAAM;AACnE,UAAQ,IAAI,QAAQ;AACxB;AAEA,SAAS,MAAM,MAAc,MAAsB;AAC/C,QAAM,SAAS,OAAO,MAAM,CAAC;AAC7B,SAAO,cAAc,KAAK,MAAM;AAChC,QAAM,OAAO,OAAO,OAAO,CAAC,OAAO,KAAK,MAAM,OAAO,GAAG,IAAI,CAAC;AAC7D,QAAM,MAAM,OAAO,MAAM,CAAC;AAC1B,MAAI,cAAc,MAAM,IAAI,CAAC;AAC7B,SAAO,OAAO,OAAO,CAAC,QAAQ,MAAM,GAAG,CAAC;AAC5C;AAGO,SAAS,UAAU,OAAe,QAAgB,KAAyB;AAC9E,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,MAAM,OAAO,MAAM,SAAS,MAAM;AACxC,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC7B,QAAI,IAAI,MAAM,IAAI;AAClB,WAAO,KAAK,IAAI,QAAQ,IAAI,aAAa,IAAI,QAAQ,GAAG,QAAQ,CAAC,EAC5D,KAAK,KAAK,IAAI,SAAS,CAAC;AAAA,EACjC;AACA,QAAM,OAAO,OAAO,MAAM,EAAE;AAC5B,OAAK,cAAc,OAAO,CAAC;AAC3B,OAAK,cAAc,QAAQ,CAAC;AAC5B,OAAK,CAAC,IAAI;AACV,OAAK,CAAC,IAAI;AACV,SAAO,OAAO,OAAO;AAAA,IACjB,OAAO,KAAK,CAAC,KAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,EAAI,CAAC;AAAA,IAC5D,MAAM,QAAQ,IAAI;AAAA,IAClB,MAAM,QAAQ,KAAK,YAAY,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;AAAA,IACjD,MAAM,QAAQ,OAAO,MAAM,CAAC,CAAC;AAAA,EACjC,CAAC;AACL;;;ADhCA,IAAM,iBAAiB;AACvB,IAAM,YAAY;AAClB,IAAM,aAAa;AAEnB,IAAM,WAA8B,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AA8B9C,IAAM,mBAAmB;AAQzB,SAAS,kBAAkB,OAAe,QAAyB;AACtE,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO;AAChE,MAAI,SAAS,KAAK,UAAU,EAAG,QAAO;AACtC,SAAO,QAAQ,UAAU;AAC7B;AAyBO,SAAS,iBAAiB,SAAmB,WAAuC;AACvF,QAAM,SAAwB,CAAC;AAC/B,QAAM,QAAoB,CAAC;AAC3B,MAAI,MAAgB,CAAC,GAAG,QAAQ;AAEhC,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACrC,UAAM,KAAK,QAAQ,CAAC;AACpB,UAAM,OAAO,UAAU,CAAC;AACxB,QAAI,OAAa,WAAI,MAAM;AACvB,YAAM,KAAK,CAAC,GAAG,GAAG,CAAC;AAAA,IACvB,WAAW,OAAa,WAAI,SAAS;AACjC,YAAM,MAAM,IAAI,KAAK,CAAC,GAAG,QAAQ;AAAA,IACrC,WAAW,OAAa,WAAI,WAAW;AACnC,UAAIC,SAAQ,IAAI,KAAK,KAAK,UAAU,KAAK,KAAK,MAAMC,SAAQ,GAAG;AAC3D,cAAY,YAAK,UAAU,KAAK,IAAgB;AAAA,MACpD;AAAA,IACJ,WAAW,OAAa,WAAI,mBAAmB;AAC3C,YAAM,MAAM,OAAO,CAAC;AACpB,YAAM,QAAQ,OAAO,CAAC;AACtB,YAAM,SAAS,OAAO,CAAC;AACvB,UAAIC,UAAS,GAAG,KAAKD,UAAS,KAAK,KAAKA,UAAS,MAAM,GAAG;AACtD,eAAO,KAAK,EAAE,KAAK,OAAO,QAAQ,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC;AAAA,MACrD;AAAA,IACJ;AAAA,EACJ;AACA,SAAO;AACX;AAYA,SAAS,QAAQ,MAAkC;AAC/C,MAAI,gBAAgB,WAAY,QAAO;AACvC,MAAI,gBAAgB,kBAAmB,QAAO,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,MAAM;AACtG,SAAO;AACX;AAOO,SAAS,MAAM,OAA2E;AAC7F,MAAI,CAACE,UAAS,KAAK,EAAG,QAAO;AAC7B,QAAM,EAAE,OAAO,QAAQ,KAAK,IAAI;AAChC,QAAM,OAAO,QAAQ,MAAM,IAAI;AAC/B,MAAI,CAACF,UAAS,KAAK,KAAK,CAACA,UAAS,MAAM,KAAK,CAAC,KAAM,QAAO;AAC3D,MAAI,SAAS,KAAK,UAAU,EAAG,QAAO;AAEtC,MAAI,SAAS,WAAW;AACpB,WAAO,KAAK,UAAU,QAAQ,SAAS,IAAI,EAAE,OAAO,QAAQ,KAAK,KAAK,IAAI;AAAA,EAC9E;AAEA,MAAI,SAAS,YAAY;AACrB,QAAI,KAAK,SAAS,QAAQ,SAAS,EAAG,QAAO;AAC7C,UAAM,MAAM,IAAI,WAAW,QAAQ,SAAS,CAAC;AAC7C,aAAS,IAAI,GAAG,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,GAAG,KAAK,GAAG;AACnD,UAAI,CAAC,IAAI,KAAK,CAAC;AACf,UAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;AACvB,UAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;AAAA,IAC3B;AACA,WAAO,EAAE,OAAO,QAAQ,IAAI;AAAA,EAChC;AAEA,MAAI,SAAS,gBAAgB;AAKzB,UAAM,WAAW,KAAK,KAAK,QAAQ,CAAC;AACpC,QAAI,KAAK,SAAS,WAAW,OAAQ,QAAO;AAC5C,UAAM,MAAM,IAAI,WAAW,QAAQ,SAAS,CAAC;AAC7C,aAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC7B,eAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC5B,cAAM,MAAM,KAAK,IAAI,YAAY,KAAK,EAAE,IAAM,QAAS,IAAI;AAC3D,cAAM,QAAQ,MAAM,MAAO;AAC3B,cAAM,KAAK,IAAI,QAAQ,KAAK;AAC5B,YAAI,CAAC,IAAI;AACT,YAAI,IAAI,CAAC,IAAI;AACb,YAAI,IAAI,CAAC,IAAI;AAAA,MACjB;AAAA,IACJ;AACA,WAAO,EAAE,OAAO,QAAQ,IAAI;AAAA,EAChC;AAEA,SAAO;AACX;AAOA,IAAM,2BAA2B;AAmBjC,SAAS,aAAa,MAA0B,KAA+B;AAE3E,QAAM,QAAQ,IAAI,WAAW,IAAI,IAAI,KAAK,aAAa,KAAK;AAC5D,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC5B,UAAM,QAAQ,WAAW,MAAM,QAAQ,IAAI,GAAG,wBAAwB;AACtE,UAAM,SAAS,CAAC,UAAmB;AAC/B,mBAAa,KAAK;AAClB,cAAQ,KAAK;AAAA,IACjB;AACA,QAAI;AACA,YAAM,IAAI,KAAK,MAAM;AAAA,IACzB,QAAQ;AACJ,aAAO,IAAI;AAAA,IACf;AAAA,EACJ,CAAC;AACL;AAkBA,eAAsB,kBAClB,OACA,aACiC;AACjC,QAAM,SAAS,cAAc,IAAI,IAAI,WAAW,IAAI;AACpD,QAAM,cAAoB,mBAAY,EAAE,MAAM,IAAI,WAAW,KAAK,GAAG,qBAAqB,uBAAuB,CAAC;AAClH,QAAM,SAAS,oBAAI,IAAyB;AAE5C,MAAI;AACA,UAAM,MAAM,MAAM,YAAY;AAC9B,aAAS,UAAU,GAAG,WAAW,IAAI,UAAU,WAAW;AACtD,UAAI,UAAU,CAAC,OAAO,IAAI,OAAO,EAAG;AACpC,YAAM,OAAO,MAAM,IAAI,QAAQ,OAAO;AACtC,YAAM,MAAM,MAAM,KAAK,gBAAgB;AAEvC,YAAM,SAAsB,CAAC;AAC7B,iBAAW,aAAa,iBAAiB,IAAI,SAAS,IAAI,SAAS,GAAG;AAIlE,YAAI,CAAC,kBAAkB,UAAU,OAAO,UAAU,MAAM,EAAG;AAC3D,cAAM,MAAM,MAAM,MAAM,aAAa,MAAM,UAAU,GAAG,CAAC;AACzD,YAAI,CAAC,IAAK;AACV,eAAO,KAAK;AAAA,UACR,KAAK,UAAU,IAAI,OAAO,IAAI,QAAQ,IAAI,GAAG;AAAA,UAC7C,OAAO,IAAI;AAAA,UACX,QAAQ,IAAI;AAAA,UACZ,KAAK,UAAU;AAAA,QACnB,CAAC;AAAA,MACL;AACA,UAAI,OAAO,SAAS,EAAG,QAAO,IAAI,SAAS,MAAM;AAAA,IACrD;AACA,WAAO;AAAA,EACX,UAAE;AACE,UAAM,YAAY,QAAQ;AAAA,EAC9B;AACJ;;;AE7QA,SAAS,iBAAAG,sBAAqB;AAC9B,SAAS,oBAAoB;AAC7B,SAAS,YAAAC,WAAU,YAAAC,iBAAgB;AAenC,IAAI;AACJ,SAAS,WAAmB;AACxB,MAAI,eAAgB,QAAO;AAC3B,QAAM,OAAgBF,eAAc,YAAY,GAAG,EAAE,wBAAwB;AAC7E,MAAI,CAACC,UAAS,IAAI,KAAK,CAACC,UAAS,KAAK,QAAQ,GAAG;AAC7C,UAAM,IAAI;AAAA,MACN;AAAA,IACJ;AAAA,EACJ;AACA,mBAAiB,KAAK;AACtB,SAAO;AACX;AA4CO,SAAS,aAAa,QAAoC;AAC7D,MAAI,OAAO;AACX,QAAM,QAAmB,CAAC;AAE1B,aAAW,SAAS,UAAU,CAAC,GAAG;AAC9B,eAAW,aAAa,MAAM,cAAc,CAAC,GAAG;AAC5C,iBAAW,QAAQ,UAAU,SAAS,CAAC,GAAG;AACtC,YAAI,YAAY;AAChB,mBAAW,QAAQ,KAAK,SAAS,CAAC,GAAG;AACjC,gBAAM,QAAQ,KAAK,KAAK,KAAK;AAC7B,cAAI,CAAC,MAAO;AACZ,cAAI,UAAW,SAAQ;AACvB,gBAAM,QAAQ,KAAK;AACnB,kBAAQ;AACR,gBAAM,KAAK;AAAA,YACP,MAAM;AAAA,YACN;AAAA,YACA,KAAK,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAYV,MAAM;AAAA,cACF,IAAI,KAAK,KAAK;AAAA,cACd,IAAI,KAAK,KAAK;AAAA,cACd,IAAI,KAAK,KAAK;AAAA,cACd,IAAI,KAAK,KAAK;AAAA,YAClB;AAAA,YACA,YAAY,KAAK;AAAA,UACrB,CAAC;AACD,sBAAY;AAAA,QAChB;AACA,YAAI,UAAW,SAAQ;AAAA,MAC3B;AACA,cAAQ;AAAA,IACZ;AAAA,EACJ;AAGA,SAAO,EAAE,MAAM,KAAK,QAAQ,GAAG,MAAM;AACzC;AAOA,eAAsB,gBAAgB,QAAsC;AACxE,MAAI,OAAO,WAAW,EAAG,QAAO,CAAC;AAGjC,QAAM,SAAS,MAAM,aAAa,OAAO,QAAW;AAAA,IAChD,UAAU,SAAS;AAAA,IACnB,aAAa;AAAA,EACjB,CAAC;AACD,MAAI;AACA,UAAM,UAAqB,CAAC;AAC5B,eAAW,SAAS,QAAQ;AAGxB,YAAM,EAAE,KAAK,IAAI,MAAM,OAAO,UAAU,OAAO,CAAC,GAAG,EAAE,QAAQ,MAAM,MAAM,MAAM,CAAC;AAChF,cAAQ,KAAK,aAAa,KAAK,MAAM,CAAC;AAAA,IAC1C;AACA,WAAO;AAAA,EACX,UAAE;AACE,UAAM,OAAO,UAAU;AAAA,EAC3B;AACJ;;;ACjJA,YAAYC,YAAW;AAWvB,SAAS,YAAY,IAAY,IAAY,WAA6C;AAEtF,QAAM,QAA0B,CAAC,KAAK,UAAU,OAAO,IAAI,KAAK,UAAU,MAAM;AAChF,EAAM,YAAK,eAAe,OAAO,UAAU,GAAG;AAC9C,SAAO;AACX;AAMO,SAAS,gBACZ,OACA,WACA,MACA,YACa;AACb,MAAI,UAAU,SAAS,KAAK,UAAU,UAAU,EAAG,QAAO,CAAC;AAE3D,SAAO,MAAM,IAAI,CAAC,SAAS;AAGvB,UAAM,CAAC,IAAI,EAAE,IAAI,YAAY,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI,SAAS;AAClE,UAAM,CAAC,IAAI,EAAE,IAAI,YAAY,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI,SAAS;AAClE,UAAM,IAAI,KAAK,IAAI,IAAI,EAAE;AACzB,UAAM,IAAI,KAAK,IAAI,IAAI,EAAE;AACzB,WAAO;AAAA,MACH,OAAO,KAAK,QAAQ;AAAA,MACpB,KAAK,KAAK,MAAM;AAAA,MAChB;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,KAAK,IAAI,KAAK,EAAE;AAAA,MACvB,QAAQ,KAAK,IAAI,KAAK,EAAE;AAAA,IAC5B;AAAA,EACJ,CAAC;AACL;;;APXO,IAAM,gBAAgB,MAAM,OAAO;AAKnC,SAAS,iBAAiB,OAAwB;AACvD,SAAO,OAAO,SAAS,KAAK,KAAK,SAAS,KAAK,SAAS;AAC1D;AAIA,IAAM,iBAAiB;AAEvB,SAAS,UAAU,aAAuD;AACxE,MAAI,YAAY,WAAW,EAAG,QAAO;AACrC,QAAM,QAAQ,YAAY,OAAO,CAAC,KAAK,MAAM,MAAM,GAAG,CAAC;AACvD,SAAO;AAAA,IACL,MAAM,KAAK,MAAO,QAAQ,YAAY,SAAU,EAAE,IAAI;AAAA,IACtD,oBAAoB,YAAY,OAAO,CAAC,MAAM,IAAI,cAAc,EAAE;AAAA,IAClE,YAAY,YAAY;AAAA,EAC1B;AACF;AAUA,eAAe,SACb,SACA,aACwB;AAIxB,QAAM,eAAe,MAAM,kBAAkB,SAAS,WAAW;AACjE,MAAI,aAAa,SAAS,EAAG,QAAO,EAAE,MAAM,IAAI,OAAO,CAAC,GAAG,aAAa,CAAC,EAAE;AAG3E,QAAM,QAAQ,CAAC,GAAG,aAAa,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAC3D,QAAM,QAAQ,MAAM,QAAQ,CAAC,SAAS,aAAa,IAAI,IAAI,EAAG,IAAI,CAAC,UAAU,MAAM,GAAG,CAAC;AACvF,QAAM,aAAa,MAAM,gBAAgB,KAAK;AAE9C,QAAM,SAAS,oBAAI,IAA2B;AAC9C,MAAI,SAAS;AACb,aAAW,QAAQ,OAAO;AACxB,UAAM,SAAS,aAAa,IAAI,IAAI;AACpC,QAAI,OAAO;AACX,UAAM,QAAuB,CAAC;AAC9B,UAAM,cAAwB,CAAC;AAC/B,eAAW,SAAS,QAAQ;AAC1B,YAAM,SAAS,WAAW,QAAQ;AAClC,UAAI,CAAC,QAAQ,KAAK,KAAK,EAAG;AAC1B,UAAI,KAAM,SAAQ;AAClB,YAAM,KAAK,GAAG,gBAAgB,OAAO,OAAO,OAAO,MAAM,KAAK,MAAM,CAAC;AACrE,kBAAY,KAAK,GAAG,OAAO,MAAM,IAAI,CAAC,SAAS,KAAK,UAAU,CAAC;AAC/D,cAAQ,OAAO;AAAA,IACjB;AACA,QAAI,KAAM,QAAO,IAAI,MAAM,EAAE,MAAM,OAAO,YAAY,CAAC;AAAA,EACzD;AAIA,SAAO,UAAU,QAAQ,CAAC;AAC5B;AAOA,SAAS,UAAU,QAAoC,YAAmC;AACxF,MAAI,OAAO;AACX,QAAM,QAAuB,CAAC;AAC9B,QAAM,cAAwB,CAAC;AAC/B,aAAW,CAAC,EAAE,IAAI,KAAK,CAAC,GAAG,OAAO,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG;AACxE,QAAI,KAAM,SAAQ;AAClB,UAAM,QAAQ,aAAa,KAAK;AAChC,eAAW,QAAQ,KAAK,OAAO;AAC7B,YAAM,KAAK,EAAE,GAAG,MAAM,OAAO,KAAK,QAAQ,OAAO,KAAK,KAAK,MAAM,MAAM,CAAC;AAAA,IAC1E;AACA,gBAAY,KAAK,GAAG,KAAK,WAAW;AACpC,YAAQ,KAAK;AAAA,EACf;AACA,SAAO,EAAE,MAAM,OAAO,YAAY;AACpC;AAQO,SAAS,iBAAiB,OAAyC;AACxE,SAAOC,UAAS,KAAK,KAAK,MAAM,SAAS,sBAAsB,cAAc;AAC/E;AAUA,SAAS,eAAe,OAAoC;AAC1D,MAAI,OAAO,MAAM;AACjB,QAAM,QAAuB,CAAC,GAAG,MAAM,KAAK;AAC5C,aAAW,SAAS,MAAM,QAAQ;AAChC,UAAM,QAAQ,KAAK,SAAS,GAAG,MAAM,IAAI,KAAK;AAC9C,YAAQ,GAAG,MAAM,IAAI,KAAK,MAAM,KAAK;AAAA;AACrC,UAAM,KAAK;AAAA,MACT;AAAA,MACA,KAAK,QAAQ,MAAM,MAAM;AAAA,MACzB,MAAM,MAAM;AAAA,MACZ,GAAG,MAAM;AAAA,MACT,GAAG,MAAM;AAAA,MACT,OAAO,MAAM;AAAA,MACb,QAAQ,MAAM;AAAA,IAChB,CAAC;AAAA,EACH;AACA,SAAO,EAAE,MAAM,aAAa,MAAM,OAAO,QAAQ,QAAQ,UAAU,IAAI;AACzE;AASA,SAAS,YAAY,OAA2C;AAC9D,QAAM,QAAQ,MAAM,MAAM,IAAI,CAAC,SAAS;AACtC,UAAM,YAAY,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,SAAS,KAAK,UAAU;AAC5E,WAAO,EAAE,MAAM,WAAW,OAAO,YAAY,WAAW,MAAM,IAAI,EAAE;AAAA,EACtE,CAAC;AACD,MAAI,CAAC,MAAM,KAAK,CAAC,MAAM,EAAE,KAAK,EAAG,QAAO;AAExC,MAAI,OAAO;AACX,QAAM,QAAuB,CAAC;AAC9B,aAAW,EAAE,MAAM,WAAW,MAAM,KAAK,OAAO;AAC9C,QAAI,OAAO;AACT,YAAM,WAAW,YAAY,OAAO,KAAK,YAAY,KAAK,MAAM;AAChE,cAAQ,SAAS;AACjB,YAAM,KAAK,GAAG,SAAS,KAAK;AAAA,IAC9B,OAAO;AAEL,YAAM,QAAQ,KAAK,SAAS,KAAK;AACjC,cAAQ,MAAM,KAAK,MAAM,KAAK,WAAW,KAAK,OAAO;AACrD,iBAAW,QAAQ,WAAW;AAC5B,cAAM,KAAK,EAAE,GAAG,MAAM,OAAO,KAAK,QAAQ,OAAO,KAAK,KAAK,MAAM,MAAM,CAAC;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,MAAM,aAAa,MAAM,OAAO,QAAQ,SAAS,UAAU,IAAI;AAC1E;AAEO,IAAM,eAAiC;AAAA;AAAA;AAAA,EAG5C,gBAAgB;AAAA,EAChB,MAAM,QAAQ,SAAS,YAAY,OAAO;AAYxC,UAAM,MAAM,MAAM,OAAO,MAAM,KAAK,MAAM,GAAG;AAC7C,QAAI,IAAK,QAAO;AAEhB,UAAM,UAAU,MAAM,WAAW,OAAO;AAUxC,QAAI,OAAO;AACT,UAAI;AACF,YAAI,QAAQ,SAAS,WAAY,OAAM,MAAM,MAAM,MAAM,MAAM,KAAK,OAAO;AAAA,iBAClE,QAAQ,MAAO,OAAM,MAAM,MAAM,MAAM,MAAM,KAAK,EAAE,GAAG,SAAS,OAAO,QAAQ,MAAM,CAAC;AAAA,MACjG,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;AAGA,eAAe,WAAW,SAA6D;AAInF,MAAI,CAAC,iBAAiB,QAAQ,MAAM,EAAG,QAAO,EAAE,MAAM,YAAY,UAAU,YAAY;AAExF,MAAI;AACJ,MAAI;AACF,YAAQ,MAAM,oBAAoB,OAAO;AAAA,EAC3C,SAAS,OAAO;AACd,WAAO,EAAE,MAAM,YAAY,UAAU,iBAAiB,KAAK,EAAE;AAAA,EAC/D;AAIA,MAAI,CAAC,OAAO;AACV,UAAMC,OAAM,MAAM,SAAS,OAAO;AAClC,QAAI,CAACA,KAAI,KAAM,QAAO,EAAE,MAAM,YAAY,UAAU,gBAAgB;AACpE,UAAMC,cAAa,UAAUD,KAAI,WAAW;AAC5C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAMA,KAAI;AAAA,MACV,OAAOA,KAAI;AAAA,MACX,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,GAAIC,cAAa,EAAE,eAAeA,YAAW,IAAI,CAAC;AAAA,IACpD;AAAA,EACF;AAKA,QAAM,SAAS,MAAM,OAAO,SAAS,IACjC,eAAe,KAAK,IACpB,YAAY,KAAK,KACd,EAAE,MAAM,aAAsB,MAAM,MAAM,MAAM,OAAO,MAAM,OAAO,QAAQ,kBAA2B,UAAU,IAAa;AAOrI,QAAM,cAAc,MAAM,MAAM,OAAO,CAAC,SAAS,CAAC,KAAK,YAAY,EAAE,IAAI,CAAC,SAAS,KAAK,UAAU;AAClG,MAAI,YAAY,WAAW,EAAG,QAAO;AAQrC,QAAM,YAAY,MAAM,SAAS,SAAS,WAAW;AACrD,QAAM,YAAY,IAAI,IAAI,UAAU,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI,CAAC;AAClE,QAAM,cAAc,YAAY,OAAO,CAAC,SAAS,CAAC,UAAU,IAAI,IAAI,CAAC;AACrE,QAAM,cAAc,OAAO,aAAa,MAAM,MAAe,OAAO;AACpE,MAAI,CAAC,UAAU,MAAM;AACnB,WAAO,EAAE,GAAG,QAAQ,aAAa,aAAa,UAAU,YAAY;AAAA,EACtE;AAGA,QAAM,QAAQ,OAAO,KAAK;AAC1B,QAAM,MAAqB;AAAA,IACzB,MAAM,UAAU;AAAA,IAChB,OAAO,UAAU,MAAM,IAAI,CAAC,UAAU,EAAE,GAAG,MAAM,OAAO,KAAK,QAAQ,OAAO,KAAK,KAAK,MAAM,MAAM,EAAE;AAAA,IACpG,aAAa,UAAU;AAAA,EACzB;AACA,QAAM,aAAa,UAAU,IAAI,WAAW;AAC5C,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM,GAAG,OAAO,IAAI,GAAG,IAAI,IAAI;AAAA;AAAA,IAC/B,OAAO,CAAC,GAAI,OAAO,SAAS,CAAC,GAAI,GAAG,IAAI,KAAK;AAAA,IAC7C,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,GAAI,aAAa,EAAE,eAAe,WAAW,IAAI,CAAC;AAAA,IAClD,GAAI,YAAY,SAAS,IAAI,EAAE,aAAa,YAAY,IAAI,CAAC;AAAA,EAC/D;AACJ;;;ADhNA,IAAM,uBAAyC;AAAA,EAC7C,gBAAgB;AAAA,EAChB,MAAM,QAAQ,SAAS,WAAW;AAChC,WAAO,EAAE,MAAM,aAAa,MAAM,qBAAqB,SAAS,SAAS,GAAG,QAAQ,mBAAmB;AAAA,EACzG;AACF;AAMO,IAAM,aAA8D;AAAA,EACzE,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB,QAAQ;AACV;;;ASjHA,OAAOC,SAAQ;AACf,OAAOC,WAAU;AACjB,SAAS,iBAAAC,sBAAqB;AAC9B,SAAS,cAAc,YAAAC,WAAU,YAAAC,WAAU,YAAAC,WAAU,WAAAC,gBAAsE;AAmF3H,SAAS,aAAqB;AAC1B,QAAMC,WAAUL,eAAc,YAAY,GAAG;AAC7C,QAAM,UAAU,CAAC,cAA8B;AAC3C,QAAI;AACA,YAAM,MAAeK,SAAQ,SAAS;AACtC,aAAOJ,UAAS,GAAG,KAAKC,UAAS,IAAI,OAAO,IAAI,IAAI,UAAU;AAAA,IAClE,QAAQ;AACJ,aAAO;AAAA,IACX;AAAA,EACJ;AACA,SAAO,WAAW,QAAQ,iBAAiB,CAAC,UAC5B,QAAQ,yBAAyB,CAAC,cAC9B,QAAQ,2BAA2B,CAAC,QAC1C,QAAQ,qCAAqC,CAAC;AAChE;AAEA,IAAM,QAAQ,WAAW;AAGlB,SAAS,YAAY,OAAoC;AAC5D,QAAM,QAAsB,CAAC;AAC7B,aAAW,QAAQ,OAAO;AACtB,UAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,QAAI,QAAQ,KAAK,MAAM,KAAK,QAAQ,KAAK,MAAM,KAAK,KAAK,KAAK,MAAM,KAAK,QAAQ;AAC7E,WAAK,MAAM,KAAK,CAAC,KAAK,GAAG,KAAK,OAAO,KAAK,OAAO,KAAK,GAAG,CAAC;AAAA,IAC9D,OAAO;AACH,YAAM,KAAK,EAAE,GAAG,KAAK,MAAM,GAAG,KAAK,GAAG,GAAG,KAAK,QAAQ,OAAO,CAAC,CAAC,KAAK,GAAG,KAAK,OAAO,KAAK,OAAO,KAAK,GAAG,CAAC,EAAE,CAAC;AAAA,IAC/G;AAAA,EACJ;AACA,SAAO;AACX;AAGO,SAAS,YAAY,OAAoC;AAC5D,QAAM,QAAuB,CAAC;AAC9B,aAAW,QAAQ,OAAO;AACtB,eAAW,CAAC,GAAG,OAAO,OAAO,GAAG,KAAK,KAAK,OAAO;AAC7C,YAAM,KAAK,EAAE,OAAO,KAAK,MAAM,KAAK,GAAG,GAAG,GAAG,KAAK,GAAG,OAAO,QAAQ,KAAK,EAAE,CAAC;AAAA,IAChF;AAAA,EACJ;AACA,SAAO;AACX;AA2CA,SAAS,SAAS,OAA6C;AAC3D,MAAI,CAACD,UAAS,KAAK,KAAK,MAAM,MAAM,KAAK,CAACC,UAAS,MAAM,KAAK,EAAG,QAAO;AACxE,MAAIA,UAAS,MAAM,QAAQ,EAAG,QAAO;AACrC,MAAI,CAACA,UAAS,MAAM,IAAI,KAAK,CAACA,UAAS,MAAM,MAAM,KAAK,CAACE,SAAQ,MAAM,KAAK,EAAG,QAAO;AACtF,SAAO,MAAM,MAAM,MAAM,CAAC,SACtBH,UAAS,IAAI,KAAKE,UAAS,KAAK,CAAC,KAAKA,UAAS,KAAK,CAAC,KAAKA,UAAS,KAAK,CAAC,KAAKC,SAAQ,KAAK,KAAK,KAC7F,KAAK,MAAM,MAAM,CAAC,MAAMA,SAAQ,CAAC,KAAK,EAAE,WAAW,KAAK,EAAE,MAAMD,SAAQ,CAAC,CAAC;AACrF;AAMA,IAAM,YAAY;AAcX,SAAS,wBAAwB,KAAa,QAAoC;AACrF,QAAM,UAAU,CAAC,QAA+B;AAC5C,QAAI,CAAC,UAAU,KAAK,GAAG,EAAG,QAAO;AACjC,UAAM,CAAC,IAAI,EAAE,IAAI,aAAa,GAAG;AACjC,WAAOJ,MAAK,KAAK,KAAK,IAAI,IAAI,GAAG,GAAG,OAAO;AAAA,EAC/C;AAEA,SAAO;AAAA,IACH,MAAM,KAAK,KAAK;AACZ,UAAI,MAAiC;AACrC,UAAI;AACA,cAAM,OAAO,QAAQ,GAAG;AACxB,YAAI,SAAS,KAAM,OAAM,IAAI,MAAM,aAAa;AAChD,cAAM,SAAkB,KAAK,MAAM,MAAMD,IAAG,SAAS,SAAS,MAAM,MAAM,CAAC;AAC3E,YAAI,SAAS,MAAM,KAAK,OAAO,UAAU,MAAO,OAAM;AAAA,MAC1D,QAAQ;AACJ,cAAM;AAAA,MACV;AAKA,cAAQ,MAAM,uBAAuB;AAAA,QACjC,SAAS,MAAM,QAAQ;AAAA,QACvB;AAAA,QACA,GAAI,MAAO,cAAc,MAAM,EAAE,UAAU,IAAI,SAAS,IAAI,EAAE,OAAO,IAAI,MAAM,OAAO,IAAK,CAAC;AAAA,MAChG,CAAC;AACD,UAAI,CAAC,IAAK,QAAO;AAIjB,UAAI,cAAc,IAAK,QAAO,EAAE,MAAM,YAAY,UAAU,IAAI,SAAS;AACzE,YAAM,EAAE,GAAG,IAAI,OAAO,QAAQ,OAAO,MAAM,GAAG,WAAW,IAAI;AAC7D,aAAO,EAAE,MAAM,aAAa,MAAM,OAAO,YAAY,KAAK,GAAG,GAAG,WAAW;AAAA,IAC/E;AAAA,IAEA,MAAM,MAAM,KAAK,SAAS;AACtB,YAAM,SAAS,QAAQ,GAAG;AAC1B,UAAI,WAAW,MAAM;AACjB,gBAAQ,MAAM,6CAA6C,EAAE,IAAI,CAAC;AAClE;AAAA,MACJ;AAGA,YAAM,QAA4B,QAAQ,SAAS,aAC7C,EAAE,GAAG,GAAG,OAAO,OAAO,UAAU,QAAQ,SAAS,KAChD,MAAM;AAKL,cAAM,EAAE,MAAM,OAAO,MAAM,OAAO,GAAG,WAAW,IAAI;AACpD,eAAO,EAAE,GAAG,GAAG,OAAO,OAAO,MAAM,OAAO,YAAY,KAAK,GAAG,GAAG,WAAW;AAAA,MAChF,GAAG;AAGP,YAAM,OAAO,GAAG,MAAM,IAAI,QAAQ,GAAG;AACrC,UAAI;AACA,cAAMA,IAAG,SAAS,MAAMC,MAAK,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AACjE,cAAMD,IAAG,SAAS,UAAU,MAAM,KAAK,UAAU,KAAK,GAAG,MAAM;AAC/D,cAAMA,IAAG,SAAS,OAAO,MAAM,MAAM;AAAA,MACzC,SAAS,OAAO;AAIZ,cAAMA,IAAG,SAAS,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAC1D,cAAM;AAAA,MACV;AAAA,IACJ;AAAA,IAEA,MAAM,OAAO;AAWT,YAAM,SAAS,KAAK,UAAU,EAAE,GAAG,GAAG,OAAO,MAAM,CAAC,EAAE,MAAM,GAAG,EAAE,IAAI;AACrE,UAAI;AACJ,UAAI;AACA,oBAAY,MAAMA,IAAG,SAAS,QAAQ,GAAG;AAAA,MAC7C,QAAQ;AACJ,eAAO,CAAC;AAAA,MACZ;AAUA,UAAI,QAAQ;AACZ,iBAAW,QAAQ,WAAW;AAC1B,YAAI,CAAC,KAAK,SAAS,OAAO,EAAG;AAC7B,cAAMA,IAAG,SAAS,GAAGC,MAAK,KAAK,KAAK,IAAI,GAAG,EAAE,OAAO,KAAK,CAAC,EAAE,KAAK,MAAM;AAAE,mBAAS;AAAA,QAAG,GAAG,MAAM;AAAA,QAAC,CAAC;AAAA,MACpG;AACA,UAAI,QAAQ,EAAG,SAAQ,KAAK,kDAAkD,EAAE,MAAM,CAAC;AAEvF,YAAM,OAAiB,CAAC;AACxB,UAAI,eAAe;AACnB,iBAAW,MAAM,WAAW;AACxB,YAAI,CAAC,gBAAgB,KAAK,EAAE,EAAG;AAC/B,YAAI;AACJ,YAAI;AACA,oBAAU,MAAMD,IAAG,SAAS,QAAQC,MAAK,KAAK,KAAK,EAAE,CAAC;AAAA,QAC1D,QAAQ;AACJ;AAAA,QACJ;AACA,mBAAW,MAAM,SAAS;AACtB,cAAI,CAAC,gBAAgB,KAAK,EAAE,EAAG;AAC/B,cAAI;AACJ,cAAI;AACA,oBAAQ,MAAMD,IAAG,SAAS,QAAQC,MAAK,KAAK,KAAK,IAAI,EAAE,CAAC;AAAA,UAC5D,QAAQ;AACJ;AAAA,UACJ;AACA,qBAAW,QAAQ,OAAO;AACtB,gBAAI,CAAC,KAAK,SAAS,OAAO,EAAG;AAS7B,kBAAM,OAAO,KAAK,MAAM,GAAG,CAAC,QAAQ,MAAM;AAC1C,gBAAI,iBAAiB,KAAK,IAAI,GAAG;AAC7B,oBAAMD,IAAG,SAAS,GAAGC,MAAK,KAAK,KAAK,IAAI,IAAI,IAAI,GAAG,EAAE,OAAO,KAAK,CAAC,EAAE,KAAK,MAAM;AAAE,gCAAgB;AAAA,cAAG,GAAG,MAAM;AAAA,cAAC,CAAC;AAC/G;AAAA,YACJ;AACA,gBAAI,SAAwC;AAC5C,gBAAI;AACA,uBAAS,MAAMD,IAAG,SAAS,KAAKC,MAAK,KAAK,KAAK,IAAI,IAAI,IAAI,GAAG,GAAG;AACjE,oBAAM,MAAM,OAAO,MAAM,OAAO,MAAM;AACtC,oBAAM,EAAE,UAAU,IAAI,MAAM,OAAO,KAAK,KAAK,GAAG,OAAO,QAAQ,CAAC;AAChE,kBAAI,cAAc,OAAO,UAAU,IAAI,SAAS,MAAM,MAAM,QAAQ;AAChE,qBAAK,KAAK,IAAI;AAAA,cAClB;AAAA,YACJ,QAAQ;AAAA,YAER,UAAE;AACE,oBAAM,QAAQ,MAAM,EAAE,MAAM,MAAM;AAAA,cAAC,CAAC;AAAA,YACxC;AAAA,UACJ;AAAA,QACJ;AAAA,MACJ;AACA,UAAI,eAAe,EAAG,SAAQ,KAAK,0DAA0D,EAAE,OAAO,aAAa,CAAC;AACpH,aAAO;AAAA,IACX;AAAA,EACJ;AACJ;;;ACjWA,SAAS,yBAAsD;AAaxD,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC/C,YAAqB,YAA6B,QAAuB;AAKvE;AAAA,MACE,WAAW,aACP,uBAAuB,UAAU,KACjC,wDAAwD,UAAU;AAAA,IACxE;AATmB;AAA6B;AAUhD,SAAK,OAAO;AAAA,EACd;AAAA,EAXqB;AAAA,EAA6B;AAYpD;AAcO,SAAS,sBAAsB,QAA8C;AAClF,QAAM,EAAE,MAAM,QAAQ,IAAI,kBAAkB,MAAM;AAElD,SAAO;AAAA,IACL,WAAW,OAAO,eAA2B;AAC3C,YAAM,MAAM,GAAG,IAAI,cAAc,mBAAmB,OAAO,UAAU,CAAC,CAAC;AACvE,YAAM,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,CAAC;AAExC,UAAI,IAAI,WAAW,KAAK;AACtB,cAAM,EAAE,OAAO,IAAI,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACpD,cAAM,IAAI;AAAA,UACR,OAAO,UAAU;AAAA,UACjB,WAAW,mBAAmB,mBAAmB;AAAA,QACnD;AAAA,MACF;AACA,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI,MAAM,qCAAqC,OAAO,UAAU,CAAC,KAAK,IAAI,MAAM,IAAI,IAAI,UAAU,EAAE;AAAA,MAC5G;AAEA,aAAO;AAAA,QACL,MAAM,MAAM,IAAI,YAAY;AAAA,QAC5B,aAAa,IAAI,QAAQ,IAAI,cAAc,KAAK;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AACF;","names":["chunk","createHash","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.
|
|
3
|
+
"version": "0.5.29",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=24.0.0"
|
|
6
6
|
},
|
|
@@ -27,19 +27,19 @@
|
|
|
27
27
|
"test:coverage": "vitest run --coverage"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@semiont/core": "0.5.
|
|
30
|
+
"@semiont/core": "0.5.29",
|
|
31
31
|
"@tesseract.js-data/eng": "^1.0.0",
|
|
32
32
|
"pdfjs-dist": "^6.2.108",
|
|
33
33
|
"tesseract.js": "^7.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@vitest/coverage-v8": "^4.1.
|
|
36
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
37
37
|
"pdf-lib": "^1.17.1",
|
|
38
|
-
"rollup": "^4.
|
|
38
|
+
"rollup": "^4.63.0",
|
|
39
39
|
"rollup-plugin-dts": "^6.4.1",
|
|
40
40
|
"tsup": "^8.0.1",
|
|
41
41
|
"typescript": "^6.0.2",
|
|
42
|
-
"vitest": "^4.1.
|
|
42
|
+
"vitest": "^4.1.11"
|
|
43
43
|
},
|
|
44
44
|
"keywords": [
|
|
45
45
|
"content",
|