@claude-flow/memory 3.0.0-alpha.9 → 3.0.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 (184) hide show
  1. package/README.md +308 -37
  2. package/dist/agent-memory-scope.d.ts.map +1 -1
  3. package/dist/agent-memory-scope.js +10 -2
  4. package/dist/agent-memory-scope.js.map +1 -1
  5. package/dist/agentdb-adapter-key-isolation.test.d.ts +2 -0
  6. package/dist/agentdb-adapter-key-isolation.test.d.ts.map +1 -0
  7. package/dist/agentdb-adapter-key-isolation.test.js +175 -0
  8. package/dist/agentdb-adapter-key-isolation.test.js.map +1 -0
  9. package/dist/agentdb-adapter-upsert.test.d.ts +2 -0
  10. package/dist/agentdb-adapter-upsert.test.d.ts.map +1 -0
  11. package/dist/agentdb-adapter-upsert.test.js +216 -0
  12. package/dist/agentdb-adapter-upsert.test.js.map +1 -0
  13. package/dist/agentdb-adapter.d.ts +70 -1
  14. package/dist/agentdb-adapter.d.ts.map +1 -1
  15. package/dist/agentdb-adapter.js +365 -32
  16. package/dist/agentdb-adapter.js.map +1 -1
  17. package/dist/agentdb-backend.d.ts +25 -0
  18. package/dist/agentdb-backend.d.ts.map +1 -1
  19. package/dist/agentdb-backend.js +113 -28
  20. package/dist/agentdb-backend.js.map +1 -1
  21. package/dist/agentdb-retrieval-guard.d.ts +76 -0
  22. package/dist/agentdb-retrieval-guard.d.ts.map +1 -0
  23. package/dist/agentdb-retrieval-guard.js +77 -0
  24. package/dist/agentdb-retrieval-guard.js.map +1 -0
  25. package/dist/agentdb-retrieval-guard.test.d.ts +12 -0
  26. package/dist/agentdb-retrieval-guard.test.d.ts.map +1 -0
  27. package/dist/agentdb-retrieval-guard.test.js +129 -0
  28. package/dist/agentdb-retrieval-guard.test.js.map +1 -0
  29. package/dist/auto-memory-bridge.d.ts +12 -1
  30. package/dist/auto-memory-bridge.d.ts.map +1 -1
  31. package/dist/auto-memory-bridge.js +101 -7
  32. package/dist/auto-memory-bridge.js.map +1 -1
  33. package/dist/auto-memory-bridge.test.js +121 -15
  34. package/dist/auto-memory-bridge.test.js.map +1 -1
  35. package/dist/auto-memory-index-3224.test.d.ts +2 -0
  36. package/dist/auto-memory-index-3224.test.d.ts.map +1 -0
  37. package/dist/auto-memory-index-3224.test.js +77 -0
  38. package/dist/auto-memory-index-3224.test.js.map +1 -0
  39. package/dist/consolidator-embedding-benchmark.test.d.ts +39 -0
  40. package/dist/consolidator-embedding-benchmark.test.d.ts.map +1 -0
  41. package/dist/consolidator-embedding-benchmark.test.js +366 -0
  42. package/dist/consolidator-embedding-benchmark.test.js.map +1 -0
  43. package/dist/consolidator.d.ts +131 -0
  44. package/dist/consolidator.d.ts.map +1 -0
  45. package/dist/consolidator.js +309 -0
  46. package/dist/consolidator.js.map +1 -0
  47. package/dist/consolidator.test.d.ts +17 -0
  48. package/dist/consolidator.test.d.ts.map +1 -0
  49. package/dist/consolidator.test.js +376 -0
  50. package/dist/consolidator.test.js.map +1 -0
  51. package/dist/controller-registry.d.ts +290 -0
  52. package/dist/controller-registry.d.ts.map +1 -0
  53. package/dist/controller-registry.js +1095 -0
  54. package/dist/controller-registry.js.map +1 -0
  55. package/dist/controller-registry.test.d.ts +14 -0
  56. package/dist/controller-registry.test.d.ts.map +1 -0
  57. package/dist/controller-registry.test.js +818 -0
  58. package/dist/controller-registry.test.js.map +1 -0
  59. package/dist/database-provider.d.ts +14 -2
  60. package/dist/database-provider.d.ts.map +1 -1
  61. package/dist/database-provider.js +63 -3
  62. package/dist/database-provider.js.map +1 -1
  63. package/dist/database-provider.test.js +56 -3
  64. package/dist/database-provider.test.js.map +1 -1
  65. package/dist/entity-tagger.d.ts +31 -0
  66. package/dist/entity-tagger.d.ts.map +1 -0
  67. package/dist/entity-tagger.js +80 -0
  68. package/dist/entity-tagger.js.map +1 -0
  69. package/dist/entity-tagger.test.d.ts +12 -0
  70. package/dist/entity-tagger.test.d.ts.map +1 -0
  71. package/dist/entity-tagger.test.js +70 -0
  72. package/dist/entity-tagger.test.js.map +1 -0
  73. package/dist/fts5.test.d.ts +13 -0
  74. package/dist/fts5.test.d.ts.map +1 -0
  75. package/dist/fts5.test.js +144 -0
  76. package/dist/fts5.test.js.map +1 -0
  77. package/dist/graceful-retrieval.test.d.ts +14 -0
  78. package/dist/graceful-retrieval.test.d.ts.map +1 -0
  79. package/dist/graceful-retrieval.test.js +251 -0
  80. package/dist/graceful-retrieval.test.js.map +1 -0
  81. package/dist/hnsw-index.d.ts +62 -0
  82. package/dist/hnsw-index.d.ts.map +1 -1
  83. package/dist/hnsw-index.js +461 -16
  84. package/dist/hnsw-index.js.map +1 -1
  85. package/dist/hnsw-persistence.test.d.ts +13 -0
  86. package/dist/hnsw-persistence.test.d.ts.map +1 -0
  87. package/dist/hnsw-persistence.test.js +208 -0
  88. package/dist/hnsw-persistence.test.js.map +1 -0
  89. package/dist/hnsw-quantization.test.d.ts +17 -0
  90. package/dist/hnsw-quantization.test.d.ts.map +1 -0
  91. package/dist/hnsw-quantization.test.js +140 -0
  92. package/dist/hnsw-quantization.test.js.map +1 -0
  93. package/dist/hybrid-backend.d.ts +46 -2
  94. package/dist/hybrid-backend.d.ts.map +1 -1
  95. package/dist/hybrid-backend.js +120 -21
  96. package/dist/hybrid-backend.js.map +1 -1
  97. package/dist/hybrid-backend.test.js +139 -5
  98. package/dist/hybrid-backend.test.js.map +1 -1
  99. package/dist/index.d.ts +164 -10
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +272 -30
  102. package/dist/index.js.map +1 -1
  103. package/dist/index.test.d.ts +10 -0
  104. package/dist/index.test.d.ts.map +1 -0
  105. package/dist/index.test.js +71 -0
  106. package/dist/index.test.js.map +1 -0
  107. package/dist/json-security.d.ts +12 -0
  108. package/dist/json-security.d.ts.map +1 -0
  109. package/dist/json-security.js +20 -0
  110. package/dist/json-security.js.map +1 -0
  111. package/dist/learning-bridge.d.ts +29 -0
  112. package/dist/learning-bridge.d.ts.map +1 -1
  113. package/dist/learning-bridge.js +71 -6
  114. package/dist/learning-bridge.js.map +1 -1
  115. package/dist/learning-bridge.test.js +181 -1
  116. package/dist/learning-bridge.test.js.map +1 -1
  117. package/dist/memory-key.d.ts +3 -0
  118. package/dist/memory-key.d.ts.map +1 -0
  119. package/dist/memory-key.js +5 -0
  120. package/dist/memory-key.js.map +1 -0
  121. package/dist/migration.d.ts +19 -4
  122. package/dist/migration.d.ts.map +1 -1
  123. package/dist/migration.js +99 -24
  124. package/dist/migration.js.map +1 -1
  125. package/dist/migration.test.d.ts +11 -0
  126. package/dist/migration.test.d.ts.map +1 -0
  127. package/dist/migration.test.js +141 -0
  128. package/dist/migration.test.js.map +1 -0
  129. package/dist/mmr-benchmark.test.d.ts +2 -0
  130. package/dist/mmr-benchmark.test.d.ts.map +1 -0
  131. package/dist/mmr-benchmark.test.js +286 -0
  132. package/dist/mmr-benchmark.test.js.map +1 -0
  133. package/dist/mmr-rerank-perf.test.d.ts +2 -0
  134. package/dist/mmr-rerank-perf.test.d.ts.map +1 -0
  135. package/dist/mmr-rerank-perf.test.js +191 -0
  136. package/dist/mmr-rerank-perf.test.js.map +1 -0
  137. package/dist/persistent-sona.d.ts +144 -0
  138. package/dist/persistent-sona.d.ts.map +1 -0
  139. package/dist/persistent-sona.js +332 -0
  140. package/dist/persistent-sona.js.map +1 -0
  141. package/dist/rvf-backend.d.ts +53 -0
  142. package/dist/rvf-backend.d.ts.map +1 -0
  143. package/dist/rvf-backend.js +643 -0
  144. package/dist/rvf-backend.js.map +1 -0
  145. package/dist/rvf-learning-store.d.ts +139 -0
  146. package/dist/rvf-learning-store.d.ts.map +1 -0
  147. package/dist/rvf-learning-store.js +295 -0
  148. package/dist/rvf-learning-store.js.map +1 -0
  149. package/dist/rvf-migration.d.ts +45 -0
  150. package/dist/rvf-migration.d.ts.map +1 -0
  151. package/dist/rvf-migration.js +254 -0
  152. package/dist/rvf-migration.js.map +1 -0
  153. package/dist/smart-retrieval-scores.test.d.ts +2 -0
  154. package/dist/smart-retrieval-scores.test.d.ts.map +1 -0
  155. package/dist/smart-retrieval-scores.test.js +75 -0
  156. package/dist/smart-retrieval-scores.test.js.map +1 -0
  157. package/dist/smart-retrieval.d.ts +141 -0
  158. package/dist/smart-retrieval.d.ts.map +1 -0
  159. package/dist/smart-retrieval.js +389 -0
  160. package/dist/smart-retrieval.js.map +1 -0
  161. package/dist/smart-retrieval.test.d.ts +2 -0
  162. package/dist/smart-retrieval.test.d.ts.map +1 -0
  163. package/dist/smart-retrieval.test.js +418 -0
  164. package/dist/smart-retrieval.test.js.map +1 -0
  165. package/dist/sqlite-backend.d.ts +10 -0
  166. package/dist/sqlite-backend.d.ts.map +1 -1
  167. package/dist/sqlite-backend.js +101 -5
  168. package/dist/sqlite-backend.js.map +1 -1
  169. package/dist/sqljs-backend.d.ts +9 -0
  170. package/dist/sqljs-backend.d.ts.map +1 -1
  171. package/dist/sqljs-backend.js +91 -3
  172. package/dist/sqljs-backend.js.map +1 -1
  173. package/dist/tiered-memory.d.ts +171 -0
  174. package/dist/tiered-memory.d.ts.map +1 -0
  175. package/dist/tiered-memory.js +350 -0
  176. package/dist/tiered-memory.js.map +1 -0
  177. package/dist/tiered-memory.test.d.ts +9 -0
  178. package/dist/tiered-memory.test.d.ts.map +1 -0
  179. package/dist/tiered-memory.test.js +220 -0
  180. package/dist/tiered-memory.test.js.map +1 -0
  181. package/dist/types.d.ts +17 -0
  182. package/dist/types.d.ts.map +1 -1
  183. package/dist/types.js.map +1 -1
  184. package/package.json +10 -8
@@ -206,8 +206,11 @@ export class HNSWIndex extends EventEmitter {
206
206
  const storedVector = this.quantizer
207
207
  ? this.quantizer.encode(vector)
208
208
  : vector;
209
- // Pre-normalize vector for O(1) cosine similarity
210
- const normalizedVector = this.config.metric === 'cosine'
209
+ // Pre-normalize vector for O(1) cosine similarity.
210
+ // Skipped for product quantization: storedVector holds PQ centroid
211
+ // indices, not embedding coordinates, so normalizing it is meaningless
212
+ // (and productQuantizeDistance() below never consults normalizedVector).
213
+ const normalizedVector = this.config.metric === 'cosine' && !this.isProductQuantized()
211
214
  ? this.normalizeVector(storedVector)
212
215
  : null;
213
216
  // Generate random level for new node
@@ -254,8 +257,9 @@ export class HNSWIndex extends EventEmitter {
254
257
  const queryVector = this.quantizer
255
258
  ? this.quantizer.encode(query)
256
259
  : query;
257
- // Pre-normalize query for O(1) cosine similarity
258
- const normalizedQuery = this.config.metric === 'cosine'
260
+ // Pre-normalize query for O(1) cosine similarity (see addPoint() for why
261
+ // this is skipped under product quantization).
262
+ const normalizedQuery = this.config.metric === 'cosine' && !this.isProductQuantized()
259
263
  ? this.normalizeVector(queryVector)
260
264
  : null;
261
265
  // Start from entry point and search down the layers
@@ -395,6 +399,192 @@ export class HNSWIndex extends EventEmitter {
395
399
  get size() {
396
400
  return this.nodes.size;
397
401
  }
402
+ /**
403
+ * Read-only view of the resolved HNSW config (dimensions, M, etc.).
404
+ * Used by {@link MemoryConsolidator.compactHnsw} to rebuild the index with
405
+ * matching parameters.
406
+ */
407
+ getConfig() {
408
+ return this.config;
409
+ }
410
+ // ===== Persistence (ADR-125 Phase 3) =====
411
+ /**
412
+ * Magic header for serialized HNSW snapshots.
413
+ * Format: "HNSW" + version byte (0x01).
414
+ */
415
+ static SERIALIZATION_MAGIC = Buffer.from([0x48, 0x4e, 0x53, 0x57, 0x01]);
416
+ /**
417
+ * Serialize the index to a binary buffer.
418
+ *
419
+ * Layout (all integers big-endian):
420
+ * - 5 bytes: magic header "HNSW" + version (0x01)
421
+ * - 4 bytes: dimensions (uint32)
422
+ * - 4 bytes: M (uint32)
423
+ * - 4 bytes: efConstruction (uint32)
424
+ * - 4 bytes: metric length (uint32) + metric utf-8 bytes
425
+ * - 4 bytes: maxLevel (uint32)
426
+ * - 4 bytes: entryPoint length (uint32) + entryPoint utf-8 bytes (0 = null)
427
+ * - 4 bytes: node count (uint32)
428
+ * - per node:
429
+ * - 4 bytes id length + id utf-8 bytes
430
+ * - 4 bytes level
431
+ * - 4 bytes vector length (in floats) + vector bytes (Float32, little-endian native)
432
+ * - 1 byte hasNormalized (0/1)
433
+ * - if hasNormalized: 4 bytes normalized length + normalized bytes
434
+ * - 4 bytes connection-level count
435
+ * - per level:
436
+ * - 4 bytes layer index
437
+ * - 4 bytes neighbor count
438
+ * - per neighbor: 4 bytes id length + id utf-8 bytes
439
+ */
440
+ serialize() {
441
+ const chunks = [];
442
+ chunks.push(HNSWIndex.SERIALIZATION_MAGIC);
443
+ // Config header
444
+ const header = Buffer.alloc(16);
445
+ header.writeUInt32BE(this.config.dimensions, 0);
446
+ header.writeUInt32BE(this.config.M, 4);
447
+ header.writeUInt32BE(this.config.efConstruction, 8);
448
+ header.writeUInt32BE(this.maxLevel, 12);
449
+ chunks.push(header);
450
+ chunks.push(this.encodeLengthPrefixedString(this.config.metric));
451
+ chunks.push(this.encodeLengthPrefixedString(this.entryPoint ?? ''));
452
+ const nodeCountBuf = Buffer.alloc(4);
453
+ nodeCountBuf.writeUInt32BE(this.nodes.size, 0);
454
+ chunks.push(nodeCountBuf);
455
+ for (const [id, node] of this.nodes) {
456
+ chunks.push(this.encodeLengthPrefixedString(id));
457
+ const meta = Buffer.alloc(4);
458
+ meta.writeUInt32BE(node.level, 0);
459
+ chunks.push(meta);
460
+ chunks.push(this.encodeFloat32Array(node.vector));
461
+ if (node.normalizedVector) {
462
+ chunks.push(Buffer.from([1]));
463
+ chunks.push(this.encodeFloat32Array(node.normalizedVector));
464
+ }
465
+ else {
466
+ chunks.push(Buffer.from([0]));
467
+ }
468
+ const levels = [...node.connections.entries()];
469
+ const lvlCountBuf = Buffer.alloc(4);
470
+ lvlCountBuf.writeUInt32BE(levels.length, 0);
471
+ chunks.push(lvlCountBuf);
472
+ for (const [layer, neighbors] of levels) {
473
+ const layerBuf = Buffer.alloc(8);
474
+ layerBuf.writeUInt32BE(layer, 0);
475
+ layerBuf.writeUInt32BE(neighbors.size, 4);
476
+ chunks.push(layerBuf);
477
+ for (const neighborId of neighbors) {
478
+ chunks.push(this.encodeLengthPrefixedString(neighborId));
479
+ }
480
+ }
481
+ }
482
+ return Buffer.concat(chunks);
483
+ }
484
+ /**
485
+ * Deserialize an HNSW index from a buffer produced by {@link serialize}.
486
+ *
487
+ * Throws on magic-header mismatch, version mismatch, or truncated input.
488
+ */
489
+ static deserialize(buf) {
490
+ if (buf.length < HNSWIndex.SERIALIZATION_MAGIC.length) {
491
+ throw new Error('HNSWIndex.deserialize: buffer too short for magic header');
492
+ }
493
+ for (let i = 0; i < HNSWIndex.SERIALIZATION_MAGIC.length; i++) {
494
+ if (buf[i] !== HNSWIndex.SERIALIZATION_MAGIC[i]) {
495
+ throw new Error(`HNSWIndex.deserialize: magic header mismatch at byte ${i} (got 0x${buf[i].toString(16)}, expected 0x${HNSWIndex.SERIALIZATION_MAGIC[i].toString(16)})`);
496
+ }
497
+ }
498
+ let offset = HNSWIndex.SERIALIZATION_MAGIC.length;
499
+ const dimensions = buf.readUInt32BE(offset);
500
+ offset += 4;
501
+ const M = buf.readUInt32BE(offset);
502
+ offset += 4;
503
+ const efConstruction = buf.readUInt32BE(offset);
504
+ offset += 4;
505
+ const maxLevel = buf.readUInt32BE(offset);
506
+ offset += 4;
507
+ const metricRead = readLengthPrefixedString(buf, offset);
508
+ offset = metricRead.offset;
509
+ const metric = metricRead.value;
510
+ const entryRead = readLengthPrefixedString(buf, offset);
511
+ offset = entryRead.offset;
512
+ const entryPoint = entryRead.value === '' ? null : entryRead.value;
513
+ const nodeCount = buf.readUInt32BE(offset);
514
+ offset += 4;
515
+ const index = new HNSWIndex({
516
+ dimensions,
517
+ M,
518
+ efConstruction,
519
+ metric,
520
+ });
521
+ index.maxLevel = maxLevel;
522
+ index.entryPoint = entryPoint;
523
+ for (let n = 0; n < nodeCount; n++) {
524
+ const idRead = readLengthPrefixedString(buf, offset);
525
+ offset = idRead.offset;
526
+ const id = idRead.value;
527
+ const level = buf.readUInt32BE(offset);
528
+ offset += 4;
529
+ const vecRead = readFloat32Array(buf, offset);
530
+ offset = vecRead.offset;
531
+ const vector = vecRead.value;
532
+ const hasNormalized = buf[offset];
533
+ offset += 1;
534
+ let normalizedVector = null;
535
+ if (hasNormalized) {
536
+ const normRead = readFloat32Array(buf, offset);
537
+ offset = normRead.offset;
538
+ normalizedVector = normRead.value;
539
+ }
540
+ const lvlCount = buf.readUInt32BE(offset);
541
+ offset += 4;
542
+ const connections = new Map();
543
+ for (let l = 0; l < lvlCount; l++) {
544
+ const layer = buf.readUInt32BE(offset);
545
+ offset += 4;
546
+ const neighborCount = buf.readUInt32BE(offset);
547
+ offset += 4;
548
+ const neighbors = new Set();
549
+ for (let k = 0; k < neighborCount; k++) {
550
+ const neighborRead = readLengthPrefixedString(buf, offset);
551
+ offset = neighborRead.offset;
552
+ neighbors.add(neighborRead.value);
553
+ }
554
+ connections.set(layer, neighbors);
555
+ }
556
+ index.nodes.set(id, {
557
+ id,
558
+ vector,
559
+ normalizedVector,
560
+ connections,
561
+ level,
562
+ });
563
+ }
564
+ return index;
565
+ }
566
+ encodeLengthPrefixedString(s) {
567
+ const strBuf = Buffer.from(s, 'utf-8');
568
+ const out = Buffer.alloc(4 + strBuf.length);
569
+ out.writeUInt32BE(strBuf.length, 0);
570
+ strBuf.copy(out, 4);
571
+ return out;
572
+ }
573
+ encodeFloat32Array(arr) {
574
+ const lenBuf = Buffer.alloc(4);
575
+ lenBuf.writeUInt32BE(arr.length, 0);
576
+ const dataBuf = Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
577
+ return Buffer.concat([lenBuf, dataBuf]);
578
+ }
579
+ /**
580
+ * Remove a point from the index — alias for {@link removePoint}.
581
+ *
582
+ * Added by ADR-125 Phase 4 so {@link MemoryConsolidator} has a stable
583
+ * synchronous-shaped API to call. Internally delegates to {@link removePoint}.
584
+ */
585
+ async remove(id) {
586
+ return this.removePoint(id);
587
+ }
398
588
  // ===== Private Methods =====
399
589
  mergeConfig(config) {
400
590
  return {
@@ -587,7 +777,43 @@ export class HNSWIndex extends EventEmitter {
587
777
  }
588
778
  }
589
779
  }
780
+ /**
781
+ * True when this index uses product quantization with trained codebooks.
782
+ * Stored vectors are then PQ centroid indices, not embedding coordinates —
783
+ * generic metric distances (cosine/euclidean/dot/manhattan) computed
784
+ * directly on those indices are meaningless (index 5 vs index 200 has no
785
+ * relation to embedding-space proximity). Falls back to false during the
786
+ * pre-training bootstrap window (see Quantizer.productQuantize()), where
787
+ * stored vectors are still sub-vector-mean floats and generic distance,
788
+ * while imprecise, is not nonsensical — matching prior behavior exactly.
789
+ */
790
+ isProductQuantized() {
791
+ return (this.quantizer !== null &&
792
+ this.config.quantization?.type === 'product' &&
793
+ this.quantizer.isPQTrained);
794
+ }
590
795
  distance(a, b) {
796
+ // Pre-existing, disclosed limitation (not introduced by this fix): any
797
+ // vector added before PQ codebook training completes is stored under a
798
+ // sub-vector-mean fallback encoding (see Quantizer.productQuantize())
799
+ // and is never re-encoded once training finishes — the original raw
800
+ // vector isn't retained. isValidPQEncoding() is a *plausibility*
801
+ // heuristic, not a proof of provenance: it structurally guards the
802
+ // crash/out-of-bounds case (fallback values outside codebook range, or
803
+ // non-integer), but a fallback encoding whose sub-vector means
804
+ // coincidentally land on small non-negative integers (most concretely,
805
+ // an all-zero or otherwise degenerate vector added pre-training) can
806
+ // still pass the check and be silently mis-dispatched to
807
+ // productQuantizeDistance() as if it were a real centroid assignment.
808
+ // Closing this fully would need per-node encoding-provenance tracking
809
+ // (e.g. a sentinel/flag threaded through HNSWNode and its persistence
810
+ // format), out of scope for this patch — flagged as a known residual
811
+ // risk, not treated as fully solved.
812
+ if (this.isProductQuantized() &&
813
+ this.quantizer.isValidPQEncoding(a) &&
814
+ this.quantizer.isValidPQEncoding(b)) {
815
+ return this.quantizer.productQuantizeDistance(a, b);
816
+ }
591
817
  switch (this.config.metric) {
592
818
  case 'cosine':
593
819
  return this.cosineDistance(a, b);
@@ -688,6 +914,14 @@ export class HNSWIndex extends EventEmitter {
688
914
  class Quantizer {
689
915
  config;
690
916
  dimensions;
917
+ /** Trained PQ codebooks: codebooks[subquantizer][centroid][dimension] */
918
+ codebooks = null;
919
+ /** Accumulated vectors for lazy codebook training */
920
+ trainingVectors = [];
921
+ /** Minimum number of vectors needed before training codebooks */
922
+ pqTrainingThreshold = 256;
923
+ /** Whether PQ codebooks have been trained */
924
+ pqTrained = false;
691
925
  constructor(config, dimensions) {
692
926
  this.config = config;
693
927
  this.dimensions = dimensions;
@@ -760,22 +994,233 @@ class Quantizer {
760
994
  return quantized;
761
995
  }
762
996
  productQuantize(vector) {
763
- // Simplified product quantization
764
- // In production, would use trained codebooks
765
- const subquantizers = this.config.subquantizers || 8;
766
- const subvectorSize = Math.ceil(vector.length / subquantizers);
767
- const quantized = new Float32Array(subquantizers);
768
- for (let i = 0; i < subquantizers; i++) {
769
- let sum = 0;
770
- const start = i * subvectorSize;
997
+ const numSubquantizers = this.config.subquantizers || 8;
998
+ const numCentroids = this.config.codebookSize || 256;
999
+ // Accumulate training vectors until we have enough to train codebooks
1000
+ if (!this.pqTrained) {
1001
+ this.trainingVectors.push(Array.from(vector));
1002
+ if (this.trainingVectors.length >= this.pqTrainingThreshold) {
1003
+ this.codebooks = this.trainProductQuantizer(this.trainingVectors, numSubquantizers, numCentroids);
1004
+ this.pqTrained = true;
1005
+ this.trainingVectors = []; // Free training data
1006
+ }
1007
+ else {
1008
+ // Not enough data to train yet; fall back to sub-vector means
1009
+ const subvectorSize = Math.ceil(vector.length / numSubquantizers);
1010
+ const quantized = new Float32Array(numSubquantizers);
1011
+ for (let i = 0; i < numSubquantizers; i++) {
1012
+ let sum = 0;
1013
+ const start = i * subvectorSize;
1014
+ const end = Math.min(start + subvectorSize, vector.length);
1015
+ for (let j = start; j < end; j++) {
1016
+ sum += vector[j];
1017
+ }
1018
+ quantized[i] = sum / (end - start);
1019
+ }
1020
+ return quantized;
1021
+ }
1022
+ }
1023
+ // Encode: assign each sub-vector to its nearest centroid
1024
+ const subvectorSize = Math.ceil(vector.length / numSubquantizers);
1025
+ const encoded = new Float32Array(numSubquantizers);
1026
+ for (let m = 0; m < numSubquantizers; m++) {
1027
+ const start = m * subvectorSize;
771
1028
  const end = Math.min(start + subvectorSize, vector.length);
772
- for (let j = start; j < end; j++) {
773
- sum += vector[j];
1029
+ const codebook = this.codebooks[m];
1030
+ let bestIdx = 0;
1031
+ let bestDist = Infinity;
1032
+ for (let c = 0; c < codebook.length; c++) {
1033
+ let dist = 0;
1034
+ for (let d = 0; d < end - start; d++) {
1035
+ const diff = vector[start + d] - codebook[c][d];
1036
+ dist += diff * diff;
1037
+ }
1038
+ if (dist < bestDist) {
1039
+ bestDist = dist;
1040
+ bestIdx = c;
1041
+ }
774
1042
  }
775
- quantized[i] = sum / (end - start);
1043
+ // Store centroid index (fits in Uint8 when numCentroids <= 256)
1044
+ encoded[m] = bestIdx;
776
1045
  }
777
- return quantized;
1046
+ return encoded;
1047
+ }
1048
+ /**
1049
+ * Train Product Quantizer codebooks using k-means on sub-vector slices.
1050
+ *
1051
+ * Splits each training vector into `numSubquantizers` sub-vectors, then
1052
+ * learns `numCentroids` centroids per sub-quantizer position.
1053
+ *
1054
+ * @returns codebooks[m][c] = centroid vector for sub-quantizer m, centroid c
1055
+ */
1056
+ trainProductQuantizer(trainingVectors, numSubquantizers, numCentroids) {
1057
+ const dim = trainingVectors[0].length;
1058
+ const subvectorSize = Math.ceil(dim / numSubquantizers);
1059
+ const codebooks = [];
1060
+ for (let m = 0; m < numSubquantizers; m++) {
1061
+ const start = m * subvectorSize;
1062
+ const end = Math.min(start + subvectorSize, dim);
1063
+ const subLen = end - start;
1064
+ // Extract sub-vectors for this position from all training vectors
1065
+ const subVectors = trainingVectors.map((vec) => {
1066
+ const sub = new Array(subLen);
1067
+ for (let d = 0; d < subLen; d++) {
1068
+ sub[d] = vec[start + d];
1069
+ }
1070
+ return sub;
1071
+ });
1072
+ // Clamp centroids to available data points
1073
+ const effectiveK = Math.min(numCentroids, subVectors.length);
1074
+ const centroids = this.kMeans(subVectors, effectiveK, 20);
1075
+ codebooks.push(centroids);
1076
+ }
1077
+ return codebooks;
1078
+ }
1079
+ /**
1080
+ * Compute approximate squared Euclidean distance between two PQ-encoded
1081
+ * vectors using their centroid indices and the shared codebooks.
1082
+ *
1083
+ * @param encoded1 - Centroid indices for vector 1 (length = numSubquantizers).
1084
+ * Typed Float32Array to match productQuantize()'s actual return type —
1085
+ * indices are small integers (0..codebookSize-1) exactly representable
1086
+ * in float32.
1087
+ * @param encoded2 - Centroid indices for vector 2 (length = numSubquantizers)
1088
+ * @param codebooks - Trained codebooks from trainProductQuantizer()
1089
+ * @returns Approximate squared Euclidean distance
1090
+ */
1091
+ productQuantizeDistance(encoded1, encoded2, codebooks) {
1092
+ const cb = codebooks || this.codebooks;
1093
+ if (!cb) {
1094
+ throw new Error('Product quantizer codebooks not trained yet');
1095
+ }
1096
+ let totalDist = 0;
1097
+ for (let m = 0; m < encoded1.length; m++) {
1098
+ const c1 = cb[m][encoded1[m]];
1099
+ const c2 = cb[m][encoded2[m]];
1100
+ for (let d = 0; d < c1.length; d++) {
1101
+ const diff = c1[d] - c2[d];
1102
+ totalDist += diff * diff;
1103
+ }
1104
+ }
1105
+ return totalDist;
1106
+ }
1107
+ /**
1108
+ * K-means clustering.
1109
+ *
1110
+ * Partitions `data` into `k` clusters and returns the centroid vectors.
1111
+ * Initialisation picks the first k data points (deterministic, avoids
1112
+ * the overhead of k-means++ for the small sub-vector slices used in PQ).
1113
+ */
1114
+ kMeans(data, k, maxIter = 20) {
1115
+ const dim = data[0].length;
1116
+ // Initialise centroids from the first k data points
1117
+ const centroids = data.slice(0, k).map((v) => [...v]);
1118
+ for (let iter = 0; iter < maxIter; iter++) {
1119
+ // --- Assign each point to nearest centroid ---
1120
+ const assignments = new Int32Array(data.length);
1121
+ for (let i = 0; i < data.length; i++) {
1122
+ let bestDist = Infinity;
1123
+ let bestIdx = 0;
1124
+ for (let j = 0; j < k; j++) {
1125
+ let dist = 0;
1126
+ for (let d = 0; d < dim; d++) {
1127
+ const diff = data[i][d] - centroids[j][d];
1128
+ dist += diff * diff;
1129
+ }
1130
+ if (dist < bestDist) {
1131
+ bestDist = dist;
1132
+ bestIdx = j;
1133
+ }
1134
+ }
1135
+ assignments[i] = bestIdx;
1136
+ }
1137
+ // --- Update centroids ---
1138
+ const sums = Array.from({ length: k }, () => new Float64Array(dim));
1139
+ const counts = new Int32Array(k);
1140
+ for (let i = 0; i < data.length; i++) {
1141
+ const c = assignments[i];
1142
+ counts[c]++;
1143
+ for (let d = 0; d < dim; d++) {
1144
+ sums[c][d] += data[i][d];
1145
+ }
1146
+ }
1147
+ let converged = true;
1148
+ for (let j = 0; j < k; j++) {
1149
+ if (counts[j] === 0)
1150
+ continue; // Dead centroid, leave unchanged
1151
+ for (let d = 0; d < dim; d++) {
1152
+ const newVal = sums[j][d] / counts[j];
1153
+ if (Math.abs(newVal - centroids[j][d]) > 1e-6)
1154
+ converged = false;
1155
+ centroids[j][d] = newVal;
1156
+ }
1157
+ }
1158
+ if (converged)
1159
+ break;
1160
+ }
1161
+ return centroids;
1162
+ }
1163
+ /**
1164
+ * Whether the product quantizer codebooks are trained and ready for encoding.
1165
+ */
1166
+ get isPQTrained() {
1167
+ return this.pqTrained;
1168
+ }
1169
+ /**
1170
+ * Access trained codebooks (null if not yet trained).
1171
+ */
1172
+ getCodebooks() {
1173
+ return this.codebooks;
1174
+ }
1175
+ /**
1176
+ * True when `encoded` looks like a real trained-codebook PQ encoding
1177
+ * (length matches numSubquantizers, every value is a non-negative integer
1178
+ * in range for its sub-quantizer's codebook). False for the pre-training
1179
+ * sub-vector-mean fallback (see productQuantize()), which cannot be
1180
+ * compared via productQuantizeDistance() without corrupting results or
1181
+ * indexing out of bounds.
1182
+ */
1183
+ isValidPQEncoding(encoded) {
1184
+ if (!this.codebooks || encoded.length !== this.codebooks.length)
1185
+ return false;
1186
+ for (let m = 0; m < encoded.length; m++) {
1187
+ const idx = encoded[m];
1188
+ if (!Number.isInteger(idx) || idx < 0 || idx >= this.codebooks[m].length)
1189
+ return false;
1190
+ }
1191
+ return true;
1192
+ }
1193
+ }
1194
+ // ===== Persistence Helpers (ADR-125 Phase 3) =====
1195
+ function readLengthPrefixedString(buf, offset) {
1196
+ if (offset + 4 > buf.length) {
1197
+ throw new Error(`HNSWIndex.deserialize: truncated string length at offset ${offset}`);
1198
+ }
1199
+ const len = buf.readUInt32BE(offset);
1200
+ const start = offset + 4;
1201
+ const end = start + len;
1202
+ if (end > buf.length) {
1203
+ throw new Error(`HNSWIndex.deserialize: truncated string payload at offset ${offset} (needed ${len} bytes, have ${buf.length - start})`);
1204
+ }
1205
+ const value = buf.toString('utf-8', start, end);
1206
+ return { value, offset: end };
1207
+ }
1208
+ function readFloat32Array(buf, offset) {
1209
+ if (offset + 4 > buf.length) {
1210
+ throw new Error(`HNSWIndex.deserialize: truncated array length at offset ${offset}`);
1211
+ }
1212
+ const floatCount = buf.readUInt32BE(offset);
1213
+ const start = offset + 4;
1214
+ const byteLen = floatCount * 4;
1215
+ const end = start + byteLen;
1216
+ if (end > buf.length) {
1217
+ throw new Error(`HNSWIndex.deserialize: truncated array payload at offset ${offset} (needed ${byteLen} bytes, have ${buf.length - start})`);
778
1218
  }
1219
+ // Copy into a fresh ArrayBuffer so Float32Array isn't a view onto the original
1220
+ // (de-aligned) Node Buffer pool.
1221
+ const copy = new ArrayBuffer(byteLen);
1222
+ new Uint8Array(copy).set(new Uint8Array(buf.buffer, buf.byteOffset + start, byteLen));
1223
+ return { value: new Float32Array(copy), offset: end };
779
1224
  }
780
1225
  export default HNSWIndex;
781
1226
  //# sourceMappingURL=hnsw-index.js.map