2025-11-14 10:26:23 -08:00
|
|
|
/**
|
|
|
|
|
* Index Operations with Rollback Support
|
|
|
|
|
*
|
|
|
|
|
* Provides transactional operations for all indexes:
|
2026-06-09 13:07:56 -07:00
|
|
|
* - JsHnswVectorIndex (unified vector index)
|
2025-11-14 10:26:23 -08:00
|
|
|
* - MetadataIndexManager (roaring bitmap filtering)
|
|
|
|
|
* - GraphAdjacencyIndex (LSM-tree graph storage)
|
|
|
|
|
*
|
|
|
|
|
* Each operation can be executed and rolled back atomically.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-06-09 13:07:56 -07:00
|
|
|
import type { JsHnswVectorIndex } from '../../hnsw/hnswIndex.js'
|
2025-11-14 10:26:23 -08:00
|
|
|
import type { MetadataIndexManager } from '../../utils/metadataIndex.js'
|
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
|
|
|
import type { GraphIndexProvider } from '../../plugin.js'
|
2025-11-14 10:26:23 -08:00
|
|
|
import type { GraphVerb } from '../../coreTypes.js'
|
|
|
|
|
import type { Operation, RollbackAction } from '../types.js'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add to HNSW index with rollback support
|
|
|
|
|
*
|
|
|
|
|
* Rollback strategy:
|
|
|
|
|
* - Remove item from index
|
feat(8.0)!: delete fork/branch/commit/history/versions — superseded by the Db API
The COW version-control surface (fork, branches, checkout, commit,
getHistory/streamHistory, asOfCommit, brain.versions) is gone, along with
its subsystems: src/versioning/, the COW object store (CommitLog,
CommitObject, RefManager, TreeObject), HistoricalStorageAdapter, and the
TypeAwareHNSWIndex path it kept alive. The Db API (now/transact/asOf/
with/persist/restore) is the one versioning model in 8.0.
Survivors and replacements:
- BlobStorage survives (the VFS stores file content through it), relocated
to src/storage/blobStorage.ts with binaryDataCodec.ts; its adapter
interface is now BlobStoreAdapter, slimmed to the consumed surface
(write/read/has/delete/getMetadata + MIME-aware compression policy).
- brain.migrate() backup branches are replaced by persist-before-migrate:
MigrateOptions.backupTo persists a hard-link snapshot of the current
generation before any transform runs; MigrationResult.backupPath reports
it, and brain.restore(path) brings it back wholesale.
- CLI: cow.ts (fork/branch/checkout/history/migrate) is replaced by
snapshot.ts — snapshot <path>, restore <path>, history (tx-log),
generation.
- New public read API: brain.transactionLog({limit}) exposes the reified
tx-log (generation/timestamp/meta, newest first) that backs the CLI
history command; TxLogEntry is exported.
Tests: superseded suites deleted; fork/commit blocks excised from shared
suites; BlobStorage tests relocated + reworked against the slimmed store;
migration tests now prove the backupTo snapshot/restore round trip; new
transactionLog coverage in db-mvcc.
2026-06-10 15:22:47 -07:00
|
|
|
|
2025-11-14 10:26:23 -08:00
|
|
|
*/
|
|
|
|
|
export class AddToHNSWOperation implements Operation {
|
|
|
|
|
readonly name = 'AddToHNSW'
|
|
|
|
|
|
|
|
|
|
constructor(
|
2026-06-09 13:07:56 -07:00
|
|
|
private readonly index: JsHnswVectorIndex,
|
2025-11-14 10:26:23 -08:00
|
|
|
private readonly id: string,
|
|
|
|
|
private readonly vector: number[]
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async execute(): Promise<RollbackAction> {
|
|
|
|
|
// Check if item already exists (for rollback decision)
|
|
|
|
|
const existed = await this.itemExists(this.id)
|
|
|
|
|
|
|
|
|
|
// Add to index
|
|
|
|
|
await this.index.addItem({ id: this.id, vector: this.vector })
|
|
|
|
|
|
|
|
|
|
// Return rollback action
|
|
|
|
|
return async () => {
|
|
|
|
|
if (!existed) {
|
|
|
|
|
// Remove newly added item
|
|
|
|
|
await this.index.removeItem(this.id)
|
|
|
|
|
}
|
|
|
|
|
// If item existed before, we don't rollback (update is OK)
|
|
|
|
|
// This prevents index corruption from removing pre-existing items
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-06-11 14:51:00 -07:00
|
|
|
* Check if item exists in index.
|
|
|
|
|
*
|
|
|
|
|
* `getItem` is an optional, feature-detected provider capability — see the
|
|
|
|
|
* VectorIndexProvider docs; it is intentionally absent from the required
|
|
|
|
|
* contract (Brainy's JS HNSW index omits it). When the capability is
|
|
|
|
|
* missing the answer must be `false`, not `true`: treating unknowable
|
|
|
|
|
* pre-existence as "existed" made every rollback skip removeItem, leaving
|
|
|
|
|
* phantom entries in the index after a failed transaction. The safe default
|
|
|
|
|
* is to remove what this operation added — update flows pair this op with a
|
|
|
|
|
* RemoveFromHNSWOperation whose own rollback restores the prior vector, so
|
|
|
|
|
* reverse-order rollback reconstructs the original state either way.
|
2025-11-14 10:26:23 -08:00
|
|
|
*/
|
|
|
|
|
private async itemExists(id: string): Promise<boolean> {
|
2026-06-11 14:51:00 -07:00
|
|
|
const index = this.index as JsHnswVectorIndex & {
|
|
|
|
|
getItem?: (id: string) => Promise<unknown>
|
|
|
|
|
}
|
|
|
|
|
if (typeof index.getItem !== 'function') return false
|
2025-11-14 10:26:23 -08:00
|
|
|
try {
|
2026-06-11 14:51:00 -07:00
|
|
|
const item = await index.getItem(id)
|
|
|
|
|
return item !== undefined && item !== null
|
2025-11-14 10:26:23 -08:00
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove from HNSW index with rollback support
|
|
|
|
|
*
|
|
|
|
|
* Rollback strategy:
|
|
|
|
|
* - Re-add item to index with original vector
|
|
|
|
|
*
|
|
|
|
|
* Note: Requires storing the vector for rollback
|
|
|
|
|
*/
|
|
|
|
|
export class RemoveFromHNSWOperation implements Operation {
|
|
|
|
|
readonly name = 'RemoveFromHNSW'
|
|
|
|
|
|
|
|
|
|
constructor(
|
2026-06-09 13:07:56 -07:00
|
|
|
private readonly index: JsHnswVectorIndex,
|
2025-11-14 10:26:23 -08:00
|
|
|
private readonly id: string,
|
|
|
|
|
private readonly vector: number[] // Required for rollback
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async execute(): Promise<RollbackAction> {
|
|
|
|
|
// Remove from index
|
|
|
|
|
await this.index.removeItem(this.id)
|
|
|
|
|
|
|
|
|
|
// Return rollback action
|
|
|
|
|
return async () => {
|
|
|
|
|
// Re-add item with original vector
|
|
|
|
|
await this.index.addItem({ id: this.id, vector: this.vector })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add to metadata index with rollback support
|
|
|
|
|
*
|
|
|
|
|
* Rollback strategy:
|
|
|
|
|
* - Remove item from index
|
|
|
|
|
*/
|
|
|
|
|
export class AddToMetadataIndexOperation implements Operation {
|
|
|
|
|
readonly name = 'AddToMetadataIndex'
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly index: MetadataIndexManager,
|
|
|
|
|
private readonly id: string,
|
|
|
|
|
private readonly entity: any // Entity or metadata structure
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async execute(): Promise<RollbackAction> {
|
|
|
|
|
// Add to metadata index (skipFlush=true for transaction atomicity)
|
|
|
|
|
await this.index.addToIndex(this.id, this.entity, true)
|
|
|
|
|
|
|
|
|
|
// Return rollback action
|
|
|
|
|
return async () => {
|
|
|
|
|
// Remove from metadata index
|
|
|
|
|
await this.index.removeFromIndex(this.id, this.entity)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove from metadata index with rollback support
|
|
|
|
|
*
|
|
|
|
|
* Rollback strategy:
|
|
|
|
|
* - Re-add item to index with original metadata
|
|
|
|
|
*/
|
|
|
|
|
export class RemoveFromMetadataIndexOperation implements Operation {
|
|
|
|
|
readonly name = 'RemoveFromMetadataIndex'
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly index: MetadataIndexManager,
|
|
|
|
|
private readonly id: string,
|
|
|
|
|
private readonly entity: any // Required for rollback
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async execute(): Promise<RollbackAction> {
|
|
|
|
|
// Remove from metadata index
|
|
|
|
|
await this.index.removeFromIndex(this.id, this.entity)
|
|
|
|
|
|
|
|
|
|
// Return rollback action
|
|
|
|
|
return async () => {
|
|
|
|
|
// Re-add with original metadata (skipFlush=true)
|
|
|
|
|
await this.index.addToIndex(this.id, this.entity, true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add verb to graph index with rollback support
|
|
|
|
|
*
|
|
|
|
|
* Rollback strategy:
|
|
|
|
|
* - Remove verb from graph index
|
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
|
|
|
*
|
|
|
|
|
* 8.0 u64 contract: the coordinator resolves both endpoint ints via the
|
|
|
|
|
* shared idMapper (`getOrAssign`) and passes them alongside the verb; the
|
|
|
|
|
* provider returns the interned verb int, which is surfaced through the
|
|
|
|
|
* optional `onVerbInt` callback so the coordinator can feed its warm cache.
|
2026-06-16 09:41:59 -07:00
|
|
|
*
|
|
|
|
|
* Generation: `generationFn` is resolved at execute time (not construction) so
|
|
|
|
|
* the edge is stamped at the transaction's in-flight commit generation — which
|
|
|
|
|
* the generation store only assigns once the batch begins executing. The same
|
|
|
|
|
* generation is reused for the rollback removal, so an add and its undo
|
|
|
|
|
* reference one watermark in a provider's per-generation edge chain.
|
2025-11-14 10:26:23 -08:00
|
|
|
*/
|
|
|
|
|
export class AddToGraphIndexOperation implements Operation {
|
|
|
|
|
readonly name = 'AddToGraphIndex'
|
|
|
|
|
|
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
|
|
|
/**
|
|
|
|
|
* @param index - The graph-index provider (JS baseline or native).
|
|
|
|
|
* @param verb - The verb to index (`sourceInt`/`targetInt` mirrored on it).
|
|
|
|
|
* @param sourceInt - The source entity's interned int.
|
|
|
|
|
* @param targetInt - The target entity's interned int.
|
2026-06-16 09:41:59 -07:00
|
|
|
* @param generationFn - Resolves the commit generation to stamp this edge at,
|
|
|
|
|
* evaluated when the operation executes (see class note).
|
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
|
|
|
* @param onVerbInt - Optional hook invoked with the interned verb int
|
|
|
|
|
* returned by the provider (feeds the coordinator's verb-int warm cache).
|
|
|
|
|
*/
|
2025-11-14 10:26:23 -08:00
|
|
|
constructor(
|
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
|
|
|
private readonly index: GraphIndexProvider,
|
|
|
|
|
private readonly verb: GraphVerb,
|
|
|
|
|
private readonly sourceInt: bigint,
|
|
|
|
|
private readonly targetInt: bigint,
|
2026-06-16 09:41:59 -07:00
|
|
|
private readonly generationFn: () => bigint,
|
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
|
|
|
private readonly onVerbInt?: (verbInt: bigint) => void
|
2025-11-14 10:26:23 -08:00
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async execute(): Promise<RollbackAction> {
|
2026-06-16 09:41:59 -07:00
|
|
|
// Stamp this edge at the in-flight commit generation; reuse it for the
|
|
|
|
|
// rollback so add + undo reference the same watermark.
|
|
|
|
|
const generation = this.generationFn()
|
|
|
|
|
const verbInt = await this.index.addVerb(this.verb, this.sourceInt, this.targetInt, generation)
|
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
|
|
|
this.onVerbInt?.(verbInt)
|
2025-11-14 10:26:23 -08:00
|
|
|
|
|
|
|
|
// Return rollback action
|
|
|
|
|
return async () => {
|
|
|
|
|
// Remove verb from graph index
|
2026-06-16 09:41:59 -07:00
|
|
|
await this.index.removeVerb(this.verb.id, generation)
|
2025-11-14 10:26:23 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove verb from graph index with rollback support
|
|
|
|
|
*
|
|
|
|
|
* Rollback strategy:
|
|
|
|
|
* - Re-add verb to graph index
|
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
|
|
|
*
|
|
|
|
|
* 8.0 u64 contract: rollback re-adds through `addVerb(verb, sourceInt,
|
2026-06-16 09:41:59 -07:00
|
|
|
* targetInt, generation)`, so the coordinator resolves the endpoint ints up
|
|
|
|
|
* front (while the entity → int mappings are guaranteed to still exist). The
|
|
|
|
|
* removal generation is resolved at execute time and reused for the rollback
|
|
|
|
|
* re-add, so the round trip references one watermark.
|
2025-11-14 10:26:23 -08:00
|
|
|
*/
|
|
|
|
|
export class RemoveFromGraphIndexOperation implements Operation {
|
|
|
|
|
readonly name = 'RemoveFromGraphIndex'
|
|
|
|
|
|
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
|
|
|
/**
|
|
|
|
|
* @param index - The graph-index provider (JS baseline or native).
|
|
|
|
|
* @param verb - The verb being removed (required for rollback re-add).
|
|
|
|
|
* @param sourceInt - The source entity's interned int (rollback re-add).
|
|
|
|
|
* @param targetInt - The target entity's interned int (rollback re-add).
|
2026-06-16 09:41:59 -07:00
|
|
|
* @param generationFn - Resolves the commit generation for this removal,
|
|
|
|
|
* evaluated when the operation executes.
|
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
|
|
|
*/
|
2025-11-14 10:26:23 -08:00
|
|
|
constructor(
|
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
|
|
|
private readonly index: GraphIndexProvider,
|
|
|
|
|
private readonly verb: GraphVerb, // Required for rollback
|
|
|
|
|
private readonly sourceInt: bigint,
|
2026-06-16 09:41:59 -07:00
|
|
|
private readonly targetInt: bigint,
|
|
|
|
|
private readonly generationFn: () => bigint
|
2025-11-14 10:26:23 -08:00
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async execute(): Promise<RollbackAction> {
|
2026-06-16 09:41:59 -07:00
|
|
|
// Resolve the removal generation once; reuse it for the rollback re-add.
|
|
|
|
|
const generation = this.generationFn()
|
|
|
|
|
await this.index.removeVerb(this.verb.id, generation)
|
2025-11-14 10:26:23 -08:00
|
|
|
|
|
|
|
|
// Return rollback action
|
|
|
|
|
return async () => {
|
|
|
|
|
// Re-add verb with original data
|
2026-06-16 09:41:59 -07:00
|
|
|
await this.index.addVerb(this.verb, this.sourceInt, this.targetInt, generation)
|
2025-11-14 10:26:23 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Batch operation: Add multiple items to HNSW index
|
|
|
|
|
*
|
|
|
|
|
* Useful for bulk imports with transaction support.
|
|
|
|
|
* Rolls back all items if any fail.
|
|
|
|
|
*/
|
|
|
|
|
export class BatchAddToHNSWOperation implements Operation {
|
|
|
|
|
readonly name = 'BatchAddToHNSW'
|
|
|
|
|
|
|
|
|
|
private operations: AddToHNSWOperation[]
|
|
|
|
|
|
|
|
|
|
constructor(
|
2026-06-09 13:07:56 -07:00
|
|
|
index: JsHnswVectorIndex,
|
2025-11-14 10:26:23 -08:00
|
|
|
items: Array<{ id: string; vector: number[] }>
|
|
|
|
|
) {
|
|
|
|
|
this.operations = items.map(
|
|
|
|
|
item => new AddToHNSWOperation(index, item.id, item.vector)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async execute(): Promise<RollbackAction> {
|
|
|
|
|
const rollbackActions: RollbackAction[] = []
|
|
|
|
|
|
|
|
|
|
// Execute all operations
|
|
|
|
|
for (const op of this.operations) {
|
|
|
|
|
const rollback = await op.execute()
|
|
|
|
|
if (rollback) {
|
|
|
|
|
rollbackActions.push(rollback)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return combined rollback action
|
|
|
|
|
return async () => {
|
|
|
|
|
// Execute all rollbacks in reverse order
|
|
|
|
|
for (let i = rollbackActions.length - 1; i >= 0; i--) {
|
|
|
|
|
await rollbackActions[i]()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Batch operation: Add multiple entities to metadata index
|
|
|
|
|
*
|
|
|
|
|
* Useful for bulk imports with transaction support.
|
|
|
|
|
*/
|
|
|
|
|
export class BatchAddToMetadataIndexOperation implements Operation {
|
|
|
|
|
readonly name = 'BatchAddToMetadataIndex'
|
|
|
|
|
|
|
|
|
|
private operations: AddToMetadataIndexOperation[]
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
index: MetadataIndexManager,
|
|
|
|
|
items: Array<{ id: string; entity: any }>
|
|
|
|
|
) {
|
|
|
|
|
this.operations = items.map(
|
|
|
|
|
item => new AddToMetadataIndexOperation(index, item.id, item.entity)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async execute(): Promise<RollbackAction> {
|
|
|
|
|
const rollbackActions: RollbackAction[] = []
|
|
|
|
|
|
|
|
|
|
// Execute all operations
|
|
|
|
|
for (const op of this.operations) {
|
|
|
|
|
const rollback = await op.execute()
|
|
|
|
|
if (rollback) {
|
|
|
|
|
rollbackActions.push(rollback)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return combined rollback action
|
|
|
|
|
return async () => {
|
|
|
|
|
// Execute all rollbacks in reverse order
|
|
|
|
|
for (let i = rollbackActions.length - 1; i >= 0; i--) {
|
|
|
|
|
await rollbackActions[i]()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|