2025-10-13 16:39:06 -07:00
/ * *
2026-05-28 09:45:22 -07:00
* EntityIdMapper - Bidirectional mapping between UUID strings and integer IDs .
2025-10-13 16:39:06 -07:00
*
2026-05-28 09:45:22 -07:00
* Roaring bitmaps require 32 - bit unsigned integers , but Brainy uses UUID strings
* as canonical entity IDs . This class provides efficient O ( 1 ) bidirectional
* mapping with persistence — and , importantly , a stability guarantee that any
* persisted int - keyed data can rely on .
*
* * * Stability guarantee ( the foundation 2.4 . 0 vector - mmap , graph - link - compression ,
* and column - store interchange all key off ) : * *
*
* - ` getOrAssign(uuid) ` is * * append - only * * : once a UUID is assigned an int , the
* mapping never changes . Subsequent ` getOrAssign ` calls for the same UUID
* return the same int .
* - ` nextId ` is * * monotonically increasing * * . New UUIDs always get an int greater
* than any previously assigned , so a removed - then - re - added UUID is treated as
* a fresh entity ( and gets a fresh int — there is no automatic "revive" ) .
* - ` remove(uuid) ` removes the mapping but does * * not * * decrement ` nextId ` or
* recycle the int . The removed int becomes a permanent hole in ` intToUuid ` —
* downstream consumers seeing ` getUuid(int) === undefined ` know the entity
* was deleted .
* - A metadata - index ` rebuild() ` does * * not * * clear the mapper ( the rebuild path
* re - iterates entities via ` getOrAssign ` , which returns existing ints unchanged ) .
* Only the explicit ` clear() ` method renumbers — used by ` clearAllIndexData() `
* as the nuclear recovery path with a documented warning .
2025-10-13 16:39:06 -07:00
*
* Features :
2026-05-28 09:45:22 -07:00
* - O ( 1 ) lookup in both directions .
* - Persistent storage via storage adapter .
* - Atomic , monotonic , append - only int counter .
* - Serialization / deserialization support .
2025-10-13 16:39:06 -07:00
*
* @module utils / entityIdMapper
* /
2025-10-13 16:42:45 -07:00
import type { StorageAdapter } from '../coreTypes.js'
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
import type { EntityIdMapperProvider } from '../plugin.js'
2025-10-13 16:39:06 -07:00
feat(8.0): EntityIdMapper U32 ceiling + EntityIdSpaceExceeded error
The JS fallback EntityIdMapper caps at u32::MAX to match the metadata
index's Roaring32 bitmap width. Before this guard, a brain past 4.29 B
entities would silently widen `nextId` into JS's safe-integer range,
truncating ints inside the bitmaps and zeroing query results — the
same silent under-count cortex 3.0's Piece 10 just closed on the
native path. Brainy 8.0 surfaces the overflow loudly instead.
When `nextId` would exceed `U32_ENTITY_ID_MAX`, `getOrAssign` throws
`EntityIdSpaceExceeded` with a message pointing at the cortex 3.0
binary mapper's `idSpace: 'u64'` mode as the migration path (mmap-
backed extendible-hash KV, persists the full u64 range losslessly).
The existing-UUID lookup path bypasses the guard — only fresh
allocations can overflow.
Surfaces via `@soulcraft/brainy/internals`:
- `EntityIdMapper` (already exported, behavior gated)
- `EntityIdSpaceExceeded` (new)
- `U32_ENTITY_ID_MAX` (new constant, = 0xFFFF_FFFF)
This is the lockstep half of cortex 3.0 / Piece 10 / Step 15. Cortex's
`NativeBinaryEntityIdMapperWrapper` ships the U64 binary mapper that
takes over above this ceiling; brainy ships the bright-line failure
that points consumers at it.
New test: tests/unit/utils/entity-id-mapper-u32-ceiling.test.ts (6
assertions covering the constant, the error shape, normal
allocations, the boundary case at exactly u32::MAX, the overflow
throw, and the existing-uuid lookup bypass).
2026-06-02 10:20:01 -07:00
/ * *
* The largest entity int the JS fallback ` EntityIdMapper ` will allocate .
* The metadata index ' s roaring bitmaps are 32 - bit - keyed ( ` Roaring32 ` ) , so
* allowing the JS counter past this point would silently corrupt every
2026-07-02 15:11:41 -07:00
* downstream bitmap . The cor 3.0 binary mapper supports a U64 IdSpace
feat(8.0): EntityIdMapper U32 ceiling + EntityIdSpaceExceeded error
The JS fallback EntityIdMapper caps at u32::MAX to match the metadata
index's Roaring32 bitmap width. Before this guard, a brain past 4.29 B
entities would silently widen `nextId` into JS's safe-integer range,
truncating ints inside the bitmaps and zeroing query results — the
same silent under-count cortex 3.0's Piece 10 just closed on the
native path. Brainy 8.0 surfaces the overflow loudly instead.
When `nextId` would exceed `U32_ENTITY_ID_MAX`, `getOrAssign` throws
`EntityIdSpaceExceeded` with a message pointing at the cortex 3.0
binary mapper's `idSpace: 'u64'` mode as the migration path (mmap-
backed extendible-hash KV, persists the full u64 range losslessly).
The existing-UUID lookup path bypasses the guard — only fresh
allocations can overflow.
Surfaces via `@soulcraft/brainy/internals`:
- `EntityIdMapper` (already exported, behavior gated)
- `EntityIdSpaceExceeded` (new)
- `U32_ENTITY_ID_MAX` (new constant, = 0xFFFF_FFFF)
This is the lockstep half of cortex 3.0 / Piece 10 / Step 15. Cortex's
`NativeBinaryEntityIdMapperWrapper` ships the U64 binary mapper that
takes over above this ceiling; brainy ships the bright-line failure
that points consumers at it.
New test: tests/unit/utils/entity-id-mapper-u32-ceiling.test.ts (6
assertions covering the constant, the error shape, normal
allocations, the boundary case at exactly u32::MAX, the overflow
throw, and the existing-uuid lookup bypass).
2026-06-02 10:20:01 -07:00
* for brains above this ceiling — see the
* [ ` EntityIdSpaceExceeded ` ] ( # EntityIdSpaceExceeded ) message for the
* migration pointer .
* /
export const U32_ENTITY_ID_MAX = 0xffff _ffff
/ * *
* Thrown by the JS fallback ` EntityIdMapper ` when ` nextId ` would exceed
* [ ` U32_ENTITY_ID_MAX ` ] ( # U32_ENTITY_ID_MAX ) . The JS path is the
2026-07-02 15:11:41 -07:00
* cor - free fallback ; once a brain has more than ~ 4.29 B entities ,
* callers MUST install the cor 3.0 ` NativeBinaryEntityIdMapper ` with
feat(8.0): EntityIdMapper U32 ceiling + EntityIdSpaceExceeded error
The JS fallback EntityIdMapper caps at u32::MAX to match the metadata
index's Roaring32 bitmap width. Before this guard, a brain past 4.29 B
entities would silently widen `nextId` into JS's safe-integer range,
truncating ints inside the bitmaps and zeroing query results — the
same silent under-count cortex 3.0's Piece 10 just closed on the
native path. Brainy 8.0 surfaces the overflow loudly instead.
When `nextId` would exceed `U32_ENTITY_ID_MAX`, `getOrAssign` throws
`EntityIdSpaceExceeded` with a message pointing at the cortex 3.0
binary mapper's `idSpace: 'u64'` mode as the migration path (mmap-
backed extendible-hash KV, persists the full u64 range losslessly).
The existing-UUID lookup path bypasses the guard — only fresh
allocations can overflow.
Surfaces via `@soulcraft/brainy/internals`:
- `EntityIdMapper` (already exported, behavior gated)
- `EntityIdSpaceExceeded` (new)
- `U32_ENTITY_ID_MAX` (new constant, = 0xFFFF_FFFF)
This is the lockstep half of cortex 3.0 / Piece 10 / Step 15. Cortex's
`NativeBinaryEntityIdMapperWrapper` ships the U64 binary mapper that
takes over above this ceiling; brainy ships the bright-line failure
that points consumers at it.
New test: tests/unit/utils/entity-id-mapper-u32-ceiling.test.ts (6
assertions covering the constant, the error shape, normal
allocations, the boundary case at exactly u32::MAX, the overflow
throw, and the existing-uuid lookup bypass).
2026-06-02 10:20:01 -07:00
* ` idSpace: 'u64' ` ( mmap - backed extendible - hash KV ; persists the full
* u64 range losslessly ) .
*
* Brainy 8.0 surfaces this loudly rather than silently widening past
* u32 : : MAX , because the metadata index ' s roaring bitmaps would silently
* truncate entity ids and zero - out query results .
* /
export class EntityIdSpaceExceeded extends Error {
/** The u32 entity-id ceiling. */
readonly ceiling : number = U32_ENTITY_ID_MAX
/ * *
* The would - be ` nextId ` value the mapper was about to assign — always
* ` U32_ENTITY_ID_MAX + 1 ` .
* /
readonly attempted : number
constructor ( attempted : number ) {
super (
` EntityIdMapper: nextId ${ attempted } would exceed u32::MAX ` +
` ( ${ U32_ENTITY_ID_MAX } ). The JS fallback mapper caps at u32 to ` +
` match the metadata index's Roaring32 bitmap width. For >4.29 B ` +
feat(8.0): id-normalization (#18) + aggregation min/max delete-safety + RC-safe release
- id-normalization (#18): `brain.newId()` (UUID v7) + v7 default ids; non-UUID string ids are
transparently normalized to a stable UUID v5 on creation AND every lookup — get/update/remove,
relate(from,to), related, find({connected}), getMany, removeMany, the transact op-handler, and
db.with() overlays — with the caller's original key preserved under `_originalId`. The engine only
ever sees a UUID; real UUIDs pass through untouched. universal/uuid.ts gains v5/v7/isUUID +
BRAINY_ID_NAMESPACE; new utils/idNormalization.ts (coerceNewEntityId / resolveEntityId).
- aggregation min/max delete-safety: `queryAggregate` no longer leaves a stale min/max after
deleting the current extreme — min/max now track a value multiset and recompute in-memory (no
entity scan; that scan was the prior delete-then-hang a consumer reported). Other metrics were
already exact across deletes. Regression test proves resolve-after-delete + correct new min/max.
- release.sh RC-safe: explicit version arg, prerelease detection (npm `--tag rc` + GitHub
`--prerelease`), full `test:ci` gate (unit + integration), current-branch push, npm-access
verification after publish.
- native-engine references point at `@soulcraft/cor` (8.0's partner) in user-facing strings/docs.
Unit 1461/0 · integration 599/0 · tsc clean.
2026-06-20 14:40:57 -07:00
` entities, install @soulcraft/cor and configure the binary ` +
feat(8.0): EntityIdMapper U32 ceiling + EntityIdSpaceExceeded error
The JS fallback EntityIdMapper caps at u32::MAX to match the metadata
index's Roaring32 bitmap width. Before this guard, a brain past 4.29 B
entities would silently widen `nextId` into JS's safe-integer range,
truncating ints inside the bitmaps and zeroing query results — the
same silent under-count cortex 3.0's Piece 10 just closed on the
native path. Brainy 8.0 surfaces the overflow loudly instead.
When `nextId` would exceed `U32_ENTITY_ID_MAX`, `getOrAssign` throws
`EntityIdSpaceExceeded` with a message pointing at the cortex 3.0
binary mapper's `idSpace: 'u64'` mode as the migration path (mmap-
backed extendible-hash KV, persists the full u64 range losslessly).
The existing-UUID lookup path bypasses the guard — only fresh
allocations can overflow.
Surfaces via `@soulcraft/brainy/internals`:
- `EntityIdMapper` (already exported, behavior gated)
- `EntityIdSpaceExceeded` (new)
- `U32_ENTITY_ID_MAX` (new constant, = 0xFFFF_FFFF)
This is the lockstep half of cortex 3.0 / Piece 10 / Step 15. Cortex's
`NativeBinaryEntityIdMapperWrapper` ships the U64 binary mapper that
takes over above this ceiling; brainy ships the bright-line failure
that points consumers at it.
New test: tests/unit/utils/entity-id-mapper-u32-ceiling.test.ts (6
assertions covering the constant, the error shape, normal
allocations, the boundary case at exactly u32::MAX, the overflow
throw, and the existing-uuid lookup bypass).
2026-06-02 10:20:01 -07:00
` mapper with idSpace: 'u64' (mmap-backed extendible-hash KV). ` ,
)
this . name = 'EntityIdSpaceExceeded'
this . attempted = attempted
}
}
2025-10-13 16:39:06 -07:00
export interface EntityIdMapperOptions {
2025-10-13 16:42:45 -07:00
storage : StorageAdapter
2025-10-13 16:39:06 -07:00
storageKey? : string
}
export interface EntityIdMapperData {
nextId : number
uuidToInt : Record < string , number >
intToUuid : Record < number , string >
}
/ * *
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
* Maps entity UUIDs to integer IDs for use with Roaring Bitmaps .
*
* Implements { @link EntityIdMapperProvider } : the surface a registered
2026-07-02 15:11:41 -07:00
* ` 'entityIdMapper' ` provider ( e . g . Cor ' s native mapper ) must also satisfy .
2025-10-13 16:39:06 -07:00
* /
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
export class EntityIdMapper implements EntityIdMapperProvider {
2025-10-13 16:42:45 -07:00
private storage : StorageAdapter
2025-10-13 16:39:06 -07:00
private storageKey : string
// Bidirectional maps
private uuidToInt = new Map < string , number > ( )
private intToUuid = new Map < number , string > ( )
// Atomic counter for next ID
private nextId = 1
// Dirty flag for persistence
private dirty = false
constructor ( options : EntityIdMapperOptions ) {
this . storage = options . storage
this . storageKey = options . storageKey || 'brainy:entityIdMapper'
}
/ * *
* Initialize the mapper by loading from storage
* /
async init ( ) : Promise < void > {
try {
2025-10-17 12:29:27 -07:00
const metadata = await this . storage . getMetadata ( this . storageKey )
2026-01-27 15:38:21 -08:00
// metadata IS the data (no nested 'data' property)
2026-06-11 14:51:00 -07:00
if ( metadata && metadata . nextId !== undefined ) {
// Typed boundary: mapper state round-trips through the storage
// metadata channel as plain JSON; the `nextId` probe above identifies
// the persisted EntityIdMapperData shape.
const data = metadata as unknown as EntityIdMapperData
2025-10-13 16:39:06 -07:00
this . nextId = data . nextId
// Rebuild maps from serialized data
this . uuidToInt = new Map ( Object . entries ( data . uuidToInt ) . map ( ( [ k , v ] ) = > [ k , Number ( v ) ] ) )
this . intToUuid = new Map ( Object . entries ( data . intToUuid ) . map ( ( [ k , v ] ) = > [ Number ( k ) , v ] ) )
2026-03-24 12:57:11 -07:00
} else {
// Guard: mapper file missing but entities may exist on disk.
// If we start from nextId=1 with existing entities, roaring bitmap
// queries will use wrong integer IDs → silent data corruption.
// Probe storage to detect this case and log a warning.
try {
const probe = await this . storage . getNouns ( { pagination : { limit : 1 , offset : 0 } } )
if ( ( probe . totalCount ? ? 0 ) > 0 || probe . items . length > 0 ) {
console . warn (
` [EntityIdMapper] Mapper file missing but entities exist on disk. ` +
` IDs will be rebuilt during metadata index reconstruction. `
)
}
} catch {
// Storage not ready
}
2025-10-13 16:39:06 -07:00
}
} catch ( error ) {
// First time initialization - maps are empty, nextId = 1
}
}
/ * *
feat(8.0): EntityIdMapper U32 ceiling + EntityIdSpaceExceeded error
The JS fallback EntityIdMapper caps at u32::MAX to match the metadata
index's Roaring32 bitmap width. Before this guard, a brain past 4.29 B
entities would silently widen `nextId` into JS's safe-integer range,
truncating ints inside the bitmaps and zeroing query results — the
same silent under-count cortex 3.0's Piece 10 just closed on the
native path. Brainy 8.0 surfaces the overflow loudly instead.
When `nextId` would exceed `U32_ENTITY_ID_MAX`, `getOrAssign` throws
`EntityIdSpaceExceeded` with a message pointing at the cortex 3.0
binary mapper's `idSpace: 'u64'` mode as the migration path (mmap-
backed extendible-hash KV, persists the full u64 range losslessly).
The existing-UUID lookup path bypasses the guard — only fresh
allocations can overflow.
Surfaces via `@soulcraft/brainy/internals`:
- `EntityIdMapper` (already exported, behavior gated)
- `EntityIdSpaceExceeded` (new)
- `U32_ENTITY_ID_MAX` (new constant, = 0xFFFF_FFFF)
This is the lockstep half of cortex 3.0 / Piece 10 / Step 15. Cortex's
`NativeBinaryEntityIdMapperWrapper` ships the U64 binary mapper that
takes over above this ceiling; brainy ships the bright-line failure
that points consumers at it.
New test: tests/unit/utils/entity-id-mapper-u32-ceiling.test.ts (6
assertions covering the constant, the error shape, normal
allocations, the boundary case at exactly u32::MAX, the overflow
throw, and the existing-uuid lookup bypass).
2026-06-02 10:20:01 -07:00
* Get integer ID for UUID , assigning a new ID if not exists .
*
* The JS fallback mapper caps at [ ` U32_ENTITY_ID_MAX ` ] ( # U32_ENTITY_ID_MAX )
* to match the metadata index ' s Roaring32 bitmap width — once ` nextId `
* would exceed that , throws { @link EntityIdSpaceExceeded } so the caller
2026-07-02 15:11:41 -07:00
* loudly migrates to cor 's binary mapper with `idSpace: ' u64 ' `
feat(8.0): EntityIdMapper U32 ceiling + EntityIdSpaceExceeded error
The JS fallback EntityIdMapper caps at u32::MAX to match the metadata
index's Roaring32 bitmap width. Before this guard, a brain past 4.29 B
entities would silently widen `nextId` into JS's safe-integer range,
truncating ints inside the bitmaps and zeroing query results — the
same silent under-count cortex 3.0's Piece 10 just closed on the
native path. Brainy 8.0 surfaces the overflow loudly instead.
When `nextId` would exceed `U32_ENTITY_ID_MAX`, `getOrAssign` throws
`EntityIdSpaceExceeded` with a message pointing at the cortex 3.0
binary mapper's `idSpace: 'u64'` mode as the migration path (mmap-
backed extendible-hash KV, persists the full u64 range losslessly).
The existing-UUID lookup path bypasses the guard — only fresh
allocations can overflow.
Surfaces via `@soulcraft/brainy/internals`:
- `EntityIdMapper` (already exported, behavior gated)
- `EntityIdSpaceExceeded` (new)
- `U32_ENTITY_ID_MAX` (new constant, = 0xFFFF_FFFF)
This is the lockstep half of cortex 3.0 / Piece 10 / Step 15. Cortex's
`NativeBinaryEntityIdMapperWrapper` ships the U64 binary mapper that
takes over above this ceiling; brainy ships the bright-line failure
that points consumers at it.
New test: tests/unit/utils/entity-id-mapper-u32-ceiling.test.ts (6
assertions covering the constant, the error shape, normal
allocations, the boundary case at exactly u32::MAX, the overflow
throw, and the existing-uuid lookup bypass).
2026-06-02 10:20:01 -07:00
* rather than silently truncating entity ids .
2025-10-13 16:39:06 -07:00
* /
getOrAssign ( uuid : string ) : number {
const existing = this . uuidToInt . get ( uuid )
if ( existing !== undefined ) {
return existing
}
// Assign new ID
feat(8.0): EntityIdMapper U32 ceiling + EntityIdSpaceExceeded error
The JS fallback EntityIdMapper caps at u32::MAX to match the metadata
index's Roaring32 bitmap width. Before this guard, a brain past 4.29 B
entities would silently widen `nextId` into JS's safe-integer range,
truncating ints inside the bitmaps and zeroing query results — the
same silent under-count cortex 3.0's Piece 10 just closed on the
native path. Brainy 8.0 surfaces the overflow loudly instead.
When `nextId` would exceed `U32_ENTITY_ID_MAX`, `getOrAssign` throws
`EntityIdSpaceExceeded` with a message pointing at the cortex 3.0
binary mapper's `idSpace: 'u64'` mode as the migration path (mmap-
backed extendible-hash KV, persists the full u64 range losslessly).
The existing-UUID lookup path bypasses the guard — only fresh
allocations can overflow.
Surfaces via `@soulcraft/brainy/internals`:
- `EntityIdMapper` (already exported, behavior gated)
- `EntityIdSpaceExceeded` (new)
- `U32_ENTITY_ID_MAX` (new constant, = 0xFFFF_FFFF)
This is the lockstep half of cortex 3.0 / Piece 10 / Step 15. Cortex's
`NativeBinaryEntityIdMapperWrapper` ships the U64 binary mapper that
takes over above this ceiling; brainy ships the bright-line failure
that points consumers at it.
New test: tests/unit/utils/entity-id-mapper-u32-ceiling.test.ts (6
assertions covering the constant, the error shape, normal
allocations, the boundary case at exactly u32::MAX, the overflow
throw, and the existing-uuid lookup bypass).
2026-06-02 10:20:01 -07:00
if ( this . nextId > U32_ENTITY_ID_MAX ) {
throw new EntityIdSpaceExceeded ( this . nextId )
}
2025-10-13 16:39:06 -07:00
const newId = this . nextId ++
this . uuidToInt . set ( uuid , newId )
this . intToUuid . set ( newId , uuid )
this . dirty = true
return newId
}
2026-01-26 12:12:11 -08:00
/ * *
2026-01-27 15:38:21 -08:00
* Get integer ID for UUID with immediate persistence guarantee
2026-01-26 12:12:11 -08:00
* Unlike getOrAssign ( ) , this method flushes to storage immediately after assigning
* a new ID . This prevents UUID → int mapping divergence if the process crashes
* before a normal flush ( ) occurs .
*
* Use this for critical operations where data integrity is paramount .
* Normal operations can use getOrAssign ( ) with batched flushing for better performance .
* /
async getOrAssignSync ( uuid : string ) : Promise < number > {
const id = this . getOrAssign ( uuid )
// If a new ID was assigned, immediately persist to storage
if ( this . dirty ) {
await this . flush ( )
}
return id
}
2025-10-13 16:39:06 -07:00
/ * *
* Get UUID for integer ID
* /
getUuid ( intId : number ) : string | undefined {
return this . intToUuid . get ( intId )
}
/ * *
* Get integer ID for UUID ( without assigning if not exists )
* /
getInt ( uuid : string ) : number | undefined {
return this . uuidToInt . get ( uuid )
}
/ * *
* Check if UUID has been assigned an integer ID
* /
has ( uuid : string ) : boolean {
return this . uuidToInt . has ( uuid )
}
/ * *
* Remove mapping for UUID
* /
remove ( uuid : string ) : boolean {
const intId = this . uuidToInt . get ( uuid )
if ( intId === undefined ) {
return false
}
this . uuidToInt . delete ( uuid )
this . intToUuid . delete ( intId )
this . dirty = true
return true
}
/ * *
* Get total number of mappings
* /
get size ( ) : number {
return this . uuidToInt . size
}
2026-02-01 16:23:49 -08:00
/ * *
* Get all mapped integer IDs without storage reads .
* Used for bitmap negation operations ( ne , exists :false , missing :true )
* to avoid full - table getAllIds ( ) scans .
* /
getAllIntIds ( ) : number [ ] {
return Array . from ( this . intToUuid . keys ( ) )
}
2025-10-13 16:39:06 -07:00
/ * *
* Convert array of UUIDs to array of integers
* /
uuidsToInts ( uuids : string [ ] ) : number [ ] {
return uuids . map ( uuid = > this . getOrAssign ( uuid ) )
}
/ * *
* Convert array of integers to array of UUIDs
* /
intsToUuids ( ints : number [ ] ) : string [ ] {
const result : string [ ] = [ ]
for ( const intId of ints ) {
const uuid = this . intToUuid . get ( intId )
if ( uuid ) {
result . push ( uuid )
}
}
return result
}
/ * *
* Convert iterable of integers to array of UUIDs ( for roaring bitmap iteration )
* /
intsIterableToUuids ( ints : Iterable < number > ) : string [ ] {
const result : string [ ] = [ ]
for ( const intId of ints ) {
const uuid = this . intToUuid . get ( intId )
if ( uuid ) {
result . push ( uuid )
}
}
return result
}
feat(8.0): GraphAccelerationProvider contract — the native graph-engine seam
Defines the optional native graph-acceleration provider (cor 3.0) that
brainy feature-detects and routes brain.graph.* / related({node}) /
find({connected}) to, falling back to pure-TS adjacency when absent. This is
the published seam cor wires its native build against (mirrors the existing
VersionedIndexProvider pattern: defined in plugin.ts, exported from index.ts,
implemented externally, feature-detected at runtime).
Contract (locked cross-team in the design thread):
- GraphAccelerationProvider: traverse, edgesForNode, graphCursorOpen/Next/Close,
pageRank, connectedComponents, shortestPath, neighborhoodSample, topByDegree.
Every read op is generation-aware (optional trailing generation?: bigint) so
db.asOf(g).graph.* resolves historically; omitted = now.
- Subgraph: columnar wire format (BigInt64Array node/edge columns, Uint16Array
edgeTypes, optional Uint8Array nodeDepth, truncated flag) — parallel typed
arrays, never array-of-objects; brainy maps u64<->UUID lazily for rendered rows.
- OpaqueIdSet: a find() universe forwarded opaquely into traverse/search for the
zero-crossing query->expand fusion (brainy never inspects it; the provider
version-tags + validates the envelope).
- VectorIndexProvider.search gains allowedIds?: OpaqueIdSet | ReadonlySet<string>
(predicate pushdown — recovers filtered recall lost to post-filtering).
- EntityIdMapperProvider gains entityIntsToUuids(BigInt64Array) — the bigint batch
reverse resolver for Subgraph.nodes; implemented on the JS mapper (TS fallback).
All new public types exported from index.ts. Test: entityIntsToUuids round-trip
+ order-preservation + empty-int sentinel.
2026-06-21 08:12:22 -07:00
/ * *
* @description Batch reverse - resolve u64 entity ints ( a ` BigInt64Array ` ) → UUID
* strings — the bigint counterpart of { @link EntityIdMapper . intsIterableToUuids } ,
* for the native graph engine whose ` Subgraph.nodes ` is a ` BigInt64Array ` . The JS
* mapper is keyed by ` number ` ( u32 - era ) ; each int is narrowed via ` Number() ` — safe
* for the JS fallback ' s id range . Order - preserving ( one entry per input ) : an int the
* mapper has not assigned yields ` '' ` ( does not occur for graph - engine results , which
* reference assigned ints only ) .
* @param nodeInts - Entity ints as returned in a graph ` Subgraph ` .
* @returns One UUID per input , in order ( ` '' ` for a never - assigned int ) .
* @example
* const uuids = mapper . entityIntsToUuids ( subgraph . nodes )
* /
entityIntsToUuids ( nodeInts : BigInt64Array ) : string [ ] {
const result : string [ ] = new Array ( nodeInts . length )
for ( let i = 0 ; i < nodeInts . length ; i ++ ) {
result [ i ] = this . intToUuid . get ( Number ( nodeInts [ i ] ) ) ? ? ''
}
return result
}
2025-10-13 16:39:06 -07:00
/ * *
* Flush mappings to storage
* /
async flush ( ) : Promise < void > {
if ( ! this . dirty ) {
return
}
// Convert maps to plain objects for serialization
2026-01-27 15:38:21 -08:00
// Add required 'noun' property for NounMetadata
2025-10-17 12:29:27 -07:00
const data = {
noun : 'EntityIdMapper' ,
2025-10-13 16:39:06 -07:00
nextId : this.nextId ,
uuidToInt : Object.fromEntries ( this . uuidToInt ) ,
intToUuid : Object.fromEntries ( this . intToUuid )
}
2026-06-11 14:51:00 -07:00
await this . storage . saveMetadata ( this . storageKey , data )
2025-10-13 16:39:06 -07:00
this . dirty = false
}
/ * *
* Clear all mappings
* /
async clear ( ) : Promise < void > {
this . uuidToInt . clear ( )
this . intToUuid . clear ( )
this . nextId = 1
this . dirty = true
await this . flush ( )
}
2026-06-23 12:02:17 -07:00
// No `rebuild()` on the JS mapper by design (CTX-BR-RESTORE-REBUILD): after a
// `brain.restore()`, `MetadataIndex.rebuild()` re-derives this mapper from the
// restored entities via append-only `getOrAssign` (the ints it picks are
// internally consistent with the bitmaps it builds), so the JS path needs no
// explicit post-restore reload — and forcing one (blanking + reloading) would
// drop a still-referenced mapping when the snapshot omits the mapper file. The
// `rebuild()` reload is the NATIVE mapper's concern: cor's binary KV holds the
// authoritative int↔uuid the native adjacency is keyed on, so it must reload
// from the restored KV before the native graph rebuild. `restore()` therefore
// calls `rebuild()` only when the provider implements it (see brainy.ts).
2025-10-13 16:39:06 -07:00
/ * *
* Get statistics about the mapper
* /
getStats() {
return {
mappings : this.uuidToInt.size ,
nextId : this.nextId ,
dirty : this.dirty ,
memoryEstimate : this.uuidToInt.size * ( 36 + 8 + 4 + 8 ) // uuid string + map overhead + int + map overhead
}
}
}