@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,187 @@
1
+ /**
2
+ * @module hnsw/AdaptiveDiskAnnModeSelector
3
+ * @description **Adaptive DiskANN** mode selector — Piece I of the
4
+ * cor 3.0 plan. Closes the loop on Track 2: the previous pieces
5
+ * built the *capability* (Piece G — Mode 1 no-PQ; Piece H — open-time
6
+ * madvise profile; Piece J — `/proc/meminfo` observation), and this
7
+ * module is the *decision* that wires those signals together into a
8
+ * single mode choice at build / open time.
9
+ *
10
+ * ## What this picks
11
+ *
12
+ * - **`"in-memory"`** — Mode 1. Build path: skip PQ entirely; vectors
13
+ * stay RAM-resident; the walk is exact (sub-ms latency). Open
14
+ * path: only valid when the on-disk header already records
15
+ * `pq_m=0` — otherwise a PQ-enabled file would force the
16
+ * `search_pq` path regardless of the open-time hint, so "in-memory"
17
+ * open mode on a PQ file would only waste RAM on vectors the walk
18
+ * doesn't read.
19
+ * - **`"hybrid"`** — Mode 2. PQ codes pinned in RAM via `MADV_WILLNEED`;
20
+ * vectors paged on demand for rerank. The default for medium-scale
21
+ * brains.
22
+ * - **`"on-disk"`** — Mode 3. PQ codes can page out under memory
23
+ * pressure. The choice for memory-constrained or multi-tenant
24
+ * deployments where many brainy+cor instances share a box.
25
+ *
26
+ * ## Algorithm
27
+ *
28
+ * Per-brain RAM budget = `0.5 × (availableBytes / max(1, activeBrains))`.
29
+ * The 0.5 headroom factor leaves the other half for OS slop, query
30
+ * intermediates, and the brain's other subsystems (metadata LSM,
31
+ * embeddings, etc.).
32
+ *
33
+ * 1. If the input names a Mode 1 file (`pqM === 0` from the header),
34
+ * return `"in-memory"` — no other open mode is valid for a no-PQ
35
+ * file.
36
+ * 2. At BUILD time (`pqM` undefined), try Mode 1 first: if the
37
+ * vectors + graph cost fits the RAM budget AND the build cost
38
+ * (`N × dim`) is below the dim-aware ceiling from
39
+ * {@link mode1BuildCostCeiling}, build no-PQ. Above the ceiling,
40
+ * Mode 1's `O(N · dim)` build becomes the limiting factor —
41
+ * Mode 2's PQ-coded distance compute is 5-10× faster at the same
42
+ * N because it replaces a `dim`-wide dot product with a
43
+ * `m=16`-wide table lookup. Falling through to PQ at large N is
44
+ * the right call even when vectors fit in RAM.
45
+ * 3. For PQ-enabled files (or build-time-too-big cases), compare the
46
+ * codes-section + codebook RAM cost against the budget:
47
+ * fits → `"hybrid"`; doesn't fit → `"on-disk"`.
48
+ * 4. If V8 heap pressure is `"critical"`, demote `"hybrid"` →
49
+ * `"on-disk"` — we don't want to pile JS-heap pressure on top of
50
+ * OS-RAM pressure when GC thrash will cost more than the SSD
51
+ * page-in.
52
+ *
53
+ * ## Decision granularity
54
+ *
55
+ * Brain-open-time decision; not re-evaluated mid-flight. Mode
56
+ * switches mid-flight would require a rebuild (Mode 1 → Mode 2 means
57
+ * training a PQ codebook; Mode 2 → Mode 1 means evicting codes +
58
+ * pinning vectors), so the selector picks once and the brain runs in
59
+ * the chosen profile until restart. A future piece may explore
60
+ * triggered rebuilds when sustained pressure crosses a threshold;
61
+ * for now the choice is sticky.
62
+ */
63
+ import { ResourceManager } from '../resource/ResourceManager.js';
64
+ /**
65
+ * The three operating modes Adaptive DiskANN selects between.
66
+ *
67
+ * The strings match the vocabulary the napi surface accepts on
68
+ * `DiskAnnConfig.mode` (Piece G) and `NativeDiskAnn.openExisting`
69
+ * (Piece H), so the selector's output flows through to the Rust
70
+ * layer without translation.
71
+ */
72
+ export type AdaptiveDiskAnnMode = 'in-memory' | 'hybrid' | 'on-disk';
73
+ /**
74
+ * Per-brain statistics the selector consumes. At OPEN time these
75
+ * come from the file header; at BUILD time the caller supplies the
76
+ * expected shape (omit `pqM` to signal "build-time decision").
77
+ */
78
+ export interface BrainStats {
79
+ /** Number of vectors. For a new build, the expected total. */
80
+ nodeCount: number;
81
+ /** Vector dimension. */
82
+ dim: number;
83
+ /** Vamana max degree (R). */
84
+ maxDegree: number;
85
+ /**
86
+ * PQ subspaces. `0` is the Mode 1 marker — file is already
87
+ * no-PQ; only `"in-memory"` is a valid open mode. Omit (or pass
88
+ * `undefined`) at build time so the selector can choose between
89
+ * Mode 1 (no PQ) and Modes 2/3 (PQ-enabled).
90
+ */
91
+ pqM?: number;
92
+ }
93
+ /**
94
+ * Resource situation at decision time. The
95
+ * {@link ResourceManager} is the canonical source; tests construct
96
+ * directly.
97
+ */
98
+ export interface SystemMemoryState {
99
+ /**
100
+ * Kernel-reported `MemAvailable` (Linux) or `os.freemem()`
101
+ * fallback. The selector's primary RAM signal.
102
+ */
103
+ availableBytes: number;
104
+ /**
105
+ * Number of brainy instances already tracked by the
106
+ * ResourceManager. Divides `availableBytes` into per-brain
107
+ * shares.
108
+ */
109
+ activeBrains: number;
110
+ /**
111
+ * V8 heap pressure classification (same three-level shape as the
112
+ * system-RSS pressure). `"critical"` demotes Hybrid → OnDisk.
113
+ * Optional — omit if the caller doesn't observe V8 heap.
114
+ */
115
+ v8Pressure?: 'normal' | 'elevated' | 'critical';
116
+ }
117
+ /**
118
+ * Reason codes for the chosen mode. Surface for production
119
+ * diagnostics — a single string is enough to reconstruct the
120
+ * selector's reasoning from a log line.
121
+ */
122
+ export type ModeSelectionReason = 'no-pq-file' | 'fits-in-memory' | 'mode1-build-too-large' | 'codes-pinned' | 'codes-paged' | 'v8-pressure-demote';
123
+ /**
124
+ * Selector output. The reasoning fields make the decision
125
+ * diagnosable: a production log line can capture `mode`, `reason`,
126
+ * and `perBrainAvailable` in ~50 bytes and the operator can
127
+ * reconstruct exactly why the brain landed in its current profile.
128
+ */
129
+ export interface ModeSelection {
130
+ mode: AdaptiveDiskAnnMode;
131
+ reason: ModeSelectionReason;
132
+ /** RAM cost the selector estimated for the chosen mode, in bytes. */
133
+ estimatedBytes: number;
134
+ /** Per-brain available memory used in the calculation. */
135
+ perBrainAvailable: number;
136
+ }
137
+ /**
138
+ * Zero-config PQ subspace count `m` for a vector dimension, targeting
139
+ * `dsub ≈ 8`. This is the **exact TS mirror of the native
140
+ * `derive_pq_m`** (cor's `diskann::builder::derive_pq_m` / the
141
+ * `pqDeriveM` napi export): `m` must divide `dim` (the codebook is
142
+ * laid out contiguously), so it returns the divisor of `dim` whose
143
+ * `dsub = dim/m` is closest to the target, ties breaking toward the
144
+ * smaller `dsub` (more subspaces → better recall). For every common
145
+ * embedding dimension (a multiple of 8) this is `dim / 8`.
146
+ *
147
+ * Used at BUILD time to estimate the codes-section size so the mode
148
+ * selector's hybrid/on-disk choice matches the `m` the native build
149
+ * will actually pick. A cross-check test pins TS ≡ native for a range
150
+ * of dims. An existing file's header always provides the real `m`.
151
+ *
152
+ * @param dim - Observed vector dimension (> 0).
153
+ * @returns The derived subspace count `m` (always divides `dim`).
154
+ */
155
+ export declare function derivePqM(dim: number): number;
156
+ /**
157
+ * Returns the build-cost ceiling above which Mode 1 falls through to
158
+ * Mode 2 at this dim. See {@link MODE1_BUILD_COST_CEILING_SMALL_DIM}.
159
+ *
160
+ * Exposed for tests that need to assert the boundary calibration
161
+ * directly; not exported beyond the module.
162
+ */
163
+ export declare function mode1BuildCostCeiling(dim: number): number;
164
+ /**
165
+ * Pure selector. The wrapper code calls this with concrete inputs;
166
+ * tests construct {@link BrainStats} + {@link SystemMemoryState}
167
+ * directly to assert each branch.
168
+ */
169
+ export declare function pickMode(stats: BrainStats, sys: SystemMemoryState): ModeSelection;
170
+ /**
171
+ * Convenience wrapper that pulls inputs from a {@link ResourceManager}
172
+ * (default: the singleton) and returns the selector's choice.
173
+ * Production code in `NativeDiskAnnWrapper` calls this at build and
174
+ * open time; tests prefer {@link pickMode} directly so they can pin
175
+ * the system state without touching the singleton.
176
+ */
177
+ export declare function selectModeFromResourceManager(stats: BrainStats, rm?: ResourceManager): ModeSelection;
178
+ /**
179
+ * The mode `MappedIndex::open_with_mode(path, Auto)` would resolve to
180
+ * for a given header (Piece H semantics). Mirrored here so the
181
+ * wrapper can skip a reopen when the selector agrees with Auto —
182
+ * avoids opening the file twice on the cold path.
183
+ */
184
+ export declare function autoModeForHeader(header: {
185
+ pqM: number;
186
+ }): AdaptiveDiskAnnMode;
187
+ //# sourceMappingURL=AdaptiveDiskAnnModeSelector.d.ts.map
@@ -0,0 +1,313 @@
1
+ /**
2
+ * @module hnsw/AdaptiveDiskAnnModeSelector
3
+ * @description **Adaptive DiskANN** mode selector — Piece I of the
4
+ * cor 3.0 plan. Closes the loop on Track 2: the previous pieces
5
+ * built the *capability* (Piece G — Mode 1 no-PQ; Piece H — open-time
6
+ * madvise profile; Piece J — `/proc/meminfo` observation), and this
7
+ * module is the *decision* that wires those signals together into a
8
+ * single mode choice at build / open time.
9
+ *
10
+ * ## What this picks
11
+ *
12
+ * - **`"in-memory"`** — Mode 1. Build path: skip PQ entirely; vectors
13
+ * stay RAM-resident; the walk is exact (sub-ms latency). Open
14
+ * path: only valid when the on-disk header already records
15
+ * `pq_m=0` — otherwise a PQ-enabled file would force the
16
+ * `search_pq` path regardless of the open-time hint, so "in-memory"
17
+ * open mode on a PQ file would only waste RAM on vectors the walk
18
+ * doesn't read.
19
+ * - **`"hybrid"`** — Mode 2. PQ codes pinned in RAM via `MADV_WILLNEED`;
20
+ * vectors paged on demand for rerank. The default for medium-scale
21
+ * brains.
22
+ * - **`"on-disk"`** — Mode 3. PQ codes can page out under memory
23
+ * pressure. The choice for memory-constrained or multi-tenant
24
+ * deployments where many brainy+cor instances share a box.
25
+ *
26
+ * ## Algorithm
27
+ *
28
+ * Per-brain RAM budget = `0.5 × (availableBytes / max(1, activeBrains))`.
29
+ * The 0.5 headroom factor leaves the other half for OS slop, query
30
+ * intermediates, and the brain's other subsystems (metadata LSM,
31
+ * embeddings, etc.).
32
+ *
33
+ * 1. If the input names a Mode 1 file (`pqM === 0` from the header),
34
+ * return `"in-memory"` — no other open mode is valid for a no-PQ
35
+ * file.
36
+ * 2. At BUILD time (`pqM` undefined), try Mode 1 first: if the
37
+ * vectors + graph cost fits the RAM budget AND the build cost
38
+ * (`N × dim`) is below the dim-aware ceiling from
39
+ * {@link mode1BuildCostCeiling}, build no-PQ. Above the ceiling,
40
+ * Mode 1's `O(N · dim)` build becomes the limiting factor —
41
+ * Mode 2's PQ-coded distance compute is 5-10× faster at the same
42
+ * N because it replaces a `dim`-wide dot product with a
43
+ * `m=16`-wide table lookup. Falling through to PQ at large N is
44
+ * the right call even when vectors fit in RAM.
45
+ * 3. For PQ-enabled files (or build-time-too-big cases), compare the
46
+ * codes-section + codebook RAM cost against the budget:
47
+ * fits → `"hybrid"`; doesn't fit → `"on-disk"`.
48
+ * 4. If V8 heap pressure is `"critical"`, demote `"hybrid"` →
49
+ * `"on-disk"` — we don't want to pile JS-heap pressure on top of
50
+ * OS-RAM pressure when GC thrash will cost more than the SSD
51
+ * page-in.
52
+ *
53
+ * ## Decision granularity
54
+ *
55
+ * Brain-open-time decision; not re-evaluated mid-flight. Mode
56
+ * switches mid-flight would require a rebuild (Mode 1 → Mode 2 means
57
+ * training a PQ codebook; Mode 2 → Mode 1 means evicting codes +
58
+ * pinning vectors), so the selector picks once and the brain runs in
59
+ * the chosen profile until restart. A future piece may explore
60
+ * triggered rebuilds when sustained pressure crosses a threshold;
61
+ * for now the choice is sticky.
62
+ */
63
+ import { ResourceManager, } from '../resource/ResourceManager.js';
64
+ /**
65
+ * Fraction of per-brain available memory the chosen mode's working
66
+ * set must fit within. Half the available; the other half is for OS
67
+ * slop, query intermediates, and the brain's other subsystems
68
+ * (metadata LSM, embeddings, page-cache headroom).
69
+ */
70
+ const HEADROOM_FACTOR = 0.5;
71
+ /**
72
+ * Target sub-vector dimension for the zero-config PQ `m` derivation —
73
+ * mirrors the native `PQ_TARGET_DSUB`. See {@link derivePqM}.
74
+ */
75
+ const PQ_TARGET_DSUB = 8;
76
+ /**
77
+ * Zero-config PQ subspace count `m` for a vector dimension, targeting
78
+ * `dsub ≈ 8`. This is the **exact TS mirror of the native
79
+ * `derive_pq_m`** (cor's `diskann::builder::derive_pq_m` / the
80
+ * `pqDeriveM` napi export): `m` must divide `dim` (the codebook is
81
+ * laid out contiguously), so it returns the divisor of `dim` whose
82
+ * `dsub = dim/m` is closest to the target, ties breaking toward the
83
+ * smaller `dsub` (more subspaces → better recall). For every common
84
+ * embedding dimension (a multiple of 8) this is `dim / 8`.
85
+ *
86
+ * Used at BUILD time to estimate the codes-section size so the mode
87
+ * selector's hybrid/on-disk choice matches the `m` the native build
88
+ * will actually pick. A cross-check test pins TS ≡ native for a range
89
+ * of dims. An existing file's header always provides the real `m`.
90
+ *
91
+ * @param dim - Observed vector dimension (> 0).
92
+ * @returns The derived subspace count `m` (always divides `dim`).
93
+ */
94
+ export function derivePqM(dim) {
95
+ if (dim <= PQ_TARGET_DSUB)
96
+ return 1;
97
+ let bestM = 1;
98
+ let bestDsub = dim;
99
+ let bestScore = Math.abs(dim - PQ_TARGET_DSUB);
100
+ for (let d = 1; d * d <= dim; d++) {
101
+ if (dim % d !== 0)
102
+ continue;
103
+ for (const m of [d, dim / d]) {
104
+ const dsub = dim / m;
105
+ const score = Math.abs(dsub - PQ_TARGET_DSUB);
106
+ if (score < bestScore || (score === bestScore && dsub < bestDsub)) {
107
+ bestScore = score;
108
+ bestDsub = dsub;
109
+ bestM = m;
110
+ }
111
+ }
112
+ }
113
+ return bestM;
114
+ }
115
+ /**
116
+ * Default codebook ksub (centroids per subspace) — 256 is the
117
+ * 8-bit-code published default the build path uses (Piece 13).
118
+ */
119
+ const DEFAULT_KSUB = 256;
120
+ /**
121
+ * Maximum `N × dim` above which Mode 1's `O(N · dim)` build cost
122
+ * dominates the selector decision. Even when the vectors + graph fit
123
+ * in RAM, the build time becomes the limiting factor at this scale —
124
+ * Mode 2's PQ-coded distance compute is 5-10× faster (16-byte code
125
+ * lookup vs `dim`-wide dot product) so a brain that builds in ~50
126
+ * minutes under Mode 1 builds in ~5-10 minutes under Mode 2 at the
127
+ * cost of ~3× query latency (sub-ms → ~3 ms p50).
128
+ *
129
+ * The ceiling is **dim-aware** because per-cost-unit wall-clock varies
130
+ * significantly with dim — SIMD width, cache footprint, and vectorised
131
+ * distance compute all favour smaller dims by roughly an order of
132
+ * magnitude per cost-unit. A single dim-blind ceiling miscalibrates
133
+ * one regime or the other.
134
+ *
135
+ * Anchor measurements (bxl9000, 32-core Zen 4, AVX-512 dispatched
136
+ * kernel from `e6d3756`):
137
+ *
138
+ * | Workload | N×dim | Build (Mode 1) | μs/cost-unit |
139
+ * |---|---:|---:|---:|
140
+ * | SIFT1M (dim=128) | 128M | 83 s | 0.65 |
141
+ * | SIFT10M (dim=128) | 1.28B | 20 min | 0.93 |
142
+ * | 1M × 384 | 384M | 17 min | 2.65 |
143
+ * | 1M × 384 (pre-SIMD) | 384M | 47 min | 7.33 |
144
+ *
145
+ * Result files: `docs/verification/result-sift1m-auto.json`,
146
+ * `result-sift10m-in-memory.json`, `result-1m-avx512.json`,
147
+ * `result-1m-auto.json`.
148
+ *
149
+ * Target wall-clock budget: ≤ 24 min (the "acceptable one-time build"
150
+ * boundary). At dim ≤ 256 the per-cost-unit time stays under ~1 μs,
151
+ * so 2 B cost-units ≈ 33 min worst case, safe with super-linear
152
+ * scaling headroom. At dim > 256 the per-cost-unit time climbs into
153
+ * the 2-7 μs range, capping the budget near 400 M cost-units.
154
+ *
155
+ * This is a single internal constant set, not a user-facing knob.
156
+ * Power users who need a specific mode override the entire selector
157
+ * via `DiskAnnConfig.mode = "in-memory" | "hybrid" | "on-disk"`, the
158
+ * existing escape hatch.
159
+ */
160
+ const MODE1_BUILD_COST_CEILING_SMALL_DIM = 2_000_000_000;
161
+ const MODE1_BUILD_COST_CEILING_LARGE_DIM = 400_000_000;
162
+ const MODE1_DIM_BOUNDARY = 256;
163
+ /**
164
+ * Returns the build-cost ceiling above which Mode 1 falls through to
165
+ * Mode 2 at this dim. See {@link MODE1_BUILD_COST_CEILING_SMALL_DIM}.
166
+ *
167
+ * Exposed for tests that need to assert the boundary calibration
168
+ * directly; not exported beyond the module.
169
+ */
170
+ export function mode1BuildCostCeiling(dim) {
171
+ return dim <= MODE1_DIM_BOUNDARY
172
+ ? MODE1_BUILD_COST_CEILING_SMALL_DIM
173
+ : MODE1_BUILD_COST_CEILING_LARGE_DIM;
174
+ }
175
+ /**
176
+ * Pure selector. The wrapper code calls this with concrete inputs;
177
+ * tests construct {@link BrainStats} + {@link SystemMemoryState}
178
+ * directly to assert each branch.
179
+ */
180
+ export function pickMode(stats, sys) {
181
+ const perBrainAvailable = sys.availableBytes / Math.max(1, sys.activeBrains);
182
+ const budget = HEADROOM_FACTOR * perBrainAvailable;
183
+ // Case A: file is already Mode 1 (pqM === 0 in header). Only
184
+ // "in-memory" is algorithmically valid — codes don't exist.
185
+ if (stats.pqM === 0) {
186
+ return {
187
+ mode: 'in-memory',
188
+ reason: 'no-pq-file',
189
+ estimatedBytes: estimateMode1Bytes(stats),
190
+ perBrainAvailable,
191
+ };
192
+ }
193
+ // Case B: BUILD-time decision (pqM undefined). Try Mode 1 first
194
+ // if BOTH the RAM cost fits the budget AND the build cost (N × dim)
195
+ // is below the dim-aware ceiling. The second guard catches the
196
+ // "vectors fit in RAM but the build would take 47 minutes" case —
197
+ // above the ceiling, Mode 2's PQ-coded build is ~5-10× faster at
198
+ // small recall cost, so the selector prefers it even though Mode 1
199
+ // would theoretically work.
200
+ if (stats.pqM === undefined) {
201
+ const mode1Cost = estimateMode1Bytes(stats);
202
+ const buildCost = stats.nodeCount * stats.dim;
203
+ const buildCostCeiling = mode1BuildCostCeiling(stats.dim);
204
+ if (mode1Cost <= budget && buildCost <= buildCostCeiling) {
205
+ return {
206
+ mode: 'in-memory',
207
+ reason: 'fits-in-memory',
208
+ estimatedBytes: mode1Cost,
209
+ perBrainAvailable,
210
+ };
211
+ }
212
+ // Mode 1 was RAM-feasible but build-cost-prohibitive: route to
213
+ // Mode 2 (hybrid) with the distinct `mode1-build-too-large`
214
+ // reason so production logs can tell this fall-through apart
215
+ // from the RAM-driven fall-through (Case C below). Codes are
216
+ // ~m bytes/node, vectors are ~dim×4 bytes/node, so codes are
217
+ // strictly smaller than vectors at any dim ≥ 4 — if mode1Cost
218
+ // fit the budget, codesCost will too. Guaranteed hybrid (no
219
+ // on-disk branch needed here).
220
+ if (mode1Cost <= budget && buildCost > buildCostCeiling) {
221
+ const codesCost = estimateCodesBytes(stats, derivePqM(stats.dim));
222
+ // V8-critical demote applies here too — same defensive rule
223
+ // as Case C below.
224
+ if (sys.v8Pressure === 'critical') {
225
+ return {
226
+ mode: 'on-disk',
227
+ reason: 'v8-pressure-demote',
228
+ estimatedBytes: codesCost,
229
+ perBrainAvailable,
230
+ };
231
+ }
232
+ return {
233
+ mode: 'hybrid',
234
+ reason: 'mode1-build-too-large',
235
+ estimatedBytes: codesCost,
236
+ perBrainAvailable,
237
+ };
238
+ }
239
+ }
240
+ // Case C: PQ-enabled file (pqM > 0) OR build-time too big for
241
+ // Mode 1. Choose between Hybrid (codes pinned) and OnDisk (codes
242
+ // can page) based on whether the codes section fits the budget.
243
+ const m = stats.pqM ?? derivePqM(stats.dim);
244
+ const codesCost = estimateCodesBytes(stats, m);
245
+ let mode;
246
+ let reason;
247
+ if (codesCost <= budget) {
248
+ mode = 'hybrid';
249
+ reason = 'codes-pinned';
250
+ }
251
+ else {
252
+ mode = 'on-disk';
253
+ reason = 'codes-paged';
254
+ }
255
+ // Defensive demote: if V8 heap is critical, don't pile JS-heap
256
+ // pressure on top of OS-RAM pressure. Drop Hybrid → OnDisk so the
257
+ // OS page cache stays in charge of codes residency.
258
+ if (mode === 'hybrid' && sys.v8Pressure === 'critical') {
259
+ mode = 'on-disk';
260
+ reason = 'v8-pressure-demote';
261
+ }
262
+ return {
263
+ mode,
264
+ reason,
265
+ estimatedBytes: codesCost,
266
+ perBrainAvailable,
267
+ };
268
+ }
269
+ /**
270
+ * Mode 1 RAM commitment. The on-disk file's codebook + codes
271
+ * sections are zero bytes in Mode 1; only the graph + vectors
272
+ * sections cost RAM if we pre-page them.
273
+ */
274
+ function estimateMode1Bytes(stats) {
275
+ return stats.nodeCount * (stats.dim + stats.maxDegree) * 4;
276
+ }
277
+ /**
278
+ * Codes-section + codebook RAM commitment for Modes 2/3. Codes
279
+ * dominate at scale (n × m bytes); codebook is constant per
280
+ * dimension (m × ksub × dsub × 4 = ksub × dim × 4 = ~1 MB at the
281
+ * default ksub=256).
282
+ */
283
+ function estimateCodesBytes(stats, m) {
284
+ const codesBytes = stats.nodeCount * m;
285
+ const codebookBytes = DEFAULT_KSUB * stats.dim * 4;
286
+ return codesBytes + codebookBytes;
287
+ }
288
+ /**
289
+ * Convenience wrapper that pulls inputs from a {@link ResourceManager}
290
+ * (default: the singleton) and returns the selector's choice.
291
+ * Production code in `NativeDiskAnnWrapper` calls this at build and
292
+ * open time; tests prefer {@link pickMode} directly so they can pin
293
+ * the system state without touching the singleton.
294
+ */
295
+ export function selectModeFromResourceManager(stats, rm = ResourceManager.getInstance()) {
296
+ const osMemory = rm.getOsMemorySnapshot();
297
+ const profile = rm.getResourceProfile();
298
+ return pickMode(stats, {
299
+ availableBytes: osMemory.availableBytes,
300
+ activeBrains: profile.activeInstances,
301
+ v8Pressure: profile.v8Heap.pressure,
302
+ });
303
+ }
304
+ /**
305
+ * The mode `MappedIndex::open_with_mode(path, Auto)` would resolve to
306
+ * for a given header (Piece H semantics). Mirrored here so the
307
+ * wrapper can skip a reopen when the selector agrees with Auto —
308
+ * avoids opening the file twice on the cold path.
309
+ */
310
+ export function autoModeForHeader(header) {
311
+ return header.pqM === 0 ? 'in-memory' : 'hybrid';
312
+ }
313
+ //# sourceMappingURL=AdaptiveDiskAnnModeSelector.js.map