@hraness/sponge 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +163 -17
  2. package/bin/sponge.js +9 -2
  3. package/dist/contracts/index.d.ts +164 -0
  4. package/dist/contracts/index.js +6 -0
  5. package/dist/contracts/validate.d.ts +14 -0
  6. package/dist/contracts/validate.js +187 -0
  7. package/dist/migrations/001.d.ts +3 -0
  8. package/dist/migrations/001.js +33 -0
  9. package/dist/migrations/002.d.ts +3 -0
  10. package/dist/migrations/002.js +6 -0
  11. package/dist/migrations/003.d.ts +3 -0
  12. package/dist/migrations/003.js +10 -0
  13. package/dist/migrations/004.d.ts +3 -0
  14. package/dist/migrations/004.js +9 -0
  15. package/dist/src/acquisition/capture.d.ts +29 -0
  16. package/dist/src/acquisition/capture.js +160 -0
  17. package/dist/src/acquisition/cleanup.d.ts +6 -0
  18. package/dist/src/acquisition/cleanup.js +20 -0
  19. package/dist/src/acquisition/errors.d.ts +3 -0
  20. package/dist/src/acquisition/errors.js +20 -0
  21. package/dist/src/acquisition/files.d.ts +4 -0
  22. package/dist/src/acquisition/files.js +49 -0
  23. package/dist/src/acquisition/index.d.ts +6 -0
  24. package/dist/src/acquisition/index.js +4 -0
  25. package/dist/src/acquisition/lock.d.ts +1 -0
  26. package/dist/src/acquisition/lock.js +2 -0
  27. package/dist/src/acquisition/normalize.d.ts +11 -0
  28. package/dist/src/acquisition/normalize.js +62 -0
  29. package/dist/src/acquisition/pins.d.ts +3 -0
  30. package/dist/src/acquisition/pins.js +9 -0
  31. package/dist/src/acquisition/process.d.ts +16 -0
  32. package/dist/src/acquisition/process.js +63 -0
  33. package/dist/src/acquisition/runtime.d.ts +53 -0
  34. package/dist/src/acquisition/runtime.js +216 -0
  35. package/dist/src/acquisition/verify-helper.d.ts +1 -0
  36. package/dist/src/acquisition/verify-helper.js +19 -0
  37. package/dist/src/app/article.d.ts +23 -0
  38. package/dist/src/app/article.js +82 -0
  39. package/dist/src/app/bundle-files.d.ts +8 -0
  40. package/dist/src/app/bundle-files.js +66 -0
  41. package/dist/src/app/markdown.d.ts +6 -0
  42. package/dist/src/app/markdown.js +90 -0
  43. package/dist/src/app/source.d.ts +15 -0
  44. package/dist/src/app/source.js +40 -0
  45. package/dist/src/cli/arguments.d.ts +9 -0
  46. package/dist/src/cli/arguments.js +106 -0
  47. package/dist/src/cli/config.d.ts +8 -0
  48. package/dist/src/cli/config.js +45 -0
  49. package/dist/src/cli/import.d.ts +4 -0
  50. package/dist/src/cli/import.js +3 -0
  51. package/dist/src/cli/indexing.d.ts +7 -0
  52. package/dist/src/cli/indexing.js +21 -0
  53. package/dist/src/cli/main.d.ts +1 -0
  54. package/dist/src/cli/main.js +333 -0
  55. package/dist/src/cli/portability.d.ts +9 -0
  56. package/dist/src/cli/portability.js +39 -0
  57. package/dist/src/cli/queue.d.ts +2 -0
  58. package/dist/src/cli/queue.js +23 -0
  59. package/dist/src/cli/terminal.d.ts +2 -0
  60. package/dist/src/cli/terminal.js +4 -0
  61. package/dist/src/cli/worker.d.ts +16 -0
  62. package/dist/src/cli/worker.js +62 -0
  63. package/dist/src/importers/apply.d.ts +29 -0
  64. package/dist/src/importers/apply.js +85 -0
  65. package/dist/src/importers/custody.d.ts +6 -0
  66. package/dist/src/importers/custody.js +35 -0
  67. package/dist/src/importers/inspect.d.ts +21 -0
  68. package/dist/src/importers/inspect.js +97 -0
  69. package/dist/src/importers/manifest.d.ts +22 -0
  70. package/dist/src/importers/manifest.js +43 -0
  71. package/dist/src/importers/verify-helper.d.ts +1 -0
  72. package/dist/src/importers/verify-helper.js +27 -0
  73. package/dist/src/indexing/image.d.ts +10 -0
  74. package/dist/src/indexing/image.js +34 -0
  75. package/dist/src/indexing/index.d.ts +53 -0
  76. package/dist/src/indexing/index.js +123 -0
  77. package/dist/src/indexing/jobs.d.ts +14 -0
  78. package/dist/src/indexing/jobs.js +49 -0
  79. package/dist/src/indexing/runner.d.ts +23 -0
  80. package/dist/src/indexing/runner.js +114 -0
  81. package/dist/src/inference/asr/bridge.d.ts +1 -0
  82. package/dist/src/inference/asr/bridge.js +16 -0
  83. package/dist/src/inference/asr/protocol.d.ts +23 -0
  84. package/dist/src/inference/asr/protocol.js +60 -0
  85. package/dist/src/inference/asr/runtime.d.ts +29 -0
  86. package/dist/src/inference/asr/runtime.js +103 -0
  87. package/dist/src/inference/asr/windows.d.ts +9 -0
  88. package/dist/src/inference/asr/windows.js +41 -0
  89. package/dist/src/inference/embedding/index.d.ts +47 -0
  90. package/dist/src/inference/embedding/index.js +122 -0
  91. package/dist/src/media/capture-helper.d.ts +1 -0
  92. package/dist/src/media/capture-helper.js +16 -0
  93. package/dist/src/media/capture.d.ts +32 -0
  94. package/dist/src/media/capture.js +193 -0
  95. package/dist/src/media/handler.d.ts +22 -0
  96. package/dist/src/media/handler.js +82 -0
  97. package/dist/src/media/transcribe.d.ts +29 -0
  98. package/dist/src/media/transcribe.js +93 -0
  99. package/dist/src/metadata/article.d.ts +5 -0
  100. package/dist/src/metadata/article.js +51 -0
  101. package/dist/src/models/artifacts.d.ts +36 -0
  102. package/dist/src/models/artifacts.js +274 -0
  103. package/dist/src/models/catalog.d.ts +35 -0
  104. package/dist/src/models/catalog.js +71 -0
  105. package/dist/src/models/download.d.ts +9 -0
  106. package/dist/src/models/download.js +147 -0
  107. package/dist/src/models/runtime.d.ts +24 -0
  108. package/dist/src/models/runtime.js +37 -0
  109. package/dist/src/pdf/capture-helper.d.ts +1 -0
  110. package/dist/src/pdf/capture-helper.js +12 -0
  111. package/dist/src/pdf/capture.d.ts +29 -0
  112. package/dist/src/pdf/capture.js +156 -0
  113. package/dist/src/pdf/errors.d.ts +2 -0
  114. package/dist/src/pdf/errors.js +13 -0
  115. package/dist/src/pdf/handler.d.ts +24 -0
  116. package/dist/src/pdf/handler.js +84 -0
  117. package/dist/src/pdf/index.d.ts +4 -0
  118. package/dist/src/pdf/index.js +4 -0
  119. package/dist/src/pdf/input.d.ts +2 -0
  120. package/dist/src/pdf/input.js +46 -0
  121. package/dist/src/pdf/normalize.d.ts +10 -0
  122. package/dist/src/pdf/normalize.js +64 -0
  123. package/dist/src/pdf/source-identity.d.ts +4 -0
  124. package/dist/src/pdf/source-identity.js +12 -0
  125. package/dist/src/pdf/source.d.ts +6 -0
  126. package/dist/src/pdf/source.js +22 -0
  127. package/dist/src/pdf/verify-helper.d.ts +1 -0
  128. package/dist/src/pdf/verify-helper.js +17 -0
  129. package/dist/src/portability/backup.d.ts +23 -0
  130. package/dist/src/portability/backup.js +138 -0
  131. package/dist/src/portability/database.d.ts +14 -0
  132. package/dist/src/portability/database.js +78 -0
  133. package/dist/src/portability/export.d.ts +8 -0
  134. package/dist/src/portability/export.js +36 -0
  135. package/dist/src/portability/files.d.ts +33 -0
  136. package/dist/src/portability/files.js +78 -0
  137. package/dist/src/portability/gc.d.ts +21 -0
  138. package/dist/src/portability/gc.js +66 -0
  139. package/dist/src/search/extension.d.ts +3 -0
  140. package/dist/src/search/extension.js +17 -0
  141. package/dist/src/search/hybrid.d.ts +39 -0
  142. package/dist/src/search/hybrid.js +118 -0
  143. package/dist/src/search/query.d.ts +12 -0
  144. package/dist/src/search/query.js +47 -0
  145. package/dist/src/search/vector.d.ts +21 -0
  146. package/dist/src/search/vector.js +21 -0
  147. package/dist/src/service/launch-agent.d.ts +47 -0
  148. package/dist/src/service/launch-agent.js +179 -0
  149. package/dist/src/service/lifetime.d.ts +7 -0
  150. package/dist/src/service/lifetime.js +73 -0
  151. package/dist/src/service/loop.d.ts +21 -0
  152. package/dist/src/service/loop.js +94 -0
  153. package/dist/src/store/files.d.ts +12 -0
  154. package/dist/src/store/files.js +101 -0
  155. package/dist/src/store/index.d.ts +118 -0
  156. package/dist/src/store/index.js +727 -0
  157. package/dist/src/store/types.d.ts +90 -0
  158. package/dist/src/store/types.js +8 -0
  159. package/dist/src/worker/basic.d.ts +10 -0
  160. package/dist/src/worker/basic.js +27 -0
  161. package/dist/src/worker/custody.d.ts +13 -0
  162. package/dist/src/worker/custody.js +185 -0
  163. package/dist/src/worker/engine.d.ts +28 -0
  164. package/dist/src/worker/engine.js +78 -0
  165. package/dist/src/worker/identity.d.ts +7 -0
  166. package/dist/src/worker/identity.js +27 -0
  167. package/dist/src/worker/journal.d.ts +5 -0
  168. package/dist/src/worker/journal.js +30 -0
  169. package/dist/src/worker/supervisor-child.d.ts +1 -0
  170. package/dist/src/worker/supervisor-child.js +33 -0
  171. package/dist/src/worker/supervisor.d.ts +3 -0
  172. package/dist/src/worker/supervisor.js +47 -0
  173. package/native/runtime-manifest.json +40 -0
  174. package/package.json +31 -5
@@ -0,0 +1,37 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { privateRoot } from '../acquisition/files.js';
4
+ import { nativeArtifactFetch } from './download.js';
5
+ import { inspectModelPackage, provisionModelPackage, recoverModelPackage, modelSetupPlan, ModelSetupError } from './artifacts.js';
6
+ import { nativePlatformSupported, nativeModelPackage, NativeCatalogError, loadNativeCatalog } from './catalog.js';
7
+ export async function nativeSetupPlan(kind, version) { const catalog = await loadNativeCatalog(version); return { ...modelSetupPlan(nativeModelPackage(catalog, kind)), supported: nativePlatformSupported(), minimumMacOS: '26.0' }; }
8
+ function supported() { if (!nativePlatformSupported())
9
+ throw new NativeCatalogError('unsupported'); }
10
+ function asr(c, p) { return { helper: p['sponge-speech'], helperSha256: c.artifacts.find(a => a.role === 'asr-helper').sha256, model: p['whistle.cact'] }; }
11
+ function embedding(c, p) { return { helper: p['sponge-embedding'], helperSha256: c.artifacts.find(a => a.role === 'embedding-helper').sha256, library: p['libCLiteRTLM_mac.dylib'], model: p['embeddinggemma-2-text-vision-440m.litertlm'] }; }
12
+ export async function setupNativeRuntime(cacheRoot, catalog, kind, options = {}) { supported(); if (options.signal?.aborted)
13
+ throw new ModelSetupError('cancelled'); const manifest = nativeModelPackage(catalog, kind); await privateRoot(dirname(cacheRoot)); try {
14
+ await mkdir(cacheRoot, { mode: 0o700 });
15
+ }
16
+ catch (error) {
17
+ if (error.code !== 'EEXIST')
18
+ throw error;
19
+ } await privateRoot(cacheRoot); await recoverModelPackage(cacheRoot, manifest); const paths = await provisionModelPackage(cacheRoot, manifest, { ...options, fetch: nativeArtifactFetch(manifest) }); return kind === 'asr' ? asr(catalog, paths) : embedding(catalog, paths); }
20
+ export async function inspectAsrRuntime(cacheRoot, catalog) { supported(); try {
21
+ return asr(catalog, await inspectModelPackage(cacheRoot, nativeModelPackage(catalog, 'asr')));
22
+ }
23
+ catch (error) {
24
+ if (error.code === 'ENOENT')
25
+ throw new NativeCatalogError('runtime-missing');
26
+ throw error;
27
+ } }
28
+ export async function inspectEmbeddingRuntime(cacheRoot, catalog) { supported(); try {
29
+ return embedding(catalog, await inspectModelPackage(cacheRoot, nativeModelPackage(catalog, 'embedding')));
30
+ }
31
+ catch (error) {
32
+ if (error.code === 'ENOENT')
33
+ throw new NativeCatalogError('runtime-missing');
34
+ throw error;
35
+ } }
36
+ export async function provisionNative(kind, cacheRoot, version, signal) { return setupNativeRuntime(cacheRoot, await loadNativeCatalog(version), kind, { signal }); }
37
+ export async function inspectNative(kind, cacheRoot, version) { const catalog = await loadNativeCatalog(version); return kind === 'asr' ? inspectAsrRuntime(cacheRoot, catalog) : inspectEmbeddingRuntime(cacheRoot, catalog); }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ // Pinned public SDK bridge. Native subprocesses inherit the supervisor's group.
2
+ const runtime = process.argv[2], args = process.argv.slice(3);
3
+ try {
4
+ const { runPdfAcquisition } = await import(import.meta.resolve('@hraness/ghostget/pdf-acquisition', `${runtime}/package.json`));
5
+ const { runPdfToolCommand } = await import(import.meta.resolve('@hraness/wordcell/pdf', `${runtime}/package.json`));
6
+ process.exitCode = await runPdfAcquisition(args, process.env, { stdout: (value) => process.stdout.write(value), stderr: (value) => process.stderr.write(value) }, { runTool: (specification) => runPdfToolCommand({ ...specification, processGroup: 'inherit' }) });
7
+ }
8
+ catch {
9
+ process.stdout.write(JSON.stringify({ ok: false, code: 'unknown' }));
10
+ process.exitCode = 1;
11
+ }
12
+ export {};
@@ -0,0 +1,29 @@
1
+ import type { CaptureResult, ErrorEnvelope } from '../../contracts/index.js';
2
+ import { runOwned } from '../acquisition/process.js';
3
+ import { type CaptureRuntime } from '../acquisition/runtime.js';
4
+ export type CaptureConnection = {
5
+ kind: 'account';
6
+ stateHome: string;
7
+ accountId: string;
8
+ } | {
9
+ kind: 'cdp';
10
+ endpoint: string;
11
+ };
12
+ export interface PdfCaptureRequest {
13
+ runtime: CaptureRuntime;
14
+ stagingRoot: string;
15
+ sourceUrl: string;
16
+ requestId: string;
17
+ connection?: CaptureConnection;
18
+ signal?: AbortSignal;
19
+ deadlineMs?: number;
20
+ allowPrivateNetwork?: boolean;
21
+ runProcess?: typeof runOwned;
22
+ }
23
+ export type CaptureOutcome = {
24
+ ok: true;
25
+ capture: CaptureResult;
26
+ bundleRoot: string;
27
+ dispose: () => Promise<void>;
28
+ } | ErrorEnvelope;
29
+ export declare function capturePdf(request: PdfCaptureRequest): Promise<CaptureOutcome>;
@@ -0,0 +1,156 @@
1
+ import { mkdir, mkdtemp, writeFile, statfs, realpath } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { limits } from '../../contracts/index.js';
5
+ import { privateRoot, physicalBytes } from '../acquisition/files.js';
6
+ import { runOwned, isolatedEnvironment } from '../acquisition/process.js';
7
+ import { inspectCaptureRuntime, CaptureSetupError } from '../acquisition/runtime.js';
8
+ import { settleCaptureRoot, CaptureCleanupError } from '../acquisition/cleanup.js';
9
+ import { failure } from '../acquisition/errors.js';
10
+ import { object } from '../acquisition/normalize.js';
11
+ import { normalizePdfBundle } from './normalize.js';
12
+ import { copyPdfInput } from './input.js';
13
+ import { pdfFailure } from './errors.js';
14
+ export async function capturePdf(request) {
15
+ const { requestId, signal } = request;
16
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(requestId))
17
+ return failure('invalid-request', 'invalid-input');
18
+ let url;
19
+ try {
20
+ url = new URL(request.sourceUrl);
21
+ if (!['http:', 'https:', 'file:'].includes(url.protocol) || url.username || url.password || request.sourceUrl.length > 16384)
22
+ throw Error();
23
+ }
24
+ catch {
25
+ return failure(requestId, 'invalid-input');
26
+ }
27
+ if (signal?.aborted)
28
+ return failure(requestId, 'cancelled');
29
+ let runtime;
30
+ try {
31
+ runtime = await inspectCaptureRuntime(request.runtime.root, request.runtime.pins);
32
+ }
33
+ catch (error) {
34
+ return failure(requestId, error instanceof CaptureSetupError ? error.code : 'runtime-missing', 'setup');
35
+ }
36
+ let base;
37
+ try {
38
+ base = await privateRoot(request.stagingRoot);
39
+ const disk = await statfs(base);
40
+ if (disk.bavail * disk.bsize < limits.stagingBytes + limits.freeReserveBytes)
41
+ return failure(requestId, 'limit-exceeded');
42
+ }
43
+ catch {
44
+ return failure(requestId, 'invalid-input');
45
+ }
46
+ const deadline = request.deadlineMs ?? limits.captureDeadlineMs;
47
+ if (!Number.isSafeInteger(deadline) || deadline < 100 || deadline > limits.captureDeadlineMs)
48
+ return failure(requestId, 'invalid-input');
49
+ if (url.protocol === 'file:' && (url.host || url.search || url.hash || request.connection))
50
+ return failure(requestId, 'invalid-input');
51
+ if (request.connection?.kind === 'cdp' || request.allowPrivateNetwork)
52
+ return failure(requestId, 'unsupported');
53
+ const captureHelper = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './capture-helper.ts' : './capture-helper.js', import.meta.url));
54
+ const args = [captureHelper, runtime.root, request.sourceUrl, '--slug', 'document', '--timeout-ms', String(deadline), '--max-pdf-bytes', String(limits.pdfBytes), '--max-pages', '500', '--max-images', String(limits.imagesPerItem), '--max-asset-bytes', String(limits.imageBytes), '--max-total-asset-bytes', String(500 * 1024 ** 2), '--quiet', '--json'];
55
+ let selectedState;
56
+ if (request.connection?.kind === 'account') {
57
+ if (!/^[a-zA-Z0-9_.-]{1,128}$/.test(request.connection.accountId))
58
+ return failure(requestId, 'invalid-input');
59
+ try {
60
+ selectedState = await privateRoot(request.connection.stateHome);
61
+ }
62
+ catch {
63
+ return failure(requestId, 'repair-auth');
64
+ }
65
+ args.push('--auth', request.connection.accountId);
66
+ }
67
+ const root = await realpath(await mkdtemp(join(base, 'capture-'))), home = join(root, 'home'), output = join(root, 'bundles');
68
+ args.push('--output', output);
69
+ const env = isolatedEnvironment(home, home, selectedState ?? join(home, 'state'));
70
+ const controller = new AbortController();
71
+ const abort = () => controller.abort();
72
+ signal?.addEventListener('abort', abort, { once: true });
73
+ if (signal?.aborted)
74
+ abort();
75
+ let cleanup = 'settled', keep = false, quota = false, checking = false;
76
+ const monitor = setInterval(async () => { if (checking)
77
+ return; checking = true; try {
78
+ if (await physicalBytes(root) > limits.stagingBytes) {
79
+ quota = true;
80
+ controller.abort();
81
+ }
82
+ }
83
+ catch {
84
+ quota = true;
85
+ controller.abort();
86
+ }
87
+ finally {
88
+ checking = false;
89
+ } }, 100);
90
+ const started = Date.now();
91
+ try {
92
+ await mkdir(home, { mode: 0o700 });
93
+ await mkdir(output, { mode: 0o700 });
94
+ await writeFile(join(root, 'ownership.json'), JSON.stringify({ schemaVersion: 1, requestId, state: 'acquiring' }), { mode: 0o600 });
95
+ let sourceHash;
96
+ if (url.protocol === 'file:') {
97
+ sourceHash = await copyPdfInput(fileURLToPath(url), join(root, 'input.pdf'), controller.signal);
98
+ args[2] = join(root, 'input.pdf');
99
+ }
100
+ cleanup = 'pending';
101
+ const result = await (request.runProcess ?? runOwned)(runtime.bun, args, { cwd: root, env, signal: controller.signal, deadlineMs: Math.max(1, deadline - (Date.now() - started)), maxOutputBytes: limits.metadataBytes, graceMs: 5000 });
102
+ cleanup = result.cleanup;
103
+ let summary;
104
+ try {
105
+ summary = JSON.parse(result.stdout);
106
+ }
107
+ catch {
108
+ summary = null;
109
+ }
110
+ const upstream = pdfFailure(summary, requestId);
111
+ if (upstream.code === 'cleanup-required')
112
+ cleanup = 'pending';
113
+ if (cleanup !== 'settled')
114
+ return failure(requestId, 'cleanup-required', 'cleanup');
115
+ if (quota)
116
+ return failure(requestId, 'limit-exceeded');
117
+ if (result.stopped)
118
+ return failure(requestId, result.stopped);
119
+ if (result.code !== 0 || object(summary).ok !== true)
120
+ return upstream;
121
+ const outputDirectory = object(summary).outputDirectory;
122
+ if (typeof outputDirectory !== 'string' || !outputDirectory.startsWith(output + '/') || await realpath(outputDirectory) !== outputDirectory)
123
+ return failure(requestId, 'integrity', 'verification');
124
+ const helper = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './verify-helper.ts' : './verify-helper.js', import.meta.url));
125
+ cleanup = 'pending';
126
+ const checked = await (request.runProcess ?? runOwned)(runtime.bun, [helper, runtime.root, outputDirectory, request.sourceUrl], { cwd: root, env, signal: controller.signal, deadlineMs: Math.max(1, deadline - (Date.now() - started)), maxOutputBytes: limits.helperFrameBytes });
127
+ cleanup = checked.cleanup;
128
+ if (cleanup !== 'settled')
129
+ return failure(requestId, 'cleanup-required', 'cleanup');
130
+ if (checked.stopped)
131
+ return failure(requestId, checked.stopped);
132
+ if (checked.code !== 0)
133
+ return failure(requestId, 'integrity', 'verification');
134
+ const capture = await normalizePdfBundle({ bundleRoot: outputDirectory, inspection: JSON.parse(checked.stdout), summary, requestId, sourceUrl: request.sourceUrl, wordcellVersion: runtime.pins.wordcell.version, ...(sourceHash ? { sourceHash } : {}) });
135
+ if (controller.signal.aborted)
136
+ return failure(requestId, quota ? 'limit-exceeded' : 'cancelled');
137
+ await writeFile(join(root, 'ownership.json'), JSON.stringify({ schemaVersion: 1, requestId, state: 'verified' }), { mode: 0o600 });
138
+ keep = true;
139
+ return { ok: true, capture, bundleRoot: outputDirectory, dispose: async () => { if (await settleCaptureRoot(root, requestId) === 'pending')
140
+ throw new CaptureCleanupError(); } };
141
+ }
142
+ catch (error) {
143
+ if (cleanup !== 'settled')
144
+ return failure(requestId, 'cleanup-required', 'cleanup');
145
+ const code = (error instanceof Error && ['integrity', 'limit-exceeded', 'schema-newer', 'invalid-input', 'cancelled'].includes(error.message) ? error.message : 'unclassified');
146
+ return failure(requestId, code, 'verification');
147
+ }
148
+ finally {
149
+ clearInterval(monitor);
150
+ while (checking)
151
+ await new Promise(r => setTimeout(r, 5));
152
+ signal?.removeEventListener('abort', abort);
153
+ if (!keep && cleanup === 'settled' && await settleCaptureRoot(root, requestId) === 'pending')
154
+ return failure(requestId, 'cleanup-required', 'cleanup');
155
+ }
156
+ }
@@ -0,0 +1,2 @@
1
+ /** Wordcell's pinned PDF code field is authoritative; prose is never classified. */
2
+ export declare function pdfFailure(value: unknown, requestId: string): import("../../contracts/index.js").ErrorEnvelope;
@@ -0,0 +1,13 @@
1
+ import { failure, upstreamFailure } from '../acquisition/errors.js';
2
+ /** Wordcell's pinned PDF code field is authoritative; prose is never classified. */
3
+ export function pdfFailure(value, requestId) {
4
+ if (!value || typeof value !== 'object' || Array.isArray(value))
5
+ return failure(requestId, 'unclassified');
6
+ const record = value;
7
+ if (record.ok !== false)
8
+ return failure(requestId, 'unclassified');
9
+ if (record.captureError)
10
+ return upstreamFailure(value, requestId);
11
+ const codes = { 'invalid-input': 'invalid-input', encrypted: 'unsupported', 'limit-exceeded': 'limit-exceeded', 'tool-unavailable': 'runtime-missing', 'extraction-failed': 'unavailable', timeout: 'unavailable', cancelled: 'cancelled', integrity: 'integrity', 'unsupported-schema': 'schema-newer', unknown: 'unclassified' };
12
+ return failure(requestId, typeof record.code === 'string' && Object.hasOwn(codes, record.code) ? codes[record.code] : 'unclassified');
13
+ }
@@ -0,0 +1,24 @@
1
+ import type { ErrorEnvelope, JobCheckpoint } from '../../contracts/index.js';
2
+ import { type CaptureRuntime, type CaptureConnection } from '../acquisition/index.js';
3
+ import { type PdfCaptureRequest as CaptureRequest, type CaptureOutcome } from './capture.js';
4
+ import type { LibraryStore, AttemptToken } from '../store/index.js';
5
+ export interface PdfResult {
6
+ ok: true;
7
+ itemId: string;
8
+ revisionId: string;
9
+ status: 'complete' | 'partial';
10
+ }
11
+ export type PdfOutcome = (PdfResult | ErrorEnvelope) & {
12
+ cleanup: 'settled' | 'pending';
13
+ };
14
+ export interface PdfOptions {
15
+ checkpoint?: (stage: JobCheckpoint['stage']) => Promise<void>;
16
+ runProcess?: CaptureRequest['runProcess'];
17
+ runtime: CaptureRuntime;
18
+ connection?: CaptureConnection;
19
+ signal?: AbortSignal;
20
+ allowPrivateNetwork?: boolean;
21
+ capture?: (request: CaptureRequest) => Promise<CaptureOutcome>;
22
+ }
23
+ /** One attempt. Durable queue policy and process recovery live above this function. */
24
+ export declare function ingestPdf(store: LibraryStore, token: AttemptToken, options: PdfOptions): Promise<PdfOutcome>;
@@ -0,0 +1,84 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+ import { validateCapture } from '../../contracts/validate.js';
4
+ import { capturePdf } from './capture.js';
5
+ import { failure } from '../acquisition/errors.js';
6
+ import { bundleBytes, bundleText } from '../app/bundle-files.js';
7
+ import { selectedMetadata } from '../metadata/article.js';
8
+ import { localizeMarkdown } from '../app/markdown.js';
9
+ const known = new Set(['integrity', 'limit-exceeded', 'cancelled', 'invalid-input', 'owner-busy', 'unavailable']);
10
+ /** One attempt. Durable queue policy and process recovery live above this function. */
11
+ export async function ingestPdf(store, token, options) {
12
+ let captured, committed = false;
13
+ let outcome = failure(token.attemptId, 'unclassified', 'storage');
14
+ let cleanup = 'settled';
15
+ try {
16
+ const source = store.sourceForJob(token.jobId);
17
+ if (!source || source.kind !== 'pdf')
18
+ throw Object.assign(Error('unsupported'), { code: 'invalid-input' });
19
+ const result = await (options.capture ?? capturePdf)({ runProcess: options.runProcess, runtime: options.runtime, stagingRoot: join(store.root, 'staging'), sourceUrl: source.locator, requestId: token.attemptId, connection: options.connection, signal: options.signal, allowPrivateNetwork: options.allowPrivateNetwork });
20
+ if (!result.ok) {
21
+ outcome = result;
22
+ cleanup = result.code === 'cleanup-required' ? 'pending' : 'settled';
23
+ }
24
+ else {
25
+ captured = result;
26
+ await options.checkpoint?.('extracting');
27
+ const capture = result.capture;
28
+ validateCapture(capture);
29
+ if (capture.kind !== 'pdf' || capture.requestId !== token.attemptId || capture.sourceUrl !== source.locator)
30
+ throw Object.assign(Error('integrity'), { code: 'integrity' });
31
+ const original = await bundleText(result.bundleRoot, capture.markdown);
32
+ const allAssets = [...capture.assets, capture.sourcePdf];
33
+ const localized = localizeMarkdown(original.replace(/^<!-- pdf-page: ([1-9][0-9]*) -->$/gm, '## Page $1'), allAssets);
34
+ const assets = [];
35
+ for (const asset of allAssets) {
36
+ if (options.signal?.aborted)
37
+ throw Object.assign(Error('cancelled'), { code: 'cancelled' });
38
+ const blob = await store.putBlob(token, asset.sha256, asset.bytes, bundleBytes(result.bundleRoot, asset));
39
+ assets.push({ ...blob, mediaType: asset.mediaType, relationship: asset === capture.sourcePdf ? 'source-pdf' : 'image' });
40
+ }
41
+ if (options.signal?.aborted) {
42
+ store.requestCancellation(token.jobId);
43
+ throw Object.assign(Error('cancelled'), { code: 'cancelled' });
44
+ }
45
+ const selectedTitle = selectedMetadata(capture.metadata, 'title')?.value;
46
+ const title = typeof selectedTitle === 'string' ? selectedTitle : undefined;
47
+ const status = localized.missingImages ? 'partial' : capture.status;
48
+ // Representation algorithm participates in identity: later extraction changes cannot reuse an old payload.
49
+ const bundleHash = createHash('sha256').update(JSON.stringify(['pdf-local-markdown-v1', capture.manifestSha256])).digest('hex');
50
+ await options.checkpoint?.('promoting');
51
+ const revision = store.commitRevision({ ...token, itemId: source.itemId, bundleHash, markdown: localized.markdown, title, extractor: capture.extractor, status, metadata: capture.metadata, assets, warnings: [...new Set([...capture.warnings, ...(localized.missingImages ? ['partial'] : [])])] });
52
+ committed = true;
53
+ outcome = { ok: true, itemId: source.itemId, revisionId: revision.revisionId, status };
54
+ }
55
+ }
56
+ catch (error) {
57
+ const code = error?.code;
58
+ outcome = failure(token.attemptId, code && known.has(code) ? code : 'unclassified', 'storage');
59
+ }
60
+ if (captured)
61
+ try {
62
+ await captured.dispose();
63
+ }
64
+ catch {
65
+ cleanup = 'pending';
66
+ }
67
+ if (!committed)
68
+ try {
69
+ store.failAttempt(token, outcome.ok ? 'unclassified' : outcome.code, outcome.ok ? {} : { retryable: outcome.retryable, retryAfterMs: outcome.retryAfterMs });
70
+ }
71
+ catch {
72
+ cleanup = 'pending';
73
+ }
74
+ if (cleanup === 'settled')
75
+ try {
76
+ store.settleAttempt(token);
77
+ }
78
+ catch {
79
+ cleanup = 'pending';
80
+ }
81
+ if (cleanup === 'pending')
82
+ return { ...failure(token.attemptId, 'cleanup-required', 'cleanup'), cleanup };
83
+ return { ...outcome, cleanup };
84
+ }
@@ -0,0 +1,4 @@
1
+ export { capturePdf, type PdfCaptureRequest, type CaptureOutcome } from './capture.js';
2
+ export { ingestPdf, type PdfOptions, type PdfOutcome, type PdfResult } from './handler.js';
3
+ export { pdfFailure } from './errors.js';
4
+ export { pdfSource } from './source.js';
@@ -0,0 +1,4 @@
1
+ export { capturePdf } from './capture.js';
2
+ export { ingestPdf } from './handler.js';
3
+ export { pdfFailure } from './errors.js';
4
+ export { pdfSource } from './source.js';
@@ -0,0 +1,2 @@
1
+ /** Copy a bounded stable regular file; never pass the user's source to a converter. */
2
+ export declare function copyPdfInput(source: string, target: string, signal?: AbortSignal): Promise<string>;
@@ -0,0 +1,46 @@
1
+ import { open } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import { limits } from '../../contracts/index.js';
5
+ /** Copy a bounded stable regular file; never pass the user's source to a converter. */
6
+ export async function copyPdfInput(source, target, signal) {
7
+ const input = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW);
8
+ try {
9
+ const before = await input.stat();
10
+ if (!before.isFile())
11
+ throw Error('invalid-input');
12
+ if (before.size > limits.pdfBytes)
13
+ throw Error('limit-exceeded');
14
+ const output = await open(target, 'wx', 0o600), hash = createHash('sha256');
15
+ let total = 0;
16
+ try {
17
+ const buffer = Buffer.alloc(1024 ** 2);
18
+ for (;;) {
19
+ if (signal?.aborted)
20
+ throw Error('cancelled');
21
+ const { bytesRead } = await input.read(buffer, 0, buffer.length, null);
22
+ if (!bytesRead)
23
+ break;
24
+ total += bytesRead;
25
+ if (total > limits.pdfBytes)
26
+ throw Error('limit-exceeded');
27
+ if (total === bytesRead && buffer.subarray(0, 5).toString() !== '%PDF-')
28
+ throw Error('invalid-input');
29
+ const bytes = buffer.subarray(0, bytesRead);
30
+ hash.update(bytes);
31
+ await output.writeFile(bytes);
32
+ }
33
+ await output.sync();
34
+ }
35
+ finally {
36
+ await output.close();
37
+ }
38
+ const after = await input.stat();
39
+ if (total < 5 || total !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs)
40
+ throw Error('integrity');
41
+ return hash.digest('hex');
42
+ }
43
+ finally {
44
+ await input.close();
45
+ }
46
+ }
@@ -0,0 +1,10 @@
1
+ import type { CaptureResult } from '../../contracts/index.js';
2
+ export declare function normalizePdfBundle(input: {
3
+ bundleRoot: string;
4
+ inspection: unknown;
5
+ summary: unknown;
6
+ requestId: string;
7
+ sourceUrl: string;
8
+ wordcellVersion: string;
9
+ sourceHash?: string;
10
+ }): Promise<CaptureResult>;
@@ -0,0 +1,64 @@
1
+ import { limits } from '../../contracts/index.js';
2
+ import { validateCapture, validateAsset } from '../../contracts/validate.js';
3
+ import { readConfined, hash } from '../acquisition/files.js';
4
+ import { object } from '../acquisition/normalize.js';
5
+ export async function normalizePdfBundle(input) {
6
+ const verified = object(input.inspection), inspection = object(verified.inspection), summary = object(input.summary);
7
+ if (verified.ok !== true || inspection.schemaVersion !== 1 || !Array.isArray(inspection.assets))
8
+ throw Error('integrity');
9
+ const manifestBytes = await readConfined(input.bundleRoot, 'capture.json', limits.metadataBytes), manifest = object(JSON.parse(manifestBytes.toString('utf8')));
10
+ if (manifest.schemaVersion !== 1 || manifest.kind !== 'pdf' || !['complete', 'partial'].includes(String(manifest.status)))
11
+ throw Error('integrity');
12
+ function asset(raw, mediaType) { const value = object(raw); const result = { relativePath: String(value.path), bytes: Number(value.bytes), sha256: String(value.sha256), mediaType }; validateAsset(result); return result; }
13
+ function assertVerified(value, checked) { const record = object(checked); if (record.integrity !== 'verified' || record.path !== value.relativePath || record.sha256 !== value.sha256 || record.bytes !== value.bytes)
14
+ throw Error('integrity'); }
15
+ const markdown = asset(manifest.markdown, 'text/markdown'), sourcePdf = asset(manifest.source, 'application/pdf');
16
+ assertVerified(markdown, inspection.markdown);
17
+ assertVerified(sourcePdf, inspection.source);
18
+ if (input.sourceHash && sourcePdf.sha256 !== input.sourceHash)
19
+ throw Error('integrity');
20
+ const source = object(manifest.source);
21
+ if (!input.sourceHash && (typeof verified.expectedSourceUrl !== 'string' || source.requestedUrl !== verified.expectedSourceUrl))
22
+ throw Error('integrity');
23
+ const markdownBytes = await readConfined(input.bundleRoot, markdown.relativePath, limits.markdownBytes);
24
+ if (hash(markdownBytes) !== markdown.sha256 || markdownBytes.length !== markdown.bytes)
25
+ throw Error('integrity');
26
+ if (!Array.isArray(manifest.images) || manifest.images.length > limits.imagesPerItem)
27
+ throw Error('limit-exceeded');
28
+ const byPath = new Map();
29
+ for (const raw of manifest.images) {
30
+ const image = object(raw), record = object(image.asset), value = asset(record, String(record.mimeType));
31
+ if (!value.mediaType.startsWith('image/'))
32
+ throw Error('integrity');
33
+ assertVerified(value, inspection.assets.find(raw => object(raw).path === value.relativePath));
34
+ const prior = byPath.get(value.relativePath);
35
+ if (prior && JSON.stringify(prior) !== JSON.stringify(value))
36
+ throw Error('integrity');
37
+ byPath.set(value.relativePath, value);
38
+ }
39
+ if (byPath.size !== inspection.assets.length)
40
+ throw Error('integrity');
41
+ const metadata = [], document = object(manifest.document), observedAt = String(manifest.capturedAt);
42
+ for (const field of ['title', 'author', 'subject', 'creator', 'producer', 'createdAt', 'modifiedAt']) {
43
+ const value = document[field];
44
+ if (typeof value === 'string' && value.length > 0 && value.length <= 16384)
45
+ metadata.push({ field, value, source: 'pdf', observedAt, rule: 'wordcell-pdf-v1', selected: true });
46
+ }
47
+ metadata.push({ field: 'pageCount', value: String(document.pageCount), source: 'pdf', observedAt, rule: 'wordcell-pdf-v1' });
48
+ if (manifest.quality) {
49
+ const quality = object(manifest.quality);
50
+ if (Array.isArray(quality.issues)) {
51
+ metadata.push({ field: 'pdfQualityIssueCount', value: String(quality.issues.length), source: 'pdf', observedAt, rule: 'wordcell-pdf-v1' });
52
+ if (quality.issues.length > 400)
53
+ metadata.push({ field: 'pdfQualityIssuesTruncated', value: 'true', source: 'pdf', observedAt, rule: 'wordcell-pdf-v1' });
54
+ for (const issue of quality.issues.slice(0, 400)) {
55
+ const row = object(issue);
56
+ metadata.push({ field: 'pdfQuality', value: JSON.stringify({ code: row.code, page: row.page }), source: 'pdf', observedAt, rule: 'wordcell-pdf-v1' });
57
+ }
58
+ }
59
+ }
60
+ const partial = manifest.status !== 'complete' || inspection.status !== 'complete' || summary.status !== 'complete';
61
+ const result = { schemaVersion: 1, requestId: input.requestId, kind: 'pdf', status: partial ? 'partial' : 'complete', sourceUrl: input.sourceUrl, markdown, sourcePdf, assets: [...byPath.values()], metadata, warnings: partial ? ['partial'] : [], extractor: { name: 'wordcell', version: input.wordcellVersion }, manifestSha256: hash(manifestBytes), completeness: { text: partial ? 'partial' : 'complete', images: partial ? 'partial' : 'complete' } };
62
+ validateCapture(result);
63
+ return result;
64
+ }
@@ -0,0 +1,4 @@
1
+ /** Local PDFs are bound to copied bytes; only remote sources use Wordcell's URL sanitizer. */
2
+ export declare function pdfSourceIdentity(sourceUrl: string, sanitize: (url: string) => string): {
3
+ expectedSourceUrl?: string;
4
+ };
@@ -0,0 +1,12 @@
1
+ /** Local PDFs are bound to copied bytes; only remote sources use Wordcell's URL sanitizer. */
2
+ export function pdfSourceIdentity(sourceUrl, sanitize) {
3
+ const url = new URL(sourceUrl);
4
+ if (url.protocol === 'file:') {
5
+ if (url.host || url.username || url.password || url.search || url.hash)
6
+ throw Error('integrity');
7
+ return {};
8
+ }
9
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password)
10
+ throw Error('integrity');
11
+ return { expectedSourceUrl: sanitize(sourceUrl) };
12
+ }
@@ -0,0 +1,6 @@
1
+ /** PDF identity includes its kind and account realm. Local files stay separate from URLs. */
2
+ export declare function pdfSource(input: string, realm?: string): {
3
+ sourceKey: string;
4
+ locator: string;
5
+ realm: string;
6
+ };
@@ -0,0 +1,22 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { InputError } from '../app/source.js';
5
+ /** PDF identity includes its kind and account realm. Local files stay separate from URLs. */
6
+ export function pdfSource(input, realm = 'public') {
7
+ if (!input || Buffer.byteLength(input) > 8192 || !realm || Buffer.byteLength(realm) > 256 || /[\u0000-\u001f\u007f]/u.test(input + realm))
8
+ throw new InputError();
9
+ let url;
10
+ try {
11
+ url = /^[a-z][a-z0-9+.-]*:/i.test(input) ? new URL(input) : pathToFileURL(resolve(input));
12
+ }
13
+ catch {
14
+ throw new InputError();
15
+ }
16
+ if (!['file:', 'https:', 'http:'].includes(url.protocol) || url.username || url.password)
17
+ throw new InputError();
18
+ if (url.protocol === 'file:' && (url.host || url.hash || url.search || realm !== 'public'))
19
+ throw new InputError();
20
+ const locator = url.href, sourceKey = createHash('sha256').update(JSON.stringify(['pdf', realm, locator])).digest('hex');
21
+ return { sourceKey, locator, realm };
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ // Public PDF verifier, executed only by the integrity-checked pinned Bun runtime.
2
+ import { pdfSourceIdentity } from './source-identity.js';
3
+ const runtime = process.argv[2], bundle = process.argv[3];
4
+ try {
5
+ const { sanitizeArtifactUrl } = await import(import.meta.resolve('@hraness/wordcell/clip/persist', `${runtime}/package.json`));
6
+ const { verifyPdfCaptureBundle } = await import(import.meta.resolve('@hraness/wordcell/pdf', `${runtime}/package.json`));
7
+ const result = await verifyPdfCaptureBundle(bundle, { maxDocumentBytes: 16 * 1024 ** 2, maxSourceBytes: 100 * 1024 ** 2, maxVerifiedAssets: 200, maxAssetBytes: 20 * 1024 ** 2, maxTotalAssetBytes: 500 * 1024 ** 2, maxVerificationMs: 30000 });
8
+ const { markdown, ...document } = result.inspection.markdown;
9
+ const output = JSON.stringify({ ...result, ...pdfSourceIdentity(process.argv[4], sanitizeArtifactUrl), inspection: { ...result.inspection, markdown: document } });
10
+ if (Buffer.byteLength(output) > 65536)
11
+ throw Error('frame-limit');
12
+ process.stdout.write(output);
13
+ }
14
+ catch {
15
+ process.stdout.write(JSON.stringify({ ok: false }));
16
+ process.exitCode = 1;
17
+ }
@@ -0,0 +1,23 @@
1
+ import type { LibraryStore } from '../store/index.js';
2
+ import { type FileRecord } from './files.js';
3
+ export interface BackupManifest {
4
+ format: 'sponge-backup';
5
+ version: 1;
6
+ schemaVersion: number;
7
+ libraryId: string;
8
+ revisionEpoch: number;
9
+ files: FileRecord[];
10
+ }
11
+ export declare function backupLibrary(store: LibraryStore, output: string): Promise<{
12
+ output: string;
13
+ sha256: string;
14
+ bytes: number;
15
+ itemsSnapshotEpoch: number;
16
+ }>;
17
+ /** Versioned length-framed archive: no tar links, executable hooks or path extraction. */
18
+ export declare function restoreLibrary(input: string, into: string): Promise<{
19
+ root: string;
20
+ libraryId: string;
21
+ incarnation: string;
22
+ heldJobs: number;
23
+ }>;