@seanhogg/builderforce-memory 2026.10.28 → 2026.10.29

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 (253) hide show
  1. package/dist/ingest/IngestionPipeline.d.ts +1 -8
  2. package/dist/ingest/IngestionPipeline.d.ts.map +1 -1
  3. package/dist/ingest/IngestionPipeline.js +3 -15
  4. package/dist/ingest/IngestionPipeline.js.map +1 -1
  5. package/dist/ingest/docsExtract.d.ts +30 -0
  6. package/dist/ingest/docsExtract.d.ts.map +1 -0
  7. package/dist/ingest/docsExtract.js +221 -0
  8. package/dist/ingest/docsExtract.js.map +1 -0
  9. package/dist/ingest/hash.d.ts +15 -0
  10. package/dist/ingest/hash.d.ts.map +1 -0
  11. package/dist/ingest/hash.js +22 -0
  12. package/dist/ingest/hash.js.map +1 -0
  13. package/dist/ingest/parsers.d.ts.map +1 -1
  14. package/dist/ingest/parsers.js +4 -0
  15. package/dist/ingest/parsers.js.map +1 -1
  16. package/dist/ingest/types.d.ts +7 -0
  17. package/dist/ingest/types.d.ts.map +1 -1
  18. package/dist/ingest/types.js.map +1 -1
  19. package/dist/knowledge/build.d.ts +24 -0
  20. package/dist/knowledge/build.d.ts.map +1 -0
  21. package/dist/knowledge/build.js +263 -0
  22. package/dist/knowledge/build.js.map +1 -0
  23. package/dist/knowledge/cards.d.ts +17 -0
  24. package/dist/knowledge/cards.d.ts.map +1 -0
  25. package/dist/knowledge/cards.js +118 -0
  26. package/dist/knowledge/cards.js.map +1 -0
  27. package/dist/knowledge/findings.d.ts +24 -0
  28. package/dist/knowledge/findings.d.ts.map +1 -0
  29. package/dist/knowledge/findings.js +152 -0
  30. package/dist/knowledge/findings.js.map +1 -0
  31. package/dist/knowledge/importance.d.ts +21 -0
  32. package/dist/knowledge/importance.d.ts.map +1 -0
  33. package/dist/knowledge/importance.js +70 -0
  34. package/dist/knowledge/importance.js.map +1 -0
  35. package/dist/knowledge/index.d.ts +31 -0
  36. package/dist/knowledge/index.d.ts.map +1 -0
  37. package/dist/knowledge/index.js +26 -0
  38. package/dist/knowledge/index.js.map +1 -0
  39. package/dist/knowledge/judge.d.ts +86 -0
  40. package/dist/knowledge/judge.d.ts.map +1 -0
  41. package/dist/knowledge/judge.js +213 -0
  42. package/dist/knowledge/judge.js.map +1 -0
  43. package/dist/knowledge/model.d.ts +333 -0
  44. package/dist/knowledge/model.d.ts.map +1 -0
  45. package/dist/knowledge/model.js +13 -0
  46. package/dist/knowledge/model.js.map +1 -0
  47. package/dist/knowledge/packs/docs.d.ts +17 -0
  48. package/dist/knowledge/packs/docs.d.ts.map +1 -0
  49. package/dist/knowledge/packs/docs.js +89 -0
  50. package/dist/knowledge/packs/docs.js.map +1 -0
  51. package/dist/knowledge/partitions.d.ts +31 -0
  52. package/dist/knowledge/partitions.d.ts.map +1 -0
  53. package/dist/knowledge/partitions.js +135 -0
  54. package/dist/knowledge/partitions.js.map +1 -0
  55. package/dist/knowledge/plan.d.ts +33 -0
  56. package/dist/knowledge/plan.d.ts.map +1 -0
  57. package/dist/knowledge/plan.js +89 -0
  58. package/dist/knowledge/plan.js.map +1 -0
  59. package/dist/knowledge/refs.d.ts +40 -0
  60. package/dist/knowledge/refs.d.ts.map +1 -0
  61. package/dist/knowledge/refs.js +102 -0
  62. package/dist/knowledge/refs.js.map +1 -0
  63. package/dist/knowledge/resolve.d.ts +35 -0
  64. package/dist/knowledge/resolve.d.ts.map +1 -0
  65. package/dist/knowledge/resolve.js +67 -0
  66. package/dist/knowledge/resolve.js.map +1 -0
  67. package/dist/knowledge/review.d.ts +23 -0
  68. package/dist/knowledge/review.d.ts.map +1 -0
  69. package/dist/knowledge/review.js +233 -0
  70. package/dist/knowledge/review.js.map +1 -0
  71. package/dist/knowledge/sources.d.ts +47 -0
  72. package/dist/knowledge/sources.d.ts.map +1 -0
  73. package/dist/knowledge/sources.js +136 -0
  74. package/dist/knowledge/sources.js.map +1 -0
  75. package/dist/knowledge/stores/localSnapshotStore.d.ts +50 -0
  76. package/dist/knowledge/stores/localSnapshotStore.d.ts.map +1 -0
  77. package/dist/knowledge/stores/localSnapshotStore.js +125 -0
  78. package/dist/knowledge/stores/localSnapshotStore.js.map +1 -0
  79. package/dist/knowledge/trace.d.ts +32 -0
  80. package/dist/knowledge/trace.d.ts.map +1 -0
  81. package/dist/knowledge/trace.js +102 -0
  82. package/dist/knowledge/trace.js.map +1 -0
  83. package/dist/knowledge/truncate.d.ts +24 -0
  84. package/dist/knowledge/truncate.d.ts.map +1 -0
  85. package/dist/knowledge/truncate.js +128 -0
  86. package/dist/knowledge/truncate.js.map +1 -0
  87. package/dist/memory/SemanticRecall.d.ts.map +1 -1
  88. package/dist/memory/SemanticRecall.js +3 -1
  89. package/dist/memory/SemanticRecall.js.map +1 -1
  90. package/dist/retrieval/HybridRetriever.d.ts.map +1 -1
  91. package/dist/retrieval/HybridRetriever.js +3 -1
  92. package/dist/retrieval/HybridRetriever.js.map +1 -1
  93. package/dist/retrieval/bm25.d.ts +3 -2
  94. package/dist/retrieval/bm25.d.ts.map +1 -1
  95. package/dist/retrieval/bm25.js +5 -3
  96. package/dist/retrieval/bm25.js.map +1 -1
  97. package/dist/retrieval/fusion.d.ts +2 -1
  98. package/dist/retrieval/fusion.d.ts.map +1 -1
  99. package/dist/retrieval/fusion.js +4 -2
  100. package/dist/retrieval/fusion.js.map +1 -1
  101. package/dist/retrieval/hnsw.d.ts +49 -11
  102. package/dist/retrieval/hnsw.d.ts.map +1 -1
  103. package/dist/retrieval/hnsw.js +57 -29
  104. package/dist/retrieval/hnsw.js.map +1 -1
  105. package/dist/retrieval/index.d.ts +7 -3
  106. package/dist/retrieval/index.d.ts.map +1 -1
  107. package/dist/retrieval/index.js +5 -2
  108. package/dist/retrieval/index.js.map +1 -1
  109. package/dist/retrieval/knowledge-index/bytes.d.ts +43 -0
  110. package/dist/retrieval/knowledge-index/bytes.d.ts.map +1 -0
  111. package/dist/retrieval/knowledge-index/bytes.js +151 -0
  112. package/dist/retrieval/knowledge-index/bytes.js.map +1 -0
  113. package/dist/retrieval/knowledge-index/cascade.d.ts +37 -0
  114. package/dist/retrieval/knowledge-index/cascade.d.ts.map +1 -0
  115. package/dist/retrieval/knowledge-index/cascade.js +111 -0
  116. package/dist/retrieval/knowledge-index/cascade.js.map +1 -0
  117. package/dist/retrieval/knowledge-index/compact.d.ts +18 -0
  118. package/dist/retrieval/knowledge-index/compact.d.ts.map +1 -0
  119. package/dist/retrieval/knowledge-index/compact.js +64 -0
  120. package/dist/retrieval/knowledge-index/compact.js.map +1 -0
  121. package/dist/retrieval/knowledge-index/csr.d.ts +41 -0
  122. package/dist/retrieval/knowledge-index/csr.d.ts.map +1 -0
  123. package/dist/retrieval/knowledge-index/csr.js +76 -0
  124. package/dist/retrieval/knowledge-index/csr.js.map +1 -0
  125. package/dist/retrieval/knowledge-index/digest.d.ts +21 -0
  126. package/dist/retrieval/knowledge-index/digest.d.ts.map +1 -0
  127. package/dist/retrieval/knowledge-index/digest.js +50 -0
  128. package/dist/retrieval/knowledge-index/digest.js.map +1 -0
  129. package/dist/retrieval/knowledge-index/embeddings.d.ts +32 -0
  130. package/dist/retrieval/knowledge-index/embeddings.d.ts.map +1 -0
  131. package/dist/retrieval/knowledge-index/embeddings.js +78 -0
  132. package/dist/retrieval/knowledge-index/embeddings.js.map +1 -0
  133. package/dist/retrieval/knowledge-index/expand.d.ts +19 -0
  134. package/dist/retrieval/knowledge-index/expand.d.ts.map +1 -0
  135. package/dist/retrieval/knowledge-index/expand.js +49 -0
  136. package/dist/retrieval/knowledge-index/expand.js.map +1 -0
  137. package/dist/retrieval/knowledge-index/index.d.ts +40 -0
  138. package/dist/retrieval/knowledge-index/index.d.ts.map +1 -0
  139. package/dist/retrieval/knowledge-index/index.js +134 -0
  140. package/dist/retrieval/knowledge-index/index.js.map +1 -0
  141. package/dist/retrieval/knowledge-index/lexical.d.ts +26 -0
  142. package/dist/retrieval/knowledge-index/lexical.d.ts.map +1 -0
  143. package/dist/retrieval/knowledge-index/lexical.js +76 -0
  144. package/dist/retrieval/knowledge-index/lexical.js.map +1 -0
  145. package/dist/retrieval/knowledge-index/pairs.d.ts +22 -0
  146. package/dist/retrieval/knowledge-index/pairs.d.ts.map +1 -0
  147. package/dist/retrieval/knowledge-index/pairs.js +51 -0
  148. package/dist/retrieval/knowledge-index/pairs.js.map +1 -0
  149. package/dist/retrieval/knowledge-index/prune.d.ts +21 -0
  150. package/dist/retrieval/knowledge-index/prune.d.ts.map +1 -0
  151. package/dist/retrieval/knowledge-index/prune.js +43 -0
  152. package/dist/retrieval/knowledge-index/prune.js.map +1 -0
  153. package/dist/retrieval/knowledge-index/quantize.d.ts +37 -0
  154. package/dist/retrieval/knowledge-index/quantize.d.ts.map +1 -0
  155. package/dist/retrieval/knowledge-index/quantize.js +77 -0
  156. package/dist/retrieval/knowledge-index/quantize.js.map +1 -0
  157. package/dist/retrieval/knowledge-index/serialize.d.ts +18 -0
  158. package/dist/retrieval/knowledge-index/serialize.d.ts.map +1 -0
  159. package/dist/retrieval/knowledge-index/serialize.js +123 -0
  160. package/dist/retrieval/knowledge-index/serialize.js.map +1 -0
  161. package/dist/retrieval/knowledge-index/sha256.d.ts +30 -0
  162. package/dist/retrieval/knowledge-index/sha256.d.ts.map +1 -0
  163. package/dist/retrieval/knowledge-index/sha256.js +130 -0
  164. package/dist/retrieval/knowledge-index/sha256.js.map +1 -0
  165. package/dist/retrieval/knowledge-index/state.d.ts +55 -0
  166. package/dist/retrieval/knowledge-index/state.d.ts.map +1 -0
  167. package/dist/retrieval/knowledge-index/state.js +67 -0
  168. package/dist/retrieval/knowledge-index/state.js.map +1 -0
  169. package/dist/retrieval/knowledge-index/tokenize.d.ts +20 -0
  170. package/dist/retrieval/knowledge-index/tokenize.d.ts.map +1 -0
  171. package/dist/retrieval/knowledge-index/tokenize.js +42 -0
  172. package/dist/retrieval/knowledge-index/tokenize.js.map +1 -0
  173. package/dist/retrieval/knowledge-index/traverse.d.ts +26 -0
  174. package/dist/retrieval/knowledge-index/traverse.d.ts.map +1 -0
  175. package/dist/retrieval/knowledge-index/traverse.js +90 -0
  176. package/dist/retrieval/knowledge-index/traverse.js.map +1 -0
  177. package/dist/retrieval/knowledge-index/types.d.ts +85 -0
  178. package/dist/retrieval/knowledge-index/types.d.ts.map +1 -0
  179. package/dist/retrieval/knowledge-index/types.js +9 -0
  180. package/dist/retrieval/knowledge-index/types.js.map +1 -0
  181. package/dist/retrieval/mmr.d.ts +11 -4
  182. package/dist/retrieval/mmr.d.ts.map +1 -1
  183. package/dist/retrieval/mmr.js +28 -8
  184. package/dist/retrieval/mmr.js.map +1 -1
  185. package/dist/retrieval/order.d.ts +41 -0
  186. package/dist/retrieval/order.d.ts.map +1 -0
  187. package/dist/retrieval/order.js +54 -0
  188. package/dist/retrieval/order.js.map +1 -0
  189. package/dist/retrieval/weightedFusion.d.ts +2 -1
  190. package/dist/retrieval/weightedFusion.d.ts.map +1 -1
  191. package/dist/retrieval/weightedFusion.js +7 -5
  192. package/dist/retrieval/weightedFusion.js.map +1 -1
  193. package/dist/vectorstore/MemoryVectorStore.d.ts.map +1 -1
  194. package/dist/vectorstore/MemoryVectorStore.js +2 -1
  195. package/dist/vectorstore/MemoryVectorStore.js.map +1 -1
  196. package/package.json +8 -3
  197. package/src/ingest/IngestionPipeline.ts +3 -15
  198. package/src/ingest/docsExtract.ts +219 -0
  199. package/src/ingest/hash.ts +22 -0
  200. package/src/ingest/parsers.ts +4 -0
  201. package/src/ingest/types.ts +7 -0
  202. package/src/knowledge/build.test.ts +150 -0
  203. package/src/knowledge/build.ts +266 -0
  204. package/src/knowledge/cards.ts +130 -0
  205. package/src/knowledge/docs.test.ts +118 -0
  206. package/src/knowledge/findings.ts +159 -0
  207. package/src/knowledge/importance.ts +72 -0
  208. package/src/knowledge/index.ts +53 -0
  209. package/src/knowledge/judge.ts +231 -0
  210. package/src/knowledge/model.ts +312 -0
  211. package/src/knowledge/packs/docs.ts +91 -0
  212. package/src/knowledge/partitions.test.ts +98 -0
  213. package/src/knowledge/partitions.ts +119 -0
  214. package/src/knowledge/plan.ts +115 -0
  215. package/src/knowledge/refs.ts +108 -0
  216. package/src/knowledge/resolve.ts +78 -0
  217. package/src/knowledge/review.test.ts +180 -0
  218. package/src/knowledge/review.ts +235 -0
  219. package/src/knowledge/sources.ts +146 -0
  220. package/src/knowledge/stores/localSnapshotStore.test.ts +82 -0
  221. package/src/knowledge/stores/localSnapshotStore.ts +159 -0
  222. package/src/knowledge/testkit.test-util.ts +92 -0
  223. package/src/knowledge/trace.test.ts +91 -0
  224. package/src/knowledge/trace.ts +111 -0
  225. package/src/knowledge/truncate.ts +118 -0
  226. package/src/memory/SemanticRecall.ts +3 -1
  227. package/src/retrieval/HybridRetriever.ts +3 -1
  228. package/src/retrieval/bm25.ts +5 -3
  229. package/src/retrieval/fusion.ts +5 -2
  230. package/src/retrieval/hnsw.ts +82 -35
  231. package/src/retrieval/index.ts +17 -3
  232. package/src/retrieval/knowledge-index/bytes.ts +166 -0
  233. package/src/retrieval/knowledge-index/cascade.ts +125 -0
  234. package/src/retrieval/knowledge-index/compact.ts +68 -0
  235. package/src/retrieval/knowledge-index/csr.ts +102 -0
  236. package/src/retrieval/knowledge-index/digest.ts +54 -0
  237. package/src/retrieval/knowledge-index/embeddings.ts +85 -0
  238. package/src/retrieval/knowledge-index/expand.ts +55 -0
  239. package/src/retrieval/knowledge-index/index.ts +162 -0
  240. package/src/retrieval/knowledge-index/lexical.ts +78 -0
  241. package/src/retrieval/knowledge-index/pairs.ts +57 -0
  242. package/src/retrieval/knowledge-index/prune.ts +48 -0
  243. package/src/retrieval/knowledge-index/quantize.ts +78 -0
  244. package/src/retrieval/knowledge-index/serialize.ts +124 -0
  245. package/src/retrieval/knowledge-index/sha256.ts +122 -0
  246. package/src/retrieval/knowledge-index/state.ts +108 -0
  247. package/src/retrieval/knowledge-index/tokenize.ts +41 -0
  248. package/src/retrieval/knowledge-index/traverse.ts +95 -0
  249. package/src/retrieval/knowledge-index/types.ts +78 -0
  250. package/src/retrieval/mmr.ts +35 -10
  251. package/src/retrieval/order.ts +68 -0
  252. package/src/retrieval/weightedFusion.ts +8 -5
  253. package/src/vectorstore/MemoryVectorStore.ts +2 -1
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import { cosineSimilarity, jaccardSimilarity, tokenize } from '../similarity/index.js';
21
+ import { compareHits, compareIds, quantizeScore, DENSE_SCORE_QUANTUM } from './order.js';
21
22
 
22
23
  export interface MmrSelectOptions<T> {
23
24
  /** Relevance of one candidate, on the same scale as {@link similarity}. */
@@ -29,19 +30,44 @@ export interface MmrSelectOptions<T> {
29
30
  /** Stop after this many picks. Default: every candidate. */
30
31
  topK?: number;
31
32
  /**
32
- * Breaks an exact MMR tie in favour of the higher value. Without it the earlier
33
- * candidate wins, so input order decides.
33
+ * Breaks an MMR tie in favour of the higher value. Ties are judged on the MMR
34
+ * score quantized to {@link quantum}.
34
35
  */
35
36
  tieScore?: (item: T) => number;
37
+ /**
38
+ * The candidate's id — the last tie-break, ascending. Without it the explicit
39
+ * id is the candidate's index in `items`, so the earlier candidate wins.
40
+ */
41
+ idOf?: (item: T) => string;
42
+ /** Quantum the MMR score is compared at. Default 1e-4 (cosine noise). */
43
+ quantum?: number;
36
44
  }
37
45
 
38
46
  /** Greedy MMR selection. Returns the picked candidates in pick order. */
39
47
  export function mmrSelect<T>(items: readonly T[], opts: MmrSelectOptions<T>): T[] {
40
48
  const lambda = opts.lambda ?? 0.7;
49
+ const quantum = opts.quantum ?? DENSE_SCORE_QUANTUM;
41
50
  const limit = Math.min(opts.topK ?? items.length, items.length);
42
51
  const remaining = [...items];
43
52
  const relevance = new Map<T, number>();
44
- for (const item of remaining) relevance.set(item, opts.relevance(item));
53
+ const position = new Map<T, number>();
54
+ items.forEach((item, i) => {
55
+ relevance.set(item, opts.relevance(item));
56
+ if (!position.has(item)) position.set(item, i);
57
+ });
58
+ const idOf = (item: T): string | number => (opts.idOf ? opts.idOf(item) : position.get(item)!);
59
+ // Higher quantized MMR score, then higher tieScore, then lower id.
60
+ const beats = (a: T, aScore: number, b: T, bScore: number): boolean => {
61
+ const qa = quantizeScore(aScore, quantum);
62
+ const qb = quantizeScore(bScore, quantum);
63
+ if (qa !== qb) return qa > qb;
64
+ if (opts.tieScore) {
65
+ const ta = opts.tieScore(a);
66
+ const tb = opts.tieScore(b);
67
+ if (ta !== tb) return ta > tb;
68
+ }
69
+ return compareIds(idOf(a), idOf(b)) < 0;
70
+ };
45
71
 
46
72
  const selected: T[] = [];
47
73
  while (selected.length < limit && remaining.length > 0) {
@@ -55,10 +81,7 @@ export function mmrSelect<T>(items: readonly T[], opts: MmrSelectOptions<T>): T[
55
81
  if (sim > maxSimToSelected) maxSimToSelected = sim;
56
82
  }
57
83
  const score = lambda * relevance.get(candidate)! - (1 - lambda) * maxSimToSelected;
58
- const wins = score > bestScore
59
- || (score === bestScore && bestIdx >= 0 && opts.tieScore !== undefined
60
- && opts.tieScore(candidate) > opts.tieScore(remaining[bestIdx]!));
61
- if (wins) { bestScore = score; bestIdx = i; }
84
+ if (bestIdx < 0 || beats(candidate, score, remaining[bestIdx]!, bestScore)) { bestScore = score; bestIdx = i; }
62
85
  }
63
86
  if (bestIdx < 0) break;
64
87
  selected.push(remaining.splice(bestIdx, 1)[0]!);
@@ -87,6 +110,7 @@ export function maximalMarginalRelevance(
87
110
  similarity: (a, b) => cosineSimilarity(a.vector, b.vector),
88
111
  lambda,
89
112
  topK,
113
+ idOf: (c) => c.id,
90
114
  }).map((c) => c.id);
91
115
  }
92
116
 
@@ -101,13 +125,13 @@ export interface TextMmrItem {
101
125
  /**
102
126
  * Text MMR. Reorders every item, judging diversity by Jaccard similarity of word
103
127
  * tokens, and relevance by the item's `score` min-max normalised onto [0,1] (all
104
- * equal → 1). An exact tie goes to the higher raw score. `lambda` is clamped to
105
- * [0,1]; at 1 this is a plain score sort.
128
+ * equal → 1). A tie goes to the higher raw score, then the lower id. `lambda` is
129
+ * clamped to [0,1]; at 1 this is a plain {@link compareHits} sort.
106
130
  */
107
131
  export function textMaximalMarginalRelevance<T extends TextMmrItem>(items: readonly T[], lambda = 0.7): T[] {
108
132
  if (items.length <= 1) return [...items];
109
133
  const l = Math.max(0, Math.min(1, lambda));
110
- if (l === 1) return [...items].sort((a, b) => b.score - a.score);
134
+ if (l === 1) return [...items].sort(compareHits);
111
135
 
112
136
  const tokens = new Map<string, Set<string>>();
113
137
  for (const item of items) tokens.set(item.id, new Set(tokenize(item.content)));
@@ -122,5 +146,6 @@ export function textMaximalMarginalRelevance<T extends TextMmrItem>(items: reado
122
146
  similarity: (a, b) => jaccardSimilarity(tokensOf(a), tokensOf(b)),
123
147
  lambda: l,
124
148
  tieScore: (item) => item.score,
149
+ idOf: (item) => item.id,
125
150
  });
126
151
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * order.ts — the ONE total order every ranker in this package returns.
3
+ *
4
+ * Score descending, then id ascending. Scores are quantized before they are
5
+ * compared, so float noise in the last bits (a CPU vs a WebGPU embedding, a
6
+ * different summation order) cannot flip a near-tie: two scores in the same
7
+ * quantum ARE tied, and the id decides. Without this, ties fell back to input
8
+ * order, and input order is not part of any index's state.
9
+ *
10
+ * Quanta: dense (cosine) scores to 1e-4, BM25 / RRF and other exact-arithmetic
11
+ * scores to 1e-6. Ids compare by UTF-16 code unit (`<`), never `localeCompare`,
12
+ * which depends on the host's ICU data.
13
+ *
14
+ * Pure and zero-dependency.
15
+ */
16
+
17
+ /** Quantum for cosine / dot-product scores — absorbs cross-platform float noise. */
18
+ export const DENSE_SCORE_QUANTUM = 1e-4;
19
+ /** Quantum for BM25, RRF and other scores computed in exact-enough arithmetic. */
20
+ export const LEXICAL_SCORE_QUANTUM = 1e-6;
21
+
22
+ /** The quantized bucket of `score`. NaN sorts last; ±Infinity keep their place. */
23
+ export function quantizeScore(score: number, quantum: number = LEXICAL_SCORE_QUANTUM): number {
24
+ if (Number.isNaN(score)) return -Infinity;
25
+ if (!Number.isFinite(score)) return score;
26
+ return Math.round(score / quantum);
27
+ }
28
+
29
+ /** Ascending id order by code unit. Numeric ids (a doc index) compare numerically. */
30
+ export function compareIds(a: string | number, b: string | number): number {
31
+ return a < b ? -1 : a > b ? 1 : 0;
32
+ }
33
+
34
+ /**
35
+ * The primitive behind {@link compareHits}, for callers whose items keep score and
36
+ * id apart (a node index → id lookup inside a graph walk) — no object per compare.
37
+ */
38
+ export function compareScoreThenId(
39
+ scoreA: number,
40
+ idA: string | number,
41
+ scoreB: number,
42
+ idB: string | number,
43
+ quantum: number = LEXICAL_SCORE_QUANTUM,
44
+ ): number {
45
+ const qa = quantizeScore(scoreA, quantum);
46
+ const qb = quantizeScore(scoreB, quantum);
47
+ if (qa !== qb) return qb > qa ? 1 : -1;
48
+ return compareIds(idA, idB);
49
+ }
50
+
51
+ /** A ranked item: anything with an id and a score. */
52
+ export interface ScoredId {
53
+ id: string | number;
54
+ score: number;
55
+ }
56
+
57
+ /**
58
+ * Score descending (quantized), then id ascending. Usable directly as an
59
+ * `Array.prototype.sort` comparator (the quantum then defaults to 1e-6).
60
+ */
61
+ export function compareHits(a: ScoredId, b: ScoredId, quantum: number = LEXICAL_SCORE_QUANTUM): number {
62
+ return compareScoreThenId(a.score, a.id, b.score, b.id, quantum);
63
+ }
64
+
65
+ /** {@link compareHits} at the dense quantum (1e-4) — for cosine-scored lists. */
66
+ export function compareDenseHits(a: ScoredId, b: ScoredId): number {
67
+ return compareScoreThenId(a.score, a.id, b.score, b.id, DENSE_SCORE_QUANTUM);
68
+ }
@@ -14,6 +14,8 @@
14
14
  * Pure and zero-dependency.
15
15
  */
16
16
 
17
+ import { compareScoreThenId, DENSE_SCORE_QUANTUM } from './order.js';
18
+
17
19
  /** Weight on the semantic arm when a caller states none. */
18
20
  export const DEFAULT_HYBRID_VECTOR_WEIGHT = 0.7;
19
21
  /** Weight on the lexical arm when a caller states none. */
@@ -70,7 +72,8 @@ export function bm25RankToScore(rank: number): number {
70
72
 
71
73
  /**
72
74
  * Fuse two arms keyed by a caller-supplied id and return the candidates ordered
73
- * best-first. `merge` builds the output row from the winning payload of each id;
75
+ * best-first: fused score (quantized at the dense quantum — the vector arm carries
76
+ * float noise) descending, then id ascending. `merge` builds the output row from the winning payload of each id;
74
77
  * later duplicates of the same id keep the first payload seen (arm order decides).
75
78
  */
76
79
  export function fuseHybridArms<TVector extends { id: string }, TText extends { id: string }, TOut>(params: {
@@ -91,11 +94,11 @@ export function fuseHybridArms<TVector extends { id: string }, TText extends { i
91
94
  if (existing) existing.scores.textScore = params.textScoreOf(row);
92
95
  else byId.set(row.id, { row, scores: { vectorScore: 0, textScore: params.textScoreOf(row) } });
93
96
  }
94
- return [...byId.values()]
95
- .map((entry) => {
97
+ return [...byId.entries()]
98
+ .map(([id, entry]) => {
96
99
  const score = hybridScore(entry.scores, weights);
97
- return { out: params.merge(entry.row, score), score };
100
+ return { id, out: params.merge(entry.row, score), score };
98
101
  })
99
- .sort((a, b) => b.score - a.score)
102
+ .sort((a, b) => compareScoreThenId(a.score, a.id, b.score, b.id, DENSE_SCORE_QUANTUM))
100
103
  .map((entry) => entry.out);
101
104
  }
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { bm25Search } from '../retrieval/bm25.js';
22
22
  import { HnswIndex, type HnswOptions } from '../retrieval/hnsw.js';
23
+ import { compareScoreThenId, DENSE_SCORE_QUANTUM } from '../retrieval/order.js';
23
24
  import { cosineSimilarity } from '../similarity/index.js';
24
25
  import { combineFilters, matchesFilter } from './filter.js';
25
26
  import type {
@@ -192,7 +193,7 @@ export class MemoryVectorStore implements VectorStore {
192
193
  const exactScan = () => embedded
193
194
  .filter((r) => matchesFilter(r.metadata, filter))
194
195
  .map((r) => ({ record: r, score: cosineSimilarity(vector, r.vector as Float32Array) }))
195
- .sort((a, b) => b.score - a.score);
196
+ .sort((a, b) => compareScoreThenId(a.score, a.record.id, b.score, b.record.id, DENSE_SCORE_QUANTUM));
196
197
 
197
198
  if (embedded.length < this._annThreshold) return exactScan();
198
199