@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
@@ -0,0 +1,102 @@
1
+ /**
2
+ * csr.ts — the knowledge index's graph, stored as CSR (compressed sparse rows).
3
+ *
4
+ * The graph is built by the package's HNSW over int8 codes (canonical, id-ordered
5
+ * inserts, hash-derived levels), its base layer is pruned (`prune.ts`), and every
6
+ * layer is flattened into a `Uint32Array` of offsets plus a `Uint32Array` of
7
+ * neighbours: no per-node arrays, a fixed byte layout to serialize and hash.
8
+ *
9
+ * Layer 0 holds every node and is indexed by node directly. An upper layer holds
10
+ * only the nodes that reach it, listed ascending in `nodes`.
11
+ */
12
+
13
+ import { HnswIndex } from '../hnsw.js';
14
+ import type { IndexParams } from './types.js';
15
+ import { pruneBaseLayer } from './prune.js';
16
+ import { cosineInt8 } from './quantize.js';
17
+
18
+ export interface CsrLayer {
19
+ /** Upper layers: the node indices present, ascending. Absent on layer 0 (every node). */
20
+ nodes?: Uint32Array;
21
+ /** Row `r`'s neighbours are `neighbours[offsets[r] .. offsets[r + 1])`. */
22
+ offsets: Uint32Array;
23
+ neighbours: Uint32Array;
24
+ }
25
+
26
+ export interface CsrGraph {
27
+ /** Entry node for search; -1 for an empty graph. */
28
+ entry: number;
29
+ /** layers[0] is the base layer; layers.length - 1 is the top layer. */
30
+ layers: CsrLayer[];
31
+ }
32
+
33
+ export const EMPTY_GRAPH: CsrGraph = Object.freeze({ entry: -1, layers: [] }) as CsrGraph;
34
+
35
+ /** Flattens per-row neighbour lists into one CSR layer. */
36
+ export function toCsrLayer(rows: ReadonlyArray<readonly number[]>, nodes?: Uint32Array): CsrLayer {
37
+ const offsets = new Uint32Array(rows.length + 1);
38
+ let total = 0;
39
+ rows.forEach((row, r) => {
40
+ total += row.length;
41
+ offsets[r + 1] = total;
42
+ });
43
+ const neighbours = new Uint32Array(total);
44
+ rows.forEach((row, r) => neighbours.set(row, offsets[r]!));
45
+ return nodes ? { nodes, offsets, neighbours } : { offsets, neighbours };
46
+ }
47
+
48
+ /** The row of `node` in `layer`, or -1 when the node is not on it. */
49
+ function rowOf(layer: CsrLayer, node: number): number {
50
+ if (!layer.nodes) return node < layer.offsets.length - 1 ? node : -1;
51
+ let lo = 0;
52
+ let hi = layer.nodes.length - 1;
53
+ while (lo <= hi) {
54
+ const mid = (lo + hi) >>> 1;
55
+ const v = layer.nodes[mid]!;
56
+ if (v === node) return mid;
57
+ if (v < node) lo = mid + 1;
58
+ else hi = mid - 1;
59
+ }
60
+ return -1;
61
+ }
62
+
63
+ /** Neighbours of `node` on `layer` (empty when the node is not on it). */
64
+ export function neighboursOf(layer: CsrLayer, node: number): Uint32Array {
65
+ const r = rowOf(layer, node);
66
+ if (r < 0) return layer.neighbours.subarray(0, 0);
67
+ return layer.neighbours.subarray(layer.offsets[r]!, layer.offsets[r + 1]!);
68
+ }
69
+
70
+ /**
71
+ * Builds the pruned CSR graph over `codes`, whose order is the id order of `ids`
72
+ * (already sorted and unique). Distances are cosine over the int8 codes, which is
73
+ * exact integer arithmetic plus correctly-rounded sqrt/division — so the same codes
74
+ * give the same graph on every platform.
75
+ */
76
+ export function buildGraph(
77
+ ids: readonly string[],
78
+ codes: readonly Int8Array[],
79
+ params: IndexParams,
80
+ ): { graph: CsrGraph; hubs: Uint8Array } {
81
+ if (ids.length === 0) return { graph: EMPTY_GRAPH, hubs: new Uint8Array(0) };
82
+ const hnsw = HnswIndex.canonicalBuild(
83
+ ids.map((id, i) => ({ id, vector: codes[i]! })),
84
+ { M: params.m, efConstruction: params.efConstruction, efSearch: params.efSearch },
85
+ );
86
+ const view = hnsw.graph();
87
+ const { rows, hubs } = pruneBaseLayer(
88
+ view.neighbours.map((layers) => layers[0] ?? []),
89
+ params.m,
90
+ params.hubShare,
91
+ (a, b) => cosineInt8(codes[a]!, codes[b]!),
92
+ );
93
+ const layers: CsrLayer[] = [toCsrLayer(rows)];
94
+ for (let l = 1; l <= view.maxLayer; l++) {
95
+ const present: number[] = [];
96
+ view.neighbours.forEach((layersOfNode, node) => {
97
+ if (layersOfNode.length > l) present.push(node);
98
+ });
99
+ layers.push(toCsrLayer(present.map((node) => view.neighbours[node]![l]!), Uint32Array.from(present)));
100
+ }
101
+ return { graph: { entry: view.entry, layers }, hubs };
102
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * digest.ts — the name of an index state.
3
+ *
4
+ * sha-256 over the canonical encoding of everything a result depends on: the
5
+ * fingerprint, dims, params and int8 scale; every live card's (id, hash, keys) in
6
+ * id order; and the compacted snapshot (node ids and hashes, hubs, the CSR graph).
7
+ * Every search result is a pure function of `(digest, query, opts)`, which is what
8
+ * lets a logged digest replay a disputed answer.
9
+ */
10
+
11
+ import { compareIds } from '../order.js';
12
+ import type { IndexParams } from './types.js';
13
+ import type { IndexState } from './state.js';
14
+ import type { CsrGraph } from './csr.js';
15
+ import { ByteWriter } from './bytes.js';
16
+ import { sha256Hex } from './sha256.js';
17
+
18
+ /** The default content hash of a card's text. */
19
+ export function hashDocText(text: string): string {
20
+ return sha256Hex(text);
21
+ }
22
+
23
+ /** Params in a fixed field order, for the digest and the serialized header. */
24
+ export function writeParams(w: ByteWriter, p: IndexParams): void {
25
+ w.u32(p.m).u32(p.efConstruction).u32(p.efSearch).f64(p.hubShare).u32(p.rerankFactor).u8(p.lean ? 1 : 0).u32(p.rrfK);
26
+ }
27
+
28
+ /** The graph's layers: entry, then per layer its node list, offsets and neighbours. */
29
+ export function writeGraph(w: ByteWriter, g: CsrGraph): void {
30
+ w.i32(g.entry).u32(g.layers.length);
31
+ for (const layer of g.layers) w.u32s(layer.nodes ?? []).u32s(layer.offsets).u32s(layer.neighbours);
32
+ }
33
+
34
+ export function stateDigest(state: IndexState): string {
35
+ const w = new ByteWriter();
36
+ w.str('bf-knowledge-index/digest/1').str(state.fingerprint).u32(state.dims).f64(state.scale);
37
+ writeParams(w, state.params);
38
+ const ids = [...state.docs.keys()].sort(compareIds);
39
+ w.u32(ids.length);
40
+ for (const id of ids) {
41
+ const doc = state.docs.get(id)!;
42
+ w.str(id).str(doc.hash).u32(doc.keys.length);
43
+ for (const key of doc.keys) w.str(key);
44
+ }
45
+ const c = state.compacted;
46
+ w.u8(c ? 1 : 0);
47
+ if (c) {
48
+ w.u32(c.ids.length);
49
+ for (let i = 0; i < c.ids.length; i++) w.str(c.ids[i]!).str(c.hashes[i]!);
50
+ w.bytes(c.hubs);
51
+ writeGraph(w, c.graph);
52
+ }
53
+ return sha256Hex(w.finish());
54
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * embeddings.ts — the index's recompute path: card text → unit float vector,
3
+ * through a bounded LRU keyed by the card's content hash.
4
+ *
5
+ * LEANN's point is that most vectors need not be stored: the dense tier
6
+ * recomputes float vectors only for the handful of candidates it reranks, and
7
+ * lean mode recomputes the codes it dropped. Keying by hash means an unchanged
8
+ * card is never re-embedded while it stays warm, and a changed one never serves
9
+ * a stale vector.
10
+ */
11
+
12
+ import type { EmbedderPort } from './types.js';
13
+ import { toUnit } from './quantize.js';
14
+
15
+ /** Default number of vectors held. */
16
+ export const EMBEDDING_CACHE_MAX = 8192;
17
+ /** Texts per `embed` call. */
18
+ export const EMBED_BATCH = 256;
19
+
20
+ export interface Embeddable {
21
+ hash: string;
22
+ text: string;
23
+ }
24
+
25
+ export class EmbeddingCache {
26
+ private readonly _lru = new Map<string, Float32Array>();
27
+
28
+ constructor(
29
+ private readonly _embedder: EmbedderPort,
30
+ private readonly _max: number = EMBEDDING_CACHE_MAX,
31
+ ) {}
32
+
33
+ /** Unit vectors for `items`, in order. Misses are embedded in batches of {@link EMBED_BATCH}. */
34
+ async vectors(items: readonly Embeddable[]): Promise<Float32Array[]> {
35
+ const out: Array<Float32Array | undefined> = items.map((it) => this._touch(it.hash));
36
+ const missing: number[] = [];
37
+ const seen = new Map<string, number>();
38
+ items.forEach((it, i) => {
39
+ if (out[i] || seen.has(it.hash)) return;
40
+ seen.set(it.hash, missing.length);
41
+ missing.push(i);
42
+ });
43
+ if (missing.length > 0) {
44
+ const raw: Float32Array[] = [];
45
+ for (let start = 0; start < missing.length; start += EMBED_BATCH) {
46
+ const batch = missing.slice(start, start + EMBED_BATCH).map((i) => items[i]!.text);
47
+ raw.push(...(await this._embedder.embed(batch)));
48
+ }
49
+ missing.forEach((i, j) => {
50
+ const vec = raw[j];
51
+ if (!vec) throw new Error('knowledge-index: embedder returned fewer vectors than texts');
52
+ this._put(items[i]!.hash, toUnit(vec));
53
+ });
54
+ items.forEach((it, i) => {
55
+ if (!out[i]) out[i] = this._touch(it.hash) ?? toUnit(raw[seen.get(it.hash)!]!);
56
+ });
57
+ }
58
+ return out as Float32Array[];
59
+ }
60
+
61
+ /** A query (not a card) vector: not cached, the query space is unbounded. */
62
+ async query(text: string): Promise<Float32Array> {
63
+ const [vec] = await this._embedder.embed([text]);
64
+ if (!vec) throw new Error('knowledge-index: embedder returned no vector for the query');
65
+ return toUnit(vec);
66
+ }
67
+
68
+ private _touch(hash: string): Float32Array | undefined {
69
+ const hit = this._lru.get(hash);
70
+ if (hit) {
71
+ this._lru.delete(hash);
72
+ this._lru.set(hash, hit);
73
+ }
74
+ return hit;
75
+ }
76
+
77
+ private _put(hash: string, vec: Float32Array): void {
78
+ this._lru.delete(hash);
79
+ this._lru.set(hash, vec);
80
+ while (this._lru.size > this._max) {
81
+ const oldest = this._lru.keys().next().value as string;
82
+ this._lru.delete(oldest);
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * expand.ts — recall expansion over the knowledge graph's edges.
3
+ *
4
+ * A hit alone is an isolated fact; its neighbours make it knowledge (a claim's
5
+ * source section, the claims contradicting it, the entity it is about). Each hop
6
+ * scores `parent score × edge weight × 0.5`; a node reached more than one way keeps
7
+ * its best score; the original hits keep theirs. The result is re-ordered by
8
+ * {@link compareHits}, so it is deterministic whatever order the graph lists
9
+ * neighbours in.
10
+ */
11
+
12
+ import { compareHits } from '../order.js';
13
+ import type { GraphNeighbours, IndexHit } from './types.js';
14
+
15
+ /** The per-hop decay. */
16
+ export const EXPAND_HOP_DECAY = 0.5;
17
+
18
+ export interface ExpandOptions {
19
+ hops?: 1 | 2;
20
+ edgeKinds?: string[];
21
+ }
22
+
23
+ export function expandHits(hits: readonly IndexHit[], graph: GraphNeighbours, opts: ExpandOptions = {}): IndexHit[] {
24
+ const hops = opts.hops ?? 1;
25
+ const best = new Map<string, IndexHit>();
26
+ for (const hit of [...hits].sort(compareHits)) {
27
+ const prev = best.get(hit.id);
28
+ if (!prev || hit.score > prev.score) best.set(hit.id, { ...hit });
29
+ }
30
+ const original = new Set(best.keys());
31
+
32
+ let frontier = [...best.values()];
33
+ for (let hop = 0; hop < hops; hop++) {
34
+ const reached = new Map<string, IndexHit>();
35
+ // Parents in total order, and a strict `>` to replace: equal-score ties keep
36
+ // the first parent in that order, independent of the graph's listing order.
37
+ for (const parent of frontier.sort(compareHits)) {
38
+ for (const edge of graph.neighbours(parent.id, opts.edgeKinds)) {
39
+ if (original.has(edge.id)) continue;
40
+ const score = parent.score * edge.weight * EXPAND_HOP_DECAY;
41
+ const prev = reached.get(edge.id);
42
+ if (!prev || score > prev.score) reached.set(edge.id, { id: edge.id, score, tier: parent.tier });
43
+ }
44
+ }
45
+ frontier = [];
46
+ for (const [id, hit] of reached) {
47
+ const prev = best.get(id);
48
+ if (!prev || hit.score > prev.score) {
49
+ best.set(id, hit);
50
+ frontier.push(hit);
51
+ }
52
+ }
53
+ }
54
+ return [...best.values()].sort(compareHits);
55
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The knowledge index — a deterministic, LEANN-style index over knowledge-graph
3
+ * cards (design: Builderforce.ai/docs/design/deterministic-knowledge-index.md).
4
+ *
5
+ * Exact → lexical → dense cascade; int8 codes with integer traversal; a
6
+ * high-degree-preserving pruned graph stored as CSR; float vectors recomputed
7
+ * only for the candidates being reranked. Every result is a pure function of
8
+ * `(digest(), query, opts)`.
9
+ *
10
+ * Runs in a Worker, the browser and Node: no Node-only API anywhere.
11
+ */
12
+
13
+ import type { EmbedderPort, GraphNeighbours, IndexDoc, IndexHit, IndexParams, IndexTier, KnowledgeIndex, SimilarPair } from './types.js';
14
+ import { availableTiers, canonicalKeys, createState, denseReady, graphNode, indexKeys, type DocEntry, type IndexState } from './state.js';
15
+ import { EmbeddingCache } from './embeddings.js';
16
+ import { cascadeSearch, denseCandidates, type SearchContext } from './cascade.js';
17
+ import { compactState } from './compact.js';
18
+ import { expandHits } from './expand.js';
19
+ import { similarPairs } from './pairs.js';
20
+ import { hashDocText, stateDigest } from './digest.js';
21
+ import { deserializeState, serializeState } from './serialize.js';
22
+
23
+ class DeterministicKnowledgeIndex implements KnowledgeIndex {
24
+ private readonly _cache: EmbeddingCache | undefined;
25
+ private _digest: string | null = null;
26
+
27
+ constructor(
28
+ private readonly _state: IndexState,
29
+ private readonly _embedder: EmbedderPort | undefined,
30
+ ) {
31
+ this._cache = _embedder ? new EmbeddingCache(_embedder) : undefined;
32
+ }
33
+
34
+ private get _ctx(): SearchContext {
35
+ return { state: this._state, embedder: this._embedder, cache: this._cache };
36
+ }
37
+
38
+ async upsert(docs: IndexDoc[]): Promise<void> {
39
+ const s = this._state;
40
+ const changed: DocEntry[] = [];
41
+ for (const d of docs) {
42
+ const entry: DocEntry = { id: d.id, text: d.text, keys: canonicalKeys(d.keys), hash: d.hash ?? hashDocText(d.text) };
43
+ const prev = s.docs.get(d.id);
44
+ if (prev) indexKeys(s, prev, false);
45
+ if (prev && prev.hash === entry.hash) {
46
+ prev.keys = entry.keys;
47
+ indexKeys(s, prev, true);
48
+ continue;
49
+ }
50
+ s.docs.set(entry.id, entry);
51
+ indexKeys(s, entry, true);
52
+ s.lexical.set(entry.id, entry.text);
53
+ changed.push(entry);
54
+ }
55
+ this._digest = null;
56
+ if (!denseReady(s, this._embedder) || !this._cache) return;
57
+ // Only the cards that are live and not already coded in the graph need a vector.
58
+ const toEmbed = changed.filter((e) => s.docs.get(e.id) === e && graphNode(s, e.id) < 0);
59
+ const vecs = await this._cache.vectors(toEmbed);
60
+ toEmbed.forEach((e, i) => { e.vec = vecs[i]; });
61
+ }
62
+
63
+ remove(ids: string[]): void {
64
+ const s = this._state;
65
+ for (const id of ids) {
66
+ const doc = s.docs.get(id);
67
+ if (!doc) continue;
68
+ indexKeys(s, doc, false);
69
+ s.lexical.delete(id);
70
+ s.docs.delete(id);
71
+ }
72
+ this._digest = null;
73
+ }
74
+
75
+ search(q: string, opts?: { k?: number; tiers?: IndexTier[] }): Promise<IndexHit[]> {
76
+ return cascadeSearch(this._ctx, q, opts);
77
+ }
78
+
79
+ expand(hits: IndexHit[], graph: GraphNeighbours, opts?: { hops?: 1 | 2; edgeKinds?: string[] }): IndexHit[] {
80
+ return expandHits(hits, graph, opts);
81
+ }
82
+
83
+ similarPairs(opts: { minSimilarity: number; maxPairs: number; idPrefix?: string }): SimilarPair[] {
84
+ return similarPairs(this._state, opts);
85
+ }
86
+
87
+ async compact(): Promise<void> {
88
+ await compactState(this._state, this._embedder, this._cache);
89
+ this._digest = null;
90
+ }
91
+
92
+ digest(): string {
93
+ return (this._digest ??= stateDigest(this._state));
94
+ }
95
+
96
+ tiers(): IndexTier[] {
97
+ return availableTiers(this._state, this._embedder);
98
+ }
99
+
100
+ size(): number {
101
+ return this._state.docs.size;
102
+ }
103
+
104
+ serialize(): Uint8Array {
105
+ return serializeState(this._state);
106
+ }
107
+
108
+ /**
109
+ * Diagnostics: the dense candidate ids for `q` before the float rerank (the
110
+ * integer graph walk plus not-yet-compacted cards). Empty without the dense tier.
111
+ */
112
+ async denseCandidates(q: string, n: number): Promise<string[]> {
113
+ if (!denseReady(this._state, this._embedder) || !this._cache) return [];
114
+ return denseCandidates(this._ctx, await this._cache.query(q), n);
115
+ }
116
+
117
+ /** Diagnostics: stored bytes of the int8 codes and the CSR graph, against float32. */
118
+ storage(): KnowledgeIndexStorage {
119
+ const c = this._state.compacted;
120
+ if (!c) return { nodes: 0, codeBytes: 0, graphBytes: 0, float32Bytes: 0 };
121
+ let codeBytes = 0;
122
+ for (const code of c.codes) codeBytes += code?.byteLength ?? 0;
123
+ let graphBytes = 0;
124
+ for (const l of c.graph.layers) graphBytes += (l.nodes?.byteLength ?? 0) + l.offsets.byteLength + l.neighbours.byteLength;
125
+ return { nodes: c.ids.length, codeBytes, graphBytes, float32Bytes: c.ids.length * this._state.dims * 4 };
126
+ }
127
+ }
128
+
129
+ export interface KnowledgeIndexStorage {
130
+ nodes: number;
131
+ codeBytes: number;
132
+ graphBytes: number;
133
+ /** What the same nodes would take as float32 vectors. */
134
+ float32Bytes: number;
135
+ }
136
+
137
+ /** The concrete index type: the contract plus diagnostics. */
138
+ export type DeterministicKnowledgeIndexType = KnowledgeIndex & {
139
+ denseCandidates(q: string, n: number): Promise<string[]>;
140
+ storage(): KnowledgeIndexStorage;
141
+ };
142
+
143
+ export function createKnowledgeIndex(
144
+ opts: { embedder?: EmbedderPort; params?: Partial<IndexParams> } = {},
145
+ ): DeterministicKnowledgeIndexType {
146
+ return new DeterministicKnowledgeIndex(createState(opts.params, opts.embedder), opts.embedder);
147
+ }
148
+
149
+ /**
150
+ * Loads a serialized index. With no embedder, or one of another fingerprint, the
151
+ * dense tier is absent until `compact()` re-embeds every card under the new one.
152
+ */
153
+ export function loadKnowledgeIndex(bytes: Uint8Array, embedder?: EmbedderPort): DeterministicKnowledgeIndexType {
154
+ return new DeterministicKnowledgeIndex(deserializeState(bytes), embedder);
155
+ }
156
+
157
+ export type { EmbedderPort, GraphNeighbours, IndexDoc, IndexHit, IndexParams, IndexTier, KnowledgeIndex, SimilarPair } from './types.js';
158
+ export { DEFAULT_INDEX_PARAMS } from './state.js';
159
+ export { codeTokenize } from './tokenize.js';
160
+ export { hashDocText } from './digest.js';
161
+ export { sha256, sha256Hex, Sha256 } from './sha256.js';
162
+ export { EXPAND_HOP_DECAY } from './expand.js';
@@ -0,0 +1,78 @@
1
+ /**
2
+ * lexical.ts — the knowledge index's BM25 tier, as an incremental inverted index.
3
+ *
4
+ * `bm25Search` rebuilds its term table per call, which is right for a bounded
5
+ * candidate set and wrong for 50k cards queried on every prompt. This keeps the
6
+ * postings across upserts and scores with the SAME Okapi core (`bm25Idf`,
7
+ * `bm25LengthNorm`, `bm25TermScore`) — the formula is not restated here.
8
+ */
9
+
10
+ import { bm25Idf, bm25LengthNorm, bm25TermScore } from '../bm25.js';
11
+ import { compareHits } from '../order.js';
12
+ import { codeTokenize } from './tokenize.js';
13
+
14
+ export interface LexicalHit {
15
+ id: string;
16
+ score: number;
17
+ }
18
+
19
+ export class LexicalIndex {
20
+ /** term → (doc id → term frequency). */
21
+ private readonly _postings = new Map<string, Map<string, number>>();
22
+ /** doc id → its distinct terms and token count. */
23
+ private readonly _docs = new Map<string, { terms: string[]; len: number }>();
24
+ private _totalLen = 0;
25
+
26
+ get size(): number {
27
+ return this._docs.size;
28
+ }
29
+
30
+ /** Indexes (or re-indexes) one document. */
31
+ set(id: string, text: string): void {
32
+ this.delete(id);
33
+ const tokens = codeTokenize(text);
34
+ const tf = new Map<string, number>();
35
+ for (const t of tokens) tf.set(t, (tf.get(t) ?? 0) + 1);
36
+ for (const [term, f] of tf) {
37
+ let posting = this._postings.get(term);
38
+ if (!posting) this._postings.set(term, (posting = new Map()));
39
+ posting.set(id, f);
40
+ }
41
+ this._docs.set(id, { terms: [...tf.keys()], len: tokens.length });
42
+ this._totalLen += tokens.length;
43
+ }
44
+
45
+ delete(id: string): void {
46
+ const doc = this._docs.get(id);
47
+ if (!doc) return;
48
+ for (const term of doc.terms) {
49
+ const posting = this._postings.get(term);
50
+ posting?.delete(id);
51
+ if (posting && posting.size === 0) this._postings.delete(term);
52
+ }
53
+ this._totalLen -= doc.len;
54
+ this._docs.delete(id);
55
+ }
56
+
57
+ /** BM25 hits for `query`, best first ({@link compareHits}), at most `limit`. */
58
+ search(query: string, limit: number): LexicalHit[] {
59
+ const n = this._docs.size;
60
+ if (n === 0 || limit <= 0) return [];
61
+ const terms = [...new Set(codeTokenize(query))].sort();
62
+ const avgdl = this._totalLen / n || 1;
63
+ const scores = new Map<string, number>();
64
+ // Terms in sorted order, so each doc's sum is accumulated in the same order everywhere.
65
+ for (const term of terms) {
66
+ const posting = this._postings.get(term);
67
+ if (!posting) continue;
68
+ const idf = bm25Idf(n, posting.size);
69
+ for (const [id, f] of posting) {
70
+ const norm = bm25LengthNorm(this._docs.get(id)!.len, avgdl);
71
+ scores.set(id, (scores.get(id) ?? 0) + bm25TermScore(f, idf, norm));
72
+ }
73
+ }
74
+ const hits: LexicalHit[] = [];
75
+ for (const [id, score] of scores) if (score > 0) hits.push({ id, score });
76
+ return hits.sort(compareHits).slice(0, limit);
77
+ }
78
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * pairs.ts — near-duplicate pairs over the int8 codes (the Duplication lens,
3
+ * review-time entity resolution, contradiction candidates).
4
+ *
5
+ * Near-duplicates are each other's nearest neighbours, so the candidate pairs are
6
+ * the compacted graph's edges on every layer rather than all n² pairs. Similarity
7
+ * is {@link cosineInt8} (exact integers, correctly-rounded sqrt/division). The
8
+ * order is total: similarity (quantized 1e-4) descending, then `a`, then `b`, and
9
+ * `a < b` by id in every pair.
10
+ *
11
+ * Cards not yet compacted, and lean-mode nodes whose codes were dropped, are not
12
+ * paired until the next compaction (the call is synchronous; it never embeds).
13
+ */
14
+
15
+ import { compareIds, quantizeScore, DENSE_SCORE_QUANTUM } from '../order.js';
16
+ import type { SimilarPair } from './types.js';
17
+ import { graphNode, type IndexState } from './state.js';
18
+ import { neighboursOf } from './csr.js';
19
+ import { cosineInt8 } from './quantize.js';
20
+
21
+ export interface SimilarPairsOptions {
22
+ minSimilarity: number;
23
+ maxPairs: number;
24
+ idPrefix?: string;
25
+ }
26
+
27
+ export function similarPairs(state: IndexState, opts: SimilarPairsOptions): SimilarPair[] {
28
+ const c = state.compacted;
29
+ if (!c || opts.maxPairs <= 0) return [];
30
+ const prefix = opts.idPrefix ?? '';
31
+ const usable = (node: number) =>
32
+ !!c.codes[node] && c.ids[node]!.startsWith(prefix) && graphNode(state, c.ids[node]!) === node;
33
+
34
+ const seen = new Set<string>();
35
+ const pairs: Array<SimilarPair & { q: number }> = [];
36
+ for (let a = 0; a < c.ids.length; a++) {
37
+ if (!usable(a)) continue;
38
+ for (const layer of c.graph.layers) {
39
+ for (const b of neighboursOf(layer, a)) {
40
+ if (b === a || !usable(b)) continue;
41
+ const lo = Math.min(a, b);
42
+ const hi = Math.max(a, b);
43
+ const key = `${lo}:${hi}`;
44
+ if (seen.has(key)) continue;
45
+ seen.add(key);
46
+ const similarity = cosineInt8(c.codes[lo]!, c.codes[hi]!);
47
+ if (similarity >= opts.minSimilarity) {
48
+ pairs.push({ a: c.ids[lo]!, b: c.ids[hi]!, similarity, q: quantizeScore(similarity, DENSE_SCORE_QUANTUM) });
49
+ }
50
+ }
51
+ }
52
+ }
53
+ return pairs
54
+ .sort((x, y) => (y.q - x.q) || compareIds(x.a, y.a) || compareIds(x.b, y.b))
55
+ .slice(0, opts.maxPairs)
56
+ .map(({ a, b, similarity }) => ({ a, b, similarity }));
57
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * prune.ts — high-degree-preserving pruning of the base layer (the LEANN storage
3
+ * technique).
4
+ *
5
+ * Most of a graph index's bytes are base-layer edges. Searches route through the
6
+ * few well-connected hubs, so those keep their full degree and every other node
7
+ * keeps half: the top `hubShare` of nodes by in-degree keep `m` edges, the rest
8
+ * `m / 2`. Upper layers are tiny and stay unpruned.
9
+ *
10
+ * Every choice is a total order (in-degree, then node index; similarity, then
11
+ * node index — and node index is id order), so pruning is deterministic.
12
+ */
13
+
14
+ import { compareScoreThenId, DENSE_SCORE_QUANTUM } from '../order.js';
15
+
16
+ /**
17
+ * Prunes base-layer rows. `similarity(a, b)` scores two nodes (higher = nearer).
18
+ * Each kept row is ordered nearest first.
19
+ */
20
+ export function pruneBaseLayer(
21
+ rows: ReadonlyArray<readonly number[]>,
22
+ m: number,
23
+ hubShare: number,
24
+ similarity: (a: number, b: number) => number,
25
+ ): { rows: number[][]; hubs: Uint8Array } {
26
+ const n = rows.length;
27
+ const inDegree = new Uint32Array(n);
28
+ for (const row of rows) for (const to of row) inDegree[to] += 1;
29
+
30
+ const hubs = new Uint8Array(n);
31
+ const hubCount = n === 0 ? 0 : Math.min(n, Math.max(1, Math.ceil(n * Math.max(0, hubShare))));
32
+ const byDegree = Array.from({ length: n }, (_, i) => i)
33
+ .sort((a, b) => inDegree[b]! - inDegree[a]! || a - b);
34
+ for (let i = 0; i < hubCount; i++) hubs[byDegree[i]!] = 1;
35
+
36
+ const full = Math.max(1, m);
37
+ const half = Math.max(1, Math.floor(m / 2));
38
+ const pruned = rows.map((row, node) => {
39
+ const keep = hubs[node] ? full : half;
40
+ return [...new Set(row)]
41
+ .filter((to) => to !== node)
42
+ .map((to) => ({ to, score: similarity(node, to) }))
43
+ .sort((a, b) => compareScoreThenId(a.score, a.to, b.score, b.to, DENSE_SCORE_QUANTUM))
44
+ .slice(0, keep)
45
+ .map((e) => e.to);
46
+ });
47
+ return { rows: pruned, hubs };
48
+ }