@lemoncat7/dsh-knowledge 1.0.2 → 1.0.11
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 +5 -2
- package/docs/architecture.md +3 -3
- package/docs/requirements.md +3 -1
- package/lib/api.js +35 -4
- package/lib/api.js.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +123 -94
- package/lib/client.js.map +4 -4
- package/lib/documents/markdown.d.ts +4 -1
- package/lib/documents/markdown.d.ts.map +1 -1
- package/lib/documents/markdown.js +11 -1
- package/lib/documents/markdown.js.map +1 -1
- package/lib/domain.d.ts +20 -2
- package/lib/domain.d.ts.map +1 -1
- package/lib/domain.js +16 -2
- package/lib/domain.js.map +1 -1
- package/lib/extraction.d.ts.map +1 -1
- package/lib/extraction.js +36 -12
- package/lib/extraction.js.map +1 -1
- package/lib/local-provider.d.ts +3 -0
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +231 -21
- package/lib/local-provider.js.map +1 -1
- package/lib/provider.d.ts +2 -0
- package/lib/provider.d.ts.map +1 -1
- package/lib/remote-provider.d.ts +2 -0
- package/lib/remote-provider.d.ts.map +1 -1
- package/lib/remote-provider.js +8 -0
- package/lib/remote-provider.js.map +1 -1
- package/lib/retrieval.d.ts +1 -1
- package/lib/retrieval.d.ts.map +1 -1
- package/lib/retrieval.js +8 -22
- package/lib/retrieval.js.map +1 -1
- package/lib/theme-bridge.js +4 -4
- package/lib/theme-bridge.js.map +1 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +6 -3
- package/lib/tools.js.map +1 -1
- package/package.json +2 -1
- package/web/app.js +159 -63
- package/web/styles.css +34 -42
package/lib/local-provider.js
CHANGED
|
@@ -8,12 +8,13 @@ import { knowledgeDocumentPath } from './documents/path.js';
|
|
|
8
8
|
import { KnowledgeDocumentStore } from './documents/store.js';
|
|
9
9
|
const ENTRY_COLUMNS = `
|
|
10
10
|
id, knowledge_base_id, title, body, type, tags_json, scope_kind, scope_id, confidence,
|
|
11
|
-
status, version, source_json, created_at, updated_at
|
|
11
|
+
status, document_state, finalized_at, finalization_note, version, source_json, created_at, updated_at
|
|
12
12
|
`;
|
|
13
13
|
const JOINED_ENTRY_COLUMNS = `
|
|
14
14
|
e.id AS id, e.knowledge_base_id AS knowledge_base_id, e.title AS title, e.body AS body, e.type AS type,
|
|
15
15
|
e.tags_json AS tags_json, e.scope_kind AS scope_kind, e.scope_id AS scope_id,
|
|
16
|
-
e.confidence AS confidence, e.status AS status, e.
|
|
16
|
+
e.confidence AS confidence, e.status AS status, e.document_state AS document_state,
|
|
17
|
+
e.finalized_at AS finalized_at, e.finalization_note AS finalization_note, e.version AS version,
|
|
17
18
|
e.source_json AS source_json, e.created_at AS created_at, e.updated_at AS updated_at
|
|
18
19
|
`;
|
|
19
20
|
export class LocalKnowledgeProvider {
|
|
@@ -33,7 +34,7 @@ export class LocalKnowledgeProvider {
|
|
|
33
34
|
}
|
|
34
35
|
migrate() {
|
|
35
36
|
let version = Number(this.db.prepare('PRAGMA user_version').get().user_version ?? 0);
|
|
36
|
-
if (version >
|
|
37
|
+
if (version > 9)
|
|
37
38
|
throw new Error(`knowledge database schema ${version} is newer than this plugin supports`);
|
|
38
39
|
if (version === 0)
|
|
39
40
|
this.db.exec(`
|
|
@@ -205,6 +206,43 @@ export class LocalKnowledgeProvider {
|
|
|
205
206
|
version = 5;
|
|
206
207
|
if (version === 5)
|
|
207
208
|
this.db.exec('PRAGMA user_version = 6;');
|
|
209
|
+
if (version <= 5)
|
|
210
|
+
version = 6;
|
|
211
|
+
if (version === 6)
|
|
212
|
+
this.db.exec(`
|
|
213
|
+
BEGIN IMMEDIATE;
|
|
214
|
+
ALTER TABLE knowledge_entries ADD COLUMN document_state TEXT NOT NULL DEFAULT 'open'
|
|
215
|
+
CHECK(document_state IN ('open','resolved','complete'));
|
|
216
|
+
ALTER TABLE knowledge_entries ADD COLUMN finalized_at TEXT;
|
|
217
|
+
ALTER TABLE knowledge_entries ADD COLUMN finalization_note TEXT;
|
|
218
|
+
ALTER TABLE knowledge_documents ADD COLUMN document_state TEXT NOT NULL DEFAULT 'open'
|
|
219
|
+
CHECK(document_state IN ('open','resolved','complete'));
|
|
220
|
+
ALTER TABLE knowledge_documents ADD COLUMN finalized_at TEXT;
|
|
221
|
+
ALTER TABLE knowledge_documents ADD COLUMN finalization_note TEXT;
|
|
222
|
+
PRAGMA user_version = 7;
|
|
223
|
+
COMMIT;
|
|
224
|
+
`);
|
|
225
|
+
if (version <= 6)
|
|
226
|
+
version = 7;
|
|
227
|
+
if (version === 7)
|
|
228
|
+
this.db.exec(`
|
|
229
|
+
BEGIN IMMEDIATE;
|
|
230
|
+
ALTER TABLE knowledge_bases ADD COLUMN writeback_policy TEXT NOT NULL DEFAULT 'conservative'
|
|
231
|
+
CHECK(writeback_policy IN ('conservative','proactive'));
|
|
232
|
+
UPDATE knowledge_bases SET writeback_policy=(SELECT writeback_policy FROM knowledge_settings WHERE id=1);
|
|
233
|
+
PRAGMA user_version = 8;
|
|
234
|
+
COMMIT;
|
|
235
|
+
`);
|
|
236
|
+
if (version <= 7)
|
|
237
|
+
version = 8;
|
|
238
|
+
if (version === 8)
|
|
239
|
+
this.db.exec(`
|
|
240
|
+
BEGIN IMMEDIATE;
|
|
241
|
+
ALTER TABLE knowledge_settings ADD COLUMN writeback_provider TEXT;
|
|
242
|
+
ALTER TABLE knowledge_settings ADD COLUMN writeback_model TEXT;
|
|
243
|
+
PRAGMA user_version = 9;
|
|
244
|
+
COMMIT;
|
|
245
|
+
`);
|
|
208
246
|
// Alpha v2 used a migration note as the default base's routing description.
|
|
209
247
|
// Clear only that exact placeholder so existing user-authored descriptions stay untouched.
|
|
210
248
|
this.db.prepare("UPDATE knowledge_bases SET description='' WHERE id=? AND description=?")
|
|
@@ -216,9 +254,12 @@ export class LocalKnowledgeProvider {
|
|
|
216
254
|
}
|
|
217
255
|
async getSettings() {
|
|
218
256
|
this.assertOpen();
|
|
219
|
-
const row = this.db.prepare('SELECT writeback_policy,updated_at FROM knowledge_settings WHERE id=1').get();
|
|
257
|
+
const row = this.db.prepare('SELECT writeback_policy,writeback_provider,writeback_model,updated_at FROM knowledge_settings WHERE id=1').get();
|
|
258
|
+
const provider = row.writeback_provider == null ? undefined : String(row.writeback_provider);
|
|
259
|
+
const model = row.writeback_model == null ? undefined : String(row.writeback_model);
|
|
220
260
|
return {
|
|
221
261
|
writebackPolicy: String(row.writeback_policy),
|
|
262
|
+
...provider === undefined || model === undefined ? {} : { writebackProvider: provider, writebackModel: model },
|
|
222
263
|
updatedAt: String(row.updated_at),
|
|
223
264
|
};
|
|
224
265
|
}
|
|
@@ -226,9 +267,18 @@ export class LocalKnowledgeProvider {
|
|
|
226
267
|
this.assertOpen();
|
|
227
268
|
const patch = normalizeKnowledgeSettings(input);
|
|
228
269
|
const updatedAt = nowIso();
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
270
|
+
const current = await this.getSettings();
|
|
271
|
+
const clearRoute = patch.writebackProvider === null || patch.writebackModel === null;
|
|
272
|
+
const next = {
|
|
273
|
+
writebackPolicy: patch.writebackPolicy ?? current.writebackPolicy,
|
|
274
|
+
...clearRoute ? {} : patch.writebackProvider && patch.writebackModel
|
|
275
|
+
? { writebackProvider: patch.writebackProvider, writebackModel: patch.writebackModel }
|
|
276
|
+
: current.writebackProvider && current.writebackModel ? { writebackProvider: current.writebackProvider, writebackModel: current.writebackModel } : {},
|
|
277
|
+
updatedAt,
|
|
278
|
+
};
|
|
279
|
+
this.db.prepare('UPDATE knowledge_settings SET writeback_policy=?,writeback_provider=?,writeback_model=?,updated_at=? WHERE id=1')
|
|
280
|
+
.run(next.writebackPolicy, next.writebackProvider ?? null, next.writebackModel ?? null, updatedAt);
|
|
281
|
+
return next;
|
|
232
282
|
}
|
|
233
283
|
async listKnowledgeBases() {
|
|
234
284
|
this.assertOpen();
|
|
@@ -248,9 +298,9 @@ export class LocalKnowledgeProvider {
|
|
|
248
298
|
const base = { ...draft, id: newId(), status: 'active', createdAt: timestamp, updatedAt: timestamp };
|
|
249
299
|
this.db.prepare(`
|
|
250
300
|
INSERT INTO knowledge_bases(
|
|
251
|
-
id,name,description,default_tags_json,extraction_instructions,writeback_provider,writeback_model,status,created_at,updated_at
|
|
252
|
-
) VALUES(
|
|
253
|
-
`).run(base.id, base.name, base.description, JSON.stringify(base.defaultTags), base.extractionInstructions, base.writebackProvider ?? null, base.writebackModel ?? null, timestamp, timestamp);
|
|
301
|
+
id,name,description,default_tags_json,extraction_instructions,writeback_policy,writeback_provider,writeback_model,status,created_at,updated_at
|
|
302
|
+
) VALUES(?,?,?,?,?,?,?,?,'active',?,?)
|
|
303
|
+
`).run(base.id, base.name, base.description, JSON.stringify(base.defaultTags), base.extractionInstructions, base.writebackPolicy, base.writebackProvider ?? null, base.writebackModel ?? null, timestamp, timestamp);
|
|
254
304
|
await this.syncKnowledgeDocuments(base.id);
|
|
255
305
|
return base;
|
|
256
306
|
}
|
|
@@ -264,9 +314,9 @@ export class LocalKnowledgeProvider {
|
|
|
264
314
|
const updated = { ...current, ...draft, updatedAt: nowIso() };
|
|
265
315
|
this.db.prepare(`
|
|
266
316
|
UPDATE knowledge_bases SET
|
|
267
|
-
name=?,description=?,default_tags_json=?,extraction_instructions=?,writeback_provider=?,writeback_model=?,updated_at=?
|
|
317
|
+
name=?,description=?,default_tags_json=?,extraction_instructions=?,writeback_policy=?,writeback_provider=?,writeback_model=?,updated_at=?
|
|
268
318
|
WHERE id=?
|
|
269
|
-
`).run(updated.name, updated.description, JSON.stringify(updated.defaultTags), updated.extractionInstructions, updated.writebackProvider ?? null, updated.writebackModel ?? null, updated.updatedAt, id);
|
|
319
|
+
`).run(updated.name, updated.description, JSON.stringify(updated.defaultTags), updated.extractionInstructions, updated.writebackPolicy, updated.writebackProvider ?? null, updated.writebackModel ?? null, updated.updatedAt, id);
|
|
270
320
|
await this.syncKnowledgeDocuments(id);
|
|
271
321
|
return updated;
|
|
272
322
|
}
|
|
@@ -283,6 +333,7 @@ export class LocalKnowledgeProvider {
|
|
|
283
333
|
description: patch.description ?? current.description,
|
|
284
334
|
defaultTags: patch.defaultTags ?? current.defaultTags,
|
|
285
335
|
extractionInstructions: patch.extractionInstructions ?? current.extractionInstructions,
|
|
336
|
+
writebackPolicy: patch.writebackPolicy ?? current.writebackPolicy,
|
|
286
337
|
...clearRoute || provider === undefined || model === undefined ? {} : { writebackProvider: provider, writebackModel: model },
|
|
287
338
|
});
|
|
288
339
|
}
|
|
@@ -642,11 +693,15 @@ export class LocalKnowledgeProvider {
|
|
|
642
693
|
}
|
|
643
694
|
const id = newId();
|
|
644
695
|
const timestamp = nowIso();
|
|
645
|
-
const entry = {
|
|
696
|
+
const entry = {
|
|
697
|
+
...draft, id, status: 'active', documentState: 'open', version: 1,
|
|
698
|
+
createdAt: timestamp, updatedAt: timestamp,
|
|
699
|
+
};
|
|
646
700
|
this.db.prepare(`
|
|
647
701
|
INSERT INTO knowledge_entries (
|
|
648
|
-
id,knowledge_base_id,title,body,type,tags_json,scope_kind,scope_id,confidence,status,
|
|
649
|
-
|
|
702
|
+
id,knowledge_base_id,title,body,type,tags_json,scope_kind,scope_id,confidence,status,
|
|
703
|
+
document_state,finalized_at,finalization_note,version,content_hash,source_json,created_at,updated_at
|
|
704
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,'open',NULL,NULL,?,?,?,?,?)
|
|
650
705
|
`).run(id, draft.knowledgeBaseId, draft.title, draft.body, draft.type, JSON.stringify(draft.tags), draft.scope.kind, draft.scope.kind === 'project' ? draft.scope.id : null, draft.confidence, 'active', 1, contentHash(draft), draft.source === undefined ? null : JSON.stringify(draft.source), timestamp, timestamp);
|
|
651
706
|
this.writeVersion(entry, 'create');
|
|
652
707
|
this.upsertFts(entry);
|
|
@@ -662,11 +717,81 @@ export class LocalKnowledgeProvider {
|
|
|
662
717
|
await this.syncKnowledgeDocuments(entry.knowledgeBaseId);
|
|
663
718
|
return entry;
|
|
664
719
|
}
|
|
720
|
+
async finalize(id, state, note) {
|
|
721
|
+
this.assertOpen();
|
|
722
|
+
await this.documentsReady;
|
|
723
|
+
const entry = this.transaction(() => {
|
|
724
|
+
const row = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id=?`).get(id);
|
|
725
|
+
if (row === undefined)
|
|
726
|
+
throw notFound('knowledge entry', id);
|
|
727
|
+
const current = rowToEntry(row);
|
|
728
|
+
if (current.status !== 'active')
|
|
729
|
+
throw conflict('only active knowledge documents can be finalized');
|
|
730
|
+
const finalizationNote = normalizeFinalizationNote(note);
|
|
731
|
+
if (current.documentState === state && current.finalizationNote === finalizationNote)
|
|
732
|
+
return current;
|
|
733
|
+
if (current.documentState !== 'open')
|
|
734
|
+
throw finalizedConflict(current);
|
|
735
|
+
const timestamp = nowIso();
|
|
736
|
+
const updated = {
|
|
737
|
+
...current,
|
|
738
|
+
documentState: state,
|
|
739
|
+
finalizedAt: timestamp,
|
|
740
|
+
...finalizationNote === undefined ? {} : { finalizationNote },
|
|
741
|
+
version: current.version + 1,
|
|
742
|
+
updatedAt: timestamp,
|
|
743
|
+
};
|
|
744
|
+
this.db.prepare(`
|
|
745
|
+
UPDATE knowledge_entries
|
|
746
|
+
SET document_state=?,finalized_at=?,finalization_note=?,version=?,updated_at=?
|
|
747
|
+
WHERE id=?
|
|
748
|
+
`).run(state, timestamp, finalizationNote ?? null, updated.version, timestamp, id);
|
|
749
|
+
this.writeVersion(updated, 'update');
|
|
750
|
+
this.upsertFts(updated);
|
|
751
|
+
return updated;
|
|
752
|
+
});
|
|
753
|
+
await this.syncKnowledgeDocuments(entry.knowledgeBaseId);
|
|
754
|
+
return entry;
|
|
755
|
+
}
|
|
756
|
+
async reopen(id) {
|
|
757
|
+
this.assertOpen();
|
|
758
|
+
await this.documentsReady;
|
|
759
|
+
const entry = this.transaction(() => {
|
|
760
|
+
const row = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id=?`).get(id);
|
|
761
|
+
if (row === undefined)
|
|
762
|
+
throw notFound('knowledge entry', id);
|
|
763
|
+
const current = rowToEntry(row);
|
|
764
|
+
if (current.status !== 'active')
|
|
765
|
+
throw conflict('only active knowledge documents can be reopened');
|
|
766
|
+
if (current.documentState === 'open')
|
|
767
|
+
return current;
|
|
768
|
+
const timestamp = nowIso();
|
|
769
|
+
const { finalizedAt: _finalizedAt, finalizationNote: _finalizationNote, ...reopened } = current;
|
|
770
|
+
const updated = {
|
|
771
|
+
...reopened,
|
|
772
|
+
documentState: 'open',
|
|
773
|
+
version: current.version + 1,
|
|
774
|
+
updatedAt: timestamp,
|
|
775
|
+
};
|
|
776
|
+
this.db.prepare(`
|
|
777
|
+
UPDATE knowledge_entries
|
|
778
|
+
SET document_state='open',finalized_at=NULL,finalization_note=NULL,version=?,updated_at=?
|
|
779
|
+
WHERE id=?
|
|
780
|
+
`).run(updated.version, timestamp, id);
|
|
781
|
+
this.writeVersion(updated, 'update');
|
|
782
|
+
this.upsertFts(updated);
|
|
783
|
+
return updated;
|
|
784
|
+
});
|
|
785
|
+
await this.syncKnowledgeDocuments(entry.knowledgeBaseId);
|
|
786
|
+
return entry;
|
|
787
|
+
}
|
|
665
788
|
updateEntry(id, input, changeKind) {
|
|
666
789
|
const currentRow = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id = ?`).get(id);
|
|
667
790
|
if (currentRow === undefined)
|
|
668
791
|
throw notFound('knowledge entry', id);
|
|
669
792
|
const current = rowToEntry(currentRow);
|
|
793
|
+
if (current.documentState !== 'open')
|
|
794
|
+
throw finalizedConflict(current);
|
|
670
795
|
const draft = normalizeDraft(input);
|
|
671
796
|
if (this.db.prepare("SELECT id FROM knowledge_bases WHERE id=? AND status='active'").get(draft.knowledgeBaseId) === undefined) {
|
|
672
797
|
throw notFound('active knowledge base', draft.knowledgeBaseId);
|
|
@@ -676,6 +801,7 @@ export class LocalKnowledgeProvider {
|
|
|
676
801
|
...draft,
|
|
677
802
|
id,
|
|
678
803
|
status: 'active',
|
|
804
|
+
documentState: current.documentState,
|
|
679
805
|
version: current.version + 1,
|
|
680
806
|
createdAt: current.createdAt,
|
|
681
807
|
updatedAt: timestamp,
|
|
@@ -724,7 +850,11 @@ export class LocalKnowledgeProvider {
|
|
|
724
850
|
}
|
|
725
851
|
async propose(input, sourceKey) {
|
|
726
852
|
this.assertOpen();
|
|
727
|
-
|
|
853
|
+
const proposal = normalizeProposal(input);
|
|
854
|
+
const finalized = this.finalizedMatch(proposal);
|
|
855
|
+
if (finalized !== undefined)
|
|
856
|
+
throw finalizedConflict(finalized);
|
|
857
|
+
return this.insertCandidate(proposal, sourceKey);
|
|
728
858
|
}
|
|
729
859
|
async writeDirect(input, sourceKey) {
|
|
730
860
|
this.assertOpen();
|
|
@@ -734,6 +864,8 @@ export class LocalKnowledgeProvider {
|
|
|
734
864
|
const resolution = this.resolveDirectProposal(normalizeProposal(input));
|
|
735
865
|
if (resolution.outcome === 'duplicate')
|
|
736
866
|
return { outcome: 'duplicate', ...resolution.entry === undefined ? {} : { entry: resolution.entry } };
|
|
867
|
+
if (resolution.outcome === 'finalized')
|
|
868
|
+
return { outcome: 'finalized', entry: resolution.entry };
|
|
737
869
|
const candidate = this.insertCandidate(resolution.proposal, sourceKey);
|
|
738
870
|
if (candidate.status !== 'pending')
|
|
739
871
|
return { outcome: 'duplicate', candidate };
|
|
@@ -790,6 +922,8 @@ export class LocalKnowledgeProvider {
|
|
|
790
922
|
return { outcome: 'conflict', proposal };
|
|
791
923
|
if (proposal.action === 'update') {
|
|
792
924
|
const target = this.activeEntry(proposal.targetId);
|
|
925
|
+
if (target.documentState !== 'open')
|
|
926
|
+
return { outcome: 'finalized', entry: target };
|
|
793
927
|
if (target.knowledgeBaseId !== proposal.draft.knowledgeBaseId) {
|
|
794
928
|
throw conflict('direct-write update cannot move knowledge between knowledge bases');
|
|
795
929
|
}
|
|
@@ -811,6 +945,8 @@ export class LocalKnowledgeProvider {
|
|
|
811
945
|
const target = bodyMatch ?? titleMatch ?? referenceMatch;
|
|
812
946
|
if (target === undefined)
|
|
813
947
|
return { outcome: 'created', proposal };
|
|
948
|
+
if (target.documentState !== 'open')
|
|
949
|
+
return { outcome: 'finalized', entry: target };
|
|
814
950
|
if (potentiallyConflicts(target, proposal.draft)) {
|
|
815
951
|
return {
|
|
816
952
|
outcome: 'conflict',
|
|
@@ -825,6 +961,16 @@ export class LocalKnowledgeProvider {
|
|
|
825
961
|
proposal: { ...proposal, action: 'update', targetId: target.id, draft },
|
|
826
962
|
};
|
|
827
963
|
}
|
|
964
|
+
finalizedMatch(proposal) {
|
|
965
|
+
if (proposal.action !== 'create') {
|
|
966
|
+
const target = this.activeEntry(proposal.targetId);
|
|
967
|
+
return target.documentState === 'open' ? undefined : target;
|
|
968
|
+
}
|
|
969
|
+
const entries = this.activeEntriesForDraft(proposal.draft);
|
|
970
|
+
return entries.find(entry => entry.documentState !== 'open' && (normalizedBody(entry.body) === normalizedBody(proposal.draft.body)
|
|
971
|
+
|| normalizedTitle(entry.title) === normalizedTitle(proposal.draft.title)
|
|
972
|
+
|| sharesCanonicalTopicReference(entry, proposal.draft)));
|
|
973
|
+
}
|
|
828
974
|
activeEntry(id) {
|
|
829
975
|
const row = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id=? AND status='active'`).get(id);
|
|
830
976
|
if (row === undefined)
|
|
@@ -858,6 +1004,9 @@ export class LocalKnowledgeProvider {
|
|
|
858
1004
|
let draft = decision.draft === undefined ? candidate.draft : normalizeDraft(decision.draft);
|
|
859
1005
|
if (decision.decision === 'approve') {
|
|
860
1006
|
if (candidate.action === 'conflict') {
|
|
1007
|
+
if (decision.resolution !== 'merge') {
|
|
1008
|
+
throw conflict('conflict candidate requires an explicit merge resolution');
|
|
1009
|
+
}
|
|
861
1010
|
if (candidate.targetId === undefined)
|
|
862
1011
|
throw new Error('candidate target is missing');
|
|
863
1012
|
const targetRow = this.db.prepare(`SELECT ${ENTRY_COLUMNS} FROM knowledge_entries WHERE id = ?`).get(candidate.targetId);
|
|
@@ -877,8 +1026,22 @@ export class LocalKnowledgeProvider {
|
|
|
877
1026
|
reason: candidate.reason,
|
|
878
1027
|
});
|
|
879
1028
|
if (resolution.outcome === 'conflict') {
|
|
880
|
-
|
|
1029
|
+
const proposal = resolution.proposal;
|
|
1030
|
+
this.db.prepare(`
|
|
1031
|
+
UPDATE knowledge_candidates
|
|
1032
|
+
SET action='conflict', target_id=?, draft_json=?, reason=?
|
|
1033
|
+
WHERE id=? AND status='pending'
|
|
1034
|
+
`).run(proposal.targetId ?? null, JSON.stringify(proposal.draft), proposal.reason, id);
|
|
1035
|
+
return {
|
|
1036
|
+
...candidate,
|
|
1037
|
+
action: 'conflict',
|
|
1038
|
+
...proposal.targetId === undefined ? {} : { targetId: proposal.targetId },
|
|
1039
|
+
draft: proposal.draft,
|
|
1040
|
+
reason: proposal.reason,
|
|
1041
|
+
};
|
|
881
1042
|
}
|
|
1043
|
+
if (resolution.outcome === 'finalized')
|
|
1044
|
+
throw finalizedConflict(resolution.entry);
|
|
882
1045
|
if (resolution.outcome !== 'duplicate') {
|
|
883
1046
|
if (resolution.proposal.action === 'create')
|
|
884
1047
|
this.insertEntry(resolution.proposal.draft);
|
|
@@ -1000,6 +1163,9 @@ export class LocalKnowledgeProvider {
|
|
|
1000
1163
|
confidence: entry.confidence,
|
|
1001
1164
|
...entry.source === undefined ? {} : { source: entry.source },
|
|
1002
1165
|
status: entry.status,
|
|
1166
|
+
documentState: entry.documentState,
|
|
1167
|
+
...entry.finalizedAt === undefined ? {} : { finalizedAt: entry.finalizedAt },
|
|
1168
|
+
...entry.finalizationNote === undefined ? {} : { finalizationNote: entry.finalizationNote },
|
|
1003
1169
|
};
|
|
1004
1170
|
this.db.prepare(`INSERT INTO knowledge_versions(id,knowledge_id,version,snapshot_json,change_kind,created_at) VALUES(?,?,?,?,?,?)`)
|
|
1005
1171
|
.run(newId(), entry.id, entry.version, JSON.stringify(snapshot), changeKind, nowIso());
|
|
@@ -1039,6 +1205,9 @@ export class LocalKnowledgeProvider {
|
|
|
1039
1205
|
scope: entry.scope,
|
|
1040
1206
|
confidence: entry.confidence,
|
|
1041
1207
|
status: entry.status,
|
|
1208
|
+
documentState: entry.documentState,
|
|
1209
|
+
...entry.finalizedAt === undefined ? {} : { finalizedAt: entry.finalizedAt },
|
|
1210
|
+
...entry.finalizationNote === undefined ? {} : { finalizationNote: entry.finalizationNote },
|
|
1042
1211
|
},
|
|
1043
1212
|
title: entry.title,
|
|
1044
1213
|
body: entry.body,
|
|
@@ -1063,17 +1232,23 @@ export class LocalKnowledgeProvider {
|
|
|
1063
1232
|
for (const document of desired.values()) {
|
|
1064
1233
|
this.db.prepare(`
|
|
1065
1234
|
INSERT INTO knowledge_documents(
|
|
1066
|
-
id,knowledge_base_id,rel_path,title,content,entry_count,content_hash,
|
|
1067
|
-
|
|
1235
|
+
id,knowledge_base_id,rel_path,title,content,entry_count,content_hash,
|
|
1236
|
+
document_state,finalized_at,finalization_note,created_at,updated_at
|
|
1237
|
+
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)
|
|
1068
1238
|
ON CONFLICT(id) DO UPDATE SET
|
|
1069
1239
|
knowledge_base_id=excluded.knowledge_base_id,rel_path=excluded.rel_path,
|
|
1070
1240
|
title=excluded.title,content=excluded.content,entry_count=excluded.entry_count,
|
|
1071
|
-
content_hash=excluded.content_hash,
|
|
1241
|
+
content_hash=excluded.content_hash,document_state=excluded.document_state,
|
|
1242
|
+
finalized_at=excluded.finalized_at,finalization_note=excluded.finalization_note,
|
|
1243
|
+
updated_at=excluded.updated_at
|
|
1072
1244
|
WHERE knowledge_documents.content_hash<>excluded.content_hash
|
|
1073
1245
|
OR knowledge_documents.rel_path<>excluded.rel_path
|
|
1074
1246
|
OR knowledge_documents.title<>excluded.title
|
|
1075
1247
|
OR knowledge_documents.entry_count<>excluded.entry_count
|
|
1076
|
-
|
|
1248
|
+
OR knowledge_documents.document_state<>excluded.document_state
|
|
1249
|
+
OR knowledge_documents.finalized_at IS NOT excluded.finalized_at
|
|
1250
|
+
OR knowledge_documents.finalization_note IS NOT excluded.finalization_note
|
|
1251
|
+
`).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);
|
|
1077
1252
|
}
|
|
1078
1253
|
for (const row of existing) {
|
|
1079
1254
|
if (!desired.has(String(row.id)))
|
|
@@ -1098,6 +1273,9 @@ function rowToEntry(row) {
|
|
|
1098
1273
|
: { kind: 'project', id: String(row.scope_id) },
|
|
1099
1274
|
confidence: Number(row.confidence),
|
|
1100
1275
|
status: String(row.status),
|
|
1276
|
+
documentState: row.document_state == null ? 'open' : String(row.document_state),
|
|
1277
|
+
...row.finalized_at == null ? {} : { finalizedAt: String(row.finalized_at) },
|
|
1278
|
+
...row.finalization_note == null ? {} : { finalizationNote: String(row.finalization_note) },
|
|
1101
1279
|
version: Number(row.version),
|
|
1102
1280
|
...source === undefined ? {} : { source },
|
|
1103
1281
|
createdAt: String(row.created_at),
|
|
@@ -1113,6 +1291,9 @@ function rowToDocument(row) {
|
|
|
1113
1291
|
content: String(row.content),
|
|
1114
1292
|
entryCount: Number(row.entry_count),
|
|
1115
1293
|
contentHash: String(row.content_hash),
|
|
1294
|
+
documentState: row.document_state == null ? 'open' : String(row.document_state),
|
|
1295
|
+
...row.finalized_at == null ? {} : { finalizedAt: String(row.finalized_at) },
|
|
1296
|
+
...row.finalization_note == null ? {} : { finalizationNote: String(row.finalization_note) },
|
|
1116
1297
|
createdAt: String(row.created_at),
|
|
1117
1298
|
updatedAt: String(row.updated_at),
|
|
1118
1299
|
};
|
|
@@ -1121,6 +1302,8 @@ function rowToVersion(row) {
|
|
|
1121
1302
|
const snapshot = JSON.parse(String(row.snapshot_json));
|
|
1122
1303
|
if (snapshot.knowledgeBaseId === undefined)
|
|
1123
1304
|
snapshot.knowledgeBaseId = DEFAULT_KNOWLEDGE_BASE_ID;
|
|
1305
|
+
if (snapshot.documentState === undefined)
|
|
1306
|
+
snapshot.documentState = 'open';
|
|
1124
1307
|
return {
|
|
1125
1308
|
id: String(row.id),
|
|
1126
1309
|
knowledgeId: String(row.knowledge_id),
|
|
@@ -1193,6 +1376,8 @@ function potentiallyConflicts(current, incoming) {
|
|
|
1193
1376
|
const incomingBody = normalizedBody(incoming.body);
|
|
1194
1377
|
if (currentBody === incomingBody || currentBody.includes(incomingBody) || incomingBody.includes(currentBody))
|
|
1195
1378
|
return false;
|
|
1379
|
+
if (addsDistinctMarkdownSections(current.body, incoming.body))
|
|
1380
|
+
return false;
|
|
1196
1381
|
const overlap = termOverlap(currentBody, incomingBody);
|
|
1197
1382
|
if (overlap < 0.35)
|
|
1198
1383
|
return false;
|
|
@@ -1205,6 +1390,18 @@ function potentiallyConflicts(current, incoming) {
|
|
|
1205
1390
|
return currentValues.length > 0 && incomingValues.length > 0
|
|
1206
1391
|
&& !currentValues.some(value => incomingValues.includes(value));
|
|
1207
1392
|
}
|
|
1393
|
+
function addsDistinctMarkdownSections(current, incoming) {
|
|
1394
|
+
const currentHeadings = markdownHeadings(current);
|
|
1395
|
+
const incomingHeadings = markdownHeadings(incoming);
|
|
1396
|
+
if (currentHeadings.size === 0 || incomingHeadings.size === 0)
|
|
1397
|
+
return false;
|
|
1398
|
+
return [...incomingHeadings].every(heading => !currentHeadings.has(heading));
|
|
1399
|
+
}
|
|
1400
|
+
function markdownHeadings(value) {
|
|
1401
|
+
return new Set([...value.matchAll(/^#{1,6}\s+(.+)$/gmu)]
|
|
1402
|
+
.map(match => normalizedTitle(match[1] ?? ''))
|
|
1403
|
+
.filter(Boolean));
|
|
1404
|
+
}
|
|
1208
1405
|
function normalizedTitle(value) {
|
|
1209
1406
|
return value.normalize('NFKC').toLocaleLowerCase().replace(/[\p{P}\p{S}\s]+/gu, '');
|
|
1210
1407
|
}
|
|
@@ -1269,6 +1466,7 @@ function rowToKnowledgeBase(row) {
|
|
|
1269
1466
|
description: String(row.description),
|
|
1270
1467
|
defaultTags: JSON.parse(String(row.default_tags_json)),
|
|
1271
1468
|
extractionInstructions: String(row.extraction_instructions),
|
|
1469
|
+
writebackPolicy: String(row.writeback_policy),
|
|
1272
1470
|
...writebackProvider === undefined || writebackModel === undefined ? {} : { writebackProvider, writebackModel },
|
|
1273
1471
|
status: String(row.status),
|
|
1274
1472
|
createdAt: String(row.created_at),
|
|
@@ -1377,4 +1575,16 @@ function notFound(kind, id) {
|
|
|
1377
1575
|
function conflict(message) {
|
|
1378
1576
|
return Object.assign(new Error(message), { code: 'CONFLICT' });
|
|
1379
1577
|
}
|
|
1578
|
+
function finalizedConflict(entry) {
|
|
1579
|
+
const label = entry.documentState === 'resolved' ? 'resolved' : 'collection complete';
|
|
1580
|
+
return conflict(`knowledge document "${entry.title}" (${entry.id}) is finalized as ${label}; reopen it before making changes`);
|
|
1581
|
+
}
|
|
1582
|
+
function normalizeFinalizationNote(value) {
|
|
1583
|
+
const note = value?.trim();
|
|
1584
|
+
if (!note)
|
|
1585
|
+
return undefined;
|
|
1586
|
+
if (note.length > 1000)
|
|
1587
|
+
throw new Error('finalization note must contain at most 1000 characters');
|
|
1588
|
+
return note;
|
|
1589
|
+
}
|
|
1380
1590
|
//# sourceMappingURL=local-provider.js.map
|