@hraness/sponge 0.2.6 → 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 +21 -9
  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 +38 -16
  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 +9 -46
  64. package/dist/src/digest/editorial.js +57 -49
  65. package/dist/src/digest/endpoint.js +63 -27
  66. package/dist/src/digest/pipeline.d.ts +49 -28
  67. package/dist/src/digest/pipeline.js +343 -100
  68. package/dist/src/digest/schema.d.ts +11 -25
  69. package/dist/src/digest/schema.js +86 -18
  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 +3 -6
  82. package/dist/src/importers/jungle-digest.js +196 -69
  83. package/dist/src/importers/jungle-store.d.ts +15 -6
  84. package/dist/src/importers/jungle-store.js +122 -28
  85. package/dist/src/importers/jungle.d.ts +14 -14
  86. package/dist/src/importers/jungle.js +136 -31
  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 +106 -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 -79
  226. package/dist/src/store/foundation.d.ts +0 -7
  227. package/dist/src/store/foundation.js +0 -7
@@ -1,54 +1,68 @@
1
1
  import { join } from 'node:path';
2
- import { StoreError } from '../store/index.js';
3
2
  import { acquireWorkerCustody, proveAttemptCleanup } from '../worker/custody.js';
4
3
  import { supervisedRunner } from '../worker/supervisor.js';
5
4
  import { directory, writeJournal } from '../worker/journal.js';
6
- import { embedBatch, embeddingFingerprint, encodeEmbeddingRequest, resolveIndexSpace, projectVector } from '../inference/embedding/index.js';
5
+ import { embedBatch, embeddingFingerprint, encodeEmbeddingRequest, resolveIndexSpace, projectVector, } from '../inference/embedding/index.js';
7
6
  import { deriveEmbeddingImage } from './image.js';
8
7
  import { failure } from '../acquisition/errors.js';
9
- /** Same durable worker lock/owner/supervisor as acquisition, with sequential bounded batches. */
8
+ import { SpongeError, codeOf, publicCode } from '../errors.js';
9
+ /** Same durable worker lock/owner/supervisor as acquisition, with sequential bounded batches. Only inputs whose content the space
10
+ * has not embedded run: a delta after captures, or everything new for a new fingerprint. Captures during a run join it. */
10
11
  export async function drainIndexQueue(store, options) {
11
12
  const custody = await acquireWorkerCustody(store.root);
12
13
  let owner, settled = true;
13
14
  const results = [];
14
15
  try {
15
- owner = store.acquireOwner(custody.incarnation);
16
+ owner = store.queue.acquireOwner(custody.incarnation);
16
17
  while (!options.signal?.aborted) {
17
- const attempt = store.claimIndexJob(owner, options.jobId);
18
+ const attempt = store.index.claim(owner, options.jobId);
18
19
  if (!attempt)
19
20
  break;
20
21
  settled = false;
21
22
  const attemptDirectory = await directory(join(custody.directory, 'attempts', attempt.attemptId)), scratchRoot = await directory(join(store.root, 'staging', attempt.attemptId));
22
- await writeJournal(join(scratchRoot, 'ownership.json'), { schemaVersion: 1, requestId: attempt.attemptId, ownerIncarnation: attempt.ownerIncarnation });
23
- const checkpoint = async (stage) => { store.checkpointIndexAttempt(attempt, stage); await writeJournal(join(attemptDirectory, 'checkpoint.json'), { schemaVersion: 1, ...attempt, stage }); await options.onCheckpoint?.(attempt, stage); };
24
- const controller = new AbortController(), cancel = () => { store.requestCancellation(attempt.jobId); controller.abort(); };
23
+ await writeJournal(join(scratchRoot, 'ownership.json'), {
24
+ schemaVersion: 1,
25
+ requestId: attempt.attemptId,
26
+ ownerIncarnation: attempt.ownerIncarnation,
27
+ });
28
+ const checkpoint = async (stage) => {
29
+ store.index.checkpoint(attempt, stage);
30
+ await writeJournal(join(attemptDirectory, 'checkpoint.json'), { schemaVersion: 1, ...attempt, stage });
31
+ await options.onCheckpoint?.(attempt, stage);
32
+ };
33
+ const controller = new AbortController(), cancel = () => {
34
+ store.queue.cancel(attempt.jobId);
35
+ controller.abort();
36
+ };
25
37
  options.signal?.addEventListener('abort', cancel, { once: true });
26
- const poll = setInterval(() => { try {
27
- if (store.jobCancellationRequested(attempt.jobId))
38
+ const poll = setInterval(() => {
39
+ try {
40
+ if (store.queue.cancelRequested(attempt.jobId))
41
+ controller.abort();
42
+ }
43
+ catch {
28
44
  controller.abort();
29
- }
30
- catch {
31
- controller.abort();
32
- } }, 100);
45
+ }
46
+ }, 100);
33
47
  const runProcess = supervisedRunner(custody.directory, custody.incarnation, attempt.attemptId);
34
48
  let result;
35
49
  try {
36
50
  if (options.signal?.aborted)
37
51
  cancel();
38
52
  await checkpoint('acquiring');
39
- const job = store.indexJob(attempt.jobId);
53
+ const job = store.index.job(attempt.jobId);
40
54
  if (!job)
41
- throw new StoreError('unavailable');
42
- const generationId = String(job.generation_id), status = store.indexStatus(generationId), space = resolveIndexSpace(options.runtime.helperSha256, status.fingerprint);
55
+ throw new SpongeError('unavailable');
56
+ const generationId = String(job.generation_id);
57
+ store.index.refresh(attempt);
58
+ const status = store.index.status(generationId), space = resolveIndexSpace(options.runtime.helperSha256, status.fingerprint);
43
59
  if (!space)
44
- throw new StoreError('integrity');
60
+ throw new SpongeError('integrity');
45
61
  await checkpoint('indexing');
46
62
  for (;;) {
47
- if (controller.signal.aborted || store.jobCancellationRequested(attempt.jobId))
48
- throw new StoreError('cancelled');
49
- if (store.revisionEpoch !== status.revisionEpoch)
50
- throw Object.assign(Error('stale-index'), { code: 'stale-index' });
51
- const inputs = store.indexInputs(generationId, 16);
63
+ if (controller.signal.aborted || store.queue.cancelRequested(attempt.jobId))
64
+ throw new SpongeError('cancelled');
65
+ const inputs = store.index.inputs(generationId, 16);
52
66
  if (!inputs.length)
53
67
  break;
54
68
  const batch = [];
@@ -57,35 +71,64 @@ export async function drainIndexQueue(store, options) {
57
71
  batch.push({ kind: 'document', text: input.text });
58
72
  continue;
59
73
  }
60
- const item = store.read(input.itemId), asset = item?.assets.find(a => a.sha256 === input.assetSha256);
74
+ const item = store.read(input.itemId), asset = item?.assets.find((a) => a.sha256 === input.assetSha256);
61
75
  if (!item || item.revisionId !== input.revisionId || !asset)
62
- throw new StoreError('unavailable');
63
- const derived = await (options.deriveImage ?? deriveEmbeddingImage)(store.readBlob(asset), { scratchRoot, runProcess, signal: controller.signal });
76
+ throw new SpongeError('unavailable');
77
+ const derived = await (options.deriveImage ?? deriveEmbeddingImage)(store.blobs.read(asset), {
78
+ scratchRoot,
79
+ runProcess,
80
+ signal: controller.signal,
81
+ });
64
82
  batch.push({ kind: 'image', png: derived.png });
65
83
  }
66
- const expected = encodeEmbeddingRequest(batch).inputHashes, embedded = await (options.embed ?? embedBatch)(batch, { runtime: options.runtime, scratchRoot, runProcess, signal: controller.signal });
67
- if (embedded.fingerprint !== embeddingFingerprint(options.runtime.helperSha256) || embedded.inputHashes.length !== expected.length || embedded.inputHashes.some((hash, i) => hash !== expected[i]))
68
- throw new StoreError('integrity');
69
- store.writeIndexAttemptBatch(attempt, status.fingerprint, inputs.map((input, i) => ({ ...input, inputHash: expected[i], vector: projectVector(embedded.vectors[i], space.dimensions) })));
70
- await options.onBatch?.(attempt, store.indexStatus(generationId).completed);
84
+ const expected = encodeEmbeddingRequest(batch).inputHashes, embedded = await (options.embed ?? embedBatch)(batch, {
85
+ runtime: options.runtime,
86
+ scratchRoot,
87
+ runProcess,
88
+ signal: controller.signal,
89
+ });
90
+ if (embedded.fingerprint !== embeddingFingerprint(options.runtime.helperSha256) ||
91
+ embedded.inputHashes.length !== expected.length ||
92
+ embedded.inputHashes.some((hash, i) => hash !== expected[i]))
93
+ throw new SpongeError('integrity');
94
+ store.index.writeAttemptBatch(attempt, status.fingerprint, inputs.map((input, i) => ({
95
+ ...input,
96
+ inputHash: expected[i],
97
+ vector: projectVector(embedded.vectors[i], space.dimensions),
98
+ })));
99
+ await options.onBatch?.(attempt, store.index.status(generationId).completed);
71
100
  }
72
101
  await checkpoint('promoting');
73
- store.completeIndexAttempt(attempt);
102
+ store.index.complete(attempt);
74
103
  result = { ok: true, jobId: attempt.jobId, generationId, cleanup: 'settled' };
75
104
  }
76
105
  catch (error) {
77
- const raw = error.code, reason = raw && /^[a-z][a-z-]{0,63}$/.test(raw) ? raw : 'unclassified';
78
- store.failIndexAttempt(attempt, reason);
79
- const known = ['integrity', 'unavailable', 'unsupported', 'cancelled', 'limit-exceeded', 'cleanup-required', 'runtime-missing', 'owner-busy'];
80
- result = { ...failure(attempt.attemptId, known.includes(reason) ? reason : 'unavailable', 'inference'), cleanup: 'pending' };
106
+ const reason = codeOf(error), code = publicCode(reason);
107
+ store.index.fail(attempt, reason);
108
+ const known = [
109
+ 'integrity',
110
+ 'unavailable',
111
+ 'unsupported',
112
+ 'cancelled',
113
+ 'limit-exceeded',
114
+ 'cleanup-required',
115
+ 'runtime-missing',
116
+ 'owner-busy',
117
+ ];
118
+ result = {
119
+ ...failure(attempt.attemptId, known.includes(code) ? code : 'unavailable', 'inference'),
120
+ cleanup: 'pending',
121
+ };
81
122
  }
82
123
  finally {
83
124
  clearInterval(poll);
84
125
  options.signal?.removeEventListener('abort', cancel);
85
126
  }
86
- settled = !(result.ok === false && result.code === 'cleanup-required') && await proveAttemptCleanup(custody.directory, custody.incarnation, attempt.attemptId);
127
+ settled =
128
+ !(result.ok === false && result.code === 'cleanup-required') &&
129
+ (await proveAttemptCleanup(custody.directory, custody.incarnation, attempt.attemptId));
87
130
  if (settled) {
88
- store.settleIndexAttempt(attempt);
131
+ store.index.settle(attempt);
89
132
  result.cleanup = 'settled';
90
133
  }
91
134
  else
@@ -96,13 +139,13 @@ export async function drainIndexQueue(store, options) {
96
139
  break;
97
140
  }
98
141
  if (!settled)
99
- throw Object.assign(Error('cleanup-required'), { code: 'cleanup-required' });
142
+ throw new SpongeError('cleanup-required');
100
143
  return results;
101
144
  }
102
145
  finally {
103
146
  if (owner && settled) {
104
147
  try {
105
- store.releaseOwner(owner);
148
+ store.queue.releaseOwner(owner);
106
149
  }
107
150
  catch {
108
151
  settled = false;
@@ -0,0 +1,9 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ /**
3
+ * Migration 10's code step. Each generation's JSON coverage becomes `index_items` and `index_inputs` rows keyed by content hash,
4
+ * and its embedded vectors move to `embeddings` under (fingerprint, content hash), so a vector shared by two generations or two
5
+ * items is stored once. All or nothing per generation: one unverifiable input leaves that generation's JSON coverage in place,
6
+ * where it never reads (as before). Then the copy of `vectors` is dropped and active generations catch up with captures made since they
7
+ * were built: their delta is queued, not a rebuild.
8
+ */
9
+ export declare function incrementalIndex(db: DatabaseSync): void;
@@ -0,0 +1,79 @@
1
+ import { chunkDocument, chunkHash, coverageMarker, documentHash, imageHash, trackLibrary } from './index.js';
2
+ const parse = (text) => {
3
+ try {
4
+ return JSON.parse(String(text));
5
+ }
6
+ catch {
7
+ return undefined;
8
+ }
9
+ };
10
+ /** Content key of one schema-9 coverage input, or undefined when its chunk hash no longer verifies (such a generation already could not build). */
11
+ function contentOf(db, input, chunks) {
12
+ if (input.kind === 'image')
13
+ return typeof input.assetSha256 === 'string' &&
14
+ input.derivationHash === imageHash(input.assetSha256) &&
15
+ input.sourceId === JSON.stringify([input.revisionId, 'image', input.assetSha256])
16
+ ? input.derivationHash
17
+ : undefined;
18
+ if (input.kind !== 'document' ||
19
+ typeof input.representationId !== 'string' ||
20
+ !Number.isSafeInteger(input.ordinal) ||
21
+ input.sourceId !== JSON.stringify([input.revisionId, 'document', input.representationId, input.ordinal]))
22
+ return undefined;
23
+ if (!chunks.has(input.representationId)) {
24
+ const markdown = db
25
+ .prepare('SELECT markdown FROM representations WHERE id=? AND revision_id=?')
26
+ .get(input.representationId, input.revisionId)?.markdown;
27
+ chunks.set(input.representationId, typeof markdown === 'string' ? chunkDocument(markdown) : undefined);
28
+ }
29
+ const text = chunks.get(input.representationId)?.[input.ordinal];
30
+ return text !== undefined && input.derivationHash === chunkHash(input.representationId, input.ordinal, text)
31
+ ? documentHash(text)
32
+ : undefined;
33
+ }
34
+ /**
35
+ * Migration 10's code step. Each generation's JSON coverage becomes `index_items` and `index_inputs` rows keyed by content hash,
36
+ * and its embedded vectors move to `embeddings` under (fingerprint, content hash), so a vector shared by two generations or two
37
+ * items is stored once. All or nothing per generation: one unverifiable input leaves that generation's JSON coverage in place,
38
+ * where it never reads (as before). Then the copy of `vectors` is dropped and active generations catch up with captures made since they
39
+ * were built: their delta is queued, not a rebuild.
40
+ */
41
+ export function incrementalIndex(db) {
42
+ const chunks = new Map();
43
+ for (const g of db.prepare('SELECT id,fingerprint,coverage_json FROM index_generations ORDER BY rowid').all()) {
44
+ const coverage = parse(g.coverage_json);
45
+ if (!coverage || coverage.version !== 1 || !Array.isArray(coverage.revisions) || !Array.isArray(coverage.inputs))
46
+ continue;
47
+ const revisions = coverage.revisions, inputs = coverage.inputs, content = new Map(), images = new Set(inputs.filter((i) => i?.kind === 'image').map((i) => i.sourceId));
48
+ if (!revisions.every((r) => typeof r?.itemId === 'string' &&
49
+ typeof r.revisionId === 'string' &&
50
+ db.prepare('SELECT 1 FROM revisions WHERE id=? AND item_id=?').get(r.revisionId, r.itemId)))
51
+ continue;
52
+ for (const input of inputs) {
53
+ const key = input && typeof input.sourceId === 'string' && typeof input.revisionId === 'string'
54
+ ? contentOf(db, input, chunks)
55
+ : undefined;
56
+ if (!key || content.has(input.sourceId))
57
+ break;
58
+ content.set(input.sourceId, key);
59
+ }
60
+ if (content.size !== inputs.length)
61
+ continue;
62
+ for (const r of revisions)
63
+ db.prepare('INSERT OR IGNORE INTO index_items VALUES(?,?,?)').run(g.id, r.itemId, r.revisionId);
64
+ for (const input of inputs)
65
+ db.prepare('INSERT INTO index_inputs VALUES(?,?,?,?)').run(g.id, input.sourceId, input.revisionId, content.get(input.sourceId));
66
+ // A document vector is the embedding of its content exactly when its input hash is that content hash; first writer wins.
67
+ for (const v of db
68
+ .prepare('SELECT source_id,input_hash,vector FROM temp.vectors_v9 WHERE generation_id=? AND input_hash IS NOT NULL ORDER BY rowid')
69
+ .all(g.id)) {
70
+ const source = String(v.source_id), key = content.get(source);
71
+ if (!key || (!images.has(source) && v.input_hash !== key))
72
+ continue;
73
+ db.prepare('INSERT OR IGNORE INTO embeddings VALUES(?,?,?,?)').run(g.fingerprint, key, v.input_hash, v.vector);
74
+ }
75
+ db.prepare('UPDATE index_generations SET coverage_json=? WHERE id=?').run(coverageMarker, g.id);
76
+ }
77
+ db.exec('DROP TABLE temp.vectors_v9');
78
+ trackLibrary(db);
79
+ }
@@ -8,8 +8,12 @@ try {
8
8
  child.stderr.pipe(process.stderr);
9
9
  child.stdin.on('error', () => { });
10
10
  child.stdin.end(JSON.stringify(config.request) + '\n');
11
- child.once('error', () => { process.exitCode = 1; });
12
- child.once('close', code => { process.exitCode = code ?? 1; });
11
+ child.once('error', () => {
12
+ process.exitCode = 1;
13
+ });
14
+ child.once('close', (code) => {
15
+ process.exitCode = code ?? 1;
16
+ });
13
17
  }
14
18
  catch {
15
19
  process.exitCode = 1;
@@ -1,14 +1,36 @@
1
- import { createHash } from 'node:crypto';
2
- export const asrIdentity = { engine: 'Cactus-Compute/needle3@93e88ede867e4311c39151285e74df513e4b835b', model: 'Cactus-Compute/whistle@ca5287601bef25af26dcf2e1b2bdc0843a7c19e5', modelSha256: 'b6e02f048568ac5d01a2042556c658061e699acbc0aa2a1439f52f3d461dffeb', modelBytes: 16919407 };
3
- export const asrLimits = { outputBytes: 4 * 1024 ** 2, frameBytes: 4 * 1024 ** 2, words: 30000, maximumDurationMs: 3600000, windowDurationMs: 60000, overlapMs: 4000, deadlineMs: 180000 };
4
- const fail = () => { throw Error('integrity'); };
1
+ import { sha256 } from '../../util/hash.js';
2
+ import { SpongeError } from '../../errors.js';
3
+ export const asrIdentity = {
4
+ engine: 'Cactus-Compute/needle3@93e88ede867e4311c39151285e74df513e4b835b',
5
+ model: 'Cactus-Compute/whistle@ca5287601bef25af26dcf2e1b2bdc0843a7c19e5',
6
+ modelSha256: 'b6e02f048568ac5d01a2042556c658061e699acbc0aa2a1439f52f3d461dffeb',
7
+ modelBytes: 16919407,
8
+ };
9
+ export const asrLimits = {
10
+ outputBytes: 4 * 1024 ** 2,
11
+ frameBytes: 4 * 1024 ** 2,
12
+ words: 30000,
13
+ maximumDurationMs: 3600000,
14
+ windowDurationMs: 60000,
15
+ overlapMs: 4000,
16
+ deadlineMs: 180000,
17
+ };
18
+ const fail = () => {
19
+ throw new SpongeError('integrity');
20
+ };
5
21
  const obj = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : fail();
6
22
  const finite = (v, min = 0, max = Number.MAX_SAFE_INTEGER) => typeof v === 'number' && Number.isFinite(v) && v >= min && v <= max ? v : fail();
7
- function time(v) { const t = obj(v); if (!Number.isSafeInteger(t.value) || !Number.isSafeInteger(t.timescale) || t.timescale <= 0)
8
- fail(); return finite(t.value / t.timescale, 0, asrLimits.maximumDurationMs / 1000); }
23
+ function time(v) {
24
+ const t = obj(v);
25
+ if (!Number.isSafeInteger(t.value) || !Number.isSafeInteger(t.timescale) || t.timescale <= 0)
26
+ fail();
27
+ return finite(t.value / t.timescale, 0, asrLimits.maximumDurationMs / 1000);
28
+ }
9
29
  /** Decode only correlated native events; diagnostics never become error classifications. */
10
30
  export function parseAsrOutput(stdout, requestId, sourceSha256, helperSha256) {
11
- if (Buffer.byteLength(stdout) > asrLimits.outputBytes || !/^[a-f0-9]{64}$/.test(sourceSha256) || !/^[a-f0-9]{64}$/.test(helperSha256))
31
+ if (Buffer.byteLength(stdout) > asrLimits.outputBytes ||
32
+ !/^[a-f0-9]{64}$/.test(sourceSha256) ||
33
+ !/^[a-f0-9]{64}$/.test(helperSha256))
12
34
  fail();
13
35
  let ready = false, result;
14
36
  for (const line of stdout.split('\n')) {
@@ -18,43 +40,111 @@ export function parseAsrOutput(stdout, requestId, sourceSha256, helperSha256) {
18
40
  fail();
19
41
  const event = obj(JSON.parse(line));
20
42
  if (event.type === 'ready') {
21
- if (ready || event.protocolVersion !== 2 || event.engine !== asrIdentity.engine || event.model !== asrIdentity.model)
43
+ if (ready ||
44
+ event.protocolVersion !== 2 ||
45
+ event.engine !== asrIdentity.engine ||
46
+ event.model !== asrIdentity.model)
22
47
  fail();
23
48
  ready = true;
24
49
  }
25
50
  else if (event.type === 'progress') {
26
- if (!ready || result || event.id !== requestId || !['verifying', 'extracting', 'recognizing'].includes(event.stage))
51
+ if (!ready ||
52
+ result ||
53
+ event.id !== requestId ||
54
+ !['verifying', 'extracting', 'recognizing'].includes(event.stage))
27
55
  fail();
28
56
  }
29
57
  else if (event.type === 'result') {
30
- if (!ready || result || event.id !== requestId || event.sourceSha256 !== sourceSha256 || event.engine !== asrIdentity.engine || event.model !== asrIdentity.model)
58
+ if (!ready ||
59
+ result ||
60
+ event.id !== requestId ||
61
+ event.sourceSha256 !== sourceSha256 ||
62
+ event.engine !== asrIdentity.engine ||
63
+ event.model !== asrIdentity.model)
31
64
  fail();
32
65
  result = event;
33
66
  }
34
67
  else if (event.type === 'error' || event.type === 'fatal')
35
- throw Error('unclassified');
68
+ throw new SpongeError('unclassified');
36
69
  else
37
70
  fail();
38
71
  }
39
72
  if (!ready || !result || result.hasAudio !== true)
40
73
  return fail();
41
74
  const recognition = obj(result.recognition), map = obj(result.audioMap);
42
- if (recognition.language !== 'en' || typeof recognition.text !== 'string' || !Array.isArray(recognition.words) || recognition.words.length > asrLimits.words || map.sampleRate !== 16000 || !Number.isSafeInteger(map.sampleCount) || map.sampleCount < 0 || map.sampleCount > asrLimits.windowDurationMs * 16 || !Array.isArray(map.spans) || map.spans.length > 30000)
75
+ if (recognition.language !== 'en' ||
76
+ typeof recognition.text !== 'string' ||
77
+ !Array.isArray(recognition.words) ||
78
+ recognition.words.length > asrLimits.words ||
79
+ map.sampleRate !== 16000 ||
80
+ !Number.isSafeInteger(map.sampleCount) ||
81
+ map.sampleCount < 0 ||
82
+ map.sampleCount > asrLimits.windowDurationMs * 16 ||
83
+ !Array.isArray(map.spans) ||
84
+ map.spans.length > 30000)
43
85
  fail();
44
86
  const durationMs = time(map.sourceDuration) * 1000;
45
87
  if (durationMs <= 0)
46
88
  fail();
47
89
  let sampleEnd = 0, sourceEnd = 0;
48
- const spans = map.spans.map((raw) => { const s = obj(raw), range = obj(s.source), start = time(range.start), end = time(range.end); if (!Number.isSafeInteger(s.sampleStart) || !Number.isSafeInteger(s.sampleEnd) || s.sampleStart !== sampleEnd || s.sampleEnd <= s.sampleStart || s.sampleEnd > map.sampleCount || start < sourceEnd || end <= start || end * 1000 > durationMs + .1 || !['audio', 'silence'].includes(s.kind))
49
- fail(); sampleEnd = s.sampleEnd; sourceEnd = end; return { sampleStart: s.sampleStart, sampleEnd: s.sampleEnd, start, end }; });
90
+ const spans = map.spans.map((raw) => {
91
+ const s = obj(raw), range = obj(s.source), start = time(range.start), end = time(range.end);
92
+ if (!Number.isSafeInteger(s.sampleStart) ||
93
+ !Number.isSafeInteger(s.sampleEnd) ||
94
+ s.sampleStart !== sampleEnd ||
95
+ s.sampleEnd <= s.sampleStart ||
96
+ s.sampleEnd > map.sampleCount ||
97
+ start < sourceEnd ||
98
+ end <= start ||
99
+ end * 1000 > durationMs + 0.1 ||
100
+ !['audio', 'silence'].includes(s.kind))
101
+ fail();
102
+ sampleEnd = s.sampleEnd;
103
+ sourceEnd = end;
104
+ return { sampleStart: s.sampleStart, sampleEnd: s.sampleEnd, start, end };
105
+ });
50
106
  if (sampleEnd !== map.sampleCount)
51
107
  fail();
52
- const sourceTime = (seconds, end) => { const sample = seconds * 16000; const span = spans.find(s => sample >= s.sampleStart && (end ? sample <= s.sampleEnd : sample < s.sampleEnd)) ?? (sample === map.sampleCount ? spans.at(-1) : undefined); if (!span)
53
- return fail(); return (span.start + (sample - span.sampleStart) / (span.sampleEnd - span.sampleStart) * (span.end - span.start)) * 1000; };
108
+ const sourceTime = (seconds, end) => {
109
+ const sample = seconds * 16000;
110
+ const span = spans.find((s) => sample >= s.sampleStart && (end ? sample <= s.sampleEnd : sample < s.sampleEnd)) ??
111
+ (sample === map.sampleCount ? spans.at(-1) : undefined);
112
+ if (!span)
113
+ return fail();
114
+ return ((span.start + ((sample - span.sampleStart) / (span.sampleEnd - span.sampleStart)) * (span.end - span.start)) *
115
+ 1000);
116
+ };
54
117
  let prior = 0;
55
- const segments = recognition.words.map((raw) => { const w = obj(raw); if (typeof w.word !== 'string' || !w.word.trim() || Buffer.byteLength(w.word) > 16384)
56
- fail(); const start = finite(w.start, prior, map.sampleCount / 16000), end = finite(w.end, start, map.sampleCount / 16000); prior = start; return { startMs: sourceTime(start, false), endMs: sourceTime(end, true), text: w.word.trim() }; });
118
+ const segments = recognition.words.map((raw) => {
119
+ const w = obj(raw);
120
+ if (typeof w.word !== 'string' || !w.word.trim() || Buffer.byteLength(w.word) > 16384)
121
+ fail();
122
+ const start = finite(w.start, prior, map.sampleCount / 16000), end = finite(w.end, start, map.sampleCount / 16000);
123
+ prior = start;
124
+ return { startMs: sourceTime(start, false), endMs: sourceTime(end, true), text: w.word.trim() };
125
+ });
57
126
  if (!segments.length && recognition.text.trim())
58
127
  fail();
59
- return { schemaVersion: 1, requestId, source: 'local-asr', language: 'en', modelFingerprint: createHash('sha256').update(JSON.stringify(['sponge-asr-v1', asrIdentity, helperSha256, 'decoded-audio-map-window-v2', { windowDurationMs: asrLimits.windowDurationMs, overlapMs: asrLimits.overlapMs, join: 'source-midpoint-overlap-dedupe-v1' }])).digest('hex'), segments, cleanup: 'settled', sourceSha256, timing: 'decoded-audio-map', durationMs };
128
+ return {
129
+ schemaVersion: 1,
130
+ requestId,
131
+ source: 'local-asr',
132
+ language: 'en',
133
+ modelFingerprint: sha256(JSON.stringify([
134
+ 'sponge-asr-v1',
135
+ asrIdentity,
136
+ helperSha256,
137
+ 'decoded-audio-map-window-v2',
138
+ {
139
+ windowDurationMs: asrLimits.windowDurationMs,
140
+ overlapMs: asrLimits.overlapMs,
141
+ join: 'source-midpoint-overlap-dedupe-v1',
142
+ },
143
+ ])),
144
+ segments,
145
+ cleanup: 'settled',
146
+ sourceSha256,
147
+ timing: 'decoded-audio-map',
148
+ durationMs,
149
+ };
60
150
  }
@@ -8,14 +8,15 @@ import { failure } from '../../acquisition/errors.js';
8
8
  import { privateRoot } from '../../acquisition/files.js';
9
9
  import { asrIdentity, asrLimits, parseAsrOutput } from './protocol.js';
10
10
  import { audioWindows, joinWindows } from './windows.js';
11
+ import { SpongeError, codeOf } from '../../errors.js';
11
12
  async function verify(path, sha256, maximum, expected) {
12
- if (!isAbsolute(path) || !/^[a-f0-9]{64}$/.test(sha256) || await realpath(path) !== path)
13
- throw Error('integrity');
13
+ if (!isAbsolute(path) || !/^[a-f0-9]{64}$/.test(sha256) || (await realpath(path)) !== path)
14
+ throw new SpongeError('integrity');
14
15
  const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
15
16
  try {
16
17
  const stat = await file.stat();
17
- if (!stat.isFile() || stat.size > maximum || expected !== undefined && stat.size !== expected)
18
- throw Error('integrity');
18
+ if (!stat.isFile() || stat.size > maximum || (expected !== undefined && stat.size !== expected))
19
+ throw new SpongeError('integrity');
19
20
  const hash = createHash('sha256'), buffer = Buffer.alloc(65536);
20
21
  let bytes = 0;
21
22
  for (;;) {
@@ -24,27 +25,38 @@ async function verify(path, sha256, maximum, expected) {
24
25
  break;
25
26
  bytes += bytesRead;
26
27
  if (bytes > maximum)
27
- throw Error('limit-exceeded');
28
+ throw new SpongeError('limit-exceeded');
28
29
  hash.update(buffer.subarray(0, bytesRead));
29
30
  }
30
31
  if (bytes !== stat.size || hash.digest('hex') !== sha256)
31
- throw Error('integrity');
32
+ throw new SpongeError('integrity');
32
33
  }
33
34
  finally {
34
35
  await file.close();
35
36
  }
36
37
  }
37
38
  /** Portable callers can use captions; this pinned native artifact requires macOS26 arm64. */
38
- export function supportsNativeAsr(platform = process.platform, arch = process.arch, kernel = release()) { return platform === 'darwin' && arch === 'arm64' && /^\d+\./.test(kernel) && Number(kernel.split('.')[0]) >= 25; }
39
+ export function supportsNativeAsr(platform = process.platform, arch = process.arch, kernel = release()) {
40
+ return platform === 'darwin' && arch === 'arm64' && /^\d+\./.test(kernel) && Number(kernel.split('.')[0]) >= 25;
41
+ }
39
42
  export async function transcribeAudio(requestId, audio, language, options) {
40
- const error = (code, cleanup = 'settled') => ({ ...failure(requestId, code, cleanup === 'pending' ? 'cleanup' : 'inference'), cleanup });
43
+ const error = (code, cleanup = 'settled') => ({
44
+ ...failure(requestId, code, cleanup === 'pending' ? 'cleanup' : 'inference'),
45
+ cleanup,
46
+ });
41
47
  if (options.signal?.aborted)
42
48
  return error('cancelled');
43
49
  if (!supportsNativeAsr() || language !== 'en')
44
50
  return error('unsupported');
45
51
  if (!options.runtime)
46
52
  return error('runtime-missing');
47
- if (!/^[A-Za-z0-9_-]{1,128}$/.test(requestId) || !Number.isSafeInteger(audio.bytes) || audio.bytes < 1 || audio.bytes > 512 * 1024 ** 2 || !Number.isFinite(audio.durationMs) || audio.durationMs <= 0 || audio.durationMs > asrLimits.maximumDurationMs)
53
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(requestId) ||
54
+ !Number.isSafeInteger(audio.bytes) ||
55
+ audio.bytes < 1 ||
56
+ audio.bytes > 512 * 1024 ** 2 ||
57
+ !Number.isFinite(audio.durationMs) ||
58
+ audio.durationMs <= 0 ||
59
+ audio.durationMs > asrLimits.maximumDurationMs)
48
60
  return error('limit-exceeded');
49
61
  let scratch, cleanup = 'settled', outcome;
50
62
  try {
@@ -60,34 +72,59 @@ export async function transcribeAudio(requestId, audio, language, options) {
60
72
  const windows = audioWindows(audio.durationMs), transcripts = [];
61
73
  let totalWords = 0;
62
74
  for (const window of windows) {
63
- await writeFile(requestFile, JSON.stringify({ helper: runtime.helper, model: runtime.model, request: { id: requestId, sourcePath: audio.path, expectedSourceSha256: audio.sha256, windowStartMs: window.startMs, windowEndMs: window.endMs } }), { mode: 0o600, flag: transcripts.length ? 'w' : 'wx' });
75
+ await writeFile(requestFile, JSON.stringify({
76
+ helper: runtime.helper,
77
+ model: runtime.model,
78
+ request: {
79
+ id: requestId,
80
+ sourcePath: audio.path,
81
+ expectedSourceSha256: audio.sha256,
82
+ windowStartMs: window.startMs,
83
+ windowEndMs: window.endMs,
84
+ },
85
+ }), { mode: 0o600, flag: transcripts.length ? 'w' : 'wx' });
64
86
  cleanup = 'pending';
65
- const result = await options.runProcess(process.execPath, source ? ['--import', import.meta.resolve('tsx'), entry, requestFile] : [entry, requestFile], { env: { PATH: '/usr/bin:/bin', HOME: scratch, TMPDIR: scratch, LANG: 'en_US.UTF-8' }, deadlineMs: asrLimits.deadlineMs, maxOutputBytes: asrLimits.outputBytes, signal: options.signal });
87
+ const result = await options.runProcess(process.execPath, source ? ['--import', import.meta.resolve('tsx'), entry, requestFile] : [entry, requestFile], {
88
+ env: { PATH: '/usr/bin:/bin', HOME: scratch, TMPDIR: scratch, LANG: 'en_US.UTF-8' },
89
+ deadlineMs: asrLimits.deadlineMs,
90
+ maxOutputBytes: asrLimits.outputBytes,
91
+ signal: options.signal,
92
+ });
66
93
  cleanup = result.cleanup;
67
94
  if (cleanup === 'pending')
68
- throw Error('cleanup-required');
95
+ throw new SpongeError('cleanup-required');
69
96
  if (options.signal?.aborted || result.stopped === 'cancelled')
70
- throw Error('cancelled');
97
+ throw new SpongeError('cancelled');
71
98
  if (result.stopped)
72
- throw Error(result.stopped);
99
+ throw new SpongeError(result.stopped);
73
100
  if (result.code !== 0)
74
- throw Error('unclassified');
101
+ throw new SpongeError('unclassified');
75
102
  const transcript = parseAsrOutput(result.stdout, requestId, audio.sha256, runtime.helperSha256);
76
103
  if (Math.abs(transcript.durationMs - audio.durationMs) > 1000)
77
- throw Error('integrity');
78
- if (transcript.segments.some(s => s.startMs < window.startMs - .1 || s.endMs > window.endMs + .1))
79
- throw Error('integrity');
104
+ throw new SpongeError('integrity');
105
+ if (transcript.segments.some((s) => s.startMs < window.startMs - 0.1 || s.endMs > window.endMs + 0.1))
106
+ throw new SpongeError('integrity');
80
107
  totalWords += transcript.segments.length;
81
108
  if (totalWords > asrLimits.words)
82
- throw Error('limit-exceeded');
109
+ throw new SpongeError('limit-exceeded');
83
110
  transcripts.push(transcript);
84
111
  }
85
112
  await verify(audio.path, audio.sha256, 512 * 1024 ** 2, audio.bytes);
86
113
  outcome = { ok: true, transcript: joinWindows(windows, transcripts), cleanup };
87
114
  }
88
115
  catch (e) {
89
- const code = e instanceof Error ? e.message : '';
90
- outcome = error(cleanup === 'pending' ? 'cleanup-required' : code === 'integrity' ? 'integrity' : code === 'limit-exceeded' ? 'limit-exceeded' : code === 'cancelled' ? 'cancelled' : code === 'unavailable' ? 'unavailable' : 'unclassified', cleanup);
116
+ const code = codeOf(e);
117
+ outcome = error(cleanup === 'pending'
118
+ ? 'cleanup-required'
119
+ : code === 'integrity'
120
+ ? 'integrity'
121
+ : code === 'limit-exceeded'
122
+ ? 'limit-exceeded'
123
+ : code === 'cancelled'
124
+ ? 'cancelled'
125
+ : code === 'unavailable'
126
+ ? 'unavailable'
127
+ : 'unclassified', cleanup);
91
128
  }
92
129
  finally {
93
130
  if (scratch && cleanup === 'settled') {
@@ -1,7 +1,8 @@
1
1
  import { asrLimits } from './protocol.js';
2
+ import { SpongeError } from '../../errors.js';
2
3
  export function audioWindows(durationMs) {
3
4
  if (!Number.isFinite(durationMs) || durationMs <= 0 || durationMs > asrLimits.maximumDurationMs)
4
- throw Error('limit-exceeded');
5
+ throw new SpongeError('limit-exceeded');
5
6
  durationMs = Math.ceil(durationMs);
6
7
  const result = [];
7
8
  for (let startMs = 0; startMs < durationMs; startMs += asrLimits.windowDurationMs - asrLimits.overlapMs) {
@@ -16,12 +17,15 @@ export function audioWindows(durationMs) {
16
17
  * words are removed only when their time ranges overlap, never globally. */
17
18
  export function joinWindows(windows, transcripts) {
18
19
  if (!transcripts.length || windows.length !== transcripts.length)
19
- throw Error('integrity');
20
+ throw new SpongeError('integrity');
20
21
  const first = transcripts[0], segments = [];
21
22
  for (let i = 0; i < windows.length; i++) {
22
23
  const window = windows[i], transcript = transcripts[i];
23
- if (transcript.sourceSha256 !== first.sourceSha256 || transcript.durationMs !== first.durationMs || transcript.modelFingerprint !== first.modelFingerprint || transcript.requestId !== first.requestId)
24
- throw Error('integrity');
24
+ if (transcript.sourceSha256 !== first.sourceSha256 ||
25
+ transcript.durationMs !== first.durationMs ||
26
+ transcript.modelFingerprint !== first.modelFingerprint ||
27
+ transcript.requestId !== first.requestId)
28
+ throw new SpongeError('integrity');
25
29
  const lower = i ? (windows[i - 1].endMs + window.startMs) / 2 : 0, upper = i + 1 < windows.length ? (window.endMs + windows[i + 1].startMs) / 2 : Infinity;
26
30
  for (const segment of transcript.segments) {
27
31
  const center = (segment.startMs + segment.endMs) / 2;
@@ -33,7 +37,7 @@ export function joinWindows(windows, transcripts) {
33
37
  continue;
34
38
  }
35
39
  if (prior && segment.startMs < prior.startMs)
36
- throw Error('integrity');
40
+ throw new SpongeError('integrity');
37
41
  segments.push({ ...segment });
38
42
  }
39
43
  }