feat(8.0): brain.graph.subgraph() + native-provider routing
Introduces the brain.graph namespace and its first op, subgraph() — bounded
multi-hop neighborhood extraction returning a hydrated GraphView (nodes with hop
depth + type/subtype, edges as Relations, a truncated flag). The one-call answer
to 'show me everything around this node, N hops out' the graph-viz path needs.
- brain.graph.subgraph(seeds, { depth, direction, type, subtype, includeInternal,
includeSystem, maxNodes, maxEdges, hydrateNodes }). seeds: id or id[].
- Feature-detect routing: when a GraphAccelerationProvider is registered
(isGraphAccelerationProvider on the 'graphAcceleration' provider) it routes to
native traverse() and hydrates the columnar Subgraph (node ints↔ids paired with
depth position-preserving, edge verb ints → Relations); otherwise a pure-TS BFS
fallback expands each frontier through the O(degree) related() adjacency.
- Both paths produce the identical GraphView, so CI exercises the shape via the
fallback; the native path is cross-layer-tested against cor's provider.
- New public types: GraphView, GraphNode, SubgraphOptions, GraphApi (the surface
grows: export/rank/path/communities follow). isGraphAccelerationProvider guard
added to plugin.ts.
Test: graph-subgraph.test.ts — depth tracking, direction, type filter, default-
hides-internal, node hydration on/off, maxNodes truncation, multi-seed.
2026-06-21 09:23:33 -07:00
/ * *
* @module tests / unit / brainy / graph - subgraph
* @description Graph engine # 25 : ` brain.graph.subgraph() ` — bounded multi - hop
* neighborhood extraction . These exercise the pure - TS fallback ( no native
* GraphAccelerationProvider is registered in CI ) ; the native path returns the
* same ` GraphView ` shape , cross - layer - tested against the provider .
*
* Graph under test ( RelatedTo unless noted ) :
* e → a → b → c , a → ( ReportsTo ) d , a → ( internal ) f
* /
import { describe , it , expect , beforeEach , afterEach } from 'vitest'
import { Brainy } from '../../../src/index.js'
import { NounType , VerbType } from '../../../src/types/graphTypes.js'
import { createTestConfig } from '../../helpers/test-factory.js'
describe ( 'brain.graph.subgraph() (graph engine #25)' , ( ) = > {
let brain : Brainy
let a : string , b : string , c : string , d : string , e : string , f : string
beforeEach ( async ( ) = > {
brain = new Brainy ( createTestConfig ( ) )
await brain . init ( )
a = await brain . add ( { type : NounType . Person , subtype : 'employee' , data : 'A' } )
b = await brain . add ( { type : NounType . Person , subtype : 'employee' , data : 'B' } )
c = await brain . add ( { type : NounType . Person , subtype : 'employee' , data : 'C' } )
d = await brain . add ( { type : NounType . Project , subtype : 'milestone' , data : 'D' } )
e = await brain . add ( { type : NounType . Person , subtype : 'employee' , data : 'E' } )
f = await brain . add ( { type : NounType . Person , subtype : 'employee' , data : 'F' } )
await brain . relate ( { from : a , to : b , type : VerbType . RelatedTo , subtype : 'colleague' } )
await brain . relate ( { from : b , to : c , type : VerbType . RelatedTo , subtype : 'colleague' } )
await brain . relate ( { from : a , to : d , type : VerbType . ReportsTo , subtype : 'direct' } )
await brain . relate ( { from : e , to : a , type : VerbType . RelatedTo , subtype : 'colleague' } )
await brain . relate ( { from : a , to : f , type : VerbType . RelatedTo , subtype : 'colleague' , visibility : 'internal' } )
} )
afterEach ( async ( ) = > {
await brain . close ( )
} )
it ( 'depth-1 returns the seed + immediate neighbors (both directions) with depths' , async ( ) = > {
const view = await brain . graph . subgraph ( a , { depth : 1 } )
const byId = new Map ( view . nodes . map ( ( n ) = > [ n . id , n ] ) )
// a is the seed (depth 0); b, d (out) and e (in) are 1 hop. f is internal → hidden.
expect ( new Set ( byId . keys ( ) ) ) . toEqual ( new Set ( [ a , b , d , e ] ) )
expect ( byId . get ( a ) ? . depth ) . toBe ( 0 )
expect ( byId . get ( b ) ? . depth ) . toBe ( 1 )
expect ( byId . get ( e ) ? . depth ) . toBe ( 1 )
// Edges among them.
const pairs = view . edges . map ( ( r ) = > ` ${ r . from } -> ${ r . to } ` ) . sort ( )
expect ( pairs ) . toEqual ( [ ` ${ a } -> ${ b } ` , ` ${ a } -> ${ d } ` , ` ${ e } -> ${ a } ` ] . sort ( ) )
} )
it ( 'depth-2 expands one more hop (c via b), tracking depth' , async ( ) = > {
const view = await brain . graph . subgraph ( a , { depth : 2 } )
const byId = new Map ( view . nodes . map ( ( n ) = > [ n . id , n ] ) )
expect ( byId . has ( c ) ) . toBe ( true )
expect ( byId . get ( c ) ? . depth ) . toBe ( 2 )
expect ( view . edges . some ( ( r ) = > r . from === b && r . to === c ) ) . toBe ( true )
} )
it ( "direction:'out' follows only outgoing edges" , async ( ) = > {
const view = await brain . graph . subgraph ( a , { depth : 1 , direction : 'out' } )
const ids = new Set ( view . nodes . map ( ( n ) = > n . id ) )
expect ( ids ) . toEqual ( new Set ( [ a , b , d ] ) ) // e (e→a, incoming) excluded
} )
it ( 'composes with a verb-type filter' , async ( ) = > {
const view = await brain . graph . subgraph ( a , { depth : 1 , type : VerbType . ReportsTo } )
expect ( view . edges . map ( ( r ) = > ` ${ r . from } -> ${ r . to } ` ) ) . toEqual ( [ ` ${ a } -> ${ d } ` ] )
expect ( new Set ( view . nodes . map ( ( n ) = > n . id ) ) ) . toEqual ( new Set ( [ a , d ] ) )
} )
it ( 'includeInternal surfaces the hidden edge/node' , async ( ) = > {
const view = await brain . graph . subgraph ( a , { depth : 1 , includeInternal : true } )
expect ( view . nodes . some ( ( n ) = > n . id === f ) ) . toBe ( true )
expect ( view . edges . some ( ( r ) = > r . to === f ) ) . toBe ( true )
} )
it ( 'hydrates node type/subtype by default; skips it with hydrateNodes:false' , async ( ) = > {
const hydrated = await brain . graph . subgraph ( a , { depth : 1 } )
const dNode = hydrated . nodes . find ( ( n ) = > n . id === d )
expect ( dNode ? . type ) . toBe ( NounType . Project )
expect ( dNode ? . subtype ) . toBe ( 'milestone' )
const bare = await brain . graph . subgraph ( a , { depth : 1 , hydrateNodes : false } )
expect ( bare . nodes . every ( ( n ) = > n . type === undefined ) ) . toBe ( true )
} )
it ( 'caps the result and flags truncated when maxNodes is hit' , async ( ) = > {
const view = await brain . graph . subgraph ( a , { depth : 2 , maxNodes : 2 } )
expect ( view . truncated ) . toBe ( true )
expect ( view . nodes . length ) . toBeLessThanOrEqual ( 2 )
} )
it ( 'accepts multiple seeds' , async ( ) = > {
const view = await brain . graph . subgraph ( [ a , c ] , { depth : 0 } )
// depth 0 = just the seeds, no expansion.
expect ( new Set ( view . nodes . map ( ( n ) = > n . id ) ) ) . toEqual ( new Set ( [ a , c ] ) )
} )
2026-06-23 13:30:30 -07:00
// ---- Query→expand fusion (graph #61): seed from a query / find() result ----
it ( 'query→expand: subgraph(query) seeds from the matches and expands their neighborhood' , async ( ) = > {
// The only Project is d; 1-hop expansion brings in a (a → d ReportsTo).
const view = await brain . graph . subgraph ( { type : NounType . Project } , { depth : 1 } )
const ids = new Set ( view . nodes . map ( ( n ) = > n . id ) )
expect ( ids . has ( d ) ) . toBe ( true ) // the query match (seed)
expect ( ids . has ( a ) ) . toBe ( true ) // expanded neighbor
expect ( view . edges . some ( ( r ) = > r . from === a && r . to === d ) ) . toBe ( true )
} )
it ( 'query→expand: depth 0 returns just the matches, no expansion' , async ( ) = > {
const view = await brain . graph . subgraph ( { type : NounType . Project } , { depth : 0 } )
expect ( new Set ( view . nodes . map ( ( n ) = > n . id ) ) ) . toEqual ( new Set ( [ d ] ) )
} )
it ( 'accepts a find() result array as the seed set' , async ( ) = > {
const matches = await brain . find ( { type : NounType . Project } ) // → [d]
expect ( matches . length ) . toBeGreaterThan ( 0 )
const view = await brain . graph . subgraph ( matches , { depth : 1 } )
const ids = new Set ( view . nodes . map ( ( n ) = > n . id ) )
expect ( ids . has ( d ) ) . toBe ( true )
expect ( ids . has ( a ) ) . toBe ( true )
} )
it ( 'query→expand returns empty when the query matches nothing' , async ( ) = > {
const view = await brain . graph . subgraph ( { type : NounType . Event } , { depth : 1 } )
expect ( view . nodes ) . toEqual ( [ ] )
expect ( view . edges ) . toEqual ( [ ] )
} )
feat(8.0): brain.graph.subgraph() + native-provider routing
Introduces the brain.graph namespace and its first op, subgraph() — bounded
multi-hop neighborhood extraction returning a hydrated GraphView (nodes with hop
depth + type/subtype, edges as Relations, a truncated flag). The one-call answer
to 'show me everything around this node, N hops out' the graph-viz path needs.
- brain.graph.subgraph(seeds, { depth, direction, type, subtype, includeInternal,
includeSystem, maxNodes, maxEdges, hydrateNodes }). seeds: id or id[].
- Feature-detect routing: when a GraphAccelerationProvider is registered
(isGraphAccelerationProvider on the 'graphAcceleration' provider) it routes to
native traverse() and hydrates the columnar Subgraph (node ints↔ids paired with
depth position-preserving, edge verb ints → Relations); otherwise a pure-TS BFS
fallback expands each frontier through the O(degree) related() adjacency.
- Both paths produce the identical GraphView, so CI exercises the shape via the
fallback; the native path is cross-layer-tested against cor's provider.
- New public types: GraphView, GraphNode, SubgraphOptions, GraphApi (the surface
grows: export/rank/path/communities follow). isGraphAccelerationProvider guard
added to plugin.ts.
Test: graph-subgraph.test.ts — depth tracking, direction, type filter, default-
hides-internal, node hydration on/off, maxNodes truncation, multi-seed.
2026-06-21 09:23:33 -07:00
} )