feat(plugin): every provider write surface carries the real committed generation

The provider contract (metadata addToIndex/removeFromIndex, vector
addItem/removeItem, id-mapper getOrAssign/remove) gains an optional
trailing generation — evaluated lazily at operation execute time (the
graph surface's thunk pattern, generalized), threaded from all 17
construction sites: undefined during generation-0 bootstrap, the real
committed generation everywhere else. Optional = additive: no existing
provider or caller breaks; native delta logs that stamped literal zero
start hearing truth. JS twins accept the parameter with parity notes.
Pins: provider doubles capture and assert nonzero monotonic generations
across add/update/remove on both surfaces.
This commit is contained in:
David Snelling 2026-08-10 09:29:06 -07:00
parent 3484107462
commit 2d532684b4
7 changed files with 583 additions and 63 deletions

View file

@ -164,8 +164,15 @@ export class EntityIdMapper implements EntityIdMapperProvider {
* would exceed that, throws {@link EntityIdSpaceExceeded} so the caller
* loudly migrates to cor's binary mapper with `idSpace: 'u64'`
* rather than silently truncating entity ids.
*
* @param generation - Brainy's commit generation current at mint time
* (contract parity with the `EntityIdMapperProvider` surface). This JS
* mapper keeps a snapshot file, not a per-record delta log, so there is
* no natural slot to store it accepted and ignored; a native mapper
* stamps its assignment records with it.
*/
getOrAssign(uuid: string): number {
getOrAssign(uuid: string, generation?: bigint): number {
void generation // Contract parity — no per-record log in the JS mapper.
const existing = this.uuidToInt.get(uuid)
if (existing !== undefined) {
return existing
@ -226,8 +233,14 @@ export class EntityIdMapper implements EntityIdMapperProvider {
/**
* Remove mapping for UUID
*
* @param generation - Brainy's commit generation for this removal (contract
* parity with the `EntityIdMapperProvider` surface). Accepted and ignored
* this JS mapper removes immediately; a native mapper tombstones the
* mapping at this generation in its version chain.
*/
remove(uuid: string): boolean {
remove(uuid: string, generation?: bigint): boolean {
void generation // Contract parity — no per-key version chain in the JS mapper.
const intId = this.uuidToInt.get(uuid)
if (intId === undefined) {
return false

View file

@ -1459,8 +1459,16 @@ export class MetadataIndexManager implements MetadataIndexProvider {
* @param id - Entity ID
* @param entityOrMetadata - Either full entity structure or plain metadata (backward compat)
* @param skipFlush - Skip automatic flush (used during batch operations)
* @param deferWrites - Batch mode: buffer postings for a later flush
* @param generation - Brainy's commit generation for this write (see the
* {@link import('../plugin.js').MetadataIndexProvider} contract). This JS
* manager keeps a single live view with no per-record delta log, so it
* has no slot to store it the value is accepted for contract parity
* and forwarded to the shared id mapper (an injected native mapper
* stamps its assignment records with it; the JS mapper ignores it).
* The JS twin adopts full per-write stamping with the watermark train.
*/
async addToIndex(id: string, entityOrMetadata: any, skipFlush: boolean = false, deferWrites: boolean = false): Promise<void> {
async addToIndex(id: string, entityOrMetadata: any, skipFlush: boolean = false, deferWrites: boolean = false, generation?: bigint): Promise<void> {
const fields = this.extractIndexableFields(entityOrMetadata)
// Sanity check for excessive indexed fields (indicates possible data issue)
@ -1508,7 +1516,10 @@ export class MetadataIndexManager implements MetadataIndexProvider {
// element, so a scalar overwrite (last-value-wins) would index only the final
// element and `contains` would miss the rest.
if (this.columnStore) {
const entityIntId = this.idMapper.getOrAssign(id)
// Thread the commit generation into the mint: an injected native mapper
// stamps the assignment record's delta log with the real watermark
// instead of a literal 0 (the JS mapper accepts and ignores it).
const entityIntId = this.idMapper.getOrAssign(id, generation)
const fieldsMap: Record<string, unknown> = {}
for (const { field, value } of fields) {
if (field === '__words__') {
@ -1600,8 +1611,13 @@ export class MetadataIndexManager implements MetadataIndexProvider {
*
* @param id - Entity ID to remove
* @param metadata - Optional entity or metadata structure (if not provided, requires scanning all fields - slow!)
* @param generation - Brainy's commit generation for this removal (see the
* {@link import('../plugin.js').MetadataIndexProvider} contract). Accepted
* for contract parity this JS manager removes immediately (no tombstone
* chain) and forwards it to the shared id mapper's `remove`, where an
* injected native mapper tombstones the mapping at this generation.
*/
async removeFromIndex(id: string, metadata?: any): Promise<void> {
async removeFromIndex(id: string, metadata?: any, generation?: bigint): Promise<void> {
if (metadata) {
const fields = this.extractIndexableFields(metadata)
@ -1625,7 +1641,9 @@ export class MetadataIndexManager implements MetadataIndexProvider {
// Clean up ID mapper — must happen AFTER column store removal since it uses
// idMapper.getInt(id). Prevents deleted IDs from persisting in the mapper
// universe, which would cause ne/exists:false queries to return deleted entities.
this.idMapper.remove(id)
// The generation rides along so a native mapper tombstones the mapping at
// the real commit watermark (the JS mapper ignores it).
this.idMapper.remove(id, generation)
await this.idMapper.flush()
}