@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
|
@@ -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
|