@gmickel/gno 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +28 -8
  2. package/assets/skill/SKILL.md +73 -27
  3. package/assets/skill/mcp-reference.md +7 -2
  4. package/assets/skill/recipes/citation-and-provenance.md +32 -9
  5. package/package.json +1 -1
  6. package/spec/cli.md +142 -17
  7. package/spec/db/schema.sql +170 -0
  8. package/spec/evals-agentic.md +87 -5
  9. package/spec/mcp.md +75 -3
  10. package/spec/output-schemas/ask.schema.json +198 -0
  11. package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
  12. package/spec/output-schemas/changes.schema.json +280 -0
  13. package/spec/output-schemas/claim-verification.schema.json +291 -0
  14. package/spec/output-schemas/context-capsule-v1.schema.json +36 -1
  15. package/spec/output-schemas/document-diff.schema.json +185 -0
  16. package/spec/output-schemas/impact.schema.json +122 -0
  17. package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
  18. package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
  19. package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
  20. package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
  21. package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
  22. package/src/app/context-runtime-contract.ts +10 -5
  23. package/src/app/context-runtime-input.ts +29 -1
  24. package/src/app/context-runtime-types.ts +4 -0
  25. package/src/app/context-runtime.ts +5 -1
  26. package/src/app/context-surface.ts +4 -0
  27. package/src/app/verified-ask.ts +291 -0
  28. package/src/cli/commands/ask-format.ts +255 -0
  29. package/src/cli/commands/ask.ts +40 -149
  30. package/src/cli/commands/changes.ts +160 -0
  31. package/src/cli/commands/context-saved.ts +189 -0
  32. package/src/cli/options.ts +8 -0
  33. package/src/cli/program.ts +227 -1
  34. package/src/core/capsule-registry.ts +279 -0
  35. package/src/core/capsule-reverification-scheduler.ts +218 -0
  36. package/src/core/capsule-reverification.ts +289 -0
  37. package/src/core/change-diff.ts +182 -0
  38. package/src/core/change-journal.ts +228 -0
  39. package/src/core/context-budget.ts +6 -0
  40. package/src/core/context-capsule-retrieval-schema.ts +4 -0
  41. package/src/core/context-capsule-schema.ts +17 -0
  42. package/src/core/context-capsule-validation.ts +3 -2
  43. package/src/core/context-capsule.ts +18 -0
  44. package/src/core/context-compiler.ts +33 -21
  45. package/src/core/context-evidence.ts +6 -0
  46. package/src/core/knowledge-delta.ts +395 -0
  47. package/src/core/knowledge-impact.ts +202 -0
  48. package/src/core/retrieval-trace-evidence-origin.ts +3 -0
  49. package/src/core/retrieval-trace-session.ts +15 -2
  50. package/src/ingestion/sync.ts +214 -165
  51. package/src/llm/errors.ts +10 -1
  52. package/src/llm/httpGeneration.ts +11 -1
  53. package/src/llm/nodeLlamaCpp/generation.ts +54 -10
  54. package/src/llm/types.ts +6 -0
  55. package/src/mcp/tools/ask.ts +228 -0
  56. package/src/mcp/tools/changes.ts +80 -0
  57. package/src/mcp/tools/context.ts +28 -7
  58. package/src/mcp/tools/index.ts +38 -0
  59. package/src/pipeline/claim-verification-schema.ts +235 -0
  60. package/src/pipeline/claim-verification.ts +487 -0
  61. package/src/pipeline/claim-verifier.ts +474 -0
  62. package/src/pipeline/types.ts +25 -0
  63. package/src/sdk/client.ts +77 -2
  64. package/src/sdk/index.ts +7 -0
  65. package/src/sdk/types.ts +22 -0
  66. package/src/serve/doc-events.ts +12 -1
  67. package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
  68. package/src/serve/public/globals.built.css +1 -1
  69. package/src/serve/public/pages/Ask.tsx +42 -4
  70. package/src/serve/resident-runtime.ts +22 -0
  71. package/src/serve/routes/api.ts +162 -3
  72. package/src/serve/routes/changes.ts +102 -0
  73. package/src/serve/server.ts +34 -0
  74. package/src/serve/watch-service.ts +9 -0
  75. package/src/store/index.ts +21 -0
  76. package/src/store/migrations/015-document-change-journal.ts +85 -0
  77. package/src/store/migrations/016-saved-capsules.ts +131 -0
  78. package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
  79. package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
  80. package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
  81. package/src/store/migrations/index.ts +10 -0
  82. package/src/store/sqlite/adapter.ts +291 -7
  83. package/src/store/sqlite/capsule-registry-store.ts +534 -0
  84. package/src/store/sqlite/change-journal-store.ts +473 -0
  85. package/src/store/types.ts +262 -0
@@ -0,0 +1,473 @@
1
+ /** SQLite persistence for the bounded, metadata-only document change journal. */
2
+
3
+ import type { Database } from "bun:sqlite";
4
+
5
+ import type {
6
+ DocumentChangeKind,
7
+ DocumentChangeListOptions,
8
+ DocumentChangePage,
9
+ DocumentChangePurgeResult,
10
+ DocumentChangeRetentionPolicy,
11
+ DocumentChangeRetentionResult,
12
+ DocumentChangeRow,
13
+ DocumentChangeStructureDelta,
14
+ DocumentRow,
15
+ StoreResult,
16
+ } from "../types";
17
+
18
+ import {
19
+ decodeDocumentChangeCursor,
20
+ DEFAULT_DOCUMENT_CHANGE_RETENTION,
21
+ encodeDocumentChangeCursor,
22
+ serializeDocumentChangeStructureDelta,
23
+ validateDocumentChangeRetentionPolicy,
24
+ } from "../../core/change-journal";
25
+ import { err, ok } from "../types";
26
+
27
+ const DAY_MS = 86_400_000;
28
+ const MAX_PAGE_SIZE = 1_000;
29
+ const RETENTION_DELETE_SCAN_BATCH_SIZE = 256;
30
+ const UTF8_ENCODER = new TextEncoder();
31
+
32
+ export interface DocumentChangeSnapshot {
33
+ id: number;
34
+ collection: string;
35
+ relPath: string;
36
+ docid: string;
37
+ uri: string;
38
+ sourceHash: string;
39
+ mirrorHash: string | null;
40
+ active: boolean;
41
+ }
42
+
43
+ export interface DocumentChangeDraft {
44
+ documentId: number;
45
+ collection: string;
46
+ kind: DocumentChangeKind;
47
+ oldSnapshot: DocumentChangeSnapshot | null;
48
+ newSnapshot: DocumentChangeSnapshot | null;
49
+ structureDelta?: Partial<DocumentChangeStructureDelta>;
50
+ observedAtMs: number;
51
+ }
52
+
53
+ interface DbDocumentChangeRow {
54
+ sequence: number;
55
+ document_id: number;
56
+ collection: string;
57
+ change_kind: DocumentChangeKind;
58
+ old_rel_path: string | null;
59
+ new_rel_path: string | null;
60
+ old_docid: string | null;
61
+ new_docid: string | null;
62
+ old_uri: string | null;
63
+ new_uri: string | null;
64
+ old_source_hash: string | null;
65
+ new_source_hash: string | null;
66
+ old_mirror_hash: string | null;
67
+ new_mirror_hash: string | null;
68
+ old_active: number | null;
69
+ new_active: number | null;
70
+ heading_delta_json: string;
71
+ link_delta_json: string;
72
+ typed_edge_delta_json: string;
73
+ date_delta_json: string;
74
+ structure_truncated: number;
75
+ observed_at_ms: number;
76
+ byte_size: number;
77
+ }
78
+
79
+ const utf8ByteLength = (value: string | null): number =>
80
+ value === null ? 0 : UTF8_ENCODER.encode(value).byteLength;
81
+
82
+ const byteSize = (
83
+ draft: DocumentChangeDraft,
84
+ deltaJson: readonly string[]
85
+ ): number => {
86
+ const old = draft.oldSnapshot;
87
+ const next = draft.newSnapshot;
88
+ return (
89
+ 32 +
90
+ utf8ByteLength(draft.collection) +
91
+ utf8ByteLength(draft.kind) +
92
+ utf8ByteLength(old?.relPath ?? null) +
93
+ utf8ByteLength(next?.relPath ?? null) +
94
+ utf8ByteLength(old?.docid ?? null) +
95
+ utf8ByteLength(next?.docid ?? null) +
96
+ utf8ByteLength(old?.uri ?? null) +
97
+ utf8ByteLength(next?.uri ?? null) +
98
+ utf8ByteLength(old?.sourceHash ?? null) +
99
+ utf8ByteLength(next?.sourceHash ?? null) +
100
+ utf8ByteLength(old?.mirrorHash ?? null) +
101
+ utf8ByteLength(next?.mirrorHash ?? null) +
102
+ deltaJson.reduce((total, value) => total + utf8ByteLength(value), 0)
103
+ );
104
+ };
105
+
106
+ const mapRow = (row: DbDocumentChangeRow): DocumentChangeRow => ({
107
+ sequence: row.sequence,
108
+ documentId: row.document_id,
109
+ collection: row.collection,
110
+ kind: row.change_kind,
111
+ oldRelPath: row.old_rel_path,
112
+ newRelPath: row.new_rel_path,
113
+ oldDocid: row.old_docid,
114
+ newDocid: row.new_docid,
115
+ oldUri: row.old_uri,
116
+ newUri: row.new_uri,
117
+ oldSourceHash: row.old_source_hash,
118
+ newSourceHash: row.new_source_hash,
119
+ oldMirrorHash: row.old_mirror_hash,
120
+ newMirrorHash: row.new_mirror_hash,
121
+ oldActive: row.old_active === null ? null : row.old_active === 1,
122
+ newActive: row.new_active === null ? null : row.new_active === 1,
123
+ structureDelta: {
124
+ headings: JSON.parse(row.heading_delta_json),
125
+ links: JSON.parse(row.link_delta_json),
126
+ typedEdges: JSON.parse(row.typed_edge_delta_json),
127
+ dates: JSON.parse(row.date_delta_json),
128
+ truncated: row.structure_truncated === 1,
129
+ },
130
+ observedAtMs: row.observed_at_ms,
131
+ byteSize: row.byte_size,
132
+ });
133
+
134
+ export const snapshotDocumentChange = (
135
+ row: DocumentRow
136
+ ): DocumentChangeSnapshot => ({
137
+ id: row.id,
138
+ collection: row.collection,
139
+ relPath: row.relPath,
140
+ docid: row.docid,
141
+ uri: row.uri,
142
+ sourceHash: row.sourceHash,
143
+ mirrorHash: row.mirrorHash,
144
+ active: row.active,
145
+ });
146
+
147
+ const enforceInTransaction = (
148
+ db: Database,
149
+ policy: DocumentChangeRetentionPolicy,
150
+ nowMs: number
151
+ ): DocumentChangeRetentionResult => {
152
+ validateDocumentChangeRetentionPolicy(policy, nowMs);
153
+ const state = db
154
+ .query<
155
+ {
156
+ retained_entries: number;
157
+ retained_bytes: number;
158
+ retention_floor: number;
159
+ },
160
+ []
161
+ >(
162
+ `SELECT retained_entries, retained_bytes, retention_floor
163
+ FROM document_change_journal_state
164
+ WHERE singleton_id = 1`
165
+ )
166
+ .get() ?? {
167
+ retained_entries: 0,
168
+ retained_bytes: 0,
169
+ retention_floor: 0,
170
+ };
171
+ const ageBoundary = nowMs - policy.maxAgeDays * DAY_MS;
172
+ const ageCutoff =
173
+ db
174
+ .query<{ sequence: number | null }, [number]>(
175
+ `SELECT MAX(sequence) AS sequence
176
+ FROM document_changes
177
+ WHERE observed_at_ms <= ?`
178
+ )
179
+ .get(ageBoundary)?.sequence ?? state.retention_floor;
180
+ let deleted = 0;
181
+ let deletedBytes = 0;
182
+ let retentionFloor = state.retention_floor;
183
+ let afterSequence = state.retention_floor;
184
+
185
+ retentionScan: while (
186
+ state.retained_entries - deleted > policy.maxEntries ||
187
+ state.retained_bytes - deletedBytes > policy.maxBytes ||
188
+ afterSequence < ageCutoff
189
+ ) {
190
+ const rows = db
191
+ .query<{ sequence: number; byte_size: number }, [number, number]>(
192
+ `SELECT sequence, byte_size
193
+ FROM document_changes
194
+ WHERE sequence > ?
195
+ ORDER BY sequence ASC
196
+ LIMIT ?`
197
+ )
198
+ .all(afterSequence, RETENTION_DELETE_SCAN_BATCH_SIZE);
199
+ if (rows.length === 0) break;
200
+
201
+ for (const row of rows) {
202
+ const overEntries = state.retained_entries - deleted > policy.maxEntries;
203
+ const overBytes = state.retained_bytes - deletedBytes > policy.maxBytes;
204
+ if (!(row.sequence <= ageCutoff || overEntries || overBytes)) {
205
+ break retentionScan;
206
+ }
207
+ deleted += 1;
208
+ deletedBytes += row.byte_size;
209
+ retentionFloor = row.sequence;
210
+ afterSequence = row.sequence;
211
+ }
212
+ }
213
+
214
+ if (deleted > 0) {
215
+ db.run("DELETE FROM document_changes WHERE sequence <= ?", [
216
+ retentionFloor,
217
+ ]);
218
+ db.run(
219
+ `UPDATE document_change_journal_state
220
+ SET retention_floor = MAX(retention_floor, ?),
221
+ retained_entries = retained_entries - ?,
222
+ retained_bytes = retained_bytes - ?
223
+ WHERE singleton_id = 1`,
224
+ [retentionFloor, deleted, deletedBytes]
225
+ );
226
+ }
227
+
228
+ return {
229
+ deleted,
230
+ remainingEntries: state.retained_entries - deleted,
231
+ remainingBytes: state.retained_bytes - deletedBytes,
232
+ earliestCursor: encodeDocumentChangeCursor(retentionFloor),
233
+ };
234
+ };
235
+
236
+ export const appendDocumentChange = (
237
+ db: Database,
238
+ draft: DocumentChangeDraft
239
+ ): void => {
240
+ if (!Number.isSafeInteger(draft.observedAtMs) || draft.observedAtMs < 0) {
241
+ throw new RangeError("Document change observedAtMs must be non-negative");
242
+ }
243
+ const {
244
+ delta,
245
+ headingDeltaJson,
246
+ linkDeltaJson,
247
+ typedEdgeDeltaJson,
248
+ dateDeltaJson,
249
+ } = serializeDocumentChangeStructureDelta(draft.structureDelta);
250
+ const old = draft.oldSnapshot;
251
+ const next = draft.newSnapshot;
252
+ const storedByteSize = byteSize(draft, [
253
+ headingDeltaJson,
254
+ linkDeltaJson,
255
+ typedEdgeDeltaJson,
256
+ dateDeltaJson,
257
+ ]);
258
+ const inserted = db.run(
259
+ `INSERT INTO document_changes (
260
+ document_id, collection, change_kind,
261
+ old_rel_path, new_rel_path, old_docid, new_docid, old_uri, new_uri,
262
+ old_source_hash, new_source_hash, old_mirror_hash, new_mirror_hash,
263
+ old_active, new_active, heading_delta_json, link_delta_json,
264
+ typed_edge_delta_json, date_delta_json, structure_truncated,
265
+ observed_at_ms, byte_size
266
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
267
+ [
268
+ draft.documentId,
269
+ draft.collection,
270
+ draft.kind,
271
+ old?.relPath ?? null,
272
+ next?.relPath ?? null,
273
+ old?.docid ?? null,
274
+ next?.docid ?? null,
275
+ old?.uri ?? null,
276
+ next?.uri ?? null,
277
+ old?.sourceHash ?? null,
278
+ next?.sourceHash ?? null,
279
+ old?.mirrorHash ?? null,
280
+ next?.mirrorHash ?? null,
281
+ old === null ? null : old.active ? 1 : 0,
282
+ next === null ? null : next.active ? 1 : 0,
283
+ headingDeltaJson,
284
+ linkDeltaJson,
285
+ typedEdgeDeltaJson,
286
+ dateDeltaJson,
287
+ delta.truncated ? 1 : 0,
288
+ draft.observedAtMs,
289
+ storedByteSize,
290
+ ]
291
+ );
292
+ db.run(
293
+ `UPDATE document_change_journal_state
294
+ SET last_sequence = ?,
295
+ retained_entries = retained_entries + 1,
296
+ retained_bytes = retained_bytes + ?
297
+ WHERE singleton_id = 1`,
298
+ [Number(inserted.lastInsertRowid), storedByteSize]
299
+ );
300
+ enforceInTransaction(
301
+ db,
302
+ DEFAULT_DOCUMENT_CHANGE_RETENTION,
303
+ draft.observedAtMs
304
+ );
305
+ };
306
+
307
+ export const listDocumentChanges = (
308
+ db: Database,
309
+ options: DocumentChangeListOptions = {}
310
+ ): StoreResult<DocumentChangePage> => {
311
+ try {
312
+ const limit = options.limit ?? 100;
313
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE_SIZE) {
314
+ return err(
315
+ "INVALID_INPUT",
316
+ `Document change limit must be between 1 and ${MAX_PAGE_SIZE}`
317
+ );
318
+ }
319
+ if (
320
+ options.documentId !== undefined &&
321
+ (!Number.isSafeInteger(options.documentId) || options.documentId < 1)
322
+ ) {
323
+ return err("INVALID_INPUT", "Document id must be a positive integer");
324
+ }
325
+ if (
326
+ options.observedAfterMs !== undefined &&
327
+ (!Number.isSafeInteger(options.observedAfterMs) ||
328
+ options.observedAfterMs < 0)
329
+ ) {
330
+ return err(
331
+ "INVALID_INPUT",
332
+ "Document change observedAfterMs must be a non-negative integer"
333
+ );
334
+ }
335
+
336
+ const state = db
337
+ .query<{ last_sequence: number; retention_floor: number }, []>(
338
+ `SELECT last_sequence, retention_floor
339
+ FROM document_change_journal_state
340
+ WHERE singleton_id = 1`
341
+ )
342
+ .get() ?? { last_sequence: 0, retention_floor: 0 };
343
+ const cursorSequence =
344
+ options.cursor === undefined
345
+ ? state.retention_floor
346
+ : decodeDocumentChangeCursor(options.cursor);
347
+ const earliestCursor = encodeDocumentChangeCursor(state.retention_floor);
348
+ const latestCursor = encodeDocumentChangeCursor(state.last_sequence);
349
+ if (cursorSequence < state.retention_floor) {
350
+ return ok({
351
+ changes: [],
352
+ nextCursor: null,
353
+ earliestCursor,
354
+ latestCursor,
355
+ cursorExpired: true,
356
+ truncated: false,
357
+ });
358
+ }
359
+ if (cursorSequence > state.last_sequence) {
360
+ return err(
361
+ "INVALID_INPUT",
362
+ "Document change cursor is ahead of the journal"
363
+ );
364
+ }
365
+
366
+ const conditions = ["sequence > ?"];
367
+ const params: (number | string)[] = [cursorSequence];
368
+ if (options.collection) {
369
+ conditions.push("collection = ?");
370
+ params.push(options.collection);
371
+ }
372
+ if (options.documentId !== undefined) {
373
+ conditions.push("document_id = ?");
374
+ params.push(options.documentId);
375
+ }
376
+ if (options.observedAfterMs !== undefined) {
377
+ conditions.push("observed_at_ms >= ?");
378
+ params.push(options.observedAfterMs);
379
+ }
380
+ const rows = db
381
+ .query<DbDocumentChangeRow, (number | string)[]>(
382
+ `SELECT *
383
+ FROM document_changes
384
+ WHERE ${conditions.join(" AND ")}
385
+ ORDER BY sequence ASC
386
+ LIMIT ?`
387
+ )
388
+ .all(...params, limit + 1);
389
+ const truncated = rows.length > limit;
390
+ const selected = rows.slice(0, limit).map(mapRow);
391
+ const lastReturned = selected.at(-1);
392
+ return ok({
393
+ changes: selected,
394
+ nextCursor:
395
+ truncated && lastReturned
396
+ ? encodeDocumentChangeCursor(lastReturned.sequence)
397
+ : null,
398
+ earliestCursor,
399
+ latestCursor,
400
+ cursorExpired: false,
401
+ truncated,
402
+ });
403
+ } catch (cause) {
404
+ const invalidCursor =
405
+ cause instanceof TypeError &&
406
+ cause.message === "Invalid document change cursor";
407
+ return err(
408
+ invalidCursor ? "INVALID_INPUT" : "QUERY_FAILED",
409
+ cause instanceof Error
410
+ ? cause.message
411
+ : "Failed to list document changes",
412
+ cause
413
+ );
414
+ }
415
+ };
416
+
417
+ export const enforceDocumentChangeRetention = (
418
+ db: Database,
419
+ policy: DocumentChangeRetentionPolicy,
420
+ nowMs: number
421
+ ): StoreResult<DocumentChangeRetentionResult> => {
422
+ try {
423
+ const transaction = db.transaction(() =>
424
+ enforceInTransaction(db, policy, nowMs)
425
+ );
426
+ return ok(transaction());
427
+ } catch (cause) {
428
+ return err(
429
+ cause instanceof RangeError ? "INVALID_INPUT" : "QUERY_FAILED",
430
+ cause instanceof Error
431
+ ? cause.message
432
+ : "Failed to enforce document change retention",
433
+ cause
434
+ );
435
+ }
436
+ };
437
+
438
+ export const purgeDocumentChanges = (
439
+ db: Database
440
+ ): StoreResult<DocumentChangePurgeResult> => {
441
+ try {
442
+ const transaction = db.transaction((): DocumentChangePurgeResult => {
443
+ const state = db
444
+ .query<{ last_sequence: number }, []>(
445
+ `SELECT last_sequence
446
+ FROM document_change_journal_state
447
+ WHERE singleton_id = 1`
448
+ )
449
+ .get() ?? { last_sequence: 0 };
450
+ const deleted = db.run("DELETE FROM document_changes").changes;
451
+ db.run(
452
+ `UPDATE document_change_journal_state
453
+ SET retention_floor = last_sequence,
454
+ retained_entries = 0,
455
+ retained_bytes = 0
456
+ WHERE singleton_id = 1`
457
+ );
458
+ return {
459
+ deleted,
460
+ earliestCursor: encodeDocumentChangeCursor(state.last_sequence),
461
+ };
462
+ });
463
+ return ok(transaction());
464
+ } catch (cause) {
465
+ return err(
466
+ "QUERY_FAILED",
467
+ cause instanceof Error
468
+ ? cause.message
469
+ : "Failed to purge document changes",
470
+ cause
471
+ );
472
+ }
473
+ };