@dikolab/kbdb 0.2.2 → 0.3.0

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.
Files changed (118) hide show
  1. package/README.md +50 -15
  2. package/dist/cli.cjs +461 -72
  3. package/dist/cli.cjs.map +4 -4
  4. package/dist/cli.mjs +453 -64
  5. package/dist/cli.mjs.map +4 -4
  6. package/dist/kbdb-worker.cjs +395 -60
  7. package/dist/kbdb-worker.cjs.map +4 -4
  8. package/dist/mod.cjs +17 -0
  9. package/dist/mod.cjs.map +2 -2
  10. package/dist/mod.mjs +17 -0
  11. package/dist/mod.mjs.map +2 -2
  12. package/dist/src/shared/cli/functions/dispatch-command.function.d.ts +3 -1
  13. package/dist/src/shared/cli/functions/install-default-skills.function.d.ts +12 -0
  14. package/dist/src/shared/cli/functions/run-check.function.d.ts +47 -5
  15. package/dist/src/shared/cli/functions/run-export.function.d.ts +34 -0
  16. package/dist/src/shared/cli/functions/run-gc.function.d.ts +6 -0
  17. package/dist/src/shared/cli/functions/run-learn.function.d.ts +35 -6
  18. package/dist/src/shared/cli/functions/run-recall.function.d.ts +45 -0
  19. package/dist/src/shared/cli/functions/run-search.function.d.ts +52 -15
  20. package/dist/src/shared/cli/index.d.ts +4 -0
  21. package/dist/src/shared/cli/typings/cli-options.interface.d.ts +13 -0
  22. package/dist/src/shared/cli/typings/learn-result.model.d.ts +19 -0
  23. package/dist/src/shared/cli/typings/search-display.model.d.ts +18 -0
  24. package/dist/src/shared/mcp/classes/mcp-server.class.d.ts +8 -0
  25. package/dist/src/shared/mcp/typings/mcp-client.interface.d.ts +43 -0
  26. package/dist/src/shared/wasm-codec/functions/decode-add-section-result.function.d.ts +48 -0
  27. package/dist/src/shared/wasm-codec/functions/decode-scored-results.function.d.ts +58 -0
  28. package/dist/src/shared/wasm-codec/functions/decode-section-records.function.d.ts +33 -0
  29. package/dist/src/shared/wasm-codec/functions/encode-add-section-params.function.d.ts +52 -0
  30. package/dist/src/shared/wasm-codec/index.d.ts +2 -0
  31. package/dist/src/shared/worker-client/classes/worker-client.class.d.ts +16 -0
  32. package/dist/src/shared/worker-client/index.d.ts +1 -1
  33. package/dist/src/shared/worker-client/typings/add-section-result.model.d.ts +29 -15
  34. package/dist/src/shared/worker-client/typings/export-result.model.d.ts +14 -0
  35. package/dist/src/shared/worker-client/typings/gc-result.model.d.ts +6 -0
  36. package/dist/src/shared/worker-client/typings/search-result.model.d.ts +12 -0
  37. package/dist/src/shared/worker-client/typings/section.model.d.ts +16 -0
  38. package/dist/src/shared/worker-daemon/classes/worker-daemon.class.d.ts +35 -0
  39. package/dist/wasm/fs-database/kbdb_fs_database.d.ts +22 -1
  40. package/dist/wasm/fs-database/kbdb_fs_database.js +28 -1
  41. package/dist/wasm/fs-database/kbdb_fs_database_bg.wasm +0 -0
  42. package/dist/wasm/fs-database/kbdb_fs_database_bg.wasm.d.ts +1 -0
  43. package/dist/wasm/kb-worker/kbdb_worker.d.ts +48 -1
  44. package/dist/wasm/kb-worker/kbdb_worker.js +60 -1
  45. package/dist/wasm/kb-worker/kbdb_worker_bg.wasm +0 -0
  46. package/dist/wasm/kb-worker/kbdb_worker_bg.wasm.d.ts +2 -0
  47. package/dist/wasm/query-parser/kbdb_query_parser_bg.wasm +0 -0
  48. package/dist/worker.mjs +396 -60
  49. package/dist/worker.mjs.map +4 -4
  50. package/docs/details/cli.md +102 -15
  51. package/docs/details/library-api.md +55 -6
  52. package/docs/details/mcp-server.md +8 -4
  53. package/docs/details/search-and-ranking.md +63 -5
  54. package/docs/details/storage.md +2 -0
  55. package/docs/goals/cli.md +255 -14
  56. package/docs/goals/database.md +244 -17
  57. package/docs/goals/document.md +1 -1
  58. package/docs/goals/mcp.md +83 -14
  59. package/docs/goals/query-parser.md +98 -4
  60. package/docs/goals/query-result.md +177 -10
  61. package/docs/goals/skills.md +72 -0
  62. package/docs/goals/worker-daemon.md +54 -7
  63. package/docs/modules/rust/fs-database/functions/integrity.md +11 -0
  64. package/docs/modules/rust/fs-database/functions/section.md +23 -6
  65. package/docs/modules/rust/fs-database/module.md +1 -0
  66. package/docs/modules/rust/fs-database/types/section.md +27 -1
  67. package/docs/modules/rust/overview.md +2 -1
  68. package/docs/modules/rust/query-parser/functions/module.md +2 -2
  69. package/docs/modules/rust/query-parser/functions/pipeline.md +44 -6
  70. package/docs/modules/rust/query-parser/module.md +5 -4
  71. package/docs/modules/rust/query-parser/types/keyphrase.md +11 -0
  72. package/docs/modules/rust/query-parser/types/module.md +3 -3
  73. package/docs/modules/typescript/shared/cli/functions.md +68 -3
  74. package/package.json +1 -1
  75. package/src/shared/cli/functions/dispatch-command.function.ts +53 -1
  76. package/src/shared/cli/functions/format-output.function.ts +36 -12
  77. package/src/shared/cli/functions/generate-help.function.ts +56 -1
  78. package/src/shared/cli/functions/install-default-skills.function.ts +296 -0
  79. package/src/shared/cli/functions/parse-args.function.ts +13 -0
  80. package/src/shared/cli/functions/run-check.function.ts +87 -6
  81. package/src/shared/cli/functions/run-export.function.ts +40 -0
  82. package/src/shared/cli/functions/run-gc.function.ts +6 -0
  83. package/src/shared/cli/functions/run-init.function.ts +2 -0
  84. package/src/shared/cli/functions/run-learn.function.ts +68 -16
  85. package/src/shared/cli/functions/run-recall.function.ts +66 -0
  86. package/src/shared/cli/functions/run-search.function.ts +58 -15
  87. package/src/shared/cli/index.ts +4 -0
  88. package/src/shared/cli/typings/cli-options.interface.ts +13 -0
  89. package/src/shared/cli/typings/learn-result.model.ts +16 -0
  90. package/src/shared/cli/typings/search-display.model.ts +18 -0
  91. package/src/shared/mcp/classes/mcp-server.class.ts +30 -1
  92. package/src/shared/mcp/functions/create-tool-definitions.function.ts +46 -5
  93. package/src/shared/mcp/functions/handle-tool-call.function.ts +70 -2
  94. package/src/shared/mcp/typings/mcp-client.interface.ts +45 -0
  95. package/src/shared/wasm-codec/functions/decode-add-section-result.function.ts +112 -0
  96. package/src/shared/wasm-codec/functions/decode-scored-results.function.ts +86 -0
  97. package/src/shared/wasm-codec/functions/decode-section-records.function.ts +87 -0
  98. package/src/shared/wasm-codec/functions/encode-add-section-params.function.ts +68 -0
  99. package/src/shared/wasm-codec/index.ts +5 -0
  100. package/src/shared/worker-client/classes/worker-client.class.ts +19 -0
  101. package/src/shared/worker-client/index.ts +4 -1
  102. package/src/shared/worker-client/typings/add-section-result.model.ts +30 -15
  103. package/src/shared/worker-client/typings/export-result.model.ts +14 -0
  104. package/src/shared/worker-client/typings/gc-result.model.ts +6 -0
  105. package/src/shared/worker-client/typings/search-result.model.ts +12 -0
  106. package/src/shared/worker-client/typings/section.model.ts +16 -0
  107. package/src/shared/worker-daemon/classes/worker-daemon.class.ts +315 -46
  108. package/src/shared/worker-daemon/functions/create-host-imports.function.ts +19 -4
  109. package/src/shared/worker-daemon/functions/create-ipc-server.function.ts +5 -1
  110. package/src/wasm/fs-database/kbdb_fs_database.d.ts +22 -1
  111. package/src/wasm/fs-database/kbdb_fs_database.js +28 -1
  112. package/src/wasm/fs-database/kbdb_fs_database_bg.wasm +0 -0
  113. package/src/wasm/fs-database/kbdb_fs_database_bg.wasm.d.ts +1 -0
  114. package/src/wasm/kb-worker/kbdb_worker.d.ts +48 -1
  115. package/src/wasm/kb-worker/kbdb_worker.js +60 -1
  116. package/src/wasm/kb-worker/kbdb_worker_bg.wasm +0 -0
  117. package/src/wasm/kb-worker/kbdb_worker_bg.wasm.d.ts +2 -0
  118. package/src/wasm/query-parser/kbdb_query_parser_bg.wasm +0 -0
@@ -171,8 +171,9 @@ function createHostImports(contextPath, memoryRef = nullMemory, fs = nodeFsAdapt
171
171
  }
172
172
  function readPathArg(ptr, len) {
173
173
  const bytes = new Uint8Array(getBuffer(), ptr, len);
174
- const relative = new TextDecoder().decode(bytes);
175
- return (0, import_node_path4.join)(contextPath, relative);
174
+ const decoded = new TextDecoder().decode(bytes);
175
+ if (decoded.startsWith("/")) return decoded;
176
+ return (0, import_node_path4.join)(contextPath, decoded);
176
177
  }
177
178
  return {
178
179
  /**
@@ -192,14 +193,15 @@ function createHostImports(contextPath, memoryRef = nullMemory, fs = nodeFsAdapt
192
193
  const filePath = readPathArg(pathPtr, pathLen);
193
194
  const data = fs.readFileSync(filePath);
194
195
  const dataLen = data.byteLength;
195
- const dv = new DataView(getBuffer());
196
196
  if (dataLen === 0) {
197
- dv.setUint32(outPtr, 0, true);
198
- dv.setUint32(outLen, 0, true);
197
+ const dv2 = new DataView(getBuffer());
198
+ dv2.setUint32(outPtr, 0, true);
199
+ dv2.setUint32(outLen, 0, true);
199
200
  return 0;
200
201
  }
201
202
  const dataPtr = allocator.alloc(dataLen);
202
203
  if (dataPtr === 0) return -1;
204
+ const dv = new DataView(getBuffer());
203
205
  const memView = new Uint8Array(getBuffer());
204
206
  memView.set(
205
207
  new Uint8Array(
@@ -277,14 +279,15 @@ function createHostImports(contextPath, memoryRef = nullMemory, fs = nodeFsAdapt
277
279
  const entries = fs.readdirSync(dirPath);
278
280
  const text = entries.join("\n");
279
281
  const encoded = new TextEncoder().encode(text);
280
- const dv = new DataView(getBuffer());
281
282
  if (encoded.length === 0) {
282
- dv.setUint32(outPtr, 0, true);
283
- dv.setUint32(outLen, 0, true);
283
+ const dv2 = new DataView(getBuffer());
284
+ dv2.setUint32(outPtr, 0, true);
285
+ dv2.setUint32(outLen, 0, true);
284
286
  return 0;
285
287
  }
286
288
  const dataPtr = allocator.alloc(encoded.length);
287
289
  if (dataPtr === 0) return -1;
290
+ const dv = new DataView(getBuffer());
288
291
  new Uint8Array(getBuffer(), dataPtr, encoded.length).set(
289
292
  encoded
290
293
  );
@@ -388,13 +391,16 @@ var import_node_net = require("node:net");
388
391
  function createIpcServer(socketPath, handler) {
389
392
  const server = (0, import_node_net.createServer)((socket) => {
390
393
  let buffer = "";
394
+ let processingQueue = Promise.resolve();
391
395
  socket.on("data", (chunk) => {
392
396
  buffer += chunk.toString();
393
397
  const lines = buffer.split("\n");
394
398
  buffer = lines.pop() ?? "";
395
399
  for (const line of lines) {
396
400
  if (!line.trim()) continue;
397
- void handleLine(line, handler, socket);
401
+ processingQueue = processingQueue.then(
402
+ () => handleLine(line, handler, socket)
403
+ );
398
404
  }
399
405
  });
400
406
  socket.on("error", (err) => {
@@ -492,13 +498,27 @@ function encodeSectionType(kind, mimeType) {
492
498
 
493
499
  // src/shared/wasm-codec/functions/encode-add-section-params.function.ts
494
500
  function encodeAddSectionParams(params) {
501
+ const tags = params.tags ?? [];
502
+ const tagParts = [];
503
+ for (const tag of tags) {
504
+ tagParts.push(encodeString(tag));
505
+ }
506
+ const tagsCountBytes = encodeU32(tags.length);
507
+ const replaceByte = new Uint8Array([
508
+ params.replace === true ? 1 : 0
509
+ ]);
495
510
  const parts = [
496
511
  encodeSectionType(params.sectionType, params.mimeType),
497
512
  encodeString(params.content),
498
513
  encodeOptionString(params.title),
499
514
  encodeOptionString(params.description),
500
515
  encodeOptionString(params.docid),
501
- encodeOptionString(params.typeName)
516
+ encodeOptionString(params.typeName),
517
+ encodeOptionString(params.sourcePath),
518
+ encodeOptionString(params.sourceMtime),
519
+ tagsCountBytes,
520
+ ...tagParts,
521
+ replaceByte
502
522
  ];
503
523
  const totalLen = parts.reduce((sum, p) => sum + p.length, 0);
504
524
  const result = new Uint8Array(totalLen);
@@ -655,6 +675,10 @@ function decodePagedSearchResult(bytes) {
655
675
  offset += limit.bytesRead;
656
676
  const hasMore = bytes[offset] === 1;
657
677
  offset += 1;
678
+ const relaxed = offset < bytes.length && bytes[offset] === 1;
679
+ if (offset < bytes.length) {
680
+ offset += 1;
681
+ }
658
682
  const itemCount = decodeU32(bytes, offset);
659
683
  offset += itemCount.bytesRead;
660
684
  const items = [];
@@ -675,6 +699,8 @@ function decodePagedSearchResult(bytes) {
675
699
  }
676
700
  const score = decodeF32(bytes, offset);
677
701
  offset += score.bytesRead;
702
+ const confidence = decodeF32(bytes, offset);
703
+ offset += confidence.bytesRead;
678
704
  const snippet = decodeString(bytes, offset);
679
705
  offset += snippet.bytesRead;
680
706
  const termCount = decodeU32(bytes, offset);
@@ -685,14 +711,27 @@ function decodePagedSearchResult(bytes) {
685
711
  offset += term.bytesRead;
686
712
  matched_terms.push(term.value);
687
713
  }
714
+ const fieldCount = decodeU32(bytes, offset);
715
+ offset += fieldCount.bytesRead;
716
+ const matched_fields = [];
717
+ for (let j = 0; j < fieldCount.value; j++) {
718
+ const field = decodeString(bytes, offset);
719
+ offset += field.bytesRead;
720
+ matched_fields.push(field.value);
721
+ }
722
+ const createdAt = decodeString(bytes, offset);
723
+ offset += createdAt.bytesRead;
688
724
  items.push({
689
725
  kbid: kbid.value,
690
726
  heading: heading.value,
691
727
  type: sectionType.value,
692
728
  docids,
693
729
  score: score.value,
730
+ confidence: confidence.value,
694
731
  snippet: snippet.value,
695
- matched_terms
732
+ matched_terms,
733
+ matched_fields,
734
+ created_at: createdAt.value
696
735
  });
697
736
  }
698
737
  return {
@@ -700,7 +739,8 @@ function decodePagedSearchResult(bytes) {
700
739
  total: total.value,
701
740
  offset: resultOffset.value,
702
741
  limit: limit.value,
703
- has_more: hasMore
742
+ has_more: hasMore,
743
+ relaxed
704
744
  };
705
745
  }
706
746
 
@@ -760,6 +800,47 @@ function decodeSectionRecords(bytes) {
760
800
  offset += codeTokenCount.bytesRead;
761
801
  const typeName = decodeString(bytes, offset);
762
802
  offset += typeName.bytesRead;
803
+ let sourceKey = null;
804
+ if (offset < bytes.length) {
805
+ const v = decodeOptionString2(bytes, offset);
806
+ offset += v.bytesRead;
807
+ sourceKey = v.value;
808
+ }
809
+ let sourcePath = null;
810
+ if (offset < bytes.length) {
811
+ const v = decodeOptionString2(bytes, offset);
812
+ offset += v.bytesRead;
813
+ sourcePath = v.value;
814
+ }
815
+ let sourceMtime = null;
816
+ if (offset < bytes.length) {
817
+ const v = decodeOptionString2(bytes, offset);
818
+ offset += v.bytesRead;
819
+ sourceMtime = v.value;
820
+ }
821
+ let updatedAt = null;
822
+ if (offset < bytes.length) {
823
+ const v = decodeOptionString2(bytes, offset);
824
+ offset += v.bytesRead;
825
+ updatedAt = v.value;
826
+ }
827
+ let tags = [];
828
+ if (offset < bytes.length) {
829
+ const tagCount = decodeU32(bytes, offset);
830
+ offset += tagCount.bytesRead;
831
+ tags = [];
832
+ for (let j = 0; j < tagCount.value; j++) {
833
+ const tag = decodeString(bytes, offset);
834
+ offset += tag.bytesRead;
835
+ tags.push(tag.value);
836
+ }
837
+ }
838
+ let supersedes = null;
839
+ if (offset < bytes.length) {
840
+ const v = decodeOptionString2(bytes, offset);
841
+ offset += v.bytesRead;
842
+ supersedes = v.value;
843
+ }
763
844
  const record = {
764
845
  kbid: kbid.value,
765
846
  docid: docid.value,
@@ -774,7 +855,13 @@ function decodeSectionRecords(bytes) {
774
855
  headingTokenCount: headingTokenCount.value,
775
856
  bodyTokenCount: bodyTokenCount.value,
776
857
  codeTokenCount: codeTokenCount.value,
777
- typeName: typeName.value
858
+ typeName: typeName.value,
859
+ sourceKey,
860
+ sourcePath,
861
+ sourceMtime,
862
+ updatedAt,
863
+ tags,
864
+ supersedes
778
865
  };
779
866
  if (st.mimeType !== void 0) {
780
867
  record.mimeType = st.mimeType;
@@ -1094,6 +1181,54 @@ function decodeOneResult(bytes, startOffset) {
1094
1181
  };
1095
1182
  }
1096
1183
 
1184
+ // src/shared/wasm-codec/functions/decode-add-section-result.function.ts
1185
+ function decodeAddSectionResult(bytes) {
1186
+ let offset = 0;
1187
+ const kbid = decodeString(bytes, offset);
1188
+ offset += kbid.bytesRead;
1189
+ const indexedTermsCount = decodeU32(bytes, offset);
1190
+ offset += indexedTermsCount.bytesRead;
1191
+ const elapsedMsBig = decodeU64(bytes, offset);
1192
+ offset += elapsedMsBig.bytesRead;
1193
+ const supersededTag = bytes[offset];
1194
+ offset += 1;
1195
+ let superseded = null;
1196
+ if (supersededTag === 1) {
1197
+ const supersededStr = decodeString(bytes, offset);
1198
+ offset += supersededStr.bytesRead;
1199
+ superseded = supersededStr.value;
1200
+ }
1201
+ const warningCount = decodeU32(bytes, offset);
1202
+ offset += warningCount.bytesRead;
1203
+ const warnings = [];
1204
+ for (let i = 0; i < warningCount.value; i++) {
1205
+ const warning = decodeString(bytes, offset);
1206
+ offset += warning.bytesRead;
1207
+ warnings.push(warning.value);
1208
+ }
1209
+ const dupCount = decodeU32(bytes, offset);
1210
+ offset += dupCount.bytesRead;
1211
+ const nearDuplicates = [];
1212
+ for (let i = 0; i < dupCount.value; i++) {
1213
+ const dupKbid = decodeString(bytes, offset);
1214
+ offset += dupKbid.bytesRead;
1215
+ const similarity = decodeF32(bytes, offset);
1216
+ offset += similarity.bytesRead;
1217
+ nearDuplicates.push({
1218
+ kbid: dupKbid.value,
1219
+ similarity: similarity.value
1220
+ });
1221
+ }
1222
+ return {
1223
+ kbid: kbid.value,
1224
+ indexedTermsCount: indexedTermsCount.value,
1225
+ elapsedMs: Number(elapsedMsBig.value),
1226
+ superseded,
1227
+ warnings,
1228
+ nearDuplicates
1229
+ };
1230
+ }
1231
+
1097
1232
  // src/shared/worker-daemon/classes/worker-daemon.class.ts
1098
1233
  var WASM_METHOD_MAP = {
1099
1234
  search: "worker_search",
@@ -1133,7 +1268,9 @@ var WASM_METHOD_MAP = {
1133
1268
  list_by_type: "worker_list_by_type",
1134
1269
  listDocumentsByType: "worker_list_docs_by_type",
1135
1270
  list_docs_by_type: "worker_list_docs_by_type",
1136
- recall: "worker_recall"
1271
+ recall: "worker_recall",
1272
+ export: "worker_export",
1273
+ exportDb: "worker_export"
1137
1274
  };
1138
1275
  var CONFIG_PREFIX = "config_";
1139
1276
  var CONFIG_KEY_MAP = {
@@ -1168,6 +1305,14 @@ var WorkerDaemon = class {
1168
1305
  _idleTimer = null;
1169
1306
  _server = null;
1170
1307
  _wasmDir;
1308
+ /** Queue of waiters for the single-writer lock. */
1309
+ _writeQueue = [];
1310
+ /** Whether the write lock is currently held. */
1311
+ _writeLocked = false;
1312
+ /** Whether the startup integrity check passed. */
1313
+ _integrityOk = true;
1314
+ /** Integrity errors found at startup, if any. */
1315
+ _integrityErrors = [];
1171
1316
  /**
1172
1317
  * @param ctx - context ID (16-hex characters), the first 16 of
1173
1318
  * the SHA-256 hash of `targetDir`
@@ -1229,6 +1374,7 @@ var WorkerDaemon = class {
1229
1374
  );
1230
1375
  this._wasmModules = wasmModules;
1231
1376
  this.initWorkerContext();
1377
+ this.runStartupIntegrityCheck();
1232
1378
  const pidPath = (0, import_node_path6.join)((0, import_node_os2.tmpdir)(), `kbdb-${this.ctx}.pid`);
1233
1379
  (0, import_node_fs4.writeFileSync)(pidPath, process.pid.toString());
1234
1380
  const ipcPath = getIpcPath(this.ctx);
@@ -1417,7 +1563,11 @@ var WorkerDaemon = class {
1417
1563
  */
1418
1564
  dispatch(method, params) {
1419
1565
  if (method === "status") {
1420
- return this._connectionCount > 0 ? "busy" : "idle";
1566
+ return {
1567
+ status: this._connectionCount > 0 ? "busy" : "idle",
1568
+ integrityOk: this._integrityOk,
1569
+ integrityErrors: this._integrityErrors
1570
+ };
1421
1571
  }
1422
1572
  if (method === "idle_at") {
1423
1573
  return this._idleAt;
@@ -1434,67 +1584,31 @@ var WorkerDaemon = class {
1434
1584
  }
1435
1585
  throw new IpcError(-32601, `Method not found: ${method}`);
1436
1586
  }
1437
- dispatchWasm(method, wasmExport, params) {
1587
+ async dispatchWasm(method, wasmExport, params) {
1438
1588
  if (method === "search" || method === "query") {
1439
1589
  return this.handleSearch(params);
1440
1590
  }
1441
- if (method === "addSection" || method === "add_section") {
1442
- return this.handleAddSection(params);
1443
- }
1444
- if (method === "updateSection" || method === "update_section") {
1445
- return this.handleUpdateSection(params);
1446
- }
1447
- if (method === "removeSection" || method === "remove_section") {
1448
- return this.handleRemoveSection(params);
1449
- }
1450
1591
  if (method === "readSections" || method === "read_sections") {
1451
1592
  return this.handleReadSections(params);
1452
1593
  }
1453
1594
  if (method === "retrieveDocument" || method === "retrieve_doc") {
1454
1595
  return this.handleRetrieveDoc(params);
1455
1596
  }
1456
- if (method === "groupSections" || method === "group_sections") {
1457
- return this.handleGroupSections(params);
1458
- }
1459
- if (method === "removeGrouping" || method === "remove_grouping") {
1460
- return this.handleRemoveGrouping(params);
1461
- }
1462
1597
  if (method === "content" || method === "compose") {
1463
1598
  return this.handleCompose(params);
1464
1599
  }
1465
1600
  if (method === "integrityCheck" || method === "integrity_check") {
1466
1601
  return this.handleIntegrityCheck();
1467
1602
  }
1468
- if (method === "gc") {
1469
- return this.handleGc();
1470
- }
1471
- if (method === "rebuildIndexes" || method === "rebuild_indexes") {
1472
- return this.handleRebuild();
1473
- }
1474
1603
  if (method === "dbStatus" || method === "db_status") {
1475
1604
  return this.handleDbStatus();
1476
1605
  }
1477
- if (method === "initDirectory" || method === "init_directory") {
1478
- return this.handleInitDirectory(params);
1479
- }
1480
- if (method === "invalidate") {
1481
- return this.handleInvalidate(params);
1482
- }
1483
- if (method === "invalidateAll" || method === "invalidate_all") {
1484
- return this.handleInvalidateAll();
1485
- }
1486
1606
  if (method === "get_result_status") {
1487
1607
  return this.handleResultStatus(params);
1488
1608
  }
1489
1609
  if (method === "get_result") {
1490
1610
  return this.handleGetResult(params);
1491
1611
  }
1492
- if (method === "migrateDatabase") {
1493
- throw new IpcError(
1494
- -32601,
1495
- "migrateDatabase is not yet supported"
1496
- );
1497
- }
1498
1612
  if (method === "listByType" || method === "list_by_type") {
1499
1613
  return this.handleListByType(params);
1500
1614
  }
@@ -1504,7 +1618,51 @@ var WorkerDaemon = class {
1504
1618
  if (method === "recall") {
1505
1619
  return this.handleRecall(params);
1506
1620
  }
1507
- return this.invokeWasmRaw(wasmExport, new Uint8Array(0));
1621
+ if (method === "export" || method === "exportDb") {
1622
+ return this.handleExport(params);
1623
+ }
1624
+ await this.acquireWriteLock();
1625
+ try {
1626
+ if (method === "invalidate") {
1627
+ return this.handleInvalidate(params);
1628
+ }
1629
+ if (method === "invalidateAll" || method === "invalidate_all") {
1630
+ return this.handleInvalidateAll();
1631
+ }
1632
+ if (method === "addSection" || method === "add_section") {
1633
+ return this.handleAddSection(params);
1634
+ }
1635
+ if (method === "updateSection" || method === "update_section") {
1636
+ return this.handleUpdateSection(params);
1637
+ }
1638
+ if (method === "removeSection" || method === "remove_section") {
1639
+ return this.handleRemoveSection(params);
1640
+ }
1641
+ if (method === "groupSections" || method === "group_sections") {
1642
+ return this.handleGroupSections(params);
1643
+ }
1644
+ if (method === "removeGrouping" || method === "remove_grouping") {
1645
+ return this.handleRemoveGrouping(params);
1646
+ }
1647
+ if (method === "gc") {
1648
+ return this.handleGc();
1649
+ }
1650
+ if (method === "rebuildIndexes" || method === "rebuild_indexes") {
1651
+ return this.handleRebuild();
1652
+ }
1653
+ if (method === "initDirectory" || method === "init_directory") {
1654
+ return this.handleInitDirectory(params);
1655
+ }
1656
+ if (method === "migrateDatabase") {
1657
+ throw new IpcError(
1658
+ -32601,
1659
+ "migrateDatabase is not yet supported"
1660
+ );
1661
+ }
1662
+ return this.invokeWasmRaw(wasmExport, new Uint8Array(0));
1663
+ } finally {
1664
+ this.releaseWriteLock();
1665
+ }
1508
1666
  }
1509
1667
  // -- Search/query (token-based async) --
1510
1668
  handleSearch(params) {
@@ -1559,6 +1717,7 @@ var WorkerDaemon = class {
1559
1717
  const sectionType = params["sectionType"] ?? "text";
1560
1718
  const semanticType = sectionType === "text" || sectionType === "code" || sectionType === "image" ? sectionType : "text";
1561
1719
  const typeName = sectionType !== semanticType ? sectionType : null;
1720
+ const tags = Array.isArray(params["tags"]) ? params["tags"] : [];
1562
1721
  const input = {
1563
1722
  sectionType: semanticType,
1564
1723
  mimeType: params["mimeType"],
@@ -1566,12 +1725,24 @@ var WorkerDaemon = class {
1566
1725
  title: params["title"] ?? null,
1567
1726
  description: params["description"] ?? null,
1568
1727
  docid: params["docid"] ?? null,
1569
- typeName
1728
+ typeName,
1729
+ sourcePath: typeof params["sourcePath"] === "string" ? params["sourcePath"] : void 0,
1730
+ sourceMtime: typeof params["sourceMtime"] === "string" ? params["sourceMtime"] : void 0,
1731
+ tags,
1732
+ replace: params["replace"] === true
1570
1733
  };
1571
1734
  const encoded = encodeAddSectionParams(input);
1572
1735
  const bytes = this.callWasmSync("worker_add_section", encoded);
1573
- const kbid = new TextDecoder().decode(bytes);
1574
- return { kbid };
1736
+ const decoded = decodeAddSectionResult(bytes);
1737
+ const nearDups = decoded.nearDuplicates.length > 0 ? decoded.nearDuplicates : this.scanNearDuplicates(input.content, decoded.kbid);
1738
+ return {
1739
+ kbid: decoded.kbid,
1740
+ indexed_terms_count: decoded.indexedTermsCount,
1741
+ elapsed_ms: decoded.elapsedMs,
1742
+ superseded: decoded.superseded,
1743
+ warnings: decoded.warnings,
1744
+ near_duplicates: nearDups
1745
+ };
1575
1746
  }
1576
1747
  handleUpdateSection(params) {
1577
1748
  const oldKbid = params["kbid"] ?? "";
@@ -1645,7 +1816,18 @@ var WorkerDaemon = class {
1645
1816
  handleInvalidate(params) {
1646
1817
  const kbid = params["kbid"] ?? "";
1647
1818
  const encoded = new TextEncoder().encode(kbid);
1648
- this.callWasmSync("worker_invalidate", encoded);
1819
+ const fn = this.getExport("worker_invalidate");
1820
+ const ptr = this.wasmAlloc(encoded.length);
1821
+ this.wasmWrite(ptr, encoded);
1822
+ const rc = fn(ptr, encoded.length);
1823
+ this.wasmFree(ptr, encoded.length);
1824
+ if (rc === -1) {
1825
+ const errBytes = this.readReturnSlot();
1826
+ throw new IpcError(
1827
+ -32e3,
1828
+ new TextDecoder().decode(errBytes) || "invalidate failed"
1829
+ );
1830
+ }
1649
1831
  return { ok: true };
1650
1832
  }
1651
1833
  handleListByType(params) {
@@ -1689,20 +1871,173 @@ var WorkerDaemon = class {
1689
1871
  return decodeKeyValueText(text);
1690
1872
  }
1691
1873
  handleGc() {
1874
+ const start = Date.now();
1692
1875
  this.callWasmZeroArg("worker_garbage_collect");
1693
1876
  const bytes = this.readReturnSlot();
1694
1877
  const text = new TextDecoder().decode(bytes);
1695
- return decodeKeyValueText(text);
1878
+ const kv = decodeKeyValueText(text);
1879
+ const removedCount = Number(kv["removed"] ?? 0);
1880
+ const staleSources = Number(kv["stale_sources"] ?? 0);
1881
+ return {
1882
+ removed_sections: removedCount,
1883
+ removed_bytes: 0,
1884
+ elapsed_ms: Date.now() - start,
1885
+ stale_sources: staleSources
1886
+ };
1696
1887
  }
1697
1888
  handleRebuild() {
1698
- this.callWasmZeroArg("worker_rebuild_indexes");
1699
- return { ok: true };
1889
+ const fn = this.getExport("worker_rebuild_indexes");
1890
+ const rc = fn();
1891
+ if (rc === -1) {
1892
+ const errBytes = this.readReturnSlot();
1893
+ const errMsg = new TextDecoder().decode(errBytes);
1894
+ throw new IpcError(-32e3, errMsg || "rebuild failed");
1895
+ }
1896
+ const resultBytes = this.readReturnSlot();
1897
+ const reindexed = resultBytes.length >= 4 ? new DataView(
1898
+ resultBytes.buffer,
1899
+ resultBytes.byteOffset,
1900
+ resultBytes.byteLength
1901
+ ).getUint32(0, true) : 0;
1902
+ return { ok: true, sections_reindexed: reindexed };
1903
+ }
1904
+ // -- Export (read operation, no lock required) --
1905
+ handleExport(params) {
1906
+ const targetPath = params["targetPath"] ?? params["path"] ?? "./kbdb-export/";
1907
+ const encoded = new TextEncoder().encode(targetPath);
1908
+ const start = Date.now();
1909
+ const bytes = this.callWasmSync("worker_export", encoded);
1910
+ const elapsed = Date.now() - start;
1911
+ const text = new TextDecoder().decode(bytes);
1912
+ const kv = decodeKeyValueText(text);
1913
+ return {
1914
+ path: kv["path"] ?? targetPath,
1915
+ sections_exported: Number(kv["sections_exported"] ?? 0),
1916
+ documents_exported: Number(kv["documents_exported"] ?? 0),
1917
+ elapsed_ms: elapsed
1918
+ };
1919
+ }
1920
+ // -- Near-duplicate detection --
1921
+ scanNearDuplicates(content, newKbid) {
1922
+ const newTerms = this.extractTermSet(content);
1923
+ if (newTerms.size === 0) return [];
1924
+ const sectionsDir = (0, import_node_path6.join)(this.targetDir, "sections");
1925
+ let shards;
1926
+ try {
1927
+ shards = (0, import_node_fs4.readdirSync)(sectionsDir);
1928
+ } catch {
1929
+ return [];
1930
+ }
1931
+ const results = [];
1932
+ for (const shard of shards) {
1933
+ let files;
1934
+ try {
1935
+ files = (0, import_node_fs4.readdirSync)((0, import_node_path6.join)(sectionsDir, shard));
1936
+ } catch {
1937
+ continue;
1938
+ }
1939
+ for (const file of files) {
1940
+ if (!file.endsWith(".section")) continue;
1941
+ const kbid = file.slice(0, -8);
1942
+ if (kbid === newKbid) continue;
1943
+ try {
1944
+ const data = (0, import_node_fs4.readFileSync)(
1945
+ (0, import_node_path6.join)(sectionsDir, shard, file),
1946
+ "utf-8"
1947
+ );
1948
+ const sep = data.indexOf("\n---\n");
1949
+ const body = sep >= 0 ? data.slice(sep + 5) : data;
1950
+ const existing = this.extractTermSet(body);
1951
+ let intersection = 0;
1952
+ for (const t of newTerms) {
1953
+ if (existing.has(t)) intersection++;
1954
+ }
1955
+ const union = newTerms.size + existing.size - intersection;
1956
+ const sim = union > 0 ? intersection / union : 0;
1957
+ if (sim >= 0.85) {
1958
+ results.push({
1959
+ kbid,
1960
+ similarity: Math.round(sim * 100) / 100
1961
+ });
1962
+ }
1963
+ } catch {
1964
+ continue;
1965
+ }
1966
+ }
1967
+ }
1968
+ return results;
1969
+ }
1970
+ extractTermSet(text) {
1971
+ return new Set(
1972
+ text.toLowerCase().split(/\W+/).filter((w) => w.length > 2)
1973
+ );
1974
+ }
1975
+ // -- Single-writer lock --
1976
+ /**
1977
+ * Acquires the write lock.
1978
+ *
1979
+ * If the lock is free it is granted immediately. If it is held the
1980
+ * caller is queued and its promise resolves once the lock is
1981
+ * released by the current holder.
1982
+ */
1983
+ acquireWriteLock() {
1984
+ if (!this._writeLocked) {
1985
+ this._writeLocked = true;
1986
+ return Promise.resolve();
1987
+ }
1988
+ return new Promise((resolve2) => {
1989
+ this._writeQueue.push({ resolve: resolve2 });
1990
+ });
1991
+ }
1992
+ /**
1993
+ * Releases the write lock.
1994
+ *
1995
+ * If waiters are queued the next one is granted the lock
1996
+ * immediately. Otherwise the lock is marked free.
1997
+ */
1998
+ releaseWriteLock() {
1999
+ const next = this._writeQueue.shift();
2000
+ if (next !== void 0) {
2001
+ next.resolve();
2002
+ } else {
2003
+ this._writeLocked = false;
2004
+ }
1700
2005
  }
1701
2006
  // -- WASM lifecycle --
1702
2007
  initWorkerContext() {
1703
2008
  const pathBytes = new TextEncoder().encode(".");
1704
2009
  this.callWasmSync("worker_init", pathBytes);
1705
2010
  }
2011
+ /**
2012
+ * Runs a database integrity check immediately after context
2013
+ * initialisation.
2014
+ *
2015
+ * Stores the result in `_integrityOk` and `_integrityErrors`.
2016
+ * Logs a warning to stderr when issues are found so operators
2017
+ * are alerted without aborting the daemon.
2018
+ */
2019
+ runStartupIntegrityCheck() {
2020
+ try {
2021
+ this.callWasmZeroArg("worker_check_integrity");
2022
+ const bytes = this.readReturnSlot();
2023
+ const text = new TextDecoder().decode(bytes);
2024
+ const kv = decodeKeyValueText(text);
2025
+ this._integrityOk = kv["ok"] === "true";
2026
+ if (!this._integrityOk) {
2027
+ this._integrityErrors = Object.entries(kv).filter(([k]) => k !== "ok").map(([k, v]) => `${k}=${v}`);
2028
+ console.error(
2029
+ "[kbdb-worker] startup integrity check failed:",
2030
+ this._integrityErrors.join(", ")
2031
+ );
2032
+ }
2033
+ } catch {
2034
+ this._integrityOk = false;
2035
+ this._integrityErrors = ["integrity_check_failed"];
2036
+ console.error(
2037
+ "[kbdb-worker] startup integrity check threw an error"
2038
+ );
2039
+ }
2040
+ }
1706
2041
  // -- WASM bridge primitives --
1707
2042
  getExport(name) {
1708
2043
  if (this._wasmModules === null) {