@soulcraft/cor 3.0.0-rc.1

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 (87) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +328 -0
  3. package/dist/aggregation/NativeAggregationEngine.d.ts +118 -0
  4. package/dist/aggregation/NativeAggregationEngine.js +186 -0
  5. package/dist/cli.d.ts +12 -0
  6. package/dist/cli.js +236 -0
  7. package/dist/graph/GraphAccelerationAdapter.d.ts +153 -0
  8. package/dist/graph/GraphAccelerationAdapter.js +326 -0
  9. package/dist/graph/NativeGraphAdjacencyIndex.d.ts +286 -0
  10. package/dist/graph/NativeGraphAdjacencyIndex.js +944 -0
  11. package/dist/hnsw/AdaptiveDiskAnnModeSelector.d.ts +187 -0
  12. package/dist/hnsw/AdaptiveDiskAnnModeSelector.js +313 -0
  13. package/dist/hnsw/NativeDiskAnnWrapper.d.ts +489 -0
  14. package/dist/hnsw/NativeDiskAnnWrapper.js +1456 -0
  15. package/dist/index.d.ts +28 -0
  16. package/dist/index.js +29 -0
  17. package/dist/legacyLayoutGuard.d.ts +93 -0
  18. package/dist/legacyLayoutGuard.js +237 -0
  19. package/dist/license/constants.d.ts +30 -0
  20. package/dist/license/constants.js +30 -0
  21. package/dist/license/onlineValidator.d.ts +43 -0
  22. package/dist/license/onlineValidator.js +225 -0
  23. package/dist/license/types.d.ts +39 -0
  24. package/dist/license/types.js +5 -0
  25. package/dist/license.d.ts +64 -0
  26. package/dist/license.js +165 -0
  27. package/dist/migration/MigrationCoordinator.d.ts +171 -0
  28. package/dist/migration/MigrationCoordinator.js +249 -0
  29. package/dist/migration/indexEpochGuard.d.ts +163 -0
  30. package/dist/migration/indexEpochGuard.js +202 -0
  31. package/dist/native/NativeEmbeddingEngine.d.ts +79 -0
  32. package/dist/native/NativeEmbeddingEngine.js +318 -0
  33. package/dist/native/NativeRoaringBitmap32.d.ts +114 -0
  34. package/dist/native/NativeRoaringBitmap32.js +221 -0
  35. package/dist/native/ffi.d.ts +20 -0
  36. package/dist/native/ffi.js +48 -0
  37. package/dist/native/idMapperTestSupport.d.ts +67 -0
  38. package/dist/native/idMapperTestSupport.js +112 -0
  39. package/dist/native/index.d.ts +49 -0
  40. package/dist/native/index.js +87 -0
  41. package/dist/native/napi.d.ts +21 -0
  42. package/dist/native/napi.js +88 -0
  43. package/dist/native/types.d.ts +1298 -0
  44. package/dist/native/types.js +16 -0
  45. package/dist/plugin.d.ts +50 -0
  46. package/dist/plugin.js +388 -0
  47. package/dist/providerContracts.d.ts +277 -0
  48. package/dist/providerContracts.js +38 -0
  49. package/dist/resource/OsMemoryProbe.d.ts +175 -0
  50. package/dist/resource/OsMemoryProbe.js +206 -0
  51. package/dist/resource/ResourceManager.d.ts +491 -0
  52. package/dist/resource/ResourceManager.js +960 -0
  53. package/dist/utils/ColumnManifest.d.ts +97 -0
  54. package/dist/utils/ColumnManifest.js +129 -0
  55. package/dist/utils/NativeColumnStore.d.ts +284 -0
  56. package/dist/utils/NativeColumnStore.js +685 -0
  57. package/dist/utils/NativeMetadataIndex.d.ts +882 -0
  58. package/dist/utils/NativeMetadataIndex.js +2631 -0
  59. package/dist/utils/NativeUnifiedCache.d.ts +87 -0
  60. package/dist/utils/NativeUnifiedCache.js +273 -0
  61. package/dist/utils/binaryIdMapperFactory.d.ts +59 -0
  62. package/dist/utils/binaryIdMapperFactory.js +94 -0
  63. package/dist/utils/collation.d.ts +30 -0
  64. package/dist/utils/collation.js +40 -0
  65. package/dist/utils/columnStoreTypes.d.ts +161 -0
  66. package/dist/utils/columnStoreTypes.js +29 -0
  67. package/dist/utils/nativeBinaryEntityIdMapper.d.ts +211 -0
  68. package/dist/utils/nativeBinaryEntityIdMapper.js +381 -0
  69. package/dist/utils/nativeEntityIdMapper.d.ts +111 -0
  70. package/dist/utils/nativeEntityIdMapper.js +170 -0
  71. package/docs/ADR-002-diskann-100-percent-rust.md +294 -0
  72. package/docs/ADR-003-semantic-time-travel.md +100 -0
  73. package/docs/aggregation.md +251 -0
  74. package/docs/comparison.md +162 -0
  75. package/docs/deployment-limits.md +87 -0
  76. package/docs/diskann.md +184 -0
  77. package/docs/migration-3.0.md +396 -0
  78. package/docs/performance-budget.md +274 -0
  79. package/docs/performance.md +117 -0
  80. package/docs/scaling.md +439 -0
  81. package/docs/snapshot-safety.md +246 -0
  82. package/docs/u64-id-space.md +214 -0
  83. package/docs/verification-report.md +670 -0
  84. package/native/brainy-native.node +0 -0
  85. package/native/index.d.ts +3916 -0
  86. package/package.json +92 -0
  87. package/scripts/migrate-cortex-2x-to-3x.mjs +970 -0
@@ -0,0 +1,294 @@
1
+ ---
2
+ title: ADR-002 — DiskANN as cor's billion-scale index option
3
+ slug: cor/adr-002-diskann
4
+ public: true
5
+ category: cor
6
+ template: concept
7
+ order: 2
8
+ description: Architectural decision record for cor's planned DiskANN integration. 100% pure Rust, filesystem-only, auto-engages when conditions are met. The billion-scale upgrade path that pairs with brainy's existing TS HNSWIndex.
9
+ ---
10
+
11
+ # ADR-002 — DiskANN as cor's billion-scale index option
12
+
13
+ **Status:** Decided 2026-05-28. Implementation queued across three coordinated sessions for cor 3.0.0 + brainy 8.0.0.
14
+
15
+ **Supersedes:** Original DiskANN spike task (#35), retired 2026-05-28 in favour of the three-session plan captured here.
16
+
17
+ **Related:**
18
+ - [ADR-001](./ADR-001-column-store-string-support.md) — native column store, shipped 2.3.0
19
+ - brainy 7.28.0 SQ4 (4-bit) quantization — paves the PQ path for DiskANN's compressed in-RAM distance
20
+ - Cortex 2.4.0 storage foundations (#23–#26) — stable IDs, mmap vector layer, graph compression — all transfer directly to DiskANN
21
+
22
+ ## Context
23
+
24
+ Brainy ships a TypeScript HNSW index that works excellently up to roughly 10M vectors per node on commodity hardware. Cortex 2.3.0 added a Rust-native HNSW variant via the `hnsw` provider hook — same algorithm, ~3–10× the throughput on hot paths thanks to SIMD distance and a tighter graph layout. The 2.4.0 storage foundations (vector mmap store, graph link compression, stable entity IDs) push HNSW further into the disk-resident regime.
25
+
26
+ But HNSW's design assumes the graph fits comfortably in memory. Past ~10M vectors, two costs compound:
27
+
28
+ 1. **Memory pressure** — the graph alone (`M × node_count` neighbour pointers + level metadata) plus the vector store (`dim × 4 × node_count` bytes for float32) blows past the RAM budget of normal nodes. At 100M vectors of 384-dim embeddings: ~150 GB of vectors + ~13 GB of graph = ~163 GB RAM minimum. At 1B vectors: ~1.6 TB RAM — out of reach on single boxes.
29
+ 2. **Disk-locality on cold caches** — even with vectors offloaded to mmap, HNSW's traversal order has no correlation with insertion order on disk. Each search hop typically faults a new page, costing ~10 μs per hop on NVMe SSD. A 100-hop search burns ~1 ms of disk wait that proper locality would have served from a single 10 μs read.
30
+
31
+ [DiskANN](https://github.com/microsoft/DiskANN) (Microsoft, 2019) was designed for exactly this regime. Its Vamana graph construction uses α-pruning to choose neighbours that produce **disk-locality natively**: nodes visited together during search end up adjacent on disk. Combined with **product quantization (PQ)** in RAM for approximate distance, and full vectors on disk for re-ranking the final candidate set, DiskANN holds single-machine billion-scale search at a fraction of HNSW's RAM cost.
32
+
33
+ **For cor specifically, DiskANN is the natural billion-scale upgrade path** because:
34
+
35
+ - The 2.4.0 foundations (stable IDs, mmap vector layer, graph link compression) transfer to it without rework
36
+ - The 2.5.0 #30 SQ4 quantization work primes the PQ codec path
37
+ - Cor's positioning has always been "billion-scale via Rust acceleration" — DiskANN fits the message
38
+ - We control the rest of the stack (storage adapters, idMapper, HNSW provider), so the integration is in friendly territory
39
+
40
+ We considered **ScaNN** (Google, Apache 2.0) as an alternative. It posts SOTA recall/QPS numbers at moderate scale with anisotropic vector quantization. We declined: ScaNN is IVF-based (inverted file with partition centroids), which doesn't align with brainy's graph-native architecture. Switching to IVF would mean losing the structural symmetry between brainy's verb graph and its vector index, plus introducing periodic clustering retraining (an operational concern brainy doesn't currently have).
41
+
42
+ ## Decisions
43
+
44
+ ### Decision 1 — DiskANN as the billion-scale upgrade path (not a replacement for HNSW)
45
+
46
+ HNSW stays the brainy default forever. Every existing user, including those without cor, continues to get the TS `HNSWIndex` they ship with today. DiskANN is added as an **alternative provider that engages when its constraints are met**.
47
+
48
+ This preserves three properties we don't want to give up:
49
+ - Zero-friction onboarding for new brainy users (no cor required, no config tuning to pick an algorithm).
50
+ - Backward compatibility for every existing brainy install (no surprise migrations on upgrade).
51
+ - The "cor makes brainy faster" story (not "cor makes brainy different").
52
+
53
+ ### Decision 2 — 100% pure Rust, no C++ FFI
54
+
55
+ We will port DiskANN's Vamana algorithm to Rust from the published paper (Subramanya et al., NeurIPS 2019; Singh et al., 2021) rather than wrap Microsoft's C++ reference implementation via FFI. The Vamana algorithm is straightforward: greedy graph construction with an α-pruning step that controls graph density. The published pseudocode plus the reference implementation's behaviour give us everything we need to validate correctness.
56
+
57
+ PQ codec: we will either compose a battle-tested Rust crate (e.g., `qdrant-quantization`, Apache 2.0) or implement PQ training + encode/decode in cor directly, depending on parity test outcomes. Either way, no C++.
58
+
59
+ **Why not FFI:** cross-platform C++ builds for Node native modules are operationally expensive (Linux/macOS/Windows × x64/arm64 binaries, headers, link-time gotchas), Microsoft's reference impl has its own build dependencies that would propagate, and we'd inherit any patent grant ambiguities at the binary level. Pure Rust gives us napi-rs's mature cross-platform binary distribution and a license posture we fully control.
60
+
61
+ **Why not adopt an existing Rust crate wholesale:** no mature Rust port of Vamana exists at our knowledge cutoff. We will track this and pivot if a high-quality one emerges; for now we're building it.
62
+
63
+ ### Decision 3 — Filesystem-only deployment in the first release
64
+
65
+ DiskANN is local-SSD-by-design. The whole point of the architecture is that disk reads are cheap (NVMe-cheap, ~10 μs) and predictable, so the search algorithm can lean on the OS page cache + the on-disk layout's locality.
66
+
67
+ Cloud object storage (S3, R2, GCS) breaks that assumption: range reads of large objects cost ~100 ms of round-trip latency, and the locality model has to account for HTTP/2 framing instead of OS pages. Supporting cloud storage for DiskANN would require either:
68
+
69
+ - A persistent "DiskANN file lives on a local cache disk that we sync from cloud" model (operationally heavy), or
70
+ - A fundamentally different search algorithm with batched range reads (no longer DiskANN, really).
71
+
72
+ **For the first DiskANN release, the activation conditions explicitly require `storage.adapter === 'filesystem'`.** Cloud-storage users continue to use HNSW. We may revisit cloud support if there's demand and an approach that doesn't compromise the algorithm's strengths.
73
+
74
+ ### Decision 4 — Auto-engagement, zero configuration
75
+
76
+ When all of these conditions hold at brainy init, DiskANN replaces HNSW as the active index without any user config:
77
+
78
+ 1. Cor is loaded as a plugin (the `index:diskann` provider is registered)
79
+ 2. The storage adapter is `FileSystemStorage` (local SSD)
80
+ 3. The metadata index exposes a stable `idMapper` (the 2.4.0 #23 foundation)
81
+
82
+ This mirrors the existing `MmapVectorBackend` wiring pattern from 2.4.0 #24: the heavy machinery activates when its preconditions are met, and otherwise silently falls back. Users who don't want it can opt out via `config.index.type = 'hnsw'`.
83
+
84
+ **Why auto-engage instead of opt-in by config:**
85
+
86
+ - Matches cor's "loading cor makes brainy faster" value proposition (no extra knob to turn)
87
+ - The constraints (cor + filesystem) are exactly the deployment shape DiskANN targets, so the conditions ARE the signal
88
+ - Opt-in-only would leave most filesystem-using cor installs on HNSW out of caution — defeating the point
89
+
90
+ **Why not unconditional default:**
91
+
92
+ - Cloud-storage users have no DiskANN-compatible path; we can't break their existing HNSW workflows
93
+ - Cor-less users (the brainy-only crowd) never see DiskANN regardless — preserves the "brainy works the same with or without cor" property
94
+
95
+ ### Decision 5 — Explicit migration API for existing installs
96
+
97
+ Existing brainy installs with an HNSW index on disk **do not auto-migrate to DiskANN on upgrade**. The on-disk HNSW state is detected at init; if `config.index.type` is unset, brainy logs:
98
+
99
+ > `[brainy] Existing HNSW index detected at <path>. The new cor default for filesystem storage is DiskANN. Continue using HNSW (set config.index.type='hnsw' to silence this message) or run brain.migrateToDiskAnn() to convert.`
100
+
101
+ The migration API:
102
+
103
+ ```typescript
104
+ // Convert an existing HNSW index to DiskANN.
105
+ // Builds the DiskANN index in parallel (separate files), verifies recall
106
+ // parity at the configured threshold, then atomically swaps the active
107
+ // index. Reversible via brain.migrateToHnsw().
108
+ await brain.migrateToDiskAnn({
109
+ recallTarget?: number, // default 0.95 — verification target before swap
110
+ paddingFactor?: number, // default 1.2 — slack for re-ranking candidate set
111
+ parallel?: boolean // default true — build new index alongside live old
112
+ })
113
+ ```
114
+
115
+ Reversibility (`brain.migrateToHnsw()`) is a contract, not a courtesy. Users need to be able to roll back if recall regression or any other issue surfaces in production.
116
+
117
+ ## Architecture
118
+
119
+ ### Brainy provider contract
120
+
121
+ Cor registers two new providers (mirrors the existing `hnsw` provider shape):
122
+
123
+ ```typescript
124
+ // brainy: src/plugin.ts
125
+ export interface DiskAnnProvider {
126
+ create(config: DiskAnnConfig, distance: DistanceFunction, options: DiskAnnOptions): DiskAnnInstance
127
+ openExisting(path: string, distance: DistanceFunction): DiskAnnInstance
128
+ }
129
+
130
+ export interface DiskAnnInstance extends HnswProvider {
131
+ // Implements the same interface HNSWIndex/HnswProvider exposes, so the rest
132
+ // of brainy doesn't care which index is active. Adds one DiskANN-specific
133
+ // method for the migration API:
134
+ rebuildPQCodebook(): Promise<void> // Re-trains PQ from current vectors
135
+ }
136
+ ```
137
+
138
+ The instance implementing `HnswProvider` is the load-bearing decision. brainy's search/find/get code paths call into the provider through this surface; an `HNSWIndex` and a `NativeDiskANN` are interchangeable from brainy's POV. No control-flow plumbing changes in brainy beyond the choice of which provider to instantiate.
139
+
140
+ ### Cor Rust modules
141
+
142
+ ```
143
+ cor/native/src/diskann/
144
+ ├── mod.rs — napi exports + the NativeDiskANN class
145
+ ├── vamana.rs — α-pruning greedy graph construction (~500 LOC)
146
+ ├── pq.rs — Product Quantization codebook training + encode/decode
147
+ ├── format.rs — On-disk file format (header + PQ codebook + graph + vectors)
148
+ └── search.rs — Greedy graph search with PQ-approximate distance + re-rank
149
+ ```
150
+
151
+ ### On-disk file format
152
+
153
+ Single contiguous file `<dataDir>/_diskann/main.bin` (path mirrors `_vectors/main.bin` from #24):
154
+
155
+ ```
156
+ +--------------------------------------------------------------+
157
+ | Header (4 KB, aligned) |
158
+ | magic: u32 "DKAN" |
159
+ | version: u32 layout revision |
160
+ | dim: u32 vector dimensionality |
161
+ | node_count: u32 total vectors |
162
+ | pq_subspaces: u8 PQ M parameter (typically 8 or 16) |
163
+ | pq_bits: u8 bits per subspace (typically 8) |
164
+ | max_degree: u8 Vamana R parameter (typically 64-96) |
165
+ | entry_point: u32 slot id of the entry node |
166
+ | ... reserved bytes for forward compatibility ... |
167
+ +--------------------------------------------------------------+
168
+ | PQ codebook (M × 256 × subvec_dim × f32) |
169
+ +--------------------------------------------------------------+
170
+ | PQ codes (node_count × M bytes) |
171
+ | — one PQ code per node, M bytes each, in slot order |
172
+ +--------------------------------------------------------------+
173
+ | Vamana graph (node_count × max_degree × u32) |
174
+ | — flat CSR-like array of neighbour slot ids |
175
+ | — fixed degree per node for predictable offset math |
176
+ +--------------------------------------------------------------+
177
+ | Full vectors (node_count × dim × f32) |
178
+ | — only touched for re-ranking the final candidate set |
179
+ +--------------------------------------------------------------+
180
+ ```
181
+
182
+ The fixed-degree Vamana graph trades a small density loss for O(1) neighbour-offset arithmetic. PQ codes pack tightly in RAM (M bytes per vector — at M=16 that's 16 bytes/vector regardless of dim, so 1B vectors fit in ~16 GB RAM for the PQ-resident layer).
183
+
184
+ ### Search algorithm
185
+
186
+ ```
187
+ async function search(query: Vector, k: number): Promise<Result[]> {
188
+ // 1. PQ-encode the query into M sub-vector codes
189
+ const queryPq = pqEncode(query, codebook)
190
+
191
+ // 2. Greedy graph walk using PQ-approximate distance
192
+ const visited = new Set<u32>()
193
+ const candidates = new BoundedHeap(maxLen = k * paddingFactor)
194
+ let current = entryPoint
195
+
196
+ while (improving(candidates)) {
197
+ const neighbours = graph[current]
198
+ for (const n of neighbours) {
199
+ if (visited.has(n)) continue
200
+ visited.add(n)
201
+ const approxDist = pqDistance(queryPq, codes[n])
202
+ candidates.insert(n, approxDist)
203
+ }
204
+ current = candidates.bestUnvisited()
205
+ }
206
+
207
+ // 3. Re-rank the top-(k * paddingFactor) candidates with full vectors
208
+ const topCandidates = candidates.topN(k * paddingFactor)
209
+ return topCandidates
210
+ .map(n => ({ id: idMapper.getUuid(n), distance: trueDistance(query, vectors[n]) }))
211
+ .sort()
212
+ .slice(0, k)
213
+ }
214
+ ```
215
+
216
+ The `paddingFactor` (default 1.2 = 20% over-fetch) controls the recall/cost tradeoff. PQ approximate distance is fast but lossy; re-ranking on the over-fetched candidate set with full-precision vectors recovers recall at a small cost (typically a few hundred extra full-vector reads per query, which is fine on SSD).
217
+
218
+ ## Implementation plan
219
+
220
+ ### Session 35a — Vamana + PQ in pure Rust (cor)
221
+
222
+ **Scope (~3–5 hrs focused):**
223
+
224
+ - `cor/native/src/diskann/vamana.rs` — Vamana graph construction with α-pruning, ~500 LOC. Inputs: vector buffer, dim, R (max degree), α (density parameter, typically 1.2–1.4). Output: CSR adjacency.
225
+ - `cor/native/src/diskann/pq.rs` — PQ codebook training (k-means on subvector partitions) + encode/decode. M subspaces × 256 centroids each, configurable.
226
+ - `cor/native/src/diskann/format.rs` — On-disk file format struct + read/write primitives.
227
+ - Rust unit tests: graph connectivity invariants, PQ recall on small synthetic dataset, format round-trip.
228
+
229
+ **Exit criteria:** Vamana graph build over 10k random vectors produces a connected graph with degree ≤ R, search recall ≥ 95% at k=10 on synthetic dataset.
230
+
231
+ ### Session 35b — Search + napi bindings (cor)
232
+
233
+ **Scope (~3–5 hrs focused):**
234
+
235
+ - `cor/native/src/diskann/search.rs` — Greedy search with PQ-approximate distance and full-vector re-ranking on the candidate set.
236
+ - `cor/native/src/diskann/mod.rs` — `#[napi]` exports of `NativeDiskANN` class with `create` / `openExisting` / `addItem` / `search` / `rebuildPQCodebook` methods.
237
+ - `cor/native/index.d.ts` regeneration.
238
+ - Recall validation against published DiskANN benchmark numbers (sanity check, not full BIGANN — that's a separate effort).
239
+
240
+ **Exit criteria:** Search recall ≥ 95% at k=10 over a 100k-vector dataset matches the published DiskANN paper's numbers within 2 percentage points.
241
+
242
+ ### Session 35c — Brainy hookup + cor 3.0.0 + brainy 8.0.0 release
243
+
244
+ **Scope (~3–5 hrs focused):**
245
+
246
+ - `brainy/src/hnsw/diskAnnIndex.ts` — TS wrapper class implementing brainy's `HnswProvider` contract over `NativeDiskANN`. Same surface as `HNSWIndex` so the rest of brainy is agnostic.
247
+ - `brainy/src/brainy.ts` — `wireDiskAnn()` private method that runs after `wireMmapVectorBackend()` during init. Auto-engagement conditions; opt-out via `config.index.type = 'hnsw'`.
248
+ - `brainy/src/plugin.ts` — `DiskAnnProvider` and `DiskAnnInstance` interfaces (mirrors the `VectorStoreMmapProvider` pattern from 2.4.0 #24).
249
+ - `brain.migrateToDiskAnn()` and `brain.migrateToHnsw()` explicit migration APIs.
250
+ - Tests: provider hookup, auto-engagement conditions, opt-out, recall parity at 10k–100k vectors, migration round-trip integrity.
251
+ - Coordinated release: `cor 3.0.0` + `brainy 8.0.0`. Major bumps because the default index type changes for filesystem+cor users (semver discipline matters).
252
+
253
+ **Exit criteria:** Recall parity between brainy 8.0.0 + cor 3.0.0 DiskANN path and brainy 7.x + cortex 2.x HNSW path is within 1% at standard k values (1, 10, 50). Migration round-trip preserves index integrity.
254
+
255
+ ## Consequences
256
+
257
+ ### Positive
258
+
259
+ - **Single-machine billion-scale becomes a supported workload.** At 100M to 1B vectors, RAM cost drops by ~16–20× compared to HNSW. NVMe disk locality replaces RAM pressure as the bottleneck.
260
+ - **Cor's "billion-scale via Rust acceleration" positioning becomes literal**, not aspirational.
261
+ - **Zero impact to non-cor users.** brainy keeps shipping its TS HNSWIndex; no API change, no behaviour change for them.
262
+ - **Foundations carry forward.** The 2.4.0 storage work (stable IDs, mmap layer, graph compression) and 2.5.0 #30 (SQ4 quantization, the PQ precursor) all transfer.
263
+ - **License posture is clean.** Pure Rust port from a published algorithm + permissive (MIT/Apache 2.0) Rust deps. No C++ FFI license entanglement.
264
+ - **Future-utility carry.** The cor Rust compaction primitives (`compute_bfs_order`, `compute_hnsw_traversal_order`) stay in the codebase; if HNSW's disk locality ever becomes interesting again, the math is already there.
265
+
266
+ ### Negative / Tradeoffs
267
+
268
+ - **Build cost.** DiskANN graph construction is slower than HNSW because Vamana's α-pruning requires examining more candidate neighbours per node. On 100M vectors this is hours, not minutes. Acceptable for once-per-deployment cost.
269
+ - **PQ recall ceiling.** Product Quantization is lossy. Recall maxes out around 95–98% on typical embedding workloads; HNSW with full precision can reach 99%+. The re-ranking step recovers most of the gap. Users with extreme recall requirements (e.g., legal-discovery search) may want to stay on HNSW.
270
+ - **Filesystem-only constraint.** Cloud-storage users get no benefit from DiskANN in the first release. We've accepted this; cloud DiskANN is a future investigation, not a commitment.
271
+ - **Major version bump.** Auto-engagement changing the default index type for filesystem+cor users is a semver-major event. brainy 8.0.0 and cor 3.0.0 must coordinate. Some communication overhead at release time.
272
+
273
+ ### Risks
274
+
275
+ - **Correctness drift from the reference implementation.** Vamana has subtle algorithmic choices (the α-pruning order, the entry-point selection strategy) that affect recall by small but real amounts. Mitigation: explicit recall validation against the published numbers + reference implementations in 35a and 35b's exit criteria.
276
+ - **Brainy provider contract surface mismatch.** The `HnswProvider` interface was designed for HNSW; DiskANN may surface operations (codebook retraining, segment-level compaction) that don't fit cleanly. Mitigation: keep `DiskAnnInstance` as an extension of `HnswProvider` plus DiskANN-specific methods; never narrow the parent interface.
277
+ - **Migration API regressions.** `migrateToDiskAnn` runs over potentially billions of vectors. A bug here could mean hours of wasted compute or, worse, an inconsistent index. Mitigation: parallel build (the old HNSW stays serving until the new DiskANN is validated), explicit recall verification before the atomic swap, fully reversible via `migrateToHnsw`.
278
+ - **Long-running PQ codebook drift.** As vectors are added over time, the original PQ codebook can drift away from the data distribution, eroding recall. Mitigation: expose `rebuildPQCodebook()` for explicit retrains; document the operational guideline (retrain after the dataset doubles, or after a measurable recall regression).
279
+
280
+ ## Open questions
281
+
282
+ 1. **PQ codebook strategy at scale.** Do we train PQ once on a sample of the data, or use online/streaming PQ updates? Tradeoff: simpler vs. better recall over time. Lean toward sample-once-with-explicit-retrain to keep the operational model simple.
283
+ 2. **Vamana parameters as runtime config vs. baked into the file format.** R (max degree), α (density), the search candidate set padding factor — how much do we expose to users? Lean toward fixed-good-defaults in 3.0.0, expose later if a workload demands it.
284
+ 3. **Filtered search support.** brainy's `find({ where, ... })` interacts with HNSW via a filter callback. DiskANN's PQ-distance loop needs different filter integration. Plan to defer — initial release supports unfiltered top-K search; filtered search is a follow-up.
285
+ 4. **Multi-shard / single-node-of-cluster deployments.** Cor isn't a cluster engine, but some users run multiple cor+brainy nodes behind a load balancer. Does each node need its own DiskANN file, or can they share one? Plan to defer — start with per-node files.
286
+
287
+ ## References
288
+
289
+ - Subramanya et al., *DiskANN: Fast Accurate Billion-Point Nearest Neighbor Search on a Single Node*, NeurIPS 2019. [arXiv:1907.07574](https://arxiv.org/abs/1907.07574)
290
+ - Singh et al., *FreshDiskANN: A Fast and Accurate Graph-Based ANN Index for Streaming Similarity Search*, 2021. [arXiv:2105.09613](https://arxiv.org/abs/2105.09613)
291
+ - Microsoft DiskANN open-source reference implementation: [github.com/microsoft/DiskANN](https://github.com/microsoft/DiskANN) (MIT licensed)
292
+ - ADR-001 — Native column store with raw mmap segments (the same architectural pattern of "cor registers a provider, brainy consumes when present")
293
+ - Brainy 7.28.0 SQ4 quantization (the PQ precursor — scalar quantization scoped to a single vector; PQ extends the same idea to subvector partitions with learned codebooks)
294
+ - Cortex 2.4.0 storage foundations: stable EntityIdMapper (#23), mmap vector backend (#24), graph link compression (#25), column-store interchange (#26)
@@ -0,0 +1,100 @@
1
+ ---
2
+ title: ADR-003 — Semantic (vector) time travel
3
+ slug: cor/adr-003-semantic-time-travel
4
+ public: true
5
+ category: cor
6
+ template: concept
7
+ order: 3
8
+ description: How db.asOf(g).find() returns semantically-correct results as of a past generation — across metadata, graph, AND the vector index. A tiered strategy that exploits triple-intelligence pre-filtering, reuses the shadow-page generation substrate, and handles embedding-model drift.
9
+ ---
10
+
11
+ # ADR-003 — Semantic (vector) time travel
12
+
13
+ **Status:** Proposed 2026-06-15. Phase 0 (verification) targeted for the 8.0.0/3.0.0 lockstep release; Phases 1–2 for the immediate follow-up (8.1/3.1); Phase 3 is a research track.
14
+
15
+ **Related:**
16
+ - [ADR-002](./ADR-002-diskann-100-percent-rust.md) — DiskANN vector index
17
+ - `docs/snapshot-safety.md` — the shadow-page generation substrate this builds on
18
+ - Graph time-travel generation-threading (cor 3.0 / brainy 8.0 `getVerbEndpointsAtGeneration`)
19
+
20
+ ## Context
21
+
22
+ Brainy 8.0's `db.asOf(g)` returns the database as it existed at generation `g`. For a triple-intelligence `find()` (metadata filter ∩ graph hop ∩ vector similarity), *every* substrate must answer "what did you look like at `g`."
23
+
24
+ Two of the three are solved by the **shadow-page generation substrate**: the metadata LSM and the graph endpoints store record brainy's generation per write and resolve point-in-time reads from a version chain. The id-mappers are monotonic. So the **existence dimension** — which entities and edges were present at `g` — is correct.
25
+
26
+ The **vector/semantic dimension is the hard one**, and it decomposes into three sub-problems:
27
+
28
+ 1. **Candidate set at `g`** — which entities existed. *Already solved* (the versioned substrates above).
29
+ 2. **Vector *values* at `g`** — an entity re-embedded after an edit has a different vector now than at `g`. The historical value must be recoverable.
30
+ 3. **Index *structure* at `g`** — HNSW/DiskANN build a navigation graph. The current graph differs from the one at `g`.
31
+
32
+ Plus a fourth, cross-cutting limit:
33
+
34
+ 4. **Embedding-model drift** — the query is embedded with the *current* model. If the embedding model changed between `g` and now, comparing a current-model query against old-model data vectors is a comparison across *different vector spaces* — silently wrong unless detected.
35
+
36
+ Versioning a million-node ANN graph per generation is prohibitive, which is why no standalone vector database offers semantic time travel. This ADR records a strategy that does, without paying that cost in the common case.
37
+
38
+ ## The key insight
39
+
40
+ In a triple-intelligence query the vector leg rarely runs alone — it runs over the **intersection of the metadata and graph filters, which are already time-travel-correct and usually small**. Over a candidate set of hundreds (not millions), an **exact rerank** over the entities' gen-`g` vectors gives perfect recall in sub-millisecond time, with **no historical ANN index at all**. The other two intelligences pre-pay the cost of vector time travel.
41
+
42
+ The hard residue is only the **unfiltered** case (pure semantic search over everything, as of `g`), which is where the index itself must time-travel.
43
+
44
+ ## Decision
45
+
46
+ A **tiered strategy** on the existing generation substrate, selected automatically by the `AdaptiveDiskAnnModeSelector` on `(filter selectivity × history depth)`:
47
+
48
+ | Regime | Mechanism | Recall | Cost |
49
+ |---|---|---|---|
50
+ | **Filtered, any depth** (common) | **Exact rerank** over the pre-filtered candidate set's gen-`g` vectors | Exact | Sub-ms (small set); no new index |
51
+ | **Unfiltered, recent** | **Differential overlay**: current index + visibility bitmap + sparse per-node vector version chain (only re-embedded nodes); navigate current graph, mask future nodes, evaluate gen-`g` vectors | Near-exact | One index; near-zero overhead |
52
+ | **Unfiltered, deep** | **LSM-tiered shadow-page ANN**: immutable index segments at generation checkpoints + deltas; search the segment(s) covering `g`; lazily materialized + cached | Exact (segment) | Bounded per-segment; pay-per-use storage |
53
+ | **Cross-cutting** | **Embedding-model versioning**: stamp each vector with its model; auto-project the query into the historical space, or hard-error — never silently wrong | — | Detection is O(1) |
54
+
55
+ ### Options considered (and why not, alone)
56
+
57
+ 1. **Exact rerank over the pre-filtered window** — chosen for the filtered case; insufficient alone (unfiltered queries fall through).
58
+ 2. **LSM-tiered / shadow-page ANN** — chosen for the unfiltered-deep case; reuses the proven base+delta+compaction primitive so the vector index joins one unified MVCC substrate. Cost: search amplification across segments.
59
+ 3. **Generation-tagged Vamana + delta-replay** — exact historical *structure*, but Vamana's robust-prune rewires many neighbors per insert → fat delta logs; folded into (2) as the segment-internal representation rather than a standalone mode.
60
+ 4. **Persistent / copy-on-write immutable ANN** (structural sharing, à la Datomic) — elegant and instant `asOf`, but ANN's high fan-out + rewiring limits sharing and CoW pointer-chasing fights the cache locality ANN depends on. Research track (Phase 3).
61
+ 5. **Differential overlay** — chosen for the unfiltered-recent case; structure is current (approximate recall for deep history), which is why it is scoped to recent generations.
62
+ 6. **Time as a first-class ANN filter** (`[birth_gen, death_gen]` predicate) — composes with our metadata/graph filters and is how the candidate set is computed; suffers known recall cliffs at extreme selectivity, so it feeds the rerank/overlay rather than standing alone.
63
+ 7. **Embedding-model re-projection** — chosen as the cross-cutting correctness layer; the alignment map (Phase 3) is research-grade, but the *detection* of model drift (Phase 1) is cheap and ships early.
64
+
65
+ ## Open-core behavior (brainy alone vs brainy + cor)
66
+
67
+ The API and correctness are identical; only speed and scale differ — the open-core contract.
68
+
69
+ - **brainy alone (MIT):** `db.asOf(g).find()` is correct via brainy's canonical-record materialization — the historical candidate set is rebuilt from versioned records and reranked in JS. Slower (brute-force, no native acceleration), bounded to the JS-feasible scale (~10⁵–10⁶), but **semantically correct**.
70
+ - **brainy + cor:** the candidate set comes from cor's versioned native substrates, the rerank is native, the unfiltered-deep path uses the tiered shadow-page ANN, and scale extends to 10⁸–10¹⁰. Same `asOf(g).find()` call; faster and larger.
71
+
72
+ This means semantic time travel is a **brainy feature** that cor accelerates — not a cor-only capability.
73
+
74
+ ## Phasing
75
+
76
+ - **Phase 0 — Verify + document (this release, 8.0/3.0). ✅ VERIFIED — see the guarantee + limits below.** A cross-layer test (`src/native/semanticAsOf.test.ts`) builds entities, re-embeds one across generations, then asserts `asOf(g).find({vector, where})` returns only the g-valid set, ranked by the *at-g* embedding, with the `where` filter seeing the at-g metadata. It passes (incl. a leak-detector that fails if the vector leg used the live index). No new critical-path code.
77
+
78
+ ### Phase 0 — verified guarantee + its limits (be honest in GA messaging)
79
+
80
+ **Guarantee (verified, 8.0.0-rc.2 + cor 3.0):** `db.asOf(g).find({ vector, where })` is set-correct, rank-correct (at-g embedding), and filter-correct (at-g metadata) — **for mutations committed via `brain.transact([...])`**.
81
+
82
+ **Limits — all four are real; do not claim past them:**
83
+ 1. **`transact()`-only in the shipped brainy rc.2.** Single-op `brain.add()`/`brain.remove()` writes bump the generation counter but do **not** retain a before-image or register the generation (`generationStore.js:178 noteSingleOpWrite`), so `asOf(g)` silently resolves to **current** state for them. Time-travel-correct history requires `transact()`. Brainy's rc.3 Model-B is slated to retain single-op writes; the guard test (`semanticAsOf.test.ts`) flips red when it lands. (Handoff: IMMUTABILITY-TIMETRAVEL thread.)
84
+ 2. **The historical leg is brainy-JS-served this release.** `asOf(g)` builds an ephemeral reader brain and rebuilds a JS HNSW from the at-g vectors; cor's native vector index (`NativeDiskAnnWrapper`) is **not** on the historical path. cor accelerates only the live (`now`) leg in 3.0. (Open-core: brainy-alone is correct via JS rerank.)
85
+ 3. **O(n-at-g) rebuild per `asOf` Db — small/medium scale only.** One `addItem` per at-g vector into a fresh JS HNSW; this OOMs at billion-scale. Billion-scale historical semantic find is **Phase 1–2** (cor native rerank / overlay / tiered ANN; #35). Phase 0 is correctness, not scale.
86
+ 4. **No embedding-model-drift guard yet.** Comparing a query embedded with model vN against stored vectors from model vM is silently wrong across a model upgrade — detection lands in Phase 1, alignment in Phase 3.
87
+ - **Phase 1 — Native-accelerate + drift detection (8.1/3.1).** Cor native exact-rerank path; embedding-model version stamping + hard-error on cross-model comparison (never silently wrong).
88
+ - **Phase 2 — Unfiltered paths (8.1/3.1+).** Differential overlay (recent) + LSM-tiered shadow-page ANN (deep), lazily materialized + cached.
89
+ - **Phase 3 — Research track (lands when ready).** Persistent/CoW ANN; learned cross-model re-projection (semantic time travel that survives embedding-model upgrades).
90
+
91
+ ## Consequences
92
+
93
+ - Semantic time travel ships **correct-first** (Phase 0 verifies the realistic query), then **fast** (Phase 1+), rather than waiting on a versioned ANN index that may never be worth its cost.
94
+ - The vector index becomes a first-class citizen of the same generation substrate as metadata and graph — one MVCC clock across all three intelligences.
95
+ - The honest limit (embedding-model drift) is made explicit and, in Phase 3, solvable — rather than ignored.
96
+
97
+ ## Division of labor
98
+
99
+ - **brainy:** `asOf` routing (when to materialize vs serve native); canonical-record retention of per-generation vectors; embedding-model version stamping in the record; the open-core JS rerank path.
100
+ - **cor:** native exact-rerank; the differential overlay; the LSM-tiered shadow-page ANN; mode selection by selectivity × depth; the model-drift detector + (Phase 3) re-projection.
@@ -0,0 +1,251 @@
1
+ ---
2
+ title: Cor Aggregation Engine
3
+ slug: cor/aggregation
4
+ public: true
5
+ category: cor
6
+ template: concept
7
+ order: 3
8
+ description: Rust-accelerated aggregation for real-time analytics — incremental updates, exact percentile and distinctCount, HAVING filters, array-unnest grouping, Welford online statistics, parallel rebuilds, and time-window bucketing.
9
+ next:
10
+ - cor/performance
11
+ - cor/scaling
12
+ ---
13
+
14
+ # Cor Aggregation Engine
15
+
16
+ Cor provides a Rust-accelerated aggregation engine for Brainy, enabling real-time analytics on entity data with incremental updates, parallel rebuilds, and precise statistical operations.
17
+
18
+ ## Architecture
19
+
20
+ Brainy owns the storage and lifecycle. Cor owns the compute.
21
+
22
+ ```
23
+ ┌─────────────────────────────────────────┐
24
+ │ Brainy │
25
+ │ ┌─────────────────────────────────┐ │
26
+ │ │ AggregationIndex │ │
27
+ │ │ ├── defineAggregate() │ │
28
+ │ │ ├── removeAggregate() │ │
29
+ │ │ ├── onEntityAdd/Update/Delete │ │
30
+ │ │ └── query() │ │
31
+ │ └──────────────┬──────────────────┘ │
32
+ │ │ provider interface │
33
+ │ ┌──────────────▼──────────────────┐ │
34
+ │ │ Cor AggregationProvider │ │
35
+ │ │ ├── NativeAggregationEngine │───── Rust (NAPI)
36
+ │ │ ├── value multiset (min/max, │ │
37
+ │ │ │ percentile, distinctCount)│ │
38
+ │ │ ├── HAVING + array-unnest │ │
39
+ │ │ ├── Rayon parallel rebuild │ │
40
+ │ │ └── Welford's online stddev │ │
41
+ │ └─────────────────────────────────┘ │
42
+ └─────────────────────────────────────────┘
43
+ ```
44
+
45
+ When Cor is installed as a Brainy plugin, the aggregation provider automatically registers. All aggregation computation runs in Rust through NAPI bindings while Brainy handles storage, persistence, and the public API.
46
+
47
+ ## Operations
48
+
49
+ Cor supports all 9 aggregation operations:
50
+
51
+ | Operation | Description | Precision |
52
+ |-----------|-------------|-----------|
53
+ | `sum` | Running total of a numeric field | Exact (f64) |
54
+ | `count` | Number of matching entities | Exact (u64) |
55
+ | `avg` | Running average (sum/count) | Exact (f64) |
56
+ | `min` | Minimum value across all entities | Exact (value multiset) |
57
+ | `max` | Maximum value across all entities | Exact (value multiset) |
58
+ | `stddev` | Sample standard deviation | Online (Welford's) |
59
+ | `variance` | Sample variance | Online (Welford's) |
60
+ | `percentile` | Value at fraction `p` (e.g. p50, p95) | Exact (value multiset, linear interpolation) |
61
+ | `distinctCount` | Number of distinct values | Exact (value multiset) |
62
+
63
+ `percentile` requires a `p` property in `[0, 1]` on the metric definition
64
+ (`{ op: 'percentile', field: 'latency', p: 0.95 }`). Both `percentile` and
65
+ `distinctCount` are **exact** — they read the same per-group value multiset that
66
+ backs precise MIN/MAX, so they are never approximate and never stale after
67
+ deletes. Percentile uses standard linear interpolation between ranks, verified
68
+ against numpy's percentiles in `src/aggregation/aggregation.test.ts`.
69
+
70
+ ### Exact value multiset (MIN/MAX, percentile, distinctCount)
71
+
72
+ Unlike simpler implementations that become stale after deletes, Cor keeps a per-group Rust `BTreeMap<OrderedFloat<f64>, u32>` (a sorted value multiset) tracking the exact frequency of every value:
73
+
74
+ - **Add**: Insert or increment the count for the value
75
+ - **Delete**: Decrement the count; remove the key if count reaches zero
76
+ - **MIN / MAX**: first / last key of the multiset
77
+ - **percentile**: walk the sorted multiset to the target rank, linear-interpolating between neighbours
78
+ - **distinctCount**: number of keys in the multiset
79
+
80
+ This single structure guarantees exact MIN, MAX, percentile, and distinct-count after any sequence of add/update/delete operations without requiring a full rescan.
81
+
82
+ ### Welford's Online Algorithm
83
+
84
+ Standard deviation and variance use Welford's numerically-stable online algorithm with `M2` tracking (sum of squared differences from the running mean). This computes incrementally without storing all values:
85
+
86
+ ```
87
+ On add(x):
88
+ n += 1
89
+ delta = x - mean
90
+ mean += delta / n
91
+ delta2 = x - mean
92
+ M2 += delta * delta2
93
+
94
+ Sample variance = M2 / (n - 1)
95
+ Sample stddev = sqrt(variance)
96
+ ```
97
+
98
+ ## Rayon Parallel Rebuild
99
+
100
+ When rebuilding an aggregate from scratch (e.g., after definition change or cold start), Cor uses Rayon's parallel iterators to process entities across all CPU cores:
101
+
102
+ - Entities below 1,000 are processed sequentially (overhead not worth it)
103
+ - Above 1,000, Rayon splits the work across threads
104
+ - Each thread computes partial aggregation state
105
+ - Results are merged with thread-safe combining
106
+
107
+ For 100K entities with 20 groups, rebuild completes in ~15ms.
108
+
109
+ ## Time Window Bucketing
110
+
111
+ GroupBy dimensions can specify time windows for temporal aggregation. The native engine performs integer-based timestamp bucketing without allocating `Date` objects:
112
+
113
+ | Window | Format | Example |
114
+ |--------|--------|---------|
115
+ | `hour` | `YYYY-MM-DDTHH` | `2024-01-15T14` |
116
+ | `day` | `YYYY-MM-DD` | `2024-01-15` |
117
+ | `week` | `YYYY-WNN` (ISO week) | `2024-W03` |
118
+ | `month` | `YYYY-MM` | `2024-01` |
119
+ | `quarter` | `YYYY-QN` | `2024-Q1` |
120
+ | `year` | `YYYY` | `2024` |
121
+ | custom (N seconds) | ISO 8601 UTC, floored | `2024-01-15T10:30:00.000Z` |
122
+
123
+ Keys are computed with pure integer arithmetic (no `Date` allocation) and are byte-for-byte identical to Brainy's `bucketTimestamp()` (`native/src/aggregation/timestamp.rs`).
124
+
125
+ Combined groupBy dimensions (plain field + time window) produce composite group keys:
126
+
127
+ ```typescript
128
+ brain.defineAggregate({
129
+ name: 'monthly_sales',
130
+ source: { type: 'Event' },
131
+ groupBy: ['region', { field: 'date', window: 'month' }],
132
+ metrics: {
133
+ revenue: { op: 'sum', field: 'amount' },
134
+ count: { op: 'count' }
135
+ }
136
+ })
137
+ ```
138
+
139
+ This produces groups like `{ region: 'US', date: '2024-01' }`.
140
+
141
+ ## Array-Unnest GroupBy
142
+
143
+ A groupBy dimension can `unnest` an array field, so an entity contributes once per
144
+ distinct array element. The classic use is tag/label frequency:
145
+
146
+ ```typescript
147
+ brain.defineAggregate({
148
+ name: 'tag_frequency',
149
+ source: { type: 'Post' },
150
+ groupBy: [{ field: 'tags', unnest: true }],
151
+ metrics: { count: { op: 'count' } }
152
+ })
153
+ ```
154
+
155
+ A post with `tags: ['rust', 'db', 'rust']` contributes once to `rust` and once to
156
+ `db` (duplicates within one entity are de-duplicated). An entity with a missing or
157
+ empty array contributes to no group.
158
+
159
+ ## HAVING
160
+
161
+ `queryAggregate` can filter groups *after* metrics are computed via a `having`
162
+ clause, using the same Brainy field-operator syntax as `where` (`greaterThan`,
163
+ `lessThan`, `between`, `anyOf`/`allOf`/`not`, …). It applies to computed metric
164
+ values and to `count`:
165
+
166
+ ```typescript
167
+ brain.queryAggregate('monthly_sales', {
168
+ having: { revenue: { greaterThan: 10000 } }
169
+ })
170
+ ```
171
+
172
+ Filtering happens post-aggregation in Rust (O(groups)), so a `having` clause never
173
+ re-scans entities.
174
+
175
+ ## State Serialization
176
+
177
+ The engine serializes all internal state (definitions, group states, the per-group
178
+ value multiset, Welford's M2 values) to JSON for persistence:
179
+
180
+ ```typescript
181
+ // Brainy handles this automatically via the provider interface
182
+ const state = engine.serializeState() // JSON string
183
+ engine.restoreState(state) // Restore on restart
184
+ ```
185
+
186
+ State includes:
187
+ - All registered definitions
188
+ - Per-aggregate, per-group metric state
189
+ - The value multiset backing MIN/MAX, percentile, and distinctCount
190
+ - Welford's `mean`, `M2`, and `count` for stddev/variance
191
+
192
+ ## Source Filtering
193
+
194
+ Aggregate definitions can specify source filters to only aggregate entities of a specific type:
195
+
196
+ ```json
197
+ {
198
+ "name": "event_stats",
199
+ "source": { "type": "Event" },
200
+ "groupBy": ["category"],
201
+ "metrics": { "count": { "op": "count" } }
202
+ }
203
+ ```
204
+
205
+ During incremental updates, entities that don't match the source filter are skipped. During rebuild, the filter is compiled and applied before aggregation.
206
+
207
+ Aggregate entities themselves (entities with `service: 'brainy:aggregation'` or `metadata.__aggregate: true`) are always skipped to prevent infinite feedback loops.
208
+
209
+ ## Incremental Update Flow
210
+
211
+ When Brainy calls `incrementalUpdate()`:
212
+
213
+ 1. **Source filter check** — skip if entity doesn't match
214
+ 2. **Aggregate entity check** — skip if entity is itself an aggregate
215
+ 3. **Group key computation** — extract groupBy fields, apply time bucketing
216
+ 4. **Metric update** — for each metric in the definition:
217
+ - `add`: Increment sum/count/mean/M2, insert into BTreeMap
218
+ - `delete`: Decrement sum/count/mean/M2, remove from BTreeMap
219
+ - `update`: Delete old values, add new values (handles group changes)
220
+
221
+ ## Performance
222
+
223
+ Run the included benchmarks: `npm run bench`
224
+
225
+ | Operation | Throughput | Latency |
226
+ |-----------|-----------|---------|
227
+ | `incrementalUpdate` (1K entities) | 809 ops/s | 1.2 ms |
228
+ | `rebuildAggregate` (10K entities) | 475 ops/s | 2.1 ms |
229
+ | `rebuildAggregate` (100K entities) | 66 ops/s | 15.2 ms |
230
+ | `queryAggregate` (1K groups, sort + paginate) | 986 ops/s | 1.0 ms |
231
+ | `computeGroupKey` (10K entities) | 146 ops/s | 6.8 ms |
232
+
233
+ ## Troubleshooting
234
+
235
+ ### Aggregation not using native engine
236
+
237
+ Verify Cor is loaded and the aggregation provider is registered:
238
+
239
+ ```typescript
240
+ const diag = brain.diagnostics()
241
+ console.log(diag.providers.aggregation)
242
+ // Should show { source: 'plugin' }
243
+ ```
244
+
245
+ ### Stale MIN/MAX after deletes
246
+
247
+ This should not happen with Cor — the BTreeMap guarantees precision. If you see stale values, verify you're running Cor (not the JS fallback) and that the delete operation includes the correct entity metadata.
248
+
249
+ ### Rebuild performance
250
+
251
+ For datasets over 100K entities, rebuild uses Rayon parallelism automatically. Ensure your system has multiple CPU cores available. Single-core environments still work but won't benefit from parallel rebuild.