Skip to content

Commit 7417d27

Browse files
committed
Phase 1: OPFSVectorStore, MemoryVectorStore, IndexedDbMetadataStore + persistence tests
- core/types.ts: canonical entity types (Page, Book, Volume, Shelf, Edge, MetroidNeighbor, MetroidSubgraph) and VectorStore / MetadataStore interfaces - storage/OPFSVectorStore.ts: append-only OPFS binary vector file with serialised write queue and byte-offset semantics - storage/MemoryVectorStore.ts: in-memory VectorStore (identical semantics, used in tests and as a safe fallback) - storage/IndexedDbMetadataStore.ts: full MetadataStore backed by IndexedDB – CRUD for all entities, reverse-index maintenance (page→book, book→volume, volume→shelf), Metroid NN neighbour index with bounded BFS subgraph expansion, and dirty-volume recalc flags - tests/Persistence.test.ts: 34 round-trip tests covering VectorStore contract via MemoryVectorStore, OPFSVectorStore with stubbed OPFS, and IndexedDbMetadataStore via fake-indexeddb - tsconfig.json: extend include to cover core/** and storage/** - package.json: add fake-indexeddb devDependency - CORTEX-DESIGN-PLAN-TODO.md: mark completed P1 items
1 parent f194cdf commit 7417d27

22 files changed

Lines changed: 4545 additions & 82 deletions

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
15+
- name: Setup Node
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: "20"
19+
cache: "npm"
20+
21+
- name: Install dependencies
22+
run: npm ci
23+
24+
- name: Lint
25+
run: npm run lint
26+
27+
- name: Typecheck
28+
run: npm run build
29+
30+
- name: Test
31+
run: npm test

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
dist/
3+
coverage/
4+
*.log

BackendKind.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export type BackendKind = "webnn" | "webgpu" | "webgl" | "wasm";
2+
3+
function hasWebGpuSupport(): boolean {
4+
return (
5+
typeof navigator !== "undefined" &&
6+
typeof (navigator as Navigator & { gpu?: unknown }).gpu !== "undefined"
7+
);
8+
}
9+
10+
function hasWebGl2Support(): boolean {
11+
if (typeof document === "undefined") {
12+
return false;
13+
}
14+
15+
const canvas = document.createElement("canvas");
16+
return canvas.getContext("webgl2") !== null;
17+
}
18+
19+
function hasWebNnSupport(): boolean {
20+
return (
21+
typeof navigator !== "undefined" &&
22+
typeof (navigator as Navigator & { ml?: unknown }).ml !== "undefined"
23+
);
24+
}
25+
26+
export function detectBackend(): BackendKind {
27+
if (hasWebGpuSupport()) {
28+
return "webgpu";
29+
}
30+
31+
if (hasWebGl2Support()) {
32+
return "webgl";
33+
}
34+
35+
if (hasWebNnSupport()) {
36+
return "webnn";
37+
}
38+
39+
return "wasm";
40+
}

CORTEX-DESIGN-PLAN-TODO.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -254,17 +254,17 @@ Priority legend:
254254
10. Add smoke test that instantiates each backend or cleanly falls back.
255255

256256
### P1 (v1 core)
257-
1. Implement `OPFSVectorStore.appendVector` and `readVector`.
258-
2. Implement `IndexedDbMetadataStore` entity CRUD methods.
259-
3. Implement metadata helper indexes (`page->book`, `book->volume`, `volume->shelf`).
260-
4. Implement `putMetroidNeighbors` and `getMetroidNeighbors`.
261-
5. Implement `getInducedMetroidSubgraph` with bounded BFS.
257+
1. ~~Implement `OPFSVectorStore.appendVector` and `readVector`.~~ ✅ Done (Phase 1, 2026-03-11)
258+
2. ~~Implement `IndexedDbMetadataStore` entity CRUD methods.~~ ✅ Done (Phase 1, 2026-03-11)
259+
3. ~~Implement metadata helper indexes (`page->book`, `book->volume`, `volume->shelf`).~~ ✅ Done (Phase 1, 2026-03-11)
260+
4. ~~Implement `putMetroidNeighbors` and `getMetroidNeighbors`.~~ ✅ Done (Phase 1, 2026-03-11)
261+
5. ~~Implement `getInducedMetroidSubgraph` with bounded BFS.~~ ✅ Done (Phase 1, 2026-03-11)
262262
6. Implement page chunking utility and deterministic page ID generation.
263263
7. Implement page signature creation and verification helpers.
264264
8. Implement Hippocampus ingest orchestration.
265265
9. Implement fast Metroid insert path for newly ingested pages.
266266
10. Implement reverse neighbor update path with bounded degree.
267-
11. Implement dirty-volume recalc flagging in metadata.
267+
11. ~~Implement dirty-volume recalc flagging in metadata.~~ ✅ Done (Phase 1, 2026-03-11)
268268
12. Implement Shelf/Volume/Book/Page ranking in Cortex query.
269269
13. Implement query seed selection threshold logic.
270270
14. Implement `findOpenTSPPath` with dummy-node open-path strategy.
@@ -275,7 +275,7 @@ Priority legend:
275275
19. Implement full neighborhood recalc for dirty volumes.
276276
20. Implement split/merge hooks for unstable clusters.
277277
21. Add end-to-end test: ingest -> query -> coherent ordered response.
278-
22. Add restart persistence test for vectors + metadata.
278+
22. ~~Add restart persistence test for vectors + metadata.~~ ✅ Done (Phase 1, 2026-03-11)
279279
23. Add regression tests for neighbor symmetry and bounded degree.
280280
24. Add benchmark harness and first baseline capture.
281281

CreateVectorBackend.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import { detectBackend } from "./BackendKind";
2+
import type { VectorBackend } from "./VectorBackend";
3+
import { WasmVectorBackend } from "./WasmVectorBackend";
4+
import { WebGlVectorBackend } from "./WebGLVectorBackend";
5+
import { WebGpuVectorBackend } from "./WebGPUVectorBackend";
6+
import { WebNnVectorBackend } from "./WebNNVectorBackend";
7+
18
export async function createVectorBackend(
29
wasmBytes: ArrayBuffer
310
): Promise<VectorBackend> {

TopK.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { DistanceResult, ScoreResult } from "./VectorBackend";
2+
3+
export function topKByScore(scores: Float32Array, k: number): ScoreResult[] {
4+
const limit = Math.max(0, Math.min(k, scores.length));
5+
const indices = Array.from({ length: scores.length }, (_, i) => i);
6+
7+
indices.sort((a, b) => scores[b] - scores[a]);
8+
9+
return indices.slice(0, limit).map((index) => ({
10+
index,
11+
score: scores[index]
12+
}));
13+
}
14+
15+
export function topKByDistance(
16+
distances: Uint32Array | Int32Array | Float32Array,
17+
k: number
18+
): DistanceResult[] {
19+
const limit = Math.max(0, Math.min(k, distances.length));
20+
const indices = Array.from({ length: distances.length }, (_, i) => i);
21+
22+
indices.sort((a, b) => Number(distances[a]) - Number(distances[b]));
23+
24+
return indices.slice(0, limit).map((index) => ({
25+
index,
26+
distance: Number(distances[index])
27+
}));
28+
}

VectorBackend.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,49 @@
1-
// TS side
1+
import type { BackendKind } from "./BackendKind";
2+
3+
export interface ScoreResult {
4+
index: number;
5+
score: number;
6+
}
7+
8+
export interface DistanceResult {
9+
index: number;
10+
distance: number;
11+
}
12+
213
export interface VectorBackend {
3-
kind: BackendKind; // "wasm" | "webgl" | "webgpu" | "webnn"
14+
kind: BackendKind;
415

5-
// ---- float space (exact or high-precision cosine) ----
16+
// Exact or high-precision dot-product scoring over row-major matrices.
617
dotMany(
7-
query: Float32Array, // length = dim
8-
matrix: Float32Array, // length = dim * count, row-major
18+
query: Float32Array,
19+
matrix: Float32Array,
920
dim: number,
1021
count: number
11-
): Promise<Float32Array>; // scores[length = count]
22+
): Promise<Float32Array>;
1223

13-
topKFromScores(
14-
scores: Float32Array,
15-
k: number
16-
): Promise<{ index: number; score: number }[]>;
24+
// Projection helper used to reduce dimensionality for routing tiers.
25+
project(
26+
vector: Float32Array,
27+
projectionMatrix: Float32Array,
28+
dimIn: number,
29+
dimOut: number
30+
): Promise<Float32Array>;
31+
32+
topKFromScores(scores: Float32Array, k: number): Promise<ScoreResult[]>;
1733

18-
// ---- binary space (approximate, XOR / popcnt) ----
34+
// Random-hyperplane hash from projected vectors into packed binary codes.
1935
hashToBinary(
20-
vector: Float32Array, // original embedding (e.g. 768-d)
21-
dim: number, // same as vector.length
22-
bits: number // e.g. 64, 128
23-
): Promise<Uint32Array>; // packed bits (words = ceil(bits/32))
36+
vector: Float32Array,
37+
projectionMatrix: Float32Array,
38+
dimIn: number,
39+
bits: number
40+
): Promise<Uint32Array>;
2441

2542
hammingTopK(
26-
queryCode: Uint32Array, // packed bits
27-
codes: Uint32Array, // concatenated codes for N items
43+
queryCode: Uint32Array,
44+
codes: Uint32Array,
2845
wordsPerCode: number,
29-
count: number, // N
46+
count: number,
3047
k: number
31-
): Promise<{ index: number; distance: number }[]>;
48+
): Promise<DistanceResult[]>;
3249
}

WasmVectorBackend.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
1+
import type {
2+
DistanceResult,
3+
ScoreResult,
4+
VectorBackend
5+
} from "./VectorBackend";
6+
7+
interface WasmVectorExports {
8+
mem: WebAssembly.Memory;
9+
dot_many(qPtr: number, mPtr: number, outPtr: number, dim: number, count: number): void;
10+
project(vecPtr: number, pPtr: number, outPtr: number, dimIn: number, dimOut: number): void;
11+
hash_binary(vecPtr: number, pPtr: number, codePtr: number, dimIn: number, bits: number): void;
12+
hamming_scores(
13+
queryCodePtr: number,
14+
codesPtr: number,
15+
outPtr: number,
16+
wordsPerCode: number,
17+
count: number
18+
): void;
19+
topk_i32(scoresPtr: number, outPtr: number, count: number, k: number): void;
20+
topk_f32(scoresPtr: number, outPtr: number, count: number, k: number): void;
21+
}
22+
123
export class WasmVectorBackend implements VectorBackend {
224
readonly kind = "wasm" as const;
3-
private exports!: Record<string, Function>;
25+
private exports!: WasmVectorExports;
426
private mem!: WebAssembly.Memory;
527
private bump = 1024; // first 1KB reserved as guard
628

729
static async create(wasmBytes: ArrayBuffer): Promise<WasmVectorBackend> {
830
const b = new WasmVectorBackend();
931
const { instance } = await WebAssembly.instantiate(wasmBytes);
10-
b.exports = instance.exports as Record<string, Function>;
32+
b.exports = instance.exports as unknown as WasmVectorExports;
1133
b.mem = instance.exports.mem as WebAssembly.Memory;
1234
return b;
1335
}
@@ -49,25 +71,27 @@ export class WasmVectorBackend implements VectorBackend {
4971
}
5072

5173
async project(
52-
vec: Float32Array, P: Float32Array,
74+
vec: Float32Array,
75+
projectionMatrix: Float32Array,
5376
dimIn: number, dimOut: number
5477
): Promise<Float32Array> {
5578
this.reset();
5679
const v_ptr = this.writeF32(vec);
57-
const P_ptr = this.writeF32(P);
80+
const P_ptr = this.writeF32(projectionMatrix);
5881
const out_ptr = this.alloc(dimOut * 4);
5982
this.exports.project(v_ptr, P_ptr, out_ptr, dimIn, dimOut);
6083
return new Float32Array(this.mem.buffer.slice(out_ptr, out_ptr + dimOut * 4));
6184
}
6285

6386
async hashToBinary(
64-
vec: Float32Array, P: Float32Array,
87+
vec: Float32Array,
88+
projectionMatrix: Float32Array,
6589
dimIn: number, bits: number
6690
): Promise<Uint32Array> {
6791
this.reset();
6892
const wordsPerCode = Math.ceil(bits / 32);
6993
const v_ptr = this.writeF32(vec);
70-
const P_ptr = this.writeF32(P);
94+
const P_ptr = this.writeF32(projectionMatrix);
7195
const code_ptr = this.alloc(wordsPerCode * 4);
7296
this.exports.hash_binary(v_ptr, P_ptr, code_ptr, dimIn, bits);
7397
return new Uint32Array(this.mem.buffer.slice(code_ptr, code_ptr + wordsPerCode * 4));
@@ -76,7 +100,7 @@ export class WasmVectorBackend implements VectorBackend {
76100
async hammingTopK(
77101
queryCode: Uint32Array, codes: Uint32Array,
78102
wordsPerCode: number, count: number, k: number
79-
): Promise<{ index: number; distance: number }[]> {
103+
): Promise<DistanceResult[]> {
80104
this.reset();
81105
const q_ptr = this.writeU32(queryCode);
82106
const codes_ptr = this.writeU32(codes);
@@ -98,7 +122,7 @@ export class WasmVectorBackend implements VectorBackend {
98122

99123
async topKFromScores(
100124
scores: Float32Array, k: number
101-
): Promise<{ index: number; score: number }[]> {
125+
): Promise<ScoreResult[]> {
102126
this.reset();
103127
// copy: topk_f32 mutates in-place
104128
const copy_ptr = this.writeF32(new Float32Array(scores));

WebGLVectorBackend.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import { topKByDistance, topKByScore } from "./TopK";
2+
import type {
3+
DistanceResult,
4+
ScoreResult,
5+
VectorBackend
6+
} from "./VectorBackend";
7+
18
const VERT_SRC = /* glsl */`#version 300 es
29
out vec2 v_uv;
310
void main() {
@@ -204,12 +211,13 @@ export class WebGlVectorBackend implements VectorBackend {
204211
}
205212

206213
async hashToBinary(
207-
vec: Float32Array, P: Float32Array,
214+
vec: Float32Array,
215+
projectionMatrix: Float32Array,
208216
dimIn: number, bits: number
209217
): Promise<Uint32Array> {
210218
const dimPacked = Math.ceil(dimIn / 4);
211219
const vTex = this.packF32Texture(vec, 1);
212-
const hTex = this.packF32Texture(P, bits);
220+
const hTex = this.packF32Texture(projectionMatrix, bits);
213221

214222
const pixels = this.drawToFramebuffer(this.hashProg, bits, (prog) => {
215223
this.bindTex(prog, "u_vec", vTex, 0);
@@ -239,7 +247,7 @@ export class WebGlVectorBackend implements VectorBackend {
239247
async hammingTopK(
240248
queryCode: Uint32Array, codes: Uint32Array,
241249
wordsPerCode: number, count: number, k: number
242-
): Promise<{ index: number; distance: number }[]> {
250+
): Promise<DistanceResult[]> {
243251
const distances = new Uint32Array(count);
244252
for (let i = 0; i < count; i++) {
245253
let dist = 0;
@@ -253,12 +261,12 @@ export class WebGlVectorBackend implements VectorBackend {
253261
}
254262
distances[i] = dist;
255263
}
256-
return topKCpu(distances, k, false);
264+
return topKByDistance(distances, k);
257265
}
258266

259267
async topKFromScores(
260268
scores: Float32Array, k: number
261-
): Promise<{ index: number; score: number }[]> {
262-
return topKCpu(scores, k, true);
269+
): Promise<ScoreResult[]> {
270+
return topKByScore(scores, k);
263271
}
264272
}

0 commit comments

Comments
 (0)