@gmickel/gno 1.20.0 → 1.22.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 (55) hide show
  1. package/README.md +29 -5
  2. package/assets/skill/SKILL.md +46 -15
  3. package/package.json +2 -1
  4. package/spec/cli.md +144 -0
  5. package/spec/db/schema.sql +170 -0
  6. package/spec/evals-agentic.md +48 -0
  7. package/spec/mcp.md +22 -0
  8. package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
  9. package/spec/output-schemas/changes.schema.json +280 -0
  10. package/spec/output-schemas/document-diff.schema.json +185 -0
  11. package/spec/output-schemas/impact.schema.json +122 -0
  12. package/spec/output-schemas/publish-artifact.schema.json +284 -0
  13. package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
  14. package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
  15. package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
  16. package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
  17. package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
  18. package/src/cli/commands/changes.ts +160 -0
  19. package/src/cli/commands/context-saved.ts +189 -0
  20. package/src/cli/options.ts +8 -0
  21. package/src/cli/program.ts +195 -0
  22. package/src/core/capsule-registry.ts +279 -0
  23. package/src/core/capsule-reverification-scheduler.ts +218 -0
  24. package/src/core/capsule-reverification.ts +289 -0
  25. package/src/core/change-diff.ts +182 -0
  26. package/src/core/change-journal.ts +228 -0
  27. package/src/core/knowledge-delta.ts +395 -0
  28. package/src/core/knowledge-impact.ts +202 -0
  29. package/src/ingestion/sync.ts +214 -165
  30. package/src/mcp/tools/changes.ts +80 -0
  31. package/src/mcp/tools/index.ts +29 -0
  32. package/src/publish/artifact-validation.ts +259 -0
  33. package/src/publish/artifact.ts +234 -118
  34. package/src/publish/export-service.ts +5 -9
  35. package/src/publish/metadata.ts +195 -0
  36. package/src/sdk/client.ts +42 -0
  37. package/src/sdk/index.ts +7 -0
  38. package/src/sdk/types.ts +22 -0
  39. package/src/serve/doc-events.ts +12 -1
  40. package/src/serve/resident-runtime.ts +22 -0
  41. package/src/serve/routes/api.ts +13 -0
  42. package/src/serve/routes/changes.ts +102 -0
  43. package/src/serve/server.ts +34 -0
  44. package/src/serve/watch-service.ts +9 -0
  45. package/src/store/index.ts +21 -0
  46. package/src/store/migrations/015-document-change-journal.ts +85 -0
  47. package/src/store/migrations/016-saved-capsules.ts +131 -0
  48. package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
  49. package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
  50. package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
  51. package/src/store/migrations/index.ts +10 -0
  52. package/src/store/sqlite/adapter.ts +291 -7
  53. package/src/store/sqlite/capsule-registry-store.ts +534 -0
  54. package/src/store/sqlite/change-journal-store.ts +473 -0
  55. package/src/store/types.ts +262 -0
@@ -0,0 +1,534 @@
1
+ /** SQLite persistence for metadata-only saved Context Capsules. */
2
+
3
+ import type { Database } from "bun:sqlite";
4
+
5
+ import type {
6
+ SavedCapsuleEvidenceReference,
7
+ SavedCapsuleRegistration,
8
+ SavedCapsuleRegistrationInput,
9
+ SavedCapsuleRegistrationRecord,
10
+ SavedCapsuleRegistrationSnapshot,
11
+ SavedCapsuleReverificationState,
12
+ SavedCapsuleVerificationExpectation,
13
+ SavedCapsuleVerificationRecord,
14
+ StoreResult,
15
+ } from "../types";
16
+
17
+ import { err, ok } from "../types";
18
+
19
+ const MAX_REGISTRATIONS = 10_000;
20
+
21
+ interface DbRegistration {
22
+ registration_id: string;
23
+ file_path: string;
24
+ file_hash: string;
25
+ capsule_id: string;
26
+ index_name: string;
27
+ question: string | null;
28
+ label: string | null;
29
+ notification_preference: "none" | "local";
30
+ registered_at_ms: number;
31
+ updated_at_ms: number;
32
+ last_attempted_sequence: number;
33
+ }
34
+
35
+ interface DbEvidence {
36
+ registration_id: string;
37
+ evidence_id: string;
38
+ canonical_uri: string;
39
+ collection: string;
40
+ source_hash: string;
41
+ mirror_hash: string;
42
+ passage_hash: string;
43
+ }
44
+
45
+ interface DbVerification {
46
+ registration_id: string;
47
+ trigger_kind: "manual" | "journal";
48
+ from_sequence: number;
49
+ through_sequence: number;
50
+ operation_status: "completed" | "failed";
51
+ affected_question_state: "unaffected" | "affected" | "unknown";
52
+ affected_reasons_json: string;
53
+ receipt_json: string | null;
54
+ receipt_hash: string | null;
55
+ error_code: string | null;
56
+ error_message: string | null;
57
+ verified_at_ms: number;
58
+ }
59
+
60
+ const mapRegistration = (row: DbRegistration): SavedCapsuleRegistration => ({
61
+ registrationId: row.registration_id,
62
+ filePath: row.file_path,
63
+ fileHash: row.file_hash,
64
+ capsuleId: row.capsule_id,
65
+ indexName: row.index_name,
66
+ question: row.question,
67
+ label: row.label,
68
+ notificationPreference: row.notification_preference,
69
+ registeredAtMs: row.registered_at_ms,
70
+ updatedAtMs: row.updated_at_ms,
71
+ lastAttemptedSequence: row.last_attempted_sequence,
72
+ });
73
+
74
+ const mapEvidence = (row: DbEvidence): SavedCapsuleEvidenceReference => ({
75
+ evidenceId: row.evidence_id,
76
+ canonicalUri: row.canonical_uri,
77
+ collection: row.collection,
78
+ sourceHash: row.source_hash,
79
+ mirrorHash: row.mirror_hash,
80
+ passageHash: row.passage_hash,
81
+ });
82
+
83
+ const mapVerification = (
84
+ row: DbVerification
85
+ ): SavedCapsuleVerificationRecord => ({
86
+ registrationId: row.registration_id,
87
+ triggerKind: row.trigger_kind,
88
+ fromSequence: row.from_sequence,
89
+ throughSequence: row.through_sequence,
90
+ operationStatus: row.operation_status,
91
+ affectedQuestionState: row.affected_question_state,
92
+ affectedReasons: JSON.parse(row.affected_reasons_json) as string[],
93
+ receiptJson: row.receipt_json,
94
+ receiptHash: row.receipt_hash,
95
+ errorCode: row.error_code,
96
+ errorMessage: row.error_message,
97
+ verifiedAtMs: row.verified_at_ms,
98
+ });
99
+
100
+ const loadRecords = (
101
+ db: Database,
102
+ registrationId?: string
103
+ ): SavedCapsuleRegistrationRecord[] => {
104
+ const registrations = registrationId
105
+ ? db
106
+ .query<DbRegistration, [string]>(
107
+ `SELECT * FROM saved_capsule_registrations
108
+ WHERE registration_id = ?`
109
+ )
110
+ .all(registrationId)
111
+ : db
112
+ .query<DbRegistration, []>(
113
+ `SELECT * FROM saved_capsule_registrations
114
+ ORDER BY registration_id ASC`
115
+ )
116
+ .all();
117
+ if (registrations.length === 0) return [];
118
+ const evidence = registrationId
119
+ ? db
120
+ .query<DbEvidence, [string]>(
121
+ `SELECT * FROM saved_capsule_evidence
122
+ WHERE registration_id = ?
123
+ ORDER BY evidence_id ASC`
124
+ )
125
+ .all(registrationId)
126
+ : db
127
+ .query<DbEvidence, []>(
128
+ `SELECT * FROM saved_capsule_evidence
129
+ ORDER BY registration_id ASC, evidence_id ASC`
130
+ )
131
+ .all();
132
+ const verifications = registrationId
133
+ ? db
134
+ .query<DbVerification, [string]>(
135
+ `SELECT * FROM saved_capsule_verifications
136
+ WHERE registration_id = ?`
137
+ )
138
+ .all(registrationId)
139
+ : db
140
+ .query<DbVerification, []>(
141
+ `SELECT * FROM saved_capsule_verifications
142
+ ORDER BY registration_id ASC`
143
+ )
144
+ .all();
145
+ const evidenceByRegistration = new Map<
146
+ string,
147
+ SavedCapsuleEvidenceReference[]
148
+ >();
149
+ for (const row of evidence) {
150
+ const values = evidenceByRegistration.get(row.registration_id) ?? [];
151
+ values.push(mapEvidence(row));
152
+ evidenceByRegistration.set(row.registration_id, values);
153
+ }
154
+ const verificationByRegistration = new Map(
155
+ verifications.map((row) => [row.registration_id, mapVerification(row)])
156
+ );
157
+ return registrations.map((row) => ({
158
+ ...mapRegistration(row),
159
+ evidence: evidenceByRegistration.get(row.registration_id) ?? [],
160
+ verification: verificationByRegistration.get(row.registration_id) ?? null,
161
+ }));
162
+ };
163
+
164
+ export const upsertSavedCapsuleRegistration = (
165
+ db: Database,
166
+ input: SavedCapsuleRegistrationInput
167
+ ): StoreResult<SavedCapsuleRegistrationRecord> => {
168
+ try {
169
+ const transaction = db.transaction(() => {
170
+ const count = db
171
+ .query<{ count: number }, []>(
172
+ "SELECT COUNT(*) AS count FROM saved_capsule_registrations"
173
+ )
174
+ .get()?.count;
175
+ const exists = db
176
+ .query<{ found: number }, [string]>(
177
+ `SELECT 1 AS found FROM saved_capsule_registrations
178
+ WHERE registration_id = ?`
179
+ )
180
+ .get(input.registrationId);
181
+ if (!exists && (count ?? 0) >= MAX_REGISTRATIONS) {
182
+ throw new RangeError("Saved Capsule registration limit reached");
183
+ }
184
+ db.run(
185
+ `UPDATE saved_capsule_reverification_state
186
+ SET last_processed_sequence = MIN(last_processed_sequence, ?),
187
+ registration_epoch = registration_epoch + 1
188
+ WHERE singleton_id = 1`,
189
+ [input.lastAttemptedSequence]
190
+ );
191
+ const registrationGeneration = db
192
+ .query<{ registration_epoch: number }, []>(
193
+ `SELECT registration_epoch
194
+ FROM saved_capsule_reverification_state
195
+ WHERE singleton_id = 1`
196
+ )
197
+ .get()?.registration_epoch;
198
+ if (registrationGeneration === undefined) {
199
+ throw new Error("Saved Capsule reverification state is missing");
200
+ }
201
+ db.run(
202
+ `INSERT INTO saved_capsule_registrations (
203
+ registration_id, file_path, file_hash, capsule_id, index_name,
204
+ question, label, notification_preference, registered_at_ms,
205
+ updated_at_ms, last_attempted_sequence, registration_generation
206
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
207
+ ON CONFLICT(registration_id) DO UPDATE SET
208
+ file_path = excluded.file_path,
209
+ file_hash = excluded.file_hash,
210
+ capsule_id = excluded.capsule_id,
211
+ index_name = excluded.index_name,
212
+ question = excluded.question,
213
+ label = excluded.label,
214
+ notification_preference = excluded.notification_preference,
215
+ updated_at_ms = excluded.updated_at_ms,
216
+ last_attempted_sequence = excluded.last_attempted_sequence,
217
+ registration_generation = excluded.registration_generation`,
218
+ [
219
+ input.registrationId,
220
+ input.filePath,
221
+ input.fileHash,
222
+ input.capsuleId,
223
+ input.indexName,
224
+ input.question,
225
+ input.label,
226
+ input.notificationPreference,
227
+ input.registeredAtMs,
228
+ input.updatedAtMs,
229
+ input.lastAttemptedSequence,
230
+ registrationGeneration,
231
+ ]
232
+ );
233
+ db.run("DELETE FROM saved_capsule_evidence WHERE registration_id = ?", [
234
+ input.registrationId,
235
+ ]);
236
+ const insertEvidence = db.prepare(
237
+ `INSERT INTO saved_capsule_evidence (
238
+ registration_id, evidence_id, canonical_uri, collection,
239
+ source_hash, mirror_hash, passage_hash
240
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`
241
+ );
242
+ for (const evidence of input.evidence) {
243
+ insertEvidence.run(
244
+ input.registrationId,
245
+ evidence.evidenceId,
246
+ evidence.canonicalUri,
247
+ evidence.collection,
248
+ evidence.sourceHash,
249
+ evidence.mirrorHash,
250
+ evidence.passageHash
251
+ );
252
+ }
253
+ db.run(
254
+ "DELETE FROM saved_capsule_verifications WHERE registration_id = ?",
255
+ [input.registrationId]
256
+ );
257
+ return loadRecords(db, input.registrationId)[0]!;
258
+ });
259
+ return ok(transaction());
260
+ } catch (cause) {
261
+ return err(
262
+ cause instanceof RangeError ? "INVALID_INPUT" : "QUERY_FAILED",
263
+ cause instanceof Error
264
+ ? cause.message
265
+ : "Failed to register saved Context Capsule",
266
+ cause
267
+ );
268
+ }
269
+ };
270
+
271
+ export const listSavedCapsuleRegistrations = (
272
+ db: Database
273
+ ): StoreResult<SavedCapsuleRegistrationRecord[]> => {
274
+ try {
275
+ return ok(loadRecords(db));
276
+ } catch (cause) {
277
+ return err("QUERY_FAILED", "Failed to list saved Context Capsules", cause);
278
+ }
279
+ };
280
+
281
+ export const getSavedCapsuleRegistration = (
282
+ db: Database,
283
+ registrationId: string
284
+ ): StoreResult<SavedCapsuleRegistrationRecord | null> => {
285
+ try {
286
+ return ok(loadRecords(db, registrationId)[0] ?? null);
287
+ } catch (cause) {
288
+ return err("QUERY_FAILED", "Failed to read saved Context Capsule", cause);
289
+ }
290
+ };
291
+
292
+ export const getSavedCapsuleRegistrationSnapshot = (
293
+ db: Database,
294
+ registrationId: string
295
+ ): StoreResult<SavedCapsuleRegistrationSnapshot | null> => {
296
+ try {
297
+ const transaction = db.transaction(() => {
298
+ const registration = loadRecords(db, registrationId)[0];
299
+ if (!registration) return null;
300
+ const registrationGeneration = db
301
+ .query<{ registration_generation: number }, [string]>(
302
+ `SELECT registration_generation
303
+ FROM saved_capsule_registrations
304
+ WHERE registration_id = ?`
305
+ )
306
+ .get(registrationId)?.registration_generation;
307
+ if (registrationGeneration === undefined) return null;
308
+ return { registration, registrationGeneration };
309
+ });
310
+ return ok(transaction());
311
+ } catch (cause) {
312
+ return err(
313
+ "QUERY_FAILED",
314
+ "Failed to read saved Context Capsule verification snapshot",
315
+ cause
316
+ );
317
+ }
318
+ };
319
+
320
+ export const deleteSavedCapsuleRegistration = (
321
+ db: Database,
322
+ registrationId: string
323
+ ): StoreResult<boolean> => {
324
+ try {
325
+ return ok(
326
+ db.run(
327
+ "DELETE FROM saved_capsule_registrations WHERE registration_id = ?",
328
+ [registrationId]
329
+ ).changes > 0
330
+ );
331
+ } catch (cause) {
332
+ return err("QUERY_FAILED", "Failed to remove saved Context Capsule", cause);
333
+ }
334
+ };
335
+
336
+ export const listSavedCapsuleIdsAffectedByChanges = (
337
+ db: Database,
338
+ afterSequence: number,
339
+ throughSequence: number,
340
+ limit: number
341
+ ): StoreResult<{ registrationIds: string[]; truncated: boolean }> => {
342
+ try {
343
+ if (
344
+ !Number.isSafeInteger(afterSequence) ||
345
+ !Number.isSafeInteger(throughSequence) ||
346
+ afterSequence < 0 ||
347
+ throughSequence < afterSequence ||
348
+ !Number.isSafeInteger(limit) ||
349
+ limit < 1 ||
350
+ limit > MAX_REGISTRATIONS
351
+ ) {
352
+ return err("INVALID_INPUT", "Invalid saved Capsule change range");
353
+ }
354
+ const rows = db
355
+ .query<{ registration_id: string }, [number, number, number]>(
356
+ `SELECT DISTINCT evidence.registration_id
357
+ FROM saved_capsule_evidence evidence
358
+ INNER JOIN saved_capsule_registrations registration
359
+ ON registration.registration_id = evidence.registration_id
360
+ INNER JOIN document_changes change
361
+ ON (
362
+ evidence.canonical_uri = change.old_uri
363
+ OR evidence.canonical_uri = change.new_uri
364
+ OR evidence.source_hash = change.old_source_hash
365
+ OR evidence.source_hash = change.new_source_hash
366
+ OR evidence.mirror_hash = change.old_mirror_hash
367
+ OR evidence.mirror_hash = change.new_mirror_hash
368
+ )
369
+ WHERE change.sequence > ?
370
+ AND change.sequence > registration.last_attempted_sequence
371
+ AND change.sequence <= ?
372
+ ORDER BY evidence.registration_id ASC
373
+ LIMIT ?`
374
+ )
375
+ .all(afterSequence, throughSequence, limit + 1);
376
+ return ok({
377
+ registrationIds: rows.slice(0, limit).map((row) => row.registration_id),
378
+ truncated: rows.length > limit,
379
+ });
380
+ } catch (cause) {
381
+ return err(
382
+ "QUERY_FAILED",
383
+ "Failed to resolve affected saved Context Capsules",
384
+ cause
385
+ );
386
+ }
387
+ };
388
+
389
+ export const upsertSavedCapsuleVerification = (
390
+ db: Database,
391
+ verification: SavedCapsuleVerificationRecord,
392
+ expectedRegistration: SavedCapsuleVerificationExpectation
393
+ ): StoreResult<boolean> => {
394
+ try {
395
+ const transaction = db.transaction(() => {
396
+ const registrationMatches = db
397
+ .query<{ found: number }, [string, number]>(
398
+ `SELECT 1 AS found
399
+ FROM saved_capsule_registrations
400
+ WHERE registration_id = ?
401
+ AND registration_generation = ?`
402
+ )
403
+ .get(
404
+ verification.registrationId,
405
+ expectedRegistration.registrationGeneration
406
+ );
407
+ if (!registrationMatches) return false;
408
+
409
+ db.run(
410
+ `INSERT INTO saved_capsule_verifications (
411
+ registration_id, trigger_kind, from_sequence, through_sequence,
412
+ operation_status, affected_question_state, affected_reasons_json,
413
+ receipt_json, receipt_hash, error_code, error_message, verified_at_ms
414
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
415
+ ON CONFLICT(registration_id) DO UPDATE SET
416
+ trigger_kind = excluded.trigger_kind,
417
+ from_sequence = excluded.from_sequence,
418
+ through_sequence = excluded.through_sequence,
419
+ operation_status = excluded.operation_status,
420
+ affected_question_state = excluded.affected_question_state,
421
+ affected_reasons_json = excluded.affected_reasons_json,
422
+ receipt_json = excluded.receipt_json,
423
+ receipt_hash = excluded.receipt_hash,
424
+ error_code = excluded.error_code,
425
+ error_message = excluded.error_message,
426
+ verified_at_ms = excluded.verified_at_ms`,
427
+ [
428
+ verification.registrationId,
429
+ verification.triggerKind,
430
+ verification.fromSequence,
431
+ verification.throughSequence,
432
+ verification.operationStatus,
433
+ verification.affectedQuestionState,
434
+ JSON.stringify(verification.affectedReasons),
435
+ verification.receiptJson,
436
+ verification.receiptHash,
437
+ verification.errorCode,
438
+ verification.errorMessage,
439
+ verification.verifiedAtMs,
440
+ ]
441
+ );
442
+ db.run(
443
+ `UPDATE saved_capsule_registrations
444
+ SET last_attempted_sequence = MAX(last_attempted_sequence, ?),
445
+ updated_at_ms = MAX(updated_at_ms, ?)
446
+ WHERE registration_id = ?
447
+ AND registration_generation = ?`,
448
+ [
449
+ verification.throughSequence,
450
+ verification.verifiedAtMs,
451
+ verification.registrationId,
452
+ expectedRegistration.registrationGeneration,
453
+ ]
454
+ );
455
+ return true;
456
+ });
457
+ return ok(transaction());
458
+ } catch (cause) {
459
+ return err(
460
+ "QUERY_FAILED",
461
+ "Failed to persist saved Context Capsule verification",
462
+ cause
463
+ );
464
+ }
465
+ };
466
+
467
+ export const getSavedCapsuleReverificationSequence = (
468
+ db: Database
469
+ ): StoreResult<number> => {
470
+ try {
471
+ return ok(
472
+ db
473
+ .query<{ last_processed_sequence: number }, []>(
474
+ `SELECT last_processed_sequence
475
+ FROM saved_capsule_reverification_state WHERE singleton_id = 1`
476
+ )
477
+ .get()?.last_processed_sequence ?? 0
478
+ );
479
+ } catch (cause) {
480
+ return err("QUERY_FAILED", "Failed to read reverification state", cause);
481
+ }
482
+ };
483
+
484
+ export const getSavedCapsuleReverificationState = (
485
+ db: Database
486
+ ): StoreResult<SavedCapsuleReverificationState> => {
487
+ try {
488
+ const row = db
489
+ .query<
490
+ {
491
+ last_processed_sequence: number;
492
+ registration_epoch: number;
493
+ },
494
+ []
495
+ >(
496
+ `SELECT last_processed_sequence, registration_epoch
497
+ FROM saved_capsule_reverification_state WHERE singleton_id = 1`
498
+ )
499
+ .get();
500
+ return ok({
501
+ lastProcessedSequence: row?.last_processed_sequence ?? 0,
502
+ registrationEpoch: row?.registration_epoch ?? 0,
503
+ });
504
+ } catch (cause) {
505
+ return err("QUERY_FAILED", "Failed to read reverification state", cause);
506
+ }
507
+ };
508
+
509
+ export const setSavedCapsuleReverificationSequence = (
510
+ db: Database,
511
+ sequence: number,
512
+ expectedRegistrationEpoch: number
513
+ ): StoreResult<boolean> => {
514
+ try {
515
+ if (
516
+ !Number.isSafeInteger(sequence) ||
517
+ sequence < 0 ||
518
+ !Number.isSafeInteger(expectedRegistrationEpoch) ||
519
+ expectedRegistrationEpoch < 0
520
+ ) {
521
+ return err("INVALID_INPUT", "Invalid reverification sequence");
522
+ }
523
+ const updated = db.run(
524
+ `UPDATE saved_capsule_reverification_state
525
+ SET last_processed_sequence = MAX(last_processed_sequence, ?)
526
+ WHERE singleton_id = 1
527
+ AND registration_epoch = ?`,
528
+ [sequence, expectedRegistrationEpoch]
529
+ ).changes;
530
+ return ok(updated > 0);
531
+ } catch (cause) {
532
+ return err("QUERY_FAILED", "Failed to update reverification state", cause);
533
+ }
534
+ };