fix(update-seam): the metadata crossing never carries BigInt endpoint ints
All checks were successful
CI / Node 22 (push) Successful in 12m19s
CI / Node 24 (push) Successful in 12m25s
CI / Bun (latest) (push) Successful in 12m24s
CI / Integration + conformance (Node 22) (push) Successful in 19m14s

resolveVerbEndpointInts mirrors the resolved u64 endpoint ints onto the
verb object itself as BigInt (verb.sourceInt/targetInt) for the graph legs'
own params. The live verb path's delete legs then reused that same object
as the metadata-index crossing — and the seam's metadata is JSON-safe by
contract (a native provider serializes it; u64 as Number corrupts above
2^53), so JSON.stringify threw and the whole transaction aborted. Found by
the first joint pair gate; four downstream suites red from one crossing.

The crossing now routes through a JSON-safe view that drops BigInt-valued
top-level keys — endpoint ints ride their own op params on the graph legs,
exactly as designed, and never the metadata crossing. Applied at the
retraction helper (cascade + unrelate + transact mirrors) and
updateRelation's remove leg.

Pinned by driving the exact shape (relate resolves ints, remove cascades
the same object) through a provider shim enforcing the JSON contract —
red-proved against the unfixed path (the joint gate's verbatim error),
green with the fix.
This commit is contained in:
David Snelling 2026-08-25 12:07:28 -07:00
parent f14da34b27
commit f4780c8e88
2 changed files with 70 additions and 2 deletions

View file

@ -3831,13 +3831,45 @@ export class Brainy<T = any> implements BrainyInterface<T> {
* @returns The operation to add to the caller's batch, or `null` when
* nothing could be done (already narrated + tracked as degraded).
*/
/**
* @description A JSON-safe view of a record bound for the metadata-index
* crossing. The seam's metadata is JSON-safe BY CONTRACT (a native provider
* serializes it; u64 ints as Number corrupt above 2^53) but
* {@link resolveVerbEndpointInts} MIRRORS the resolved endpoint ints onto
* the verb object itself as BigInt (`verb.sourceInt`/`targetInt`), so a
* verb object reused as index metadata carried BigInts into
* JSON.stringify, which throws, aborting the whole transaction (found by
* the first joint pair gate). Endpoint ints ride their OWN op params on the
* graph legs the metadata crossing drops every BigInt-valued top-level
* key instead of guessing at a lossy numeric encoding.
* @param metadata - The candidate index-metadata record.
* @returns The same object when already JSON-safe, else a shallow copy
* without the BigInt-valued keys.
*/
private static jsonSafeIndexMetadata(metadata: unknown): unknown {
if (metadata === null || typeof metadata !== 'object') return metadata
const rec = metadata as Record<string, unknown>
let hasBigint = false
for (const k in rec) {
if (typeof rec[k] === 'bigint') { hasBigint = true; break }
}
if (!hasBigint) return metadata
const out: Record<string, unknown> = {}
for (const k in rec) {
if (typeof rec[k] !== 'bigint') out[k] = rec[k]
}
return out
}
private metadataIndexRetractionOp(
id: string,
metadata: unknown,
context: string
): Operation | null {
if (metadata) {
return new RemoveFromMetadataIndexOperation(this.metadataIndex, id, metadata, this.indexWriteGeneration)
return new RemoveFromMetadataIndexOperation(
this.metadataIndex, id, Brainy.jsonSafeIndexMetadata(metadata), this.indexWriteGeneration
)
}
const prov = this.metadataIndex as unknown as {
removeEntityById?: (id: string) => Promise<void>
@ -5114,7 +5146,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
// read above); `updatedMetadata` is the raw stored record just
// persisted — the same shape relate()/rebuild() use to add.
tx.addOperation(
new RemoveFromMetadataIndexOperation(this.metadataIndex, params.id, existing, this.indexWriteGeneration)
new RemoveFromMetadataIndexOperation(this.metadataIndex, params.id, Brainy.jsonSafeIndexMetadata(existing), this.indexWriteGeneration)
)
tx.addOperation(
new AddToMetadataIndexOperation(this.metadataIndex, params.id, updatedMetadata, this.indexWriteGeneration)