@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.
- package/LICENSE +16 -0
- package/README.md +328 -0
- package/dist/aggregation/NativeAggregationEngine.d.ts +118 -0
- package/dist/aggregation/NativeAggregationEngine.js +186 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +236 -0
- package/dist/graph/GraphAccelerationAdapter.d.ts +153 -0
- package/dist/graph/GraphAccelerationAdapter.js +326 -0
- package/dist/graph/NativeGraphAdjacencyIndex.d.ts +286 -0
- package/dist/graph/NativeGraphAdjacencyIndex.js +944 -0
- package/dist/hnsw/AdaptiveDiskAnnModeSelector.d.ts +187 -0
- package/dist/hnsw/AdaptiveDiskAnnModeSelector.js +313 -0
- package/dist/hnsw/NativeDiskAnnWrapper.d.ts +489 -0
- package/dist/hnsw/NativeDiskAnnWrapper.js +1456 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +29 -0
- package/dist/legacyLayoutGuard.d.ts +93 -0
- package/dist/legacyLayoutGuard.js +237 -0
- package/dist/license/constants.d.ts +30 -0
- package/dist/license/constants.js +30 -0
- package/dist/license/onlineValidator.d.ts +43 -0
- package/dist/license/onlineValidator.js +225 -0
- package/dist/license/types.d.ts +39 -0
- package/dist/license/types.js +5 -0
- package/dist/license.d.ts +64 -0
- package/dist/license.js +165 -0
- package/dist/migration/MigrationCoordinator.d.ts +171 -0
- package/dist/migration/MigrationCoordinator.js +249 -0
- package/dist/migration/indexEpochGuard.d.ts +163 -0
- package/dist/migration/indexEpochGuard.js +202 -0
- package/dist/native/NativeEmbeddingEngine.d.ts +79 -0
- package/dist/native/NativeEmbeddingEngine.js +318 -0
- package/dist/native/NativeRoaringBitmap32.d.ts +114 -0
- package/dist/native/NativeRoaringBitmap32.js +221 -0
- package/dist/native/ffi.d.ts +20 -0
- package/dist/native/ffi.js +48 -0
- package/dist/native/idMapperTestSupport.d.ts +67 -0
- package/dist/native/idMapperTestSupport.js +112 -0
- package/dist/native/index.d.ts +49 -0
- package/dist/native/index.js +87 -0
- package/dist/native/napi.d.ts +21 -0
- package/dist/native/napi.js +88 -0
- package/dist/native/types.d.ts +1298 -0
- package/dist/native/types.js +16 -0
- package/dist/plugin.d.ts +50 -0
- package/dist/plugin.js +388 -0
- package/dist/providerContracts.d.ts +277 -0
- package/dist/providerContracts.js +38 -0
- package/dist/resource/OsMemoryProbe.d.ts +175 -0
- package/dist/resource/OsMemoryProbe.js +206 -0
- package/dist/resource/ResourceManager.d.ts +491 -0
- package/dist/resource/ResourceManager.js +960 -0
- package/dist/utils/ColumnManifest.d.ts +97 -0
- package/dist/utils/ColumnManifest.js +129 -0
- package/dist/utils/NativeColumnStore.d.ts +284 -0
- package/dist/utils/NativeColumnStore.js +685 -0
- package/dist/utils/NativeMetadataIndex.d.ts +882 -0
- package/dist/utils/NativeMetadataIndex.js +2631 -0
- package/dist/utils/NativeUnifiedCache.d.ts +87 -0
- package/dist/utils/NativeUnifiedCache.js +273 -0
- package/dist/utils/binaryIdMapperFactory.d.ts +59 -0
- package/dist/utils/binaryIdMapperFactory.js +94 -0
- package/dist/utils/collation.d.ts +30 -0
- package/dist/utils/collation.js +40 -0
- package/dist/utils/columnStoreTypes.d.ts +161 -0
- package/dist/utils/columnStoreTypes.js +29 -0
- package/dist/utils/nativeBinaryEntityIdMapper.d.ts +211 -0
- package/dist/utils/nativeBinaryEntityIdMapper.js +381 -0
- package/dist/utils/nativeEntityIdMapper.d.ts +111 -0
- package/dist/utils/nativeEntityIdMapper.js +170 -0
- package/docs/ADR-002-diskann-100-percent-rust.md +294 -0
- package/docs/ADR-003-semantic-time-travel.md +100 -0
- package/docs/aggregation.md +251 -0
- package/docs/comparison.md +162 -0
- package/docs/deployment-limits.md +87 -0
- package/docs/diskann.md +184 -0
- package/docs/migration-3.0.md +396 -0
- package/docs/performance-budget.md +274 -0
- package/docs/performance.md +117 -0
- package/docs/scaling.md +439 -0
- package/docs/snapshot-safety.md +246 -0
- package/docs/u64-id-space.md +214 -0
- package/docs/verification-report.md +670 -0
- package/native/brainy-native.node +0 -0
- package/native/index.d.ts +3916 -0
- package/package.json +92 -0
- package/scripts/migrate-cortex-2x-to-3x.mjs +970 -0
package/docs/scaling.md
ADDED
|
@@ -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.
|