@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.
- package/README.md +19 -4
- package/assets/skill/SKILL.md +46 -15
- package/package.json +1 -1
- package/spec/cli.md +100 -0
- package/spec/db/schema.sql +170 -0
- package/spec/mcp.md +22 -0
- package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
- package/spec/output-schemas/changes.schema.json +280 -0
- package/spec/output-schemas/document-diff.schema.json +185 -0
- package/spec/output-schemas/impact.schema.json +122 -0
- package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
- package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
- package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
- package/src/cli/commands/changes.ts +160 -0
- package/src/cli/commands/context-saved.ts +189 -0
- package/src/cli/options.ts +8 -0
- package/src/cli/program.ts +195 -0
- package/src/core/capsule-registry.ts +279 -0
- package/src/core/capsule-reverification-scheduler.ts +218 -0
- package/src/core/capsule-reverification.ts +289 -0
- package/src/core/change-diff.ts +182 -0
- package/src/core/change-journal.ts +228 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/ingestion/sync.ts +214 -165
- package/src/mcp/tools/changes.ts +80 -0
- package/src/mcp/tools/index.ts +29 -0
- package/src/sdk/client.ts +42 -0
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +22 -0
- package/src/serve/doc-events.ts +12 -1
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +13 -0
- package/src/serve/routes/changes.ts +102 -0
- package/src/serve/server.ts +34 -0
- package/src/serve/watch-service.ts +9 -0
- package/src/store/index.ts +21 -0
- package/src/store/migrations/015-document-change-journal.ts +85 -0
- package/src/store/migrations/016-saved-capsules.ts +131 -0
- package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
- package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
- package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
- package/src/store/migrations/index.ts +10 -0
- package/src/store/sqlite/adapter.ts +291 -7
- package/src/store/sqlite/capsule-registry-store.ts +534 -0
- package/src/store/sqlite/change-journal-store.ts +473 -0
- 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
|
|
1457
|
-
const
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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(
|
|
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
|
// ─────────────────────────────────────────────────────────────────────────
|