@hraness/sponge 0.2.6 → 0.3.1
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 +369 -102
- 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 +19 -6
- package/dist/src/importers/jungle-store.js +124 -28
- package/dist/src/importers/jungle.d.ts +14 -14
- package/dist/src/importers/jungle.js +142 -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-v5';
|
|
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,28 +118,95 @@ 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', 'context', '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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Style examples are invented sources shaped like reviewed digests (short verdict claims, dense details); no library
|
|
182
|
+
* text. They show voice only: the model must still draw every claim and number from the source it is given.
|
|
183
|
+
*/
|
|
184
|
+
const styleExamples = `Style examples (invented sources; copy the voice, never the content):
|
|
185
|
+
Source: a database company's post on moving its write path to object storage.
|
|
186
|
+
lead: Acme moves Ledger's write path onto an append-only log in S3 and cuts p99 commit latency from 40 ms to 9 ms.
|
|
187
|
+
context: Postgres becomes a read replica rebuilt from the log. Batching makes object storage affordable; single-row workloads still get slower.
|
|
188
|
+
claim: The log is the database. | detail: Every write appends one immutable segment; Postgres is rebuilt from the log and serves reads only.
|
|
189
|
+
claim: Batching pays for the bucket. | detail: Commits wait up to 5 ms, so one PUT carries about 300 writes and request cost falls 40-fold.
|
|
190
|
+
claim: Small writers lose. | detail: Under ten writes per second, latency is higher than before the move.
|
|
191
|
+
Source: an essay arguing that taste is a skill.
|
|
192
|
+
lead: Mara Lind argues taste is trained by making and comparing many small things, not inherited, and that teams can schedule it.
|
|
193
|
+
claim: Taste is trained. | detail: Lind's students improved most after drawing fifty quick versions, not after studying masters.
|
|
194
|
+
claim: Volume beats polish early. | detail: Side-by-side comparison of many rough drafts sharpens judgment faster than finishing one.
|
|
195
|
+
Bad claim: The essay explores how practice over time shapes a person's sense of taste.
|
|
196
|
+
Good claim: Taste is trained.`;
|
|
197
|
+
const system = (budget, vocabulary) => `You write a Digest v2 of one source for a private reading library, in the owner's voice: terse, declarative, specific. Reply with JSON only, matching the schema.
|
|
198
|
+
- lead: one sentence of at most ${L.leadChars} characters stating this source's specific thesis or result: who (author, organization or product by name) shows or argues what, with the number, name or mechanism that makes it this source. Do not restate the title, describe the source ("This article discusses"), or tease.
|
|
199
|
+
- context: 1 or 2 sentences, at most 45 words: the evidence, method, numbers or limits the lead could not hold. Lead and context together stay within ${L.gistWords} words and ${L.gistSentences} sentences.
|
|
200
|
+
- 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:
|
|
201
|
+
- claim: a verdict of 4 to 8 words (never more than 12): subject, verb, point, naming this source's own subject ("Batching pays for the bucket", not "Costs drop"). Something a reader could disagree with; never a topic label or a description of the source.
|
|
202
|
+
- detail: one or two sentences, 15 to 30 words, dense with the source's own names, numbers, mechanism or limits behind the claim. Do not repeat the claim.
|
|
203
|
+
- 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.
|
|
204
|
+
- 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.
|
|
205
|
+
- tags: 0 to ${L.maxTags} ${vocabulary?.length ? `from: ${vocabulary.join(', ')}` : 'lowercase kebab-case topics'}; usually one subject tag.
|
|
206
|
+
Plain words. Name things and use numbers. No hedges or filler: may, might, potentially, various, significantly, key, crucial, notably, highlights, explores, discusses, "the author argues", "the article".
|
|
207
|
+
Use only what the source says.
|
|
208
|
+
|
|
209
|
+
${styleExamples}`;
|
|
132
210
|
function sourceBlock(title, form, list, highlights) {
|
|
133
211
|
let heading;
|
|
134
212
|
const lines = [];
|
|
@@ -143,10 +221,26 @@ function sourceBlock(title, form, list, highlights) {
|
|
|
143
221
|
const marks = highlights.map((h, i) => `[h${i + 1}${h.passage ? `, passage ${h.passage}` : ''}] ${collapse(h.text).slice(0, 400)}`);
|
|
144
222
|
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
223
|
}
|
|
146
|
-
const rule = {
|
|
224
|
+
const rule = {
|
|
225
|
+
gist: `lead and context within ${L.gistWords} words and ${L.gistSentences} sentences`,
|
|
226
|
+
lead: `one sentence, at most ${L.leadChars} characters`,
|
|
227
|
+
context: `at most ${L.contextWords} words`,
|
|
228
|
+
claim: `one distinct sentence, at most ${L.claimWords} words`,
|
|
229
|
+
detail: `at most ${L.detailWords} words and ${L.detailSentences} sentences`,
|
|
230
|
+
quotes: `0-${L.maxQuotesPerIdea} quotes per idea, each copied exactly from its passage, ${minQuoteWords} to ${L.quoteWords} words, not overlapping another quote`,
|
|
231
|
+
attribution: `at most ${L.attributionWords} words`,
|
|
232
|
+
ideas: `${L.minIdeas}-${L.maxIdeas} ideas; ${L.minQuotes}-${L.maxQuotes} quotes in all, within the total budget`,
|
|
233
|
+
tags: `0-${L.maxTags} distinct allowed tags`,
|
|
234
|
+
};
|
|
147
235
|
function repairPrompt(issues) {
|
|
148
|
-
const lines = issues.slice(0, 40).map(i
|
|
149
|
-
|
|
236
|
+
const lines = issues.slice(0, 40).map((i) => {
|
|
237
|
+
const last = i.path
|
|
238
|
+
.split('.')
|
|
239
|
+
.filter((s) => !/^\d+$/.test(s))
|
|
240
|
+
.pop() ?? '';
|
|
241
|
+
return `- ${i.path || 'output'}: ${i.code}${rule[last] ? ` (rule: ${rule[last]})` : ''}`;
|
|
242
|
+
});
|
|
243
|
+
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
244
|
}
|
|
151
245
|
/** Map the model's draft shape into Digest v2; parseDigest then decides whether the shape is valid. Ideas keep the model's order. */
|
|
152
246
|
function fromModel(text, form) {
|
|
@@ -159,18 +253,31 @@ function fromModel(text, form) {
|
|
|
159
253
|
}
|
|
160
254
|
if (!object(v) || !Array.isArray(v.ideas) || !Array.isArray(v.tags))
|
|
161
255
|
return { issues: [{ path: '', code: 'shape' }] };
|
|
162
|
-
const trim = (s) => typeof s === 'string' ? s.trim() : s, cites = [];
|
|
256
|
+
const trim = (s) => (typeof s === 'string' ? s.trim() : s), cites = [];
|
|
163
257
|
const ideas = v.ideas.map((idea) => {
|
|
164
258
|
if (!object(idea)) {
|
|
165
259
|
cites.push([]);
|
|
166
260
|
return idea;
|
|
167
261
|
}
|
|
168
262
|
const quotes = Array.isArray(idea.quotes) ? idea.quotes : [];
|
|
169
|
-
cites.push(quotes.map((q) => Number.isSafeInteger(q?.passage) ? q.passage : undefined));
|
|
170
|
-
return {
|
|
263
|
+
cites.push(quotes.map((q) => (Number.isSafeInteger(q?.passage) ? q.passage : undefined)));
|
|
264
|
+
return {
|
|
265
|
+
claim: trim(idea.claim),
|
|
266
|
+
detail: trim(idea.detail),
|
|
267
|
+
quotes: quotes.map((q) => {
|
|
268
|
+
const attribution = trim(q?.attribution);
|
|
269
|
+
return { text: trim(q?.text), ...(attribution ? { attribution } : {}) };
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
171
272
|
});
|
|
172
273
|
const context = trim(v.context);
|
|
173
|
-
const parsed = parseDigest({
|
|
274
|
+
const parsed = parseDigest({
|
|
275
|
+
schemaVersion: 2,
|
|
276
|
+
form,
|
|
277
|
+
gist: { lead: trim(v.lead), ...(context ? { context } : {}) },
|
|
278
|
+
ideas,
|
|
279
|
+
tags: v.tags.map(trim),
|
|
280
|
+
});
|
|
174
281
|
return parsed.digest?.schemaVersion === 2 ? { digest: parsed.digest, cites } : { issues: parsed.issues };
|
|
175
282
|
}
|
|
176
283
|
/**
|
|
@@ -181,7 +288,10 @@ export function snapQuote(source, text, within) {
|
|
|
181
288
|
if (source.includes(text))
|
|
182
289
|
return { text, match: 'exact' };
|
|
183
290
|
const inside = (m) => !!within && m.start >= within.start && m.end <= within.end;
|
|
184
|
-
const pick = (all) => {
|
|
291
|
+
const pick = (all) => {
|
|
292
|
+
const local = all.filter(inside);
|
|
293
|
+
return local.length === 1 ? local[0] : all.length === 1 ? all[0] : undefined;
|
|
294
|
+
};
|
|
185
295
|
const folded = locateAll(source, text);
|
|
186
296
|
if (folded.length) {
|
|
187
297
|
const m = pick(folded);
|
|
@@ -198,13 +308,39 @@ export function snapQuote(source, text, within) {
|
|
|
198
308
|
end++;
|
|
199
309
|
return { text: source.slice(start, end), match: 'snapped' };
|
|
200
310
|
}
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
311
|
+
/**
|
|
312
|
+
* A located quote over the word limit keeps its leading words: a prefix of a source span is still verbatim. Trailing
|
|
313
|
+
* clause punctuation goes; unlocated quotes are left for the checker.
|
|
314
|
+
*/
|
|
315
|
+
export function trimQuote(text, max = L.quoteWords) {
|
|
316
|
+
if (words(text) <= max)
|
|
317
|
+
return text;
|
|
318
|
+
const ends = [...text.matchAll(/\S+/g)].map((m) => m.index + m[0].length);
|
|
319
|
+
let n = Math.min(max, ends.length), out = text;
|
|
320
|
+
do {
|
|
321
|
+
out = text.slice(0, ends[n - 1]).replace(/[\s,;:(\[\u2014\u2013-]+$/, '');
|
|
322
|
+
n--;
|
|
323
|
+
} while (n > 0 && words(out) > max);
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
/** Snap every quote in place, trim located quotes over the word limit, and drop quotes under `minQuoteWords`. */
|
|
327
|
+
function snapQuotes(draft, source, list) {
|
|
328
|
+
const stats = { exact: 0, folded: 0, snapped: 0, missing: 0, short: 0, trimmed: 0 }, byId = new Map(list.map((p) => [p.id, p]));
|
|
204
329
|
draft.digest.ideas.forEach((idea, i) => {
|
|
205
|
-
idea.quotes.forEach((q, j) => {
|
|
206
|
-
|
|
207
|
-
|
|
330
|
+
idea.quotes.forEach((q, j) => {
|
|
331
|
+
const r = snapQuote(source, q.text, byId.get(draft.cites[i]?.[j] ?? 0));
|
|
332
|
+
q.text = r.text;
|
|
333
|
+
stats[r.match]++;
|
|
334
|
+
if (r.match !== 'missing') {
|
|
335
|
+
const t = trimQuote(q.text);
|
|
336
|
+
if (t !== q.text) {
|
|
337
|
+
q.text = t;
|
|
338
|
+
stats.trimmed++;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
const keep = idea.quotes.map((q) => words(q.text) >= minQuoteWords);
|
|
343
|
+
stats.short += keep.filter((k) => !k).length;
|
|
208
344
|
idea.quotes = idea.quotes.filter((_, j) => keep[j]);
|
|
209
345
|
draft.cites[i] = (draft.cites[i] ?? []).filter((_, j) => keep[j]);
|
|
210
346
|
});
|
|
@@ -216,11 +352,28 @@ const pruneable = new Set(['not-verbatim', 'overlap', 'unsupported', 'too-long',
|
|
|
216
352
|
* budget (second quotes first, longest first), never the last quote. Quoteless ideas stay: v2 allows them.
|
|
217
353
|
*/
|
|
218
354
|
function prune(draft, issues, source, options) {
|
|
219
|
-
const dropped = { quotes: 0, ideas: 0, tags: 0 }, d = draft.digest
|
|
220
|
-
|
|
355
|
+
const dropped = { quotes: 0, ideas: 0, tags: 0 }, d = draft.digest;
|
|
356
|
+
// Context is optional in Digest v2: an over-long gist loses its context rather than the draft.
|
|
357
|
+
if (d.gist.context !== undefined && issues.some((i) => i.path === 'gist' || i.path === 'gist.context'))
|
|
358
|
+
delete d.gist.context;
|
|
359
|
+
const bad = new Set(issues
|
|
360
|
+
.filter((i) => /^ideas\.\d+\.quotes\.\d+(?:\.attribution)?$/.test(i.path) && pruneable.has(i.code))
|
|
361
|
+
.map((i) => i.path.replace(/\.attribution$/, '')));
|
|
362
|
+
d.ideas.forEach((idea, i) => {
|
|
363
|
+
const keep = idea.quotes
|
|
364
|
+
.map((_, j) => !bad.has(`ideas.${i}.quotes.${j}`))
|
|
365
|
+
.map((k, j) => k && j < L.maxQuotesPerIdea);
|
|
366
|
+
dropped.quotes += keep.filter((k) => !k).length;
|
|
367
|
+
idea.quotes = idea.quotes.filter((_, j) => keep[j]);
|
|
368
|
+
draft.cites[i] = (draft.cites[i] ?? []).filter((_, j) => keep[j]);
|
|
369
|
+
});
|
|
221
370
|
const seen = new Set();
|
|
222
|
-
const keepIdea = d.ideas.map(idea => {
|
|
223
|
-
|
|
371
|
+
const keepIdea = d.ideas.map((idea) => {
|
|
372
|
+
const key = foldText(idea.claim).toLowerCase(), ok = !seen.has(key);
|
|
373
|
+
seen.add(key);
|
|
374
|
+
return ok;
|
|
375
|
+
});
|
|
376
|
+
dropped.ideas = keepIdea.filter((k) => !k).length;
|
|
224
377
|
d.ideas = d.ideas.filter((_, i) => keepIdea[i]);
|
|
225
378
|
draft.cites = draft.cites.filter((_, i) => keepIdea[i]);
|
|
226
379
|
if (d.ideas.length > L.maxIdeas) {
|
|
@@ -228,7 +381,9 @@ function prune(draft, issues, source, options) {
|
|
|
228
381
|
d.ideas = d.ideas.slice(0, L.maxIdeas);
|
|
229
382
|
draft.cites = draft.cites.slice(0, L.maxIdeas);
|
|
230
383
|
}
|
|
231
|
-
const tags = [...new Set(d.tags)]
|
|
384
|
+
const tags = [...new Set(d.tags)]
|
|
385
|
+
.filter((t) => (options.tags ? options.tags.includes(t) : /^[a-z0-9][a-z0-9-]{0,63}$/.test(t)))
|
|
386
|
+
.slice(0, L.maxTags);
|
|
232
387
|
dropped.tags = d.tags.length - tags.length;
|
|
233
388
|
d.tags = tags;
|
|
234
389
|
const budget = quoteBudget(source), all = () => d.ideas.flatMap((idea, i) => idea.quotes.map((q, j) => ({ i, j, n: words(q.text) })));
|
|
@@ -240,76 +395,132 @@ function prune(draft, issues, source, options) {
|
|
|
240
395
|
}
|
|
241
396
|
return dropped;
|
|
242
397
|
}
|
|
243
|
-
const hashedParams = (p) => ({
|
|
398
|
+
const hashedParams = (p) => ({
|
|
399
|
+
temperature: p.temperature,
|
|
400
|
+
seed: p.seed,
|
|
401
|
+
maxTokens: p.maxTokens,
|
|
402
|
+
contextChars: p.contextChars,
|
|
403
|
+
maxSections: p.maxSections,
|
|
404
|
+
});
|
|
244
405
|
function resolveParams(params = {}) {
|
|
245
406
|
const p = { ...defaultDigestParams, ...params }, int = (v, min, max) => Number.isSafeInteger(v) && v >= min && v <= max;
|
|
246
|
-
if (!(p.temperature >= 0 && p.temperature <= 2) ||
|
|
247
|
-
|
|
407
|
+
if (!(p.temperature >= 0 && p.temperature <= 2) ||
|
|
408
|
+
!int(p.seed, 0, 2 ** 31) ||
|
|
409
|
+
!int(p.maxTokens, 256, 32768) ||
|
|
410
|
+
!int(p.contextChars, 2000, 1_000_000) ||
|
|
411
|
+
!int(p.maxSections, 1, 256) ||
|
|
412
|
+
!int(p.timeoutMs, 1, 3_600_000) ||
|
|
413
|
+
!int(p.deadlineMs, 1, 14_400_000) ||
|
|
414
|
+
!int(p.maxOutputBytes, 1024, 4 * 1024 * 1024))
|
|
415
|
+
throw new SpongeError('invalid-input');
|
|
248
416
|
return p;
|
|
249
417
|
}
|
|
250
418
|
function validIdentity(id) {
|
|
251
419
|
const ok = (v) => typeof v === 'string' && v.length > 0 && v.length <= 256 && !/[\u0000-\u001f\u007f]/.test(v);
|
|
252
420
|
if (!object(id) || !ok(id.model) || !ok(id.quantization) || !ok(id.runtime))
|
|
253
|
-
throw new
|
|
421
|
+
throw new SpongeError('invalid-input');
|
|
254
422
|
return { model: id.model, quantization: id.quantization, runtime: id.runtime };
|
|
255
423
|
}
|
|
256
424
|
/** sha256 over canonical JSON of the revision, its Markdown, the prompt version, the model identity and every output-shaping input. */
|
|
257
425
|
export function digestInputHash(input) {
|
|
258
|
-
return sha256(canonicalJson({
|
|
426
|
+
return sha256(canonicalJson({
|
|
427
|
+
revisionId: input.revisionId,
|
|
428
|
+
markdownSha256: sha256(input.markdown),
|
|
429
|
+
promptVersion: PROMPT_VERSION,
|
|
430
|
+
model: validIdentity(input.model),
|
|
431
|
+
params: {
|
|
432
|
+
...hashedParams(resolveParams(input.params)),
|
|
433
|
+
form: input.form,
|
|
434
|
+
vocabulary: input.vocabulary ? [...input.vocabulary] : null,
|
|
435
|
+
highlights: (input.highlights ?? []).map((h) => h.text),
|
|
436
|
+
support: input.embed ? (input.supportThreshold ?? defaultSupportThreshold) : null,
|
|
437
|
+
},
|
|
438
|
+
}));
|
|
259
439
|
}
|
|
260
440
|
/** Unqualified on real claims: a deliberately low bar that only catches quotes unrelated to their claim. */
|
|
261
|
-
|
|
441
|
+
const defaultSupportThreshold = 0.35;
|
|
262
442
|
export async function draftDigest(input) {
|
|
263
443
|
const p = resolveParams(input.params), identity = validIdentity(input.model.identity);
|
|
264
444
|
if (typeof input.markdown !== 'string' || !input.markdown.trim() || !forms.includes(input.form))
|
|
265
|
-
throw new
|
|
266
|
-
if (input.vocabulary &&
|
|
267
|
-
|
|
445
|
+
throw new SpongeError('invalid-input');
|
|
446
|
+
if (input.vocabulary &&
|
|
447
|
+
(!input.vocabulary.length ||
|
|
448
|
+
input.vocabulary.length > 256 ||
|
|
449
|
+
input.vocabulary.some((t) => !/^[a-z0-9][a-z0-9-]{0,63}$/.test(t))))
|
|
450
|
+
throw new SpongeError('invalid-input');
|
|
268
451
|
const inputHash = digestInputHash({ ...input, model: identity }), source = input.markdown, list = passages(source);
|
|
269
452
|
if (list.length === 0)
|
|
270
|
-
throw new
|
|
453
|
+
throw new SpongeError('invalid-input');
|
|
271
454
|
const userSignal = input.signal ?? new AbortController().signal, overall = AbortSignal.any([userSignal, AbortSignal.timeout(p.deadlineMs)]);
|
|
272
|
-
const trace = {
|
|
455
|
+
const trace = {
|
|
456
|
+
strategy: 'single',
|
|
457
|
+
sections: 1,
|
|
458
|
+
calls: 0,
|
|
459
|
+
repaired: false,
|
|
460
|
+
attempts: [],
|
|
461
|
+
dropped: { quotes: 0, ideas: 0, tags: 0 },
|
|
462
|
+
};
|
|
273
463
|
const call = async (messages, schema) => {
|
|
274
464
|
if (userSignal.aborted)
|
|
275
|
-
throw new
|
|
465
|
+
throw new SpongeError('cancelled');
|
|
276
466
|
if (overall.aborted)
|
|
277
|
-
throw new
|
|
467
|
+
throw new SpongeError('limit-exceeded');
|
|
278
468
|
const signal = AbortSignal.any([overall, AbortSignal.timeout(p.timeoutMs)]);
|
|
279
469
|
trace.calls++;
|
|
280
470
|
let text;
|
|
281
471
|
try {
|
|
282
|
-
text = await input.model.generate(messages, schema, {
|
|
472
|
+
text = await input.model.generate(messages, schema, {
|
|
473
|
+
signal,
|
|
474
|
+
maxOutputBytes: p.maxOutputBytes,
|
|
475
|
+
maxTokens: p.maxTokens,
|
|
476
|
+
temperature: p.temperature,
|
|
477
|
+
seed: p.seed,
|
|
478
|
+
});
|
|
283
479
|
}
|
|
284
480
|
catch (error) {
|
|
285
481
|
if (userSignal.aborted)
|
|
286
|
-
throw new
|
|
482
|
+
throw new SpongeError('cancelled');
|
|
287
483
|
if (signal.aborted)
|
|
288
|
-
throw new
|
|
289
|
-
if (error instanceof
|
|
484
|
+
throw new SpongeError('limit-exceeded');
|
|
485
|
+
if (error instanceof SpongeError)
|
|
290
486
|
throw error;
|
|
291
|
-
throw new
|
|
487
|
+
throw new SpongeError('unavailable');
|
|
292
488
|
}
|
|
293
489
|
if (userSignal.aborted)
|
|
294
|
-
throw new
|
|
490
|
+
throw new SpongeError('cancelled');
|
|
295
491
|
if (typeof text !== 'string' || Buffer.byteLength(text) > p.maxOutputBytes)
|
|
296
|
-
throw new
|
|
492
|
+
throw new SpongeError('limit-exceeded');
|
|
297
493
|
return text;
|
|
298
494
|
};
|
|
299
|
-
const highlights = (input.highlights ?? [])
|
|
495
|
+
const highlights = (input.highlights ?? [])
|
|
496
|
+
.filter((h) => typeof h?.text === 'string' && h.text.trim())
|
|
497
|
+
.slice(0, 32)
|
|
498
|
+
.map((h) => {
|
|
499
|
+
const at = h.start ?? locateAll(source, h.text)[0]?.start;
|
|
500
|
+
return {
|
|
501
|
+
text: h.text,
|
|
502
|
+
passage: at === undefined ? undefined : list.find((x) => at >= x.start && at < x.end)?.id,
|
|
503
|
+
};
|
|
504
|
+
});
|
|
300
505
|
let shown = list, notesText;
|
|
301
506
|
const total = list.reduce((n, x) => n + display(x).length + 1, 0);
|
|
302
507
|
if (total > p.contextChars) {
|
|
303
508
|
// Long source: notes per section, then one combined pass over the cited and highlighted passages only.
|
|
304
509
|
const sections = sectionsOf(list, p.contextChars);
|
|
305
510
|
if (sections.length > p.maxSections)
|
|
306
|
-
throw new
|
|
511
|
+
throw new SpongeError('limit-exceeded');
|
|
307
512
|
trace.strategy = 'sections';
|
|
308
513
|
trace.sections = sections.length;
|
|
309
514
|
const notes = [];
|
|
310
515
|
for (const [k, section] of sections.entries()) {
|
|
311
516
|
const first = section[0].id, last = section[section.length - 1].id;
|
|
312
|
-
const text = await call([
|
|
517
|
+
const text = await call([
|
|
518
|
+
{
|
|
519
|
+
role: 'system',
|
|
520
|
+
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.`,
|
|
521
|
+
},
|
|
522
|
+
{ role: 'user', content: sourceBlock(input.title, input.form, section, []) },
|
|
523
|
+
], notesSchema(first, last));
|
|
313
524
|
let v;
|
|
314
525
|
try {
|
|
315
526
|
v = JSON.parse(text);
|
|
@@ -319,11 +530,23 @@ export async function draftDigest(input) {
|
|
|
319
530
|
}
|
|
320
531
|
if (object(v) && Array.isArray(v.notes))
|
|
321
532
|
for (const n of v.notes.slice(0, 4))
|
|
322
|
-
if (object(n) &&
|
|
323
|
-
|
|
533
|
+
if (object(n) &&
|
|
534
|
+
typeof n.point === 'string' &&
|
|
535
|
+
Number.isSafeInteger(n.passage) &&
|
|
536
|
+
n.passage >= first &&
|
|
537
|
+
n.passage <= last &&
|
|
538
|
+
typeof n.quote === 'string')
|
|
539
|
+
notes.push({
|
|
540
|
+
point: collapse(n.point).slice(0, 300),
|
|
541
|
+
passage: n.passage,
|
|
542
|
+
quote: collapse(n.quote).slice(0, 300),
|
|
543
|
+
});
|
|
324
544
|
}
|
|
325
|
-
const wanted = new Set([
|
|
326
|
-
|
|
545
|
+
const wanted = new Set([
|
|
546
|
+
...notes.map((n) => n.passage),
|
|
547
|
+
...highlights.flatMap((h) => (h.passage ? [h.passage] : [])),
|
|
548
|
+
]);
|
|
549
|
+
notesText = notes.map((n) => `- [${n.passage}] ${n.point}`).join('\n');
|
|
327
550
|
shown = [];
|
|
328
551
|
let size = notesText.length;
|
|
329
552
|
for (const x of list)
|
|
@@ -332,21 +555,36 @@ export async function draftDigest(input) {
|
|
|
332
555
|
size += display(x).length + 1;
|
|
333
556
|
}
|
|
334
557
|
if (!shown.length)
|
|
335
|
-
throw new
|
|
558
|
+
throw new SpongeError('unavailable', { issues: [{ path: '', code: 'no-notes' }] });
|
|
336
559
|
}
|
|
337
560
|
const budget = quoteBudget(source), schema = draftSchema(list.length, input.vocabulary);
|
|
338
|
-
const messages = [
|
|
561
|
+
const messages = [
|
|
562
|
+
{ role: 'system', content: system(budget, input.vocabulary) },
|
|
563
|
+
{
|
|
564
|
+
role: 'user',
|
|
565
|
+
content: (notesText ? `Notes from each part of the source, in order:\n${notesText}\n\n` : '') +
|
|
566
|
+
sourceBlock(input.title, input.form, shown, highlights),
|
|
567
|
+
},
|
|
568
|
+
];
|
|
339
569
|
const cache = new Map(), options = { ...(input.vocabulary ? { tags: input.vocabulary } : {}) };
|
|
340
570
|
const embedKey = (t) => Buffer.from(foldText(t)).subarray(0, 900).toString();
|
|
341
571
|
if (input.embed)
|
|
342
|
-
Object.assign(options, {
|
|
572
|
+
Object.assign(options, {
|
|
573
|
+
supportThreshold: input.supportThreshold ?? defaultSupportThreshold,
|
|
574
|
+
supports: (claim, quote) => {
|
|
575
|
+
const a = cache.get(embedKey(claim)), b = cache.get(embedKey(quote));
|
|
576
|
+
return a && b ? a.reduce((n, x, i) => n + x * b[i], 0) : 0;
|
|
577
|
+
},
|
|
578
|
+
});
|
|
343
579
|
const check = async (draft) => {
|
|
344
580
|
if (input.embed) {
|
|
345
|
-
const missing = [
|
|
581
|
+
const missing = [
|
|
582
|
+
...new Set(draft.digest.ideas.flatMap((i) => [i.claim, ...i.quotes.map((q) => q.text)]).map(embedKey)),
|
|
583
|
+
].filter((t) => !cache.has(t));
|
|
346
584
|
if (missing.length) {
|
|
347
585
|
const vectors = await input.embed(missing, overall);
|
|
348
586
|
if (!Array.isArray(vectors) || vectors.length !== missing.length)
|
|
349
|
-
throw new
|
|
587
|
+
throw new SpongeError('unavailable');
|
|
350
588
|
missing.forEach((t, i) => cache.set(t, vectors[i]));
|
|
351
589
|
}
|
|
352
590
|
}
|
|
@@ -370,30 +608,59 @@ export async function draftDigest(input) {
|
|
|
370
608
|
result = await attempt(await call([...messages, { role: 'assistant', content: first }, { role: 'user', content: repairPrompt(result.issues) }], schema));
|
|
371
609
|
}
|
|
372
610
|
if (!result.draft)
|
|
373
|
-
throw new
|
|
611
|
+
throw new SpongeError('unavailable', { issues: result.issues });
|
|
374
612
|
let issues = result.issues;
|
|
375
613
|
if (issues.length) {
|
|
376
614
|
trace.dropped = prune(result.draft, issues, source, options);
|
|
377
615
|
issues = await check(result.draft);
|
|
378
616
|
if (result.draft.digest.ideas.length < L.minIdeas)
|
|
379
|
-
issues = [...issues.filter(i => !(i.path === 'ideas' && i.code === 'count')), { path: 'ideas', code: 'count' }];
|
|
617
|
+
issues = [...issues.filter((i) => !(i.path === 'ideas' && i.code === 'count')), { path: 'ideas', code: 'count' }];
|
|
380
618
|
if (issues.length)
|
|
381
|
-
throw new
|
|
619
|
+
throw new SpongeError('unavailable', { issues });
|
|
382
620
|
}
|
|
383
|
-
const producer = {
|
|
621
|
+
const producer = {
|
|
622
|
+
pipeline: 'sponge-digest',
|
|
623
|
+
promptVersion: PROMPT_VERSION,
|
|
624
|
+
model: identity,
|
|
625
|
+
params: hashedParams(p),
|
|
626
|
+
strategy: trace.strategy,
|
|
627
|
+
sections: trace.sections,
|
|
628
|
+
calls: trace.calls,
|
|
629
|
+
repaired: trace.repaired,
|
|
630
|
+
dropped: trace.dropped,
|
|
631
|
+
...(input.embed ? { supportThreshold: options.supportThreshold } : {}),
|
|
632
|
+
};
|
|
384
633
|
return { output: result.draft.digest, inputHash, producer, options, trace };
|
|
385
634
|
}
|
|
386
|
-
const formFor = (kind) => kind === 'pdf' ? 'pdf' : kind === 'media' ? 'video' : 'article';
|
|
635
|
+
const formFor = (kind) => (kind === 'pdf' ? 'pdf' : kind === 'media' ? 'video' : 'article');
|
|
387
636
|
/** Draft and store a digest for the item's current revision. A matching input hash returns the stored draft without calling the model. */
|
|
388
637
|
export async function createDigest(store, itemId, model, options = {}) {
|
|
389
638
|
const item = store.read(itemId);
|
|
390
639
|
if (!item)
|
|
391
|
-
throw new
|
|
392
|
-
const highlights = store.
|
|
393
|
-
|
|
394
|
-
|
|
640
|
+
throw new SpongeError('unavailable');
|
|
641
|
+
const highlights = store.annotations
|
|
642
|
+
.list(itemId)
|
|
643
|
+
.filter((a) => a.kind === 'highlight' && a.revisionId === item.revisionId && a.anchored !== false)
|
|
644
|
+
.map((a) => ({ text: a.anchor.exact, start: a.anchor.start }));
|
|
645
|
+
const input = {
|
|
646
|
+
...options,
|
|
647
|
+
revisionId: item.revisionId,
|
|
648
|
+
markdown: item.markdown,
|
|
649
|
+
title: item.title,
|
|
650
|
+
form: formFor(item.kind),
|
|
651
|
+
model,
|
|
652
|
+
highlights,
|
|
653
|
+
};
|
|
654
|
+
const prior = store.derivations.find('digest', item.revisionId, digestInputHash({ ...input, model: model.identity }));
|
|
395
655
|
if (prior)
|
|
396
656
|
return { derivation: prior, duplicate: true };
|
|
397
|
-
const draft = await draftDigest(input), stored = store.
|
|
398
|
-
|
|
657
|
+
const draft = await draftDigest(input), stored = store.derivations.put('digest', {
|
|
658
|
+
itemId,
|
|
659
|
+
revisionId: item.revisionId,
|
|
660
|
+
producer: draft.producer,
|
|
661
|
+
inputHash: draft.inputHash,
|
|
662
|
+
output: draft.output,
|
|
663
|
+
options: draft.options,
|
|
664
|
+
});
|
|
665
|
+
return { derivation: store.derivations.read('digest', stored.id), duplicate: stored.duplicate, trace: draft.trace };
|
|
399
666
|
}
|