@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
@@ -2,33 +2,51 @@ import { randomUUID } from 'node:crypto';
2
2
  import { join } from 'node:path';
3
3
  import { rm } from 'node:fs/promises';
4
4
  import { acquireWorkerCustody, proveAttemptCleanup } from '../worker/custody.js';
5
- import { directory, writeJournal, readJournal, syncDir } from '../worker/journal.js';
5
+ import { directory, writeJournal, readJournal } from '../worker/journal.js';
6
+ import { syncDirectoryAsync } from '../util/fsync.js';
6
7
  import { supervisedRunner } from '../worker/supervisor.js';
7
- const pending = () => Object.assign(Error('cleanup-required'), { code: 'cleanup-required' });
8
+ import { SpongeError, codeOf } from '../errors.js';
9
+ const pending = () => new SpongeError('cleanup-required');
8
10
  /** Read-only verification still owns subprocesses, including during an import preview. */
9
11
  export async function withImportVerification(libraryRoot, verify) {
10
12
  const custody = await acquireWorkerCustody(libraryRoot), attemptId = randomUUID();
11
13
  let scratchRoot, unknown = false;
12
14
  try {
13
15
  const attempt = await directory(join(custody.directory, 'attempts', attemptId));
14
- await writeJournal(join(attempt, 'import-verification.json'), { schemaVersion: 1, kind: 'import-verification', attemptId, ownerIncarnation: custody.incarnation });
16
+ await writeJournal(join(attempt, 'import-verification.json'), {
17
+ schemaVersion: 1,
18
+ kind: 'import-verification',
19
+ attemptId,
20
+ ownerIncarnation: custody.incarnation,
21
+ });
15
22
  scratchRoot = await directory(join(libraryRoot, 'staging', 'import-verification-' + attemptId));
16
- await writeJournal(join(scratchRoot, 'ownership.json'), { schemaVersion: 1, kind: 'import-verification', requestId: attemptId, ownerIncarnation: custody.incarnation });
17
- return await verify({ scratchRoot, runProcess: supervisedRunner(custody.directory, custody.incarnation, attemptId) });
23
+ await writeJournal(join(scratchRoot, 'ownership.json'), {
24
+ schemaVersion: 1,
25
+ kind: 'import-verification',
26
+ requestId: attemptId,
27
+ ownerIncarnation: custody.incarnation,
28
+ });
29
+ return await verify({
30
+ scratchRoot,
31
+ runProcess: supervisedRunner(custody.directory, custody.incarnation, attemptId),
32
+ });
18
33
  }
19
34
  catch (error) {
20
- unknown = error?.code === 'cleanup-required' || (error instanceof Error && error.message === 'cleanup-required');
35
+ unknown = codeOf(error) === 'cleanup-required';
21
36
  throw error;
22
37
  }
23
38
  finally {
24
- if (unknown || !await proveAttemptCleanup(custody.directory, custody.incarnation, attemptId))
39
+ if (unknown || !(await proveAttemptCleanup(custody.directory, custody.incarnation, attemptId)))
25
40
  throw pending();
26
41
  if (scratchRoot) {
27
42
  const marker = await readJournal(join(scratchRoot, 'ownership.json'));
28
- if (marker.schemaVersion !== 1 || marker.kind !== 'import-verification' || marker.requestId !== attemptId || marker.ownerIncarnation !== custody.incarnation)
43
+ if (marker.schemaVersion !== 1 ||
44
+ marker.kind !== 'import-verification' ||
45
+ marker.requestId !== attemptId ||
46
+ marker.ownerIncarnation !== custody.incarnation)
29
47
  throw pending();
30
48
  await rm(scratchRoot, { recursive: true });
31
- await syncDir(join(libraryRoot, 'staging'));
49
+ await syncDirectoryAsync(join(libraryRoot, 'staging'));
32
50
  }
33
51
  await custody.release();
34
52
  }
@@ -3,84 +3,153 @@ import { join } from 'node:path';
3
3
  import { limits } from '../../contracts/index.js';
4
4
  import { validateAsset } from '../../contracts/validate.js';
5
5
  import { object, metadataFromMarkdown } from '../acquisition/normalize.js';
6
- import { readConfined, hash } from '../acquisition/files.js';
6
+ import { readConfined } from '../acquisition/files.js';
7
+ import { sha256 as hash } from '../util/hash.js';
7
8
  import { withImportVerification } from './custody.js';
8
9
  import { inspectCaptureRuntime } from '../acquisition/runtime.js';
9
10
  import { articleMetadata } from '../metadata/article.js';
10
11
  import { bundleBytes } from '../app/bundle-files.js';
12
+ import { SpongeError } from '../errors.js';
11
13
  export function releasedBundleVerifier(runtime, libraryRoot, signal) {
12
14
  return async (root, kind) => {
13
15
  const checked = await inspectCaptureRuntime(runtime.root, runtime.pins), helper = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './verify-helper.ts' : './verify-helper.js', import.meta.url));
14
16
  return withImportVerification(libraryRoot, async ({ scratchRoot, runProcess }) => {
15
17
  const args = [helper, checked.root, root, kind];
16
- const result = await runProcess(process.platform === 'darwin' ? '/usr/bin/sandbox-exec' : checked.bun, process.platform === 'darwin' ? ['-p', '(version 1) (allow default) (deny network*)', checked.bun, ...args] : args, { env: { PATH: '/usr/bin:/bin', HOME: checked.root, TMPDIR: scratchRoot, LANG: 'en_US.UTF-8' }, deadlineMs: 45000, maxOutputBytes: 65536, signal });
18
+ const result = await runProcess(process.platform === 'darwin' ? '/usr/bin/sandbox-exec' : checked.bun, process.platform === 'darwin'
19
+ ? ['-p', '(version 1) (allow default) (deny network*)', checked.bun, ...args]
20
+ : args, {
21
+ env: { PATH: '/usr/bin:/bin', HOME: checked.root, TMPDIR: scratchRoot, LANG: 'en_US.UTF-8' },
22
+ deadlineMs: 45000,
23
+ maxOutputBytes: 65536,
24
+ signal,
25
+ });
17
26
  if (result.cleanup !== 'settled')
18
- throw Error('cleanup-required');
27
+ throw new SpongeError('cleanup-required');
19
28
  if (result.stopped)
20
- throw Error(result.stopped);
29
+ throw new SpongeError(result.stopped);
21
30
  if (result.code !== 0)
22
- throw Error('unsupported');
31
+ throw new SpongeError('unsupported');
23
32
  return JSON.parse(result.stdout);
24
33
  });
25
34
  };
26
35
  }
27
- function imageType(bytes) { if (bytes[0] === 255 && bytes[1] === 216)
28
- return 'image/jpeg'; if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
29
- return 'image/png'; if (bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP')
30
- return 'image/webp'; if (/^GIF8[79]a/.test(bytes.toString('ascii', 0, 6)))
31
- return 'image/gif'; if (bytes.toString('ascii', 4, 12) === 'ftypavif')
32
- return 'image/avif'; throw Error('unsupported'); }
36
+ function imageType(bytes) {
37
+ if (bytes[0] === 255 && bytes[1] === 216)
38
+ return 'image/jpeg';
39
+ if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
40
+ return 'image/png';
41
+ if (bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP')
42
+ return 'image/webp';
43
+ if (/^GIF8[79]a/.test(bytes.toString('ascii', 0, 6)))
44
+ return 'image/gif';
45
+ if (bytes.toString('ascii', 4, 12) === 'ftypavif')
46
+ return 'image/avif';
47
+ throw new SpongeError('unsupported');
48
+ }
33
49
  /** An import-time digest never upgrades a missing historical document digest. */
34
50
  export async function inspectImportBundle(root, verify) {
51
+ // A bundle and its verifier report are untrusted: malformed JSON, a listed file that is missing, a bad URL or asset record is an integrity failure.
52
+ try {
53
+ return await inspectBundle(root, verify);
54
+ }
55
+ catch (error) {
56
+ if (error instanceof SpongeError)
57
+ throw error;
58
+ throw new SpongeError('integrity', { cause: error });
59
+ }
60
+ }
61
+ async function inspectBundle(root, verify) {
35
62
  const manifestBytes = await readConfined(root, 'capture.json', limits.metadataBytes), manifest = object(JSON.parse(manifestBytes.toString('utf8'))), kind = manifest.kind === 'pdf' ? 'pdf' : 'article';
36
63
  if (kind === 'pdf' ? manifest.schemaVersion !== 1 : ![1, 2, 3, 4].includes(Number(manifest.schemaVersion)))
37
- throw Error('unsupported');
64
+ throw new SpongeError('unsupported');
38
65
  const result = object(await verify(root, kind)), inspection = object(result.inspection), document = object(kind === 'pdf' ? inspection.markdown : inspection.document);
39
- if (inspection.schemaVersion !== manifest.schemaVersion || !['verified', 'unavailable'].includes(String(document.integrity)) || !Array.isArray(inspection.assets) || inspection.assets.length > limits.imagesPerItem)
40
- throw Error('integrity');
41
- if (result.ok !== true && (!Array.isArray(result.issues) || !result.issues.length || result.issues.some(raw => { const issue = object(raw); return !(issue.kind === 'document-integrity' && issue.path === document.path && document.integrity === 'unavailable' && (kind === 'article' || issue.integrity === 'unavailable')); })))
42
- throw Error('integrity');
43
- if (inspection.status !== undefined && inspection.status !== null && !['complete', 'partial'].includes(String(inspection.status)))
44
- throw Error('unsupported');
45
- const markdownBytes = await readConfined(root, String(document.path), limits.markdownBytes), markdown = { relativePath: String(document.path), bytes: markdownBytes.length, sha256: hash(markdownBytes), mediaType: 'text/markdown' };
66
+ if (inspection.schemaVersion !== manifest.schemaVersion ||
67
+ !['verified', 'unavailable'].includes(String(document.integrity)) ||
68
+ !Array.isArray(inspection.assets) ||
69
+ inspection.assets.length > limits.imagesPerItem)
70
+ throw new SpongeError('integrity');
71
+ if (result.ok !== true &&
72
+ (!Array.isArray(result.issues) ||
73
+ !result.issues.length ||
74
+ result.issues.some((raw) => {
75
+ const issue = object(raw);
76
+ return !(issue.kind === 'document-integrity' &&
77
+ issue.path === document.path &&
78
+ document.integrity === 'unavailable' &&
79
+ (kind === 'article' || issue.integrity === 'unavailable'));
80
+ })))
81
+ throw new SpongeError('integrity');
82
+ if (inspection.status !== undefined &&
83
+ inspection.status !== null &&
84
+ !['complete', 'partial'].includes(String(inspection.status)))
85
+ throw new SpongeError('unsupported');
86
+ const markdownBytes = await readConfined(root, String(document.path), limits.markdownBytes), markdown = {
87
+ relativePath: String(document.path),
88
+ bytes: markdownBytes.length,
89
+ sha256: hash(markdownBytes),
90
+ mediaType: 'text/markdown',
91
+ };
46
92
  validateAsset(markdown);
47
93
  if (markdown.sha256 !== document.sha256 || markdown.bytes !== document.bytes)
48
- throw Error('integrity');
94
+ throw new SpongeError('integrity');
49
95
  let unavailable = document.integrity === 'unavailable';
50
96
  const assets = [];
51
97
  let total = 0;
52
98
  for (const raw of inspection.assets) {
53
99
  const asset = object(raw);
54
100
  if (!['verified', 'unavailable'].includes(String(asset.integrity)))
55
- throw Error('integrity');
101
+ throw new SpongeError('integrity');
56
102
  unavailable ||= asset.integrity === 'unavailable';
57
103
  const bytes = await readConfined(root, String(asset.path), limits.imageBytes);
58
104
  total += bytes.length;
59
105
  if (total > 500 * 1024 ** 2)
60
- throw Error('limit-exceeded');
61
- const value = { relativePath: String(asset.path), bytes: bytes.length, sha256: hash(bytes), mediaType: imageType(bytes), relationship: 'image' };
106
+ throw new SpongeError('limit-exceeded');
107
+ const value = {
108
+ relativePath: String(asset.path),
109
+ bytes: bytes.length,
110
+ sha256: hash(bytes),
111
+ mediaType: imageType(bytes),
112
+ relationship: 'image',
113
+ };
62
114
  validateAsset(value);
63
115
  const expectedHash = kind === 'pdf' ? asset.sha256 : asset.expectedSha256, expectedBytes = kind === 'pdf' ? asset.bytes : asset.expectedBytes;
64
116
  if (asset.integrity === 'verified' && (expectedHash !== value.sha256 || expectedBytes !== value.bytes))
65
- throw Error('integrity');
117
+ throw new SpongeError('integrity');
66
118
  assets.push(value);
67
119
  }
68
120
  if (kind === 'pdf') {
69
121
  const source = object(inspection.source);
70
122
  if (source.integrity !== 'verified')
71
- throw Error('integrity');
72
- const asset = { relativePath: String(source.path), sha256: String(source.sha256), bytes: Number(source.bytes), mediaType: 'application/pdf', relationship: 'source-pdf' };
123
+ throw new SpongeError('integrity');
124
+ const asset = {
125
+ relativePath: String(source.path),
126
+ sha256: String(source.sha256),
127
+ bytes: Number(source.bytes),
128
+ mediaType: 'application/pdf',
129
+ relationship: 'source-pdf',
130
+ };
73
131
  validateAsset(asset);
74
- for await (const _ of bundleBytes(root, asset)) { }
132
+ for await (const _ of bundleBytes(root, asset)) {
133
+ }
75
134
  assets.push(asset);
76
135
  }
77
- if (assets.length !== new Set(assets.map(a => a.relativePath)).size)
78
- throw Error('integrity');
79
- const sourceUrl = kind === 'pdf' ? (object(manifest.source).requestedUrl ?? pathToFileURL(join(root, String(object(inspection.source).path))).href) : inspection.sourceUrl;
80
- if (typeof sourceUrl !== 'string' || sourceUrl.length > 16384 || !['http:', 'https:', 'file:'].includes(new URL(sourceUrl).protocol))
81
- throw Error('integrity');
82
- const observedAt = typeof manifest.capturedAt === 'string' && Number.isFinite(Date.parse(manifest.capturedAt)) ? manifest.capturedAt : '1970-01-01T00:00:00.000Z';
83
- const metadata = kind === 'article' ? (manifest.metadata === undefined ? metadataFromMarkdown(markdownBytes.toString('utf8'), observedAt) : articleMetadata(manifest.metadata, observedAt)) : [];
136
+ if (assets.length !== new Set(assets.map((a) => a.relativePath)).size)
137
+ throw new SpongeError('integrity');
138
+ const sourceUrl = kind === 'pdf'
139
+ ? (object(manifest.source).requestedUrl ?? pathToFileURL(join(root, String(object(inspection.source).path))).href)
140
+ : inspection.sourceUrl;
141
+ if (typeof sourceUrl !== 'string' ||
142
+ sourceUrl.length > 16384 ||
143
+ !['http:', 'https:', 'file:'].includes(new URL(sourceUrl).protocol))
144
+ throw new SpongeError('integrity');
145
+ const observedAt = typeof manifest.capturedAt === 'string' && Number.isFinite(Date.parse(manifest.capturedAt))
146
+ ? manifest.capturedAt
147
+ : '1970-01-01T00:00:00.000Z';
148
+ const metadata = kind === 'article'
149
+ ? manifest.metadata === undefined
150
+ ? metadataFromMarkdown(markdownBytes.toString('utf8'), observedAt)
151
+ : articleMetadata(manifest.metadata, observedAt)
152
+ : [];
84
153
  if (kind === 'pdf') {
85
154
  const document = object(manifest.document ?? {});
86
155
  for (const field of ['title', 'author', 'subject', 'creator', 'producer', 'createdAt', 'modifiedAt']) {
@@ -89,9 +158,27 @@ export async function inspectImportBundle(root, verify) {
89
158
  metadata.push({ field, value, source: 'pdf', observedAt, rule: 'wordcell-pdf-v1', selected: true });
90
159
  }
91
160
  }
92
- metadata.push({ field: 'historicalIntegrity', value: unavailable ? 'unavailable' : 'verified', source: kind === 'pdf' ? 'pdf' : 'html', observedAt, rule: 'sponge-import-v1' });
161
+ metadata.push({
162
+ field: 'historicalIntegrity',
163
+ value: unavailable ? 'unavailable' : 'verified',
164
+ source: kind === 'pdf' ? 'pdf' : 'html',
165
+ observedAt,
166
+ rule: 'sponge-import-v1',
167
+ });
93
168
  if (hash(await readConfined(root, 'capture.json', limits.metadataBytes)) !== hash(manifestBytes))
94
- throw Error('integrity');
95
- const manifestSha256 = hash(manifestBytes), importHash = hash(JSON.stringify([manifestSha256, markdown.sha256, ...assets.map(a => [a.relativePath, a.sha256, a.bytes])]));
96
- return { root, kind, sourceUrl, schemaVersion: Number(manifest.schemaVersion), status: unavailable || inspection.status !== 'complete' ? 'partial' : 'complete', markdown, assets, metadata, manifestSha256, historicalIntegrity: unavailable ? 'unavailable' : 'verified', importHash };
169
+ throw new SpongeError('integrity');
170
+ const manifestSha256 = hash(manifestBytes), importHash = hash(JSON.stringify([manifestSha256, markdown.sha256, ...assets.map((a) => [a.relativePath, a.sha256, a.bytes])]));
171
+ return {
172
+ root,
173
+ kind,
174
+ sourceUrl,
175
+ schemaVersion: Number(manifest.schemaVersion),
176
+ status: unavailable || inspection.status !== 'complete' ? 'partial' : 'complete',
177
+ markdown,
178
+ assets,
179
+ metadata,
180
+ manifestSha256,
181
+ historicalIntegrity: unavailable ? 'unavailable' : 'verified',
182
+ importHash,
183
+ };
97
184
  }
@@ -1,18 +1,15 @@
1
- import { type DigestIssue, type DigestV1 } from '../digest/schema.js';
1
+ import { type DigestIssue, type DigestV2 } from '../digest/schema.js';
2
2
  import { type Form, type QuoteMatch } from '../digest/pipeline.js';
3
3
  /**
4
- * Pure conversion of one reviewed Jungle Reading digest note (`kb/notes/reading/<slug>.md`) into Digest v1. Jungle keeps
5
- * quotes in their own section, so each quote is assigned to the idea it shares the most content words with.
4
+ * Pure conversion of one reviewed Jungle Reading digest note (`kb/notes/reading/<slug>.md`) into Digest v2. Jungle keeps
5
+ * quotes in their own section, so each verbatim quote is assigned to the idea it shares the most content words with.
6
+ * Owner text is never cut or rewritten: a note that does not fit v2 as written is reported, not converted.
6
7
  * Nothing here reads files; callers pass the note and, when available, its capture Markdown.
7
8
  */
8
9
  export declare const jungleTags: readonly ["agent-eng", "ai", "work", "business", "creative-coding", "finance", "music", "internet", "security", "history", "art", "hardware", "climate", "article", "interactive", "code", "x", "video"];
9
- /** Tags that name how a source is consumed; Digest v1 carries that as `form`. */
10
+ /** Tags that name how a source is consumed; a digest carries that as `form`. */
10
11
  export declare const jungleFormatTags: readonly ["article", "interactive", "x", "video"];
11
- export declare class JungleDigestError extends Error {
12
- readonly code: 'not-digest' | 'malformed' | 'no-capture';
13
- constructor(code: 'not-digest' | 'malformed' | 'no-capture');
14
- }
15
- export interface JungleQuote {
12
+ interface JungleQuote {
16
13
  text: string;
17
14
  attribution: string;
18
15
  speaker: string;
@@ -32,25 +29,27 @@ export interface JungleNote {
32
29
  }[];
33
30
  quotes: JungleQuote[];
34
31
  }
35
- export type Lossy = 'context-trimmed' | 'quote-unassigned' | 'quote-missing' | 'idea-unquoted' | 'no-content-tag';
32
+ /**
33
+ * Why a Jungle quote is left out of the digest: not found as written in the capture (`snapped` matches differ in markup),
34
+ * under four words, over the quote budget (count, words or source share), or every idea already holds two.
35
+ */
36
+ export type QuoteDrop = 'not-verbatim' | 'too-short' | 'budget' | 'overflow';
36
37
  export interface JungleConversion {
37
38
  note: JungleNote;
38
- /** Every idea, including unquoted ones: the evaluation gold. */
39
- gold: DigestV1;
40
- /** Per Jungle quote: the gold idea it was assigned to, and how it located in the capture. */
39
+ /** The note as Digest v2: the Gist's first sentence as lead and the rest as context, every idea, and the quotes that survive. */
40
+ gold: DigestV2;
41
+ /** Per Jungle quote, in note order: the idea it went to (null when dropped), how it located in the capture, and why it was dropped. */
41
42
  quotes: {
42
43
  idea: number | null;
43
44
  match?: QuoteMatch;
45
+ dropped?: QuoteDrop;
44
46
  }[];
45
- lossy: Lossy[];
46
- /** checkDigest issues of the gold against the capture; empty without a capture. */
47
+ /** Why the gold is not a valid Digest v2: shape issues (an empty detail), then checkDigest issues against the capture when one is given. */
47
48
  issues: DigestIssue[];
48
- /** The gold with unlocated quotes and then unquoted ideas dropped, when that still passes every v1 rule. */
49
- importable?: DigestV1;
50
- /** Gold passes every v1 rule against the capture with nothing dropped or trimmed. `kind` is never gold: Jungle has no kinds. */
51
- clean: boolean;
49
+ /** The gold, when a capture was given and the gold passes every v2 rule against it. */
50
+ digest?: DigestV2;
52
51
  }
53
- export type Yaml = string | boolean | Yaml[] | {
52
+ type Yaml = string | boolean | Yaml[] | {
54
53
  [k: string]: Yaml;
55
54
  };
56
55
  /** Capture Markdown without its YAML frontmatter: what a Sponge revision of the same page would hold. */
@@ -58,7 +57,7 @@ export declare function captureBody(capture: string): string;
58
57
  export declare function parseJungleNote(markdown: string): JungleNote;
59
58
  /**
60
59
  * One Jungle Reading note of any form (digest, social, note, conversation), kept whole: the exact Markdown and its hash,
61
- * the parsed frontmatter, and the gist, ideas and quotes where the form has them. Nothing is trimmed to Digest v1 limits.
60
+ * the parsed frontmatter, and the gist, ideas and quotes where the form has them. Nothing is trimmed to digest limits.
62
61
  * `capture` is the evidence the note is about: the first `synthesizes` relation, else the first `evidenced-by`.
63
62
  */
64
63
  export interface JungleEditorial {
@@ -84,14 +83,22 @@ export declare function parseJungleEditorial(markdown: string): JungleEditorial;
84
83
  /** Content-word stems: lowercase, stopwords out, a crude plural/tense strip. Shared by idea assignment and lexical coverage. */
85
84
  export declare function contentTerms(text: string): Set<string>;
86
85
  export declare function termSimilarity(a: Set<string>, b: Set<string>): number;
87
- /** Best-first assignment: every idea gets at most one quote before any gets a second; zero-overlap pairs are never made. */
86
+ /**
87
+ * Each quote in order goes to the idea sharing the most content words with its claim and detail (ties: the earliest idea),
88
+ * else to the next best idea with room; null when every idea already holds `cap` quotes.
89
+ */
88
90
  export declare function assignQuotes(ideas: {
89
91
  claim: string;
90
92
  detail: string;
91
93
  }[], quotes: {
92
94
  text: string;
93
- }[]): (number | null)[];
95
+ }[], cap?: number): (number | null)[];
94
96
  export declare const firstSentence: (text: string) => readonly [string, string];
95
97
  export declare function formOf(note: JungleNote): Form;
96
- /** Convert one Jungle digest note. With `capture` (the capture Markdown, frontmatter allowed), quotes are located and the gold is checked. */
98
+ /**
99
+ * Convert one Jungle digest note. With `capture` (the capture Markdown, frontmatter allowed), each quote is kept only when it
100
+ * occurs as written (folded typography only) and has at least four words; whole quotes then leave, longest and then latest in
101
+ * the note first, until the rest fit the quote budget (never the last one); survivors are assigned to ideas, and the gold is checked.
102
+ */
97
103
  export declare function convertJungleDigest(noteMarkdown: string, capture?: string): JungleConversion;
104
+ export {};