@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.
- package/README.md +21 -9
- 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 +38 -16
- 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 +9 -46
- package/dist/src/digest/editorial.js +57 -49
- package/dist/src/digest/endpoint.js +63 -27
- package/dist/src/digest/pipeline.d.ts +49 -28
- package/dist/src/digest/pipeline.js +343 -100
- package/dist/src/digest/schema.d.ts +11 -25
- package/dist/src/digest/schema.js +86 -18
- 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 +3 -6
- package/dist/src/importers/jungle-digest.js +196 -69
- package/dist/src/importers/jungle-store.d.ts +15 -6
- package/dist/src/importers/jungle-store.js +122 -28
- package/dist/src/importers/jungle.d.ts +14 -14
- package/dist/src/importers/jungle.js +136 -31
- 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 +106 -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 -79
- 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, parseDigest } from './schema.js';
|
|
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
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-
|
|
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,27 +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
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
|
|
124
|
-
- context: optional, at most ${L.contextWords} words
|
|
125
|
-
- ideas: ${L.minIdeas} to ${L.maxIdeas}, in the order the author makes them. Each idea is a
|
|
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
|
|
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 = {
|
|
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
|
|
149
|
-
|
|
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 {
|
|
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({
|
|
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) => {
|
|
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
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
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) => {
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
220
|
-
|
|
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 => {
|
|
223
|
-
|
|
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)]
|
|
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) => ({
|
|
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) ||
|
|
247
|
-
|
|
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
|
|
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({
|
|
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
|
-
|
|
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
|
|
266
|
-
if (input.vocabulary &&
|
|
267
|
-
|
|
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
|
|
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 = {
|
|
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
|
|
441
|
+
throw new SpongeError('cancelled');
|
|
276
442
|
if (overall.aborted)
|
|
277
|
-
throw new
|
|
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, {
|
|
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
|
|
458
|
+
throw new SpongeError('cancelled');
|
|
287
459
|
if (signal.aborted)
|
|
288
|
-
throw new
|
|
289
|
-
if (error instanceof
|
|
460
|
+
throw new SpongeError('limit-exceeded');
|
|
461
|
+
if (error instanceof SpongeError)
|
|
290
462
|
throw error;
|
|
291
|
-
throw new
|
|
463
|
+
throw new SpongeError('unavailable');
|
|
292
464
|
}
|
|
293
465
|
if (userSignal.aborted)
|
|
294
|
-
throw new
|
|
466
|
+
throw new SpongeError('cancelled');
|
|
295
467
|
if (typeof text !== 'string' || Buffer.byteLength(text) > p.maxOutputBytes)
|
|
296
|
-
throw new
|
|
468
|
+
throw new SpongeError('limit-exceeded');
|
|
297
469
|
return text;
|
|
298
470
|
};
|
|
299
|
-
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
|
+
});
|
|
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
|
|
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([
|
|
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) &&
|
|
323
|
-
|
|
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([
|
|
326
|
-
|
|
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
|
|
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 = [
|
|
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, {
|
|
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 = [
|
|
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
|
|
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
|
|
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
|
|
595
|
+
throw new SpongeError('unavailable', { issues });
|
|
382
596
|
}
|
|
383
|
-
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
|
+
};
|
|
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
|
|
392
|
-
const highlights = store.
|
|
393
|
-
|
|
394
|
-
|
|
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.
|
|
398
|
-
|
|
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
|
}
|