@gmickel/gno 1.20.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 (49) hide show
  1. package/README.md +19 -4
  2. package/assets/skill/SKILL.md +46 -15
  3. package/package.json +1 -1
  4. package/spec/cli.md +100 -0
  5. package/spec/db/schema.sql +170 -0
  6. package/spec/mcp.md +22 -0
  7. package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
  8. package/spec/output-schemas/changes.schema.json +280 -0
  9. package/spec/output-schemas/document-diff.schema.json +185 -0
  10. package/spec/output-schemas/impact.schema.json +122 -0
  11. package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
  12. package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
  13. package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
  14. package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
  15. package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
  16. package/src/cli/commands/changes.ts +160 -0
  17. package/src/cli/commands/context-saved.ts +189 -0
  18. package/src/cli/options.ts +8 -0
  19. package/src/cli/program.ts +195 -0
  20. package/src/core/capsule-registry.ts +279 -0
  21. package/src/core/capsule-reverification-scheduler.ts +218 -0
  22. package/src/core/capsule-reverification.ts +289 -0
  23. package/src/core/change-diff.ts +182 -0
  24. package/src/core/change-journal.ts +228 -0
  25. package/src/core/knowledge-delta.ts +395 -0
  26. package/src/core/knowledge-impact.ts +202 -0
  27. package/src/ingestion/sync.ts +214 -165
  28. package/src/mcp/tools/changes.ts +80 -0
  29. package/src/mcp/tools/index.ts +29 -0
  30. package/src/sdk/client.ts +42 -0
  31. package/src/sdk/index.ts +7 -0
  32. package/src/sdk/types.ts +22 -0
  33. package/src/serve/doc-events.ts +12 -1
  34. package/src/serve/resident-runtime.ts +22 -0
  35. package/src/serve/routes/api.ts +13 -0
  36. package/src/serve/routes/changes.ts +102 -0
  37. package/src/serve/server.ts +34 -0
  38. package/src/serve/watch-service.ts +9 -0
  39. package/src/store/index.ts +21 -0
  40. package/src/store/migrations/015-document-change-journal.ts +85 -0
  41. package/src/store/migrations/016-saved-capsules.ts +131 -0
  42. package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
  43. package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
  44. package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
  45. package/src/store/migrations/index.ts +10 -0
  46. package/src/store/sqlite/adapter.ts +291 -7
  47. package/src/store/sqlite/capsule-registry-store.ts +534 -0
  48. package/src/store/sqlite/change-journal-store.ts +473 -0
  49. package/src/store/types.ts +262 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Migration: metadata-only saved Context Capsule registry.
3
+ *
4
+ * Capsule bodies remain user-owned files. The database stores only file and
5
+ * Capsule identities, evidence references, the latest verification outcome,
6
+ * and the journal high-water sequence used by the resident scheduler.
7
+ *
8
+ * @module src/store/migrations/016-saved-capsules
9
+ */
10
+
11
+ import type { Database } from "bun:sqlite";
12
+
13
+ import type { Migration } from "./runner";
14
+
15
+ export const migration: Migration = {
16
+ version: 16,
17
+ name: "saved_capsules",
18
+
19
+ up(db: Database): void {
20
+ db.exec(`
21
+ CREATE TABLE saved_capsule_registrations (
22
+ registration_id TEXT PRIMARY KEY,
23
+ file_path TEXT NOT NULL UNIQUE,
24
+ file_hash TEXT NOT NULL,
25
+ capsule_id TEXT NOT NULL,
26
+ index_name TEXT NOT NULL,
27
+ question TEXT,
28
+ label TEXT,
29
+ notification_preference TEXT NOT NULL DEFAULT 'none'
30
+ CHECK (notification_preference IN ('none', 'local')),
31
+ registered_at_ms INTEGER NOT NULL CHECK (registered_at_ms >= 0),
32
+ updated_at_ms INTEGER NOT NULL CHECK (updated_at_ms >= registered_at_ms),
33
+ last_attempted_sequence INTEGER NOT NULL DEFAULT 0
34
+ CHECK (last_attempted_sequence >= 0),
35
+ CHECK (length(registration_id) BETWEEN 1 AND 128),
36
+ CHECK (length(CAST(file_path AS BLOB)) BETWEEN 1 AND 8192),
37
+ CHECK (length(file_hash) = 64 AND file_hash NOT GLOB '*[^0-9a-f]*'),
38
+ CHECK (length(capsule_id) = 64 AND capsule_id NOT GLOB '*[^0-9a-f]*'),
39
+ CHECK (length(CAST(index_name AS BLOB)) BETWEEN 1 AND 128),
40
+ CHECK (question IS NULL OR length(CAST(question AS BLOB)) <= 8192),
41
+ CHECK (label IS NULL OR length(CAST(label AS BLOB)) <= 512)
42
+ );
43
+
44
+ CREATE INDEX idx_saved_capsules_index
45
+ ON saved_capsule_registrations(index_name, registration_id);
46
+
47
+ CREATE TABLE saved_capsule_evidence (
48
+ registration_id TEXT NOT NULL,
49
+ evidence_id TEXT NOT NULL,
50
+ canonical_uri TEXT NOT NULL,
51
+ collection TEXT NOT NULL,
52
+ source_hash TEXT NOT NULL,
53
+ mirror_hash TEXT NOT NULL,
54
+ passage_hash TEXT NOT NULL,
55
+ PRIMARY KEY (registration_id, evidence_id),
56
+ FOREIGN KEY (registration_id)
57
+ REFERENCES saved_capsule_registrations(registration_id)
58
+ ON DELETE CASCADE,
59
+ CHECK (length(evidence_id) = 64 AND evidence_id NOT GLOB '*[^0-9a-f]*'),
60
+ CHECK (length(CAST(canonical_uri AS BLOB)) BETWEEN 1 AND 8192),
61
+ CHECK (length(CAST(collection AS BLOB)) BETWEEN 1 AND 256),
62
+ CHECK (length(source_hash) = 64 AND source_hash NOT GLOB '*[^0-9a-f]*'),
63
+ CHECK (length(mirror_hash) = 64 AND mirror_hash NOT GLOB '*[^0-9a-f]*'),
64
+ CHECK (length(passage_hash) = 64 AND passage_hash NOT GLOB '*[^0-9a-f]*')
65
+ );
66
+
67
+ CREATE INDEX idx_saved_capsule_evidence_uri
68
+ ON saved_capsule_evidence(canonical_uri, registration_id);
69
+
70
+ CREATE INDEX idx_saved_capsule_evidence_source
71
+ ON saved_capsule_evidence(source_hash, registration_id);
72
+
73
+ CREATE INDEX idx_saved_capsule_evidence_mirror
74
+ ON saved_capsule_evidence(mirror_hash, registration_id);
75
+
76
+ CREATE TABLE saved_capsule_verifications (
77
+ registration_id TEXT PRIMARY KEY,
78
+ trigger_kind TEXT NOT NULL CHECK (trigger_kind IN ('manual', 'journal')),
79
+ from_sequence INTEGER NOT NULL CHECK (from_sequence >= 0),
80
+ through_sequence INTEGER NOT NULL
81
+ CHECK (through_sequence >= from_sequence),
82
+ operation_status TEXT NOT NULL CHECK (operation_status IN ('completed', 'failed')),
83
+ affected_question_state TEXT NOT NULL
84
+ CHECK (affected_question_state IN ('unaffected', 'affected', 'unknown')),
85
+ affected_reasons_json TEXT NOT NULL,
86
+ receipt_json TEXT,
87
+ receipt_hash TEXT,
88
+ error_code TEXT,
89
+ error_message TEXT,
90
+ verified_at_ms INTEGER NOT NULL CHECK (verified_at_ms >= 0),
91
+ FOREIGN KEY (registration_id)
92
+ REFERENCES saved_capsule_registrations(registration_id)
93
+ ON DELETE CASCADE,
94
+ CHECK (length(CAST(affected_reasons_json AS BLOB)) <= 4096),
95
+ CHECK (receipt_json IS NULL OR length(CAST(receipt_json AS BLOB)) <= 16777216),
96
+ CHECK (receipt_hash IS NULL OR (
97
+ length(receipt_hash) = 64 AND receipt_hash NOT GLOB '*[^0-9a-f]*'
98
+ )),
99
+ CHECK (error_code IS NULL OR length(CAST(error_code AS BLOB)) <= 256),
100
+ CHECK (error_message IS NULL OR length(CAST(error_message AS BLOB)) <= 4096),
101
+ CHECK (
102
+ (
103
+ operation_status = 'completed'
104
+ AND receipt_json IS NOT NULL
105
+ AND receipt_hash IS NOT NULL
106
+ AND error_code IS NULL
107
+ AND error_message IS NULL
108
+ )
109
+ OR
110
+ (
111
+ operation_status = 'failed'
112
+ AND receipt_json IS NULL
113
+ AND receipt_hash IS NULL
114
+ AND error_code IS NOT NULL
115
+ AND error_message IS NOT NULL
116
+ )
117
+ )
118
+ );
119
+
120
+ CREATE TABLE saved_capsule_reverification_state (
121
+ singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
122
+ last_processed_sequence INTEGER NOT NULL DEFAULT 0
123
+ CHECK (last_processed_sequence >= 0)
124
+ );
125
+
126
+ INSERT INTO saved_capsule_reverification_state (
127
+ singleton_id, last_processed_sequence
128
+ ) VALUES (1, 0);
129
+ `);
130
+ },
131
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Migration: transactional retained-row/byte counters for the change journal.
3
+ *
4
+ * The counters let append-time retention inspect only the oldest bounded
5
+ * prefix that may be deleted instead of rescanning/materializing the journal.
6
+ */
7
+
8
+ import type { Database } from "bun:sqlite";
9
+
10
+ import type { Migration } from "./runner";
11
+
12
+ export const migration: Migration = {
13
+ version: 17,
14
+ name: "document_change_retention_counters",
15
+
16
+ up(db: Database): void {
17
+ db.exec(`
18
+ ALTER TABLE document_change_journal_state
19
+ ADD COLUMN retained_entries INTEGER NOT NULL DEFAULT 0
20
+ CHECK (retained_entries >= 0);
21
+ ALTER TABLE document_change_journal_state
22
+ ADD COLUMN retained_bytes INTEGER NOT NULL DEFAULT 0
23
+ CHECK (retained_bytes >= 0);
24
+
25
+ UPDATE document_change_journal_state
26
+ SET retained_entries = (SELECT COUNT(*) FROM document_changes),
27
+ retained_bytes = (
28
+ SELECT COALESCE(SUM(byte_size), 0) FROM document_changes
29
+ )
30
+ WHERE singleton_id = 1;
31
+ `);
32
+ },
33
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Migration: saved Capsule registration epoch for race-free scheduler drains.
3
+ *
4
+ * Registration updates rewind the durable journal high-water mark and advance
5
+ * this epoch in the same transaction. A scheduler drain may advance the
6
+ * high-water mark only when no registration changed since the drain began.
7
+ */
8
+
9
+ import type { Database } from "bun:sqlite";
10
+
11
+ import type { Migration } from "./runner";
12
+
13
+ export const migration: Migration = {
14
+ version: 18,
15
+ name: "saved_capsule_registration_epoch",
16
+
17
+ up(db: Database): void {
18
+ db.exec(`
19
+ ALTER TABLE saved_capsule_reverification_state
20
+ ADD COLUMN registration_epoch INTEGER NOT NULL DEFAULT 0
21
+ CHECK (registration_epoch >= 0);
22
+ `);
23
+ },
24
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Migration: per-registration generations for receipt persistence CAS.
3
+ *
4
+ * Existing rows receive unique generations above the durable global epoch.
5
+ * Future upserts allocate from that same monotonic epoch, so delete/recreate
6
+ * cannot reuse a verification generation even for identical Capsule bytes.
7
+ */
8
+
9
+ import type { Database } from "bun:sqlite";
10
+
11
+ import type { Migration } from "./runner";
12
+
13
+ export const migration: Migration = {
14
+ version: 19,
15
+ name: "saved_capsule_registration_generation",
16
+
17
+ up(db: Database): void {
18
+ db.exec(`
19
+ ALTER TABLE saved_capsule_registrations
20
+ ADD COLUMN registration_generation INTEGER NOT NULL DEFAULT 0
21
+ CHECK (registration_generation >= 0);
22
+ `);
23
+ const currentEpoch =
24
+ db
25
+ .query<{ registration_epoch: number }, []>(
26
+ `SELECT registration_epoch
27
+ FROM saved_capsule_reverification_state
28
+ WHERE singleton_id = 1`
29
+ )
30
+ .get()?.registration_epoch ?? 0;
31
+ const rows = db
32
+ .query<{ registration_id: string }, []>(
33
+ `SELECT registration_id
34
+ FROM saved_capsule_registrations
35
+ ORDER BY registration_id ASC`
36
+ )
37
+ .all();
38
+ const update = db.prepare(
39
+ `UPDATE saved_capsule_registrations
40
+ SET registration_generation = ?
41
+ WHERE registration_id = ?`
42
+ );
43
+ for (const [index, row] of rows.entries()) {
44
+ update.run(currentEpoch + index + 1, row.registration_id);
45
+ }
46
+ db.run(
47
+ `UPDATE saved_capsule_reverification_state
48
+ SET registration_epoch = ?
49
+ WHERE singleton_id = 1`,
50
+ [currentEpoch + rows.length]
51
+ );
52
+ },
53
+ };
@@ -28,6 +28,11 @@ import { migration as m011 } from "./011-doc-edge-traversal-indexes";
28
28
  import { migration as m012 } from "./012-activation-receipts";
29
29
  import { migration as m013 } from "./013-fts-sync-marker";
30
30
  import { migration as m014 } from "./014-retrieval-traces";
31
+ import { migration as m015 } from "./015-document-change-journal";
32
+ import { migration as m016 } from "./016-saved-capsules";
33
+ import { migration as m017 } from "./017-document-change-retention-counters";
34
+ import { migration as m018 } from "./018-saved-capsule-registration-epoch";
35
+ import { migration as m019 } from "./019-saved-capsule-registration-generation";
31
36
 
32
37
  /** All migrations in order */
33
38
  export const migrations = [
@@ -45,4 +50,9 @@ export const migrations = [
45
50
  m012,
46
51
  m013,
47
52
  m014,
53
+ m015,
54
+ m016,
55
+ m017,
56
+ m018,
57
+ m019,
48
58
  ];
@@ -35,6 +35,12 @@ import type {
35
35
  DocLinkInput,
36
36
  DocLinkRow,
37
37
  DocLinkSource,
38
+ DocumentChangeKind,
39
+ DocumentChangeListOptions,
40
+ DocumentChangePage,
41
+ DocumentChangePurgeResult,
42
+ DocumentChangeRetentionPolicy,
43
+ DocumentChangeRetentionResult,
38
44
  DocumentInput,
39
45
  DocumentRow,
40
46
  EmbeddingCleanupStats,
@@ -70,6 +76,13 @@ import type {
70
76
  RetrievalTraceRow,
71
77
  RetrievalTraceRunInput,
72
78
  RetrievalTraceTerminalStatus,
79
+ RenameDocumentOptions,
80
+ SavedCapsuleRegistrationInput,
81
+ SavedCapsuleRegistrationRecord,
82
+ SavedCapsuleRegistrationSnapshot,
83
+ SavedCapsuleReverificationState,
84
+ SavedCapsuleVerificationExpectation,
85
+ SavedCapsuleVerificationRecord,
73
86
  StorePort,
74
87
  StoreResult,
75
88
  TagCount,
@@ -95,6 +108,25 @@ import { getSchemaVersion, migrations, runMigrations } from "../migrations";
95
108
  import { err, ok } from "../types";
96
109
  import { getStoredEmbeddingFingerprint } from "../vector/freshness";
97
110
  import { modelTableName } from "../vector/sqlite-vec";
111
+ import {
112
+ deleteSavedCapsuleRegistration as deleteStoredSavedCapsuleRegistration,
113
+ getSavedCapsuleRegistration as getStoredSavedCapsuleRegistration,
114
+ getSavedCapsuleRegistrationSnapshot as getStoredSavedCapsuleRegistrationSnapshot,
115
+ getSavedCapsuleReverificationState as getStoredSavedCapsuleReverificationState,
116
+ getSavedCapsuleReverificationSequence as getStoredSavedCapsuleReverificationSequence,
117
+ listSavedCapsuleIdsAffectedByChanges as listStoredSavedCapsuleIdsAffectedByChanges,
118
+ listSavedCapsuleRegistrations as listStoredSavedCapsuleRegistrations,
119
+ setSavedCapsuleReverificationSequence as setStoredSavedCapsuleReverificationSequence,
120
+ upsertSavedCapsuleRegistration as upsertStoredSavedCapsuleRegistration,
121
+ upsertSavedCapsuleVerification as upsertStoredSavedCapsuleVerification,
122
+ } from "./capsule-registry-store";
123
+ import {
124
+ appendDocumentChange as appendStoredDocumentChange,
125
+ enforceDocumentChangeRetention as enforceStoredDocumentChangeRetention,
126
+ listDocumentChanges as listStoredDocumentChanges,
127
+ purgeDocumentChanges as purgeStoredDocumentChanges,
128
+ snapshotDocumentChange,
129
+ } from "./change-journal-store";
98
130
  import { loadFts5Snowball } from "./fts5-snowball";
99
131
  import {
100
132
  appendExportManifest as appendStoredTraceExportManifest,
@@ -940,6 +972,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
940
972
  const uri = buildUri(doc.collection, doc.relPath);
941
973
 
942
974
  const transaction = db.transaction((): UpsertDocumentResult => {
975
+ const previousRow = db
976
+ .query<DbDocumentRow, [string, string]>(
977
+ "SELECT * FROM documents WHERE collection = ? AND rel_path = ?"
978
+ )
979
+ .get(doc.collection, doc.relPath);
980
+
943
981
  db.run(
944
982
  `
945
983
  INSERT INTO documents (
@@ -1030,6 +1068,41 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1030
1068
  ]);
1031
1069
  }
1032
1070
 
1071
+ if (doc.changeJournal !== false) {
1072
+ const nextRow = db
1073
+ .query<DbDocumentRow, [number]>(
1074
+ "SELECT * FROM documents WHERE id = ?"
1075
+ )
1076
+ .get(idRow.id);
1077
+ if (!nextRow) {
1078
+ throw new Error("Failed to read document after upsert");
1079
+ }
1080
+ const previous = previousRow
1081
+ ? snapshotDocumentChange(mapDocumentRow(previousRow))
1082
+ : null;
1083
+ const next = snapshotDocumentChange(mapDocumentRow(nextRow));
1084
+ const kind: DocumentChangeKind | null =
1085
+ previous === null
1086
+ ? "create"
1087
+ : !previous.active
1088
+ ? "reactivate"
1089
+ : previous.sourceHash !== next.sourceHash ||
1090
+ previous.mirrorHash !== next.mirrorHash
1091
+ ? "update"
1092
+ : null;
1093
+ if (kind) {
1094
+ appendStoredDocumentChange(db, {
1095
+ documentId: idRow.id,
1096
+ collection: doc.collection,
1097
+ kind,
1098
+ oldSnapshot: previous,
1099
+ newSnapshot: next,
1100
+ structureDelta: doc.changeJournal?.structureDelta,
1101
+ observedAtMs: doc.changeJournal?.observedAtMs ?? Date.now(),
1102
+ });
1103
+ }
1104
+ }
1105
+
1033
1106
  return { id: idRow.id, docid };
1034
1107
  });
1035
1108
 
@@ -1043,6 +1116,82 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1043
1116
  }
1044
1117
  }
1045
1118
 
1119
+ async renameDocument(
1120
+ collection: string,
1121
+ oldRelPath: string,
1122
+ newRelPath: string,
1123
+ options: RenameDocumentOptions = {}
1124
+ ): Promise<StoreResult<DocumentRow>> {
1125
+ try {
1126
+ if (!oldRelPath || !newRelPath) {
1127
+ return err("INVALID_INPUT", "Rename paths must be non-empty");
1128
+ }
1129
+ const db = this.ensureOpen();
1130
+ const transaction = db.transaction((): DocumentRow => {
1131
+ const oldRow = db
1132
+ .query<DbDocumentRow, [string, string]>(
1133
+ "SELECT * FROM documents WHERE collection = ? AND rel_path = ?"
1134
+ )
1135
+ .get(collection, oldRelPath);
1136
+ if (!oldRow) {
1137
+ throw new Error("DOCUMENT_RENAME_NOT_FOUND");
1138
+ }
1139
+ if (oldRelPath === newRelPath) {
1140
+ return mapDocumentRow(oldRow);
1141
+ }
1142
+
1143
+ const newUri = buildUri(collection, newRelPath);
1144
+ db.run(
1145
+ `UPDATE documents
1146
+ SET rel_path = ?, uri = ?, updated_at = datetime('now')
1147
+ WHERE id = ?`,
1148
+ [newRelPath, newUri, oldRow.id]
1149
+ );
1150
+ db.run("UPDATE documents_fts SET filepath = ? WHERE rowid = ?", [
1151
+ newRelPath,
1152
+ oldRow.id,
1153
+ ]);
1154
+ const nextRow = db
1155
+ .query<DbDocumentRow, [number]>(
1156
+ "SELECT * FROM documents WHERE id = ?"
1157
+ )
1158
+ .get(oldRow.id);
1159
+ if (!nextRow) {
1160
+ throw new Error("Failed to read document after rename");
1161
+ }
1162
+ const oldSnapshot = snapshotDocumentChange(mapDocumentRow(oldRow));
1163
+ const nextDocument = mapDocumentRow(nextRow);
1164
+ appendStoredDocumentChange(db, {
1165
+ documentId: oldRow.id,
1166
+ collection,
1167
+ kind: "rename",
1168
+ oldSnapshot,
1169
+ newSnapshot: snapshotDocumentChange(nextDocument),
1170
+ structureDelta: options.structureDelta,
1171
+ observedAtMs: options.observedAtMs ?? Date.now(),
1172
+ });
1173
+ return nextDocument;
1174
+ });
1175
+
1176
+ return ok(transaction());
1177
+ } catch (cause) {
1178
+ if (
1179
+ cause instanceof Error &&
1180
+ cause.message === "DOCUMENT_RENAME_NOT_FOUND"
1181
+ ) {
1182
+ return err(
1183
+ "NOT_FOUND",
1184
+ `Document not found for rename: ${collection}/${oldRelPath}`
1185
+ );
1186
+ }
1187
+ return err(
1188
+ "QUERY_FAILED",
1189
+ cause instanceof Error ? cause.message : "Failed to rename document",
1190
+ cause
1191
+ );
1192
+ }
1193
+ }
1194
+
1046
1195
  async getDocument(
1047
1196
  collection: string,
1048
1197
  relPath: string
@@ -1453,14 +1602,45 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1453
1602
  return ok(0);
1454
1603
  }
1455
1604
 
1456
- const placeholders = relPaths.map(() => "?").join(",");
1457
- const result = db.run(
1458
- `UPDATE documents SET active = 0, updated_at = datetime('now')
1459
- WHERE collection = ? AND rel_path IN (${placeholders})`,
1460
- [collection, ...relPaths]
1461
- );
1605
+ const uniquePaths = [...new Set(relPaths)];
1606
+ const placeholders = uniquePaths.map(() => "?").join(",");
1607
+ const transaction = db.transaction((): number => {
1608
+ const activeRows = db
1609
+ .query<DbDocumentRow, (string | number)[]>(
1610
+ `SELECT *
1611
+ FROM documents
1612
+ WHERE collection = ?
1613
+ AND active = 1
1614
+ AND rel_path IN (${placeholders})
1615
+ ORDER BY rel_path ASC, id ASC`
1616
+ )
1617
+ .all(collection, ...uniquePaths);
1618
+ if (activeRows.length === 0) {
1619
+ return 0;
1620
+ }
1621
+ const result = db.run(
1622
+ `UPDATE documents SET active = 0, updated_at = datetime('now')
1623
+ WHERE collection = ?
1624
+ AND active = 1
1625
+ AND rel_path IN (${placeholders})`,
1626
+ [collection, ...uniquePaths]
1627
+ );
1628
+ const observedAtMs = Date.now();
1629
+ for (const activeRow of activeRows) {
1630
+ const previous = snapshotDocumentChange(mapDocumentRow(activeRow));
1631
+ appendStoredDocumentChange(db, {
1632
+ documentId: activeRow.id,
1633
+ collection,
1634
+ kind: "inactivate",
1635
+ oldSnapshot: previous,
1636
+ newSnapshot: { ...previous, active: false },
1637
+ observedAtMs,
1638
+ });
1639
+ }
1640
+ return result.changes;
1641
+ });
1462
1642
 
1463
- return ok(result.changes);
1643
+ return ok(transaction());
1464
1644
  } catch (cause) {
1465
1645
  return err(
1466
1646
  "QUERY_FAILED",
@@ -1472,6 +1652,110 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1472
1652
  }
1473
1653
  }
1474
1654
 
1655
+ async listDocumentChanges(
1656
+ options: DocumentChangeListOptions = {}
1657
+ ): Promise<StoreResult<DocumentChangePage>> {
1658
+ return listStoredDocumentChanges(this.ensureOpen(), options);
1659
+ }
1660
+
1661
+ async enforceDocumentChangeRetention(
1662
+ policy: DocumentChangeRetentionPolicy,
1663
+ nowMs: number
1664
+ ): Promise<StoreResult<DocumentChangeRetentionResult>> {
1665
+ return enforceStoredDocumentChangeRetention(
1666
+ this.ensureOpen(),
1667
+ policy,
1668
+ nowMs
1669
+ );
1670
+ }
1671
+
1672
+ async purgeDocumentChanges(): Promise<
1673
+ StoreResult<DocumentChangePurgeResult>
1674
+ > {
1675
+ return purgeStoredDocumentChanges(this.ensureOpen());
1676
+ }
1677
+
1678
+ async upsertSavedCapsuleRegistration(
1679
+ input: SavedCapsuleRegistrationInput
1680
+ ): Promise<StoreResult<SavedCapsuleRegistrationRecord>> {
1681
+ return upsertStoredSavedCapsuleRegistration(this.ensureOpen(), input);
1682
+ }
1683
+
1684
+ async listSavedCapsuleRegistrations(): Promise<
1685
+ StoreResult<SavedCapsuleRegistrationRecord[]>
1686
+ > {
1687
+ return listStoredSavedCapsuleRegistrations(this.ensureOpen());
1688
+ }
1689
+
1690
+ async getSavedCapsuleRegistration(
1691
+ registrationId: string
1692
+ ): Promise<StoreResult<SavedCapsuleRegistrationRecord | null>> {
1693
+ return getStoredSavedCapsuleRegistration(this.ensureOpen(), registrationId);
1694
+ }
1695
+
1696
+ async getSavedCapsuleRegistrationSnapshot(
1697
+ registrationId: string
1698
+ ): Promise<StoreResult<SavedCapsuleRegistrationSnapshot | null>> {
1699
+ return getStoredSavedCapsuleRegistrationSnapshot(
1700
+ this.ensureOpen(),
1701
+ registrationId
1702
+ );
1703
+ }
1704
+
1705
+ async deleteSavedCapsuleRegistration(
1706
+ registrationId: string
1707
+ ): Promise<StoreResult<boolean>> {
1708
+ return deleteStoredSavedCapsuleRegistration(
1709
+ this.ensureOpen(),
1710
+ registrationId
1711
+ );
1712
+ }
1713
+
1714
+ async listSavedCapsuleIdsAffectedByChanges(
1715
+ afterSequence: number,
1716
+ throughSequence: number,
1717
+ limit: number
1718
+ ): Promise<StoreResult<{ registrationIds: string[]; truncated: boolean }>> {
1719
+ return listStoredSavedCapsuleIdsAffectedByChanges(
1720
+ this.ensureOpen(),
1721
+ afterSequence,
1722
+ throughSequence,
1723
+ limit
1724
+ );
1725
+ }
1726
+
1727
+ async upsertSavedCapsuleVerification(
1728
+ verification: SavedCapsuleVerificationRecord,
1729
+ expectedRegistration: SavedCapsuleVerificationExpectation
1730
+ ): Promise<StoreResult<boolean>> {
1731
+ return upsertStoredSavedCapsuleVerification(
1732
+ this.ensureOpen(),
1733
+ verification,
1734
+ expectedRegistration
1735
+ );
1736
+ }
1737
+
1738
+ async getSavedCapsuleReverificationSequence(): Promise<StoreResult<number>> {
1739
+ return getStoredSavedCapsuleReverificationSequence(this.ensureOpen());
1740
+ }
1741
+
1742
+ async getSavedCapsuleReverificationState(): Promise<
1743
+ StoreResult<SavedCapsuleReverificationState>
1744
+ > {
1745
+ return getStoredSavedCapsuleReverificationState(this.ensureOpen());
1746
+ }
1747
+
1748
+ async setSavedCapsuleReverificationSequence(
1749
+ sequence: number,
1750
+ expectedRegistrationEpoch: number
1751
+ ): Promise<StoreResult<boolean>> {
1752
+ return setStoredSavedCapsuleReverificationSequence(
1753
+ this.ensureOpen(),
1754
+ sequence,
1755
+ expectedRegistrationEpoch
1756
+ );
1757
+ }
1758
+
1475
1759
  // ─────────────────────────────────────────────────────────────────────────
1476
1760
  // Content
1477
1761
  // ─────────────────────────────────────────────────────────────────────────