@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,882 @@
1
+ /**
2
+ * NativeMetadataIndex — TypeScript wrapper around the Rust NativeMetadataIndex.
3
+ *
4
+ * Implements the same public API as the old MetadataIndexManager (3,721 lines),
5
+ * delegating core operations (query, mutation, normalization) to Rust.
6
+ *
7
+ * Architecture:
8
+ * - Rust owns: posting-list algebra, filter evaluation, value normalization,
9
+ * field extraction, text search, entity ID mapping, and the LSM engine
10
+ * (the sole metadata storage engine — SSTables + manifest)
11
+ * - TS owns: async storage I/O, LSM SSTable/manifest persistence, lazy
12
+ * value-enumeration field-index loading, rebuild orchestration, flush,
13
+ * getSortedIdsForFilter (loads sort values from storage)
14
+ *
15
+ * Buffer exchange pattern: TS loads data from storage, passes JSON to Rust.
16
+ * Rust operates in-memory, returns serialized state for TS to persist.
17
+ */
18
+ import type { StorageAdapter } from '@soulcraft/brainy';
19
+ import { NounType, VerbType } from '@soulcraft/brainy/types/graphTypes';
20
+ import { NativeColumnStore } from './NativeColumnStore.js';
21
+ import type { EntityIdMapperLike } from './columnStoreTypes.js';
22
+ import type { MetadataIndexProvider } from '../providerContracts.js';
23
+ import type { MigrationCoordinator, MigrationStatus } from '../migration/MigrationCoordinator.js';
24
+ export interface MetadataIndexEntry {
25
+ field: string;
26
+ value: string | number | boolean;
27
+ ids: Set<string>;
28
+ lastUpdated: number;
29
+ }
30
+ export interface FieldIndexData {
31
+ values: Record<string, number>;
32
+ lastUpdated: number;
33
+ }
34
+ export interface MetadataIndexStats {
35
+ totalEntries: number;
36
+ totalIds: number;
37
+ fieldsIndexed: string[];
38
+ lastRebuild: number;
39
+ indexSize: number;
40
+ }
41
+ export interface MetadataIndexConfig {
42
+ maxIndexSize?: number;
43
+ rebuildThreshold?: number;
44
+ autoOptimize?: boolean;
45
+ indexedFields?: string[];
46
+ excludeFields?: string[];
47
+ }
48
+ interface CardinalityInfo {
49
+ uniqueValues: number;
50
+ totalValues: number;
51
+ distribution: 'uniform' | 'skewed' | 'sparse';
52
+ updateFrequency: number;
53
+ lastAnalyzed: number;
54
+ }
55
+ interface FieldStats {
56
+ cardinality: CardinalityInfo;
57
+ queryCount: number;
58
+ rangeQueryCount: number;
59
+ exactQueryCount: number;
60
+ avgQueryTime: number;
61
+ indexType: 'hash';
62
+ normalizationStrategy?: 'none' | 'precision' | 'bucket';
63
+ }
64
+ /**
65
+ * MetadataIndexManager — native Rust implementation with TS storage bridge.
66
+ *
67
+ * Drop-in replacement for the old pure-TS MetadataIndexManager.
68
+ * All bitmap AND/OR/NOT operations execute in Rust without crossing FFI.
69
+ *
70
+ * Implements {@link MetadataIndexProvider} so a compile error fires the moment
71
+ * this wrapper drops or drifts a member brainy calls via `this.metadataIndex.*`.
72
+ */
73
+ /**
74
+ * **Piece 11 closeout — init-time gate.** Thrown by
75
+ * {@link MetadataIndexManager.init} when the brain carries cortex 2.x legacy
76
+ * JSON metadata state on disk. cor 3.0 uses the LSM engine exclusively (the
77
+ * legacy JSON chunk engine has been removed), so an un-migrated brain must be
78
+ * migrated rather than silently switched. Catchable for graceful fallback
79
+ * paths in callers that prefer a one-shot warning over a hard refusal; the
80
+ * default behaviour (uncaught) is the right one for production deployments —
81
+ * silent degradation to an empty LSM engine would mask the on-disk JSON state
82
+ * and surface as a mysteriously-empty brain at first query.
83
+ */
84
+ export declare class MetadataIndexLegacyJsonStateDetected extends Error {
85
+ readonly name = "MetadataIndexLegacyJsonStateDetected";
86
+ }
87
+ export declare class MetadataIndexManager implements MetadataIndexProvider {
88
+ /**
89
+ * Lazy resolver to the shared {@link MigrationCoordinator} (the plugin creates
90
+ * one per brain and hands the same getter to all three derived-index providers).
91
+ * Undefined for a raw-napi construction (tests) or a tier that never migrates —
92
+ * then {@link isMigrating} is always `false` and the epoch guard's large-brain
93
+ * outcome falls through to Brainy's own path.
94
+ */
95
+ private readonly migrationCoordinatorGetter?;
96
+ private storage;
97
+ private config;
98
+ private native;
99
+ private isRebuilding;
100
+ /**
101
+ * **#18** — set true by {@link guardIndexEpoch} outcome 4 (large stale brain,
102
+ * auto-migrate enabled) and read once at the END of {@link init}, which kicks the
103
+ * coordinated migration only AFTER the index is fully constructed. Flagging here
104
+ * (not kicking inline) avoids racing the migration's destructive rebuild
105
+ * (`reopenLsmEngineFresh` + `columnStore.reset`) against the remainder of the same
106
+ * init() (`columnStore.init` / `warmCache` / `lazyLoadCounts`) on shared state.
107
+ */
108
+ private epochMigrationPending;
109
+ private lastFlushTime;
110
+ private autoFlushThreshold;
111
+ private dirtyFields;
112
+ private loadedFields;
113
+ private knownFields;
114
+ /**
115
+ * **Piece 6 generation counter — reader-side cache.** Records the
116
+ * generation observed at the most recent `invalidateIfStale` call.
117
+ * Mutations on the native side bump `core.generation` via
118
+ * `bump_generation`; a subsequent reader's `invalidateIfStale` call
119
+ * sees a delta and clears `loadedFields`, forcing the next query to
120
+ * re-load from the current state instead of trusting the lazy-load
121
+ * snapshot.
122
+ *
123
+ * Default `-1n` means "never observed"; the first call will see any
124
+ * non-zero current generation and trigger an initial invalidate.
125
+ * (We don't invalidate on the very first call regardless because
126
+ * `loadedFields` is empty anyway.)
127
+ */
128
+ private lastSeenGeneration;
129
+ /**
130
+ * **Piece 15 — per-field generation cache.** Keyed by field name,
131
+ * value is the per-field generation last observed via
132
+ * `native.fieldGeneration(field)`. Updated on every
133
+ * {@link invalidateFieldIfStale} call.
134
+ *
135
+ * Selective invalidation contract: when a probe of field X's
136
+ * counter advances, ONLY X is removed from `loadedFields` — every
137
+ * other loaded field stays valid. This is the load-bearing
138
+ * difference vs the index-wide {@link lastSeenGeneration} +
139
+ * {@link invalidateIfStale} path, which clears the entire
140
+ * `loadedFields` Set on any mutation.
141
+ *
142
+ * Cold-boot: an empty Map. The first probe of any field records
143
+ * its current native counter without invalidating (matches the
144
+ * `lastSeenGeneration: -1n` semantic — never-observed implies
145
+ * loadedFields is empty anyway).
146
+ */
147
+ private lastSeenFieldGenerations;
148
+ private fieldStats;
149
+ private typeFieldAffinity;
150
+ private totalEntitiesByType;
151
+ private entityCountsByTypeFixed;
152
+ private verbCountsByTypeFixed;
153
+ private unifiedCache;
154
+ private fieldTypeInference;
155
+ /**
156
+ * **The native LSM engine (cor 3.0 — the sole metadata engine).** Opened
157
+ * in {@link init} (persistent + mmap-backed on filesystem storage, or a
158
+ * non-persistent in-RAM engine on non-filesystem adapters) and attached to
159
+ * `this.native` via `setLsmEngine` so the metadata-index dispatch routes
160
+ * through the engine's memtable + admission control. `null` only between
161
+ * construction and {@link openLsmEngine} during `init()`.
162
+ */
163
+ private lsmEngine;
164
+ /**
165
+ * **LSM durability (steps 3-4).** In-memory mirror of the on-disk LSM
166
+ * manifest. `null` in JSON mode and on the non-persistent fallback engine
167
+ * (where there is no SSTable surface). Set by {@link openLsmEngine} at cold
168
+ * open (parsed from {@link LSM_MANIFEST_KEY}, or a fresh empty manifest) and
169
+ * grown by {@link flushLsm} as each memtable drain produces an SSTable.
170
+ */
171
+ private lsmManifest;
172
+ /**
173
+ * **LSM durability (steps 3-4).** The resolved on-disk metadata directory
174
+ * (`getBinaryBlobPath('_metadata')`) backing the persistent engine's
175
+ * memtable logs. `null` in JSON mode and on the non-persistent fallback.
176
+ * Its presence is the load-bearing signal that {@link flushLsm} should drain
177
+ * the memtable to a durable SSTable.
178
+ */
179
+ private lsmMetaDir;
180
+ /**
181
+ * **LSM durability (step 5) — bounded-memtable flush trigger.** The memtable
182
+ * resident-byte ceiling at which {@link addToIndex} proactively drains the
183
+ * memtable to a durable SSTable via {@link flushLsm}, so the memtable never
184
+ * climbs to the admission ceiling in steady state (which would wedge every
185
+ * writer — the failure mode the persistent-open switch would otherwise
186
+ * introduce). Computed by {@link computeLsmFlushThreshold} as ~50% of the
187
+ * engine's dynamic `allowedBytes`, clamped to `[32 MiB, 256 MiB]`, and
188
+ * RECOMPUTED inside the ResourceManager subscriber callback
189
+ * ({@link attachLsmBudgetSubscriber}) so it tracks the live per-category
190
+ * budget. Seeded to the floor until the first budget callback fires.
191
+ */
192
+ private lsmFlushThresholdBytes;
193
+ /**
194
+ * Deregistration hook returned by
195
+ * {@link ResourceManager.registerMemoryBudgetSubscriber}. Called from
196
+ * {@link close} so the rebalance loop stops firing the engine's
197
+ * `setAllowedBytes` after this index has shut down. `null` in JSON
198
+ * mode (no subscriber registered).
199
+ */
200
+ private unsubscribeFromBudget;
201
+ /**
202
+ * Unique subscriber name used for both the
203
+ * `ResourceManager.registerMemoryBudgetSubscriber` call and the
204
+ * corresponding `reportNativeMemory` callback that drives the
205
+ * dynamic per-category rebalance. Each MetadataIndexManager instance
206
+ * gets a fresh name from {@link nextLsmSubscriberId} so multiple
207
+ * brains in one process don't collide on the same subscriber slot.
208
+ */
209
+ private lsmSubscriberName;
210
+ /**
211
+ * Native column store — per-field sorted columns backing brainy's unfiltered
212
+ * `find({ orderBy })` path. Brainy calls `metadataIndex.columnStore.sortTopK(...)`
213
+ * directly, so this MUST be present (and is why it is public). Cor's metadata
214
+ * index does its own native filtering; the column store's jobs here are populate
215
+ * (on `addToIndex`), persist (on `flush`), and sort (`sortTopK`).
216
+ */
217
+ readonly columnStore: NativeColumnStore;
218
+ /**
219
+ * Lazily built id-mapper façade over the native UUID ↔ int methods, returned by
220
+ * {@link getIdMapper}. Cached so brainy's orderBy path reuses one object.
221
+ */
222
+ private idMapperFacade;
223
+ /**
224
+ * **#72 Phase C.** The native mmap `BinaryIdMapper` injected into the
225
+ * Rust metadata core as its canonical UUID ↔ entity-int allocator,
226
+ * replacing the resident JSON id-maps (~56 GB @1B + the ~13M-UUID JSON
227
+ * cliff). Built in {@link init} from the brain's `_id_mapper/*` paths and
228
+ * handed to the core via `native.setIdMapper`. Kept here so {@link flush}
229
+ * can msync it and a full {@link rebuild} can truncate + re-inject it.
230
+ * `null` until init wires it (or on non-filesystem storage, where the
231
+ * mapper cannot engage — the metadata index then has no allocator and
232
+ * the brain must run unaccelerated).
233
+ */
234
+ private idMapperNative;
235
+ /**
236
+ * Set by `getIdMapper().rebuild()` so the immediately-following
237
+ * `rebuild()` (brainy `restore()` runs metadataIndex.rebuild() in a
238
+ * `Promise.all` right after the mapper hook) is a no-op: re-clearing the
239
+ * entity-id mapper there would race the concurrent `graphIndex.rebuild()`
240
+ * that resolves verb endpoints through this same mapper. The hook already
241
+ * did the full, identical rebuild, so the second one is skipped and the
242
+ * mapper stays stable for the graph pass. One-shot: consumed by the next
243
+ * `rebuild()`. (#68)
244
+ */
245
+ private skipRebuildOnce;
246
+ constructor(storage: StorageAdapter, config?: MetadataIndexConfig,
247
+ /**
248
+ * Lazy resolver to the shared {@link MigrationCoordinator} (the plugin creates
249
+ * one per brain and hands the same getter to all three derived-index providers).
250
+ * Undefined for a raw-napi construction (tests) or a tier that never migrates —
251
+ * then {@link isMigrating} is always `false` and the epoch guard's large-brain
252
+ * outcome falls through to Brainy's own path.
253
+ */
254
+ migrationCoordinatorGetter?: (() => MigrationCoordinator | undefined) | undefined);
255
+ /**
256
+ * **LSM durability (steps 3-4) — the async open.** Construct the native LSM
257
+ * engine, attach it to the metadata index, register the ResourceManager
258
+ * budget subscriber, and cold-load every persisted SSTable into the
259
+ * engine's live read set. Called once from {@link init} (after
260
+ * {@link wireIdMapper}) — the LSM engine is the sole metadata engine.
261
+ *
262
+ * ## Persistent vs fallback
263
+ *
264
+ * When the storage is filesystem-backed (exposes `getBinaryBlobPath`), the
265
+ * engine is opened via `NativeLsmEngine.openWithDir(metaDir, manifestJson,
266
+ * …)`: its memtable is backed by mmap'd per-shard append logs and its epoch
267
+ * counter is seeded from the persisted manifest so new writes outrank every
268
+ * already-flushed SSTable. Non-filesystem storage falls back to a
269
+ * non-persistent in-RAM engine (`new NativeLsmEngine`) — reads/writes work
270
+ * for the session but there is no SSTable durability (and {@link flushLsm}
271
+ * becomes a no-op).
272
+ *
273
+ * ## Cold-load = no rebuild
274
+ *
275
+ * Cold start reads the manifest, seeds the epoch via `openWithDir`, then
276
+ * calls `registerSstablePath` once per SSTable — the entire restore. The
277
+ * O(N) {@link rebuild} path is NOT taken: the SSTables ARE the durable
278
+ * posting lists, served from mmap.
279
+ *
280
+ * @throws If a persisted SSTable's path cannot be resolved.
281
+ */
282
+ private openLsmEngine;
283
+ /**
284
+ * **LSM durability (steps 3-4) — the sync part.** Register the
285
+ * ResourceManager budget subscriber that keeps the engine's admission
286
+ * ceiling in sync with the dynamic 'metadata' budget. Split out of the
287
+ * async {@link openLsmEngine} so the registration (and its synchronous
288
+ * initial callback) is an isolated, side-effect-free-on-failure step that
289
+ * runs immediately after the engine is attached.
290
+ *
291
+ * Idempotent: a second call (e.g. a re-init) is a no-op while a subscriber
292
+ * is live. The unique subscriber name keeps multiple brains in one process
293
+ * from colliding on the same ResourceManager slot.
294
+ *
295
+ * The callback path is load-bearing for the dynamic rebalance: every
296
+ * `notifySubscribers` cycle (rebalance + pressure tick) hits
297
+ * `this.lsmEngine.setAllowedBytes(BigInt(allowed))`, applied by the native
298
+ * engine on its very next admission check.
299
+ */
300
+ private attachLsmBudgetSubscriber;
301
+ /**
302
+ * **LSM durability (steps 3-4) — cold-load read.** Read and validate the
303
+ * persisted LSM manifest from storage. Returns `null` when no manifest
304
+ * exists yet (a fresh brain). Validates the format version + shape and
305
+ * throws on a mismatch rather than registering a partial SSTable set (which
306
+ * would silently drop rows) — a forward-incompatible or torn manifest must
307
+ * fail loud.
308
+ *
309
+ * @returns The parsed manifest, or `null` if absent.
310
+ * @throws If the manifest is present but malformed or carries an
311
+ * unsupported {@link LsmManifest.version}.
312
+ */
313
+ private loadLsmManifest;
314
+ /**
315
+ * **LSM durability (step 6).** Whether the LSM engine recovered durable
316
+ * on-disk state at cold open — used by {@link init} to demote the
317
+ * field-registry-missing rebuild to disaster-recovery-only. Durable state
318
+ * is either ≥1 SSTable in the cold-loaded manifest OR a non-empty memtable
319
+ * replayed from the per-shard logs (records inserted but not yet flushed
320
+ * before the last shutdown/crash). When present, the posting lists are
321
+ * served from the engine independent of the JSON field registry, so a
322
+ * rebuild would be a redundant O(N) re-scan.
323
+ *
324
+ * @returns `true` when the engine holds durable posting-list state.
325
+ */
326
+ private lsmHasDurableState;
327
+ /**
328
+ * **LSM durability (step 6) — restore/rebuild re-open.** Re-open the LSM
329
+ * engine fresh against the (wiped + recreated) storage tree and re-attach
330
+ * it to the native core. Called from {@link rebuild} in LSM mode.
331
+ *
332
+ * ## Why a full re-open, not just a `mkdir`
333
+ *
334
+ * A rebuild commonly follows brainy's `restore()`, which WIPES and replaces
335
+ * the storage tree under the still-live engine. But `native.clear()` (the
336
+ * in-memory reset rebuild runs) deliberately does NOT touch the attached
337
+ * engine — the engine owns its own memtable + copy-on-write live-SSTable
338
+ * set, neither of which `clear()` can see. So WITHOUT this re-open the
339
+ * engine would keep:
340
+ * - **stale live-SSTable readers** — mmap handles on the pre-restore
341
+ * `.sst` files. On Linux an mmap survives the file's `unlink`, so those
342
+ * readers keep serving PRE-restore rows; a query after rebuild would
343
+ * merge the restored data WITH ghosts of the wiped entities.
344
+ * - **a stale in-memory manifest** ({@link lsmManifest}) still listing the
345
+ * deleted SSTables — the next cold open would try to `registerSstablePath`
346
+ * files that no longer exist and fail loud.
347
+ * - **a stale memtable** holding pre-restore writes not yet flushed.
348
+ *
349
+ * This method makes the engine adopt the new tree: it deletes the stale
350
+ * on-disk SSTables + memtable logs, drops the stale manifest, constructs a
351
+ * FRESH `openWithDir` engine (empty live set, empty memtable, epoch reset
352
+ * from the clean logs), and re-attaches it via `setLsmEngine` (which
353
+ * overwrites the core's engine Arc — verified in the Rust binding). The
354
+ * ResourceManager subscriber callback dereferences `this.lsmEngine`
355
+ * dynamically, so the fresh engine inherits budget tracking on the next
356
+ * tick; meanwhile the re-applied admission ceiling below keeps it bounded,
357
+ * and the step-5 size-flush trigger drains it during the rebuild's
358
+ * re-index walk.
359
+ *
360
+ * A no-op on the non-persistent fallback engine (no `lsmMetaDir`), where
361
+ * there is no SSTable surface to go stale.
362
+ */
363
+ private reopenLsmEngineFresh;
364
+ /**
365
+ * **LSM durability (steps 3-4) — the flush/close drain.** Drain the LSM
366
+ * engine's memtable to a durable SSTable and splice it into the manifest.
367
+ * Called from {@link flush} (and therefore {@link close}, which flushes).
368
+ * No-op when there is no memtable content to flush, and on the
369
+ * non-persistent fallback engine (no manifest / no metaDir).
370
+ *
371
+ * ## Ordering invariant (load-bearing — a reviewer MUST preserve it)
372
+ *
373
+ * The four steps run in this exact order, and `commitFlushThrough` is NEVER
374
+ * called before the `.sst` bytes AND the manifest referencing them are
375
+ * durable:
376
+ *
377
+ * 1. write the `.sst` bytes (`saveBinaryBlob`, atomic tmp+rename)
378
+ * 2. write the manifest that references the new SSTable (`saveMetadata`)
379
+ * 3. register the SSTable into the engine's live read set
380
+ * (`registerSstablePath` — mmap + SHA-256 verify)
381
+ * 4. ONLY NOW retire the memtable log below the flushed epoch
382
+ * (`commitFlushThrough`)
383
+ *
384
+ * A crash between any two steps is safe: the just-flushed records still live
385
+ * in the uncommitted memtable log, so the next `openWithDir` replays them
386
+ * and merge-by-epoch idempotently collapses the transient double presence. A
387
+ * crash before step 2 leaves an orphaned `.sst` no manifest references — its
388
+ * id is harmlessly re-used on the next flush. Calling `commitFlushThrough`
389
+ * early would drop the log copy while the only on-disk copy might not yet be
390
+ * durable — unrecoverable data loss. Hence step 4 is strictly last.
391
+ */
392
+ private flushLsm;
393
+ /**
394
+ * **Piece 11 closeout.** Detect cortex 2.x legacy JSON metadata
395
+ * state on storage. The canonical marker is the
396
+ * `__metadata_field_registry__` key the JSON path writes on
397
+ * every flush. If present, the brain was built before the LSM
398
+ * engine became the default and refuses to silently switch.
399
+ *
400
+ * On detection: throw an init-time error directing the operator to run
401
+ * the migration script (the cor 3.0 LSM engine is the only engine — the
402
+ * legacy JSON chunk path has been removed). The error message is the
403
+ * migration runbook.
404
+ *
405
+ * No legacy state present → the brain is either fresh (no prior
406
+ * cor install) or already on the LSM path → proceed normally
407
+ * with the LSM engine.
408
+ *
409
+ * @throws {@link MetadataIndexLegacyJsonStateDetected} when cortex 2.x
410
+ * JSON metadata state is detected on disk.
411
+ */
412
+ private detectLegacyJsonStateAndGate;
413
+ /**
414
+ * **#18 read-side epoch guard.** Detect 7.x → 8.0 derived-index epoch drift
415
+ * on open and route it to the right outcome. Called from {@link init} AFTER
416
+ * {@link detectLegacyJsonStateAndGate} (the cortex-2.x JSON gate) and
417
+ * {@link openLsmEngine} (the LSM cold-open), and BEFORE the column-store init
418
+ * and any rebuild branch — i.e. while reads are still un-served.
419
+ *
420
+ * The on-disk version-handshake marker (`_system/brain-format.json`) carries a
421
+ * shared, lockstep-bumped `indexEpoch`. cor reads it directly off storage (it
422
+ * cannot reach `brain.formatInfo()`, and Brainy's marker module is not
423
+ * exported) and compares against {@link COR_EXPECTED_INDEX_EPOCH}.
424
+ *
425
+ * Four fully-wired outcomes — no dangling flag:
426
+ *
427
+ * 1. **Epoch CURRENT (the common case)** — a single tiny marker read + an
428
+ * integer compare, then return. NO size probe, NO further work: the
429
+ * warm-restart path stays a cheap no-op.
430
+ * 2. **Stale + SMALL** (`< {@link resolveLargeBrainThreshold}`) — NO-OP. Brainy's
431
+ * `rebuildIndexesIfNeeded` already drives the rebuild from the canonical
432
+ * records for sub-threshold brains (its `AUTO_REBUILD_THRESHOLD` matches
433
+ * ours) and re-stamps the marker after the rebuild verifies. cor must NOT
434
+ * throw here; it simply lets Brainy's path run.
435
+ * 3. **Stale + LARGE + auto-migrate disabled** (`COR_AUTO_MIGRATE=false`) —
436
+ * THROW {@link IndexMigrationRequiredError}, loud and pre-serve. A service
437
+ * that refuses to start is recoverable; serving on indexes that predate
438
+ * this build would surface stale/empty results with no signal.
439
+ * 4. **Stale + LARGE + auto-migrate enabled (default)** — NO-OP / fall through.
440
+ * Brainy's existing large-brain path defers to a lazy first-query rebuild
441
+ * (force=true, BLOCKING inline). That blocking rebuild is correct but not
442
+ * yet "optimized": the non-blocking background build-new → verify → swap
443
+ * that removes the first-query freeze is the **rc.8 increment** (it needs
444
+ * Brainy's deference hook + a cor-invokable stamp method, neither shipped on
445
+ * rc.7). We deliberately do NOT set an `epochMigrationPending` flag here:
446
+ * nothing on rc.7 reads it (the MigrationCoordinator is the rc.8 unit), and
447
+ * an unread flag would be unwired dead state.
448
+ *
449
+ * @throws {@link IndexMigrationRequiredError} on outcome 3 only.
450
+ */
451
+ private guardIndexEpoch;
452
+ /**
453
+ * @description **#18 migration lock — feature-detected by Brainy.** `true` while
454
+ * this brain is undergoing (or has failed) the coordinated 7.x → 8.0 rebuild, so
455
+ * Brainy holds every public read and write behind the lock. Delegates to the
456
+ * shared {@link MigrationCoordinator}; `false` when no coordinator is wired.
457
+ * @returns `true` when the brain must not be read from or written to.
458
+ */
459
+ isMigrating(): boolean;
460
+ /**
461
+ * @description **#18 migration lock — structured progress** Brainy relays into
462
+ * `getIndexStatus().migration`. `null` unless a migration is in flight.
463
+ * @returns The current {@link MigrationStatus}, or `null`.
464
+ */
465
+ migrationStatus(): MigrationStatus | null;
466
+ /**
467
+ * **#18 in-place migration runner (stub).** Reads the un-migrated metadata
468
+ * state from storage and replays it through the LSM-attached mutation path,
469
+ * leaving the brain on the cor 3.0 LSM-native on-disk layout.
470
+ *
471
+ * **NOT YET IMPLEMENTED — GA path is the rc.8 increment.** The read-side
472
+ * epoch guard (#18, {@link guardIndexEpoch}) ships now and makes the SMALL
473
+ * 7.x → 8.0 brain GA on rc.7 (Brainy drives the rebuild from canonical). The
474
+ * LARGE-brain non-blocking background build-new → verify → swap that this
475
+ * runner will drive needs Brainy's deference hook + a cor-invokable stamp
476
+ * method, which land together in the rc.8 increment; until then a large stale
477
+ * brain falls through to Brainy's blocking inline rebuild on first query.
478
+ *
479
+ * The wrapper for this method exists so callers have a stable reference point
480
+ * in the API surface; today it throws with a pointer to the supported
481
+ * back-compat path. Until the in-place runner lands, migrate via the
482
+ * standalone script `scripts/migrate-cortex-2x-to-3x.mjs`, which snapshots the
483
+ * brain via Brainy 7.x, wipes the legacy state, and restores via Brainy 8.0 +
484
+ * cor 3.0 (producing the LSM-native on-disk layout).
485
+ *
486
+ * @throws Always — the in-place runner is the rc.8 increment.
487
+ */
488
+ migrateToLsm(): Promise<void>;
489
+ /**
490
+ * Get the shared entity id-mapper façade. Brainy's unfiltered `orderBy` path
491
+ * calls `metadataIndex.getIdMapper().getUuid(intId)` to turn the column store's
492
+ * sorted int ids back into UUIDs for batchGet. The façade delegates to the
493
+ * native index's O(1) UUID ↔ int methods — the canonical mapping that
494
+ * `addToIndex` assigns and the column store is populated against.
495
+ *
496
+ * @returns An object exposing the brainy `EntityIdMapper` methods callers use.
497
+ */
498
+ getIdMapper(): EntityIdMapperLike;
499
+ init(): Promise<void>;
500
+ /**
501
+ * **#72 Phase C.** Build the shared mmap `BinaryIdMapper` from the
502
+ * brain's `_id_mapper/*` storage paths and inject it into the Rust
503
+ * core as the canonical UUID ↔ entity-int allocator. This replaces the
504
+ * legacy `loadEntityIdMapper`/`saveEntityIdMapper` JSON path, whose two
505
+ * resident HashMaps cost ~56 GB at 1 B entities and whose serialize
506
+ * threw at the V8 ~512 MB / ~13 M-UUID cliff.
507
+ *
508
+ * @throws If storage is not filesystem-backed (no `getBinaryBlobPath`)
509
+ * — cor's metadata index requires local paths; brainy runs
510
+ * unaccelerated on cloud adapters.
511
+ */
512
+ private wireIdMapper;
513
+ private loadFieldRegistry;
514
+ private saveFieldRegistry;
515
+ /**
516
+ * **Piece 6 generation counter — invalidate stale lazy-load state.**
517
+ * Checks the native generation counter against our last-seen value.
518
+ * If a mutation has bumped the counter since we last looked,
519
+ * `loadedFields` is cleared so every subsequent `ensureFieldLoaded`
520
+ * re-pulls the current value-enumeration field index from storage.
521
+ *
522
+ * Called at the start of every read entry point. The check itself
523
+ * is one atomic load + one BigInt compare — sub-microsecond.
524
+ *
525
+ * Without this hook, after a mutation on the native side the
526
+ * cached `loadedFields` set would prevent re-reading the updated
527
+ * value enumeration, surfacing as stale-result bugs of the same shape
528
+ * as R5 (but for the lazy-load path rather than the filter path).
529
+ */
530
+ private invalidateIfStale;
531
+ /**
532
+ * **Piece 15 — per-field selective invalidation.** Reads the
533
+ * field's per-field generation from the native side and compares
534
+ * against {@link lastSeenFieldGenerations}. If the field's counter
535
+ * advanced since we last observed it, removes ONLY that field
536
+ * from {@link loadedFields} — every other loaded field stays
537
+ * valid. Then records the current value so the next call sees a
538
+ * no-op until another mutation touches this field.
539
+ *
540
+ * Called at the start of every read entry point that knows which
541
+ * fields it needs. The check is one FFI hop (native atomic load)
542
+ * + one Map get/set — sub-microsecond per field. For multi-field
543
+ * filters (`ensureFieldsLoaded`) the wrapper iterates this method
544
+ * once per field instead of doing the old index-wide check.
545
+ *
546
+ * Cold-boot semantic: first probe records the current counter
547
+ * without invalidating (matches the old `lastSeenGeneration ===
548
+ * -1n` initial-call behavior). A counter advance from 0 → 1 on
549
+ * the FIRST observation is treated as "first sighting", NOT
550
+ * "stale" — `loadedFields` doesn't have this field yet anyway, so
551
+ * there's nothing to invalidate.
552
+ */
553
+ private invalidateFieldIfStale;
554
+ /**
555
+ * Ensure a field's persisted value-enumeration index is loaded into the
556
+ * native core (the LSM read paths enumerate candidate values from it).
557
+ * Lazy + idempotent — guarded by {@link loadedFields}.
558
+ */
559
+ private ensureFieldLoaded;
560
+ /**
561
+ * **LSM durability (steps 3-4) — cold-load value enumeration.** Restore a
562
+ * field's persisted value-enumeration index
563
+ * (`__metadata_field_index__field_<field>`) into the native core. In LSM
564
+ * mode the read paths for field-wide `exists` / `missing` / `ne` and range
565
+ * (`between` / `gt` / `lt`) queries enumerate a field's distinct values from
566
+ * `core.field_indexes`. That map is built incrementally by `addToIndex`
567
+ * within a session but is EMPTY after a cold reopen — the registered
568
+ * SSTables hold the posting lists, but without the value enumeration the
569
+ * read paths have no value keys to look those lists up by. Loading this JSON
570
+ * (persisted by `persistMutationResult` / `flush`) closes that gap so the
571
+ * cold-loaded SSTables serve every filter shape with no rebuild.
572
+ *
573
+ * No-op for a field with no persisted index (a brand-new field this
574
+ * session) — `addToIndex` then populates `core.field_indexes` incrementally.
575
+ * Only the first access loads it (the caller guards via `loadedFields`), so
576
+ * an in-session field index built by `addToIndex` is never overwritten.
577
+ *
578
+ * @param field - The field whose value-enumeration index to restore.
579
+ */
580
+ private loadPersistedFieldIndex;
581
+ /**
582
+ * Ensure multiple fields are loaded (parallel).
583
+ *
584
+ * **Piece 15.** Per-field stale checks instead of one
585
+ * index-wide check. The wrapper now iterates {@link invalidateFieldIfStale}
586
+ * once per requested field — selectively dropping ONLY the
587
+ * fields whose counters advanced since we last looked. Cross-
588
+ * field churn (a mutation on `noun` while we're reading `status`)
589
+ * no longer invalidates `status`'s lazy-load. Cost: N FFI hops
590
+ * (each a single atomic Acquire load) instead of 1 — typically
591
+ * 3-5 hops on a brainy filter query, negligible vs the storage
592
+ * I/O the wrapper does next.
593
+ */
594
+ private ensureFieldsLoaded;
595
+ /**
596
+ * Internal variant of `ensureFieldLoaded` that skips the generation
597
+ * check (used by `ensureFieldsLoaded` which already did one batched
598
+ * check at the top).
599
+ */
600
+ private ensureFieldLoadedNoCheck;
601
+ /**
602
+ * Extract filter field names for lazy loading.
603
+ */
604
+ private extractFilterFields;
605
+ /**
606
+ * Persist the dirty value-enumeration field indexes + field registry from a
607
+ * MutationResult. Posting-list durability is owned by the LSM engine
608
+ * ({@link flushLsm}), so this only writes the JSON value-enumeration surface
609
+ * the LSM read paths cold-load.
610
+ */
611
+ private persistMutationResult;
612
+ private warmCache;
613
+ private warmCacheForTopTypes;
614
+ private lazyLoadCounts;
615
+ private syncTypeCountsToFixed;
616
+ private syncTypeCountsFromFixed;
617
+ /**
618
+ * Serialize a query value to JSON for the native String param. `JSON.stringify`
619
+ * returns the JS value `undefined` (NOT a string) for `undefined`/functions —
620
+ * which the napi String param rejects ("Failed to convert Null/Undefined into
621
+ * String"). Coalesce to the JSON literal `'null'` so the Rust side parses it to
622
+ * `serde_json::Value::Null`, normalizes, and returns the no-match result (0/[]) —
623
+ * bug-for-bug compatible with brainy's "no match → empty". (#79 cluster B.)
624
+ */
625
+ private serializeValue;
626
+ getIds(field: string, value: any): Promise<string[]>;
627
+ /**
628
+ * Resolve a `where` filter to its matching entity ids.
629
+ *
630
+ * @param filter - Brainy field-operator filter (eq / allOf / anyOf / ne /
631
+ * range / exists / …).
632
+ * @param opts - OPTIONAL page bound (#75). When `{ limit }` is supplied the
633
+ * native producer early-stops and returns only the `[offset, offset+limit)`
634
+ * PREFIX of the sorted match set — it short-circuits the posting-list →
635
+ * UUID conversion at `offset+limit` instead of materializing the full list
636
+ * and slicing, which removes the O(N) FFI marshal for a broad-filter
637
+ * single-page `find()` at billion scale. Brainy calls this with
638
+ * `{ limit: pageEnd, offset: 0 }` and re-windows the prefix itself
639
+ * (visibility filter + slice), so the producer honors the literal `offset`
640
+ * (brainy's `0` ⇒ the `[0, limit)` prefix). Omitting `opts` returns the
641
+ * FULL match set (back-compat).
642
+ */
643
+ getIdsForFilter(filter: any, opts?: {
644
+ limit?: number;
645
+ offset?: number;
646
+ }): Promise<string[]>;
647
+ getIdsForMultipleFields(fieldValuePairs: Array<{
648
+ field: string;
649
+ value: any;
650
+ }>): Promise<string[]>;
651
+ getIdsForTextQuery(query: string): Promise<Array<{
652
+ id: string;
653
+ matchCount: number;
654
+ }>>;
655
+ getSortedIdsForFilter(filter: any, orderBy: string, order?: 'asc' | 'desc', topK?: number): Promise<string[]>;
656
+ /**
657
+ * Get field value for a specific entity (helper for sorted queries).
658
+ *
659
+ * Three-path lookup:
660
+ *
661
+ * 1. **Bucketed fields** (timestamps) — the native index stores values
662
+ * rounded to coarser buckets to keep the index compact for range
663
+ * queries. That bucketing loses precision, so sorting must read the
664
+ * actual value directly from entity storage via resolveEntityField.
665
+ *
666
+ * 2. **Custom fields with no indexed values** — VFS fields like
667
+ * `modified`/`accessed`, plus any user custom field not currently
668
+ * present in the native index. Resolved from entity storage via
669
+ * resolveEntityField, which knows the top-level-vs-metadata
670
+ * shape contract.
671
+ *
672
+ * 3. **Indexed fields** — strings, enums, and low-cardinality ints that
673
+ * the native index knows about. O(values) lookup.
674
+ *
675
+ * Mirrors getFieldValueForEntity in brainy's src/utils/metadataIndex.ts —
676
+ * keep in lockstep when the entity shape contract evolves.
677
+ */
678
+ getFieldValueForEntity(entityId: string, field: string): Promise<any>;
679
+ private denormalizeValue;
680
+ getFilterValues(field: string): Promise<string[]>;
681
+ getFilterFields(): Promise<string[]>;
682
+ addToIndex(id: string, entityOrMetadata: any, skipFlush?: boolean, deferWrites?: boolean): Promise<void>;
683
+ removeFromIndex(id: string, metadata?: any): Promise<void>;
684
+ /**
685
+ * Report the LSM engine's current native memory footprint
686
+ * (memtable + reader cache) to the ResourceManager so the dynamic
687
+ * per-category rebalance can detect pressure on the 'metadata'
688
+ * category and shift capacity from underutilized neighbors.
689
+ *
690
+ * No-op in JSON mode (no engine attached) and after `close()` has
691
+ * unregistered the subscriber. Called from `addToIndex` /
692
+ * `removeFromIndex` on the production hot path — the underlying
693
+ * `nativeMemoryBytes()` is a single atomic load + sum, sub-microsecond
694
+ * cost.
695
+ *
696
+ * The reportNativeMemory call short-circuits inside ResourceManager
697
+ * if the subscriber name was already cleared (deregistered between
698
+ * the mutation and this call), so the unregister race is benign.
699
+ */
700
+ private reportLsmMemoryToResourceManager;
701
+ private updateTypeTracking;
702
+ tokenize(text: string): string[];
703
+ hashWord(word: string): number;
704
+ extractTextContent(data: any): string;
705
+ /**
706
+ * Whether a field should be indexed, honoring this index's configured
707
+ * `excludeFields` / `indexedFields`. Mirrors the Rust `should_index_field`
708
+ * (and brainy's TS `shouldIndexField`) so the column store indexes exactly the
709
+ * fields the metadata index does.
710
+ *
711
+ * @param field - Fully-qualified field name (dotted for nested).
712
+ * @returns True if the field is indexable under the current config.
713
+ */
714
+ private shouldIndexField;
715
+ /**
716
+ * Build the indexable field→value map fed to the column store, identical to
717
+ * brainy's `MetadataIndexManager.extractIndexableFields()` → `fieldsMap`:
718
+ *
719
+ * - Recurses objects; flattens a top-level `metadata` object to the root.
720
+ * - Maps a top-level `type` field to `noun` (backward-compat with indexes).
721
+ * - Skips the never-index set (vector/embedding/connections/level/id), purely
722
+ * numeric keys (array indices), config-excluded fields, and arrays longer
723
+ * than 10 elements (likely vectors/bulk data).
724
+ * - Small arrays (≤10) become multi-value fields: one array entry per element.
725
+ * - Adds `__words__` as a multi-value array of FNV-1a int32 word hashes from
726
+ * the entity's text content (capped at 5000 words), matching brainy.
727
+ *
728
+ * Replicated in TS rather than reusing the Rust extractor because the native
729
+ * `addToIndex` does not return the extracted pairs and the engine is frozen.
730
+ *
731
+ * @param data - The entity or plain-metadata object being indexed.
732
+ * @returns Field → value (or array of values) map for `columnStore.addEntity`.
733
+ */
734
+ private extractColumnFields;
735
+ getEntityCountByType(type: string): number;
736
+ getTotalEntityCount(): number;
737
+ getAllEntityCounts(): Map<string, number>;
738
+ getEntityCountByTypeEnum(type: NounType): number;
739
+ getVerbCountByTypeEnum(type: VerbType): number;
740
+ getTopNounTypes(n: number): NounType[];
741
+ getTopVerbTypes(n: number): VerbType[];
742
+ getAllNounTypeCounts(): Map<NounType, number>;
743
+ getAllVerbTypeCounts(): Map<VerbType, number>;
744
+ getVFSEntityCountByType(type: string): Promise<number>;
745
+ getAllVFSEntityCounts(): Promise<Map<string, number>>;
746
+ getTotalVFSEntityCount(): Promise<number>;
747
+ getCountForCriteria(field: string, value: any): Promise<number>;
748
+ flush(): Promise<void>;
749
+ /**
750
+ * Persist all state and release the column store's resources. Brainy's
751
+ * shutdown path calls `metadataIndex.close()` when present; flushing here
752
+ * guarantees the column store's tail buffers reach disk before the process
753
+ * exits, so a reopened index can serve `orderBy` from persisted segments.
754
+ */
755
+ close(): Promise<void>;
756
+ getStats(): Promise<MetadataIndexStats>;
757
+ /**
758
+ * @description Report which index path a `find({ where: { [field]: ... } })`
759
+ * clause will hit. Brainy's `brain.explain()` calls this per `where` field so an
760
+ * operator can see *before* running a query whether the field has any index
761
+ * entries at all — a `where` on an unindexed field returns `[]` silently, and
762
+ * `path: 'none'` gives that empty result an explanation.
763
+ *
764
+ * The native engine serves all `where` lookups from the column store (the
765
+ * single source of truth — there is no separate legacy sparse-index path at
766
+ * query time, unlike brainy's TypeScript baseline). So a field either has
767
+ * column-store entries (`'column-store'`) or none (`'none'`). The field's
768
+ * segments are loaded first so `hasField` reflects on-disk state, not just
769
+ * what has been touched this session.
770
+ *
771
+ * @param field - The `where`-clause field name to explain.
772
+ * @returns The serving path and an operator-facing note.
773
+ * @example
774
+ * const { path, notes } = await metadataIndex.explainField('status')
775
+ * if (path === 'none') console.warn(notes)
776
+ */
777
+ explainField(field: string): Promise<{
778
+ path: 'column-store' | 'sparse-chunked' | 'none';
779
+ notes?: string;
780
+ }>;
781
+ validateConsistency(): Promise<{
782
+ healthy: boolean;
783
+ avgEntriesPerEntity: number;
784
+ entityCount: number;
785
+ indexEntryCount: number;
786
+ recommendation: string | null;
787
+ }>;
788
+ /**
789
+ * O(1)-ish cross-bucket consistency probe (brainy rc.6 matrix 7.7).
790
+ *
791
+ * Brainy calls this ONCE on the first read; on `false` it runs
792
+ * {@link detectAndRepairCorruption}. The check is fully internal — it never
793
+ * re-reads entity data. It loads a bounded set of value-enumeration field
794
+ * indexes (so the native sampler can enumerate each field's value buckets),
795
+ * biased toward `noun` (the canonical single-valued type field where the
796
+ * staff→timeslot class of phantom lands), then defers to the native
797
+ * {@link NativeMetadataIndexInstance.probeConsistency} bounded sampler.
798
+ *
799
+ * Best-effort: any error (a sampling glitch, a storage hiccup loading a field
800
+ * index) degrades to `true` so the probe NEVER breaks brainy's read path.
801
+ *
802
+ * @returns `true` if the index looks consistent (or the check is
803
+ * indeterminate), `false` if a cross-bucket phantom was sampled.
804
+ */
805
+ probeConsistency(): Promise<boolean>;
806
+ detectAndRepairCorruption(): Promise<void>;
807
+ /**
808
+ * State-level cross-bucket phantom purge (brainy matrix 7.8b).
809
+ *
810
+ * Scans the native index for entities whose int sits in more than one value
811
+ * bucket of a field, then tombstones the stale buckets against each entity's
812
+ * CURRENT stored value(s) — read straight from storage (brainy's source of
813
+ * truth), NOT from the index being repaired. A multi-valued field keeps all
814
+ * its stored values (a no-op); a single-valued phantom is purged down to the
815
+ * one current value; an orphaned int (entity gone) is evicted entirely.
816
+ *
817
+ * Only reached after {@link probeConsistency} flagged the index, so the
818
+ * full field-index load + scan is acceptable here (unlike the O(1) probe).
819
+ */
820
+ private purgeCrossBucketPhantoms;
821
+ rebuild(): Promise<void>;
822
+ /**
823
+ * Cursor-paginate every entity of a kind ('noun'|'verb') through the metadata
824
+ * index during rebuild. ONE streaming path for ALL storage types — replaces
825
+ * the old local-vs-cloud fork that OOM'd / silently TRUNCATED at scale: the
826
+ * local branch loaded the whole corpus in a single `getNouns/getVerbs({limit:
827
+ * 1e6})` call (cap 1M), and the cloud branch capped at MAX_ITERATIONS(10000)×
828
+ * batch(25) ≈ 250K. Cursor pagination is O(page) RAM and complete at any
829
+ * scale; it terminates on `hasMore === false` (8.0's cursor is no-rescan), with
830
+ * an empty-page backstop. (#74 — perf-at-all-scales mandate.)
831
+ * @returns the number of entities indexed (those carrying metadata).
832
+ */
833
+ private rebuildEntityMetadata;
834
+ /**
835
+ * Batch-load metadata for a page of noun/verb ids — prefers the storage
836
+ * adapter's batch API, falls back to per-id reads.
837
+ */
838
+ private loadRebuildMetadataBatch;
839
+ /**
840
+ * During rebuild, msync the durable state accumulated so far.
841
+ *
842
+ * Posting lists land in the LSM engine's memtable (drained to durable
843
+ * SSTables by the step-5 size-flush trigger as the re-index walk proceeds)
844
+ * and the entity ints land in the mmap id mapper — so the only durable
845
+ * write to force here is the mapper msync. The value-enumeration field
846
+ * indexes + the LSM memtable tail are persisted by the final {@link flush}
847
+ * at the end of the rebuild.
848
+ */
849
+ private flushRebuildDirty;
850
+ clearAllIndexData(): Promise<void>;
851
+ private getPersistedFieldList;
852
+ /**
853
+ * Delete a field's persisted value-enumeration index
854
+ * (`__metadata_field_index__field_<field>`) from storage. Called by
855
+ * {@link rebuild} and {@link clearAllIndexData} to wipe the JSON
856
+ * value-enumeration surface before re-indexing; the posting lists
857
+ * themselves are wiped from the LSM engine separately
858
+ * ({@link reopenLsmEngineFresh}).
859
+ */
860
+ private deletePersistedFieldIndex;
861
+ getFieldStatistics(): Promise<Map<string, FieldStats>>;
862
+ getFieldCardinality(field: string): Promise<CardinalityInfo | null>;
863
+ getFieldsWithCardinality(): Promise<Array<{
864
+ field: string;
865
+ cardinality: number;
866
+ distribution: string;
867
+ }>>;
868
+ getOptimalQueryPlan(filters: Record<string, any>): Promise<{
869
+ strategy: 'exact' | 'range' | 'hybrid';
870
+ fieldOrder: string[];
871
+ estimatedCost: number;
872
+ }>;
873
+ getFieldsForType(nounType: NounType): Promise<Array<{
874
+ field: string;
875
+ affinity: number;
876
+ occurrences: number;
877
+ totalEntities: number;
878
+ }>>;
879
+ private yieldToEventLoop;
880
+ }
881
+ export {};
882
+ //# sourceMappingURL=NativeMetadataIndex.d.ts.map