@hraness/sponge 0.2.5 → 0.3.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 (227) hide show
  1. package/README.md +23 -10
  2. package/bin/sponge.js +8 -7
  3. package/dist/contracts/index.js +24 -6
  4. package/dist/contracts/validate.js +106 -29
  5. package/dist/migrations/008.d.ts +3 -0
  6. package/dist/migrations/008.js +5 -0
  7. package/dist/migrations/009.d.ts +3 -0
  8. package/dist/migrations/009.js +8 -0
  9. package/dist/migrations/010.d.ts +3 -0
  10. package/dist/migrations/010.js +11 -0
  11. package/dist/migrations/index.d.ts +15 -0
  12. package/dist/migrations/index.js +39 -0
  13. package/dist/src/acquisition/capture.js +87 -28
  14. package/dist/src/acquisition/cleanup.d.ts +0 -4
  15. package/dist/src/acquisition/cleanup.js +3 -5
  16. package/dist/src/acquisition/errors.js +17 -2
  17. package/dist/src/acquisition/files.d.ts +0 -1
  18. package/dist/src/acquisition/files.js +14 -11
  19. package/dist/src/acquisition/index.d.ts +2 -3
  20. package/dist/src/acquisition/index.js +1 -2
  21. package/dist/src/acquisition/lock.js +1 -1
  22. package/dist/src/acquisition/normalize.js +57 -18
  23. package/dist/src/acquisition/pins.js +22 -4
  24. package/dist/src/acquisition/process.js +67 -29
  25. package/dist/src/acquisition/runtime.d.ts +5 -7
  26. package/dist/src/acquisition/runtime.js +129 -68
  27. package/dist/src/acquisition/verify-helper.js +10 -3
  28. package/dist/src/annotations/quote.js +40 -12
  29. package/dist/src/annotations/store.d.ts +0 -1
  30. package/dist/src/annotations/store.js +66 -14
  31. package/dist/src/app/article.d.ts +2 -1
  32. package/dist/src/app/article.js +66 -70
  33. package/dist/src/app/bundle-files.d.ts +0 -4
  34. package/dist/src/app/bundle-files.js +11 -14
  35. package/dist/src/app/markdown.js +18 -6
  36. package/dist/src/app/source.d.ts +0 -4
  37. package/dist/src/app/source.js +18 -14
  38. package/dist/src/cli/arguments.d.ts +2 -8
  39. package/dist/src/cli/arguments.js +67 -80
  40. package/dist/src/cli/commands.d.ts +42 -0
  41. package/dist/src/cli/commands.js +795 -0
  42. package/dist/src/cli/config.js +12 -9
  43. package/dist/src/cli/digest.d.ts +1 -1
  44. package/dist/src/cli/digest.js +39 -17
  45. package/dist/src/cli/import.js +25 -4
  46. package/dist/src/cli/indexing.d.ts +2 -0
  47. package/dist/src/cli/indexing.js +26 -9
  48. package/dist/src/cli/main.js +47 -386
  49. package/dist/src/cli/portability.d.ts +0 -2
  50. package/dist/src/cli/portability.js +13 -8
  51. package/dist/src/cli/queue.js +12 -8
  52. package/dist/src/cli/runtimes.d.ts +1 -3
  53. package/dist/src/cli/runtimes.js +13 -6
  54. package/dist/src/cli/terminal.js +1 -1
  55. package/dist/src/cli/worker.d.ts +7 -0
  56. package/dist/src/cli/worker.js +54 -15
  57. package/dist/src/derivations/kinds.d.ts +34 -0
  58. package/dist/src/derivations/kinds.js +14 -0
  59. package/dist/src/derivations/types.d.ts +48 -0
  60. package/dist/src/derivations/types.js +1 -0
  61. package/dist/src/digest/derivation.d.ts +25 -0
  62. package/dist/src/digest/derivation.js +124 -0
  63. package/dist/src/digest/editorial.d.ts +10 -46
  64. package/dist/src/digest/editorial.js +73 -59
  65. package/dist/src/digest/endpoint.js +63 -27
  66. package/dist/src/digest/pipeline.d.ts +53 -32
  67. package/dist/src/digest/pipeline.js +357 -125
  68. package/dist/src/digest/schema.d.ts +44 -17
  69. package/dist/src/digest/schema.js +132 -44
  70. package/dist/src/errors.d.ts +46 -0
  71. package/dist/src/errors.js +117 -0
  72. package/dist/src/identity/query-policy.d.ts +0 -5
  73. package/dist/src/identity/query-policy.js +61 -9
  74. package/dist/src/identity/uri.d.ts +6 -5
  75. package/dist/src/identity/uri.js +37 -18
  76. package/dist/src/identity/visibility.d.ts +11 -1
  77. package/dist/src/identity/visibility.js +81 -17
  78. package/dist/src/importers/apply.js +104 -36
  79. package/dist/src/importers/custody.js +27 -9
  80. package/dist/src/importers/inspect.js +125 -38
  81. package/dist/src/importers/jungle-digest.d.ts +31 -24
  82. package/dist/src/importers/jungle-digest.js +217 -115
  83. package/dist/src/importers/jungle-store.d.ts +20 -11
  84. package/dist/src/importers/jungle-store.js +136 -41
  85. package/dist/src/importers/jungle.d.ts +32 -16
  86. package/dist/src/importers/jungle.js +145 -32
  87. package/dist/src/importers/manifest.js +57 -21
  88. package/dist/src/importers/verify-helper.js +18 -4
  89. package/dist/src/indexing/image.js +29 -10
  90. package/dist/src/indexing/index.d.ts +37 -6
  91. package/dist/src/indexing/index.js +360 -124
  92. package/dist/src/indexing/jobs.js +156 -47
  93. package/dist/src/indexing/prune-run.d.ts +2 -0
  94. package/dist/src/indexing/prune-run.js +69 -26
  95. package/dist/src/indexing/prune.d.ts +12 -3
  96. package/dist/src/indexing/prune.js +85 -35
  97. package/dist/src/indexing/runner.d.ts +2 -1
  98. package/dist/src/indexing/runner.js +83 -40
  99. package/dist/src/indexing/upgrade.d.ts +9 -0
  100. package/dist/src/indexing/upgrade.js +79 -0
  101. package/dist/src/inference/asr/bridge.js +6 -2
  102. package/dist/src/inference/asr/protocol.js +109 -19
  103. package/dist/src/inference/asr/runtime.js +58 -21
  104. package/dist/src/inference/asr/windows.js +9 -5
  105. package/dist/src/inference/embedding/index.d.ts +2 -6
  106. package/dist/src/inference/embedding/index.js +89 -29
  107. package/dist/src/ingest/attempt.d.ts +29 -0
  108. package/dist/src/ingest/attempt.js +55 -0
  109. package/dist/src/media/capture-helper.js +7 -2
  110. package/dist/src/media/capture.d.ts +2 -1
  111. package/dist/src/media/capture.js +124 -43
  112. package/dist/src/media/handler.d.ts +4 -2
  113. package/dist/src/media/handler.js +88 -70
  114. package/dist/src/media/transcribe.js +63 -21
  115. package/dist/src/metadata/article.js +49 -17
  116. package/dist/src/models/artifacts.d.ts +10 -8
  117. package/dist/src/models/artifacts.js +237 -106
  118. package/dist/src/models/catalog.d.ts +2 -5
  119. package/dist/src/models/catalog.js +94 -25
  120. package/dist/src/models/download.js +55 -15
  121. package/dist/src/models/runtime.d.ts +0 -2
  122. package/dist/src/models/runtime.js +69 -33
  123. package/dist/src/pdf/capture-helper.js +3 -1
  124. package/dist/src/pdf/capture.d.ts +2 -1
  125. package/dist/src/pdf/capture.js +95 -27
  126. package/dist/src/pdf/errors.js +12 -1
  127. package/dist/src/pdf/handler.d.ts +2 -1
  128. package/dist/src/pdf/handler.js +70 -71
  129. package/dist/src/pdf/index.d.ts +1 -3
  130. package/dist/src/pdf/index.js +0 -2
  131. package/dist/src/pdf/input.js +12 -7
  132. package/dist/src/pdf/normalize.js +80 -21
  133. package/dist/src/pdf/source-identity.js +3 -2
  134. package/dist/src/pdf/source.js +12 -8
  135. package/dist/src/pdf/verify-helper.js +15 -3
  136. package/dist/src/portability/backup.d.ts +2 -9
  137. package/dist/src/portability/backup.js +108 -22
  138. package/dist/src/portability/database.js +51 -17
  139. package/dist/src/portability/export.js +35 -9
  140. package/dist/src/portability/files.d.ts +1 -4
  141. package/dist/src/portability/files.js +127 -67
  142. package/dist/src/portability/gc.js +65 -22
  143. package/dist/src/publish/component.js +1 -1
  144. package/dist/src/publish/feed.d.ts +2 -2
  145. package/dist/src/publish/feed.js +75 -29
  146. package/dist/src/publish/identity.d.ts +5 -1
  147. package/dist/src/publish/identity.js +31 -11
  148. package/dist/src/publish/index.d.ts +2 -3
  149. package/dist/src/publish/index.js +34 -21
  150. package/dist/src/publish/projection.d.ts +2 -9
  151. package/dist/src/publish/projection.js +21 -14
  152. package/dist/src/search/extension.js +3 -1
  153. package/dist/src/search/hybrid.d.ts +3 -8
  154. package/dist/src/search/hybrid.js +45 -11
  155. package/dist/src/search/lexical.js +18 -4
  156. package/dist/src/search/query.js +56 -25
  157. package/dist/src/search/vector.js +37 -10
  158. package/dist/src/service/launch-agent.d.ts +5 -3
  159. package/dist/src/service/launch-agent.js +272 -83
  160. package/dist/src/service/lifetime.js +47 -14
  161. package/dist/src/service/loop.d.ts +3 -3
  162. package/dist/src/service/loop.js +47 -19
  163. package/dist/src/store/annotations.d.ts +32 -0
  164. package/dist/src/store/annotations.js +44 -0
  165. package/dist/src/store/backups.d.ts +18 -0
  166. package/dist/src/store/backups.js +110 -0
  167. package/dist/src/store/blobs.d.ts +24 -0
  168. package/dist/src/store/blobs.js +139 -0
  169. package/dist/src/store/changes.d.ts +5 -12
  170. package/dist/src/store/changes.js +108 -56
  171. package/dist/src/store/context.d.ts +15 -0
  172. package/dist/src/store/context.js +19 -0
  173. package/dist/src/store/derivations.d.ts +42 -0
  174. package/dist/src/store/derivations.js +104 -0
  175. package/dist/src/store/files.d.ts +0 -2
  176. package/dist/src/store/files.js +19 -24
  177. package/dist/src/store/ids.d.ts +30 -0
  178. package/dist/src/store/ids.js +34 -0
  179. package/dist/src/store/imports.d.ts +26 -0
  180. package/dist/src/store/imports.js +57 -0
  181. package/dist/src/store/index.d.ts +3 -186
  182. package/dist/src/store/index.js +2 -809
  183. package/dist/src/store/lease.d.ts +20 -0
  184. package/dist/src/store/lease.js +62 -0
  185. package/dist/src/store/library.d.ts +75 -0
  186. package/dist/src/store/library.js +174 -0
  187. package/dist/src/store/open.d.ts +7 -0
  188. package/dist/src/store/open.js +183 -0
  189. package/dist/src/store/queue.d.ts +65 -0
  190. package/dist/src/store/queue.js +327 -0
  191. package/dist/src/store/revisions.d.ts +12 -0
  192. package/dist/src/store/revisions.js +189 -0
  193. package/dist/src/store/search-index.d.ts +63 -0
  194. package/dist/src/store/search-index.js +190 -0
  195. package/dist/src/store/sync-ready.d.ts +6 -0
  196. package/dist/src/store/sync-ready.js +121 -0
  197. package/dist/src/store/types.d.ts +1 -5
  198. package/dist/src/store/types.js +1 -8
  199. package/dist/src/util/canonical.d.ts +2 -0
  200. package/dist/src/util/canonical.js +17 -0
  201. package/dist/src/util/exact.d.ts +2 -0
  202. package/dist/src/util/exact.js +9 -0
  203. package/dist/src/util/fsync.d.ts +4 -0
  204. package/dist/src/util/fsync.js +22 -0
  205. package/dist/src/util/hash.d.ts +4 -0
  206. package/dist/src/util/hash.js +8 -0
  207. package/dist/src/worker/basic.d.ts +0 -1
  208. package/dist/src/worker/basic.js +23 -7
  209. package/dist/src/worker/custody.d.ts +4 -1
  210. package/dist/src/worker/custody.js +111 -38
  211. package/dist/src/worker/engine.d.ts +2 -1
  212. package/dist/src/worker/engine.js +42 -20
  213. package/dist/src/worker/identity.js +13 -5
  214. package/dist/src/worker/journal.d.ts +1 -1
  215. package/dist/src/worker/journal.js +54 -24
  216. package/dist/src/worker/lock.d.ts +18 -0
  217. package/dist/src/worker/lock.js +196 -0
  218. package/dist/src/worker/runtime-gc.d.ts +4 -3
  219. package/dist/src/worker/runtime-gc.js +68 -41
  220. package/dist/src/worker/supervisor-child.js +19 -6
  221. package/dist/src/worker/supervisor.js +41 -8
  222. package/native/runtime-manifest.json +4 -4
  223. package/package.json +10 -2
  224. package/dist/src/digest/store.d.ts +0 -57
  225. package/dist/src/digest/store.js +0 -77
  226. package/dist/src/store/foundation.d.ts +0 -7
  227. package/dist/src/store/foundation.js +0 -7
@@ -1,82 +1,100 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { join } from 'node:path';
3
- import { failure } from '../acquisition/errors.js';
2
+ import { runAttempt, classifyErrors } from '../ingest/attempt.js';
3
+ import { sha256 } from '../util/hash.js';
4
4
  import { bundleBytes } from '../app/bundle-files.js';
5
5
  import { runOwned } from '../acquisition/process.js';
6
6
  import { captureMedia } from './capture.js';
7
7
  import { transcribeMedia } from './transcribe.js';
8
+ import { SpongeError } from '../errors.js';
9
+ const mediaCode = classifyErrors([
10
+ 'cleanup-required',
11
+ 'integrity',
12
+ 'invalid-input',
13
+ 'limit-exceeded',
14
+ 'cancelled',
15
+ 'owner-busy',
16
+ ]);
8
17
  /** Only a transcript and thumbnail enter durable storage; acquisition owns all source media. */
9
- export async function ingestMedia(store, token, options) {
10
- let captured, committed = false, cleanup = 'settled';
11
- let outcome = failure(token.attemptId, 'unclassified');
12
- try {
13
- const source = store.sourceForJob(token.jobId);
18
+ export function ingestMedia(store, token, options) {
19
+ // An uncertain native process may still be reading source bytes: runAttempt then preserves ownership.
20
+ return runAttempt(store, token, (error, attempt) => {
21
+ const code = mediaCode(error);
22
+ if (code === 'cleanup-required')
23
+ attempt.cleanup = 'pending';
24
+ return code;
25
+ }, async (attempt) => {
26
+ const source = store.queue.source(token.jobId);
14
27
  if (!source || source.kind !== 'media')
15
- throw Error('invalid-input');
16
- const result = await (options.capture ?? captureMedia)({ ...options, stagingRoot: join(store.root, 'staging'), requestId: token.attemptId, sourceUrl: source.locator });
28
+ throw new SpongeError('invalid-input');
29
+ const result = await (options.capture ?? captureMedia)({
30
+ ...options,
31
+ stagingRoot: join(store.root, 'staging'),
32
+ requestId: token.attemptId,
33
+ sourceUrl: source.locator,
34
+ });
17
35
  if (!result.ok) {
18
- outcome = result;
19
- cleanup = result.code === 'cleanup-required' ? 'pending' : 'settled';
36
+ if (result.code === 'cleanup-required')
37
+ attempt.cleanup = 'pending';
38
+ return result;
20
39
  }
21
- else {
22
- captured = result;
23
- await options.checkpoint?.('extracting');
24
- const transcription = await (options.transcribe ?? transcribeMedia)({ requestId: token.attemptId, audio: result.audio, captions: result.captions, language: options.language, forceLocal: options.forceLocal }, { scratchRoot: result.root, runtime: options.asrRuntime, signal: options.signal, runProcess: options.runProcess ?? runOwned });
25
- cleanup = transcription.cleanup;
26
- if (!transcription.ok)
27
- outcome = transcription;
28
- else {
29
- const transcriptBytes = Buffer.from(JSON.stringify(transcription.transcript)), transcriptHash = createHash('sha256').update(transcriptBytes).digest('hex');
30
- const transcriptBlob = await store.putBlob(token, transcriptHash, transcriptBytes.length, (async function* () { yield transcriptBytes; })());
31
- const assets = [{ ...transcriptBlob, mediaType: 'application/json', relationship: 'transcript' }];
32
- if (result.thumbnail) {
33
- const blob = await store.putBlob(token, result.thumbnail.sha256, result.thumbnail.bytes, bundleBytes(result.root, result.thumbnail));
34
- assets.push({ ...blob, mediaType: result.thumbnail.mediaType, relationship: 'thumbnail' });
35
- }
36
- if (options.signal?.aborted) {
37
- store.requestCancellation(token.jobId);
38
- throw Error('cancelled');
39
- }
40
- const observedAt = new Date().toISOString(), metadata = [{ field: 'transcriptSource', value: transcription.transcript.source, source: 'media', observedAt }, { field: 'language', value: transcription.transcript.language, source: 'media', observedAt }, { field: 'durationMs', value: String(transcription.transcript.durationMs), source: 'media', observedAt }];
41
- for (const field of ['title', 'author'])
42
- if (result[field])
43
- metadata.push({ field: field === 'author' ? 'authors' : field, value: field === 'author' ? [result[field]] : result[field], source: 'media', observedAt, selected: true });
44
- const status = result.warnings.length ? 'partial' : 'complete';
45
- await options.checkpoint?.('promoting');
46
- const revision = store.commitRevision({ ...token, itemId: source.itemId, bundleHash: createHash('sha256').update(JSON.stringify(['media-transcript-v1', result.manifestSha256, transcriptHash])).digest('hex'), markdown: transcription.markdown, title: result.title, extractor: { name: 'sponge-media', version: '1' }, status, metadata, assets, warnings: result.warnings.length ? ['partial'] : [] });
47
- committed = true;
48
- outcome = { ok: true, itemId: source.itemId, revisionId: revision.revisionId, status };
49
- }
40
+ attempt.hold(result);
41
+ await options.checkpoint?.('extracting');
42
+ const transcription = await (options.transcribe ?? transcribeMedia)({
43
+ requestId: token.attemptId,
44
+ audio: result.audio,
45
+ captions: result.captions,
46
+ language: options.language,
47
+ forceLocal: options.forceLocal,
48
+ }, {
49
+ scratchRoot: result.root,
50
+ runtime: options.asrRuntime,
51
+ signal: options.signal,
52
+ runProcess: options.runProcess ?? runOwned,
53
+ });
54
+ attempt.cleanup = transcription.cleanup;
55
+ if (!transcription.ok)
56
+ return transcription;
57
+ const transcriptBytes = Buffer.from(JSON.stringify(transcription.transcript)), transcriptHash = sha256(transcriptBytes);
58
+ const transcriptBlob = await store.blobs.put(token, transcriptHash, transcriptBytes.length, (async function* () {
59
+ yield transcriptBytes;
60
+ })());
61
+ const assets = [{ ...transcriptBlob, mediaType: 'application/json', relationship: 'transcript' }];
62
+ if (result.thumbnail) {
63
+ const blob = await store.blobs.put(token, result.thumbnail.sha256, result.thumbnail.bytes, bundleBytes(result.root, result.thumbnail));
64
+ assets.push({ ...blob, mediaType: result.thumbnail.mediaType, relationship: 'thumbnail' });
50
65
  }
51
- }
52
- catch (error) {
53
- const raw = error?.code ?? (error instanceof Error ? error.message : '');
54
- if (raw === 'cleanup-required')
55
- cleanup = 'pending';
56
- const code = ['cleanup-required', 'integrity', 'invalid-input', 'limit-exceeded', 'cancelled', 'owner-busy'].includes(raw) ? raw : 'unclassified';
57
- outcome = failure(token.attemptId, code, 'storage');
58
- }
59
- // An uncertain native process may still be reading source bytes: preserve ownership.
60
- if (captured && cleanup === 'settled')
61
- try {
62
- await captured.dispose();
66
+ if (options.signal?.aborted) {
67
+ store.queue.cancel(token.jobId);
68
+ throw new SpongeError('cancelled');
63
69
  }
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
- return cleanup === 'pending' ? { ...failure(token.attemptId, 'cleanup-required', 'cleanup'), cleanup } : { ...outcome, cleanup };
70
+ const observedAt = new Date().toISOString(), metadata = [
71
+ { field: 'transcriptSource', value: transcription.transcript.source, source: 'media', observedAt },
72
+ { field: 'language', value: transcription.transcript.language, source: 'media', observedAt },
73
+ { field: 'durationMs', value: String(transcription.transcript.durationMs), source: 'media', observedAt },
74
+ ];
75
+ for (const field of ['title', 'author'])
76
+ if (result[field])
77
+ metadata.push({
78
+ field: field === 'author' ? 'authors' : field,
79
+ value: field === 'author' ? [result[field]] : result[field],
80
+ source: 'media',
81
+ observedAt,
82
+ selected: true,
83
+ });
84
+ const status = result.warnings.length ? 'partial' : 'complete';
85
+ await options.checkpoint?.('promoting');
86
+ const revision = store.revisions.commit({
87
+ ...token,
88
+ itemId: source.itemId,
89
+ bundleHash: sha256(JSON.stringify(['media-transcript-v1', result.manifestSha256, transcriptHash])),
90
+ markdown: transcription.markdown,
91
+ title: result.title,
92
+ extractor: { name: 'sponge-media', version: '1' },
93
+ status,
94
+ metadata,
95
+ assets,
96
+ warnings: result.warnings.length ? ['partial'] : [],
97
+ });
98
+ return { ok: true, itemId: source.itemId, revisionId: revision.revisionId, status };
99
+ });
82
100
  }
@@ -1,17 +1,36 @@
1
- import { createHash } from 'node:crypto';
1
+ import { sha256 } from '../util/hash.js';
2
2
  import { failure } from '../acquisition/errors.js';
3
3
  import { transcribeAudio } from '../inference/asr/runtime.js';
4
- const timestamp = (s) => { const m = /^(?:(\d{2,}):)?(\d{2}):(\d{2})\.(\d{3})$/.exec(s); if (!m || Number(m[2]) > 59 || Number(m[3]) > 59)
5
- throw Error('integrity'); const n = ((Number(m[1] ?? 0) * 60 + Number(m[2])) * 60 + Number(m[3])) * 1000 + Number(m[4]); if (n > 86400000)
6
- throw Error('limit-exceeded'); return n; };
7
- function plain(text) { return text.replace(/<[^>]*>/g, '').replace(/&(?:amp|lt|gt|quot|apos|nbsp);/g, s => ({ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&apos;': "'", '&nbsp;': ' ' })[s]).replace(/\s+/g, ' ').trim(); }
4
+ import { SpongeError, codeOf } from '../errors.js';
5
+ const timestamp = (s) => {
6
+ const m = /^(?:(\d{2,}):)?(\d{2}):(\d{2})\.(\d{3})$/.exec(s);
7
+ if (!m || Number(m[2]) > 59 || Number(m[3]) > 59)
8
+ throw new SpongeError('integrity');
9
+ const n = ((Number(m[1] ?? 0) * 60 + Number(m[2])) * 60 + Number(m[3])) * 1000 + Number(m[4]);
10
+ if (n > 86400000)
11
+ throw new SpongeError('limit-exceeded');
12
+ return n;
13
+ };
14
+ function plain(text) {
15
+ return text
16
+ .replace(/<[^>]*>/g, '')
17
+ .replace(/&(?:amp|lt|gt|quot|apos|nbsp);/g, (s) => ({ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&apos;': "'", '&nbsp;': ' ' })[s])
18
+ .replace(/\s+/g, ' ')
19
+ .trim();
20
+ }
8
21
  /** Strict, bounded timed WebVTT. Rolling captions only deduplicate already-overlapping text. */
9
22
  export function parseWebVtt(captions, requestId) {
10
- if (Buffer.byteLength(captions.text) > 4 * 1024 ** 2 || createHash('sha256').update(captions.text).digest('hex') !== captions.sha256 || !['manual', 'automatic'].includes(captions.source) || !/^[a-zA-Z]{2,8}(?:-[a-zA-Z0-9]{1,8})*$/.test(captions.language) || !Number.isFinite(captions.durationMs) || captions.durationMs <= 0 || captions.durationMs > 86400000)
11
- throw Error('integrity');
23
+ if (Buffer.byteLength(captions.text) > 4 * 1024 ** 2 ||
24
+ sha256(captions.text) !== captions.sha256 ||
25
+ !['manual', 'automatic'].includes(captions.source) ||
26
+ !/^[a-zA-Z]{2,8}(?:-[a-zA-Z0-9]{1,8})*$/.test(captions.language) ||
27
+ !Number.isFinite(captions.durationMs) ||
28
+ captions.durationMs <= 0 ||
29
+ captions.durationMs > 86400000)
30
+ throw new SpongeError('integrity');
12
31
  const text = captions.text.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
13
32
  if (!/^WEBVTT(?:[ \t][^\n]*)?\n/.test(text) || text.includes('\0'))
14
- throw Error('integrity');
33
+ throw new SpongeError('integrity');
15
34
  const segments = [];
16
35
  let lastStart = 0;
17
36
  let previousCue;
@@ -21,17 +40,17 @@ export function parseWebVtt(captions, requestId) {
21
40
  continue;
22
41
  const index = lines[0].includes('-->') ? 0 : 1, line = lines[index];
23
42
  if (!line)
24
- throw Error('integrity');
43
+ throw new SpongeError('integrity');
25
44
  const match = /^(\S+)\s+-->\s+(\S+)(?:\s+.*)?$/.exec(line);
26
45
  if (!match)
27
- throw Error('integrity');
46
+ throw new SpongeError('integrity');
28
47
  const startMs = timestamp(match[1]), endMs = timestamp(match[2]);
29
48
  if (startMs < lastStart || endMs <= startMs || endMs > captions.durationMs + 1000)
30
- throw Error('integrity');
49
+ throw new SpongeError('integrity');
31
50
  lastStart = startMs;
32
51
  let cue = plain(lines.slice(index + 1).join(' '));
33
52
  if (Buffer.byteLength(cue) > 16384)
34
- throw Error('limit-exceeded');
53
+ throw new SpongeError('limit-exceeded');
35
54
  if (!cue)
36
55
  continue;
37
56
  const previous = segments.at(-1), rawCue = cue;
@@ -51,29 +70,52 @@ export function parseWebVtt(captions, requestId) {
51
70
  }
52
71
  segments.push({ startMs, endMs, text: cue });
53
72
  if (segments.length > 30000)
54
- throw Error('limit-exceeded');
73
+ throw new SpongeError('limit-exceeded');
55
74
  }
56
75
  if (!segments.length)
57
- throw Error('unsupported');
58
- return { schemaVersion: 1, requestId, source: captions.source === 'manual' ? 'human-captions' : 'automatic-captions', language: captions.language, segments, cleanup: 'settled', sourceSha256: captions.sha256, timing: 'source-captions', durationMs: captions.durationMs };
76
+ throw new SpongeError('unsupported');
77
+ return {
78
+ schemaVersion: 1,
79
+ requestId,
80
+ source: captions.source === 'manual' ? 'human-captions' : 'automatic-captions',
81
+ language: captions.language,
82
+ segments,
83
+ cleanup: 'settled',
84
+ sourceSha256: captions.sha256,
85
+ timing: 'source-captions',
86
+ durationMs: captions.durationMs,
87
+ };
59
88
  }
60
- const stamp = (ms) => { const seconds = Math.floor(ms / 1000); return [Math.floor(seconds / 3600), Math.floor(seconds / 60) % 60, seconds % 60].map(n => String(n).padStart(2, '0')).join(':'); };
89
+ const stamp = (ms) => {
90
+ const seconds = Math.floor(ms / 1000);
91
+ return [Math.floor(seconds / 3600), Math.floor(seconds / 60) % 60, seconds % 60]
92
+ .map((n) => String(n).padStart(2, '0'))
93
+ .join(':');
94
+ };
61
95
  export function transcriptMarkdown(transcript) {
62
96
  const paragraphs = [];
63
97
  for (const segment of transcript.segments) {
64
98
  const prior = paragraphs.at(-1);
65
- if (transcript.source === 'local-asr' && prior && segment.startMs - prior.endMs <= 2000 && segment.endMs - prior.startMs <= 15000 && !/[.!?]["')]*$/.test(prior.text)) {
99
+ if (transcript.source === 'local-asr' &&
100
+ prior &&
101
+ segment.startMs - prior.endMs <= 2000 &&
102
+ segment.endMs - prior.startMs <= 15000 &&
103
+ !/[.!?]["')]*$/.test(prior.text)) {
66
104
  prior.text += ' ' + segment.text;
67
105
  prior.endMs = segment.endMs;
68
106
  }
69
107
  else
70
108
  paragraphs.push({ ...segment });
71
109
  }
72
- return paragraphs.map(s => `[${stamp(s.startMs)}] ${s.text.replace(/[\\`*_{}\[\]()<>!#|]/g, '\\$&')}`).join('\n\n') + '\n';
110
+ return (paragraphs.map((s) => `[${stamp(s.startMs)}] ${s.text.replace(/[\\`*_{}\[\]()<>!#|]/g, '\\$&')}`).join('\n\n') +
111
+ '\n');
73
112
  }
74
113
  /** Acquisition owns media lifetime; transcription never deletes or mutates caller source files. */
75
114
  export async function transcribeMedia(input, options) {
76
- const failed = (code) => ({ ...failure(input.requestId, code, 'inference'), cleanup: 'settled' });
115
+ const failed = (code) => ({
116
+ ...failure(input.requestId, code, 'inference'),
117
+ cleanup: 'settled',
118
+ });
77
119
  if (options.signal?.aborted)
78
120
  return failed('cancelled');
79
121
  if (input.captions && !input.forceLocal) {
@@ -82,8 +124,8 @@ export async function transcribeMedia(input, options) {
82
124
  return { ok: true, transcript, markdown: transcriptMarkdown(transcript), cleanup: 'settled' };
83
125
  }
84
126
  catch (e) {
85
- if (!(e instanceof Error) || e.message !== 'unsupported' || !input.audio)
86
- return failed(e instanceof Error && e.message === 'limit-exceeded' ? 'limit-exceeded' : 'integrity');
127
+ if (codeOf(e) !== 'unsupported' || !input.audio)
128
+ return failed(codeOf(e) === 'limit-exceeded' ? 'limit-exceeded' : 'integrity');
87
129
  }
88
130
  }
89
131
  if (!input.audio)
@@ -1,26 +1,58 @@
1
1
  import { limits } from '../../contracts/index.js';
2
2
  import { validateMetadataClaim, validateMetadataWarnings } from '../../contracts/validate.js';
3
- function object(v) { if (!v || typeof v !== 'object' || Array.isArray(v))
4
- throw Error('integrity'); return v; }
3
+ import { SpongeError } from '../errors.js';
4
+ function object(v) {
5
+ if (!v || typeof v !== 'object' || Array.isArray(v))
6
+ throw new SpongeError('integrity');
7
+ return v;
8
+ }
5
9
  /** Consume bounded observed Wordcell evidence without fetching linked JSON-LD contexts. */
6
10
  export function articleMetadata(input, observedAt) {
7
11
  const envelope = object(input);
8
12
  articleMetadataWarnings(input);
9
13
  if (envelope.schemaVersion !== 1 || envelope.extractorVersion !== 'wordcell-metadata-v1')
10
- throw Error('schema-newer');
11
- if (!Array.isArray(envelope.claims) || envelope.claims.length > limits.metadataClaims || Buffer.byteLength(JSON.stringify(input)) > limits.metadataBytes)
12
- throw Error('limit-exceeded');
13
- const claims = envelope.claims.map(raw => {
14
+ throw new SpongeError('schema-newer');
15
+ if (!Array.isArray(envelope.claims) ||
16
+ envelope.claims.length > limits.metadataClaims ||
17
+ Buffer.byteLength(JSON.stringify(input)) > limits.metadataBytes)
18
+ throw new SpongeError('limit-exceeded');
19
+ const claims = envelope.claims.map((raw) => {
14
20
  const claim = object(raw), locator = claim.locator;
15
21
  if (typeof locator !== 'string' || typeof claim.selected !== 'boolean')
16
- throw Error('integrity');
17
- const sources = { 'html-title': 'html', 'html-meta': 'html', 'html-link': 'html', 'html-language': 'html', 'json-ld': 'jsonld', extractor: 'html' };
22
+ throw new SpongeError('integrity');
23
+ const sources = {
24
+ 'html-title': 'html',
25
+ 'html-meta': 'html',
26
+ 'html-link': 'html',
27
+ 'html-language': 'html',
28
+ 'json-ld': 'jsonld',
29
+ extractor: 'html',
30
+ };
18
31
  if (typeof claim.source !== 'string' || !Object.hasOwn(sources, claim.source))
19
- throw Error('integrity');
20
- const source = /^meta\[(?:property|name)=og:/.test(locator) ? 'opengraph' : /^meta\[(?:property|name)=twitter:/.test(locator) ? 'twitter' : sources[claim.source];
21
- if (claim.timezone !== undefined && (typeof claim.timezone !== 'string' || !['explicit', 'unknown', 'not-applicable'].includes(claim.timezone)))
22
- throw Error('integrity');
23
- const result = { field: claim.field, value: claim.value, observed: claim.observed, source, locator, selected: claim.selected, observedAt, rule: claim.rule, extractionVersion: envelope.extractorVersion, ...(claim.datePrecision !== undefined ? { datePrecision: claim.datePrecision } : {}), ...(claim.timezone === 'explicit' || claim.timezone === 'unknown' ? { timezoneKnown: claim.timezone === 'explicit' } : {}) };
32
+ throw new SpongeError('integrity');
33
+ const source = /^meta\[(?:property|name)=og:/.test(locator)
34
+ ? 'opengraph'
35
+ : /^meta\[(?:property|name)=twitter:/.test(locator)
36
+ ? 'twitter'
37
+ : sources[claim.source];
38
+ if (claim.timezone !== undefined &&
39
+ (typeof claim.timezone !== 'string' || !['explicit', 'unknown', 'not-applicable'].includes(claim.timezone)))
40
+ throw new SpongeError('integrity');
41
+ const result = {
42
+ field: claim.field,
43
+ value: claim.value,
44
+ observed: claim.observed,
45
+ source,
46
+ locator,
47
+ selected: claim.selected,
48
+ observedAt,
49
+ rule: claim.rule,
50
+ extractionVersion: envelope.extractorVersion,
51
+ ...(claim.datePrecision !== undefined ? { datePrecision: claim.datePrecision } : {}),
52
+ ...(claim.timezone === 'explicit' || claim.timezone === 'unknown'
53
+ ? { timezoneKnown: claim.timezone === 'explicit' }
54
+ : {}),
55
+ };
24
56
  validateMetadataClaim(result);
25
57
  return result;
26
58
  });
@@ -30,14 +62,14 @@ export function articleMetadata(input, observedAt) {
30
62
  if (!claim.selected)
31
63
  continue;
32
64
  if (selected.has(claim.field) && !['authors', 'tags', 'alternate'].includes(claim.field))
33
- throw Error('integrity');
65
+ throw new SpongeError('integrity');
34
66
  selected.add(claim.field);
35
67
  }
36
68
  return claims;
37
69
  }
38
70
  export function selectedMetadata(claims, field) {
39
- const matches = claims.filter(claim => claim.field === field);
40
- return matches.find(claim => claim.selected === true) ?? matches.find(claim => claim.selected === undefined);
71
+ const matches = claims.filter((claim) => claim.field === field);
72
+ return matches.find((claim) => claim.selected === true) ?? matches.find((claim) => claim.selected === undefined);
41
73
  }
42
74
  export function articleMetadataWarnings(input) {
43
75
  const envelope = object(input);
@@ -45,7 +77,7 @@ export function articleMetadataWarnings(input) {
45
77
  validateMetadataWarnings(envelope.warnings);
46
78
  }
47
79
  catch {
48
- throw Error('integrity');
80
+ throw new SpongeError('integrity');
49
81
  }
50
82
  return [...envelope.warnings];
51
83
  }
@@ -10,10 +10,6 @@ export interface ModelPackage {
10
10
  license: string;
11
11
  artifacts: readonly ModelArtifact[];
12
12
  }
13
- export declare class ModelSetupError extends Error {
14
- readonly code: 'invalid-input' | 'integrity' | 'unavailable' | 'cancelled' | 'owner-busy' | 'limit-exceeded' | 'cleanup-required' | 'repair-auth';
15
- constructor(code: 'invalid-input' | 'integrity' | 'unavailable' | 'cancelled' | 'owner-busy' | 'limit-exceeded' | 'cleanup-required' | 'repair-auth');
16
- }
17
13
  export declare function modelSetupPlan(manifest: ModelPackage): {
18
14
  id: string;
19
15
  license: string;
@@ -23,14 +19,20 @@ export declare function modelSetupPlan(manifest: ModelPackage): {
23
19
  artifacts: readonly ModelArtifact[];
24
20
  };
25
21
  export declare function inspectModelPackage(cacheRoot: string, manifest: ModelPackage): Promise<Record<string, string>>;
26
- /** Explicit recovery only: an absent or different process birth identity proves the downloader is gone.
27
- * Downloading is in-process fetch; no native child can outlive this owner. A missing or
28
- * foreign stage receipt remains pending, as does an interrupted recovery guard. */
29
22
  export declare function recoverModelPackage(cacheRoot: string, manifest: ModelPackage): Promise<{
30
23
  recovered: boolean;
31
24
  }>;
32
- /** Explicit setup only. Never called by install hooks or by inference fallback. */
25
+ /** Explicit setup only. Never called by install hooks or by inference fallback, except `reuseOnly`: that adopts an
26
+ * installed package from verified local artifacts under the same setup lock and never reaches the network. */
33
27
  export declare function provisionModelPackage(cacheRoot: string, manifest: ModelPackage, options?: {
34
28
  signal?: AbortSignal;
35
29
  fetch?: typeof fetch;
30
+ reuseOnly?: boolean;
36
31
  }): Promise<Record<string, string>>;
32
+ /** The installed package for this manifest, by content, or null when none is. A package installed under an earlier
33
+ * URL-bound fingerprint is adopted once: its verified artifacts are hard-linked into the content-named directory under
34
+ * the setup lock (no network; the old directory stays intact for any process holding its paths until runtime GC
35
+ * supersedes it). Anything short of a fully verified adoption is reported as not installed, as before. */
36
+ export declare function inspectInstalledModelPackage(cacheRoot: string, manifest: ModelPackage, options?: {
37
+ pause?: (ms: number) => Promise<void>;
38
+ }): Promise<Record<string, string> | null>;