@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.
@@ -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.version AS version,
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 > 6)
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
- this.db.prepare('UPDATE knowledge_settings SET writeback_policy=?,updated_at=? WHERE id=1')
230
- .run(patch.writebackPolicy, updatedAt);
231
- return { ...patch, updatedAt };
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(?,?,?,?,?,?,?,'active',?,?)
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 = { ...draft, id, status: 'active', version: 1, createdAt: timestamp, updatedAt: timestamp };
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,version,content_hash,source_json,created_at,updated_at
649
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
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
- return this.insertCandidate(normalizeProposal(input), sourceKey);
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
- throw conflict('knowledge changed during review and now requires conflict resolution');
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,created_at,updated_at
1067
- ) VALUES(?,?,?,?,?,?,?,?,?)
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,updated_at=excluded.updated_at
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
- `).run(document.entry.id, knowledgeBaseId, document.relPath, document.entry.title, document.content, 1, document.contentHash, document.entry.createdAt, document.entry.updatedAt);
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