@hraness/sponge 0.2.3 → 0.2.5
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 +53 -0
- package/dist/migrations/005.d.ts +3 -0
- package/dist/migrations/005.js +19 -0
- package/dist/migrations/006.d.ts +3 -0
- package/dist/migrations/006.js +9 -0
- package/dist/migrations/007.d.ts +3 -0
- package/dist/migrations/007.js +10 -0
- package/dist/src/acquisition/lock.d.ts +1 -1
- package/dist/src/acquisition/lock.js +1 -1
- package/dist/src/acquisition/pins.js +3 -3
- package/dist/src/annotations/quote.d.ts +32 -0
- package/dist/src/annotations/quote.js +135 -0
- package/dist/src/annotations/store.d.ts +50 -0
- package/dist/src/annotations/store.js +89 -0
- package/dist/src/app/source.d.ts +2 -0
- package/dist/src/app/source.js +13 -17
- package/dist/src/cli/arguments.js +24 -7
- package/dist/src/cli/digest.d.ts +10 -0
- package/dist/src/cli/digest.js +63 -0
- package/dist/src/cli/import.d.ts +11 -0
- package/dist/src/cli/import.js +10 -0
- package/dist/src/cli/indexing.d.ts +3 -0
- package/dist/src/cli/indexing.js +5 -2
- package/dist/src/cli/main.js +94 -8
- package/dist/src/cli/runtimes.d.ts +10 -0
- package/dist/src/cli/runtimes.js +32 -0
- package/dist/src/digest/editorial.d.ts +53 -0
- package/dist/src/digest/editorial.js +81 -0
- package/dist/src/digest/endpoint.d.ts +20 -0
- package/dist/src/digest/endpoint.js +103 -0
- package/dist/src/digest/pipeline.d.ts +148 -0
- package/dist/src/digest/pipeline.js +410 -0
- package/dist/src/digest/schema.d.ts +85 -0
- package/dist/src/digest/schema.js +118 -0
- package/dist/src/digest/store.d.ts +57 -0
- package/dist/src/digest/store.js +77 -0
- package/dist/src/identity/query-policy.d.ts +21 -0
- package/dist/src/identity/query-policy.js +55 -0
- package/dist/src/identity/uri.d.ts +30 -0
- package/dist/src/identity/uri.js +94 -0
- package/dist/src/identity/visibility.d.ts +24 -0
- package/dist/src/identity/visibility.js +75 -0
- package/dist/src/importers/apply.d.ts +10 -0
- package/dist/src/importers/apply.js +25 -22
- package/dist/src/importers/jungle-digest.d.ts +97 -0
- package/dist/src/importers/jungle-digest.js +221 -0
- package/dist/src/importers/jungle-store.d.ts +45 -0
- package/dist/src/importers/jungle-store.js +95 -0
- package/dist/src/importers/jungle.d.ts +54 -0
- package/dist/src/importers/jungle.js +134 -0
- package/dist/src/indexing/index.d.ts +7 -0
- package/dist/src/indexing/index.js +31 -8
- package/dist/src/indexing/prune-run.d.ts +22 -0
- package/dist/src/indexing/prune-run.js +67 -0
- package/dist/src/indexing/prune.d.ts +30 -0
- package/dist/src/indexing/prune.js +95 -0
- package/dist/src/indexing/runner.js +5 -5
- package/dist/src/inference/embedding/index.d.ts +25 -0
- package/dist/src/inference/embedding/index.js +11 -0
- package/dist/src/models/artifacts.js +175 -33
- package/dist/src/portability/database.js +3 -0
- package/dist/src/publish/component.d.ts +8 -0
- package/dist/src/publish/component.js +114 -0
- package/dist/src/publish/feed.d.ts +24 -0
- package/dist/src/publish/feed.js +77 -0
- package/dist/src/publish/identity.d.ts +6 -0
- package/dist/src/publish/identity.js +68 -0
- package/dist/src/publish/index.d.ts +34 -0
- package/dist/src/publish/index.js +137 -0
- package/dist/src/publish/projection.d.ts +29 -0
- package/dist/src/publish/projection.js +43 -0
- package/dist/src/search/hybrid.js +2 -2
- package/dist/src/search/lexical.d.ts +11 -0
- package/dist/src/search/lexical.js +6 -0
- package/dist/src/search/query.d.ts +7 -3
- package/dist/src/search/query.js +52 -33
- package/dist/src/search/vector.d.ts +1 -1
- package/dist/src/search/vector.js +6 -5
- package/dist/src/store/changes.d.ts +42 -0
- package/dist/src/store/changes.js +178 -0
- package/dist/src/store/foundation.d.ts +7 -0
- package/dist/src/store/foundation.js +7 -0
- package/dist/src/store/index.d.ts +69 -0
- package/dist/src/store/index.js +134 -51
- package/dist/src/worker/runtime-gc.d.ts +30 -0
- package/dist/src/worker/runtime-gc.js +296 -0
- package/native/runtime-manifest.json +4 -4
- package/package.json +1 -1
package/dist/src/cli/main.js
CHANGED
|
@@ -15,10 +15,16 @@ import { workerAction, serveWorker, hasDueAcquisition } from './worker.js';
|
|
|
15
15
|
import { nativeSetupPlan, provisionNative, inspectNative } from '../models/runtime.js';
|
|
16
16
|
import { nativeDownloadAuthGuidance } from '../models/download.js';
|
|
17
17
|
import { drainIndexQueue } from '../indexing/runner.js';
|
|
18
|
-
import { createQueryEmbedder } from '../search/query.js';
|
|
19
18
|
import { indexAction } from './indexing.js';
|
|
20
|
-
import {
|
|
19
|
+
import { pruneText } from '../indexing/prune-run.js';
|
|
20
|
+
import { importAction, jungleAction, jungleText } from './import.js';
|
|
21
|
+
import { digestText, reviewFromSpec } from './digest.js';
|
|
22
|
+
import { connectEndpoint } from '../digest/endpoint.js';
|
|
23
|
+
import { createDigest } from '../digest/pipeline.js';
|
|
24
|
+
import { createQueryEmbedder, createTextEmbedder } from '../search/query.js';
|
|
25
|
+
import { runtimeGcAction, runtimeGcText, setupRuntimeGc } from './runtimes.js';
|
|
21
26
|
import { drainLibrary } from '../worker/basic.js';
|
|
27
|
+
import { minAge, publishSite } from '../publish/index.js';
|
|
22
28
|
import { failure } from '../acquisition/errors.js';
|
|
23
29
|
const help = `sponge — a local article library
|
|
24
30
|
|
|
@@ -40,19 +46,34 @@ const help = `sponge — a local article library
|
|
|
40
46
|
index status GENERATION Inspect generation coverage
|
|
41
47
|
index run Drain queued embedding jobs
|
|
42
48
|
index rollback GENERATION Reactivate a compatible complete generation
|
|
49
|
+
index prune [--keep 1] [--dry-run] [--vacuum] Delete vectors of retired generations
|
|
43
50
|
doctor Check library and runtime states
|
|
44
51
|
export ITEM --output DIR Export Markdown and local assets
|
|
45
52
|
backup --output FILE Save a verified offline backup
|
|
46
53
|
restore FILE --into NEW_DIR Restore into a new private library
|
|
47
54
|
gc --dry-run Write a report of unreferenced blobs
|
|
48
55
|
gc --apply REPORT Recheck and remove reported blobs
|
|
56
|
+
gc --runtimes [--dry-run] Remove superseded runtimes, models and worker journals
|
|
49
57
|
worker install [--language en] Enable the optional background worker
|
|
50
58
|
worker status Inspect the background worker
|
|
51
59
|
worker uninstall Stop it and preserve the library
|
|
52
60
|
import MANIFEST [--apply] Preview or import existing Wordcell captures
|
|
61
|
+
import jungle DIR [--apply] Preview or import Jungle Reading notes, their captures and published state
|
|
62
|
+
visibility ITEM public|private|default Override or restore the default
|
|
63
|
+
highlight ITEM TEXT [--note BODY] [--occurrence N] Anchor a verbatim quote
|
|
64
|
+
comment ITEM BODY [--on ANNOTATION] Comment on an item or reply
|
|
65
|
+
publish --dry-run [--min-age-days N] Show the public reading-list entries
|
|
66
|
+
publish --output DIR [--title T] [--base-url https://...] [--min-age-days N]
|
|
67
|
+
Write a signed static feed site with <sponge-list>
|
|
68
|
+
digest ITEM --endpoint http://127.0.0.1:PORT --model NAME [--check-support]
|
|
69
|
+
Draft a digest with a local model server you started
|
|
70
|
+
digest ITEM Show the current digest and its review state
|
|
71
|
+
review DIGEST --accept SPEC --reject SPEC Review fields: all, gist, idea:N, quote:N.M
|
|
53
72
|
|
|
54
73
|
Global flags: --library /absolute/path --json
|
|
74
|
+
Items are public by default only when captured from a public source.
|
|
55
75
|
Use -- before an option-like search query. Capture setup is explicit.
|
|
76
|
+
Digest endpoints must be literal loopback HTTP (127.0.0.1 or [::1]); Sponge never starts a model server.
|
|
56
77
|
Media: --language en enables local English ASR; --force-local bypasses captions.
|
|
57
78
|
These flags apply to --wait, run --drain, or worker serve.
|
|
58
79
|
worker install --language en saves the English ASR choice for background work.
|
|
@@ -102,6 +123,11 @@ export async function main(argv, version) {
|
|
|
102
123
|
emit(await workerAction(store, args.positional[0], version, undefined, { language: args.flags.language }));
|
|
103
124
|
return 0;
|
|
104
125
|
}
|
|
126
|
+
if (args.command === 'gc' && args.flags.runtimes) {
|
|
127
|
+
const report = await runtimeGcAction(store, version, args.flags['dry-run'] === true);
|
|
128
|
+
emit(report, runtimeGcText(report));
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
105
131
|
if (args.command === 'export' || args.command === 'backup' || args.command === 'gc') {
|
|
106
132
|
emit(await portabilityAction(store, args.command, { itemId: args.positional[0], output: args.flags.output, dryRun: args.flags['dry-run'] === true, report: args.flags.apply }));
|
|
107
133
|
return 0;
|
|
@@ -119,14 +145,16 @@ export async function main(argv, version) {
|
|
|
119
145
|
process.stderr.write(nativeDownloadAuthGuidance + '\n');
|
|
120
146
|
throw error;
|
|
121
147
|
}
|
|
122
|
-
|
|
148
|
+
const gc = await setupRuntimeGc(store, version);
|
|
149
|
+
emit({ [kind]: 'ready', gc });
|
|
123
150
|
return 0;
|
|
124
151
|
}
|
|
125
152
|
if (args.command === 'setup') {
|
|
126
153
|
const plan = captureSetupPlan(defaultCapturePins);
|
|
127
154
|
process.stderr.write(`Capture setup: ${plan.downloadBytes} pinned artifact bytes, plus frozen dependency downloads. Temporary installation limit: ${plan.stagingBudgetBytes} bytes; retained runtime limit: ${plan.installationBudgetBytes} bytes; dependency installation deadline: ${plan.installationDeadlineMs / 60000} minutes. ${plan.requiredFreeBytes} free bytes required.\n`);
|
|
128
155
|
await provisionCapture({ runtimeRoot: join(store.root, 'runtime', 'capture'), pins: defaultCapturePins, signal: controller.signal });
|
|
129
|
-
|
|
156
|
+
const gc = await setupRuntimeGc(store, version);
|
|
157
|
+
emit({ capture: 'ready', gc }, 'skipped' in gc ? `Capture ready\nRuntime GC skipped: ${gc.skipped}` : `Capture ready\n${runtimeGcText(gc)}`);
|
|
130
158
|
return 0;
|
|
131
159
|
}
|
|
132
160
|
if (args.command === 'list') {
|
|
@@ -138,7 +166,7 @@ export async function main(argv, version) {
|
|
|
138
166
|
const item = store.read(args.positional[0]);
|
|
139
167
|
if (!item)
|
|
140
168
|
throw new StoreError('unavailable');
|
|
141
|
-
emit({ item }, item.markdown);
|
|
169
|
+
emit({ item, identity: store.identity(item.itemId), annotations: store.listAnnotations(item.itemId), digest: store.currentDigest(item.itemId) ?? null, editorial: store.currentEditorial(item.itemId) ?? null }, item.markdown);
|
|
142
170
|
return item.status === 'partial' ? 3 : 0;
|
|
143
171
|
}
|
|
144
172
|
if (args.command === 'asset') {
|
|
@@ -157,8 +185,8 @@ export async function main(argv, version) {
|
|
|
157
185
|
}
|
|
158
186
|
if (args.command === 'index') {
|
|
159
187
|
const action = args.positional[0], runtime = ['create', 'run'].includes(action) ? await inspectNative('embedding', nativeRoot, version) : undefined;
|
|
160
|
-
const result = await indexAction(store, action, args.positional[1], { runtime, profile: args.flags.profile, signal: controller.signal });
|
|
161
|
-
emit(result);
|
|
188
|
+
const result = await indexAction(store, action, args.positional[1], { runtime, profile: args.flags.profile, signal: controller.signal, ...(action === 'prune' ? { keep: args.flags.keep === undefined ? undefined : Number(args.flags.keep), dryRun: args.flags['dry-run'] === true, vacuum: args.flags.vacuum === true } : {}) });
|
|
189
|
+
emit(result, result.prune ? pruneText(result.prune) : undefined);
|
|
162
190
|
const failed = result.results?.find(r => !r.ok);
|
|
163
191
|
return failed ? exitFor(failed.code) : 0;
|
|
164
192
|
}
|
|
@@ -177,6 +205,55 @@ export async function main(argv, version) {
|
|
|
177
205
|
emit({ items: result.hits, warnings: result.warnings, generation: result.generation }, [...result.hits.map(item => `${item.itemId}\t${item.title}`), ...result.warnings.map(warning => `Warning: ${warning}`)].join('\n'));
|
|
178
206
|
return controller.signal.aborted ? 130 : 0;
|
|
179
207
|
}
|
|
208
|
+
if (args.command === 'visibility') {
|
|
209
|
+
const value = args.positional[1] === 'default' ? null : args.positional[1];
|
|
210
|
+
const state = store.setVisibility(args.positional[0], value);
|
|
211
|
+
emit({ itemId: args.positional[0], visibility: state }, `${state.effective}${state.override ? ' (set)' : ' (default)'}`);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
if (args.command === 'highlight') {
|
|
215
|
+
const annotation = store.addHighlight({ itemId: args.positional[0], quote: args.positional[1], ...(typeof args.flags.note === 'string' ? { note: args.flags.note } : {}), ...(args.flags.occurrence !== undefined ? { occurrence: Number(args.flags.occurrence) } : {}) });
|
|
216
|
+
emit({ annotation }, annotation.id);
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
if (args.command === 'comment') {
|
|
220
|
+
const annotation = store.addComment({ itemId: args.positional[0], body: args.positional[1], ...(typeof args.flags.on === 'string' ? { parentId: args.flags.on } : {}) });
|
|
221
|
+
emit({ annotation }, annotation.id);
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
if (args.command === 'publish') {
|
|
225
|
+
const minAgeDays = Number(args.flags['min-age-days'] ?? 0);
|
|
226
|
+
if (typeof args.flags.output === 'string') {
|
|
227
|
+
const result = publishSite(store, { output: args.flags.output, minAgeDays, ...(typeof args.flags.title === 'string' ? { title: args.flags.title } : {}), ...(typeof args.flags['base-url'] === 'string' ? { baseUrl: args.flags['base-url'] } : {}) });
|
|
228
|
+
emit(result, `Published ${result.entries} entr${result.entries === 1 ? 'y' : 'ies'} to ${result.output}`);
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
const all = store.publicProjection(), projection = { ...all, entries: minAge(all.entries, minAgeDays) };
|
|
232
|
+
emit({ dryRun: true, ...projection, ...(minAgeDays ? { held: all.entries.length - projection.entries.length } : {}) }, [...projection.entries.map(entry => `${entry.uri}\t${entry.title}`), ...(projection.skipped.length ? [`Skipped ${projection.skipped.length} public item(s) without a public link`] : [])].join('\n') || 'Nothing public');
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
if (args.command === 'digest') {
|
|
236
|
+
if (!args.flags.endpoint) {
|
|
237
|
+
const digest = store.currentDigest(args.positional[0]);
|
|
238
|
+
if (!digest)
|
|
239
|
+
throw new StoreError('unavailable');
|
|
240
|
+
emit({ digest }, digestText(digest));
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
const model = await connectEndpoint({ endpoint: String(args.flags.endpoint), model: String(args.flags.model), signal: controller.signal });
|
|
244
|
+
const embed = args.flags['check-support'] ? createTextEmbedder(store, await inspectNative('embedding', nativeRoot, version)) : undefined;
|
|
245
|
+
const result = await createDigest(store, args.positional[0], model, { signal: controller.signal, ...(embed ? { embed } : {}) });
|
|
246
|
+
emit({ digest: result.derivation, duplicate: result.duplicate, ...(result.trace ? { trace: result.trace } : {}) }, digestText(result.derivation));
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
if (args.command === 'review') {
|
|
250
|
+
const prior = store.readDigest(args.positional[0]);
|
|
251
|
+
if (!prior)
|
|
252
|
+
throw new StoreError('unavailable');
|
|
253
|
+
const digest = store.reviewDigest(prior.id, reviewFromSpec(prior, args.flags.accept, args.flags.reject));
|
|
254
|
+
emit({ digest }, digestText(digest));
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
180
257
|
if (args.command === 'delete') {
|
|
181
258
|
store.delete(args.positional[0]);
|
|
182
259
|
emit({ itemId: args.positional[0], deleted: true }, 'Deleted');
|
|
@@ -270,6 +347,13 @@ export async function main(argv, version) {
|
|
|
270
347
|
throw Object.assign(Error('repair-auth'), { code: 'repair-auth' }); return connection.capture; } }, version));
|
|
271
348
|
return 0;
|
|
272
349
|
}
|
|
350
|
+
if (args.command === 'import' && args.positional[0] === 'jungle') {
|
|
351
|
+
// A dry run reads Jungle and the library only; applying reverifies every capture with the released verifier.
|
|
352
|
+
const dryRun = args.flags.apply !== true, runtime = dryRun ? undefined : await inspectCaptureRuntime(runtimeRoot, defaultCapturePins).catch(error => { const code = errorCode(error, 'runtime-missing'); throw Object.assign(Error(code), { code }); });
|
|
353
|
+
const report = await jungleAction(store, args.positional[1], { dryRun, runtime, limit: Number(args.flags.limit ?? 1000), offset: Number(args.flags.offset ?? 0), signal: controller.signal });
|
|
354
|
+
emit({ report }, jungleText(report));
|
|
355
|
+
return controller.signal.aborted ? 130 : report.counts.skipped['capture-failed'] || report.counts.skipped['capture-unsupported'] ? 3 : 0;
|
|
356
|
+
}
|
|
273
357
|
let runtime;
|
|
274
358
|
try {
|
|
275
359
|
runtime = await inspectCaptureRuntime(runtimeRoot, defaultCapturePins);
|
|
@@ -321,7 +405,9 @@ export async function main(argv, version) {
|
|
|
321
405
|
return results.some(result => result.ok && result.status === 'partial') ? 3 : 0;
|
|
322
406
|
}
|
|
323
407
|
catch (error) {
|
|
324
|
-
const value = error?.code, code = value && codes.has(value) ? value : 'unclassified';
|
|
408
|
+
const value = error?.code, code = value && codes.has(value) ? value : 'unclassified', issues = error?.issues;
|
|
409
|
+
if (Array.isArray(issues))
|
|
410
|
+
process.stderr.write(JSON.stringify({ issues }) + '\n');
|
|
325
411
|
process.stderr.write(JSON.stringify(failure(requestId, code)) + '\n');
|
|
326
412
|
return exitFor(code);
|
|
327
413
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LibraryStore } from '../store/index.js';
|
|
2
|
+
import { type RuntimeGcOptions, type RuntimeGcReport } from '../worker/runtime-gc.js';
|
|
3
|
+
/** References of this installed Sponge: its capture pin and its catalog's model packages. */
|
|
4
|
+
export declare function installedReferences(version: string): Promise<Omit<RuntimeGcOptions, 'dryRun'>>;
|
|
5
|
+
export declare function runtimeGcAction(store: LibraryStore, version: string, dryRun: boolean): Promise<RuntimeGcReport>;
|
|
6
|
+
export declare function runtimeGcText(report: RuntimeGcReport): string;
|
|
7
|
+
/** Setup collection follows a verified install and never fails that install. */
|
|
8
|
+
export declare function setupRuntimeGc(store: LibraryStore, version: string): Promise<RuntimeGcReport | {
|
|
9
|
+
skipped: string;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defaultCapturePins } from '../acquisition/index.js';
|
|
2
|
+
import { loadNativeCatalog, nativeModelPackage } from '../models/catalog.js';
|
|
3
|
+
import { modelSetupPlan } from '../models/artifacts.js';
|
|
4
|
+
import { collectRuntimes } from '../worker/runtime-gc.js';
|
|
5
|
+
/** References of this installed Sponge: its capture pin and its catalog's model packages. */
|
|
6
|
+
export async function installedReferences(version) {
|
|
7
|
+
let models = null;
|
|
8
|
+
try {
|
|
9
|
+
const catalog = await loadNativeCatalog(version);
|
|
10
|
+
models = { fingerprints: ['asr', 'embedding'].map(kind => modelSetupPlan(nativeModelPackage(catalog, kind)).fingerprint) };
|
|
11
|
+
}
|
|
12
|
+
catch { /* Without the catalog no model directory is provably superseded. */ }
|
|
13
|
+
return { captureIds: [defaultCapturePins.id], models };
|
|
14
|
+
}
|
|
15
|
+
export async function runtimeGcAction(store, version, dryRun) { return collectRuntimes(store, { ...await installedReferences(version), dryRun }); }
|
|
16
|
+
export function runtimeGcText(report) {
|
|
17
|
+
const lines = report.removed.map(e => `${report.dryRun ? 'would remove' : 'removed'}\t${e.kind}\t${e.name}\t${e.bytes}`);
|
|
18
|
+
for (const e of report.kept)
|
|
19
|
+
if (e.reason !== 'referenced')
|
|
20
|
+
lines.push(`kept\t${e.kind}\t${e.name}\t${e.reason}`);
|
|
21
|
+
lines.push(`${report.dryRun ? 'Would reclaim' : 'Reclaimed'} ${report.bytes} bytes`);
|
|
22
|
+
return lines.join('\n');
|
|
23
|
+
}
|
|
24
|
+
/** Setup collection follows a verified install and never fails that install. */
|
|
25
|
+
export async function setupRuntimeGc(store, version) {
|
|
26
|
+
try {
|
|
27
|
+
return await runtimeGcAction(store, version, false);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return { skipped: String(error?.code ?? 'unclassified') };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { type JungleEditorial } from '../importers/jungle-digest.js';
|
|
3
|
+
import { type ReviewStatus } from '../store/changes.js';
|
|
4
|
+
/**
|
|
5
|
+
* An editorial derivation keeps a reviewed source note whole (today: one Jungle Reading note of any form), without
|
|
6
|
+
* Digest v1's limits. It is immutable data addressed like a digest; its review is one logged path, `review.note`.
|
|
7
|
+
*/
|
|
8
|
+
export interface Editorial {
|
|
9
|
+
id: string;
|
|
10
|
+
itemId: string;
|
|
11
|
+
revisionId: string;
|
|
12
|
+
kind: 'editorial';
|
|
13
|
+
producer: unknown;
|
|
14
|
+
inputHash: string;
|
|
15
|
+
output: JungleEditorial;
|
|
16
|
+
review: {
|
|
17
|
+
note?: ReviewStatus;
|
|
18
|
+
};
|
|
19
|
+
createdAt: string;
|
|
20
|
+
}
|
|
21
|
+
/** Bound on one editorial object: Jungle's largest notes (full transcripts) are a few hundred KiB. */
|
|
22
|
+
export declare const editorialBytes: number;
|
|
23
|
+
/** Fixed key order, so the same note is the same object on every device. */
|
|
24
|
+
export declare function canonicalEditorial(e: JungleEditorial): string;
|
|
25
|
+
/** The input hash is the note's own Markdown hash: an edited note is a new object, an unchanged one a duplicate. */
|
|
26
|
+
export declare function putEditorial(db: DatabaseSync, input: {
|
|
27
|
+
itemId: string;
|
|
28
|
+
revisionId: string;
|
|
29
|
+
producer: Record<string, unknown>;
|
|
30
|
+
output: JungleEditorial;
|
|
31
|
+
}): {
|
|
32
|
+
id: string;
|
|
33
|
+
duplicate: boolean;
|
|
34
|
+
};
|
|
35
|
+
export declare function readEditorial(db: DatabaseSync, id: string): Editorial | undefined;
|
|
36
|
+
/** The newest editorial derivation of the item's current revision. */
|
|
37
|
+
export declare function currentEditorial(db: DatabaseSync, itemId: string): Editorial | undefined;
|
|
38
|
+
export declare function listEditorials(db: DatabaseSync, itemId: string): Editorial[];
|
|
39
|
+
/** One logged change, and only when the status differs. */
|
|
40
|
+
export declare function reviewEditorial(db: DatabaseSync, id: string, status: ReviewStatus | null): Editorial;
|
|
41
|
+
/**
|
|
42
|
+
* The public subset of an accepted editorial note, in the public digest shape: a lead only when the note's first
|
|
43
|
+
* sentence fits the v1 lead limit (never cut), claims with detail, and quotes that occur verbatim in the capture,
|
|
44
|
+
* each at most 25 words, within the v1 per-entry budget (150 words, and the 10% source share with its 25-word floor).
|
|
45
|
+
*/
|
|
46
|
+
export declare function publicEditorial(db: DatabaseSync, e: Editorial): {
|
|
47
|
+
lead?: string;
|
|
48
|
+
ideas: {
|
|
49
|
+
claim: string;
|
|
50
|
+
detail: string;
|
|
51
|
+
quotes: string[];
|
|
52
|
+
}[];
|
|
53
|
+
} | undefined;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { locateAll, words } from '../annotations/quote.js';
|
|
2
|
+
import { markdownFor } from '../annotations/store.js';
|
|
3
|
+
import { assignQuotes, firstSentence } from '../importers/jungle-digest.js';
|
|
4
|
+
import { mutate } from '../store/changes.js';
|
|
5
|
+
import { StoreError } from '../store/types.js';
|
|
6
|
+
import { digest } from '../store/files.js';
|
|
7
|
+
import { derivationId } from './store.js';
|
|
8
|
+
import { digestLimits as L } from './schema.js';
|
|
9
|
+
const now = () => new Date().toISOString();
|
|
10
|
+
const fail = (code = 'invalid-input') => { throw new StoreError(code); };
|
|
11
|
+
/** Bound on one editorial object: Jungle's largest notes (full transcripts) are a few hundred KiB. */
|
|
12
|
+
export const editorialBytes = 1024 ** 2;
|
|
13
|
+
const str = (v, max = 65536) => typeof v === 'string' && Buffer.byteLength(v) <= max;
|
|
14
|
+
/** Fixed key order, so the same note is the same object on every device. */
|
|
15
|
+
export function canonicalEditorial(e) {
|
|
16
|
+
return JSON.stringify({ schemaVersion: e.schemaVersion, source: e.source, form: e.form, slug: e.slug, status: e.status, title: e.title, sourceUrl: e.sourceUrl, capture: e.capture, frontmatter: e.frontmatter, ...(e.gist !== undefined ? { gist: e.gist } : {}), ...(e.ideas ? { ideas: e.ideas.map(i => ({ claim: i.claim, detail: i.detail })) } : {}), ...(e.quotes ? { quotes: e.quotes.map(q => ({ text: q.text, attribution: q.attribution, speaker: q.speaker })) } : {}), markdownSha256: e.markdownSha256, markdown: e.markdown });
|
|
17
|
+
}
|
|
18
|
+
/** The input hash is the note's own Markdown hash: an edited note is a new object, an unchanged one a duplicate. */
|
|
19
|
+
export function putEditorial(db, input) {
|
|
20
|
+
const e = input.output, producer = JSON.stringify(input.producer);
|
|
21
|
+
if (!producer || Buffer.byteLength(producer) > 4096)
|
|
22
|
+
fail();
|
|
23
|
+
if (db.prepare('SELECT item_id FROM revisions WHERE id=?').get(input.revisionId)?.item_id !== input.itemId)
|
|
24
|
+
fail('unavailable');
|
|
25
|
+
if (e?.schemaVersion !== 1 || e.source !== 'jungle-reading' || ![e.form, e.slug, e.status, e.title, e.sourceUrl, e.capture].every(v => str(v, 16384)) || !str(e.markdown, 4 * 1024 ** 2) || digest(e.markdown) !== e.markdownSha256)
|
|
26
|
+
fail();
|
|
27
|
+
const output = canonicalEditorial(e);
|
|
28
|
+
if (Buffer.byteLength(output) > editorialBytes)
|
|
29
|
+
fail();
|
|
30
|
+
const id = derivationId(input.revisionId, 'editorial', e.markdownSha256, output);
|
|
31
|
+
if (db.prepare('SELECT 1 FROM derivations WHERE id=?').get(id))
|
|
32
|
+
return { id, duplicate: true };
|
|
33
|
+
db.prepare("INSERT INTO derivations(id,item_id,revision_id,kind,schema_version,producer_json,input_hash,output_json,created_at) VALUES(?,?,?,'editorial',1,?,?,?,?)").run(id, input.itemId, input.revisionId, producer, e.markdownSha256, output, now());
|
|
34
|
+
return { id, duplicate: false };
|
|
35
|
+
}
|
|
36
|
+
function editorialRow(r) { return { id: String(r.id), itemId: String(r.item_id), revisionId: String(r.revision_id), kind: 'editorial', producer: JSON.parse(String(r.producer_json)), inputHash: String(r.input_hash), output: JSON.parse(String(r.output_json)), review: JSON.parse(String(r.review_json)), createdAt: String(r.created_at) }; }
|
|
37
|
+
export function readEditorial(db, id) { const r = db.prepare("SELECT * FROM derivations WHERE id=? AND kind='editorial'").get(id); return r ? editorialRow(r) : undefined; }
|
|
38
|
+
/** The newest editorial derivation of the item's current revision. */
|
|
39
|
+
export function currentEditorial(db, itemId) {
|
|
40
|
+
const r = db.prepare("SELECT d.* FROM derivations d JOIN items i ON i.id=d.item_id AND i.current_revision=d.revision_id WHERE d.item_id=? AND d.kind='editorial' ORDER BY d.created_at DESC,d.rowid DESC LIMIT 1").get(itemId);
|
|
41
|
+
return r ? editorialRow(r) : undefined;
|
|
42
|
+
}
|
|
43
|
+
export function listEditorials(db, itemId) { return db.prepare("SELECT * FROM derivations WHERE item_id=? AND kind='editorial' ORDER BY created_at,rowid").all(itemId).map(editorialRow); }
|
|
44
|
+
/** One logged change, and only when the status differs. */
|
|
45
|
+
export function reviewEditorial(db, id, status) {
|
|
46
|
+
const d = readEditorial(db, id) ?? fail('unavailable');
|
|
47
|
+
if (status !== null && status !== 'accepted' && status !== 'rejected')
|
|
48
|
+
fail();
|
|
49
|
+
if ((d.review.note ?? null) !== status)
|
|
50
|
+
mutate(db, 'derivation', id, 'review.note', status);
|
|
51
|
+
return readEditorial(db, id);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The public subset of an accepted editorial note, in the public digest shape: a lead only when the note's first
|
|
55
|
+
* sentence fits the v1 lead limit (never cut), claims with detail, and quotes that occur verbatim in the capture,
|
|
56
|
+
* each at most 25 words, within the v1 per-entry budget (150 words, and the 10% source share with its 25-word floor).
|
|
57
|
+
*/
|
|
58
|
+
export function publicEditorial(db, e) {
|
|
59
|
+
if (e.review.note !== 'accepted')
|
|
60
|
+
return undefined;
|
|
61
|
+
const o = e.output, [first] = firstSentence(o.gist ?? ''), lead = first && first.length <= L.leadChars ? first : undefined;
|
|
62
|
+
const ideas = (o.ideas ?? []).map(i => ({ claim: i.claim, detail: i.detail, quotes: [] }));
|
|
63
|
+
if (ideas.length && o.quotes?.length) {
|
|
64
|
+
const source = markdownFor(db, e.revisionId), budget = Math.min(L.totalQuoteWords, Math.max(L.quoteWords, Math.floor(words(source) * L.sourceQuoteShare))), taken = [];
|
|
65
|
+
let used = 0;
|
|
66
|
+
assignQuotes(ideas, o.quotes).forEach((idea, k) => {
|
|
67
|
+
if (idea === null || ideas[idea].quotes.length >= L.maxQuotesPerIdea)
|
|
68
|
+
return;
|
|
69
|
+
const match = locateAll(source, o.quotes[k].text).find(m => !taken.some(t => m.start < t.end && t.start < m.end));
|
|
70
|
+
if (!match)
|
|
71
|
+
return;
|
|
72
|
+
const text = source.slice(match.start, match.end), n = words(text);
|
|
73
|
+
if (n < L.minQuoteWords || n > L.quoteWords || used + n > budget)
|
|
74
|
+
return;
|
|
75
|
+
used += n;
|
|
76
|
+
taken.push(match);
|
|
77
|
+
ideas[idea].quotes.push(text);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return lead || ideas.length ? { ...(lead ? { lead } : {}), ideas } : undefined;
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type DigestModel } from './pipeline.js';
|
|
2
|
+
/**
|
|
3
|
+
* A user-started local OpenAI-compatible server (llama.cpp `llama-server`, LM Studio, Ollama). Captured text must not
|
|
4
|
+
* leave the machine, so only literal loopback HTTP origins are accepted, redirects are refused, and Sponge never starts a server.
|
|
5
|
+
*/
|
|
6
|
+
export declare function loopbackEndpoint(value: string): URL;
|
|
7
|
+
/** GGUF quantization from a model filename, e.g. `gemma-4-12b-it-Q4_K_M.gguf` → `Q4_K_M`. */
|
|
8
|
+
export declare function quantizationOf(path: string): string | undefined;
|
|
9
|
+
export interface EndpointOptions {
|
|
10
|
+
endpoint: string;
|
|
11
|
+
model: string;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
fetch?: typeof fetch;
|
|
14
|
+
probeTimeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Confirm the server lists `model`, read what identity it exposes (llama.cpp `/props`, Ollama `/api/version`),
|
|
18
|
+
* and return a DigestModel that requests JSON-schema-constrained chat completions.
|
|
19
|
+
*/
|
|
20
|
+
export declare function connectEndpoint(options: EndpointOptions): Promise<DigestModel>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { DigestError } from './pipeline.js';
|
|
2
|
+
/**
|
|
3
|
+
* A user-started local OpenAI-compatible server (llama.cpp `llama-server`, LM Studio, Ollama). Captured text must not
|
|
4
|
+
* leave the machine, so only literal loopback HTTP origins are accepted, redirects are refused, and Sponge never starts a server.
|
|
5
|
+
*/
|
|
6
|
+
export function loopbackEndpoint(value) {
|
|
7
|
+
let url;
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(value);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new DigestError('invalid-input');
|
|
13
|
+
}
|
|
14
|
+
// A hostname such as `localhost` resolves through configuration Sponge does not control; only literal addresses pass.
|
|
15
|
+
if (url.protocol !== 'http:' || !['127.0.0.1', '[::1]'].includes(url.hostname) || url.username || url.password || url.search || url.hash || !['', '/', '/v1', '/v1/'].includes(url.pathname))
|
|
16
|
+
throw new DigestError('invalid-input');
|
|
17
|
+
return new URL(url.origin);
|
|
18
|
+
}
|
|
19
|
+
async function readCapped(response, max) {
|
|
20
|
+
const reader = response.body?.getReader(), chunks = [];
|
|
21
|
+
if (!reader)
|
|
22
|
+
throw new DigestError('unavailable');
|
|
23
|
+
let size = 0;
|
|
24
|
+
for (;;) {
|
|
25
|
+
const { done, value } = await reader.read();
|
|
26
|
+
if (done)
|
|
27
|
+
break;
|
|
28
|
+
size += value.byteLength;
|
|
29
|
+
if (size > max) {
|
|
30
|
+
await reader.cancel().catch(() => undefined);
|
|
31
|
+
throw new DigestError('limit-exceeded');
|
|
32
|
+
}
|
|
33
|
+
chunks.push(value);
|
|
34
|
+
}
|
|
35
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
36
|
+
}
|
|
37
|
+
const label = (v, max = 128) => typeof v === 'string' && v.trim() && v.length <= max && !/[\u0000-\u001f\u007f]/.test(v) ? v.trim() : undefined;
|
|
38
|
+
/** GGUF quantization from a model filename, e.g. `gemma-4-12b-it-Q4_K_M.gguf` → `Q4_K_M`. */
|
|
39
|
+
export function quantizationOf(path) {
|
|
40
|
+
return /(?:^|[-_.])((?:IQ|Q)\d(?:_[A-Z0-9]+)*|BF16|F16|F32)(?=\.gguf$|[-_.])/i.exec(path.split('/').pop() ?? '')?.[1]?.toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Confirm the server lists `model`, read what identity it exposes (llama.cpp `/props`, Ollama `/api/version`),
|
|
44
|
+
* and return a DigestModel that requests JSON-schema-constrained chat completions.
|
|
45
|
+
*/
|
|
46
|
+
export async function connectEndpoint(options) {
|
|
47
|
+
const origin = loopbackEndpoint(options.endpoint), request = options.fetch ?? fetch, model = label(options.model, 256);
|
|
48
|
+
if (!model)
|
|
49
|
+
throw new DigestError('invalid-input');
|
|
50
|
+
const get = async (path) => {
|
|
51
|
+
const signal = AbortSignal.any([options.signal ?? new AbortController().signal, AbortSignal.timeout(options.probeTimeoutMs ?? 10000)]);
|
|
52
|
+
try {
|
|
53
|
+
const r = await request(new URL(path, origin), { signal, redirect: 'error', headers: { accept: 'application/json' } });
|
|
54
|
+
return r.ok ? JSON.parse(await readCapped(r, 1 << 20)) : undefined;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (options.signal?.aborted)
|
|
58
|
+
throw new DigestError('cancelled');
|
|
59
|
+
if (error instanceof DigestError)
|
|
60
|
+
throw error;
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const models = await get('/v1/models');
|
|
65
|
+
if (!Array.isArray(models?.data))
|
|
66
|
+
throw new DigestError('unavailable');
|
|
67
|
+
if (!models.data.some(m => m?.id === model))
|
|
68
|
+
throw new DigestError('invalid-input');
|
|
69
|
+
const props = await get('/props'), build = label(props?.build_info);
|
|
70
|
+
let runtime = build ? `llama.cpp ${build}` : undefined;
|
|
71
|
+
if (!runtime) {
|
|
72
|
+
const ollama = label((await get('/api/version'))?.version);
|
|
73
|
+
runtime = ollama ? `ollama ${ollama}` : 'openai-compatible';
|
|
74
|
+
}
|
|
75
|
+
const identity = { model, quantization: (typeof props?.model_path === 'string' && quantizationOf(props.model_path)) || quantizationOf(model) || 'unknown', runtime };
|
|
76
|
+
return { identity, generate: (messages, schema, generate) => complete(origin, request, model, messages, schema, generate) };
|
|
77
|
+
}
|
|
78
|
+
async function complete(origin, request, model, messages, schema, o) {
|
|
79
|
+
const body = JSON.stringify({ model, messages, stream: false, temperature: o.temperature, seed: o.seed, max_tokens: o.maxTokens, response_format: { type: 'json_schema', json_schema: { name: 'sponge_digest', strict: true, schema } } });
|
|
80
|
+
const response = await request(new URL('/v1/chat/completions', origin), { method: 'POST', signal: o.signal, redirect: 'error', headers: { 'content-type': 'application/json', accept: 'application/json' }, body });
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
await response.body?.cancel().catch(() => undefined);
|
|
83
|
+
throw new DigestError('unavailable');
|
|
84
|
+
}
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(await readCapped(response, o.maxOutputBytes * 2 + 65536));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error instanceof DigestError)
|
|
91
|
+
throw error;
|
|
92
|
+
throw new DigestError('unavailable');
|
|
93
|
+
}
|
|
94
|
+
const choice = parsed?.choices?.[0];
|
|
95
|
+
if (choice?.finish_reason === 'length')
|
|
96
|
+
throw new DigestError('limit-exceeded');
|
|
97
|
+
const content = choice?.message?.content;
|
|
98
|
+
if (typeof content !== 'string' || !content.trim())
|
|
99
|
+
throw new DigestError('unavailable');
|
|
100
|
+
if (Buffer.byteLength(content) > o.maxOutputBytes)
|
|
101
|
+
throw new DigestError('limit-exceeded');
|
|
102
|
+
return content;
|
|
103
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { forms, type DigestIssue, type DigestOptions, type DigestV1 } from './schema.js';
|
|
2
|
+
import type { LibraryStore } from '../store/index.js';
|
|
3
|
+
import type { Derivation } from '../store/foundation.js';
|
|
4
|
+
/**
|
|
5
|
+
* Model-agnostic Digest v1 drafting. The model sees numbered passages and cites a passage id plus a short exact
|
|
6
|
+
* span; code snaps, orders, prunes and checks. Nothing leaves this module except through the injected model.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PROMPT_VERSION = "sponge-digest-prompt-v1";
|
|
9
|
+
export type ChatMessage = {
|
|
10
|
+
role: 'system' | 'user' | 'assistant';
|
|
11
|
+
content: string;
|
|
12
|
+
};
|
|
13
|
+
export type JsonSchema = Record<string, unknown>;
|
|
14
|
+
/** Stable identity recorded in the producer and the input hash. Unknown parts are the literal 'unknown'. */
|
|
15
|
+
export interface ModelIdentity {
|
|
16
|
+
model: string;
|
|
17
|
+
quantization: string;
|
|
18
|
+
runtime: string;
|
|
19
|
+
}
|
|
20
|
+
export interface GenerateOptions {
|
|
21
|
+
signal: AbortSignal;
|
|
22
|
+
maxOutputBytes: number;
|
|
23
|
+
maxTokens: number;
|
|
24
|
+
temperature: number;
|
|
25
|
+
seed: number;
|
|
26
|
+
}
|
|
27
|
+
export interface DigestModel {
|
|
28
|
+
identity: ModelIdentity;
|
|
29
|
+
generate(messages: ChatMessage[], schema: JsonSchema, options: GenerateOptions): Promise<string>;
|
|
30
|
+
}
|
|
31
|
+
/** Output-shaping parameters are hashed; time and byte bounds are not, because they only decide whether a run finishes. */
|
|
32
|
+
export interface DigestParams {
|
|
33
|
+
temperature: number;
|
|
34
|
+
seed: number;
|
|
35
|
+
maxTokens: number;
|
|
36
|
+
contextChars: number;
|
|
37
|
+
maxSections: number;
|
|
38
|
+
timeoutMs: number;
|
|
39
|
+
deadlineMs: number;
|
|
40
|
+
maxOutputBytes: number;
|
|
41
|
+
}
|
|
42
|
+
/** 24k characters is roughly 6k tokens of passages, leaving room for instructions and output in an 8k context on an 8 GB Mac. */
|
|
43
|
+
export declare const defaultDigestParams: DigestParams;
|
|
44
|
+
export type DigestErrorCode = 'invalid-input' | 'unavailable' | 'cancelled' | 'limit-exceeded';
|
|
45
|
+
export declare class DigestError extends Error {
|
|
46
|
+
readonly code: DigestErrorCode;
|
|
47
|
+
readonly issues?: DigestIssue[] | undefined;
|
|
48
|
+
constructor(code: DigestErrorCode, issues?: DigestIssue[] | undefined);
|
|
49
|
+
}
|
|
50
|
+
export type Form = typeof forms[number];
|
|
51
|
+
/** Quotes under this many words carry no evidence; the checker is gaining the same floor. */
|
|
52
|
+
export declare const minQuoteWords = 4;
|
|
53
|
+
export interface Passage {
|
|
54
|
+
id: number;
|
|
55
|
+
start: number;
|
|
56
|
+
end: number;
|
|
57
|
+
heading: string;
|
|
58
|
+
text: string;
|
|
59
|
+
}
|
|
60
|
+
export interface Highlight {
|
|
61
|
+
text: string;
|
|
62
|
+
start?: number;
|
|
63
|
+
}
|
|
64
|
+
/** Sorted-key JSON; the only serialization that enters input hashes. */
|
|
65
|
+
export declare function canonicalJson(value: unknown): string;
|
|
66
|
+
/** Paragraph-level passages with raw offsets. Headings label passages; code fences and image-only blocks are not quotable. */
|
|
67
|
+
export declare function passages(markdown: string, maxChars?: number): Passage[];
|
|
68
|
+
/** Pack passages into sections of at most `budget` display characters, preferring heading boundaries past half a budget. */
|
|
69
|
+
export declare function sectionsOf(list: Passage[], budget: number): Passage[][];
|
|
70
|
+
export declare const quoteBudget: (markdown: string) => number;
|
|
71
|
+
/** JSON Schema for the model's draft. Tags become an enum when a vocabulary is given. */
|
|
72
|
+
export declare function draftSchema(passageCount: number, vocabulary?: readonly string[]): JsonSchema;
|
|
73
|
+
export declare function notesSchema(first: number, last: number): JsonSchema;
|
|
74
|
+
interface Draft {
|
|
75
|
+
digest: DigestV1;
|
|
76
|
+
cites: (number | undefined)[][];
|
|
77
|
+
}
|
|
78
|
+
export interface QuoteStats {
|
|
79
|
+
exact: number;
|
|
80
|
+
folded: number;
|
|
81
|
+
snapped: number;
|
|
82
|
+
missing: number;
|
|
83
|
+
short: number;
|
|
84
|
+
}
|
|
85
|
+
export type QuoteMatch = 'exact' | 'folded' | 'snapped' | 'missing';
|
|
86
|
+
/**
|
|
87
|
+
* Make a quote exactly verbatim when that is unambiguous: a unique folded or Markdown-insensitive match, inside
|
|
88
|
+
* `within` first, becomes the raw source span. Ambiguous folded matches keep their text for the checker; absent ones are 'missing'.
|
|
89
|
+
*/
|
|
90
|
+
export declare function snapQuote(source: string, text: string, within?: {
|
|
91
|
+
start: number;
|
|
92
|
+
end: number;
|
|
93
|
+
}): {
|
|
94
|
+
text: string;
|
|
95
|
+
match: QuoteMatch;
|
|
96
|
+
};
|
|
97
|
+
/** Snap every quote in place and drop quotes under `minQuoteWords`. */
|
|
98
|
+
export declare function snapQuotes(draft: Draft, source: string, list: Passage[]): QuoteStats;
|
|
99
|
+
export interface DraftInput {
|
|
100
|
+
revisionId: string;
|
|
101
|
+
markdown: string;
|
|
102
|
+
title?: string;
|
|
103
|
+
form: Form;
|
|
104
|
+
model: DigestModel;
|
|
105
|
+
highlights?: readonly Highlight[];
|
|
106
|
+
vocabulary?: readonly string[];
|
|
107
|
+
params?: Partial<DigestParams>;
|
|
108
|
+
signal?: AbortSignal;
|
|
109
|
+
/** Unit-normalized embeddings for short texts; when given, quotes must support their claims by cosine. */
|
|
110
|
+
embed?: (texts: string[], signal: AbortSignal) => Promise<number[][]>;
|
|
111
|
+
supportThreshold?: number;
|
|
112
|
+
}
|
|
113
|
+
export interface DraftTrace {
|
|
114
|
+
strategy: 'single' | 'sections';
|
|
115
|
+
sections: number;
|
|
116
|
+
calls: number;
|
|
117
|
+
repaired: boolean;
|
|
118
|
+
attempts: {
|
|
119
|
+
issues: DigestIssue[];
|
|
120
|
+
quotes?: QuoteStats;
|
|
121
|
+
}[];
|
|
122
|
+
dropped: {
|
|
123
|
+
quotes: number;
|
|
124
|
+
ideas: number;
|
|
125
|
+
tags: number;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export interface DraftResult {
|
|
129
|
+
output: DigestV1;
|
|
130
|
+
inputHash: string;
|
|
131
|
+
producer: Record<string, unknown>;
|
|
132
|
+
options: DigestOptions;
|
|
133
|
+
trace: DraftTrace;
|
|
134
|
+
}
|
|
135
|
+
/** sha256 over canonical JSON of the revision, its Markdown, the prompt version, the model identity and every output-shaping input. */
|
|
136
|
+
export declare function digestInputHash(input: Pick<DraftInput, 'revisionId' | 'markdown' | 'form' | 'highlights' | 'vocabulary' | 'params' | 'supportThreshold' | 'embed'> & {
|
|
137
|
+
model: ModelIdentity;
|
|
138
|
+
}): string;
|
|
139
|
+
/** Unqualified on real claims: a deliberately low bar that only catches quotes unrelated to their claim. */
|
|
140
|
+
export declare const defaultSupportThreshold = 0.35;
|
|
141
|
+
export declare function draftDigest(input: DraftInput): Promise<DraftResult>;
|
|
142
|
+
/** Draft and store a digest for the item's current revision. A matching input hash returns the stored draft without calling the model. */
|
|
143
|
+
export declare function createDigest(store: LibraryStore, itemId: string, model: DigestModel, options?: Omit<DraftInput, 'revisionId' | 'markdown' | 'title' | 'form' | 'model' | 'highlights'>): Promise<{
|
|
144
|
+
derivation: Derivation;
|
|
145
|
+
duplicate: boolean;
|
|
146
|
+
trace?: DraftTrace;
|
|
147
|
+
}>;
|
|
148
|
+
export {};
|