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

@ -187,4 +187,40 @@ describe('verb metadata rows — the live path matches the rebuild walk', () =>
expect(await index.getIds('tag', 'parity-f')).toEqual([])
})
it('the metadata crossing never carries BigInt endpoint ints — a cascade delete after graph resolution survives JSON', async () => {
// resolveVerbEndpointInts MIRRORS the resolved u64 ints onto the verb
// object as BigInt (verb.sourceInt/targetInt). A provider that JSON-
// serializes the metadata crossing dies on BigInt — found by the first
// joint pair gate. This pin drives the exact shape: relate (graph legs
// resolve ints), then remove the source entity (the cascade passes the
// SAME verb object to the retraction), through a provider shim that
// enforces the JSON-safety contract the way a native provider does.
const employee = await brain.add({ data: 'cascade employee', type: 'person' })
const invoice = await brain.add({ data: 'cascade invoice', type: 'document' })
await brain.relate({ from: employee, to: invoice, type: 'relatedTo' })
const mgr: any = (brain as any).metadataIndex
const origRemove = mgr.removeFromIndex.bind(mgr)
const seen: unknown[] = []
mgr.removeFromIndex = async (id: string, metadata?: unknown, generation?: bigint) => {
seen.push(metadata)
JSON.stringify(metadata) // the contract: throws on BigInt, exactly like a native crossing
return origRemove(id, metadata, generation)
}
try {
await brain.remove(employee) // cascades the relation's retraction
} finally {
mgr.removeFromIndex = origRemove
}
expect(seen.length).toBeGreaterThan(0)
for (const m of seen) {
if (m && typeof m === 'object') {
for (const [k, v] of Object.entries(m as Record<string, unknown>)) {
expect(typeof v, `metadata key ${k} must be JSON-safe`).not.toBe('bigint')
}
}
}
})
})