feat: stable EntityIdMapper — rebuild() no longer renumbers UUID→int

Previously metadataIndex.rebuild() called idMapper.clear() which reset nextId
to 1 and renumbered every UUID by re-insertion order. Any consumer that had
persisted int-keyed data against the old map was silently invalidated — and
2.4.0's vector mmap store (#20), graph link compression (#21), and column-store
JS↔native interchange all need persisted int indices that survive a rebuild.

Remove the unconditional clear() in rebuild(). The rebuild already re-iterates
every entity via idMapper.getOrAssign(uuid), which returns the existing int
unchanged for known UUIDs. Stale UUID→int entries for entities no longer in
storage persist as harmless memory overhead; a dedicated prune step can be
added if it ever matters.

clearAllIndexData() — the explicit nuclear recovery path — keeps its existing
idMapper.clear() call (renumbering is intentional there), and now logs a
prodLog.warn making it explicit that any persisted int-keyed data is invalidated
and must be rebuilt from canonical sources.

Strengthened the EntityIdMapper class JSDoc to document the stability guarantee
as a contract — append-only getOrAssign, monotonic nextId, remove() leaves
permanent holes, rebuild() never renumbers, only clear() does.

Added tests/regression/entity-id-mapper-stability.test.ts pinning down the
five-point contract: (1) single-rebuild stability; (2) many-rebuild stability;
(3) post-rebuild adds get fresh monotonic ints; (4) removes leave permanent
holes — new entities never recycle; (5) clearAllIndexData() explicitly
renumbers (the documented destructive path).

Foundation for 2.4.0 #2-#4. Full test suite (62 files, 1417 tests) green.
This commit is contained in:
David Snelling 2026-05-28 09:45:22 -07:00
parent df7d739008
commit 01cf4b1b7e
3 changed files with 209 additions and 10 deletions

View file

@ -1,14 +1,34 @@
/**
* EntityIdMapper - Bidirectional mapping between UUID strings and integer IDs for roaring bitmaps
* EntityIdMapper - Bidirectional mapping between UUID strings and integer IDs.
*
* Roaring bitmaps require 32-bit unsigned integers, but Brainy uses UUID strings as entity IDs.
* This class provides efficient bidirectional mapping with persistence support.
* 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.
*
* Features:
* - O(1) lookup in both directions
* - Persistent storage via storage adapter
* - Atomic counter for next ID
* - Serialization/deserialization support
* - O(1) lookup in both directions.
* - Persistent storage via storage adapter.
* - Atomic, monotonic, append-only int counter.
* - Serialization/deserialization support.
*
* @module utils/entityIdMapper
*/