@lemoncat7/dsh-knowledge 2.2.1 → 2.2.8
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 +55 -24
- 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.d.ts.map +1 -1
- package/lib/client.js +53 -17
- package/lib/client.js.map +2 -2
- package/lib/domain.d.ts +16 -0
- package/lib/domain.d.ts.map +1 -1
- package/lib/domain.js.map +1 -1
- package/lib/extraction.d.ts +3 -0
- package/lib/extraction.d.ts.map +1 -1
- package/lib/extraction.js +65 -11
- package/lib/extraction.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +101 -29
- package/lib/index.js.map +1 -1
- package/lib/local-provider.d.ts +13 -2
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +307 -73
- 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 +4 -2
- package/lib/provider.d.ts.map +1 -1
- package/lib/remote-provider.d.ts +4 -2
- package/lib/remote-provider.d.ts.map +1 -1
- package/lib/remote-provider.js +22 -2
- 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 > 13)
|
|
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,52 @@ 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
|
+
`);
|
|
314
|
+
if (version <= 11)
|
|
315
|
+
version = 12;
|
|
316
|
+
if (version === 12) {
|
|
317
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
318
|
+
try {
|
|
319
|
+
const extractionTable = this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='extraction_jobs'").get();
|
|
320
|
+
if (extractionTable === undefined) {
|
|
321
|
+
this.db.exec(`
|
|
322
|
+
CREATE TABLE extraction_jobs (
|
|
323
|
+
source_key TEXT PRIMARY KEY,
|
|
324
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
|
|
325
|
+
attempts INTEGER NOT NULL,
|
|
326
|
+
candidate_count INTEGER NOT NULL DEFAULT 0,
|
|
327
|
+
last_error TEXT,
|
|
328
|
+
completion_json TEXT,
|
|
329
|
+
updated_at TEXT NOT NULL
|
|
330
|
+
)
|
|
331
|
+
`);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const extractionColumns = this.db.prepare('PRAGMA table_info(extraction_jobs)').all();
|
|
335
|
+
if (!extractionColumns.some(column => String(column.name) === 'completion_json')) {
|
|
336
|
+
this.db.exec('ALTER TABLE extraction_jobs ADD COLUMN completion_json TEXT');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
this.db.exec('PRAGMA user_version = 13; COMMIT');
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
this.db.exec('ROLLBACK');
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
299
346
|
// Alpha v2 used a migration note as the default base's routing description.
|
|
300
347
|
// Clear only that exact placeholder so existing user-authored descriptions stay untouched.
|
|
301
348
|
this.db.prepare("UPDATE knowledge_bases SET description='' WHERE id=? AND description=?")
|
|
@@ -371,7 +418,7 @@ export class LocalKnowledgeProvider {
|
|
|
371
418
|
id,name,description,default_tags_json,extraction_instructions,writeback_policy,writeback_provider,writeback_model,status,created_at,updated_at
|
|
372
419
|
) VALUES(?,?,?,?,?,?,?,?,'active',?,?)
|
|
373
420
|
`).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.
|
|
421
|
+
await this.syncKnowledgeBaseManifestQueued(base.id);
|
|
375
422
|
return base;
|
|
376
423
|
}
|
|
377
424
|
async updateKnowledgeBase(id, input) {
|
|
@@ -393,7 +440,7 @@ export class LocalKnowledgeProvider {
|
|
|
393
440
|
name=?,description=?,default_tags_json=?,extraction_instructions=?,writeback_policy=?,writeback_provider=?,writeback_model=?,updated_at=?
|
|
394
441
|
WHERE id=?
|
|
395
442
|
`).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.
|
|
443
|
+
await this.syncKnowledgeBaseManifestQueued(id);
|
|
397
444
|
return updated;
|
|
398
445
|
}
|
|
399
446
|
async patchKnowledgeBase(id, patch) {
|
|
@@ -642,11 +689,12 @@ export class LocalKnowledgeProvider {
|
|
|
642
689
|
resolved.set(mount.knowledgeBaseId, { mount, inheritedFrom: 'project' });
|
|
643
690
|
for (const mount of session)
|
|
644
691
|
resolved.set(mount.knowledgeBaseId, { mount });
|
|
692
|
+
const bases = new Map((await this.listKnowledgeBases()).map(base => [base.id, base]));
|
|
645
693
|
const output = [];
|
|
646
694
|
for (const { mount, inheritedFrom } of resolved.values()) {
|
|
647
695
|
if (!mount.enabled)
|
|
648
696
|
continue;
|
|
649
|
-
const base =
|
|
697
|
+
const base = bases.get(mount.knowledgeBaseId);
|
|
650
698
|
if (base === undefined || base.status !== 'active')
|
|
651
699
|
continue;
|
|
652
700
|
output.push({ ...mount, base, ...inheritedFrom === undefined ? {} : { inheritedFrom } });
|
|
@@ -823,6 +871,24 @@ export class LocalKnowledgeProvider {
|
|
|
823
871
|
const row = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id = ?`).get(id);
|
|
824
872
|
return row === undefined ? undefined : rowToEntry(row);
|
|
825
873
|
}
|
|
874
|
+
/** Management-only bulk lookup used to avoid one HTTP/SQL round trip per review target. */
|
|
875
|
+
entriesByIds(ids) {
|
|
876
|
+
this.assertOpen();
|
|
877
|
+
const uniqueIds = [...new Set(ids.map(id => id.trim()).filter(Boolean))].slice(0, 100);
|
|
878
|
+
if (uniqueIds.length === 0)
|
|
879
|
+
return [];
|
|
880
|
+
const placeholders = uniqueIds.map(() => '?').join(',');
|
|
881
|
+
const rows = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id IN (${placeholders})`)
|
|
882
|
+
.all(...uniqueIds);
|
|
883
|
+
const entries = new Map(rows.map(row => {
|
|
884
|
+
const entry = rowToEntry(row);
|
|
885
|
+
return [entry.id, entry];
|
|
886
|
+
}));
|
|
887
|
+
return uniqueIds.flatMap(id => {
|
|
888
|
+
const entry = entries.get(id);
|
|
889
|
+
return entry === undefined ? [] : [entry];
|
|
890
|
+
});
|
|
891
|
+
}
|
|
826
892
|
async versions(id) {
|
|
827
893
|
this.assertOpen();
|
|
828
894
|
const rows = this.db.prepare('SELECT * FROM knowledge_versions WHERE knowledge_id = ? ORDER BY version DESC').all(id);
|
|
@@ -832,7 +898,7 @@ export class LocalKnowledgeProvider {
|
|
|
832
898
|
this.assertOpen();
|
|
833
899
|
await this.documentsReady;
|
|
834
900
|
const entry = this.transaction(() => this.insertEntry(draft));
|
|
835
|
-
await this.
|
|
901
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
836
902
|
return entry;
|
|
837
903
|
}
|
|
838
904
|
insertEntry(input) {
|
|
@@ -859,11 +925,8 @@ export class LocalKnowledgeProvider {
|
|
|
859
925
|
async update(id, draft) {
|
|
860
926
|
this.assertOpen();
|
|
861
927
|
await this.documentsReady;
|
|
862
|
-
const current = await this.get(id);
|
|
863
928
|
const entry = this.transaction(() => this.updateEntry(id, draft, 'update'));
|
|
864
|
-
|
|
865
|
-
await this.syncKnowledgeDocumentsQueued(current.knowledgeBaseId);
|
|
866
|
-
await this.syncKnowledgeDocumentsQueued(entry.knowledgeBaseId);
|
|
929
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
867
930
|
return entry;
|
|
868
931
|
}
|
|
869
932
|
async finalize(id, state, note) {
|
|
@@ -899,7 +962,7 @@ export class LocalKnowledgeProvider {
|
|
|
899
962
|
this.upsertFts(updated);
|
|
900
963
|
return updated;
|
|
901
964
|
});
|
|
902
|
-
await this.
|
|
965
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
903
966
|
return entry;
|
|
904
967
|
}
|
|
905
968
|
async reopen(id) {
|
|
@@ -931,7 +994,42 @@ export class LocalKnowledgeProvider {
|
|
|
931
994
|
this.upsertFts(updated);
|
|
932
995
|
return updated;
|
|
933
996
|
});
|
|
934
|
-
await this.
|
|
997
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
998
|
+
return entry;
|
|
999
|
+
}
|
|
1000
|
+
async moveDocument(id, knowledgeBaseId) {
|
|
1001
|
+
this.assertOpen();
|
|
1002
|
+
await this.documentsReady;
|
|
1003
|
+
let changed = false;
|
|
1004
|
+
const entry = this.transaction(() => {
|
|
1005
|
+
const row = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id=?`).get(id);
|
|
1006
|
+
if (row === undefined)
|
|
1007
|
+
throw notFound('knowledge entry', id);
|
|
1008
|
+
const current = rowToEntry(row);
|
|
1009
|
+
if (current.status !== 'active')
|
|
1010
|
+
throw conflict('only active knowledge documents can be moved');
|
|
1011
|
+
if (current.knowledgeBaseId === knowledgeBaseId)
|
|
1012
|
+
return current;
|
|
1013
|
+
if (this.db.prepare("SELECT id FROM knowledge_bases WHERE id=? AND status='active'").get(knowledgeBaseId) === undefined) {
|
|
1014
|
+
throw notFound('active knowledge base', knowledgeBaseId);
|
|
1015
|
+
}
|
|
1016
|
+
const updated = {
|
|
1017
|
+
...current,
|
|
1018
|
+
knowledgeBaseId,
|
|
1019
|
+
version: current.version + 1,
|
|
1020
|
+
updatedAt: nowIso(),
|
|
1021
|
+
};
|
|
1022
|
+
this.db.prepare(`
|
|
1023
|
+
UPDATE knowledge_entries
|
|
1024
|
+
SET knowledge_base_id=?,version=?,updated_at=?
|
|
1025
|
+
WHERE id=?
|
|
1026
|
+
`).run(knowledgeBaseId, updated.version, updated.updatedAt, id);
|
|
1027
|
+
this.writeVersion(updated, 'update');
|
|
1028
|
+
changed = true;
|
|
1029
|
+
return updated;
|
|
1030
|
+
});
|
|
1031
|
+
if (changed)
|
|
1032
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
935
1033
|
return entry;
|
|
936
1034
|
}
|
|
937
1035
|
updateEntry(id, input, changeKind) {
|
|
@@ -984,7 +1082,7 @@ export class LocalKnowledgeProvider {
|
|
|
984
1082
|
this.db.prepare('DELETE FROM knowledge_fts WHERE knowledge_id = ?').run(id);
|
|
985
1083
|
return updated;
|
|
986
1084
|
});
|
|
987
|
-
await this.
|
|
1085
|
+
await this.syncKnowledgeEntryQueued(entry.id);
|
|
988
1086
|
return entry;
|
|
989
1087
|
}
|
|
990
1088
|
async delete(id) {
|
|
@@ -998,7 +1096,7 @@ export class LocalKnowledgeProvider {
|
|
|
998
1096
|
throw notFound('knowledge entry', id);
|
|
999
1097
|
});
|
|
1000
1098
|
if (current !== undefined)
|
|
1001
|
-
await this.
|
|
1099
|
+
await this.syncKnowledgeEntryQueued(current.id);
|
|
1002
1100
|
}
|
|
1003
1101
|
async listNotes(request = {}) {
|
|
1004
1102
|
this.assertOpen();
|
|
@@ -1138,6 +1236,30 @@ export class LocalKnowledgeProvider {
|
|
|
1138
1236
|
documentTitle: String(row.document_title),
|
|
1139
1237
|
}));
|
|
1140
1238
|
}
|
|
1239
|
+
/** Compatibility path for manually embedded legacy note:// markers. */
|
|
1240
|
+
legacyNoteReferencesForNotes(noteIds) {
|
|
1241
|
+
this.assertOpen();
|
|
1242
|
+
const requested = new Set(noteIds.map(id => id.toLocaleLowerCase()));
|
|
1243
|
+
if (requested.size === 0)
|
|
1244
|
+
return [];
|
|
1245
|
+
const rows = this.db.prepare(`
|
|
1246
|
+
SELECT knowledge_base_id,id,title,content
|
|
1247
|
+
FROM knowledge_documents
|
|
1248
|
+
WHERE content LIKE '%note://note_%'
|
|
1249
|
+
ORDER BY updated_at DESC,id
|
|
1250
|
+
`).all();
|
|
1251
|
+
return rows.flatMap(row => {
|
|
1252
|
+
const references = String(row.content).match(/note:\/\/(note_[a-f0-9]{32})/giu) ?? [];
|
|
1253
|
+
return [...new Set(references.map(value => value.slice('note://'.length).toLocaleLowerCase()))]
|
|
1254
|
+
.filter(noteId => requested.has(noteId))
|
|
1255
|
+
.map(noteId => ({
|
|
1256
|
+
noteId,
|
|
1257
|
+
knowledgeBaseId: String(row.knowledge_base_id),
|
|
1258
|
+
documentId: String(row.id),
|
|
1259
|
+
documentTitle: String(row.title),
|
|
1260
|
+
}));
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1141
1263
|
deleteNoteReferences(noteIds) {
|
|
1142
1264
|
this.assertOpen();
|
|
1143
1265
|
const ids = [...new Set(noteIds)];
|
|
@@ -1157,7 +1279,7 @@ export class LocalKnowledgeProvider {
|
|
|
1157
1279
|
async writeDirect(input, sourceKey) {
|
|
1158
1280
|
this.assertOpen();
|
|
1159
1281
|
await this.documentsReady;
|
|
1160
|
-
let
|
|
1282
|
+
let touchedEntryId;
|
|
1161
1283
|
const result = this.transaction(() => {
|
|
1162
1284
|
const resolution = this.resolveDirectProposal(normalizeProposal(input));
|
|
1163
1285
|
if (resolution.outcome === 'duplicate')
|
|
@@ -1177,7 +1299,7 @@ export class LocalKnowledgeProvider {
|
|
|
1177
1299
|
.run('approved', reviewedAt, resolution.outcome === 'merged'
|
|
1178
1300
|
? 'Automatically merged by direct-write reconciliation.'
|
|
1179
1301
|
: 'Automatically approved by direct-write policy.', candidate.id);
|
|
1180
|
-
|
|
1302
|
+
touchedEntryId = entry.id;
|
|
1181
1303
|
return {
|
|
1182
1304
|
outcome: resolution.outcome,
|
|
1183
1305
|
candidate: {
|
|
@@ -1191,8 +1313,8 @@ export class LocalKnowledgeProvider {
|
|
|
1191
1313
|
entry,
|
|
1192
1314
|
};
|
|
1193
1315
|
});
|
|
1194
|
-
if (
|
|
1195
|
-
await this.
|
|
1316
|
+
if (touchedEntryId !== undefined)
|
|
1317
|
+
await this.syncKnowledgeEntryQueued(touchedEntryId);
|
|
1196
1318
|
return result;
|
|
1197
1319
|
}
|
|
1198
1320
|
insertCandidate(proposal, sourceKey) {
|
|
@@ -1303,6 +1425,7 @@ export class LocalKnowledgeProvider {
|
|
|
1303
1425
|
async review(id, decision) {
|
|
1304
1426
|
this.assertOpen();
|
|
1305
1427
|
await this.documentsReady;
|
|
1428
|
+
let touchedEntryId;
|
|
1306
1429
|
const reviewed = this.transaction(() => {
|
|
1307
1430
|
const row = this.db.prepare('SELECT * FROM knowledge_candidates WHERE id = ?').get(id);
|
|
1308
1431
|
if (row === undefined)
|
|
@@ -1323,7 +1446,7 @@ export class LocalKnowledgeProvider {
|
|
|
1323
1446
|
throw conflict('candidate approval cannot move a document between knowledge bases');
|
|
1324
1447
|
}
|
|
1325
1448
|
assertExpectedReviewVersion(target, decision.expectedVersion);
|
|
1326
|
-
this.updateEntry(candidate.targetId, editedKnowledgeDraft(target, draft), 'update');
|
|
1449
|
+
touchedEntryId = this.updateEntry(candidate.targetId, editedKnowledgeDraft(target, draft), 'update').id;
|
|
1327
1450
|
}
|
|
1328
1451
|
else if (candidate.action === 'conflict') {
|
|
1329
1452
|
if (decision.resolution !== 'merge') {
|
|
@@ -1338,7 +1461,7 @@ export class LocalKnowledgeProvider {
|
|
|
1338
1461
|
const applied = applyCandidateToTarget(target, candidate, true);
|
|
1339
1462
|
if (!applied.ok)
|
|
1340
1463
|
throw conflict(`${applied.reason}; edit the current document to resolve this conflict`);
|
|
1341
|
-
this.updateEntry(candidate.targetId, applied.draft, 'update');
|
|
1464
|
+
touchedEntryId = this.updateEntry(candidate.targetId, applied.draft, 'update').id;
|
|
1342
1465
|
}
|
|
1343
1466
|
else {
|
|
1344
1467
|
const resolution = this.resolveDirectProposal({
|
|
@@ -1366,10 +1489,10 @@ export class LocalKnowledgeProvider {
|
|
|
1366
1489
|
if (resolution.outcome === 'finalized')
|
|
1367
1490
|
throw finalizedConflict(resolution.entry);
|
|
1368
1491
|
if (resolution.outcome !== 'duplicate') {
|
|
1369
|
-
|
|
1370
|
-
this.insertEntry(resolution.proposal.draft)
|
|
1371
|
-
|
|
1372
|
-
|
|
1492
|
+
const entry = resolution.proposal.action === 'create'
|
|
1493
|
+
? this.insertEntry(resolution.proposal.draft)
|
|
1494
|
+
: this.updateEntry(resolution.proposal.targetId, resolution.proposal.draft, 'update');
|
|
1495
|
+
touchedEntryId = entry.id;
|
|
1373
1496
|
}
|
|
1374
1497
|
}
|
|
1375
1498
|
}
|
|
@@ -1380,8 +1503,8 @@ export class LocalKnowledgeProvider {
|
|
|
1380
1503
|
.run(status, reviewedAt, note ?? null, id);
|
|
1381
1504
|
return { ...candidate, status, reviewedAt, ...note === undefined ? {} : { reviewNote: note } };
|
|
1382
1505
|
});
|
|
1383
|
-
if (
|
|
1384
|
-
await this.
|
|
1506
|
+
if (touchedEntryId !== undefined)
|
|
1507
|
+
await this.syncKnowledgeEntryQueued(touchedEntryId);
|
|
1385
1508
|
return reviewed;
|
|
1386
1509
|
}
|
|
1387
1510
|
async claimExtraction(sourceKey) {
|
|
@@ -1393,7 +1516,7 @@ export class LocalKnowledgeProvider {
|
|
|
1393
1516
|
VALUES(?,'running',1,0,?)
|
|
1394
1517
|
ON CONFLICT(source_key) DO UPDATE SET
|
|
1395
1518
|
status='running', attempts=extraction_jobs.attempts+1, candidate_count=0,
|
|
1396
|
-
last_error=NULL, updated_at=excluded.updated_at
|
|
1519
|
+
last_error=NULL, completion_json=NULL, updated_at=excluded.updated_at
|
|
1397
1520
|
WHERE extraction_jobs.attempts < 3 AND (
|
|
1398
1521
|
extraction_jobs.status='failed'
|
|
1399
1522
|
OR (extraction_jobs.status='running' AND extraction_jobs.updated_at < ?)
|
|
@@ -1401,12 +1524,13 @@ export class LocalKnowledgeProvider {
|
|
|
1401
1524
|
`).run(sourceKey, claimedAt, staleBefore);
|
|
1402
1525
|
return result.changes === 1;
|
|
1403
1526
|
}
|
|
1404
|
-
async completeExtraction(sourceKey,
|
|
1527
|
+
async completeExtraction(sourceKey, value) {
|
|
1405
1528
|
this.assertOpen();
|
|
1529
|
+
const completion = normalizeExtractionCompletion(value);
|
|
1406
1530
|
const result = this.db.prepare(`
|
|
1407
|
-
UPDATE extraction_jobs SET status='completed', candidate_count=?, last_error=NULL, updated_at=?
|
|
1531
|
+
UPDATE extraction_jobs SET status='completed', candidate_count=?, last_error=NULL, completion_json=?, updated_at=?
|
|
1408
1532
|
WHERE source_key=? AND status='running'
|
|
1409
|
-
`).run(candidateCount, nowIso(), sourceKey);
|
|
1533
|
+
`).run(completion.candidateCount, JSON.stringify(completion), nowIso(), sourceKey);
|
|
1410
1534
|
if (result.changes === 0) {
|
|
1411
1535
|
const current = await this.extractionJob(sourceKey);
|
|
1412
1536
|
if (current?.status !== 'completed')
|
|
@@ -1427,7 +1551,7 @@ export class LocalKnowledgeProvider {
|
|
|
1427
1551
|
}
|
|
1428
1552
|
async resetExtraction(sourceKey) {
|
|
1429
1553
|
this.assertOpen();
|
|
1430
|
-
this.db.prepare(`UPDATE extraction_jobs SET status='failed', attempts=0, candidate_count=0, last_error=NULL, updated_at=? WHERE source_key=?`)
|
|
1554
|
+
this.db.prepare(`UPDATE extraction_jobs SET status='failed', attempts=0, candidate_count=0, last_error=NULL, completion_json=NULL, updated_at=? WHERE source_key=?`)
|
|
1431
1555
|
.run(nowIso(), sourceKey);
|
|
1432
1556
|
}
|
|
1433
1557
|
async extractionJob(sourceKey) {
|
|
@@ -1542,12 +1666,86 @@ export class LocalKnowledgeProvider {
|
|
|
1542
1666
|
syncKnowledgeDocumentsQueued(knowledgeBaseId) {
|
|
1543
1667
|
return this.enqueueDocumentSync(() => this.syncKnowledgeDocuments(knowledgeBaseId));
|
|
1544
1668
|
}
|
|
1669
|
+
syncKnowledgeBaseManifestQueued(knowledgeBaseId) {
|
|
1670
|
+
return this.enqueueDocumentSync(async () => {
|
|
1671
|
+
const row = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?').get(knowledgeBaseId);
|
|
1672
|
+
if (row !== undefined)
|
|
1673
|
+
await this.documentStore.ensureBase(rowToKnowledgeBase(row));
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
syncKnowledgeEntryQueued(entryId) {
|
|
1677
|
+
return this.enqueueDocumentSync(() => this.syncKnowledgeEntry(entryId));
|
|
1678
|
+
}
|
|
1679
|
+
/** Keep the derived Markdown projection proportional to one changed entry. */
|
|
1680
|
+
async syncKnowledgeEntry(entryId) {
|
|
1681
|
+
const projected = this.db.prepare(`
|
|
1682
|
+
SELECT id,knowledge_base_id,rel_path FROM knowledge_documents WHERE id=?
|
|
1683
|
+
`).get(entryId);
|
|
1684
|
+
const entryRow = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id=?`).get(entryId);
|
|
1685
|
+
if (entryRow === undefined || entryRow.status !== 'active') {
|
|
1686
|
+
if (projected !== undefined)
|
|
1687
|
+
await this.removeProjectedDocument(projected);
|
|
1688
|
+
this.db.prepare('DELETE FROM knowledge_documents WHERE id=?').run(entryId);
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
const entry = rowToEntry(entryRow);
|
|
1692
|
+
const baseRow = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?').get(entry.knowledgeBaseId);
|
|
1693
|
+
if (baseRow === undefined)
|
|
1694
|
+
return;
|
|
1695
|
+
const base = rowToKnowledgeBase(baseRow);
|
|
1696
|
+
const directory = await this.documentStore.ensureBase(base);
|
|
1697
|
+
const relPath = knowledgeDocumentPath(entry);
|
|
1698
|
+
const markdown = renderEntryMarkdown(entry);
|
|
1699
|
+
const stored = await this.documentStore.writeDocument(directory, relPath, markdown);
|
|
1700
|
+
this.upsertProjectedDocument(entry, relPath, stored.contentHash);
|
|
1701
|
+
if (projected !== undefined && (String(projected.knowledge_base_id) !== entry.knowledgeBaseId
|
|
1702
|
+
|| String(projected.rel_path) !== relPath))
|
|
1703
|
+
await this.removeProjectedDocument(projected);
|
|
1704
|
+
}
|
|
1705
|
+
async removeProjectedDocument(projected) {
|
|
1706
|
+
const baseRow = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?')
|
|
1707
|
+
.get(String(projected.knowledge_base_id));
|
|
1708
|
+
if (baseRow === undefined)
|
|
1709
|
+
return;
|
|
1710
|
+
const directory = this.documentStore.baseDirectory(rowToKnowledgeBase(baseRow));
|
|
1711
|
+
try {
|
|
1712
|
+
await this.documentStore.deleteDocument(directory, String(projected.rel_path));
|
|
1713
|
+
}
|
|
1714
|
+
catch (error) {
|
|
1715
|
+
if (!(error instanceof Error && error.code === 'ENOENT'))
|
|
1716
|
+
throw error;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
upsertProjectedDocument(entry, relPath, projectedHash) {
|
|
1720
|
+
this.db.prepare(`
|
|
1721
|
+
INSERT INTO knowledge_documents(
|
|
1722
|
+
id,knowledge_base_id,rel_path,title,content,entry_count,content_hash,
|
|
1723
|
+
document_state,finalized_at,finalization_note,created_at,updated_at
|
|
1724
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
|
|
1725
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1726
|
+
knowledge_base_id=excluded.knowledge_base_id,rel_path=excluded.rel_path,
|
|
1727
|
+
title=excluded.title,content=excluded.content,entry_count=excluded.entry_count,
|
|
1728
|
+
content_hash=excluded.content_hash,document_state=excluded.document_state,
|
|
1729
|
+
finalized_at=excluded.finalized_at,finalization_note=excluded.finalization_note,
|
|
1730
|
+
updated_at=excluded.updated_at
|
|
1731
|
+
WHERE knowledge_documents.knowledge_base_id<>excluded.knowledge_base_id
|
|
1732
|
+
OR knowledge_documents.content_hash<>excluded.content_hash
|
|
1733
|
+
OR knowledge_documents.rel_path<>excluded.rel_path
|
|
1734
|
+
OR knowledge_documents.title<>excluded.title
|
|
1735
|
+
OR knowledge_documents.entry_count<>excluded.entry_count
|
|
1736
|
+
OR knowledge_documents.document_state<>excluded.document_state
|
|
1737
|
+
OR knowledge_documents.finalized_at IS NOT excluded.finalized_at
|
|
1738
|
+
OR knowledge_documents.finalization_note IS NOT excluded.finalization_note
|
|
1739
|
+
`).run(entry.id, entry.knowledgeBaseId, relPath, entry.title, renderEntryContent(entry), 1, projectedHash, entry.documentState, entry.finalizedAt ?? null, entry.finalizationNote ?? null, entry.createdAt, entry.updatedAt);
|
|
1740
|
+
}
|
|
1545
1741
|
async syncKnowledgeDocuments(knowledgeBaseId) {
|
|
1546
1742
|
const baseRow = this.db.prepare('SELECT * FROM knowledge_bases WHERE id=?').get(knowledgeBaseId);
|
|
1547
1743
|
if (baseRow === undefined)
|
|
1548
1744
|
return;
|
|
1549
1745
|
const base = rowToKnowledgeBase(baseRow);
|
|
1550
1746
|
const directory = await this.documentStore.ensureBase(base);
|
|
1747
|
+
const storedDocuments = await this.documentStore.listDocuments(directory);
|
|
1748
|
+
const storedById = new Map(storedDocuments.map(document => [document.metadata.id, document]));
|
|
1551
1749
|
const entries = this.db.prepare(`
|
|
1552
1750
|
SELECT ${ENTRY_COLUMNS} FROM knowledge_entries
|
|
1553
1751
|
WHERE knowledge_base_id=? AND status='active'
|
|
@@ -1556,58 +1754,28 @@ export class LocalKnowledgeProvider {
|
|
|
1556
1754
|
const desired = new Map();
|
|
1557
1755
|
for (const entry of entries) {
|
|
1558
1756
|
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);
|
|
1757
|
+
const markdown = renderEntryMarkdown(entry);
|
|
1758
|
+
const current = storedById.get(entry.id);
|
|
1759
|
+
const contentHash = markdownHash(markdown);
|
|
1760
|
+
const stored = current?.relPath === relPath && current.contentHash === contentHash
|
|
1761
|
+
? current
|
|
1762
|
+
: await this.documentStore.writeDocument(directory, relPath, markdown);
|
|
1575
1763
|
desired.set(entry.id, {
|
|
1576
1764
|
entry,
|
|
1577
1765
|
relPath,
|
|
1578
|
-
content: `# ${markdownHeading(entry.title)}\n\n${entry.body.trim()}\n`,
|
|
1579
1766
|
contentHash: stored.contentHash,
|
|
1580
1767
|
});
|
|
1581
1768
|
}
|
|
1582
|
-
const storedDocuments = await this.documentStore.listDocuments(directory);
|
|
1583
1769
|
for (const document of storedDocuments) {
|
|
1584
1770
|
const expected = desired.get(document.metadata.id);
|
|
1585
1771
|
if (expected === undefined || expected.relPath !== document.relPath) {
|
|
1586
1772
|
await this.documentStore.deleteDocument(directory, document.relPath, document.contentHash);
|
|
1587
1773
|
}
|
|
1588
1774
|
}
|
|
1589
|
-
const existing = this.db.prepare('SELECT id
|
|
1775
|
+
const existing = this.db.prepare('SELECT id FROM knowledge_documents WHERE knowledge_base_id=?')
|
|
1590
1776
|
.all(knowledgeBaseId);
|
|
1591
1777
|
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);
|
|
1778
|
+
this.upsertProjectedDocument(document.entry, document.relPath, document.contentHash);
|
|
1611
1779
|
}
|
|
1612
1780
|
for (const row of existing) {
|
|
1613
1781
|
if (!desired.has(String(row.id)))
|
|
@@ -1615,6 +1783,26 @@ export class LocalKnowledgeProvider {
|
|
|
1615
1783
|
}
|
|
1616
1784
|
}
|
|
1617
1785
|
}
|
|
1786
|
+
function renderEntryMarkdown(entry) {
|
|
1787
|
+
return renderKnowledgeMarkdown({
|
|
1788
|
+
metadata: {
|
|
1789
|
+
id: entry.id,
|
|
1790
|
+
type: entry.type,
|
|
1791
|
+
tags: entry.tags,
|
|
1792
|
+
scope: entry.scope,
|
|
1793
|
+
confidence: entry.confidence,
|
|
1794
|
+
status: entry.status,
|
|
1795
|
+
documentState: entry.documentState,
|
|
1796
|
+
...entry.finalizedAt === undefined ? {} : { finalizedAt: entry.finalizedAt },
|
|
1797
|
+
...entry.finalizationNote === undefined ? {} : { finalizationNote: entry.finalizationNote },
|
|
1798
|
+
},
|
|
1799
|
+
title: entry.title,
|
|
1800
|
+
body: entry.body,
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
function renderEntryContent(entry) {
|
|
1804
|
+
return `# ${markdownHeading(entry.title)}\n\n${entry.body.trim()}\n`;
|
|
1805
|
+
}
|
|
1618
1806
|
function markdownHeading(value) {
|
|
1619
1807
|
return value.replace(/[\r\n]+/g, ' ').replace(/^#+\s*/, '').trim();
|
|
1620
1808
|
}
|
|
@@ -1929,15 +2117,61 @@ function rowToMount(row) {
|
|
|
1929
2117
|
}
|
|
1930
2118
|
function rowToExtractionJob(row) {
|
|
1931
2119
|
const lastError = row.last_error == null ? undefined : String(row.last_error);
|
|
2120
|
+
const completion = row.completion_json == null
|
|
2121
|
+
? undefined
|
|
2122
|
+
: normalizeExtractionCompletion(JSON.parse(String(row.completion_json)));
|
|
1932
2123
|
return {
|
|
1933
2124
|
sourceKey: String(row.source_key),
|
|
1934
2125
|
status: String(row.status),
|
|
1935
2126
|
attempts: Number(row.attempts),
|
|
1936
2127
|
candidateCount: Number(row.candidate_count),
|
|
1937
2128
|
...lastError === undefined ? {} : { lastError },
|
|
2129
|
+
...completion === undefined ? {} : { completion },
|
|
1938
2130
|
updatedAt: String(row.updated_at),
|
|
1939
2131
|
};
|
|
1940
2132
|
}
|
|
2133
|
+
function normalizeExtractionCompletion(value) {
|
|
2134
|
+
if (typeof value === 'number') {
|
|
2135
|
+
const candidateCount = Math.max(0, Math.floor(value));
|
|
2136
|
+
return { outcome: 'completed', candidateCount, directCount: 0, auditCount: candidateCount, destinations: [] };
|
|
2137
|
+
}
|
|
2138
|
+
if (value.outcome !== 'completed' && value.outcome !== 'skipped' && value.outcome !== 'unmounted') {
|
|
2139
|
+
throw new Error('extraction completion outcome is invalid');
|
|
2140
|
+
}
|
|
2141
|
+
const count = (input, field) => {
|
|
2142
|
+
if (!Number.isInteger(input) || input < 0 || input > 10_000)
|
|
2143
|
+
throw new Error(`extraction completion ${field} is invalid`);
|
|
2144
|
+
return input;
|
|
2145
|
+
};
|
|
2146
|
+
if (!Array.isArray(value.destinations) || value.destinations.length > 100) {
|
|
2147
|
+
throw new Error('extraction completion destinations are invalid');
|
|
2148
|
+
}
|
|
2149
|
+
const text = (input, field, limit) => {
|
|
2150
|
+
const normalized = typeof input === 'string' ? input.trim() : '';
|
|
2151
|
+
if (normalized.length === 0 || normalized.length > limit)
|
|
2152
|
+
throw new Error(`extraction completion ${field} is invalid`);
|
|
2153
|
+
return normalized;
|
|
2154
|
+
};
|
|
2155
|
+
return {
|
|
2156
|
+
outcome: value.outcome,
|
|
2157
|
+
candidateCount: count(value.candidateCount, 'candidateCount'),
|
|
2158
|
+
directCount: count(value.directCount, 'directCount'),
|
|
2159
|
+
auditCount: count(value.auditCount, 'auditCount'),
|
|
2160
|
+
destinations: value.destinations.map(destination => {
|
|
2161
|
+
if (destination.disposition !== 'written' && destination.disposition !== 'pending-review') {
|
|
2162
|
+
throw new Error('extraction completion disposition is invalid');
|
|
2163
|
+
}
|
|
2164
|
+
return {
|
|
2165
|
+
knowledgeBaseId: text(destination.knowledgeBaseId, 'knowledgeBaseId', 200),
|
|
2166
|
+
knowledgeBaseName: text(destination.knowledgeBaseName, 'knowledgeBaseName', 200),
|
|
2167
|
+
...destination.documentId === undefined ? {} : { documentId: text(destination.documentId, 'documentId', 200) },
|
|
2168
|
+
documentTitle: text(destination.documentTitle, 'documentTitle', 500),
|
|
2169
|
+
...destination.documentPath === undefined ? {} : { documentPath: text(destination.documentPath, 'documentPath', 1000) },
|
|
2170
|
+
disposition: destination.disposition,
|
|
2171
|
+
};
|
|
2172
|
+
}),
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
1941
2175
|
function rowToToken(row) {
|
|
1942
2176
|
const lastUsedAt = row.last_used_at == null ? undefined : String(row.last_used_at);
|
|
1943
2177
|
const revokedAt = row.revoked_at == null ? undefined : String(row.revoked_at);
|