@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,37 +1,43 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { foldText, locateAll, locateLoose, words } from '../annotations/quote.js';
3
- import { checkDigest, digestLimits as L, forms, parseDigest } from './schema.js';
4
- import { StoreError } from '../store/types.js';
2
+ import { checkDigest, digestLimits as L, forms, parseDigest, } from './schema.js';
3
+ import { SpongeError } from '../errors.js';
4
+ import { sha256 } from '../util/hash.js';
5
5
  /**
6
6
  * Model-agnostic Digest v2 drafting. The model sees numbered passages and cites a passage id plus a short exact
7
7
  * span; code snaps, prunes and checks. Nothing leaves this module except through the injected model.
8
8
  */
9
- export const PROMPT_VERSION = 'sponge-digest-prompt-v2';
9
+ export const PROMPT_VERSION = 'sponge-digest-prompt-v4';
10
10
  /** 24k characters is roughly 6k tokens of passages, leaving room for instructions and output in an 8k context on an 8 GB Mac. */
11
- export const defaultDigestParams = { temperature: 0, seed: 1, maxTokens: 2048, contextChars: 24000, maxSections: 24, timeoutMs: 300000, deadlineMs: 1800000, maxOutputBytes: 65536 };
12
- export class DigestError extends Error {
13
- code;
14
- issues;
15
- constructor(code, issues) {
16
- super(code);
17
- this.code = code;
18
- this.issues = issues;
19
- this.name = 'DigestError';
20
- }
21
- }
11
+ const defaultDigestParams = {
12
+ temperature: 0,
13
+ seed: 1,
14
+ maxTokens: 2048,
15
+ contextChars: 24000,
16
+ maxSections: 24,
17
+ timeoutMs: 300000,
18
+ deadlineMs: 1800000,
19
+ maxOutputBytes: 65536,
20
+ };
22
21
  /** Quotes under this many words carry no evidence; the checker is gaining the same floor. */
23
- export const minQuoteWords = 4;
24
- const sha256 = (v) => createHash('sha256').update(v).digest('hex');
22
+ const minQuoteWords = 4;
25
23
  const collapse = (v) => v.replace(/\s+/g, ' ').trim();
26
24
  const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
27
- /** Sorted-key JSON; the only serialization that enters input hashes. */
25
+ /**
26
+ * Sorted-key JSON; the only serialization that enters input hashes. Deliberately not `util/canonical.ts`: this one
27
+ * accepts finite non-integer numbers (temperature) and writes `undefined` array entries as null, so merging would
28
+ * change digest input hashes.
29
+ */
28
30
  export function canonicalJson(value) {
29
31
  if (Array.isArray(value))
30
32
  return `[${value.map(canonicalJson).join(',')}]`;
31
33
  if (object(value))
32
- return `{${Object.keys(value).filter(k => value[k] !== undefined).sort().map(k => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`;
34
+ return `{${Object.keys(value)
35
+ .filter((k) => value[k] !== undefined)
36
+ .sort()
37
+ .map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`)
38
+ .join(',')}}`;
33
39
  if (typeof value === 'number' && !Number.isFinite(value))
34
- throw new DigestError('invalid-input');
40
+ throw new SpongeError('invalid-input');
35
41
  return JSON.stringify(value ?? null);
36
42
  }
37
43
  /** Paragraph-level passages with raw offsets. Headings label passages; code fences and image-only blocks are not quotable. */
@@ -85,8 +91,12 @@ export function passages(markdown, maxChars = 1200) {
85
91
  }
86
92
  return out;
87
93
  }
88
- function lastSentenceEnd(window) { let best = -1; for (const m of window.matchAll(/[.!?]["')\]]*(?=\s)/g))
89
- best = m.index + m[0].length; return best; }
94
+ function lastSentenceEnd(window) {
95
+ let best = -1;
96
+ for (const m of window.matchAll(/[.!?]["')\]]*(?=\s)/g))
97
+ best = m.index + m[0].length;
98
+ return best;
99
+ }
90
100
  const display = (p) => `[${p.id}] ${collapse(p.text)}`;
91
101
  /** Pack passages into sections of at most `budget` display characters, preferring heading boundaries past half a budget. */
92
102
  export function sectionsOf(list, budget) {
@@ -94,7 +104,8 @@ export function sectionsOf(list, budget) {
94
104
  let current = [], size = 0;
95
105
  for (const p of list) {
96
106
  const n = display(p).length + 1;
97
- if (current.length && (size + n > budget || p.heading !== current[current.length - 1].heading && size > budget / 2)) {
107
+ if (current.length &&
108
+ (size + n > budget || (p.heading !== current[current.length - 1].heading && size > budget / 2))) {
98
109
  out.push(current);
99
110
  current = [];
100
111
  size = 0;
@@ -107,27 +118,74 @@ export function sectionsOf(list, budget) {
107
118
  return out;
108
119
  }
109
120
  const sourceWords = (markdown) => words(markdown);
110
- export const quoteBudget = (markdown) => Math.min(L.totalQuoteWords, Math.max(L.quoteWords, Math.floor(sourceWords(markdown) * L.sourceQuoteShare)));
121
+ const quoteBudget = (markdown) => Math.min(L.totalQuoteWords, Math.max(L.quoteWords, Math.floor(sourceWords(markdown) * L.sourceQuoteShare)));
111
122
  const str = (maxLength) => ({ type: 'string', minLength: 1, maxLength });
112
123
  /** JSON Schema for the model's draft. Tags become an enum when a vocabulary is given. */
113
124
  export function draftSchema(passageCount, vocabulary) {
114
- const quote = { type: 'object', additionalProperties: false, required: ['passage', 'text'], properties: { passage: { type: 'integer', minimum: 1, maximum: Math.max(1, passageCount) }, text: str(300), attribution: str(200) } };
115
- const idea = { type: 'object', additionalProperties: false, required: ['claim', 'detail', 'quotes'], properties: { claim: str(200), detail: str(600), quotes: { type: 'array', minItems: 0, maxItems: L.maxQuotesPerIdea, items: quote } } };
116
- const tag = vocabulary?.length ? { type: 'string', enum: [...vocabulary] } : { type: 'string', pattern: '^[a-z0-9][a-z0-9-]{0,63}$' };
117
- return { type: 'object', additionalProperties: false, required: ['lead', 'ideas', 'tags'], properties: { lead: str(L.leadChars), context: str(600), ideas: { type: 'array', minItems: L.minIdeas, maxItems: L.maxIdeas, items: idea }, tags: { type: 'array', minItems: 0, maxItems: L.maxTags, items: tag } } };
125
+ const quote = {
126
+ type: 'object',
127
+ additionalProperties: false,
128
+ required: ['passage', 'text'],
129
+ properties: {
130
+ passage: { type: 'integer', minimum: 1, maximum: Math.max(1, passageCount) },
131
+ text: str(300),
132
+ attribution: str(200),
133
+ },
134
+ };
135
+ const idea = {
136
+ type: 'object',
137
+ additionalProperties: false,
138
+ required: ['claim', 'detail', 'quotes'],
139
+ properties: {
140
+ claim: str(200),
141
+ detail: str(600),
142
+ quotes: { type: 'array', minItems: 0, maxItems: L.maxQuotesPerIdea, items: quote },
143
+ },
144
+ };
145
+ const tag = vocabulary?.length
146
+ ? { type: 'string', enum: [...vocabulary] }
147
+ : { type: 'string', pattern: '^[a-z0-9][a-z0-9-]{0,63}$' };
148
+ return {
149
+ type: 'object',
150
+ additionalProperties: false,
151
+ required: ['lead', 'ideas', 'tags'],
152
+ properties: {
153
+ lead: str(L.leadChars),
154
+ context: str(600),
155
+ ideas: { type: 'array', minItems: L.minIdeas, maxItems: L.maxIdeas, items: idea },
156
+ tags: { type: 'array', minItems: 0, maxItems: L.maxTags, items: tag },
157
+ },
158
+ };
118
159
  }
119
- export function notesSchema(first, last) {
120
- return { type: 'object', additionalProperties: false, required: ['notes'], properties: { notes: { type: 'array', minItems: 1, maxItems: 4, items: { type: 'object', additionalProperties: false, required: ['point', 'passage', 'quote'], properties: { point: str(300), passage: { type: 'integer', minimum: first, maximum: last }, quote: str(300) } } } } };
160
+ function notesSchema(first, last) {
161
+ return {
162
+ type: 'object',
163
+ additionalProperties: false,
164
+ required: ['notes'],
165
+ properties: {
166
+ notes: {
167
+ type: 'array',
168
+ minItems: 1,
169
+ maxItems: 4,
170
+ items: {
171
+ type: 'object',
172
+ additionalProperties: false,
173
+ required: ['point', 'passage', 'quote'],
174
+ properties: { point: str(300), passage: { type: 'integer', minimum: first, maximum: last }, quote: str(300) },
175
+ },
176
+ },
177
+ },
178
+ };
121
179
  }
122
180
  const system = (budget, vocabulary) => `You write a Digest v2 of one source for a private reading library. Reply with JSON only, matching the schema.
123
- - lead: one sentence of at most ${L.leadChars} characters that names the author or organization when known and states the main finding.
124
- - context: optional, at most ${L.contextWords} words of essential background. Lead and context together stay within ${L.gistWords} words and ${L.gistSentences} sentences.
125
- - ideas: ${L.minIdeas} to ${L.maxIdeas}, in the order the author makes them. Each idea is a bold one-sentence claim with detail:
126
- - claim: one sentence of at most ${L.claimWords} words that asserts something; never a topic label.
127
- - detail: at most ${L.detailWords} words in at most ${L.detailSentences} sentences: evidence, mechanism or limits.
128
- - quotes: 0 to ${L.maxQuotesPerIdea} that support this idea, each copied character for character from one numbered passage, ${minQuoteWords} to ${L.quoteWords} words, with that passage number. No added quotation marks or ellipses. Give attribution (who said it) when it is not the author. Prefer reader highlights.
129
- - Attach at least ${L.minQuotes} and at most ${L.maxQuotes} quotes in all. All quotes together stay within ${budget} words and never overlap.
130
- - tags: 0 to ${L.maxTags} ${vocabulary?.length ? `from: ${vocabulary.join(', ')}` : 'lowercase kebab-case topics'}.
181
+ - lead: one dense sentence of at most ${L.leadChars} characters: who (author, organization or product by name) did or argues what, with the concrete specifics that make it this source (names, numbers, mechanism). Not a teaser.
182
+ - context: optional, at most ${L.contextWords} words: the key evidence, numbers or limits the lead could not hold. Lead and context together stay within ${L.gistWords} words and ${L.gistSentences} sentences.
183
+ - ideas: ${L.minIdeas} to ${L.maxIdeas} (prefer ${L.maxIdeas} when the source has them), in the order the author makes them, covering the whole source including its caveats and limits. Each idea is a claim with detail:
184
+ - claim: one short sentence of at most ${L.claimWords} words (often 4 to 10) that asserts something; never a topic label.
185
+ - detail: at most ${L.detailWords} words in at most ${L.detailSentences} sentences: the specific evidence, mechanism, numbers or limits from the source.
186
+ - quotes: 0 to ${L.maxQuotesPerIdea} that support this idea, each a contiguous span copied character for character from one numbered passage, ${minQuoteWords + 2} to 20 words, with that passage number. A quote is a fragment, not a whole long sentence: when a sentence runs past 20 words, copy only its strongest part. No added quotation marks or ellipses. Give attribution (who said it) when it is not the author. Prefer reader highlights.
187
+ - Attach at least ${L.minQuotes} and at most ${L.maxQuotes} quotes in all; ${L.minQuotes + 1} to 3 is typical. All quotes together stay within ${budget} words and never overlap.
188
+ - tags: 0 to ${L.maxTags} ${vocabulary?.length ? `from: ${vocabulary.join(', ')}` : 'lowercase kebab-case topics'}; usually one subject tag.
131
189
  Use only what the source says.`;
132
190
  function sourceBlock(title, form, list, highlights) {
133
191
  let heading;
@@ -143,10 +201,26 @@ function sourceBlock(title, form, list, highlights) {
143
201
  const marks = highlights.map((h, i) => `[h${i + 1}${h.passage ? `, passage ${h.passage}` : ''}] ${collapse(h.text).slice(0, 400)}`);
144
202
  return `Form: ${form}${title ? `\nTitle: ${collapse(title).slice(0, 300)}` : ''}${marks.length ? `\n\nReader highlights (preferred quotes):\n${marks.join('\n')}` : ''}\n\nPassages:\n${lines.join('\n')}`;
145
203
  }
146
- const rule = { gist: `lead and context within ${L.gistWords} words and ${L.gistSentences} sentences`, lead: `one sentence, at most ${L.leadChars} characters`, context: `at most ${L.contextWords} words`, claim: `one distinct sentence, at most ${L.claimWords} words`, detail: `at most ${L.detailWords} words and ${L.detailSentences} sentences`, quotes: `0-${L.maxQuotesPerIdea} quotes per idea, each copied exactly from its passage, ${minQuoteWords} to ${L.quoteWords} words, not overlapping another quote`, attribution: `at most ${L.attributionWords} words`, ideas: `${L.minIdeas}-${L.maxIdeas} ideas; ${L.minQuotes}-${L.maxQuotes} quotes in all, within the total budget`, tags: `0-${L.maxTags} distinct allowed tags` };
204
+ const rule = {
205
+ gist: `lead and context within ${L.gistWords} words and ${L.gistSentences} sentences`,
206
+ lead: `one sentence, at most ${L.leadChars} characters`,
207
+ context: `at most ${L.contextWords} words`,
208
+ claim: `one distinct sentence, at most ${L.claimWords} words`,
209
+ detail: `at most ${L.detailWords} words and ${L.detailSentences} sentences`,
210
+ quotes: `0-${L.maxQuotesPerIdea} quotes per idea, each copied exactly from its passage, ${minQuoteWords} to ${L.quoteWords} words, not overlapping another quote`,
211
+ attribution: `at most ${L.attributionWords} words`,
212
+ ideas: `${L.minIdeas}-${L.maxIdeas} ideas; ${L.minQuotes}-${L.maxQuotes} quotes in all, within the total budget`,
213
+ tags: `0-${L.maxTags} distinct allowed tags`,
214
+ };
147
215
  function repairPrompt(issues) {
148
- const lines = issues.slice(0, 40).map(i => { const last = i.path.split('.').filter(s => !/^\d+$/.test(s)).pop() ?? ''; return `- ${i.path || 'output'}: ${i.code}${rule[last] ? ` (rule: ${rule[last]})` : ''}`; });
149
- return `That draft broke these rules:\n${lines.join('\n')}\nReturn the complete corrected JSON. Copy quotes exactly from the numbered passages.`;
216
+ const lines = issues.slice(0, 40).map((i) => {
217
+ const last = i.path
218
+ .split('.')
219
+ .filter((s) => !/^\d+$/.test(s))
220
+ .pop() ?? '';
221
+ return `- ${i.path || 'output'}: ${i.code}${rule[last] ? ` (rule: ${rule[last]})` : ''}`;
222
+ });
223
+ return `That draft broke these rules:\n${lines.join('\n')}\nReturn the complete corrected JSON. Copy quotes exactly from the numbered passages; shorten a too-long quote to its strongest contiguous 6 to 20 words.`;
150
224
  }
151
225
  /** Map the model's draft shape into Digest v2; parseDigest then decides whether the shape is valid. Ideas keep the model's order. */
152
226
  function fromModel(text, form) {
@@ -159,18 +233,31 @@ function fromModel(text, form) {
159
233
  }
160
234
  if (!object(v) || !Array.isArray(v.ideas) || !Array.isArray(v.tags))
161
235
  return { issues: [{ path: '', code: 'shape' }] };
162
- const trim = (s) => typeof s === 'string' ? s.trim() : s, cites = [];
236
+ const trim = (s) => (typeof s === 'string' ? s.trim() : s), cites = [];
163
237
  const ideas = v.ideas.map((idea) => {
164
238
  if (!object(idea)) {
165
239
  cites.push([]);
166
240
  return idea;
167
241
  }
168
242
  const quotes = Array.isArray(idea.quotes) ? idea.quotes : [];
169
- cites.push(quotes.map((q) => Number.isSafeInteger(q?.passage) ? q.passage : undefined));
170
- return { claim: trim(idea.claim), detail: trim(idea.detail), quotes: quotes.map((q) => { const attribution = trim(q?.attribution); return { text: trim(q?.text), ...(attribution ? { attribution } : {}) }; }) };
243
+ cites.push(quotes.map((q) => (Number.isSafeInteger(q?.passage) ? q.passage : undefined)));
244
+ return {
245
+ claim: trim(idea.claim),
246
+ detail: trim(idea.detail),
247
+ quotes: quotes.map((q) => {
248
+ const attribution = trim(q?.attribution);
249
+ return { text: trim(q?.text), ...(attribution ? { attribution } : {}) };
250
+ }),
251
+ };
171
252
  });
172
253
  const context = trim(v.context);
173
- const parsed = parseDigest({ schemaVersion: 2, form, gist: { lead: trim(v.lead), ...(context ? { context } : {}) }, ideas, tags: v.tags.map(trim) });
254
+ const parsed = parseDigest({
255
+ schemaVersion: 2,
256
+ form,
257
+ gist: { lead: trim(v.lead), ...(context ? { context } : {}) },
258
+ ideas,
259
+ tags: v.tags.map(trim),
260
+ });
174
261
  return parsed.digest?.schemaVersion === 2 ? { digest: parsed.digest, cites } : { issues: parsed.issues };
175
262
  }
176
263
  /**
@@ -181,7 +268,10 @@ export function snapQuote(source, text, within) {
181
268
  if (source.includes(text))
182
269
  return { text, match: 'exact' };
183
270
  const inside = (m) => !!within && m.start >= within.start && m.end <= within.end;
184
- const pick = (all) => { const local = all.filter(inside); return local.length === 1 ? local[0] : all.length === 1 ? all[0] : undefined; };
271
+ const pick = (all) => {
272
+ const local = all.filter(inside);
273
+ return local.length === 1 ? local[0] : all.length === 1 ? all[0] : undefined;
274
+ };
185
275
  const folded = locateAll(source, text);
186
276
  if (folded.length) {
187
277
  const m = pick(folded);
@@ -198,13 +288,39 @@ export function snapQuote(source, text, within) {
198
288
  end++;
199
289
  return { text: source.slice(start, end), match: 'snapped' };
200
290
  }
201
- /** Snap every quote in place and drop quotes under `minQuoteWords`. */
202
- export function snapQuotes(draft, source, list) {
203
- const stats = { exact: 0, folded: 0, snapped: 0, missing: 0, short: 0 }, byId = new Map(list.map(p => [p.id, p]));
291
+ /**
292
+ * A located quote over the word limit keeps its leading words: a prefix of a source span is still verbatim. Trailing
293
+ * clause punctuation goes; unlocated quotes are left for the checker.
294
+ */
295
+ export function trimQuote(text, max = L.quoteWords) {
296
+ if (words(text) <= max)
297
+ return text;
298
+ const ends = [...text.matchAll(/\S+/g)].map((m) => m.index + m[0].length);
299
+ let n = Math.min(max, ends.length), out = text;
300
+ do {
301
+ out = text.slice(0, ends[n - 1]).replace(/[\s,;:(\[\u2014\u2013-]+$/, '');
302
+ n--;
303
+ } while (n > 0 && words(out) > max);
304
+ return out;
305
+ }
306
+ /** Snap every quote in place, trim located quotes over the word limit, and drop quotes under `minQuoteWords`. */
307
+ function snapQuotes(draft, source, list) {
308
+ const stats = { exact: 0, folded: 0, snapped: 0, missing: 0, short: 0, trimmed: 0 }, byId = new Map(list.map((p) => [p.id, p]));
204
309
  draft.digest.ideas.forEach((idea, i) => {
205
- idea.quotes.forEach((q, j) => { const r = snapQuote(source, q.text, byId.get(draft.cites[i]?.[j] ?? 0)); q.text = r.text; stats[r.match]++; });
206
- const keep = idea.quotes.map(q => words(q.text) >= minQuoteWords);
207
- stats.short += keep.filter(k => !k).length;
310
+ idea.quotes.forEach((q, j) => {
311
+ const r = snapQuote(source, q.text, byId.get(draft.cites[i]?.[j] ?? 0));
312
+ q.text = r.text;
313
+ stats[r.match]++;
314
+ if (r.match !== 'missing') {
315
+ const t = trimQuote(q.text);
316
+ if (t !== q.text) {
317
+ q.text = t;
318
+ stats.trimmed++;
319
+ }
320
+ }
321
+ });
322
+ const keep = idea.quotes.map((q) => words(q.text) >= minQuoteWords);
323
+ stats.short += keep.filter((k) => !k).length;
208
324
  idea.quotes = idea.quotes.filter((_, j) => keep[j]);
209
325
  draft.cites[i] = (draft.cites[i] ?? []).filter((_, j) => keep[j]);
210
326
  });
@@ -216,11 +332,24 @@ const pruneable = new Set(['not-verbatim', 'overlap', 'unsupported', 'too-long',
216
332
  * budget (second quotes first, longest first), never the last quote. Quoteless ideas stay: v2 allows them.
217
333
  */
218
334
  function prune(draft, issues, source, options) {
219
- const dropped = { quotes: 0, ideas: 0, tags: 0 }, d = draft.digest, bad = new Set(issues.filter(i => /^ideas\.\d+\.quotes\.\d+(?:\.attribution)?$/.test(i.path) && pruneable.has(i.code)).map(i => i.path.replace(/\.attribution$/, '')));
220
- d.ideas.forEach((idea, i) => { const keep = idea.quotes.map((_, j) => !bad.has(`ideas.${i}.quotes.${j}`)).map((k, j) => k && j < L.maxQuotesPerIdea); dropped.quotes += keep.filter(k => !k).length; idea.quotes = idea.quotes.filter((_, j) => keep[j]); draft.cites[i] = (draft.cites[i] ?? []).filter((_, j) => keep[j]); });
335
+ const dropped = { quotes: 0, ideas: 0, tags: 0 }, d = draft.digest, bad = new Set(issues
336
+ .filter((i) => /^ideas\.\d+\.quotes\.\d+(?:\.attribution)?$/.test(i.path) && pruneable.has(i.code))
337
+ .map((i) => i.path.replace(/\.attribution$/, '')));
338
+ d.ideas.forEach((idea, i) => {
339
+ const keep = idea.quotes
340
+ .map((_, j) => !bad.has(`ideas.${i}.quotes.${j}`))
341
+ .map((k, j) => k && j < L.maxQuotesPerIdea);
342
+ dropped.quotes += keep.filter((k) => !k).length;
343
+ idea.quotes = idea.quotes.filter((_, j) => keep[j]);
344
+ draft.cites[i] = (draft.cites[i] ?? []).filter((_, j) => keep[j]);
345
+ });
221
346
  const seen = new Set();
222
- const keepIdea = d.ideas.map(idea => { const key = foldText(idea.claim).toLowerCase(), ok = !seen.has(key); seen.add(key); return ok; });
223
- dropped.ideas = keepIdea.filter(k => !k).length;
347
+ const keepIdea = d.ideas.map((idea) => {
348
+ const key = foldText(idea.claim).toLowerCase(), ok = !seen.has(key);
349
+ seen.add(key);
350
+ return ok;
351
+ });
352
+ dropped.ideas = keepIdea.filter((k) => !k).length;
224
353
  d.ideas = d.ideas.filter((_, i) => keepIdea[i]);
225
354
  draft.cites = draft.cites.filter((_, i) => keepIdea[i]);
226
355
  if (d.ideas.length > L.maxIdeas) {
@@ -228,7 +357,9 @@ function prune(draft, issues, source, options) {
228
357
  d.ideas = d.ideas.slice(0, L.maxIdeas);
229
358
  draft.cites = draft.cites.slice(0, L.maxIdeas);
230
359
  }
231
- const tags = [...new Set(d.tags)].filter(t => options.tags ? options.tags.includes(t) : /^[a-z0-9][a-z0-9-]{0,63}$/.test(t)).slice(0, L.maxTags);
360
+ const tags = [...new Set(d.tags)]
361
+ .filter((t) => (options.tags ? options.tags.includes(t) : /^[a-z0-9][a-z0-9-]{0,63}$/.test(t)))
362
+ .slice(0, L.maxTags);
232
363
  dropped.tags = d.tags.length - tags.length;
233
364
  d.tags = tags;
234
365
  const budget = quoteBudget(source), all = () => d.ideas.flatMap((idea, i) => idea.quotes.map((q, j) => ({ i, j, n: words(q.text) })));
@@ -240,76 +371,132 @@ function prune(draft, issues, source, options) {
240
371
  }
241
372
  return dropped;
242
373
  }
243
- const hashedParams = (p) => ({ temperature: p.temperature, seed: p.seed, maxTokens: p.maxTokens, contextChars: p.contextChars, maxSections: p.maxSections });
374
+ const hashedParams = (p) => ({
375
+ temperature: p.temperature,
376
+ seed: p.seed,
377
+ maxTokens: p.maxTokens,
378
+ contextChars: p.contextChars,
379
+ maxSections: p.maxSections,
380
+ });
244
381
  function resolveParams(params = {}) {
245
382
  const p = { ...defaultDigestParams, ...params }, int = (v, min, max) => Number.isSafeInteger(v) && v >= min && v <= max;
246
- if (!(p.temperature >= 0 && p.temperature <= 2) || !int(p.seed, 0, 2 ** 31) || !int(p.maxTokens, 256, 32768) || !int(p.contextChars, 2000, 1_000_000) || !int(p.maxSections, 1, 256) || !int(p.timeoutMs, 1, 3_600_000) || !int(p.deadlineMs, 1, 14_400_000) || !int(p.maxOutputBytes, 1024, 4 * 1024 * 1024))
247
- throw new DigestError('invalid-input');
383
+ if (!(p.temperature >= 0 && p.temperature <= 2) ||
384
+ !int(p.seed, 0, 2 ** 31) ||
385
+ !int(p.maxTokens, 256, 32768) ||
386
+ !int(p.contextChars, 2000, 1_000_000) ||
387
+ !int(p.maxSections, 1, 256) ||
388
+ !int(p.timeoutMs, 1, 3_600_000) ||
389
+ !int(p.deadlineMs, 1, 14_400_000) ||
390
+ !int(p.maxOutputBytes, 1024, 4 * 1024 * 1024))
391
+ throw new SpongeError('invalid-input');
248
392
  return p;
249
393
  }
250
394
  function validIdentity(id) {
251
395
  const ok = (v) => typeof v === 'string' && v.length > 0 && v.length <= 256 && !/[\u0000-\u001f\u007f]/.test(v);
252
396
  if (!object(id) || !ok(id.model) || !ok(id.quantization) || !ok(id.runtime))
253
- throw new DigestError('invalid-input');
397
+ throw new SpongeError('invalid-input');
254
398
  return { model: id.model, quantization: id.quantization, runtime: id.runtime };
255
399
  }
256
400
  /** sha256 over canonical JSON of the revision, its Markdown, the prompt version, the model identity and every output-shaping input. */
257
401
  export function digestInputHash(input) {
258
- return sha256(canonicalJson({ revisionId: input.revisionId, markdownSha256: sha256(input.markdown), promptVersion: PROMPT_VERSION, model: validIdentity(input.model), params: { ...hashedParams(resolveParams(input.params)), form: input.form, vocabulary: input.vocabulary ? [...input.vocabulary] : null, highlights: (input.highlights ?? []).map(h => h.text), support: input.embed ? input.supportThreshold ?? defaultSupportThreshold : null } }));
402
+ return sha256(canonicalJson({
403
+ revisionId: input.revisionId,
404
+ markdownSha256: sha256(input.markdown),
405
+ promptVersion: PROMPT_VERSION,
406
+ model: validIdentity(input.model),
407
+ params: {
408
+ ...hashedParams(resolveParams(input.params)),
409
+ form: input.form,
410
+ vocabulary: input.vocabulary ? [...input.vocabulary] : null,
411
+ highlights: (input.highlights ?? []).map((h) => h.text),
412
+ support: input.embed ? (input.supportThreshold ?? defaultSupportThreshold) : null,
413
+ },
414
+ }));
259
415
  }
260
416
  /** Unqualified on real claims: a deliberately low bar that only catches quotes unrelated to their claim. */
261
- export const defaultSupportThreshold = 0.35;
417
+ const defaultSupportThreshold = 0.35;
262
418
  export async function draftDigest(input) {
263
419
  const p = resolveParams(input.params), identity = validIdentity(input.model.identity);
264
420
  if (typeof input.markdown !== 'string' || !input.markdown.trim() || !forms.includes(input.form))
265
- throw new DigestError('invalid-input');
266
- if (input.vocabulary && (!input.vocabulary.length || input.vocabulary.length > 256 || input.vocabulary.some(t => !/^[a-z0-9][a-z0-9-]{0,63}$/.test(t))))
267
- throw new DigestError('invalid-input');
421
+ throw new SpongeError('invalid-input');
422
+ if (input.vocabulary &&
423
+ (!input.vocabulary.length ||
424
+ input.vocabulary.length > 256 ||
425
+ input.vocabulary.some((t) => !/^[a-z0-9][a-z0-9-]{0,63}$/.test(t))))
426
+ throw new SpongeError('invalid-input');
268
427
  const inputHash = digestInputHash({ ...input, model: identity }), source = input.markdown, list = passages(source);
269
428
  if (list.length === 0)
270
- throw new DigestError('invalid-input');
429
+ throw new SpongeError('invalid-input');
271
430
  const userSignal = input.signal ?? new AbortController().signal, overall = AbortSignal.any([userSignal, AbortSignal.timeout(p.deadlineMs)]);
272
- const trace = { strategy: 'single', sections: 1, calls: 0, repaired: false, attempts: [], dropped: { quotes: 0, ideas: 0, tags: 0 } };
431
+ const trace = {
432
+ strategy: 'single',
433
+ sections: 1,
434
+ calls: 0,
435
+ repaired: false,
436
+ attempts: [],
437
+ dropped: { quotes: 0, ideas: 0, tags: 0 },
438
+ };
273
439
  const call = async (messages, schema) => {
274
440
  if (userSignal.aborted)
275
- throw new DigestError('cancelled');
441
+ throw new SpongeError('cancelled');
276
442
  if (overall.aborted)
277
- throw new DigestError('limit-exceeded');
443
+ throw new SpongeError('limit-exceeded');
278
444
  const signal = AbortSignal.any([overall, AbortSignal.timeout(p.timeoutMs)]);
279
445
  trace.calls++;
280
446
  let text;
281
447
  try {
282
- text = await input.model.generate(messages, schema, { signal, maxOutputBytes: p.maxOutputBytes, maxTokens: p.maxTokens, temperature: p.temperature, seed: p.seed });
448
+ text = await input.model.generate(messages, schema, {
449
+ signal,
450
+ maxOutputBytes: p.maxOutputBytes,
451
+ maxTokens: p.maxTokens,
452
+ temperature: p.temperature,
453
+ seed: p.seed,
454
+ });
283
455
  }
284
456
  catch (error) {
285
457
  if (userSignal.aborted)
286
- throw new DigestError('cancelled');
458
+ throw new SpongeError('cancelled');
287
459
  if (signal.aborted)
288
- throw new DigestError('limit-exceeded');
289
- if (error instanceof DigestError)
460
+ throw new SpongeError('limit-exceeded');
461
+ if (error instanceof SpongeError)
290
462
  throw error;
291
- throw new DigestError('unavailable');
463
+ throw new SpongeError('unavailable');
292
464
  }
293
465
  if (userSignal.aborted)
294
- throw new DigestError('cancelled');
466
+ throw new SpongeError('cancelled');
295
467
  if (typeof text !== 'string' || Buffer.byteLength(text) > p.maxOutputBytes)
296
- throw new DigestError('limit-exceeded');
468
+ throw new SpongeError('limit-exceeded');
297
469
  return text;
298
470
  };
299
- const highlights = (input.highlights ?? []).filter(h => typeof h?.text === 'string' && h.text.trim()).slice(0, 32).map(h => { const at = h.start ?? locateAll(source, h.text)[0]?.start; return { text: h.text, passage: at === undefined ? undefined : list.find(x => at >= x.start && at < x.end)?.id }; });
471
+ const highlights = (input.highlights ?? [])
472
+ .filter((h) => typeof h?.text === 'string' && h.text.trim())
473
+ .slice(0, 32)
474
+ .map((h) => {
475
+ const at = h.start ?? locateAll(source, h.text)[0]?.start;
476
+ return {
477
+ text: h.text,
478
+ passage: at === undefined ? undefined : list.find((x) => at >= x.start && at < x.end)?.id,
479
+ };
480
+ });
300
481
  let shown = list, notesText;
301
482
  const total = list.reduce((n, x) => n + display(x).length + 1, 0);
302
483
  if (total > p.contextChars) {
303
484
  // Long source: notes per section, then one combined pass over the cited and highlighted passages only.
304
485
  const sections = sectionsOf(list, p.contextChars);
305
486
  if (sections.length > p.maxSections)
306
- throw new DigestError('limit-exceeded');
487
+ throw new SpongeError('limit-exceeded');
307
488
  trace.strategy = 'sections';
308
489
  trace.sections = sections.length;
309
490
  const notes = [];
310
491
  for (const [k, section] of sections.entries()) {
311
492
  const first = section[0].id, last = section[section.length - 1].id;
312
- const text = await call([{ role: 'system', content: `You take notes on part ${k + 1} of ${sections.length} of a longer source. Reply with JSON only. Give 1 to 4 notes, each a point of at most 25 words that the section asserts, with one passage number from this part and a short quote copied exactly from that passage.` }, { role: 'user', content: sourceBlock(input.title, input.form, section, []) }], notesSchema(first, last));
493
+ const text = await call([
494
+ {
495
+ role: 'system',
496
+ content: `You take notes on part ${k + 1} of ${sections.length} of a longer source. Reply with JSON only. Give 1 to 4 notes, each a point of at most 25 words that the section asserts, with one passage number from this part and a short quote copied exactly from that passage.`,
497
+ },
498
+ { role: 'user', content: sourceBlock(input.title, input.form, section, []) },
499
+ ], notesSchema(first, last));
313
500
  let v;
314
501
  try {
315
502
  v = JSON.parse(text);
@@ -319,11 +506,23 @@ export async function draftDigest(input) {
319
506
  }
320
507
  if (object(v) && Array.isArray(v.notes))
321
508
  for (const n of v.notes.slice(0, 4))
322
- if (object(n) && typeof n.point === 'string' && Number.isSafeInteger(n.passage) && n.passage >= first && n.passage <= last && typeof n.quote === 'string')
323
- notes.push({ point: collapse(n.point).slice(0, 300), passage: n.passage, quote: collapse(n.quote).slice(0, 300) });
509
+ if (object(n) &&
510
+ typeof n.point === 'string' &&
511
+ Number.isSafeInteger(n.passage) &&
512
+ n.passage >= first &&
513
+ n.passage <= last &&
514
+ typeof n.quote === 'string')
515
+ notes.push({
516
+ point: collapse(n.point).slice(0, 300),
517
+ passage: n.passage,
518
+ quote: collapse(n.quote).slice(0, 300),
519
+ });
324
520
  }
325
- const wanted = new Set([...notes.map(n => n.passage), ...highlights.flatMap(h => h.passage ? [h.passage] : [])]);
326
- notesText = notes.map(n => `- [${n.passage}] ${n.point}`).join('\n');
521
+ const wanted = new Set([
522
+ ...notes.map((n) => n.passage),
523
+ ...highlights.flatMap((h) => (h.passage ? [h.passage] : [])),
524
+ ]);
525
+ notesText = notes.map((n) => `- [${n.passage}] ${n.point}`).join('\n');
327
526
  shown = [];
328
527
  let size = notesText.length;
329
528
  for (const x of list)
@@ -332,21 +531,36 @@ export async function draftDigest(input) {
332
531
  size += display(x).length + 1;
333
532
  }
334
533
  if (!shown.length)
335
- throw new DigestError('unavailable', [{ path: '', code: 'no-notes' }]);
534
+ throw new SpongeError('unavailable', { issues: [{ path: '', code: 'no-notes' }] });
336
535
  }
337
536
  const budget = quoteBudget(source), schema = draftSchema(list.length, input.vocabulary);
338
- const messages = [{ role: 'system', content: system(budget, input.vocabulary) }, { role: 'user', content: (notesText ? `Notes from each part of the source, in order:\n${notesText}\n\n` : '') + sourceBlock(input.title, input.form, shown, highlights) }];
537
+ const messages = [
538
+ { role: 'system', content: system(budget, input.vocabulary) },
539
+ {
540
+ role: 'user',
541
+ content: (notesText ? `Notes from each part of the source, in order:\n${notesText}\n\n` : '') +
542
+ sourceBlock(input.title, input.form, shown, highlights),
543
+ },
544
+ ];
339
545
  const cache = new Map(), options = { ...(input.vocabulary ? { tags: input.vocabulary } : {}) };
340
546
  const embedKey = (t) => Buffer.from(foldText(t)).subarray(0, 900).toString();
341
547
  if (input.embed)
342
- Object.assign(options, { supportThreshold: input.supportThreshold ?? defaultSupportThreshold, supports: (claim, quote) => { const a = cache.get(embedKey(claim)), b = cache.get(embedKey(quote)); return a && b ? a.reduce((n, x, i) => n + x * b[i], 0) : 0; } });
548
+ Object.assign(options, {
549
+ supportThreshold: input.supportThreshold ?? defaultSupportThreshold,
550
+ supports: (claim, quote) => {
551
+ const a = cache.get(embedKey(claim)), b = cache.get(embedKey(quote));
552
+ return a && b ? a.reduce((n, x, i) => n + x * b[i], 0) : 0;
553
+ },
554
+ });
343
555
  const check = async (draft) => {
344
556
  if (input.embed) {
345
- const missing = [...new Set(draft.digest.ideas.flatMap(i => [i.claim, ...i.quotes.map(q => q.text)]).map(embedKey))].filter(t => !cache.has(t));
557
+ const missing = [
558
+ ...new Set(draft.digest.ideas.flatMap((i) => [i.claim, ...i.quotes.map((q) => q.text)]).map(embedKey)),
559
+ ].filter((t) => !cache.has(t));
346
560
  if (missing.length) {
347
561
  const vectors = await input.embed(missing, overall);
348
562
  if (!Array.isArray(vectors) || vectors.length !== missing.length)
349
- throw new DigestError('unavailable');
563
+ throw new SpongeError('unavailable');
350
564
  missing.forEach((t, i) => cache.set(t, vectors[i]));
351
565
  }
352
566
  }
@@ -370,30 +584,59 @@ export async function draftDigest(input) {
370
584
  result = await attempt(await call([...messages, { role: 'assistant', content: first }, { role: 'user', content: repairPrompt(result.issues) }], schema));
371
585
  }
372
586
  if (!result.draft)
373
- throw new DigestError('unavailable', result.issues);
587
+ throw new SpongeError('unavailable', { issues: result.issues });
374
588
  let issues = result.issues;
375
589
  if (issues.length) {
376
590
  trace.dropped = prune(result.draft, issues, source, options);
377
591
  issues = await check(result.draft);
378
592
  if (result.draft.digest.ideas.length < L.minIdeas)
379
- issues = [...issues.filter(i => !(i.path === 'ideas' && i.code === 'count')), { path: 'ideas', code: 'count' }];
593
+ issues = [...issues.filter((i) => !(i.path === 'ideas' && i.code === 'count')), { path: 'ideas', code: 'count' }];
380
594
  if (issues.length)
381
- throw new DigestError('unavailable', issues);
595
+ throw new SpongeError('unavailable', { issues });
382
596
  }
383
- const producer = { pipeline: 'sponge-digest', promptVersion: PROMPT_VERSION, model: identity, params: hashedParams(p), strategy: trace.strategy, sections: trace.sections, calls: trace.calls, repaired: trace.repaired, dropped: trace.dropped, ...(input.embed ? { supportThreshold: options.supportThreshold } : {}) };
597
+ const producer = {
598
+ pipeline: 'sponge-digest',
599
+ promptVersion: PROMPT_VERSION,
600
+ model: identity,
601
+ params: hashedParams(p),
602
+ strategy: trace.strategy,
603
+ sections: trace.sections,
604
+ calls: trace.calls,
605
+ repaired: trace.repaired,
606
+ dropped: trace.dropped,
607
+ ...(input.embed ? { supportThreshold: options.supportThreshold } : {}),
608
+ };
384
609
  return { output: result.draft.digest, inputHash, producer, options, trace };
385
610
  }
386
- const formFor = (kind) => kind === 'pdf' ? 'pdf' : kind === 'media' ? 'video' : 'article';
611
+ const formFor = (kind) => (kind === 'pdf' ? 'pdf' : kind === 'media' ? 'video' : 'article');
387
612
  /** Draft and store a digest for the item's current revision. A matching input hash returns the stored draft without calling the model. */
388
613
  export async function createDigest(store, itemId, model, options = {}) {
389
614
  const item = store.read(itemId);
390
615
  if (!item)
391
- throw new StoreError('unavailable');
392
- const highlights = store.listAnnotations(itemId).filter(a => a.kind === 'highlight' && a.revisionId === item.revisionId && a.anchored !== false).map(a => ({ text: a.anchor.exact, start: a.anchor.start }));
393
- const input = { ...options, revisionId: item.revisionId, markdown: item.markdown, title: item.title, form: formFor(item.kind), model, highlights };
394
- const prior = store.findDigest(item.revisionId, digestInputHash({ ...input, model: model.identity }));
616
+ throw new SpongeError('unavailable');
617
+ const highlights = store.annotations
618
+ .list(itemId)
619
+ .filter((a) => a.kind === 'highlight' && a.revisionId === item.revisionId && a.anchored !== false)
620
+ .map((a) => ({ text: a.anchor.exact, start: a.anchor.start }));
621
+ const input = {
622
+ ...options,
623
+ revisionId: item.revisionId,
624
+ markdown: item.markdown,
625
+ title: item.title,
626
+ form: formFor(item.kind),
627
+ model,
628
+ highlights,
629
+ };
630
+ const prior = store.derivations.find('digest', item.revisionId, digestInputHash({ ...input, model: model.identity }));
395
631
  if (prior)
396
632
  return { derivation: prior, duplicate: true };
397
- const draft = await draftDigest(input), stored = store.putDigest({ itemId, revisionId: item.revisionId, producer: draft.producer, inputHash: draft.inputHash, output: draft.output, options: draft.options });
398
- return { derivation: store.readDigest(stored.id), duplicate: stored.duplicate, trace: draft.trace };
633
+ const draft = await draftDigest(input), stored = store.derivations.put('digest', {
634
+ itemId,
635
+ revisionId: item.revisionId,
636
+ producer: draft.producer,
637
+ inputHash: draft.inputHash,
638
+ output: draft.output,
639
+ options: draft.options,
640
+ });
641
+ return { derivation: store.derivations.read('digest', stored.id), duplicate: stored.duplicate, trace: draft.trace };
399
642
  }