@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.
- package/README.md +23 -10
- package/bin/sponge.js +8 -7
- package/dist/contracts/index.js +24 -6
- package/dist/contracts/validate.js +106 -29
- package/dist/migrations/008.d.ts +3 -0
- package/dist/migrations/008.js +5 -0
- package/dist/migrations/009.d.ts +3 -0
- package/dist/migrations/009.js +8 -0
- package/dist/migrations/010.d.ts +3 -0
- package/dist/migrations/010.js +11 -0
- package/dist/migrations/index.d.ts +15 -0
- package/dist/migrations/index.js +39 -0
- package/dist/src/acquisition/capture.js +87 -28
- package/dist/src/acquisition/cleanup.d.ts +0 -4
- package/dist/src/acquisition/cleanup.js +3 -5
- package/dist/src/acquisition/errors.js +17 -2
- package/dist/src/acquisition/files.d.ts +0 -1
- package/dist/src/acquisition/files.js +14 -11
- package/dist/src/acquisition/index.d.ts +2 -3
- package/dist/src/acquisition/index.js +1 -2
- package/dist/src/acquisition/lock.js +1 -1
- package/dist/src/acquisition/normalize.js +57 -18
- package/dist/src/acquisition/pins.js +22 -4
- package/dist/src/acquisition/process.js +67 -29
- package/dist/src/acquisition/runtime.d.ts +5 -7
- package/dist/src/acquisition/runtime.js +129 -68
- package/dist/src/acquisition/verify-helper.js +10 -3
- package/dist/src/annotations/quote.js +40 -12
- package/dist/src/annotations/store.d.ts +0 -1
- package/dist/src/annotations/store.js +66 -14
- package/dist/src/app/article.d.ts +2 -1
- package/dist/src/app/article.js +66 -70
- package/dist/src/app/bundle-files.d.ts +0 -4
- package/dist/src/app/bundle-files.js +11 -14
- package/dist/src/app/markdown.js +18 -6
- package/dist/src/app/source.d.ts +0 -4
- package/dist/src/app/source.js +18 -14
- package/dist/src/cli/arguments.d.ts +2 -8
- package/dist/src/cli/arguments.js +67 -80
- package/dist/src/cli/commands.d.ts +42 -0
- package/dist/src/cli/commands.js +795 -0
- package/dist/src/cli/config.js +12 -9
- package/dist/src/cli/digest.d.ts +1 -1
- package/dist/src/cli/digest.js +39 -17
- package/dist/src/cli/import.js +25 -4
- package/dist/src/cli/indexing.d.ts +2 -0
- package/dist/src/cli/indexing.js +26 -9
- package/dist/src/cli/main.js +47 -386
- package/dist/src/cli/portability.d.ts +0 -2
- package/dist/src/cli/portability.js +13 -8
- package/dist/src/cli/queue.js +12 -8
- package/dist/src/cli/runtimes.d.ts +1 -3
- package/dist/src/cli/runtimes.js +13 -6
- package/dist/src/cli/terminal.js +1 -1
- package/dist/src/cli/worker.d.ts +7 -0
- package/dist/src/cli/worker.js +54 -15
- package/dist/src/derivations/kinds.d.ts +34 -0
- package/dist/src/derivations/kinds.js +14 -0
- package/dist/src/derivations/types.d.ts +48 -0
- package/dist/src/derivations/types.js +1 -0
- package/dist/src/digest/derivation.d.ts +25 -0
- package/dist/src/digest/derivation.js +124 -0
- package/dist/src/digest/editorial.d.ts +10 -46
- package/dist/src/digest/editorial.js +73 -59
- package/dist/src/digest/endpoint.js +63 -27
- package/dist/src/digest/pipeline.d.ts +53 -32
- package/dist/src/digest/pipeline.js +357 -125
- package/dist/src/digest/schema.d.ts +44 -17
- package/dist/src/digest/schema.js +132 -44
- package/dist/src/errors.d.ts +46 -0
- package/dist/src/errors.js +117 -0
- package/dist/src/identity/query-policy.d.ts +0 -5
- package/dist/src/identity/query-policy.js +61 -9
- package/dist/src/identity/uri.d.ts +6 -5
- package/dist/src/identity/uri.js +37 -18
- package/dist/src/identity/visibility.d.ts +11 -1
- package/dist/src/identity/visibility.js +81 -17
- package/dist/src/importers/apply.js +104 -36
- package/dist/src/importers/custody.js +27 -9
- package/dist/src/importers/inspect.js +125 -38
- package/dist/src/importers/jungle-digest.d.ts +31 -24
- package/dist/src/importers/jungle-digest.js +217 -115
- package/dist/src/importers/jungle-store.d.ts +20 -11
- package/dist/src/importers/jungle-store.js +136 -41
- package/dist/src/importers/jungle.d.ts +32 -16
- package/dist/src/importers/jungle.js +145 -32
- package/dist/src/importers/manifest.js +57 -21
- package/dist/src/importers/verify-helper.js +18 -4
- package/dist/src/indexing/image.js +29 -10
- package/dist/src/indexing/index.d.ts +37 -6
- package/dist/src/indexing/index.js +360 -124
- package/dist/src/indexing/jobs.js +156 -47
- package/dist/src/indexing/prune-run.d.ts +2 -0
- package/dist/src/indexing/prune-run.js +69 -26
- package/dist/src/indexing/prune.d.ts +12 -3
- package/dist/src/indexing/prune.js +85 -35
- package/dist/src/indexing/runner.d.ts +2 -1
- package/dist/src/indexing/runner.js +83 -40
- package/dist/src/indexing/upgrade.d.ts +9 -0
- package/dist/src/indexing/upgrade.js +79 -0
- package/dist/src/inference/asr/bridge.js +6 -2
- package/dist/src/inference/asr/protocol.js +109 -19
- package/dist/src/inference/asr/runtime.js +58 -21
- package/dist/src/inference/asr/windows.js +9 -5
- package/dist/src/inference/embedding/index.d.ts +2 -6
- package/dist/src/inference/embedding/index.js +89 -29
- package/dist/src/ingest/attempt.d.ts +29 -0
- package/dist/src/ingest/attempt.js +55 -0
- package/dist/src/media/capture-helper.js +7 -2
- package/dist/src/media/capture.d.ts +2 -1
- package/dist/src/media/capture.js +124 -43
- package/dist/src/media/handler.d.ts +4 -2
- package/dist/src/media/handler.js +88 -70
- package/dist/src/media/transcribe.js +63 -21
- package/dist/src/metadata/article.js +49 -17
- package/dist/src/models/artifacts.d.ts +10 -8
- package/dist/src/models/artifacts.js +237 -106
- package/dist/src/models/catalog.d.ts +2 -5
- package/dist/src/models/catalog.js +94 -25
- package/dist/src/models/download.js +55 -15
- package/dist/src/models/runtime.d.ts +0 -2
- package/dist/src/models/runtime.js +69 -33
- package/dist/src/pdf/capture-helper.js +3 -1
- package/dist/src/pdf/capture.d.ts +2 -1
- package/dist/src/pdf/capture.js +95 -27
- package/dist/src/pdf/errors.js +12 -1
- package/dist/src/pdf/handler.d.ts +2 -1
- package/dist/src/pdf/handler.js +70 -71
- package/dist/src/pdf/index.d.ts +1 -3
- package/dist/src/pdf/index.js +0 -2
- package/dist/src/pdf/input.js +12 -7
- package/dist/src/pdf/normalize.js +80 -21
- package/dist/src/pdf/source-identity.js +3 -2
- package/dist/src/pdf/source.js +12 -8
- package/dist/src/pdf/verify-helper.js +15 -3
- package/dist/src/portability/backup.d.ts +2 -9
- package/dist/src/portability/backup.js +108 -22
- package/dist/src/portability/database.js +51 -17
- package/dist/src/portability/export.js +35 -9
- package/dist/src/portability/files.d.ts +1 -4
- package/dist/src/portability/files.js +127 -67
- package/dist/src/portability/gc.js +65 -22
- package/dist/src/publish/component.js +1 -1
- package/dist/src/publish/feed.d.ts +2 -2
- package/dist/src/publish/feed.js +75 -29
- package/dist/src/publish/identity.d.ts +5 -1
- package/dist/src/publish/identity.js +31 -11
- package/dist/src/publish/index.d.ts +2 -3
- package/dist/src/publish/index.js +34 -21
- package/dist/src/publish/projection.d.ts +2 -9
- package/dist/src/publish/projection.js +21 -14
- package/dist/src/search/extension.js +3 -1
- package/dist/src/search/hybrid.d.ts +3 -8
- package/dist/src/search/hybrid.js +45 -11
- package/dist/src/search/lexical.js +18 -4
- package/dist/src/search/query.js +56 -25
- package/dist/src/search/vector.js +37 -10
- package/dist/src/service/launch-agent.d.ts +5 -3
- package/dist/src/service/launch-agent.js +272 -83
- package/dist/src/service/lifetime.js +47 -14
- package/dist/src/service/loop.d.ts +3 -3
- package/dist/src/service/loop.js +47 -19
- package/dist/src/store/annotations.d.ts +32 -0
- package/dist/src/store/annotations.js +44 -0
- package/dist/src/store/backups.d.ts +18 -0
- package/dist/src/store/backups.js +110 -0
- package/dist/src/store/blobs.d.ts +24 -0
- package/dist/src/store/blobs.js +139 -0
- package/dist/src/store/changes.d.ts +5 -12
- package/dist/src/store/changes.js +108 -56
- package/dist/src/store/context.d.ts +15 -0
- package/dist/src/store/context.js +19 -0
- package/dist/src/store/derivations.d.ts +42 -0
- package/dist/src/store/derivations.js +104 -0
- package/dist/src/store/files.d.ts +0 -2
- package/dist/src/store/files.js +19 -24
- package/dist/src/store/ids.d.ts +30 -0
- package/dist/src/store/ids.js +34 -0
- package/dist/src/store/imports.d.ts +26 -0
- package/dist/src/store/imports.js +57 -0
- package/dist/src/store/index.d.ts +3 -186
- package/dist/src/store/index.js +2 -809
- package/dist/src/store/lease.d.ts +20 -0
- package/dist/src/store/lease.js +62 -0
- package/dist/src/store/library.d.ts +75 -0
- package/dist/src/store/library.js +174 -0
- package/dist/src/store/open.d.ts +7 -0
- package/dist/src/store/open.js +183 -0
- package/dist/src/store/queue.d.ts +65 -0
- package/dist/src/store/queue.js +327 -0
- package/dist/src/store/revisions.d.ts +12 -0
- package/dist/src/store/revisions.js +189 -0
- package/dist/src/store/search-index.d.ts +63 -0
- package/dist/src/store/search-index.js +190 -0
- package/dist/src/store/sync-ready.d.ts +6 -0
- package/dist/src/store/sync-ready.js +121 -0
- package/dist/src/store/types.d.ts +1 -5
- package/dist/src/store/types.js +1 -8
- package/dist/src/util/canonical.d.ts +2 -0
- package/dist/src/util/canonical.js +17 -0
- package/dist/src/util/exact.d.ts +2 -0
- package/dist/src/util/exact.js +9 -0
- package/dist/src/util/fsync.d.ts +4 -0
- package/dist/src/util/fsync.js +22 -0
- package/dist/src/util/hash.d.ts +4 -0
- package/dist/src/util/hash.js +8 -0
- package/dist/src/worker/basic.d.ts +0 -1
- package/dist/src/worker/basic.js +23 -7
- package/dist/src/worker/custody.d.ts +4 -1
- package/dist/src/worker/custody.js +111 -38
- package/dist/src/worker/engine.d.ts +2 -1
- package/dist/src/worker/engine.js +42 -20
- package/dist/src/worker/identity.js +13 -5
- package/dist/src/worker/journal.d.ts +1 -1
- package/dist/src/worker/journal.js +54 -24
- package/dist/src/worker/lock.d.ts +18 -0
- package/dist/src/worker/lock.js +196 -0
- package/dist/src/worker/runtime-gc.d.ts +4 -3
- package/dist/src/worker/runtime-gc.js +68 -41
- package/dist/src/worker/supervisor-child.js +19 -6
- package/dist/src/worker/supervisor.js +41 -8
- package/native/runtime-manifest.json +4 -4
- package/package.json +10 -2
- package/dist/src/digest/store.d.ts +0 -57
- package/dist/src/digest/store.js +0 -77
- package/dist/src/store/foundation.d.ts +0 -7
- 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,
|
|
4
|
-
import {
|
|
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
|
-
* Model-agnostic Digest
|
|
7
|
-
* span; code snaps,
|
|
6
|
+
* Model-agnostic Digest v2 drafting. The model sees numbered passages and cites a passage id plus a short exact
|
|
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-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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)
|
|
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
|
|
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) {
|
|
89
|
-
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 &&
|
|
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,29 +118,74 @@ export function sectionsOf(list, budget) {
|
|
|
107
118
|
return out;
|
|
108
119
|
}
|
|
109
120
|
const sourceWords = (markdown) => words(markdown);
|
|
110
|
-
|
|
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 = {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
return {
|
|
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
|
-
const system = (budget, vocabulary) => `You write a Digest
|
|
123
|
-
- lead: one sentence of at most ${L.leadChars} characters
|
|
124
|
-
- context: optional, at most ${L.contextWords} words
|
|
125
|
-
- ideas: ${L.minIdeas} to ${L.maxIdeas}, in the order
|
|
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
|
-
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
- All quotes together stay within ${budget} words and never overlap.
|
|
132
|
-
- tags: 1 to ${L.maxTags} ${vocabulary?.length ? `from: ${vocabulary.join(', ')}` : 'lowercase kebab-case topics'}.
|
|
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.
|
|
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.
|
|
133
189
|
Use only what the source says.`;
|
|
134
190
|
function sourceBlock(title, form, list, highlights) {
|
|
135
191
|
let heading;
|
|
@@ -145,12 +201,28 @@ function sourceBlock(title, form, list, highlights) {
|
|
|
145
201
|
const marks = highlights.map((h, i) => `[h${i + 1}${h.passage ? `, passage ${h.passage}` : ''}] ${collapse(h.text).slice(0, 400)}`);
|
|
146
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')}`;
|
|
147
203
|
}
|
|
148
|
-
const rule = {
|
|
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
|
+
};
|
|
149
215
|
function repairPrompt(issues) {
|
|
150
|
-
const lines = issues.slice(0, 40).map(i
|
|
151
|
-
|
|
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.`;
|
|
152
224
|
}
|
|
153
|
-
/** Map the model's draft shape into
|
|
225
|
+
/** Map the model's draft shape into Digest v2; parseDigest then decides whether the shape is valid. Ideas keep the model's order. */
|
|
154
226
|
function fromModel(text, form) {
|
|
155
227
|
let v;
|
|
156
228
|
try {
|
|
@@ -161,20 +233,32 @@ function fromModel(text, form) {
|
|
|
161
233
|
}
|
|
162
234
|
if (!object(v) || !Array.isArray(v.ideas) || !Array.isArray(v.tags))
|
|
163
235
|
return { issues: [{ path: '', code: 'shape' }] };
|
|
164
|
-
const trim = (s) => typeof s === 'string' ? s.trim() : s, cites = [];
|
|
236
|
+
const trim = (s) => (typeof s === 'string' ? s.trim() : s), cites = [];
|
|
165
237
|
const ideas = v.ideas.map((idea) => {
|
|
166
238
|
if (!object(idea)) {
|
|
167
239
|
cites.push([]);
|
|
168
240
|
return idea;
|
|
169
241
|
}
|
|
170
|
-
const voice = object(idea.voice) && idea.voice.type !== 'author' ? { type: idea.voice.type, name: trim(idea.voice.name) } : { type: 'author' };
|
|
171
242
|
const quotes = Array.isArray(idea.quotes) ? idea.quotes : [];
|
|
172
|
-
cites.push(quotes.map((q) => Number.isSafeInteger(q?.passage) ? q.passage : undefined));
|
|
173
|
-
return {
|
|
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
|
+
};
|
|
174
252
|
});
|
|
175
253
|
const context = trim(v.context);
|
|
176
|
-
const parsed = parseDigest({
|
|
177
|
-
|
|
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
|
+
});
|
|
261
|
+
return parsed.digest?.schemaVersion === 2 ? { digest: parsed.digest, cites } : { issues: parsed.issues };
|
|
178
262
|
}
|
|
179
263
|
/**
|
|
180
264
|
* Make a quote exactly verbatim when that is unambiguous: a unique folded or Markdown-insensitive match, inside
|
|
@@ -184,7 +268,10 @@ export function snapQuote(source, text, within) {
|
|
|
184
268
|
if (source.includes(text))
|
|
185
269
|
return { text, match: 'exact' };
|
|
186
270
|
const inside = (m) => !!within && m.start >= within.start && m.end <= within.end;
|
|
187
|
-
const pick = (all) => {
|
|
271
|
+
const pick = (all) => {
|
|
272
|
+
const local = all.filter(inside);
|
|
273
|
+
return local.length === 1 ? local[0] : all.length === 1 ? all[0] : undefined;
|
|
274
|
+
};
|
|
188
275
|
const folded = locateAll(source, text);
|
|
189
276
|
if (folded.length) {
|
|
190
277
|
const m = pick(folded);
|
|
@@ -201,34 +288,68 @@ export function snapQuote(source, text, within) {
|
|
|
201
288
|
end++;
|
|
202
289
|
return { text: source.slice(start, end), match: 'snapped' };
|
|
203
290
|
}
|
|
204
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
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]));
|
|
207
309
|
draft.digest.ideas.forEach((idea, i) => {
|
|
208
|
-
idea.quotes.forEach((q, j) => {
|
|
209
|
-
|
|
210
|
-
|
|
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;
|
|
211
324
|
idea.quotes = idea.quotes.filter((_, j) => keep[j]);
|
|
212
325
|
draft.cites[i] = (draft.cites[i] ?? []).filter((_, j) => keep[j]);
|
|
213
326
|
});
|
|
214
327
|
return stats;
|
|
215
328
|
}
|
|
216
|
-
/** Ideas and their quotes in source order, by first located occurrence: the checker requires non-decreasing idea starts. */
|
|
217
|
-
function order(draft, source) {
|
|
218
|
-
const at = (text) => locateAll(source, text)[0]?.start ?? Number.MAX_SAFE_INTEGER;
|
|
219
|
-
const rows = draft.digest.ideas.map((idea, i) => { const qs = idea.quotes.map((q, j) => ({ q, cite: draft.cites[i]?.[j], at: at(q.text) })).sort((a, b) => a.at - b.at); return { idea: { ...idea, quotes: qs.map(x => x.q) }, cites: qs.map(x => x.cite), at: qs[0]?.at ?? Number.MAX_SAFE_INTEGER }; });
|
|
220
|
-
rows.sort((a, b) => a.at - b.at);
|
|
221
|
-
draft.digest.ideas = rows.map(r => r.idea);
|
|
222
|
-
draft.cites = rows.map(r => r.cites);
|
|
223
|
-
}
|
|
224
329
|
const pruneable = new Set(['not-verbatim', 'overlap', 'unsupported', 'too-long', 'speaker-required']);
|
|
225
|
-
/**
|
|
330
|
+
/**
|
|
331
|
+
* Deterministic last resort: drop failing quotes, then duplicate ideas and invalid tags, then quotes over the count or word
|
|
332
|
+
* budget (second quotes first, longest first), never the last quote. Quoteless ideas stay: v2 allows them.
|
|
333
|
+
*/
|
|
226
334
|
function prune(draft, issues, source, options) {
|
|
227
|
-
const dropped = { quotes: 0, ideas: 0, tags: 0 }, d = draft.digest, bad = new Set(issues
|
|
228
|
-
|
|
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
|
+
});
|
|
229
346
|
const seen = new Set();
|
|
230
|
-
const keepIdea = d.ideas.map(idea => {
|
|
231
|
-
|
|
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;
|
|
232
353
|
d.ideas = d.ideas.filter((_, i) => keepIdea[i]);
|
|
233
354
|
draft.cites = draft.cites.filter((_, i) => keepIdea[i]);
|
|
234
355
|
if (d.ideas.length > L.maxIdeas) {
|
|
@@ -236,90 +357,146 @@ function prune(draft, issues, source, options) {
|
|
|
236
357
|
d.ideas = d.ideas.slice(0, L.maxIdeas);
|
|
237
358
|
draft.cites = draft.cites.slice(0, L.maxIdeas);
|
|
238
359
|
}
|
|
239
|
-
const tags = [...new Set(d.tags)]
|
|
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);
|
|
240
363
|
dropped.tags = d.tags.length - tags.length;
|
|
241
364
|
d.tags = tags;
|
|
242
|
-
const budget = quoteBudget(source),
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
d.ideas[i.k].quotes.pop();
|
|
248
|
-
draft.cites[i.k]?.pop();
|
|
365
|
+
const budget = quoteBudget(source), all = () => d.ideas.flatMap((idea, i) => idea.quotes.map((q, j) => ({ i, j, n: words(q.text) })));
|
|
366
|
+
for (let q = all(); q.length > 1 && (q.length > L.maxQuotes || q.reduce((n, x) => n + x.n, 0) > budget); q = all()) {
|
|
367
|
+
const x = q.sort((a, b) => b.j - a.j || b.n - a.n)[0];
|
|
368
|
+
d.ideas[x.i].quotes.splice(x.j, 1);
|
|
369
|
+
draft.cites[x.i]?.splice(x.j, 1);
|
|
249
370
|
dropped.quotes++;
|
|
250
371
|
}
|
|
251
372
|
return dropped;
|
|
252
373
|
}
|
|
253
|
-
const hashedParams = (p) => ({
|
|
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
|
+
});
|
|
254
381
|
function resolveParams(params = {}) {
|
|
255
382
|
const p = { ...defaultDigestParams, ...params }, int = (v, min, max) => Number.isSafeInteger(v) && v >= min && v <= max;
|
|
256
|
-
if (!(p.temperature >= 0 && p.temperature <= 2) ||
|
|
257
|
-
|
|
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');
|
|
258
392
|
return p;
|
|
259
393
|
}
|
|
260
394
|
function validIdentity(id) {
|
|
261
395
|
const ok = (v) => typeof v === 'string' && v.length > 0 && v.length <= 256 && !/[\u0000-\u001f\u007f]/.test(v);
|
|
262
396
|
if (!object(id) || !ok(id.model) || !ok(id.quantization) || !ok(id.runtime))
|
|
263
|
-
throw new
|
|
397
|
+
throw new SpongeError('invalid-input');
|
|
264
398
|
return { model: id.model, quantization: id.quantization, runtime: id.runtime };
|
|
265
399
|
}
|
|
266
400
|
/** sha256 over canonical JSON of the revision, its Markdown, the prompt version, the model identity and every output-shaping input. */
|
|
267
401
|
export function digestInputHash(input) {
|
|
268
|
-
return sha256(canonicalJson({
|
|
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
|
+
}));
|
|
269
415
|
}
|
|
270
416
|
/** Unqualified on real claims: a deliberately low bar that only catches quotes unrelated to their claim. */
|
|
271
|
-
|
|
417
|
+
const defaultSupportThreshold = 0.35;
|
|
272
418
|
export async function draftDigest(input) {
|
|
273
419
|
const p = resolveParams(input.params), identity = validIdentity(input.model.identity);
|
|
274
420
|
if (typeof input.markdown !== 'string' || !input.markdown.trim() || !forms.includes(input.form))
|
|
275
|
-
throw new
|
|
276
|
-
if (input.vocabulary &&
|
|
277
|
-
|
|
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');
|
|
278
427
|
const inputHash = digestInputHash({ ...input, model: identity }), source = input.markdown, list = passages(source);
|
|
279
428
|
if (list.length === 0)
|
|
280
|
-
throw new
|
|
429
|
+
throw new SpongeError('invalid-input');
|
|
281
430
|
const userSignal = input.signal ?? new AbortController().signal, overall = AbortSignal.any([userSignal, AbortSignal.timeout(p.deadlineMs)]);
|
|
282
|
-
const trace = {
|
|
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
|
+
};
|
|
283
439
|
const call = async (messages, schema) => {
|
|
284
440
|
if (userSignal.aborted)
|
|
285
|
-
throw new
|
|
441
|
+
throw new SpongeError('cancelled');
|
|
286
442
|
if (overall.aborted)
|
|
287
|
-
throw new
|
|
443
|
+
throw new SpongeError('limit-exceeded');
|
|
288
444
|
const signal = AbortSignal.any([overall, AbortSignal.timeout(p.timeoutMs)]);
|
|
289
445
|
trace.calls++;
|
|
290
446
|
let text;
|
|
291
447
|
try {
|
|
292
|
-
text = await input.model.generate(messages, schema, {
|
|
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
|
+
});
|
|
293
455
|
}
|
|
294
456
|
catch (error) {
|
|
295
457
|
if (userSignal.aborted)
|
|
296
|
-
throw new
|
|
458
|
+
throw new SpongeError('cancelled');
|
|
297
459
|
if (signal.aborted)
|
|
298
|
-
throw new
|
|
299
|
-
if (error instanceof
|
|
460
|
+
throw new SpongeError('limit-exceeded');
|
|
461
|
+
if (error instanceof SpongeError)
|
|
300
462
|
throw error;
|
|
301
|
-
throw new
|
|
463
|
+
throw new SpongeError('unavailable');
|
|
302
464
|
}
|
|
303
465
|
if (userSignal.aborted)
|
|
304
|
-
throw new
|
|
466
|
+
throw new SpongeError('cancelled');
|
|
305
467
|
if (typeof text !== 'string' || Buffer.byteLength(text) > p.maxOutputBytes)
|
|
306
|
-
throw new
|
|
468
|
+
throw new SpongeError('limit-exceeded');
|
|
307
469
|
return text;
|
|
308
470
|
};
|
|
309
|
-
const highlights = (input.highlights ?? [])
|
|
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
|
+
});
|
|
310
481
|
let shown = list, notesText;
|
|
311
482
|
const total = list.reduce((n, x) => n + display(x).length + 1, 0);
|
|
312
483
|
if (total > p.contextChars) {
|
|
313
484
|
// Long source: notes per section, then one combined pass over the cited and highlighted passages only.
|
|
314
485
|
const sections = sectionsOf(list, p.contextChars);
|
|
315
486
|
if (sections.length > p.maxSections)
|
|
316
|
-
throw new
|
|
487
|
+
throw new SpongeError('limit-exceeded');
|
|
317
488
|
trace.strategy = 'sections';
|
|
318
489
|
trace.sections = sections.length;
|
|
319
490
|
const notes = [];
|
|
320
491
|
for (const [k, section] of sections.entries()) {
|
|
321
492
|
const first = section[0].id, last = section[section.length - 1].id;
|
|
322
|
-
const text = await call([
|
|
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));
|
|
323
500
|
let v;
|
|
324
501
|
try {
|
|
325
502
|
v = JSON.parse(text);
|
|
@@ -329,11 +506,23 @@ export async function draftDigest(input) {
|
|
|
329
506
|
}
|
|
330
507
|
if (object(v) && Array.isArray(v.notes))
|
|
331
508
|
for (const n of v.notes.slice(0, 4))
|
|
332
|
-
if (object(n) &&
|
|
333
|
-
|
|
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
|
+
});
|
|
334
520
|
}
|
|
335
|
-
const wanted = new Set([
|
|
336
|
-
|
|
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');
|
|
337
526
|
shown = [];
|
|
338
527
|
let size = notesText.length;
|
|
339
528
|
for (const x of list)
|
|
@@ -342,21 +531,36 @@ export async function draftDigest(input) {
|
|
|
342
531
|
size += display(x).length + 1;
|
|
343
532
|
}
|
|
344
533
|
if (!shown.length)
|
|
345
|
-
throw new
|
|
534
|
+
throw new SpongeError('unavailable', { issues: [{ path: '', code: 'no-notes' }] });
|
|
346
535
|
}
|
|
347
536
|
const budget = quoteBudget(source), schema = draftSchema(list.length, input.vocabulary);
|
|
348
|
-
const messages = [
|
|
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
|
+
];
|
|
349
545
|
const cache = new Map(), options = { ...(input.vocabulary ? { tags: input.vocabulary } : {}) };
|
|
350
546
|
const embedKey = (t) => Buffer.from(foldText(t)).subarray(0, 900).toString();
|
|
351
547
|
if (input.embed)
|
|
352
|
-
Object.assign(options, {
|
|
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
|
+
});
|
|
353
555
|
const check = async (draft) => {
|
|
354
556
|
if (input.embed) {
|
|
355
|
-
const missing = [
|
|
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));
|
|
356
560
|
if (missing.length) {
|
|
357
561
|
const vectors = await input.embed(missing, overall);
|
|
358
562
|
if (!Array.isArray(vectors) || vectors.length !== missing.length)
|
|
359
|
-
throw new
|
|
563
|
+
throw new SpongeError('unavailable');
|
|
360
564
|
missing.forEach((t, i) => cache.set(t, vectors[i]));
|
|
361
565
|
}
|
|
362
566
|
}
|
|
@@ -369,7 +573,6 @@ export async function draftDigest(input) {
|
|
|
369
573
|
return { issues: draft.issues };
|
|
370
574
|
}
|
|
371
575
|
const quotes = snapQuotes(draft, source, list);
|
|
372
|
-
order(draft, source);
|
|
373
576
|
const issues = await check(draft);
|
|
374
577
|
trace.attempts.push({ issues, quotes });
|
|
375
578
|
return { draft, issues };
|
|
@@ -381,30 +584,59 @@ export async function draftDigest(input) {
|
|
|
381
584
|
result = await attempt(await call([...messages, { role: 'assistant', content: first }, { role: 'user', content: repairPrompt(result.issues) }], schema));
|
|
382
585
|
}
|
|
383
586
|
if (!result.draft)
|
|
384
|
-
throw new
|
|
587
|
+
throw new SpongeError('unavailable', { issues: result.issues });
|
|
385
588
|
let issues = result.issues;
|
|
386
589
|
if (issues.length) {
|
|
387
590
|
trace.dropped = prune(result.draft, issues, source, options);
|
|
388
591
|
issues = await check(result.draft);
|
|
389
592
|
if (result.draft.digest.ideas.length < L.minIdeas)
|
|
390
|
-
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' }];
|
|
391
594
|
if (issues.length)
|
|
392
|
-
throw new
|
|
595
|
+
throw new SpongeError('unavailable', { issues });
|
|
393
596
|
}
|
|
394
|
-
const producer = {
|
|
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
|
+
};
|
|
395
609
|
return { output: result.draft.digest, inputHash, producer, options, trace };
|
|
396
610
|
}
|
|
397
|
-
const formFor = (kind) => kind === 'pdf' ? 'pdf' : kind === 'media' ? 'video' : 'article';
|
|
611
|
+
const formFor = (kind) => (kind === 'pdf' ? 'pdf' : kind === 'media' ? 'video' : 'article');
|
|
398
612
|
/** Draft and store a digest for the item's current revision. A matching input hash returns the stored draft without calling the model. */
|
|
399
613
|
export async function createDigest(store, itemId, model, options = {}) {
|
|
400
614
|
const item = store.read(itemId);
|
|
401
615
|
if (!item)
|
|
402
|
-
throw new
|
|
403
|
-
const highlights = store.
|
|
404
|
-
|
|
405
|
-
|
|
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 }));
|
|
406
631
|
if (prior)
|
|
407
632
|
return { derivation: prior, duplicate: true };
|
|
408
|
-
const draft = await draftDigest(input), stored = store.
|
|
409
|
-
|
|
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 };
|
|
410
642
|
}
|