@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.
- package/dist/ingest/IngestionPipeline.d.ts +1 -8
- package/dist/ingest/IngestionPipeline.d.ts.map +1 -1
- package/dist/ingest/IngestionPipeline.js +3 -15
- package/dist/ingest/IngestionPipeline.js.map +1 -1
- package/dist/ingest/docsExtract.d.ts +30 -0
- package/dist/ingest/docsExtract.d.ts.map +1 -0
- package/dist/ingest/docsExtract.js +221 -0
- package/dist/ingest/docsExtract.js.map +1 -0
- package/dist/ingest/hash.d.ts +15 -0
- package/dist/ingest/hash.d.ts.map +1 -0
- package/dist/ingest/hash.js +22 -0
- package/dist/ingest/hash.js.map +1 -0
- package/dist/ingest/parsers.d.ts.map +1 -1
- package/dist/ingest/parsers.js +4 -0
- package/dist/ingest/parsers.js.map +1 -1
- package/dist/ingest/types.d.ts +7 -0
- package/dist/ingest/types.d.ts.map +1 -1
- package/dist/ingest/types.js.map +1 -1
- package/dist/knowledge/build.d.ts +24 -0
- package/dist/knowledge/build.d.ts.map +1 -0
- package/dist/knowledge/build.js +263 -0
- package/dist/knowledge/build.js.map +1 -0
- package/dist/knowledge/cards.d.ts +17 -0
- package/dist/knowledge/cards.d.ts.map +1 -0
- package/dist/knowledge/cards.js +118 -0
- package/dist/knowledge/cards.js.map +1 -0
- package/dist/knowledge/findings.d.ts +24 -0
- package/dist/knowledge/findings.d.ts.map +1 -0
- package/dist/knowledge/findings.js +152 -0
- package/dist/knowledge/findings.js.map +1 -0
- package/dist/knowledge/importance.d.ts +21 -0
- package/dist/knowledge/importance.d.ts.map +1 -0
- package/dist/knowledge/importance.js +70 -0
- package/dist/knowledge/importance.js.map +1 -0
- package/dist/knowledge/index.d.ts +31 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge/index.js +26 -0
- package/dist/knowledge/index.js.map +1 -0
- package/dist/knowledge/judge.d.ts +86 -0
- package/dist/knowledge/judge.d.ts.map +1 -0
- package/dist/knowledge/judge.js +213 -0
- package/dist/knowledge/judge.js.map +1 -0
- package/dist/knowledge/model.d.ts +333 -0
- package/dist/knowledge/model.d.ts.map +1 -0
- package/dist/knowledge/model.js +13 -0
- package/dist/knowledge/model.js.map +1 -0
- package/dist/knowledge/packs/docs.d.ts +17 -0
- package/dist/knowledge/packs/docs.d.ts.map +1 -0
- package/dist/knowledge/packs/docs.js +89 -0
- package/dist/knowledge/packs/docs.js.map +1 -0
- package/dist/knowledge/partitions.d.ts +31 -0
- package/dist/knowledge/partitions.d.ts.map +1 -0
- package/dist/knowledge/partitions.js +135 -0
- package/dist/knowledge/partitions.js.map +1 -0
- package/dist/knowledge/plan.d.ts +33 -0
- package/dist/knowledge/plan.d.ts.map +1 -0
- package/dist/knowledge/plan.js +89 -0
- package/dist/knowledge/plan.js.map +1 -0
- package/dist/knowledge/refs.d.ts +40 -0
- package/dist/knowledge/refs.d.ts.map +1 -0
- package/dist/knowledge/refs.js +102 -0
- package/dist/knowledge/refs.js.map +1 -0
- package/dist/knowledge/resolve.d.ts +35 -0
- package/dist/knowledge/resolve.d.ts.map +1 -0
- package/dist/knowledge/resolve.js +67 -0
- package/dist/knowledge/resolve.js.map +1 -0
- package/dist/knowledge/review.d.ts +23 -0
- package/dist/knowledge/review.d.ts.map +1 -0
- package/dist/knowledge/review.js +233 -0
- package/dist/knowledge/review.js.map +1 -0
- package/dist/knowledge/sources.d.ts +47 -0
- package/dist/knowledge/sources.d.ts.map +1 -0
- package/dist/knowledge/sources.js +136 -0
- package/dist/knowledge/sources.js.map +1 -0
- package/dist/knowledge/stores/localSnapshotStore.d.ts +50 -0
- package/dist/knowledge/stores/localSnapshotStore.d.ts.map +1 -0
- package/dist/knowledge/stores/localSnapshotStore.js +125 -0
- package/dist/knowledge/stores/localSnapshotStore.js.map +1 -0
- package/dist/knowledge/trace.d.ts +32 -0
- package/dist/knowledge/trace.d.ts.map +1 -0
- package/dist/knowledge/trace.js +102 -0
- package/dist/knowledge/trace.js.map +1 -0
- package/dist/knowledge/truncate.d.ts +24 -0
- package/dist/knowledge/truncate.d.ts.map +1 -0
- package/dist/knowledge/truncate.js +128 -0
- package/dist/knowledge/truncate.js.map +1 -0
- package/dist/memory/SemanticRecall.d.ts.map +1 -1
- package/dist/memory/SemanticRecall.js +3 -1
- package/dist/memory/SemanticRecall.js.map +1 -1
- package/dist/retrieval/HybridRetriever.d.ts.map +1 -1
- package/dist/retrieval/HybridRetriever.js +3 -1
- package/dist/retrieval/HybridRetriever.js.map +1 -1
- package/dist/retrieval/bm25.d.ts +3 -2
- package/dist/retrieval/bm25.d.ts.map +1 -1
- package/dist/retrieval/bm25.js +5 -3
- package/dist/retrieval/bm25.js.map +1 -1
- package/dist/retrieval/fusion.d.ts +2 -1
- package/dist/retrieval/fusion.d.ts.map +1 -1
- package/dist/retrieval/fusion.js +4 -2
- package/dist/retrieval/fusion.js.map +1 -1
- package/dist/retrieval/hnsw.d.ts +49 -11
- package/dist/retrieval/hnsw.d.ts.map +1 -1
- package/dist/retrieval/hnsw.js +57 -29
- package/dist/retrieval/hnsw.js.map +1 -1
- package/dist/retrieval/index.d.ts +7 -3
- package/dist/retrieval/index.d.ts.map +1 -1
- package/dist/retrieval/index.js +5 -2
- package/dist/retrieval/index.js.map +1 -1
- package/dist/retrieval/knowledge-index/bytes.d.ts +43 -0
- package/dist/retrieval/knowledge-index/bytes.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/bytes.js +151 -0
- package/dist/retrieval/knowledge-index/bytes.js.map +1 -0
- package/dist/retrieval/knowledge-index/cascade.d.ts +37 -0
- package/dist/retrieval/knowledge-index/cascade.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/cascade.js +111 -0
- package/dist/retrieval/knowledge-index/cascade.js.map +1 -0
- package/dist/retrieval/knowledge-index/compact.d.ts +18 -0
- package/dist/retrieval/knowledge-index/compact.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/compact.js +64 -0
- package/dist/retrieval/knowledge-index/compact.js.map +1 -0
- package/dist/retrieval/knowledge-index/csr.d.ts +41 -0
- package/dist/retrieval/knowledge-index/csr.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/csr.js +76 -0
- package/dist/retrieval/knowledge-index/csr.js.map +1 -0
- package/dist/retrieval/knowledge-index/digest.d.ts +21 -0
- package/dist/retrieval/knowledge-index/digest.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/digest.js +50 -0
- package/dist/retrieval/knowledge-index/digest.js.map +1 -0
- package/dist/retrieval/knowledge-index/embeddings.d.ts +32 -0
- package/dist/retrieval/knowledge-index/embeddings.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/embeddings.js +78 -0
- package/dist/retrieval/knowledge-index/embeddings.js.map +1 -0
- package/dist/retrieval/knowledge-index/expand.d.ts +19 -0
- package/dist/retrieval/knowledge-index/expand.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/expand.js +49 -0
- package/dist/retrieval/knowledge-index/expand.js.map +1 -0
- package/dist/retrieval/knowledge-index/index.d.ts +40 -0
- package/dist/retrieval/knowledge-index/index.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/index.js +134 -0
- package/dist/retrieval/knowledge-index/index.js.map +1 -0
- package/dist/retrieval/knowledge-index/lexical.d.ts +26 -0
- package/dist/retrieval/knowledge-index/lexical.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/lexical.js +76 -0
- package/dist/retrieval/knowledge-index/lexical.js.map +1 -0
- package/dist/retrieval/knowledge-index/pairs.d.ts +22 -0
- package/dist/retrieval/knowledge-index/pairs.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/pairs.js +51 -0
- package/dist/retrieval/knowledge-index/pairs.js.map +1 -0
- package/dist/retrieval/knowledge-index/prune.d.ts +21 -0
- package/dist/retrieval/knowledge-index/prune.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/prune.js +43 -0
- package/dist/retrieval/knowledge-index/prune.js.map +1 -0
- package/dist/retrieval/knowledge-index/quantize.d.ts +37 -0
- package/dist/retrieval/knowledge-index/quantize.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/quantize.js +77 -0
- package/dist/retrieval/knowledge-index/quantize.js.map +1 -0
- package/dist/retrieval/knowledge-index/serialize.d.ts +18 -0
- package/dist/retrieval/knowledge-index/serialize.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/serialize.js +123 -0
- package/dist/retrieval/knowledge-index/serialize.js.map +1 -0
- package/dist/retrieval/knowledge-index/sha256.d.ts +30 -0
- package/dist/retrieval/knowledge-index/sha256.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/sha256.js +130 -0
- package/dist/retrieval/knowledge-index/sha256.js.map +1 -0
- package/dist/retrieval/knowledge-index/state.d.ts +55 -0
- package/dist/retrieval/knowledge-index/state.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/state.js +67 -0
- package/dist/retrieval/knowledge-index/state.js.map +1 -0
- package/dist/retrieval/knowledge-index/tokenize.d.ts +20 -0
- package/dist/retrieval/knowledge-index/tokenize.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/tokenize.js +42 -0
- package/dist/retrieval/knowledge-index/tokenize.js.map +1 -0
- package/dist/retrieval/knowledge-index/traverse.d.ts +26 -0
- package/dist/retrieval/knowledge-index/traverse.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/traverse.js +90 -0
- package/dist/retrieval/knowledge-index/traverse.js.map +1 -0
- package/dist/retrieval/knowledge-index/types.d.ts +85 -0
- package/dist/retrieval/knowledge-index/types.d.ts.map +1 -0
- package/dist/retrieval/knowledge-index/types.js +9 -0
- package/dist/retrieval/knowledge-index/types.js.map +1 -0
- package/dist/retrieval/mmr.d.ts +11 -4
- package/dist/retrieval/mmr.d.ts.map +1 -1
- package/dist/retrieval/mmr.js +28 -8
- package/dist/retrieval/mmr.js.map +1 -1
- package/dist/retrieval/order.d.ts +41 -0
- package/dist/retrieval/order.d.ts.map +1 -0
- package/dist/retrieval/order.js +54 -0
- package/dist/retrieval/order.js.map +1 -0
- package/dist/retrieval/weightedFusion.d.ts +2 -1
- package/dist/retrieval/weightedFusion.d.ts.map +1 -1
- package/dist/retrieval/weightedFusion.js +7 -5
- package/dist/retrieval/weightedFusion.js.map +1 -1
- package/dist/vectorstore/MemoryVectorStore.d.ts.map +1 -1
- package/dist/vectorstore/MemoryVectorStore.js +2 -1
- package/dist/vectorstore/MemoryVectorStore.js.map +1 -1
- package/package.json +8 -3
- package/src/ingest/IngestionPipeline.ts +3 -15
- package/src/ingest/docsExtract.ts +219 -0
- package/src/ingest/hash.ts +22 -0
- package/src/ingest/parsers.ts +4 -0
- package/src/ingest/types.ts +7 -0
- package/src/knowledge/build.test.ts +150 -0
- package/src/knowledge/build.ts +266 -0
- package/src/knowledge/cards.ts +130 -0
- package/src/knowledge/docs.test.ts +118 -0
- package/src/knowledge/findings.ts +159 -0
- package/src/knowledge/importance.ts +72 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/judge.ts +231 -0
- package/src/knowledge/model.ts +312 -0
- package/src/knowledge/packs/docs.ts +91 -0
- package/src/knowledge/partitions.test.ts +98 -0
- package/src/knowledge/partitions.ts +119 -0
- package/src/knowledge/plan.ts +115 -0
- package/src/knowledge/refs.ts +108 -0
- package/src/knowledge/resolve.ts +78 -0
- package/src/knowledge/review.test.ts +180 -0
- package/src/knowledge/review.ts +235 -0
- package/src/knowledge/sources.ts +146 -0
- package/src/knowledge/stores/localSnapshotStore.test.ts +82 -0
- package/src/knowledge/stores/localSnapshotStore.ts +159 -0
- package/src/knowledge/testkit.test-util.ts +92 -0
- package/src/knowledge/trace.test.ts +91 -0
- package/src/knowledge/trace.ts +111 -0
- package/src/knowledge/truncate.ts +118 -0
- package/src/memory/SemanticRecall.ts +3 -1
- package/src/retrieval/HybridRetriever.ts +3 -1
- package/src/retrieval/bm25.ts +5 -3
- package/src/retrieval/fusion.ts +5 -2
- package/src/retrieval/hnsw.ts +82 -35
- package/src/retrieval/index.ts +17 -3
- package/src/retrieval/knowledge-index/bytes.ts +166 -0
- package/src/retrieval/knowledge-index/cascade.ts +125 -0
- package/src/retrieval/knowledge-index/compact.ts +68 -0
- package/src/retrieval/knowledge-index/csr.ts +102 -0
- package/src/retrieval/knowledge-index/digest.ts +54 -0
- package/src/retrieval/knowledge-index/embeddings.ts +85 -0
- package/src/retrieval/knowledge-index/expand.ts +55 -0
- package/src/retrieval/knowledge-index/index.ts +162 -0
- package/src/retrieval/knowledge-index/lexical.ts +78 -0
- package/src/retrieval/knowledge-index/pairs.ts +57 -0
- package/src/retrieval/knowledge-index/prune.ts +48 -0
- package/src/retrieval/knowledge-index/quantize.ts +78 -0
- package/src/retrieval/knowledge-index/serialize.ts +124 -0
- package/src/retrieval/knowledge-index/sha256.ts +122 -0
- package/src/retrieval/knowledge-index/state.ts +108 -0
- package/src/retrieval/knowledge-index/tokenize.ts +41 -0
- package/src/retrieval/knowledge-index/traverse.ts +95 -0
- package/src/retrieval/knowledge-index/types.ts +78 -0
- package/src/retrieval/mmr.ts +35 -10
- package/src/retrieval/order.ts +68 -0
- package/src/retrieval/weightedFusion.ts +8 -5
- package/src/vectorstore/MemoryVectorStore.ts +2 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quantize.ts — int8 codes with one symmetric per-index scale.
|
|
3
|
+
*
|
|
4
|
+
* Graph traversal compares int8 codes with integer dot products. A sum of
|
|
5
|
+
* int8×int8 products stays far inside 2^53, so it is exact in a JS number and
|
|
6
|
+
* bit-identical on every platform — the float noise between a CPU and a WebGPU
|
|
7
|
+
* embedding only matters where a component sits on a rounding boundary.
|
|
8
|
+
*
|
|
9
|
+
* The scale is fitted once (at the index's first compaction) and then fixed, so a
|
|
10
|
+
* code never changes meaning while the index lives.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Largest code magnitude; symmetric, so -128 is never produced. */
|
|
14
|
+
export const CODE_MAX = 127;
|
|
15
|
+
|
|
16
|
+
/** How far from unit norm a vector may be and still be taken as already normalised. */
|
|
17
|
+
const UNIT_TOLERANCE = 1e-3;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A unit-norm Float32 copy of `v`. Every embedder behind the port already emits
|
|
21
|
+
* L2-normalised vectors; those (|‖v‖² − 1| ≤ 1e-3) are copied as-is, so a
|
|
22
|
+
* last-bit difference between two hosts' embeddings is not amplified by a
|
|
23
|
+
* renormalisation. Anything else is normalised; a zero vector stays zero.
|
|
24
|
+
*/
|
|
25
|
+
export function toUnit(v: ArrayLike<number>): Float32Array {
|
|
26
|
+
let sum = 0;
|
|
27
|
+
for (let i = 0; i < v.length; i++) sum += v[i]! * v[i]!;
|
|
28
|
+
const out = Float32Array.from(v);
|
|
29
|
+
if (sum === 0 || Math.abs(sum - 1) <= UNIT_TOLERANCE) return out;
|
|
30
|
+
const norm = Math.sqrt(sum);
|
|
31
|
+
for (let i = 0; i < out.length; i++) out[i] = out[i]! / norm;
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The symmetric scale for a set of (normalised) vectors: CODE_MAX over the largest
|
|
37
|
+
* component magnitude, rounded UP to 1/64 first so a little float noise in the
|
|
38
|
+
* maximum does not move the scale.
|
|
39
|
+
*/
|
|
40
|
+
export function fitScale(vectors: Iterable<ArrayLike<number>>): number {
|
|
41
|
+
let maxAbs = 0;
|
|
42
|
+
for (const v of vectors) {
|
|
43
|
+
for (let i = 0; i < v.length; i++) {
|
|
44
|
+
const a = Math.abs(v[i]!);
|
|
45
|
+
if (a > maxAbs) maxAbs = a;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const bound = Math.max(1, Math.ceil(maxAbs * 64)) / 64;
|
|
49
|
+
return CODE_MAX / bound;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** int8 codes of `v` at `scale`, clamped to ±CODE_MAX. */
|
|
53
|
+
export function quantize(v: ArrayLike<number>, scale: number): Int8Array {
|
|
54
|
+
const out = new Int8Array(v.length);
|
|
55
|
+
for (let i = 0; i < v.length; i++) {
|
|
56
|
+
const q = Math.round(v[i]! * scale);
|
|
57
|
+
out[i] = q > CODE_MAX ? CODE_MAX : q < -CODE_MAX ? -CODE_MAX : q;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Exact integer dot product of two code vectors. */
|
|
63
|
+
export function dotInt8(a: Int8Array, b: Int8Array): number {
|
|
64
|
+
const n = Math.min(a.length, b.length);
|
|
65
|
+
let dot = 0;
|
|
66
|
+
for (let i = 0; i < n; i++) dot += a[i]! * b[i]!;
|
|
67
|
+
return dot;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Cosine of two code vectors: an exact integer dot over exact integer norms, then
|
|
72
|
+
* IEEE `sqrt` and division — both correctly rounded, so the result is the same
|
|
73
|
+
* everywhere. 0 when either is a zero vector.
|
|
74
|
+
*/
|
|
75
|
+
export function cosineInt8(a: Int8Array, b: Int8Array): number {
|
|
76
|
+
const denom = Math.sqrt(dotInt8(a, a)) * Math.sqrt(dotInt8(b, b));
|
|
77
|
+
return denom === 0 ? 0 : dotInt8(a, b) / denom;
|
|
78
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serialize.ts — the versioned binary form of an index state.
|
|
3
|
+
*
|
|
4
|
+
* Layout (little-endian, via `bytes.ts`):
|
|
5
|
+
* magic "BFKI" · u32 version
|
|
6
|
+
* fingerprint · dims · scale · params
|
|
7
|
+
* docs (id order): id · text · hash · keys · optional pending float vector
|
|
8
|
+
* u8 compacted? → node ids · node hashes · hubs · code-present flags · codes · graph
|
|
9
|
+
*
|
|
10
|
+
* Texts travel with the index because the lexical tier, the recompute path and a
|
|
11
|
+
* re-embed under a new fingerprint all need them. Loading rebuilds the derived
|
|
12
|
+
* structures (key map, BM25 postings) and gives back the same digest.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { compareIds } from '../order.js';
|
|
16
|
+
import type { IndexParams } from './types.js';
|
|
17
|
+
import { createState, indexKeys, type Compacted, type DocEntry, type IndexState } from './state.js';
|
|
18
|
+
import type { CsrLayer } from './csr.js';
|
|
19
|
+
import { ByteReader, ByteWriter } from './bytes.js';
|
|
20
|
+
import { writeGraph, writeParams } from './digest.js';
|
|
21
|
+
|
|
22
|
+
const MAGIC = [0x42, 0x46, 0x4b, 0x49]; // "BFKI"
|
|
23
|
+
export const SERIALIZE_VERSION = 1;
|
|
24
|
+
|
|
25
|
+
export function serializeState(state: IndexState): Uint8Array {
|
|
26
|
+
const w = new ByteWriter();
|
|
27
|
+
w.raw(Uint8Array.from(MAGIC)).u32(SERIALIZE_VERSION);
|
|
28
|
+
w.str(state.fingerprint).u32(state.dims).f64(state.scale);
|
|
29
|
+
writeParams(w, state.params);
|
|
30
|
+
|
|
31
|
+
const ids = [...state.docs.keys()].sort(compareIds);
|
|
32
|
+
w.u32(ids.length);
|
|
33
|
+
for (const id of ids) {
|
|
34
|
+
const doc = state.docs.get(id)!;
|
|
35
|
+
w.str(doc.id).str(doc.text).str(doc.hash).u32(doc.keys.length);
|
|
36
|
+
for (const key of doc.keys) w.str(key);
|
|
37
|
+
w.u8(doc.vec ? 1 : 0);
|
|
38
|
+
if (doc.vec) w.f32s(doc.vec);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const c = state.compacted;
|
|
42
|
+
w.u8(c ? 1 : 0);
|
|
43
|
+
if (c) {
|
|
44
|
+
w.u32(c.ids.length);
|
|
45
|
+
for (let i = 0; i < c.ids.length; i++) w.str(c.ids[i]!).str(c.hashes[i]!);
|
|
46
|
+
w.bytes(c.hubs);
|
|
47
|
+
w.bytes(Uint8Array.from(c.codes, (code) => (code ? 1 : 0)));
|
|
48
|
+
for (const code of c.codes) if (code) w.bytes(code);
|
|
49
|
+
writeGraph(w, c.graph);
|
|
50
|
+
}
|
|
51
|
+
return w.finish();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readParams(r: ByteReader): IndexParams {
|
|
55
|
+
return {
|
|
56
|
+
m: r.u32(),
|
|
57
|
+
efConstruction: r.u32(),
|
|
58
|
+
efSearch: r.u32(),
|
|
59
|
+
hubShare: r.f64(),
|
|
60
|
+
rerankFactor: r.u32(),
|
|
61
|
+
lean: r.u8() === 1,
|
|
62
|
+
rrfK: r.u32(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function deserializeState(bytes: Uint8Array): IndexState {
|
|
67
|
+
const r = new ByteReader(bytes);
|
|
68
|
+
const magic = r.raw(4);
|
|
69
|
+
if (MAGIC.some((b, i) => magic[i] !== b)) throw new Error('knowledge-index: not a serialized index');
|
|
70
|
+
const version = r.u32();
|
|
71
|
+
if (version !== SERIALIZE_VERSION) throw new Error(`knowledge-index: unsupported version ${version}`);
|
|
72
|
+
|
|
73
|
+
const fingerprint = r.str();
|
|
74
|
+
const dims = r.u32();
|
|
75
|
+
const scale = r.f64();
|
|
76
|
+
const state = createState(readParams(r));
|
|
77
|
+
Object.assign(state, { fingerprint, dims, scale });
|
|
78
|
+
|
|
79
|
+
const docCount = r.u32();
|
|
80
|
+
for (let i = 0; i < docCount; i++) {
|
|
81
|
+
const doc: DocEntry = { id: r.str(), text: r.str(), hash: r.str(), keys: [] };
|
|
82
|
+
const keyCount = r.u32();
|
|
83
|
+
for (let k = 0; k < keyCount; k++) doc.keys.push(r.str());
|
|
84
|
+
if (r.u8() === 1) doc.vec = r.f32s();
|
|
85
|
+
state.docs.set(doc.id, doc);
|
|
86
|
+
indexKeys(state, doc, true);
|
|
87
|
+
state.lexical.set(doc.id, doc.text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (r.u8() === 1) {
|
|
91
|
+
const n = r.u32();
|
|
92
|
+
const ids: string[] = [];
|
|
93
|
+
const hashes: string[] = [];
|
|
94
|
+
for (let i = 0; i < n; i++) {
|
|
95
|
+
ids.push(r.str());
|
|
96
|
+
hashes.push(r.str());
|
|
97
|
+
}
|
|
98
|
+
const hubs = r.bytes();
|
|
99
|
+
const present = r.bytes();
|
|
100
|
+
const codes: Compacted['codes'] = Array.from(present, (p) => {
|
|
101
|
+
if (p !== 1) return undefined;
|
|
102
|
+
const raw = r.bytes();
|
|
103
|
+
return new Int8Array(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
104
|
+
});
|
|
105
|
+
const entry = r.i32();
|
|
106
|
+
const layerCount = r.u32();
|
|
107
|
+
const layers: CsrLayer[] = [];
|
|
108
|
+
for (let l = 0; l < layerCount; l++) {
|
|
109
|
+
const nodes = r.u32s();
|
|
110
|
+
const offsets = r.u32s();
|
|
111
|
+
const neighbours = r.u32s();
|
|
112
|
+
layers.push(l === 0 ? { offsets, neighbours } : { nodes, offsets, neighbours });
|
|
113
|
+
}
|
|
114
|
+
state.compacted = {
|
|
115
|
+
ids,
|
|
116
|
+
hashes,
|
|
117
|
+
codes,
|
|
118
|
+
hubs,
|
|
119
|
+
graph: { entry, layers },
|
|
120
|
+
nodeOf: new Map(ids.map((id, i) => [id, i])),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return state;
|
|
124
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sha256.ts — a synchronous, pure-TypeScript SHA-256 (FIPS 180-4).
|
|
3
|
+
*
|
|
4
|
+
* The knowledge index names its state with a digest that must be identical on
|
|
5
|
+
* every host. WebCrypto is async and `node:crypto` is Node-only, so this is the
|
|
6
|
+
* one portable, synchronous hasher in the package: it runs in a Worker, a browser
|
|
7
|
+
* and Node, and it is incremental so a large state is hashed without first
|
|
8
|
+
* concatenating it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const K = new Uint32Array([
|
|
12
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
13
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
14
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
15
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
16
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
17
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
18
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
19
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const encoder = new TextEncoder();
|
|
23
|
+
|
|
24
|
+
/** Incremental SHA-256: `update(...)` any number of times, then `digest()` once. */
|
|
25
|
+
export class Sha256 {
|
|
26
|
+
private readonly _h = new Uint32Array([
|
|
27
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
28
|
+
]);
|
|
29
|
+
private readonly _block = new Uint8Array(64);
|
|
30
|
+
private readonly _w = new Uint32Array(64);
|
|
31
|
+
private _blockLen = 0;
|
|
32
|
+
private _bytes = 0;
|
|
33
|
+
private _done = false;
|
|
34
|
+
|
|
35
|
+
/** Feeds bytes, or a string as UTF-8. */
|
|
36
|
+
update(data: Uint8Array | string): this {
|
|
37
|
+
if (this._done) throw new Error('Sha256: update after digest');
|
|
38
|
+
const bytes = typeof data === 'string' ? encoder.encode(data) : data;
|
|
39
|
+
this._bytes += bytes.length;
|
|
40
|
+
let i = 0;
|
|
41
|
+
while (i < bytes.length) {
|
|
42
|
+
const take = Math.min(64 - this._blockLen, bytes.length - i);
|
|
43
|
+
this._block.set(bytes.subarray(i, i + take), this._blockLen);
|
|
44
|
+
this._blockLen += take;
|
|
45
|
+
i += take;
|
|
46
|
+
if (this._blockLen === 64) {
|
|
47
|
+
this._compress();
|
|
48
|
+
this._blockLen = 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The 32-byte digest. The hasher is finished afterwards. */
|
|
55
|
+
digest(): Uint8Array {
|
|
56
|
+
if (this._done) throw new Error('Sha256: digest called twice');
|
|
57
|
+
const bitsHi = Math.floor(this._bytes / 0x20000000);
|
|
58
|
+
const bitsLo = (this._bytes * 8) >>> 0;
|
|
59
|
+
this.update(new Uint8Array([0x80]));
|
|
60
|
+
while (this._blockLen !== 56) this.update(new Uint8Array(1));
|
|
61
|
+
const len = new Uint8Array(8);
|
|
62
|
+
const view = new DataView(len.buffer);
|
|
63
|
+
view.setUint32(0, bitsHi);
|
|
64
|
+
view.setUint32(4, bitsLo);
|
|
65
|
+
this.update(len);
|
|
66
|
+
this._done = true;
|
|
67
|
+
const out = new Uint8Array(32);
|
|
68
|
+
const outView = new DataView(out.buffer);
|
|
69
|
+
for (let j = 0; j < 8; j++) outView.setUint32(j * 4, this._h[j]!);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The digest as lowercase hex. */
|
|
74
|
+
hex(): string {
|
|
75
|
+
return toHex(this.digest());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private _compress(): void {
|
|
79
|
+
const w = this._w;
|
|
80
|
+
const b = this._block;
|
|
81
|
+
for (let t = 0; t < 16; t++) {
|
|
82
|
+
w[t] = ((b[t * 4]! << 24) | (b[t * 4 + 1]! << 16) | (b[t * 4 + 2]! << 8) | b[t * 4 + 3]!) >>> 0;
|
|
83
|
+
}
|
|
84
|
+
for (let t = 16; t < 64; t++) {
|
|
85
|
+
const x = w[t - 15]!;
|
|
86
|
+
const y = w[t - 2]!;
|
|
87
|
+
const s0 = ((x >>> 7) | (x << 25)) ^ ((x >>> 18) | (x << 14)) ^ (x >>> 3);
|
|
88
|
+
const s1 = ((y >>> 17) | (y << 15)) ^ ((y >>> 19) | (y << 13)) ^ (y >>> 10);
|
|
89
|
+
w[t] = (w[t - 16]! + s0 + w[t - 7]! + s1) >>> 0;
|
|
90
|
+
}
|
|
91
|
+
const h = this._h;
|
|
92
|
+
let a = h[0]!, bb = h[1]!, c = h[2]!, d = h[3]!, e = h[4]!, f = h[5]!, g = h[6]!, hh = h[7]!;
|
|
93
|
+
for (let t = 0; t < 64; t++) {
|
|
94
|
+
const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
|
|
95
|
+
const ch = (e & f) ^ (~e & g);
|
|
96
|
+
const t1 = (hh + S1 + ch + K[t]! + w[t]!) >>> 0;
|
|
97
|
+
const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
|
|
98
|
+
const maj = (a & bb) ^ (a & c) ^ (bb & c);
|
|
99
|
+
const t2 = (S0 + maj) >>> 0;
|
|
100
|
+
hh = g; g = f; f = e; e = (d + t1) >>> 0;
|
|
101
|
+
d = c; c = bb; bb = a; a = (t1 + t2) >>> 0;
|
|
102
|
+
}
|
|
103
|
+
h[0] = (h[0]! + a) >>> 0; h[1] = (h[1]! + bb) >>> 0; h[2] = (h[2]! + c) >>> 0; h[3] = (h[3]! + d) >>> 0;
|
|
104
|
+
h[4] = (h[4]! + e) >>> 0; h[5] = (h[5]! + f) >>> 0; h[6] = (h[6]! + g) >>> 0; h[7] = (h[7]! + hh) >>> 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function toHex(bytes: Uint8Array): string {
|
|
109
|
+
let s = '';
|
|
110
|
+
for (let i = 0; i < bytes.length; i++) s += bytes[i]!.toString(16).padStart(2, '0');
|
|
111
|
+
return s;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** SHA-256 of a string (UTF-8) or bytes. */
|
|
115
|
+
export function sha256(data: Uint8Array | string): Uint8Array {
|
|
116
|
+
return new Sha256().update(data).digest();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** SHA-256 hex of a string (UTF-8) or bytes. */
|
|
120
|
+
export function sha256Hex(data: Uint8Array | string): string {
|
|
121
|
+
return new Sha256().update(data).hex();
|
|
122
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state.ts — the knowledge index's in-memory state, shared by the cascade,
|
|
3
|
+
* compaction, digest and serializer. Everything a result depends on lives here,
|
|
4
|
+
* so `digest()` can name it and `serialize()` can carry it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { EmbedderPort, IndexParams, IndexTier } from './types.js';
|
|
8
|
+
import type { CsrGraph } from './csr.js';
|
|
9
|
+
import { LexicalIndex } from './lexical.js';
|
|
10
|
+
import { foldKey } from './tokenize.js';
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_INDEX_PARAMS: Readonly<IndexParams> = Object.freeze({
|
|
13
|
+
m: 16,
|
|
14
|
+
efConstruction: 200,
|
|
15
|
+
efSearch: 64,
|
|
16
|
+
hubShare: 0.02,
|
|
17
|
+
rerankFactor: 4,
|
|
18
|
+
lean: false,
|
|
19
|
+
rrfK: 60,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** One live card. `vec` is its unit float embedding while it is not yet in the graph. */
|
|
23
|
+
export interface DocEntry {
|
|
24
|
+
id: string;
|
|
25
|
+
text: string;
|
|
26
|
+
/** Case-folded, de-duplicated, sorted. */
|
|
27
|
+
keys: string[];
|
|
28
|
+
hash: string;
|
|
29
|
+
vec?: Float32Array;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The canonical, id-ordered snapshot written by `compact()`. Node index = rank of its id. */
|
|
33
|
+
export interface Compacted {
|
|
34
|
+
ids: string[];
|
|
35
|
+
/** The doc hash each node was coded from; a node whose doc changed is stale. */
|
|
36
|
+
hashes: string[];
|
|
37
|
+
/** int8 codes by node; `undefined` where lean mode dropped a non-hub's code. */
|
|
38
|
+
codes: Array<Int8Array | undefined>;
|
|
39
|
+
/** 1 for a hub (top `hubShare` by in-degree). */
|
|
40
|
+
hubs: Uint8Array;
|
|
41
|
+
graph: CsrGraph;
|
|
42
|
+
nodeOf: Map<string, number>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface IndexState {
|
|
46
|
+
params: IndexParams;
|
|
47
|
+
/** Fingerprint of every stored vector and code; '' when none was ever embedded. */
|
|
48
|
+
fingerprint: string;
|
|
49
|
+
dims: number;
|
|
50
|
+
/** The int8 scale, fixed at the first compaction; 0 until then. */
|
|
51
|
+
scale: number;
|
|
52
|
+
docs: Map<string, DocEntry>;
|
|
53
|
+
/** Folded key → ids. */
|
|
54
|
+
keys: Map<string, Set<string>>;
|
|
55
|
+
lexical: LexicalIndex;
|
|
56
|
+
compacted: Compacted | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createState(params: Partial<IndexParams> | undefined, embedder?: EmbedderPort): IndexState {
|
|
60
|
+
return {
|
|
61
|
+
params: { ...DEFAULT_INDEX_PARAMS, ...params },
|
|
62
|
+
fingerprint: embedder?.fingerprint ?? '',
|
|
63
|
+
dims: embedder?.dims ?? 0,
|
|
64
|
+
scale: 0,
|
|
65
|
+
docs: new Map(),
|
|
66
|
+
keys: new Map(),
|
|
67
|
+
lexical: new LexicalIndex(),
|
|
68
|
+
compacted: null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Folded, unique, sorted keys. */
|
|
73
|
+
export function canonicalKeys(keys: readonly string[] | undefined): string[] {
|
|
74
|
+
return [...new Set((keys ?? []).map(foldKey).filter(Boolean))].sort();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Adds (`on`) or removes a doc's keys from the exact-match map. */
|
|
78
|
+
export function indexKeys(state: IndexState, doc: DocEntry, on: boolean): void {
|
|
79
|
+
for (const key of doc.keys) {
|
|
80
|
+
let ids = state.keys.get(key);
|
|
81
|
+
if (on) {
|
|
82
|
+
if (!ids) state.keys.set(key, (ids = new Set()));
|
|
83
|
+
ids.add(doc.id);
|
|
84
|
+
} else if (ids) {
|
|
85
|
+
ids.delete(doc.id);
|
|
86
|
+
if (ids.size === 0) state.keys.delete(key);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The node index of `id` when its doc is live in the graph at the coded hash, else -1. */
|
|
92
|
+
export function graphNode(state: IndexState, id: string): number {
|
|
93
|
+
const c = state.compacted;
|
|
94
|
+
if (!c) return -1;
|
|
95
|
+
const node = c.nodeOf.get(id);
|
|
96
|
+
if (node === undefined) return -1;
|
|
97
|
+
return state.docs.get(id)?.hash === c.hashes[node] ? node : -1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Whether `embedder` may produce vectors comparable with the stored ones. */
|
|
101
|
+
export function denseReady(state: IndexState, embedder: EmbedderPort | undefined): embedder is EmbedderPort {
|
|
102
|
+
return !!embedder && embedder.fingerprint === state.fingerprint && embedder.dims === state.dims;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The tiers a search can run right now. */
|
|
106
|
+
export function availableTiers(state: IndexState, embedder: EmbedderPort | undefined): IndexTier[] {
|
|
107
|
+
return denseReady(state, embedder) ? ['exact', 'lexical', 'dense'] : ['exact', 'lexical'];
|
|
108
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tokenize.ts — the code-aware tokenizer the knowledge index's lexical tier uses.
|
|
3
|
+
*
|
|
4
|
+
* Cards name things the way code does: `retryPolicy.ts`, `stripe_webhook`,
|
|
5
|
+
* `api/src/billing/invoice.ts`. The package's plain `tokenize` keeps
|
|
6
|
+
* `retrypolicy` whole, so "retry policy" never matches it. This splits paths on
|
|
7
|
+
* separators, then each word on snake_case and camelCase boundaries, and emits
|
|
8
|
+
* the parts AND the whole compound — an exact identifier still scores higher.
|
|
9
|
+
*
|
|
10
|
+
* Lowercasing is `toLowerCase` (locale-free), so the tokens are the same on every
|
|
11
|
+
* host. Pure and zero-dependency.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** camelCase / PascalCase / acronym boundaries: `parseHTTPResponse` → parse, HTTP, Response. */
|
|
15
|
+
function splitCamel(word: string): string[] {
|
|
16
|
+
return word
|
|
17
|
+
.replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, '$1 $2')
|
|
18
|
+
.replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, '$1 $2')
|
|
19
|
+
.split(' ');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Splits `text` into lowercase terms: every word (a run of letters, digits and `_`)
|
|
24
|
+
* yields its snake/camel parts, plus the whole word when it has more than one part.
|
|
25
|
+
*/
|
|
26
|
+
export function codeTokenize(text: string): string[] {
|
|
27
|
+
const out: string[] = [];
|
|
28
|
+
for (const word of text.split(/[^\p{L}\p{N}_]+/u)) {
|
|
29
|
+
if (!word) continue;
|
|
30
|
+
const parts = word.split('_').flatMap(splitCamel).filter(Boolean).map((p) => p.toLowerCase());
|
|
31
|
+
if (parts.length === 0) continue;
|
|
32
|
+
out.push(...parts);
|
|
33
|
+
if (parts.length > 1) out.push(word.toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Case-folds an exact-match key or query the same way on insert and on lookup. */
|
|
39
|
+
export function foldKey(key: string): string {
|
|
40
|
+
return key.trim().toLowerCase();
|
|
41
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* traverse.ts — HNSW search over the CSR graph with integer scores.
|
|
3
|
+
*
|
|
4
|
+
* Greedy descent through the upper layers, then a bounded best-first beam on the
|
|
5
|
+
* base layer. Scores are int8 dot products (exact integers), and equal scores
|
|
6
|
+
* break on node index (= id order), so the candidates are bit-identical on every
|
|
7
|
+
* platform for the same codes and query code.
|
|
8
|
+
*
|
|
9
|
+
* Lean mode stores no code for most nodes; `ensure` is called with the nodes about
|
|
10
|
+
* to be scored so the caller can recompute their codes first. It is awaited only
|
|
11
|
+
* when it returns a promise, so the non-lean walk never yields.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { neighboursOf, type CsrGraph } from './csr.js';
|
|
15
|
+
|
|
16
|
+
export interface GraphScorer {
|
|
17
|
+
/** The node's score against the query; higher is nearer. */
|
|
18
|
+
score(node: number): number;
|
|
19
|
+
/** Make codes available for `nodes` before they are scored (lean mode). */
|
|
20
|
+
ensure?(nodes: number[]): Promise<void> | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface GraphCandidate {
|
|
24
|
+
node: number;
|
|
25
|
+
score: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Higher score first, then lower node index. */
|
|
29
|
+
function before(a: GraphCandidate, b: GraphCandidate): boolean {
|
|
30
|
+
return a.score > b.score || (a.score === b.score && a.node < b.node);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Inserts `c` into `list`, kept in {@link before} order. */
|
|
34
|
+
function insertOrdered(list: GraphCandidate[], c: GraphCandidate): void {
|
|
35
|
+
let lo = 0;
|
|
36
|
+
let hi = list.length;
|
|
37
|
+
while (lo < hi) {
|
|
38
|
+
const mid = (lo + hi) >>> 1;
|
|
39
|
+
if (before(list[mid]!, c)) lo = mid + 1;
|
|
40
|
+
else hi = mid;
|
|
41
|
+
}
|
|
42
|
+
list.splice(lo, 0, c);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The scorer's `ensure`, or `undefined` when there is nothing to wait for. */
|
|
46
|
+
function ensure(scorer: GraphScorer, nodes: number[]): Promise<void> | undefined {
|
|
47
|
+
if (!scorer.ensure || nodes.length === 0) return undefined;
|
|
48
|
+
return scorer.ensure(nodes);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The best `ef` nodes for the query, in {@link before} order. */
|
|
52
|
+
export async function searchGraph(graph: CsrGraph, scorer: GraphScorer, ef: number): Promise<GraphCandidate[]> {
|
|
53
|
+
if (graph.entry < 0 || graph.layers.length === 0 || ef <= 0) return [];
|
|
54
|
+
{ const p = ensure(scorer, [graph.entry]); if (p) await p; }
|
|
55
|
+
let ep: GraphCandidate = { node: graph.entry, score: scorer.score(graph.entry) };
|
|
56
|
+
|
|
57
|
+
for (let l = graph.layers.length - 1; l > 0; l--) {
|
|
58
|
+
let improved = true;
|
|
59
|
+
while (improved) {
|
|
60
|
+
improved = false;
|
|
61
|
+
const nbs = Array.from(neighboursOf(graph.layers[l]!, ep.node));
|
|
62
|
+
const p = ensure(scorer, nbs);
|
|
63
|
+
if (p) await p;
|
|
64
|
+
for (const node of nbs) {
|
|
65
|
+
const c = { node, score: scorer.score(node) };
|
|
66
|
+
if (before(c, ep)) {
|
|
67
|
+
ep = c;
|
|
68
|
+
improved = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const base = graph.layers[0]!;
|
|
75
|
+
const visited = new Set<number>([ep.node]);
|
|
76
|
+
const frontier: GraphCandidate[] = [ep];
|
|
77
|
+
const results: GraphCandidate[] = [ep];
|
|
78
|
+
while (frontier.length > 0) {
|
|
79
|
+
const cur = frontier.shift()!;
|
|
80
|
+
if (results.length >= ef && before(results[results.length - 1]!, cur)) break;
|
|
81
|
+
const fresh = Array.from(neighboursOf(base, cur.node)).filter((node) => !visited.has(node));
|
|
82
|
+
for (const node of fresh) visited.add(node);
|
|
83
|
+
const p = ensure(scorer, fresh);
|
|
84
|
+
if (p) await p;
|
|
85
|
+
for (const node of fresh) {
|
|
86
|
+
const c = { node, score: scorer.score(node) };
|
|
87
|
+
if (results.length < ef || before(c, results[results.length - 1]!)) {
|
|
88
|
+
insertOrdered(frontier, c);
|
|
89
|
+
insertOrdered(results, c);
|
|
90
|
+
if (results.length > ef) results.pop();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The knowledge index contract — a deterministic, LEANN-style index over knowledge-graph
|
|
3
|
+
* cards (design: Builderforce.ai/docs/design/deterministic-knowledge-index.md).
|
|
4
|
+
*
|
|
5
|
+
* Every result is a pure function of `(digest, query, opts)`: one total order
|
|
6
|
+
* (`compareHits`), an insertion-order-free graph, integer (int8) traversal similarity.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** One card. `id` is stable (a knowledge-graph NodeRef); `hash` defaults to a hash of `text`. */
|
|
10
|
+
export interface IndexDoc {
|
|
11
|
+
id: string;
|
|
12
|
+
text: string;
|
|
13
|
+
/** Exact-match keys (path, basename, symbol, heading slug, entity name) — case-folded on insert. */
|
|
14
|
+
keys?: string[];
|
|
15
|
+
hash?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The embedder seam. Vectors from two fingerprints are never compared. */
|
|
19
|
+
export interface EmbedderPort {
|
|
20
|
+
/** model id + version + dims. */
|
|
21
|
+
fingerprint: string;
|
|
22
|
+
dims: number;
|
|
23
|
+
embed(texts: string[]): Promise<Float32Array[]>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type IndexTier = 'exact' | 'lexical' | 'dense';
|
|
27
|
+
|
|
28
|
+
export interface IndexHit {
|
|
29
|
+
id: string;
|
|
30
|
+
score: number;
|
|
31
|
+
tier: IndexTier;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Neighbours of a node, supplied by the knowledge graph for recall expansion. */
|
|
35
|
+
export interface GraphNeighbours {
|
|
36
|
+
neighbours(id: string, edgeKinds?: readonly string[]): ReadonlyArray<{ id: string; kind: string; weight: number }>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IndexParams {
|
|
40
|
+
/** HNSW max neighbours for hubs (non-hubs keep m/2 after pruning). Default 16. */
|
|
41
|
+
m: number;
|
|
42
|
+
efConstruction: number;
|
|
43
|
+
efSearch: number;
|
|
44
|
+
/** Share of nodes (by in-degree) that keep full degree. Default 0.02. */
|
|
45
|
+
hubShare: number;
|
|
46
|
+
/** Dense candidates = k * rerankFactor, recomputed in float and reranked. Default 4. */
|
|
47
|
+
rerankFactor: number;
|
|
48
|
+
/** Drop int8 codes of non-hub nodes and recompute on the search path. Default false. */
|
|
49
|
+
lean: boolean;
|
|
50
|
+
/** RRF constant. Default 60. */
|
|
51
|
+
rrfK: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SimilarPair {
|
|
55
|
+
a: string;
|
|
56
|
+
b: string;
|
|
57
|
+
similarity: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface KnowledgeIndex {
|
|
61
|
+
/** Inserts or replaces; skips docs whose hash is unchanged. */
|
|
62
|
+
upsert(docs: IndexDoc[]): Promise<void>;
|
|
63
|
+
remove(ids: string[]): void;
|
|
64
|
+
/** Cascade: exact → lexical → dense (when an embedder of the stored fingerprint is present) → RRF → compareHits. */
|
|
65
|
+
search(q: string, opts?: { k?: number; tiers?: IndexTier[] }): Promise<IndexHit[]>;
|
|
66
|
+
/** Adds graph neighbours (score × weight × 0.5 per hop), re-ordered by compareHits. */
|
|
67
|
+
expand(hits: IndexHit[], graph: GraphNeighbours, opts?: { hops?: 1 | 2; edgeKinds?: string[] }): IndexHit[];
|
|
68
|
+
/** Deterministic near-duplicate pairs over int8 codes; `idPrefix` filters (e.g. `src:`). */
|
|
69
|
+
similarPairs(opts: { minSimilarity: number; maxPairs: number; idPrefix?: string }): SimilarPair[];
|
|
70
|
+
/** Canonical rebuild (ids sorted) — the same doc set + fingerprint ⇒ the same bytes. */
|
|
71
|
+
compact(): Promise<void>;
|
|
72
|
+
/** sha-256 hex over the canonical state. */
|
|
73
|
+
digest(): string;
|
|
74
|
+
/** Tiers available right now (dense only with a matching embedder). */
|
|
75
|
+
tiers(): IndexTier[];
|
|
76
|
+
size(): number;
|
|
77
|
+
serialize(): Uint8Array;
|
|
78
|
+
}
|