@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,439 @@
1
+ ---
2
+ title: Scaling
3
+ slug: cor/scaling
4
+ public: true
5
+ category: cor
6
+ template: guide
7
+ order: 4
8
+ description: How Brainy + Cor scale from a single laptop to billions of entities across multiple machines — single machine, multi-tenant density, billion-vector search, and horizontal scaling patterns. No managed service, no required coordinator, no vendor lock-in.
9
+ next:
10
+ - cor/diskann
11
+ - cor/performance
12
+ - cor/comparison
13
+ ---
14
+
15
+ # Scaling
16
+
17
+ > **One engine. Three tiers. Same files, same API.**
18
+ > Run it on a laptop for development, a single commodity server for production,
19
+ > or across many servers for global scale — Brainy + Cor is the same code at
20
+ > every step.
21
+
22
+ There is no managed-service tier and no required coordinator. The engine is
23
+ local Rust + local files, and "going bigger" means adding hardware, not
24
+ adopting a different product. This page covers what fits at each tier and
25
+ how to compose multiple instances when one machine isn't enough.
26
+
27
+ ## At a glance
28
+
29
+ | Tier | Hardware | Capacity |
30
+ |---|---|---|
31
+ | **Single laptop** | 16-64 GB RAM, 1-4 TB NVMe | 1 billion+ entities |
32
+ | **Single commodity server** | Ryzen 9 / 64 GB DDR5 / 4 TB NVMe Gen 4 | 10 billion+ entities |
33
+ | **Multiple machines** | Independent servers, hash-sharded | Effectively unbounded |
34
+
35
+ Moving up a tier is `rsync` plus restart at the single-machine side, and adding
36
+ a routing layer at the horizontal side. The Brainy API doesn't change — your
37
+ code looks the same whether you're running one instance or 256.
38
+
39
+ ## Single machine
40
+
41
+ The default unit of deployment is **one Brainy instance** (the data layer —
42
+ JSON files on disk) **plus one Cor accelerator** (the native Rust engine
43
+ that runs SIMD distance, mmap-backed indexes, and the DiskANN search graph).
44
+ Together they handle workloads that would normally require a dedicated
45
+ database cluster.
46
+
47
+ Everything stays local: data, vectors, metadata, query engine. No network
48
+ hops at query time. No third party that can take your database down. Your
49
+ only operational concern is the machine itself.
50
+
51
+ ### Multi-tenant density on one machine
52
+
53
+ A typical deployment runs 10–200 Brainy instances inside one Node.js process
54
+ on a single VM. Each instance has its own data directory on SSD plus its own
55
+ indexes, and Cor manages what's shared and what's isolated:
56
+
57
+ | Component | Scope | Memory |
58
+ |---|---|---|
59
+ | Embedding model (all-MiniLM-L6-v2) | **Shared singleton** | 88 MB, loaded once |
60
+ | SIMD distance calculations | **Shared** | Zero per-instance cost |
61
+ | UnifiedCache (eviction engine) | **Shared singleton** | Dynamic — adapts to instance count |
62
+ | HNSW vector index | **Per-instance** | ~700 bytes per entity |
63
+ | Metadata inverted index | **Per-instance** | ~300 bytes per entity |
64
+ | Entity ID mapper | **Per-instance** | ~56 bytes per entity |
65
+ | Graph adjacency (LSM-trees) | **Per-instance** | Mmapped — kernel manages pages |
66
+ | Storage adapter | **Per-instance** | Minimal (file handles) |
67
+
68
+ ### Adaptive resource manager
69
+
70
+ Cor observes system resources and adjusts memory budgets in real time. No
71
+ configuration required — it detects VM size, container limits (cgroups
72
+ v1/v2), and active instance count automatically.
73
+
74
+ **What it observes**: total and free system memory, container memory limits,
75
+ process RSS and heap usage, number of active Brainy instances, per-instance
76
+ entity counts.
77
+
78
+ **What it decides**: cache budget (shrinks dynamically as more instances are
79
+ loaded), memory pressure level (normal / elevated / critical), eviction
80
+ candidates (weighted by idle time × memory usage, so large idle instances
81
+ are evicted before small active ones).
82
+
83
+ ### Memory-mapped storage
84
+
85
+ Cor's `MmapFileSystemStorage` extends Brainy's filesystem storage with
86
+ zero-copy binary I/O. The Rust native layer memory-maps data files via
87
+ `memmap2`, letting the Linux kernel manage which pages stay in RAM.
88
+
89
+ What mmap provides:
90
+
91
+ - **Vector index**: the entire HNSW graph (vectors + connections) is stored
92
+ as a single binary `.hnsw` file, memory-mapped for zero-copy reads. Search
93
+ traverses the graph directly on mmap'd pages — no JSON parsing, no gzip
94
+ decompression, no heap allocation. The kernel manages which pages stay in
95
+ RAM.
96
+ - **Graph adjacency SSTables**: four LSM-trees for relationship data are
97
+ memory-mapped. Read-only pages are automatically paged out under memory
98
+ pressure.
99
+ - **Dual-mode search**: after `flush()`, search reads vectors from mmap pages
100
+ (zero-copy). During active mutations, the existing in-memory engine handles
101
+ search. Maximum density between mutations, mutation speed preserved.
102
+
103
+ On a 16 GB VM with a 200 GB SSD, you can effectively manage 200 GB of brainy
104
+ data with only the hot working set in RAM. The SSD acts as an extension of
105
+ memory, managed by the Linux kernel with no application-level complexity. An
106
+ idle tenant's data is automatically paged out by the kernel — no explicit
107
+ eviction needed.
108
+
109
+ ### Instant suspend & resume
110
+
111
+ Because each Brainy instance has its own isolated data directory, Cor can
112
+ evict instances from memory and reload them later without data loss:
113
+
114
+ 1. **Eviction**: `brain.close()` flushes any pending writes to SSD, then
115
+ frees all in-memory structures.
116
+ 2. **Data persists**: the data directory on SSD remains intact.
117
+ 3. **Reload**: next request for the same workspace/tenant re-initializes from
118
+ SSD — binary mmap load is a single syscall, typically under 100 ms for a
119
+ 500-entity tenant *(PROJECTED — design target; reload-time benchmark
120
+ ships in `docs/verification-report.md`)*.
121
+
122
+ This is fundamentally different from traditional databases that need
123
+ replication or WAL replay. Each Brainy instance is a single-writer local
124
+ store — suspend/resume is an `munmap`/`mmap` away.
125
+
126
+ ### Capacity planning
127
+
128
+ #### Per-instance memory
129
+
130
+ > **PROJECTED — design targets, not measured.** Extrapolated from
131
+ > per-component memory analysis (HNSW graph + metadata inverted index +
132
+ > entity ID mapper sizings under their target capacity). Measured
133
+ > per-instance memory at each tier ships in `docs/verification-report.md`
134
+ > as part of Piece 9 of the cor 3.0 release.
135
+
136
+ | Entity count | Estimated memory | Typical use case |
137
+ |---|---|---|
138
+ | 100 | ~4 MB | Light workspace or new tenant |
139
+ | 500 | ~4.5 MB | Tenant after onboarding (time slots, customers, bookings) |
140
+ | 2,000 | ~6 MB | Medium workspace with documents and notes |
141
+ | 5,000 | ~9 MB | Large workspace with extensive content |
142
+ | 10,000 | ~14 MB | Power user with months of accumulated data |
143
+ | 50,000 | ~54 MB | Heavy workspace (years of data, many entity types) |
144
+
145
+ #### VM sizing
146
+
147
+ **Fixed overhead per process**: ~240 MB (88 MB embedding model + Node.js baseline).
148
+
149
+ > **PROJECTED — design targets, not measured.** Extrapolated from the
150
+ > per-instance memory model above plus a fixed per-process overhead
151
+ > estimate. Measured tenant capacity at each VM tier ships in
152
+ > `docs/verification-report.md`.
153
+
154
+ **Per-tenant workloads** (~500 entities average):
155
+
156
+ | VM | RAM | Active tenants | Cache | Notes |
157
+ |---|---|---|---|---|
158
+ | e2-medium | 4 GB | 30–40 | 500 MB | Minimum viable — tight under load |
159
+ | e2-standard-4 | 8 GB | 80–100 | 1.5 GB | Recommended — comfortable headroom |
160
+ | e2-standard-8 | 16 GB | 200+ | 4 GB | High-traffic — handles spikes easily |
161
+
162
+ **Per-user workloads** (mixed 100–50K entities):
163
+
164
+ | VM | RAM | Active (small) | Active (large) | Cache | Notes |
165
+ |---|---|---|---|---|---|
166
+ | e2-standard-4 | 8 GB | 200+ | 20–30 | 1.5 GB | Good for early-stage |
167
+ | e2-standard-8 | 16 GB | 500+ | 60–80 | 4 GB | Balanced density and speed |
168
+ | n2-standard-8 | 32 GB | 1,000+ | 150–200 | 8 GB | High density for growth |
169
+
170
+ ### Zero-config adaptation
171
+
172
+ Cor adapts automatically when the VM is resized:
173
+
174
+ - **Upgrade from 8 GB to 16 GB**: cache budget doubles, more instances stay
175
+ resident, fewer cold starts.
176
+ - **Downgrade or container limit**: cache shrinks, instances are evicted more
177
+ aggressively, data stays on SSD.
178
+ - **No config changes required** — Cor reads system memory and cgroup
179
+ limits at startup.
180
+
181
+ A VM can simultaneously run heterogeneous workloads: 100 small tenants
182
+ (500 entities each, ~4.5 MB) totaling 450 MB plus two large workspaces
183
+ (50K entities each, ~54 MB) totaling 108 MB, with dynamic cache filling the
184
+ remainder. The resource manager balances between them — when memory pressure
185
+ rises, the large idle workspace is evicted first (weighted by idle time ×
186
+ memory usage), not the small active tenant accessed two seconds ago.
187
+
188
+ ## Billion-scale single-machine search (DiskANN)
189
+
190
+ For single-instance workloads beyond ~10 million vectors, HNSW's memory cost
191
+ compounds quickly: at 1 B vectors × 384 dimensions, the float32 vectors alone
192
+ are ~1.5 TB of RAM, and the HNSW graph metadata adds another ~2 TB. No
193
+ commodity machine has that.
194
+
195
+ Cor ships a **100% pure-Rust DiskANN** engine
196
+ ([ADR-002](./ADR-002-diskann.md)) that targets **~5 ms search latency at
197
+ billion scale with ~20 GB RAM** *(PROJECTED — design target; awaiting
198
+ verification-report.md)*. The architecture is the Vamana α-pruned graph
199
+ (Subramanya et al., NeurIPS 2019) plus Product Quantization, and the on-disk
200
+ file is a single mmap-mappable contiguous layout. None of this requires a
201
+ separate service or external dependency — it's the same `@soulcraft/cor`
202
+ plugin.
203
+
204
+ > **PROJECTED — design targets, not measured.** Extrapolated from algorithm
205
+ > math (Vamana traversal cost + PQ ADC table-lookup cost + per-vector
206
+ > storage). The largest DiskANN dataset exercised in CI today is 10k
207
+ > synthetic dim=64 random vectors. Measured RAM + latency at 100M and 1B on
208
+ > real embedding corpora on cgroup-limited 32 GB hardware ship in
209
+ > `docs/verification-report.md` as part of Piece 9 of the cor 3.0 release.
210
+
211
+ | Vectors | RAM with DiskANN | RAM with HNSW | Search latency (warm cache) |
212
+ |---:|---:|---:|---:|
213
+ | 1 M | 0.5–2 GB | 0.5–2 GB | <1 ms |
214
+ | 10 M | 1–5 GB | 8–20 GB | 1–3 ms |
215
+ | 100 M | 5–20 GB | 80–200 GB (impractical) | 2–5 ms |
216
+ | 1 B | 20–70 GB | 1.5+ TB (single-machine impossible) | 5–10 ms |
217
+ | 10 B | 200–700 GB total (multi-shard on one box) | — | 5–15 ms |
218
+
219
+ A single DiskANN shard tops out near `u32::MAX` slots (~4 B vectors) by
220
+ construction — for 10 B+ on one machine the data is split across multiple
221
+ DiskANN shards on the same box. Same engine, same files, same API; the shard
222
+ boundary is internal to Cor.
223
+
224
+ These numbers are search latency for the index itself. End-to-end query
225
+ latency includes filesystem hydration of the returned entities — see the
226
+ roadmap section below for the remaining full-stack work.
227
+
228
+ DiskANN engages automatically when conditions are met (registered provider,
229
+ local filesystem path available, stable EntityIdMapper) or explicitly via
230
+ `config.index.type: 'diskann'`. All Brainy APIs — `add`, `search`, `relate`,
231
+ `searchSimilarVerbs`, `find` — work unchanged. DiskANN is an HNSW-shaped
232
+ drop-in, with a reversible migration:
233
+
234
+ ```typescript
235
+ import { BrainyData } from '@soulcraft/brainy'
236
+ import { register as registerCor } from '@soulcraft/cor'
237
+
238
+ const brain = new BrainyData({
239
+ storage: { type: 'filesystem', rootDirectory: '/data/idx' }
240
+ })
241
+ await registerCor(brain)
242
+ await brain.init()
243
+ // → [brainy] DiskANN engaged (path=/data/idx/_diskann/main.bin, dim=384)
244
+
245
+ const hits = await brain.search(queryVector, 10)
246
+ ```
247
+
248
+ Migration is reversible — production rollbacks are always available:
249
+
250
+ ```typescript
251
+ await brain.migrateToDiskAnn({
252
+ recallTarget: 0.95, // require ≥95% recall vs old index before swapping
253
+ paddingFactor: 1.2,
254
+ verifySampleSize: 100
255
+ })
256
+
257
+ // And back if needed:
258
+ await brain.migrateToHnsw()
259
+ ```
260
+
261
+ ## Multiple machines (horizontal scaling)
262
+
263
+ Once a workload exceeds what fits on one box — either entity count past
264
+ ~10 billion or query traffic past one machine's CPU — Brainy + Cor scale
265
+ horizontally by running multiple independent instances. There is **no
266
+ built-in coordinator**: each instance is self-contained, and the patterns
267
+ below describe how to compose them.
268
+
269
+ All five patterns work today without external dependencies. The Datomic-style
270
+ multi-region merge (#4) becomes cleanest with the immutable `Db` API shipping
271
+ in Brainy 8.0 / Cor 3.0.
272
+
273
+ > **Roadmap — managed distributed cluster (Cor 3.1, fast-follow).** The 3.0
274
+ > release is deliberately a **single-node engine that scales from a laptop to
275
+ > the most powerful single server**, plus the application-level horizontal
276
+ > patterns below (you wire the shard routing). A **built-in distributed
277
+ > cluster** — a coordinator that handles shard placement, cross-node
278
+ > consistency, rebalancing, and a single logical query surface across nodes —
279
+ > is the headline of the **3.1 fast-follow**, immediately after the 3.0 GA.
280
+ > Until then, "the most advanced cluster of servers" is served by (a) running
281
+ > the same binary on the largest single instance your cloud offers and (b) the
282
+ > self-managed sharding patterns in this section. 3.0's single-node numbers
283
+ > (`verification-report.md`) are the per-node performance the 3.1 cluster
284
+ > composes.
285
+
286
+ ### 1. Shard by entity
287
+
288
+ Hash the actor or tenant id and route to one of N brain instances:
289
+
290
+ ```
291
+ shard = hash(actorId) mod N
292
+ ```
293
+
294
+ Every node computes the same shard map — no coordinator state. Each user's
295
+ entities, edges, and history live on one shard. Cross-shard reads fan out in
296
+ parallel; each individual shard returns sub-millisecond.
297
+
298
+ **Trade-off**: Hot accounts (high-follower users in a social workload) create
299
+ a hot shard. Combine with the edge-cache pattern (#5) when that shows up.
300
+
301
+ **When to use**: Multi-tenant SaaS, social workloads, anything with a
302
+ natural partition key.
303
+
304
+ ### 2. Read replicas via filesystem sync
305
+
306
+ Brainy persists everything as JSON files in a directory. Replicate the
307
+ directory with `rsync`, `btrfs send`, or ZFS snapshots — POSIX primitives,
308
+ no application-level replication code. Writes hit the primary; reads scale
309
+ linearly across replicas.
310
+
311
+ **Trade-off**: Eventually consistent (sync lag = seconds, sometimes minutes
312
+ depending on tooling). Fine for read-heavy workloads where slightly-stale is
313
+ acceptable.
314
+
315
+ **When to use**: Read-heavy serving, regional read caches, analytics
316
+ followers that don't accept writes.
317
+
318
+ ### 3. Functional partitioning
319
+
320
+ Give each domain its own brain instance, tuned to its access pattern:
321
+
322
+ | Brain | Workload | Cor mode |
323
+ |---|---|---|
324
+ | Users / profiles | Small, hot | In-memory — sub-millisecond reads |
325
+ | Posts / content | Large, content-heavy | Hybrid — PQ + mmap |
326
+ | Social graph | Billion edges, append-heavy | On-disk + LSM verb storage |
327
+ | Media metadata | Small, mostly-immutable | In-memory |
328
+
329
+ Cor's adaptive mode selection picks the right mode per brain
330
+ automatically based on observed memory. Cross-domain queries do N short hops
331
+ at the application layer instead of complex joins inside the database.
332
+
333
+ **Trade-off**: Application-level composition replaces SQL joins. Subtype
334
+ indexes on the joining fields keep cross-domain lookups O(1).
335
+
336
+ **When to use**: Workloads with clean domain boundaries (the AT Protocol's
337
+ actor records / repo records / blob storage split is a natural example).
338
+
339
+ ### 4. Multi-region with eventual merge
340
+
341
+ Brainy 8.0 / Cor 3.0 ship a Datomic-style immutable `Db` API: every write
342
+ produces a new `Db` value via `brain.transact(tx)`, and one `Db` can be
343
+ folded into another via `Db.with(tx)`. Each region writes to its local brain
344
+ in real time. Regions exchange `tx` blobs over plain HTTP — no central
345
+ coordinator, no global lock.
346
+
347
+ **Trade-off**: Eventually consistent across regions. Conflicts resolve with
348
+ last-writer-wins per entity, or naturally via union-merge on graph edges
349
+ (follow lists are CRDTs — adding two different follows from two regions
350
+ produces the same set either way).
351
+
352
+ **When to use**: Geographic distribution where intra-region latency matters
353
+ more than global consistency. Social, content delivery, multi-region SaaS.
354
+
355
+ ### 5. Edge caches over a hot subset
356
+
357
+ Each edge node runs a small Brainy + Cor with a curated hot subset —
358
+ recent posts, popular accounts, viral content, whatever your access pattern
359
+ makes hot. Cache misses fall back to the primary brains over the same Brainy
360
+ API used everywhere else.
361
+
362
+ **Trade-off**: Eviction policy matters. Cor's `UnifiedCache` handles
363
+ in-memory hot data; a thin persisted brain at the edge handles
364
+ "recently popular" with controlled size.
365
+
366
+ **When to use**: Skewed access (10% of accounts get 90% of reads),
367
+ geographic latency requirements, public read APIs at scale.
368
+
369
+ ## Massive scale — composing patterns
370
+
371
+ For workloads at the limits — billion-entity multi-tenant SaaS, global social
372
+ networks, multi-region compliance — these patterns compose:
373
+
374
+ | Tier | Recommended composition |
375
+ |---|---|
376
+ | **10 B+ entities on one region** | #1 shard + #2 replicas + #3 functional partitioning |
377
+ | **10 B+ entities, hot-account pain** | Add #5 edge caches |
378
+ | **Multi-region** | Add #4 Datomic merge between regional clusters |
379
+ | **100 B+ entities global** | All five composed: regional clusters of sharded primaries with edge caches, merged across regions |
380
+
381
+ A concrete example for a social network at 30 million users and a few billion
382
+ posts: 64 sharded primaries (each a $1000 commodity box at ~470 K users per
383
+ shard) × 4 read replicas per shard = 256 boxes serving the regional workload.
384
+ Hot accounts get an edge cache layer in front; geographic redundancy adds a
385
+ second region merging via #4. Total bill of materials is hardware plus
386
+ operations — no database vendor invoice.
387
+
388
+ ## What stays the same across all tiers
389
+
390
+ | | Laptop | Single server | Multiple machines |
391
+ |---|---|---|---|
392
+ | **API** | Same Brainy methods | Same | Same — call the right instance |
393
+ | **Data files** | JSON on local disk | JSON on local disk | JSON on each instance |
394
+ | **Cor install** | `npm install @soulcraft/cor` | Same | Same on every instance |
395
+ | **Upgrade story** | `npm update` and restart | Same | Same, one instance at a time |
396
+ | **Operational dependencies** | None | None | None — no coordinator, no broker, no managed service |
397
+
398
+ Moving up the scaling ladder doesn't change your code. You add hardware,
399
+ configure routing, and the same engine runs at every step.
400
+
401
+ ## Path to 10 billion end-to-end
402
+
403
+ DiskANN solves the vector-search bottleneck. The supporting subsystems that
404
+ used to cap end-to-end operation at ~1 B have been widened to u64 throughout
405
+ in the Cor 3.0 release — what remains is the verification report and a
406
+ handful of bookkeeping items.
407
+
408
+ | Subsystem | Cortex 2.x ceiling | Cor 3.0 status |
409
+ |---|---|---|
410
+ | Entity ID space | ~4.3 B (u32) | ✅ u64 throughout (Piece 10) |
411
+ | EntityIdMapper persistence | ~500 M entries (JSON I/O) | ✅ Native binary mmap'd `uuid↔int` map (Piece 1) |
412
+ | Metadata sparse-field index | ~100 M entities | ✅ Native Rust LSM column store + Roaring64 widening (Piece E) |
413
+ | Verb-graph LSM trees | ~500 M edges | ✅ u64-keyed LSM + pair-value `verbs_source` for native BFS (Piece D) |
414
+ | Verb-id namespace | ~4.3 B verbs (u32) | ✅ u64 widened with IdSpace tagging (Piece B) |
415
+ | Verb endpoints store | In-memory HashMap | ✅ Packed-array mmap with adaptive sizing (Piece C) |
416
+ | FileSystemStorage sharding | ~2.5 M entities per directory | Configurable shard depth (in progress) |
417
+ | Search-result hydration | ~10 K results/query | Batch shard-grouped reads (planned) |
418
+ | End-to-end verification | — | `docs/verification-report.md` ships with the Cor 3.0 GA tag |
419
+
420
+ All fixes ship inside Cor — **no external databases, no competing
421
+ engines**. The 5–15 ms search latency target at 10 B vectors *(PROJECTED —
422
+ awaiting verification-report.md)* holds; the remaining work brings
423
+ filesystem hydration and shard layout up to match.
424
+
425
+ ## What we don't ship (deliberately)
426
+
427
+ Brainy + Cor deliberately does **not** ship:
428
+
429
+ - A built-in coordinator for transparent multi-machine clustering.
430
+ - A managed-service tier or SaaS deployment.
431
+ - A query language that hides machine boundaries.
432
+
433
+ The architecture is single-box-per-instance, by design. At billion-per-tenant
434
+ scale, a single commodity machine delivers the workload, and the cost of
435
+ operating a distributed coordinator outweighs the benefit. Horizontal scaling
436
+ means running more instances — same engine, no shared state.
437
+
438
+ If a future workload genuinely requires N-node coherence inside one logical
439
+ database, we'll revisit. We will not ship that complexity speculatively.
@@ -0,0 +1,246 @@
1
+ ---
2
+ title: Snapshot Safety
3
+ slug: cor/snapshot-safety
4
+ public: true
5
+ category: cor
6
+ template: concept
7
+ order: 10
8
+ description: How cor guarantees coherent point-in-time views to any reader of any persistent file at any moment, without relying on caller coordination.
9
+ next:
10
+ - cor/performance-budget
11
+ - cor/comparison
12
+ ---
13
+
14
+ # Snapshot Safety
15
+
16
+ This document is a contract. Every persistent file cor writes
17
+ satisfies the property described here. Code that violates the
18
+ contract does not ship.
19
+
20
+ ## What "snapshot" means in cor
21
+
22
+ Cor is the native acceleration layer underneath brainy. Brainy
23
+ exposes a transactional `Db` API with `asOf`, `with`, and
24
+ `transact` semantics, and persists durable state via `db.persist()`.
25
+ Brainy's persistence model is **hard-link snapshots**: at `persist()`
26
+ time, every file under the brain's root directory is hard-linked
27
+ into the snapshot target.
28
+
29
+ A hard-link shares the inode with the live file. The directory
30
+ entry is a separate pointer to the same on-disk data. If the live
31
+ writer mutates the inode after the link is taken, those mutations
32
+ are visible to any reader of the snapshot's hard-link. The
33
+ filesystem provides no isolation between the two views.
34
+
35
+ That means hard-link snapshots are only point-in-time coherent for
36
+ files that are **immutable post-link** or only modified via
37
+ **atomic rename**. Any in-place mutation pattern — even one
38
+ protected by a SeqLock or Release-Acquire pair for live concurrent
39
+ reads — leaks live state into the snapshot. SeqLock makes
40
+ concurrent readers retry until consistent; it does not freeze
41
+ the bytes a snapshot reader holds.
42
+
43
+ ## The cor promise
44
+
45
+ Cor guarantees that **any hard-link of any persistent file taken
46
+ at any moment** opens a coherent point-in-time view, regardless of
47
+ what the live writer is doing concurrently. The view is
48
+ self-contained: no coordination with brainy's pin/release protocol
49
+ or transact-commit boundary is required for the snapshot reader to
50
+ see consistent state.
51
+
52
+ The coordination layer (brainy's `VersionedIndexProvider`
53
+ `pin`/`release` + the post-commit applier protocol described in
54
+ the handoff Section L.2) provides cross-subsystem **generation**
55
+ consistency — every cor provider answers reads at the same
56
+ brainy generation. The per-file snapshot safety described here is
57
+ the load-bearing layer underneath it. Coordination is
58
+ defense-in-depth, not the primary safety mechanism.
59
+
60
+ ## Why this is hard
61
+
62
+ mmap'd files plus hard-links are a foundational trap. A file that
63
+ "feels safe" because individual reads are atomic (`AtomicU8`,
64
+ `AtomicU32`, single-word store) is not snapshot-safe when:
65
+
66
+ - The logical state requires multiple atomic updates to publish
67
+ (a bucket split touches both a payload region and a directory
68
+ region; a memtable append touches both the record bytes and the
69
+ header's `next_offset`).
70
+ - The file grows in place (a directory doubling, a bucket
71
+ allocation past the current high-water mark, a memtable log
72
+ growth event).
73
+ - A "frozen" region is later overwritten (a memtable log
74
+ truncate-to-header pattern; a tombstone state byte replacing an
75
+ occupied state byte).
76
+
77
+ Each of these has a window during which a hard-link reader sees
78
+ a state that the live writer didn't intend to publish. The window
79
+ can be nanoseconds; it can be milliseconds; it is non-zero. Any
80
+ non-zero window violates the contract.
81
+
82
+ The pattern that ALL existing alternatives fail at:
83
+
84
+ > "This file is safe to hard-link because the access pattern only
85
+ > writes to never-written regions" — false the moment any other
86
+ > caller starts overwriting.
87
+ > "This file is safe to hard-link because the SeqLock guard makes
88
+ > readers retry" — true for live readers; false for a snapshot
89
+ > reader holding a hard-linked inode that the writer is still
90
+ > mutating.
91
+ > "This file is safe to hard-link because brainy only snapshots at
92
+ > commit boundaries" — true under the current cor-brainy
93
+ > coordination, false as a property of the file. Contingent
94
+ > safety is not safety; the moment a customer uses cor outside
95
+ > brainy (or brainy changes the coordination contract) the
96
+ > assumption silently breaks.
97
+
98
+ ## The canonical pattern: LSM shadow-page
99
+
100
+ Every cor storage primitive that holds mutable state uses the
101
+ same three-layer pattern. The G4.3 verb endpoint store
102
+ (`crate::verb_endpoints_shadow_page::ShadowPageEndpointsStore`)
103
+ is the reference implementation.
104
+
105
+ ```
106
+ {store-dir}/
107
+ head # PointerFile, atomic-rename swap
108
+ base-N.ext # frozen base for generation N (immutable post-write)
109
+ delta-N+1.log # append-only log for writes since base-N
110
+ delta-N+1.log.K.archive # rotation archives, retention-bounded
111
+ ```
112
+
113
+ ### Three layers
114
+
115
+ 1. **Frozen base** (`base-N.ext`) — the consolidated state at the
116
+ start of generation N+1. Never mutated in place after it is
117
+ sealed. Compaction produces `base-N+1.ext` as a brand new file,
118
+ never edits the existing one. The old base file's inode
119
+ remains reachable by any hard-link that captured it before the
120
+ pointer swap; once no snapshot needs it, the directory entry
121
+ is unlinked and the kernel reclaims the storage.
122
+
123
+ 2. **Append-only delta** (`delta-N+1.log`) — every mutation since
124
+ the base was sealed lands here as a fixed-format record
125
+ (length-prefix + payload + CRC32, framed so a torn write at
126
+ the tail is detected at exactly the boundary it occurred).
127
+ Snapshot readers replay the delta from the head's recorded
128
+ tail offset; live writers append past that offset without
129
+ affecting what the snapshot reader sees. On compaction
130
+ commit the delta is renamed to a `.archive` sibling and a
131
+ fresh empty delta replaces it.
132
+
133
+ 3. **Atomic head pointer** (`head`) — a tiny text file named by
134
+ `PointerFile` (`diskann::pointer_file`). Names the current
135
+ `base` and `delta` files plus the current generation
136
+ counter. Updated via `save_atomic`: write the new content to
137
+ `head.next`, msync, `rename(head.next, head)`. POSIX `rename`
138
+ is atomic. A snapshot taken before the rename captures the
139
+ old head's inode; a snapshot taken after captures the new.
140
+ There is no window in which the snapshot sees a half-written
141
+ head.
142
+
143
+ ### Why this is snapshot-safe
144
+
145
+ A hard-link snapshot captures a directory entry for each file
146
+ present at the moment of the link. The directory entries point
147
+ at inodes. After the snapshot, the live writer mutates the
148
+ delta's inode (append-only growth) and creates new inodes for
149
+ the next base and the new delta on compaction. The snapshot's
150
+ directory entries are untouched.
151
+
152
+ A snapshot reader opening the snapshot directory resolves `head`
153
+ to a (base, delta) pair. That base file's inode is the one the
154
+ snapshot captured — frozen since it was sealed. That delta file's
155
+ inode is the one the snapshot captured — the writer's subsequent
156
+ appends went past the offset the snapshot recorded, so the
157
+ snapshot reader's replay stops at the right boundary. Compaction
158
+ events in the live store produce new base + delta files at new
159
+ inodes that the snapshot's directory has no reference to.
160
+
161
+ The contract holds at every moment, not just at coordination
162
+ boundaries.
163
+
164
+ ## Inventory: how cor's storage primitives implement this
165
+
166
+ | Subsystem | Primitive | Pattern | Status |
167
+ |---|---|---|---|
168
+ | Verb endpoints | `ShadowPageEndpointsStore` | LSM shadow-page (canonical) | shipped (G4.3) |
169
+ | Memtable log | `MemtableLog` rotation | rotation-on-commit (delta-only, no separate base) | self-contained safety in scope for G4.2 redo |
170
+ | Entity int → UUID | `BinaryIdMapper::I2uStore` (`int_to_uuid.bin`) | currently in-place with Release-Acquire state byte | LSM conversion in scope for G4.4 redo |
171
+ | UUID → entity int | `MmapKv` (`uuid_to_int.mkv`, `verb-uuid-to-int.mkv`) | currently in-place extendible hash | LSM conversion in scope for G4.1 |
172
+ | LSM SSTables | `MmapSSTable` files | written once, atomic-rename committed, immutable thereafter | correct as shipped |
173
+ | DiskANN index | `.dkann` file | written once during build, mutated only at delete-marker byte | audit in scope |
174
+ | Column store segments | `column_store/segment-*.col` | audit in scope | audit in scope |
175
+ | LSM manifest | `manifest.json` | atomic-rename | correct as shipped |
176
+ | DiskANN build temp | `_system/scratch/build-*.adj` | excluded from brainy's persist set | correct as shipped (G4.5) |
177
+
178
+ The "in scope" rows are the cor 3.0 release plan items. No
179
+ piece ships under the weaker "contingent safety" bar.
180
+
181
+ ## Forbidden patterns
182
+
183
+ 1. **In-place multi-word mutation of any byte range a snapshot can
184
+ observe.** Includes overwriting an occupied slot's payload,
185
+ rewriting a directory region on growth, in-place truncate of a
186
+ log file. If the operation requires multiple stores to publish
187
+ the new logical state, it cannot mutate bytes a hard-link
188
+ reader holds. Convert to atomic-rename of a new file.
189
+
190
+ 2. **"Safe under access pattern X" reasoning.** If a primitive is
191
+ only snapshot-safe because the current caller is insert-only,
192
+ monotonic, single-writer, or otherwise constrained, the
193
+ primitive itself is not safe. Either the primitive's contract
194
+ gets tightened (its callers are checked against it) or the
195
+ primitive is converted to the canonical pattern.
196
+
197
+ 3. **Coordination-dependent safety.** If snapshot safety only holds
198
+ when brainy snapshots at commit boundaries, when pin/release
199
+ has been called in some specific order, or when some other
200
+ external invariant is met, that's not safety. Cor primitives
201
+ must be safe in isolation. Coordination is for cross-subsystem
202
+ generation consistency, not for per-file safety.
203
+
204
+ 4. **Hidden tombstones in frozen files.** A frozen base file
205
+ cannot grow new tombstone bytes after it is sealed. Tombstones
206
+ are recorded in the delta log; compaction omits tombstoned
207
+ entries when writing the next base.
208
+
209
+ 5. **Crash-recovery as a substitute for atomic publish.** A
210
+ recovery pass that "fixes up" half-written state on open is
211
+ acceptable as a defense against crashes. It is not a
212
+ substitute for atomic publish during normal operation. A
213
+ snapshot taken between the half-write and the next open is
214
+ not protected by crash recovery.
215
+
216
+ ## Verification methodology
217
+
218
+ Every storage primitive that holds mutable state ships with at
219
+ least one `snapshot_via_hard_link_survives_*` property test:
220
+
221
+ - Set up the live store.
222
+ - Perform writes.
223
+ - Hard-link every file in the store directory to a sibling
224
+ snapshot directory.
225
+ - Continue mutating the live store (further writes, compactions,
226
+ rotations).
227
+ - Open the snapshot directory as an independent store instance.
228
+ - Assert that the snapshot reads return exactly the pre-snapshot
229
+ state, with no live-writer drift visible.
230
+
231
+ The reference test is
232
+ `verb_endpoints_shadow_page::tests::snapshot_via_hard_link_survives_compaction`.
233
+ Every G4 conversion ships with an analogous test.
234
+
235
+ ## See also
236
+
237
+ - `docs/performance-budget.md` — the latency budget cor commits
238
+ to. Snapshot safety and performance are co-designed: the LSM
239
+ shadow-page pattern's hot read path is sub-microsecond via the
240
+ in-memory delta cache.
241
+ - `PLATFORM-HANDOFF.md` Section L.2 (brainy's post-commit applier
242
+ contract) and Section R (`VersionedIndexProvider` interface) —
243
+ the coordination layer that turns per-file snapshot safety into
244
+ cross-subsystem generation consistency.
245
+ - `docs/file-mutation-inventory.md` — per-file analysis of the
246
+ pre-G4 state and the conversions in scope.