@sigrun/knowledge 0.0.0-stage → 0.1.1

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/dist/undo.js ADDED
@@ -0,0 +1,335 @@
1
+ function writeVersion(db, changeId, table, rowId, before, after, at) {
2
+ db.raw()
3
+ .prepare('INSERT INTO versions (changeId, "table", rowId, before, after, at) VALUES (?, ?, ?, ?, ?, ?)')
4
+ .run(changeId, table, rowId, before === undefined || before === null
5
+ ? null
6
+ : JSON.stringify(before), after === undefined || after === null
7
+ ? null
8
+ : JSON.stringify(after), at);
9
+ }
10
+ /**
11
+ * Undo a change: walk its versions newest-first and restore each row's
12
+ * `before` state — but only while the row still equals the change's
13
+ * `after`. A row a later change edited is skipped, so undo restores only
14
+ * what the change last wrote. Every restore is itself recorded as a
15
+ * version under `undo:<changeId>`, so nothing is lost.
16
+ */
17
+ export function undoChange(db, changeId, now) {
18
+ return db.transaction(() => {
19
+ const undoId = `undo:${changeId}`;
20
+ const versions = db
21
+ .raw()
22
+ .prepare('SELECT seq, changeId, "table", rowId, before, after, at FROM versions WHERE changeId = ? ORDER BY seq DESC')
23
+ // SAFETY: the versions table columns are fixed by db.ts's schema;
24
+ // node:sqlite only types rows generically.
25
+ .all(changeId);
26
+ let restored = 0;
27
+ const skipped = [];
28
+ const touched = new Set();
29
+ for (const version of versions) {
30
+ switch (version.table) {
31
+ case "entities": {
32
+ const outcome = undoEntity(db, version, undoId, now);
33
+ if (outcome === "restored") {
34
+ restored += 1;
35
+ touched.add(version.rowId);
36
+ }
37
+ else if (outcome !== "unchanged") {
38
+ skipped.push(outcome);
39
+ }
40
+ break;
41
+ }
42
+ case "roles": {
43
+ const outcome = undoRole(db, version, undoId, now);
44
+ if (outcome === "restored")
45
+ restored += 1;
46
+ else if (outcome !== "unchanged")
47
+ skipped.push(outcome);
48
+ break;
49
+ }
50
+ case "inquiries": {
51
+ const outcome = undoInquiry(db, version, undoId, now);
52
+ if (outcome === "restored")
53
+ restored += 1;
54
+ else if (outcome !== "unchanged")
55
+ skipped.push(outcome);
56
+ break;
57
+ }
58
+ case "types": {
59
+ // Type rows are shared schema: undo restores them only while
60
+ // the row still equals what the change wrote (a later
61
+ // extension of the type blocks the revert, not other
62
+ // types). A type the change created is archived in turn.
63
+ const outcome = undoType(db, version, undoId, now);
64
+ if (outcome === "restored")
65
+ restored += 1;
66
+ else if (outcome !== "unchanged")
67
+ skipped.push(outcome);
68
+ break;
69
+ }
70
+ case "entity_keys": {
71
+ const outcome = undoEntityKey(db, version, undoId, now);
72
+ if (outcome === "restored")
73
+ restored += 1;
74
+ else if (outcome !== "unchanged")
75
+ skipped.push(outcome);
76
+ break;
77
+ }
78
+ }
79
+ }
80
+ return { restored, skipped, touched: [...touched] };
81
+ });
82
+ }
83
+ function undoEntity(db, version, undoId, now) {
84
+ const row = db
85
+ .raw()
86
+ .prepare("SELECT id, typeId, name, keys, fields, attributes, cls, indexedAt, archived FROM entities WHERE id = ?")
87
+ .get(version.rowId);
88
+ if (row === undefined) {
89
+ return `entity ${version.rowId}: no longer present`;
90
+ }
91
+ if (version.before === null) {
92
+ // The change created this entity: undo archives it — but only
93
+ // while the row still equals what the change wrote; a later
94
+ // change's edits (rename, new fields, new keys) stand.
95
+ if (Number(row.archived) !== 0)
96
+ return "unchanged";
97
+ const afterCreate = JSON.parse(version.after ?? "null");
98
+ const differs = afterDiffersFromRow(afterCreate, row, [
99
+ "name",
100
+ "keys",
101
+ "fields",
102
+ "attributes",
103
+ "typeId",
104
+ ]);
105
+ if (differs !== null) {
106
+ return `entity "${String(row.name)}" (${version.rowId}): ${differs}`;
107
+ }
108
+ writeVersion(db, undoId, "entities", version.rowId, row, { ...row, archived: 1 }, now);
109
+ db.raw()
110
+ .prepare("UPDATE entities SET archived = 1 WHERE id = ?")
111
+ .run(version.rowId);
112
+ return "restored";
113
+ }
114
+ const before = JSON.parse(version.before ?? "null");
115
+ const after = JSON.parse(version.after ?? "null");
116
+ // Whole-row equality: a later change touching anything the change
117
+ // wrote (name, keys, attributes, type — not just fields) blocks the
118
+ // undo, so undoing A can never silently revert B's rename.
119
+ const differs = afterDiffersFromRow(after, row, [
120
+ "name",
121
+ "keys",
122
+ "fields",
123
+ "attributes",
124
+ "typeId",
125
+ ]);
126
+ if (before === null || after === null || differs !== null) {
127
+ return `entity "${String(row.name)}" (${version.rowId}): ${differs ?? "changed by a later change"}`;
128
+ }
129
+ const restoredRow = { ...row, ...before, id: row.id, archived: row.archived };
130
+ writeVersion(db, undoId, "entities", version.rowId, row, restoredRow, now);
131
+ db.raw()
132
+ .prepare("UPDATE entities SET typeId = ?, name = ?, keys = ?, fields = ?, attributes = ?, indexedAt = ? WHERE id = ?")
133
+ .run(restoredRow.typeId, restoredRow.name, restoredRow.keys, restoredRow.fields, restoredRow.attributes, restoredRow.indexedAt, version.rowId);
134
+ return "restored";
135
+ }
136
+ /**
137
+ * JSON-stable compare of a version's `after` against the current row for
138
+ * the given columns. Returns null when equal, otherwise a short text
139
+ * naming the first differing column.
140
+ */
141
+ function afterDiffersFromRow(after, row, columns) {
142
+ if (after === null)
143
+ return "changed by a later change";
144
+ for (const column of columns) {
145
+ // Absent key = the version blob doesn't carry that column (partial
146
+ // blobs, e.g. inquiry updates store only conversations/outcome);
147
+ // undefined can only mean missing — a stored null is "null".
148
+ if (after[column] === undefined)
149
+ continue;
150
+ if (JSON.stringify(after[column]) !== JSON.stringify(row[column])) {
151
+ return `${column} changed by a later change`;
152
+ }
153
+ }
154
+ return null;
155
+ }
156
+ function undoRole(db, version, undoId, now) {
157
+ const row = db
158
+ .raw()
159
+ .prepare("SELECT id, inquiryId, entityId, role, reason FROM roles WHERE id = ?")
160
+ .get(Number(version.rowId));
161
+ if (row === undefined)
162
+ return "unchanged";
163
+ if (version.before === null) {
164
+ // The change created this role: undo deletes it.
165
+ const after = JSON.parse(version.after ?? "null");
166
+ const differs = afterDiffersFromRow(after, row, ["role", "reason"]);
167
+ if (differs !== null) {
168
+ return `role ${version.rowId}: ${differs}`;
169
+ }
170
+ writeVersion(db, undoId, "roles", version.rowId, row, null, now);
171
+ db.raw().prepare("DELETE FROM roles WHERE id = ?").run(Number(version.rowId));
172
+ return "restored";
173
+ }
174
+ const before = JSON.parse(version.before ?? "null");
175
+ const after = JSON.parse(version.after ?? "null");
176
+ const differs = afterDiffersFromRow(after, row, ["role", "reason"]);
177
+ if (before === null || after === null || differs !== null) {
178
+ return `role ${version.rowId}: ${differs ?? "changed by a later change"}`;
179
+ }
180
+ writeVersion(db, undoId, "roles", version.rowId, row, { ...row, ...before, id: row.id }, now);
181
+ db.raw()
182
+ .prepare("UPDATE roles SET role = ?, reason = ? WHERE id = ?")
183
+ .run(before.role, before.reason ?? null, Number(version.rowId));
184
+ return "restored";
185
+ }
186
+ function undoInquiry(db, version, undoId, now) {
187
+ const row = db
188
+ .raw()
189
+ .prepare("SELECT id, question, why, startedAt, touchedAt, conversations, outcome, archived FROM inquiries WHERE id = ?")
190
+ .get(Number(version.rowId));
191
+ if (row === undefined)
192
+ return "unchanged";
193
+ if (version.before === null) {
194
+ // The change created this inquiry: undo archives it (records are
195
+ // never deleted). Only while the row still equals what the change
196
+ // wrote.
197
+ if (Number(row.archived) !== 0)
198
+ return "unchanged";
199
+ const after = JSON.parse(version.after ?? "null");
200
+ const differs = afterDiffersFromRow(after, row, [
201
+ "question",
202
+ "why",
203
+ "outcome",
204
+ "conversations",
205
+ ]);
206
+ if (differs !== null) {
207
+ return `inquiry "${String(row.question)}" (${version.rowId}): ${differs}`;
208
+ }
209
+ writeVersion(db, undoId, "inquiries", version.rowId, row, { ...row, archived: 1 }, now);
210
+ db.raw()
211
+ .prepare("UPDATE inquiries SET archived = 1, touchedAt = ? WHERE id = ?")
212
+ .run(now, Number(version.rowId));
213
+ return "restored";
214
+ }
215
+ const before = JSON.parse(version.before ?? "null");
216
+ const after = JSON.parse(version.after ?? "null");
217
+ const differs = afterDiffersFromRow(after, row, [
218
+ "question",
219
+ "why",
220
+ "outcome",
221
+ "conversations",
222
+ ]);
223
+ if (before === null || after === null || differs !== null) {
224
+ return `inquiry "${String(row.question)}" (${version.rowId}): ${differs ?? "changed by a later change"}`;
225
+ }
226
+ const restored = { ...row, ...before, id: row.id };
227
+ writeVersion(db, undoId, "inquiries", version.rowId, row, restored, now);
228
+ db.raw()
229
+ .prepare("UPDATE inquiries SET conversations = ?, outcome = ?, archived = ?, touchedAt = ? WHERE id = ?")
230
+ .run(restored.conversations, restored.outcome ?? null, Number(restored.archived ?? 0), now, Number(version.rowId));
231
+ return "restored";
232
+ }
233
+ function undoEntityKey(db, version, undoId, now) {
234
+ const row = db
235
+ .raw()
236
+ .prepare("SELECT id, entityId, typeFamilyRoot, key, normalized FROM entity_keys WHERE id = ?")
237
+ .get(Number(version.rowId));
238
+ if (version.before !== null) {
239
+ // A re-root (retypeEntities / maintain): restore the old family
240
+ // root while the row still sits at the change's after root.
241
+ if (row === undefined)
242
+ return "unchanged";
243
+ const before = JSON.parse(version.before);
244
+ const after = JSON.parse(version.after ?? "null");
245
+ const differs = afterDiffersFromRow(after, row, ["typeFamilyRoot"]);
246
+ if (differs !== null)
247
+ return `entity_keys ${version.rowId}: ${differs}`;
248
+ writeVersion(db, undoId, "entity_keys", version.rowId, row, before, now);
249
+ db.raw()
250
+ .prepare("UPDATE entity_keys SET typeFamilyRoot = ? WHERE id = ?")
251
+ .run(before.typeFamilyRoot, Number(version.rowId));
252
+ return "restored";
253
+ }
254
+ // applyRecord only ever creates key rows; undo removes them.
255
+ if (row === undefined)
256
+ return "unchanged";
257
+ writeVersion(db, undoId, "entity_keys", version.rowId, row, null, now);
258
+ db.raw()
259
+ .prepare("DELETE FROM entity_keys WHERE id = ?")
260
+ .run(Number(version.rowId));
261
+ return "restored";
262
+ }
263
+ /**
264
+ * Undo a type version: a type the change created is archived (while the
265
+ * row still equals the creation blob); a type the change edited gets its
266
+ * parentId/fields/description back (while the row still equals the
267
+ * change's after). Edits from a later change to the same row block the
268
+ * revert; other rows are never consulted (a type's schema is its own
269
+ * row, so undoing a generalization after a child was extended reverts
270
+ * the child alone).
271
+ */
272
+ function undoType(db, version, undoId, now) {
273
+ // Schema rows written by knowledge_record are shared schema and stay
274
+ // audit-only; only the idle pass's own hierarchy edits (change ids
275
+ // prefixed "maintain:") are reverted, so a generalization is
276
+ // undoable without record-applied types ever being rewritten.
277
+ if (!version.changeId.startsWith("maintain:")) {
278
+ return `type ${version.rowId}: type schema changes are not reverted`;
279
+ }
280
+ const row = db
281
+ .raw()
282
+ .prepare("SELECT id, name, description, parentId, fields, version, archived FROM types WHERE id = ?")
283
+ .get(Number(version.rowId));
284
+ if (row === undefined)
285
+ return "unchanged";
286
+ if (version.before === null) {
287
+ // Created by this change: archive it (schema rows are never
288
+ // deleted) while the row still equals the creation blob.
289
+ if (Number(row.archived) !== 0)
290
+ return "unchanged";
291
+ const afterCreate = JSON.parse(version.after ?? "null");
292
+ const differs = afterDiffersFromRow(afterCreate, row, [
293
+ "name",
294
+ "description",
295
+ "parentId",
296
+ "fields",
297
+ ]);
298
+ if (differs !== null) {
299
+ return `type "${String(row.name)}" (${version.rowId}): ${differs}`;
300
+ }
301
+ // A parent with live children still attached stays live: archiving
302
+ // it would orphan the hierarchy (the child revert was skipped
303
+ // because a later change edited that child).
304
+ const liveChildren = db
305
+ .raw()
306
+ .prepare("SELECT 1 AS x FROM types WHERE parentId = ? AND archived = 0 LIMIT 1")
307
+ .get(Number(version.rowId));
308
+ if (liveChildren !== undefined) {
309
+ return `type "${String(row.name)}" (${version.rowId}): live children still attached`;
310
+ }
311
+ writeVersion(db, undoId, "types", version.rowId, row, { ...row, archived: 1 }, now);
312
+ db.raw()
313
+ .prepare("UPDATE types SET archived = 1, version = version + 1 WHERE id = ?")
314
+ .run(Number(version.rowId));
315
+ return "restored";
316
+ }
317
+ const before = JSON.parse(version.before ?? "null");
318
+ const after = JSON.parse(version.after ?? "null");
319
+ const differs = afterDiffersFromRow(after, row, [
320
+ "name",
321
+ "description",
322
+ "parentId",
323
+ "fields",
324
+ "archived",
325
+ ]);
326
+ if (before === null || after === null || differs !== null) {
327
+ return `type "${String(row.name)}" (${version.rowId}): ${differs ?? "changed by a later change"}`;
328
+ }
329
+ const restoredRow = { ...row, ...before, id: row.id };
330
+ writeVersion(db, undoId, "types", version.rowId, row, restoredRow, now);
331
+ db.raw()
332
+ .prepare("UPDATE types SET description = ?, parentId = ?, fields = ?, archived = ?, version = version + 1 WHERE id = ?")
333
+ .run(restoredRow.description, restoredRow.parentId ?? null, restoredRow.fields, Number(restoredRow.archived ?? 0), Number(version.rowId));
334
+ return "restored";
335
+ }
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
- "name": "@sigrun/knowledge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@sigrun/knowledge",
3
+ "version": "0.1.1",
4
+ "description": "Curated per-user knowledge extension for sigrun (pi-durable)",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.19.0"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "scripts": {
22
+ "test": "node scripts/check-manifest.mjs --expect-name @sigrun/knowledge && vitest run",
23
+ "typecheck": "tsc --noEmit",
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "prepare": "npm run build"
26
+ },
27
+ "peerDependencies": {
28
+ "@earendil-works/chord": "1.4.0",
29
+ "@earendil-works/pi-ai": "1.4.0",
30
+ "@earendil-works/pi-durable": "1.4.0",
31
+ "@sigrun/shared": "0.5.0 || 0.6.0 || 0.6.1 || 0.7.0 || 0.8.0"
32
+ },
33
+ "devDependencies": {
34
+ "@earendil-works/chord": "1.4.0",
35
+ "@earendil-works/pi-ai": "1.4.0",
36
+ "@earendil-works/pi-durable": "1.4.0",
37
+ "@sigrun/shared": "file:../sigrun/shared",
38
+ "@sigrun/sigrun": "file:../sigrun",
39
+ "@types/node": "^26.6.4",
40
+ "tsx": "^4.23.15",
41
+ "typebox": "1.3.27",
42
+ "typescript": "^7.0.2",
43
+ "vitest": "^5.0.3"
44
+ },
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "github",
48
+ "url": "git+https://github.com/sigrun-ai/sigrun-knowledge.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/sigrun-ai/sigrun-knowledge/issues"
52
+ },
53
+ "homepage": "https://github.com/sigrun-ai/sigrun-knowledge#readme",
54
+ "publishConfig": {
55
+ "access": "public",
56
+ "provenance": true
57
+ }
58
+ }