@lemoncat7/dsh-knowledge 2.2.0 → 2.2.7
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/docs/architecture.md +5 -5
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +13 -23
- package/lib/api.js.map +1 -1
- package/lib/async-pool.d.ts +7 -0
- package/lib/async-pool.d.ts.map +1 -0
- package/lib/async-pool.js +21 -0
- package/lib/async-pool.js.map +1 -0
- package/lib/client.js +1 -1
- package/lib/client.js.map +1 -1
- package/lib/extraction.d.ts.map +1 -1
- package/lib/extraction.js +3 -2
- package/lib/extraction.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +13 -6
- package/lib/index.js.map +1 -1
- package/lib/local-provider.d.ts +11 -0
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +223 -68
- package/lib/local-provider.js.map +1 -1
- package/lib/note-tools.js +4 -4
- package/lib/note-tools.js.map +1 -1
- package/lib/provider-router.d.ts +6 -2
- package/lib/provider-router.d.ts.map +1 -1
- package/lib/provider-router.js +10 -6
- package/lib/provider-router.js.map +1 -1
- package/lib/provider.d.ts +3 -1
- package/lib/provider.d.ts.map +1 -1
- package/lib/remote-provider.d.ts +3 -1
- package/lib/remote-provider.d.ts.map +1 -1
- package/lib/remote-provider.js +17 -0
- package/lib/remote-provider.js.map +1 -1
- package/lib/retrieval.d.ts.map +1 -1
- package/lib/retrieval.js +9 -7
- package/lib/retrieval.js.map +1 -1
- package/lib/tool-authorization.d.ts +3 -1
- package/lib/tool-authorization.d.ts.map +1 -1
- package/lib/tool-authorization.js +27 -14
- package/lib/tool-authorization.js.map +1 -1
- package/lib/tools.js +1 -1
- package/lib/tools.js.map +1 -1
- package/lib/tracking.d.ts.map +1 -1
- package/lib/tracking.js +8 -3
- package/lib/tracking.js.map +1 -1
- package/lib/web-workspace-effects.d.ts.map +1 -1
- package/lib/web-workspace-effects.js +10 -1
- package/lib/web-workspace-effects.js.map +1 -1
- package/lib/web.d.ts.map +1 -1
- package/lib/web.js +7 -4
- package/lib/web.js.map +1 -1
- package/package.json +2 -2
- package/web/app.js +215 -21
- package/web/styles.css +31 -17
- package/web/workspace-effects.js +2 -2
package/lib/local-provider.js
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdirSync } from 'node:fs';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { DatabaseSync } from 'node:sqlite';
|
|
6
6
|
import { contentHash, DEFAULT_KNOWLEDGE_BASE_ID, newId, normalizeDraft, normalizeKnowledgeBaseDraft, normalizeKnowledgeMountDraft, normalizeKnowledgeSettings, nowIso, } from './domain.js';
|
|
7
|
-
import { renderKnowledgeMarkdown } from './documents/markdown.js';
|
|
7
|
+
import { markdownHash, renderKnowledgeMarkdown } from './documents/markdown.js';
|
|
8
8
|
import { knowledgeDocumentPath } from './documents/path.js';
|
|
9
9
|
import { KnowledgeDocumentStore } from './documents/store.js';
|
|
10
10
|
import { enqueueDocumentProjection } from './documents/projection-queue.js';
|
|
@@ -33,10 +33,11 @@ export class LocalKnowledgeProvider {
|
|
|
33
33
|
if (path !== ':memory:')
|
|
34
34
|
mkdirSync(dirname(path), { recursive: true });
|
|
35
35
|
const inMemory = path === ':memory:';
|
|
36
|
-
|
|
37
|
-
? join(tmpdir(), `dsh-knowledge
|
|
38
|
-
:
|
|
39
|
-
this.
|
|
36
|
+
const storageRoot = inMemory
|
|
37
|
+
? join(tmpdir(), `dsh-knowledge-${randomUUID()}`)
|
|
38
|
+
: dirname(path);
|
|
39
|
+
this.notes = new NoteStore(inMemory ? storageRoot : join(storageRoot, 'notes'), inMemory);
|
|
40
|
+
this.documentStore = new KnowledgeDocumentStore(join(storageRoot, 'documents'));
|
|
40
41
|
this.db = new DatabaseSync(path);
|
|
41
42
|
this.db.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
|
|
42
43
|
this.migrate();
|
|
@@ -44,7 +45,7 @@ export class LocalKnowledgeProvider {
|
|
|
44
45
|
}
|
|
45
46
|
migrate() {
|
|
46
47
|
let version = Number(this.db.prepare('PRAGMA user_version').get().user_version ?? 0);
|
|
47
|
-
if (version >
|
|
48
|
+
if (version > 12)
|
|
48
49
|
throw new Error(`knowledge database schema ${version} is newer than this plugin supports`);
|
|
49
50
|
if (version === 0)
|
|
50
51
|
this.db.exec(`
|
|
@@ -296,6 +297,20 @@ export class LocalKnowledgeProvider {
|
|
|
296
297
|
throw error;
|
|
297
298
|
}
|
|
298
299
|
}
|
|
300
|
+
if (version <= 10)
|
|
301
|
+
version = 11;
|
|
302
|
+
if (version === 11)
|
|
303
|
+
this.db.exec(`
|
|
304
|
+
BEGIN IMMEDIATE;
|
|
305
|
+
CREATE INDEX IF NOT EXISTS knowledge_documents_index_order ON knowledge_documents(
|
|
306
|
+
knowledge_base_id,
|
|
307
|
+
(CASE WHEN rel_path='README.md' THEN 0 ELSE 1 END),
|
|
308
|
+
rel_path,
|
|
309
|
+
id
|
|
310
|
+
);
|
|
311
|
+
PRAGMA user_version = 12;
|
|
312
|
+
COMMIT;
|
|
313
|
+
`);
|
|
299
314
|
// Alpha v2 used a migration note as the default base's routing description.
|
|
300
315
|
// Clear only that exact placeholder so existing user-authored descriptions stay untouched.
|
|
301
316
|
this.db.prepare("UPDATE knowledge_bases SET description='' WHERE id=? AND description=?")
|
|
@@ -371,7 +386,7 @@ export class LocalKnowledgeProvider {
|
|
|
371
386
|
id,name,description,default_tags_json,extraction_instructions,writeback_policy,writeback_provider,writeback_model,status,created_at,updated_at
|
|
372
387
|
) VALUES(?,?,?,?,?,?,?,?,'active',?,?)
|
|
373
388
|
`).run(base.id, base.name, base.description, JSON.stringify(base.defaultTags), base.extractionInstructions, base.writebackPolicy, base.writebackProvider ?? null, base.writebackModel ?? null, timestamp, timestamp);
|
|
374
|
-
await this.
|
|
389
|
+
await this.syncKnowledgeBaseManifestQueued(base.id);
|
|
375
390
|
return base;
|
|
376
391
|
}
|
|
377
392
|
async updateKnowledgeBase(id, input) {
|
|
@@ -393,7 +408,7 @@ export class LocalKnowledgeProvider {
|
|
|
393
408
|
name=?,description=?,default_tags_json=?,extraction_instructions=?,writeback_policy=?,writeback_provider=?,writeback_model=?,updated_at=?
|
|
394
409
|
WHERE id=?
|
|
395
410
|
`).run(updated.name, updated.description, JSON.stringify(updated.defaultTags), updated.extractionInstructions, updated.writebackPolicy, updated.writebackProvider ?? null, updated.writebackModel ?? null, updated.updatedAt, id);
|
|
396
|
-
await this.
|
|
411
|
+
await this.syncKnowledgeBaseManifestQueued(id);
|
|
397
412
|
return updated;
|
|
398
413
|
}
|
|
399
414
|
async patchKnowledgeBase(id, patch) {
|
|
@@ -642,11 +657,12 @@ export class LocalKnowledgeProvider {
|
|
|
642
657
|
resolved.set(mount.knowledgeBaseId, { mount, inheritedFrom: 'project' });
|
|
643
658
|
for (const mount of session)
|
|
644
659
|
resolved.set(mount.knowledgeBaseId, { mount });
|
|
660
|
+
const bases = new Map((await this.listKnowledgeBases()).map(base => [base.id, base]));
|
|
645
661
|
const output = [];
|
|
646
662
|
for (const { mount, inheritedFrom } of resolved.values()) {
|
|
647
663
|
if (!mount.enabled)
|
|
648
664
|
continue;
|
|
649
|
-
const base =
|
|
665
|
+
const base = bases.get(mount.knowledgeBaseId);
|
|
650
666
|
if (base === undefined || base.status !== 'active')
|
|
651
667
|
continue;
|
|
652
668
|
output.push({ ...mount, base, ...inheritedFrom === undefined ? {} : { inheritedFrom } });
|
|
@@ -823,6 +839,24 @@ export class LocalKnowledgeProvider {
|
|
|
823
839
|
const row = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id = ?`).get(id);
|
|
824
840
|
return row === undefined ? undefined : rowToEntry(row);
|
|
825
841
|
}
|
|
842
|
+
/** Management-only bulk lookup used to avoid one HTTP/SQL round trip per review target. */
|
|
843
|
+
entriesByIds(ids) {
|
|
844
|
+
this.assertOpen();
|
|
845
|
+
const uniqueIds = [...new Set(ids.map(id => id.trim()).filter(Boolean))].slice(0, 100);
|
|
846
|
+
if (uniqueIds.length === 0)
|
|
847
|
+
return [];
|
|
848
|
+
const placeholders = uniqueIds.map(() => '?').join(',');
|
|
849
|
+
const rows = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id IN (${placeholders})`)
|
|
850
|
+
.all(...uniqueIds);
|
|
851
|
+
const entries = new Map(rows.map(row => {
|
|
852
|
+
const entry = rowToEntry(row);
|
|
853
|
+
return [entry.id, entry];
|
|
854
|
+
}));
|
|
855
|
+
return uniqueIds.flatMap(id => {
|
|
856
|
+
const entry = entries.get(id);
|
|
857
|
+
return entry === undefined ? [] : [entry];
|
|
858
|
+
});
|
|
859
|
+
}
|
|
826
860
|
async versions(id) {
|
|
827
861
|
this.assertOpen();
|
|
828
862
|
const rows = this.db.prepare('SELECT * FROM knowledge_versions WHERE knowledge_id = ? ORDER BY version DESC').all(id);
|
|
@@ -832,7 +866,7 @@ export class LocalKnowledgeProvider {
|
|
|
832
866
|
this.assertOpen();
|
|
833
867
|
await this.documentsReady;
|
|
834
868
|
const entry = this.transaction(() => this.insertEntry(draft));
|
|
835
|
-
await this.
|
|
869
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
836
870
|
return entry;
|
|
837
871
|
}
|
|
838
872
|
insertEntry(input) {
|
|
@@ -859,11 +893,8 @@ export class LocalKnowledgeProvider {
|
|
|
859
893
|
async update(id, draft) {
|
|
860
894
|
this.assertOpen();
|
|
861
895
|
await this.documentsReady;
|
|
862
|
-
const current = await this.get(id);
|
|
863
896
|
const entry = this.transaction(() => this.updateEntry(id, draft, 'update'));
|
|
864
|
-
|
|
865
|
-
await this.syncKnowledgeDocumentsQueued(current.knowledgeBaseId);
|
|
866
|
-
await this.syncKnowledgeDocumentsQueued(entry.knowledgeBaseId);
|
|
897
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
867
898
|
return entry;
|
|
868
899
|
}
|
|
869
900
|
async finalize(id, state, note) {
|
|
@@ -899,7 +930,7 @@ export class LocalKnowledgeProvider {
|
|
|
899
930
|
this.upsertFts(updated);
|
|
900
931
|
return updated;
|
|
901
932
|
});
|
|
902
|
-
await this.
|
|
933
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
903
934
|
return entry;
|
|
904
935
|
}
|
|
905
936
|
async reopen(id) {
|
|
@@ -931,7 +962,42 @@ export class LocalKnowledgeProvider {
|
|
|
931
962
|
this.upsertFts(updated);
|
|
932
963
|
return updated;
|
|
933
964
|
});
|
|
934
|
-
await this.
|
|
965
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
966
|
+
return entry;
|
|
967
|
+
}
|
|
968
|
+
async moveDocument(id, knowledgeBaseId) {
|
|
969
|
+
this.assertOpen();
|
|
970
|
+
await this.documentsReady;
|
|
971
|
+
let changed = false;
|
|
972
|
+
const entry = this.transaction(() => {
|
|
973
|
+
const row = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id=?`).get(id);
|
|
974
|
+
if (row === undefined)
|
|
975
|
+
throw notFound('knowledge entry', id);
|
|
976
|
+
const current = rowToEntry(row);
|
|
977
|
+
if (current.status !== 'active')
|
|
978
|
+
throw conflict('only active knowledge documents can be moved');
|
|
979
|
+
if (current.knowledgeBaseId === knowledgeBaseId)
|
|
980
|
+
return current;
|
|
981
|
+
if (this.db.prepare("SELECT id FROM knowledge_bases WHERE id=? AND status='active'").get(knowledgeBaseId) === undefined) {
|
|
982
|
+
throw notFound('active knowledge base', knowledgeBaseId);
|
|
983
|
+
}
|
|
984
|
+
const updated = {
|
|
985
|
+
...current,
|
|
986
|
+
knowledgeBaseId,
|
|
987
|
+
version: current.version + 1,
|
|
988
|
+
updatedAt: nowIso(),
|
|
989
|
+
};
|
|
990
|
+
this.db.prepare(`
|
|
991
|
+
UPDATE knowledge_entries
|
|
992
|
+
SET knowledge_base_id=?,version=?,updated_at=?
|
|
993
|
+
WHERE id=?
|
|
994
|
+
`).run(knowledgeBaseId, updated.version, updated.updatedAt, id);
|
|
995
|
+
this.writeVersion(updated, 'update');
|
|
996
|
+
changed = true;
|
|
997
|
+
return updated;
|
|
998
|
+
});
|
|
999
|
+
if (changed)
|
|
1000
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
935
1001
|
return entry;
|
|
936
1002
|
}
|
|
937
1003
|
updateEntry(id, input, changeKind) {
|
|
@@ -984,7 +1050,7 @@ export class LocalKnowledgeProvider {
|
|
|
984
1050
|
this.db.prepare('DELETE FROM knowledge_fts WHERE knowledge_id = ?').run(id);
|
|
985
1051
|
return updated;
|
|
986
1052
|
});
|
|
987
|
-
await this.
|
|
1053
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
988
1054
|
return entry;
|
|
989
1055
|
}
|
|
990
1056
|
async delete(id) {
|
|
@@ -998,7 +1064,7 @@ export class LocalKnowledgeProvider {
|
|
|
998
1064
|
throw notFound('knowledge entry', id);
|
|
999
1065
|
});
|
|
1000
1066
|
if (current !== undefined)
|
|
1001
|
-
await this.
|
|
1067
|
+
await this.syncKnowledgeEntryQueued(current.id);
|
|
1002
1068
|
}
|
|
1003
1069
|
async listNotes(request = {}) {
|
|
1004
1070
|
this.assertOpen();
|
|
@@ -1138,6 +1204,30 @@ export class LocalKnowledgeProvider {
|
|
|
1138
1204
|
documentTitle: String(row.document_title),
|
|
1139
1205
|
}));
|
|
1140
1206
|
}
|
|
1207
|
+
/** Compatibility path for manually embedded legacy note:// markers. */
|
|
1208
|
+
legacyNoteReferencesForNotes(noteIds) {
|
|
1209
|
+
this.assertOpen();
|
|
1210
|
+
const requested = new Set(noteIds.map(id => id.toLocaleLowerCase()));
|
|
1211
|
+
if (requested.size === 0)
|
|
1212
|
+
return [];
|
|
1213
|
+
const rows = this.db.prepare(`
|
|
1214
|
+
SELECT knowledge_base_id,id,title,content
|
|
1215
|
+
FROM knowledge_documents
|
|
1216
|
+
WHERE content LIKE '%note://note_%'
|
|
1217
|
+
ORDER BY updated_at DESC,id
|
|
1218
|
+
`).all();
|
|
1219
|
+
return rows.flatMap(row => {
|
|
1220
|
+
const references = String(row.content).match(/note:\/\/(note_[a-f0-9]{32})/giu) ?? [];
|
|
1221
|
+
return [...new Set(references.map(value => value.slice('note://'.length).toLocaleLowerCase()))]
|
|
1222
|
+
.filter(noteId => requested.has(noteId))
|
|
1223
|
+
.map(noteId => ({
|
|
1224
|
+
noteId,
|
|
1225
|
+
knowledgeBaseId: String(row.knowledge_base_id),
|
|
1226
|
+
documentId: String(row.id),
|
|
1227
|
+
documentTitle: String(row.title),
|
|
1228
|
+
}));
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1141
1231
|
deleteNoteReferences(noteIds) {
|
|
1142
1232
|
this.assertOpen();
|
|
1143
1233
|
const ids = [...new Set(noteIds)];
|
|
@@ -1157,7 +1247,7 @@ export class LocalKnowledgeProvider {
|
|
|
1157
1247
|
async writeDirect(input, sourceKey) {
|
|
1158
1248
|
this.assertOpen();
|
|
1159
1249
|
await this.documentsReady;
|
|
1160
|
-
let
|
|
1250
|
+
let touchedEntryId;
|
|
1161
1251
|
const result = this.transaction(() => {
|
|
1162
1252
|
const resolution = this.resolveDirectProposal(normalizeProposal(input));
|
|
1163
1253
|
if (resolution.outcome === 'duplicate')
|
|
@@ -1177,7 +1267,7 @@ export class LocalKnowledgeProvider {
|
|
|
1177
1267
|
.run('approved', reviewedAt, resolution.outcome === 'merged'
|
|
1178
1268
|
? 'Automatically merged by direct-write reconciliation.'
|
|
1179
1269
|
: 'Automatically approved by direct-write policy.', candidate.id);
|
|
1180
|
-
|
|
1270
|
+
touchedEntryId = entry.id;
|
|
1181
1271
|
return {
|
|
1182
1272
|
outcome: resolution.outcome,
|
|
1183
1273
|
candidate: {
|
|
@@ -1191,8 +1281,8 @@ export class LocalKnowledgeProvider {
|
|
|
1191
1281
|
entry,
|
|
1192
1282
|
};
|
|
1193
1283
|
});
|
|
1194
|
-
if (
|
|
1195
|
-
await this.
|
|
1284
|
+
if (touchedEntryId !== undefined)
|
|
1285
|
+
await this.syncKnowledgeEntryQueued(touchedEntryId);
|
|
1196
1286
|
return result;
|
|
1197
1287
|
}
|
|
1198
1288
|
insertCandidate(proposal, sourceKey) {
|
|
@@ -1303,6 +1393,7 @@ export class LocalKnowledgeProvider {
|
|
|
1303
1393
|
async review(id, decision) {
|
|
1304
1394
|
this.assertOpen();
|
|
1305
1395
|
await this.documentsReady;
|
|
1396
|
+
let touchedEntryId;
|
|
1306
1397
|
const reviewed = this.transaction(() => {
|
|
1307
1398
|
const row = this.db.prepare('SELECT * FROM knowledge_candidates WHERE id = ?').get(id);
|
|
1308
1399
|
if (row === undefined)
|
|
@@ -1323,7 +1414,7 @@ export class LocalKnowledgeProvider {
|
|
|
1323
1414
|
throw conflict('candidate approval cannot move a document between knowledge bases');
|
|
1324
1415
|
}
|
|
1325
1416
|
assertExpectedReviewVersion(target, decision.expectedVersion);
|
|
1326
|
-
this.updateEntry(candidate.targetId, editedKnowledgeDraft(target, draft), 'update');
|
|
1417
|
+
touchedEntryId = this.updateEntry(candidate.targetId, editedKnowledgeDraft(target, draft), 'update').id;
|
|
1327
1418
|
}
|
|
1328
1419
|
else if (candidate.action === 'conflict') {
|
|
1329
1420
|
if (decision.resolution !== 'merge') {
|
|
@@ -1338,7 +1429,7 @@ export class LocalKnowledgeProvider {
|
|
|
1338
1429
|
const applied = applyCandidateToTarget(target, candidate, true);
|
|
1339
1430
|
if (!applied.ok)
|
|
1340
1431
|
throw conflict(`${applied.reason}; edit the current document to resolve this conflict`);
|
|
1341
|
-
this.updateEntry(candidate.targetId, applied.draft, 'update');
|
|
1432
|
+
touchedEntryId = this.updateEntry(candidate.targetId, applied.draft, 'update').id;
|
|
1342
1433
|
}
|
|
1343
1434
|
else {
|
|
1344
1435
|
const resolution = this.resolveDirectProposal({
|
|
@@ -1366,10 +1457,10 @@ export class LocalKnowledgeProvider {
|
|
|
1366
1457
|
if (resolution.outcome === 'finalized')
|
|
1367
1458
|
throw finalizedConflict(resolution.entry);
|
|
1368
1459
|
if (resolution.outcome !== 'duplicate') {
|
|
1369
|
-
|
|
1370
|
-
this.insertEntry(resolution.proposal.draft)
|
|
1371
|
-
|
|
1372
|
-
|
|
1460
|
+
const entry = resolution.proposal.action === 'create'
|
|
1461
|
+
? this.insertEntry(resolution.proposal.draft)
|
|
1462
|
+
: this.updateEntry(resolution.proposal.targetId, resolution.proposal.draft, 'update');
|
|
1463
|
+
touchedEntryId = entry.id;
|
|
1373
1464
|
}
|
|
1374
1465
|
}
|
|
1375
1466
|
}
|
|
@@ -1380,8 +1471,8 @@ export class LocalKnowledgeProvider {
|
|
|
1380
1471
|
.run(status, reviewedAt, note ?? null, id);
|
|
1381
1472
|
return { ...candidate, status, reviewedAt, ...note === undefined ? {} : { reviewNote: note } };
|
|
1382
1473
|
});
|
|
1383
|
-
if (
|
|
1384
|
-
await this.
|
|
1474
|
+
if (touchedEntryId !== undefined)
|
|
1475
|
+
await this.syncKnowledgeEntryQueued(touchedEntryId);
|
|
1385
1476
|
return reviewed;
|
|
1386
1477
|
}
|
|
1387
1478
|
async claimExtraction(sourceKey) {
|
|
@@ -1542,12 +1633,86 @@ export class LocalKnowledgeProvider {
|
|
|
1542
1633
|
syncKnowledgeDocumentsQueued(knowledgeBaseId) {
|
|
1543
1634
|
return this.enqueueDocumentSync(() => this.syncKnowledgeDocuments(knowledgeBaseId));
|
|
1544
1635
|
}
|
|
1636
|
+
syncKnowledgeBaseManifestQueued(knowledgeBaseId) {
|
|
1637
|
+
return this.enqueueDocumentSync(async () => {
|
|
1638
|
+
const row = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?').get(knowledgeBaseId);
|
|
1639
|
+
if (row !== undefined)
|
|
1640
|
+
await this.documentStore.ensureBase(rowToKnowledgeBase(row));
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
syncKnowledgeEntryQueued(entryId) {
|
|
1644
|
+
return this.enqueueDocumentSync(() => this.syncKnowledgeEntry(entryId));
|
|
1645
|
+
}
|
|
1646
|
+
/** Keep the derived Markdown projection proportional to one changed entry. */
|
|
1647
|
+
async syncKnowledgeEntry(entryId) {
|
|
1648
|
+
const projected = this.db.prepare(`
|
|
1649
|
+
SELECT id,knowledge_base_id,rel_path FROM knowledge_documents WHERE id=?
|
|
1650
|
+
`).get(entryId);
|
|
1651
|
+
const entryRow = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id=?`).get(entryId);
|
|
1652
|
+
if (entryRow === undefined || entryRow.status !== 'active') {
|
|
1653
|
+
if (projected !== undefined)
|
|
1654
|
+
await this.removeProjectedDocument(projected);
|
|
1655
|
+
this.db.prepare('DELETE FROM knowledge_documents WHERE id=?').run(entryId);
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
const entry = rowToEntry(entryRow);
|
|
1659
|
+
const baseRow = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?').get(entry.knowledgeBaseId);
|
|
1660
|
+
if (baseRow === undefined)
|
|
1661
|
+
return;
|
|
1662
|
+
const base = rowToKnowledgeBase(baseRow);
|
|
1663
|
+
const directory = await this.documentStore.ensureBase(base);
|
|
1664
|
+
const relPath = knowledgeDocumentPath(entry);
|
|
1665
|
+
const markdown = renderEntryMarkdown(entry);
|
|
1666
|
+
const stored = await this.documentStore.writeDocument(directory, relPath, markdown);
|
|
1667
|
+
this.upsertProjectedDocument(entry, relPath, stored.contentHash);
|
|
1668
|
+
if (projected !== undefined && (String(projected.knowledge_base_id) !== entry.knowledgeBaseId
|
|
1669
|
+
|| String(projected.rel_path) !== relPath))
|
|
1670
|
+
await this.removeProjectedDocument(projected);
|
|
1671
|
+
}
|
|
1672
|
+
async removeProjectedDocument(projected) {
|
|
1673
|
+
const baseRow = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?')
|
|
1674
|
+
.get(String(projected.knowledge_base_id));
|
|
1675
|
+
if (baseRow === undefined)
|
|
1676
|
+
return;
|
|
1677
|
+
const directory = this.documentStore.baseDirectory(rowToKnowledgeBase(baseRow));
|
|
1678
|
+
try {
|
|
1679
|
+
await this.documentStore.deleteDocument(directory, String(projected.rel_path));
|
|
1680
|
+
}
|
|
1681
|
+
catch (error) {
|
|
1682
|
+
if (!(error instanceof Error && error.code === 'ENOENT'))
|
|
1683
|
+
throw error;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
upsertProjectedDocument(entry, relPath, projectedHash) {
|
|
1687
|
+
this.db.prepare(`
|
|
1688
|
+
INSERT INTO knowledge_documents(
|
|
1689
|
+
id,knowledge_base_id,rel_path,title,content,entry_count,content_hash,
|
|
1690
|
+
document_state,finalized_at,finalization_note,created_at,updated_at
|
|
1691
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
|
|
1692
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1693
|
+
knowledge_base_id=excluded.knowledge_base_id,rel_path=excluded.rel_path,
|
|
1694
|
+
title=excluded.title,content=excluded.content,entry_count=excluded.entry_count,
|
|
1695
|
+
content_hash=excluded.content_hash,document_state=excluded.document_state,
|
|
1696
|
+
finalized_at=excluded.finalized_at,finalization_note=excluded.finalization_note,
|
|
1697
|
+
updated_at=excluded.updated_at
|
|
1698
|
+
WHERE knowledge_documents.knowledge_base_id<>excluded.knowledge_base_id
|
|
1699
|
+
OR knowledge_documents.content_hash<>excluded.content_hash
|
|
1700
|
+
OR knowledge_documents.rel_path<>excluded.rel_path
|
|
1701
|
+
OR knowledge_documents.title<>excluded.title
|
|
1702
|
+
OR knowledge_documents.entry_count<>excluded.entry_count
|
|
1703
|
+
OR knowledge_documents.document_state<>excluded.document_state
|
|
1704
|
+
OR knowledge_documents.finalized_at IS NOT excluded.finalized_at
|
|
1705
|
+
OR knowledge_documents.finalization_note IS NOT excluded.finalization_note
|
|
1706
|
+
`).run(entry.id, entry.knowledgeBaseId, relPath, entry.title, renderEntryContent(entry), 1, projectedHash, entry.documentState, entry.finalizedAt ?? null, entry.finalizationNote ?? null, entry.createdAt, entry.updatedAt);
|
|
1707
|
+
}
|
|
1545
1708
|
async syncKnowledgeDocuments(knowledgeBaseId) {
|
|
1546
1709
|
const baseRow = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?').get(knowledgeBaseId);
|
|
1547
1710
|
if (baseRow === undefined)
|
|
1548
1711
|
return;
|
|
1549
1712
|
const base = rowToKnowledgeBase(baseRow);
|
|
1550
1713
|
const directory = await this.documentStore.ensureBase(base);
|
|
1714
|
+
const storedDocuments = await this.documentStore.listDocuments(directory);
|
|
1715
|
+
const storedById = new Map(storedDocuments.map(document => [document.metadata.id, document]));
|
|
1551
1716
|
const entries = this.db.prepare(`
|
|
1552
1717
|
SELECT ${ENTRY_COLUMNS} FROM knowledge_entries
|
|
1553
1718
|
WHERE knowledge_base_id=? AND status='active'
|
|
@@ -1556,58 +1721,28 @@ export class LocalKnowledgeProvider {
|
|
|
1556
1721
|
const desired = new Map();
|
|
1557
1722
|
for (const entry of entries) {
|
|
1558
1723
|
const relPath = knowledgeDocumentPath(entry);
|
|
1559
|
-
const markdown =
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
confidence: entry.confidence,
|
|
1566
|
-
status: entry.status,
|
|
1567
|
-
documentState: entry.documentState,
|
|
1568
|
-
...entry.finalizedAt === undefined ? {} : { finalizedAt: entry.finalizedAt },
|
|
1569
|
-
...entry.finalizationNote === undefined ? {} : { finalizationNote: entry.finalizationNote },
|
|
1570
|
-
},
|
|
1571
|
-
title: entry.title,
|
|
1572
|
-
body: entry.body,
|
|
1573
|
-
});
|
|
1574
|
-
const stored = await this.documentStore.writeDocument(directory, relPath, markdown);
|
|
1724
|
+
const markdown = renderEntryMarkdown(entry);
|
|
1725
|
+
const current = storedById.get(entry.id);
|
|
1726
|
+
const contentHash = markdownHash(markdown);
|
|
1727
|
+
const stored = current?.relPath === relPath && current.contentHash === contentHash
|
|
1728
|
+
? current
|
|
1729
|
+
: await this.documentStore.writeDocument(directory, relPath, markdown);
|
|
1575
1730
|
desired.set(entry.id, {
|
|
1576
1731
|
entry,
|
|
1577
1732
|
relPath,
|
|
1578
|
-
content: `# ${markdownHeading(entry.title)}\n\n${entry.body.trim()}\n`,
|
|
1579
1733
|
contentHash: stored.contentHash,
|
|
1580
1734
|
});
|
|
1581
1735
|
}
|
|
1582
|
-
const storedDocuments = await this.documentStore.listDocuments(directory);
|
|
1583
1736
|
for (const document of storedDocuments) {
|
|
1584
1737
|
const expected = desired.get(document.metadata.id);
|
|
1585
1738
|
if (expected === undefined || expected.relPath !== document.relPath) {
|
|
1586
1739
|
await this.documentStore.deleteDocument(directory, document.relPath, document.contentHash);
|
|
1587
1740
|
}
|
|
1588
1741
|
}
|
|
1589
|
-
const existing = this.db.prepare('SELECT id
|
|
1742
|
+
const existing = this.db.prepare('SELECT id FROM knowledge_documents WHERE knowledge_base_id=?')
|
|
1590
1743
|
.all(knowledgeBaseId);
|
|
1591
1744
|
for (const document of desired.values()) {
|
|
1592
|
-
this.
|
|
1593
|
-
INSERT INTO knowledge_documents(
|
|
1594
|
-
id,knowledge_base_id,rel_path,title,content,entry_count,content_hash,
|
|
1595
|
-
document_state,finalized_at,finalization_note,created_at,updated_at
|
|
1596
|
-
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
|
|
1597
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
1598
|
-
knowledge_base_id=excluded.knowledge_base_id,rel_path=excluded.rel_path,
|
|
1599
|
-
title=excluded.title,content=excluded.content,entry_count=excluded.entry_count,
|
|
1600
|
-
content_hash=excluded.content_hash,document_state=excluded.document_state,
|
|
1601
|
-
finalized_at=excluded.finalized_at,finalization_note=excluded.finalization_note,
|
|
1602
|
-
updated_at=excluded.updated_at
|
|
1603
|
-
WHERE knowledge_documents.content_hash<>excluded.content_hash
|
|
1604
|
-
OR knowledge_documents.rel_path<>excluded.rel_path
|
|
1605
|
-
OR knowledge_documents.title<>excluded.title
|
|
1606
|
-
OR knowledge_documents.entry_count<>excluded.entry_count
|
|
1607
|
-
OR knowledge_documents.document_state<>excluded.document_state
|
|
1608
|
-
OR knowledge_documents.finalized_at IS NOT excluded.finalized_at
|
|
1609
|
-
OR knowledge_documents.finalization_note IS NOT excluded.finalization_note
|
|
1610
|
-
`).run(document.entry.id, knowledgeBaseId, document.relPath, document.entry.title, document.content, 1, document.contentHash, document.entry.documentState, document.entry.finalizedAt ?? null, document.entry.finalizationNote ?? null, document.entry.createdAt, document.entry.updatedAt);
|
|
1745
|
+
this.upsertProjectedDocument(document.entry, document.relPath, document.contentHash);
|
|
1611
1746
|
}
|
|
1612
1747
|
for (const row of existing) {
|
|
1613
1748
|
if (!desired.has(String(row.id)))
|
|
@@ -1615,6 +1750,26 @@ export class LocalKnowledgeProvider {
|
|
|
1615
1750
|
}
|
|
1616
1751
|
}
|
|
1617
1752
|
}
|
|
1753
|
+
function renderEntryMarkdown(entry) {
|
|
1754
|
+
return renderKnowledgeMarkdown({
|
|
1755
|
+
metadata: {
|
|
1756
|
+
id: entry.id,
|
|
1757
|
+
type: entry.type,
|
|
1758
|
+
tags: entry.tags,
|
|
1759
|
+
scope: entry.scope,
|
|
1760
|
+
confidence: entry.confidence,
|
|
1761
|
+
status: entry.status,
|
|
1762
|
+
documentState: entry.documentState,
|
|
1763
|
+
...entry.finalizedAt === undefined ? {} : { finalizedAt: entry.finalizedAt },
|
|
1764
|
+
...entry.finalizationNote === undefined ? {} : { finalizationNote: entry.finalizationNote },
|
|
1765
|
+
},
|
|
1766
|
+
title: entry.title,
|
|
1767
|
+
body: entry.body,
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
function renderEntryContent(entry) {
|
|
1771
|
+
return `# ${markdownHeading(entry.title)}\n\n${entry.body.trim()}\n`;
|
|
1772
|
+
}
|
|
1618
1773
|
function markdownHeading(value) {
|
|
1619
1774
|
return value.replace(/[\r\n]+/g, ' ').replace(/^#+\s*/, '').trim();
|
|
1620
1775
|
}
|