feat(8.0): GraphAccelerationProvider contract — the native graph-engine seam

Defines the optional native graph-acceleration provider (cor 3.0) that
brainy feature-detects and routes brain.graph.* / related({node}) /
find({connected}) to, falling back to pure-TS adjacency when absent. This is
the published seam cor wires its native build against (mirrors the existing
VersionedIndexProvider pattern: defined in plugin.ts, exported from index.ts,
implemented externally, feature-detected at runtime).

Contract (locked cross-team in the design thread):
- GraphAccelerationProvider: traverse, edgesForNode, graphCursorOpen/Next/Close,
  pageRank, connectedComponents, shortestPath, neighborhoodSample, topByDegree.
  Every read op is generation-aware (optional trailing generation?: bigint) so
  db.asOf(g).graph.* resolves historically; omitted = now.
- Subgraph: columnar wire format (BigInt64Array node/edge columns, Uint16Array
  edgeTypes, optional Uint8Array nodeDepth, truncated flag) — parallel typed
  arrays, never array-of-objects; brainy maps u64<->UUID lazily for rendered rows.
- OpaqueIdSet: a find() universe forwarded opaquely into traverse/search for the
  zero-crossing query->expand fusion (brainy never inspects it; the provider
  version-tags + validates the envelope).
- VectorIndexProvider.search gains allowedIds?: OpaqueIdSet | ReadonlySet<string>
  (predicate pushdown — recovers filtered recall lost to post-filtering).
- EntityIdMapperProvider gains entityIntsToUuids(BigInt64Array) — the bigint batch
  reverse resolver for Subgraph.nodes; implemented on the JS mapper (TS fallback).

All new public types exported from index.ts. Test: entityIntsToUuids round-trip
+ order-preservation + empty-int sentinel.
This commit is contained in:
David Snelling 2026-06-21 08:12:22 -07:00
parent 682e786cc3
commit a3d6fdb8b3
4 changed files with 391 additions and 1 deletions

View file

@ -180,6 +180,28 @@ export type {
// Optional provider capability for generation-aware native indexes
export { isVersionedIndexProvider } from './plugin.js'
export type { VersionedIndexProvider } from './plugin.js'
// Optional native graph-acceleration engine (cor 3.0) — the published provider
// contract + its columnar wire types. Brainy feature-detects an implementation
// and falls back to its pure-TS adjacency when absent.
export type {
GraphAccelerationProvider,
Subgraph,
OpaqueIdSet,
GraphTraversalDirection,
TraverseOptions,
EdgesForNodeOptions,
GraphCursorHandle,
GraphCursorOptions,
GraphCursorChunk,
GraphScores,
GraphComponents,
GraphPath,
PageRankOptions,
ComponentsOptions,
ShortestPathOptions,
NeighborhoodSampleOptions,
TopByDegreeOptions
} from './plugin.js'
// Export embedding functionality
import {

View file

@ -287,6 +287,298 @@ export interface GraphIndexProvider {
getAllRelationshipCounts(): Map<string, number>
}
// ============= Graph Acceleration (optional native engine) =============
/**
* Opaque serialized id-set a cor-internal roaring payload (a Node `Buffer` at
* runtime when the native engine is present), passed straight from a `find()`
* universe into `traverse` / `VectorIndexProvider.search` with NO id
* materialization in TypeScript (the O(1)-crossing queryexpand win). Brainy
* NEVER inspects it; the provider version-tags the envelope and throws on a
* format mismatch (it owns integrity + the id-space-width guarantee). Typed as
* `Uint8Array` (which a Node `Buffer` satisfies) so the universal build stays
* browser-safe.
*/
export type OpaqueIdSet = Uint8Array
/** Direction of a graph traversal relative to each frontier node. */
export type GraphTraversalDirection = 'in' | 'out' | 'both'
/**
* Columnar subgraph the shared wire format for every native graph read
* (`traverse`, `edgesForNode`, cursor chunks). Parallel typed arrays, never an
* array-of-objects, so the NAPI boundary transfers them near-zero-copy and
* Brainy maps u64UUID **lazily** (only for the rows a caller actually renders,
* via `EntityIdMapperProvider.entityIntsToUuids` + `GraphIndexProvider.verbIntsToIds`).
* The three `edge*` arrays are parallel (index `i` is one edge); `nodes` /
* `nodeDepth` are parallel.
*/
export interface Subgraph {
/** Discovered entity ints; resolve via `entityIntsToUuids`. */
nodes: BigInt64Array
/** Hop distance of each node from the nearest seed (parallel to `nodes`). Present iff `includeDepth`. */
nodeDepth?: Uint8Array
/** Edge source entity ints. */
edgeSources: BigInt64Array
/** Edge target entity ints. */
edgeTargets: BigInt64Array
/** Edge verb ints; resolve via `verbIntsToIds`. */
edgeVerbInts: BigInt64Array
/** Edge verb-type indices (stable TypeIdx; resolve via `TypeUtils.getVerbFromIndex`). */
edgeTypes: Uint16Array
/**
* `true` when a `maxNodes` / `maxEdges` cap truncated the result. The returned
* rows are the deterministic BFS-order prefix; page the remainder with a
* `graphCursor` (traverse stays stateless/bounded no continuation token).
*/
truncated?: boolean
/** When `truncated`, how many nodes/edges were cut (for "+N more" affordances). */
truncatedNodeCount?: number
truncatedEdgeCount?: number
}
/** Knobs shared by the bounded traversals. */
export interface TraverseOptions {
/** Max hop distance from any seed. */
depth: number
/** Edge direction to follow (default `'both'`). */
direction?: GraphTraversalDirection
/** Restrict traversed edges to these verb-type indices (TypeIdx). */
verbTypes?: number[]
/** Restrict traversed edges to these subtypes. */
subtypes?: string[]
/**
* Visibility tiers to EXCLUDE from the frontier (default: `['internal','system']`,
* matching `related()`). Pass `[]` for an all-tiers (admin) view.
*/
excludeVisibility?: string[]
/** Cap on returned nodes (deterministic BFS-order prefix; sets `Subgraph.truncated`). */
maxNodes?: number
/** Cap on returned edges. */
maxEdges?: number
/** Include the edge arrays (`false` = nodes-only reachability). Default `true`. */
includeEdges?: boolean
/** Populate `Subgraph.nodeDepth`. Default `false`. */
includeDepth?: boolean
}
/** Options for the both-direction single-node edge read. */
export interface EdgesForNodeOptions {
/** Edge direction (default `'both'`). */
direction?: GraphTraversalDirection
verbTypes?: number[]
subtypes?: string[]
excludeVisibility?: string[]
limit?: number
}
/** Opaque server-side cursor handle (TTL-bounded; pinned to a generation at open). */
export type GraphCursorHandle = string
/** Options for opening a streaming whole-graph (or seeded) cursor. */
export interface GraphCursorOptions {
direction?: GraphTraversalDirection
excludeVisibility?: string[]
/** `'light'` = ids/types only, no metadata-join columns (viz default); `'full'` = with joins. */
projection?: 'light' | 'full'
/** Seed set to bound the walk; omitted = the whole graph. */
seeds?: bigint[] | OpaqueIdSet
/**
* Resume token from a prior chunk used to continue after a `SnapshotExpired`
* (the pinned generation's TTL lapsed): reopen with this to resume the walk.
*/
cursor?: string
}
/** One streamed chunk of a graph cursor walk. */
export interface GraphCursorChunk {
/** The chunk's nodes + edges, columnar. */
subgraph: Subgraph
/** Opaque resume token (pass to `graphCursorOpen({ cursor })` after `SnapshotExpired`). */
cursor?: string
/** `true` once the walk is exhausted; `graphCursorNext` should not be called again. */
done: boolean
}
/** Per-node scalar scores (pageRank, topByDegree); `scores[i]` belongs to `nodeInts[i]`. */
export interface GraphScores {
nodeInts: BigInt64Array
scores: Float64Array
}
/** Connected-component labelling; `componentIds[i]` is the component of `nodeInts[i]`. */
export interface GraphComponents {
nodeInts: BigInt64Array
componentIds: Uint32Array
componentCount: number
}
/** A shortest path: the node sequence + the edges between them (or `null` if unreachable). */
export interface GraphPath {
nodeInts: BigInt64Array
edgeVerbInts: BigInt64Array
/** Sum of edge weights for a weighted path; hop count for an unweighted one. */
cost: number
}
export interface PageRankOptions {
/** Damping factor (default 0.85). */
damping?: number
/** Max power-iterations (default ~20). */
maxIterations?: number
/** Convergence tolerance. */
tolerance?: number
excludeVisibility?: string[]
/** Return only the top-K by score (default: all). */
topK?: number
}
export interface ComponentsOptions {
/** `'weak'` ignores direction (default); `'strong'` honors it. */
mode?: 'weak' | 'strong'
excludeVisibility?: string[]
}
export interface ShortestPathOptions {
direction?: GraphTraversalDirection
/** `'edge'` = weighted by edge weight; `'hops'` = unweighted BFS (default `'hops'`). */
weight?: 'edge' | 'hops'
verbTypes?: number[]
excludeVisibility?: string[]
/** Abandon the search past this many hops. */
maxDepth?: number
}
export interface NeighborhoodSampleOptions {
depth: number
direction?: GraphTraversalDirection
/** Max neighbors to sample per node (random, seeded for reproducibility). */
fanout: number
/** RNG seed so a given (seeds, opts, seed) yields a stable sample for viz. */
seed?: number
excludeVisibility?: string[]
maxNodes?: number
}
export interface TopByDegreeOptions {
direction?: GraphTraversalDirection
topK: number
excludeVisibility?: string[]
}
/**
* The `'graphAcceleration'` provider an OPTIONAL native graph engine (the
* cor 3.0 acceleration layer). Brainy feature-detects it on the registered
* providers: when present, the public `brain.graph.*` surface and the
* `related({ node })` / `neighbors({ depth })` / `find({ connected })` paths
* route here; when absent, Brainy serves the same operations from its pure-TS
* adjacency fallback (correct, small-graph-scale). It is SEPARATE from the
* required {@link GraphIndexProvider} (which stays the bigint-adjacency
* contract) so "optional native acceleration" is explicit at the seam.
*
* **Generation-aware (8.0 time-travel).** Every read takes an optional trailing
* `generation?: bigint`; omitted = the current generation ("now"). With a
* generation, the provider resolves the graph **as of** that generation
* (`db.asOf(g).graph.*`), reading its generation-filtered adjacency rather than
* the now-only fast path.
*
* **Columnar + opaque-id-set.** Reads return the columnar {@link Subgraph};
* `traverse` / sample accept seeds as `bigint[]` OR an {@link OpaqueIdSet}
* (a `find()` universe forwarded with no id materialization the queryexpand
* fusion). Visibility tiers are excluded at the index by default.
*/
export interface GraphAccelerationProvider {
/** `false` until the native engine has loaded; Brainy probes before routing. */
readonly isInitialized: boolean
/**
* @description Bounded multi-hop expansion from `seeds`, returning the reachable
* subgraph. One call replaces Brainy's per-hop BFS round-trips.
* @param seeds - Start nodes as entity ints, OR an {@link OpaqueIdSet} (a
* `find()` universe the frontier is intersected in id-space, zero crossing).
* @param options - Depth, direction, edge/visibility filters, and caps.
* @param generation - Optional as-of generation (omitted = now).
* @returns The reachable {@link Subgraph} (BFS-order; `truncated` set if capped).
*/
traverse(seeds: bigint[] | OpaqueIdSet, options: TraverseOptions, generation?: bigint): Promise<Subgraph>
/**
* @description All edges incident to one node, both directions, as structure +
* cor-indexed fields (Brainy hydrates full edge metadata lazily the provider
* cannot recover verb-id strings from its interned form).
* @param nodeInt - The node's interned entity int.
* @param options - Direction, edge/visibility filters, limit.
* @param generation - Optional as-of generation.
* @returns A {@link Subgraph} of the node's incident edges.
*/
edgesForNode(nodeInt: bigint, options: EdgesForNodeOptions, generation?: bigint): Promise<Subgraph>
/**
* @description Open a snapshot-consistent streaming walk of the whole graph (or a
* seeded region) for O(N) viz loads. Pins a generation at open; the walk reads
* as-of that pin (no dup/skip under concurrent writes). Handles are TTL-bounded
* `graphCursorNext` after expiry rejects with `SnapshotExpired`; reopen with the
* last chunk's `cursor` to resume. Call `graphCursorClose` on normal completion.
* @param options - Direction, visibility, projection mode, optional seeds / resume cursor.
* @param generation - Optional explicit as-of generation to pin (else pins current).
* @returns A handle for `graphCursorNext` / `graphCursorClose`.
*/
graphCursorOpen(options: GraphCursorOptions, generation?: bigint): Promise<GraphCursorHandle>
/**
* @description Pull the next chunk from an open cursor.
* @param handle - From `graphCursorOpen`.
* @param chunkSize - Target number of nodes/edges in this chunk.
* @returns The next {@link GraphCursorChunk}; `done: true` when exhausted.
*/
graphCursorNext(handle: GraphCursorHandle, chunkSize: number): Promise<GraphCursorChunk>
/**
* @description Release an open cursor's pinned generation and server-side state.
* @param handle - From `graphCursorOpen`.
*/
graphCursorClose(handle: GraphCursorHandle): Promise<void>
/**
* @description PageRank over the visible graph (hidden tiers excluded by default,
* so importance reflects the public view).
* @param options - Damping, iterations, tolerance, visibility, optional top-K.
* @param generation - Optional as-of generation.
* @returns Per-node scores.
*/
pageRank(options: PageRankOptions, generation?: bigint): Promise<GraphScores>
/**
* @description Connected components over the visible graph.
* @param options - Weak/strong, visibility.
* @param generation - Optional as-of generation.
* @returns Per-node component labels + component count.
*/
connectedComponents(options: ComponentsOptions, generation?: bigint): Promise<GraphComponents>
/**
* @description Shortest path between two nodes (weighted by edge weight or hop count).
* @param fromInt - Start node int.
* @param toInt - End node int.
* @param options - Direction, weighting, filters, max depth.
* @param generation - Optional as-of generation.
* @returns The path, or `null` if `toInt` is unreachable from `fromInt`.
*/
shortestPath(fromInt: bigint, toInt: bigint, options: ShortestPathOptions, generation?: bigint): Promise<GraphPath | null>
/**
* @description Bounded, randomly-sampled neighborhood expansion (capped fan-out per
* node) a representative subgraph for dense-graph viz. Seeded for reproducibility.
* @param seeds - Start nodes (ints or an {@link OpaqueIdSet}).
* @param options - Depth, fan-out, RNG seed, visibility, node cap.
* @param generation - Optional as-of generation.
* @returns The sampled {@link Subgraph}.
*/
neighborhoodSample(seeds: bigint[] | OpaqueIdSet, options: NeighborhoodSampleOptions, generation?: bigint): Promise<Subgraph>
/**
* @description The top-K nodes by degree over the visible graph.
* @param options - Direction, K, visibility.
* @param generation - Optional as-of generation.
* @returns Per-node degree scores (descending).
*/
topByDegree(options: TopByDegreeOptions, generation?: bigint): Promise<GraphScores>
}
/**
* Optional capability interface for index providers that maintain versioned
* (generation-aware) internal state the provider-side half of Brainy 8.0's
@ -411,7 +703,20 @@ export interface VectorIndexProvider {
queryVector: Vector,
k?: number,
filter?: (id: string) => Promise<boolean>,
options?: { rerank?: { multiplier: number }; candidateIds?: string[] }
options?: {
rerank?: { multiplier: number }
candidateIds?: string[]
/**
* Predicate-pushdown universe restrict the result to this id-set, applied
* INSIDE the beam walk (walk traverses all nodes, collects only allowed), which
* recovers the filtered recall that post-filtering loses. A native provider with
* cor as the metadata index gets the `find()` universe as an {@link OpaqueIdSet}
* (zero id materialization); the pure-JS path gets a `ReadonlySet<string>`. Absent
* = no restriction. Optional a provider that doesn't pushdown ignores it and
* falls back to `filter`.
*/
allowedIds?: OpaqueIdSet | ReadonlySet<string>
}
): Promise<Array<[string, number]>>
size(): number
clear(): void
@ -437,6 +742,20 @@ export interface EntityIdMapperProvider {
clear(): Promise<void>
getAllIntIds(): number[]
intsIterableToUuids(ints: Iterable<number>): string[]
/**
* @description Batch reverse-resolve u64 entity ints UUID strings the
* bigint counterpart of {@link EntityIdMapperProvider.intsIterableToUuids},
* for the native graph engine ({@link GraphAccelerationProvider}, whose
* `Subgraph.nodes` is a `BigInt64Array`). Brainy resolves lazily only the
* rows a caller renders but viz/export render many at once, so one batch
* crossing beats N. Order-preserving (one entry per input). A native mapper
* resolves every int from its binary intuuid store; the JS fallback resolves
* assigned ints and yields `''` for a never-assigned int (which does not occur
* for graph-engine results, since those only reference assigned ints).
* @param nodeInts - Entity ints as returned in a {@link Subgraph}.
* @returns One UUID per input, in order.
*/
entityIntsToUuids(nodeInts: BigInt64Array): string[]
readonly size: number
}

View file

@ -291,6 +291,27 @@ export class EntityIdMapper implements EntityIdMapperProvider {
return result
}
/**
* @description Batch reverse-resolve u64 entity ints (a `BigInt64Array`) UUID
* strings the bigint counterpart of {@link EntityIdMapper.intsIterableToUuids},
* for the native graph engine whose `Subgraph.nodes` is a `BigInt64Array`. The JS
* mapper is keyed by `number` (u32-era); each int is narrowed via `Number()` safe
* for the JS fallback's id range. Order-preserving (one entry per input): an int the
* mapper has not assigned yields `''` (does not occur for graph-engine results, which
* reference assigned ints only).
* @param nodeInts - Entity ints as returned in a graph `Subgraph`.
* @returns One UUID per input, in order (`''` for a never-assigned int).
* @example
* const uuids = mapper.entityIntsToUuids(subgraph.nodes)
*/
entityIntsToUuids(nodeInts: BigInt64Array): string[] {
const result: string[] = new Array(nodeInts.length)
for (let i = 0; i < nodeInts.length; i++) {
result[i] = this.intToUuid.get(Number(nodeInts[i])) ?? ''
}
return result
}
/**
* Flush mappings to storage
*/