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).
This commit is contained in:
David Snelling 2026-06-02 10:20:01 -07:00
parent 8f130d3e73
commit e47fea0917
3 changed files with 158 additions and 2 deletions

View file

@ -7,7 +7,11 @@ export type { UnifiedCacheConfig, CacheItem } from './utils/unifiedCache.js'
export { prodLog, createModuleLogger } from './utils/logger.js'
export { FieldTypeInference, FieldType } from './utils/fieldTypeInference.js'
export type { FieldTypeInfo } from './utils/fieldTypeInference.js'
export { EntityIdMapper } from './utils/entityIdMapper.js'
export {
EntityIdMapper,
EntityIdSpaceExceeded,
U32_ENTITY_ID_MAX,
} from './utils/entityIdMapper.js'
export type { EntityIdMapperOptions, EntityIdMapperData } from './utils/entityIdMapper.js'
export { getRecommendedCacheConfig, formatBytes, checkMemoryPressure } from './utils/memoryDetection.js'
export type { MemoryInfo, CacheAllocationStrategy } from './utils/memoryDetection.js'

View file

@ -36,6 +36,51 @@
import type { StorageAdapter } from '../coreTypes.js'
import type { EntityIdMapperProvider } from '../plugin.js'
/**
* 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
* downstream bitmap. The cortex 3.0 binary mapper supports a U64 IdSpace
* 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
* cortex-free fallback; once a brain has more than ~4.29 B entities,
* callers MUST install the cortex 3.0 `NativeBinaryEntityIdMapper` with
* `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 ` +
`entities, install @soulcraft/cortex and configure the binary ` +
`mapper with idSpace: 'u64' (mmap-backed extendible-hash KV).`,
)
this.name = 'EntityIdSpaceExceeded'
this.attempted = attempted
}
}
export interface EntityIdMapperOptions {
storage: StorageAdapter
storageKey?: string
@ -109,7 +154,13 @@ export class EntityIdMapper implements EntityIdMapperProvider {
}
/**
* Get integer ID for UUID, assigning a new ID if not exists
* 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
* loudly migrates to cortex's binary mapper with `idSpace: 'u64'`
* rather than silently truncating entity ids.
*/
getOrAssign(uuid: string): number {
const existing = this.uuidToInt.get(uuid)
@ -118,6 +169,9 @@ export class EntityIdMapper implements EntityIdMapperProvider {
}
// Assign new ID
if (this.nextId > U32_ENTITY_ID_MAX) {
throw new EntityIdSpaceExceeded(this.nextId)
}
const newId = this.nextId++
this.uuidToInt.set(uuid, newId)
this.intToUuid.set(newId, uuid)