feat(8.0): graph analytics — brain.graph.rank / communities / path

Adds three intent-level graph reads to the `brain.graph` namespace, each
native-dispatched to the optional `@soulcraft/cor` 3.0 graph engine when present
and served from pure-TS kernels otherwise (identical public shapes, default
visibility filter respected on both paths):

- `rank(opts?)` → `{ id, score }[]` descending — importance / centrality.
  TS fallback: PageRank power-iteration with dangling-mass redistribution.
- `communities(opts?)` → `{ groups, count }` — connected grouping. TS fallback:
  union-find weakly-connected components, or iterative Tarjan SCC when
  `{ directed: true }`.
- `path(from, to, opts?)` → `{ nodes, relationships, cost } | null` — best route.
  TS fallback: BFS for fewest hops, Dijkstra (min-heap) for least summed edge
  weight (`by: 'weight'`); on-demand frontier expansion so short paths terminate
  early. `direction` / `type` / `maxDepth` filters apply.

These are intent contracts, not algorithm contracts — the question is the
promise, the algorithm is the engine's choice.

Pure kernels live in src/graph/analyticsFallback.ts (PageRank, connected
components, Tarjan SCC, MinHeap) — unit-tested in isolation. The full surface is
tested end-to-end through the TS fallback, and the native dispatch + int↔uuid
hydration paths are covered by a mock provider in graph-native-routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
David Snelling 2026-06-23 12:32:03 -07:00
parent 4d0b64f455
commit 632d90aac5
7 changed files with 1164 additions and 11 deletions

View file

@ -837,14 +837,87 @@ export interface GraphExportOptions {
includeEdges?: boolean
}
/**
* Options for {@link GraphApi.communities}.
*/
export interface GraphCommunitiesOptions {
/** Treat edges as directed when grouping (default `false` — direction-agnostic). */
directed?: boolean
/** Also group through `internal`-visibility nodes/edges (hidden by default). */
includeInternal?: boolean
/** Also group through `system`-visibility nodes/edges (hidden by default). */
includeSystem?: boolean
}
/**
* Result of {@link GraphApi.communities} the graph partitioned into connected
* groups of entity ids.
*/
export interface GraphCommunitiesResult {
/** Each group is the member entity ids; largest group first. */
groups: string[][]
/** Number of distinct groups (`= groups.length`). */
count: number
}
/**
* Options for {@link GraphApi.rank} (importance ranking). INTENT-level only the
* ranking ALGORITHM is the provider's choice (the TS fallback uses PageRank), so
* no algorithm-tuning knobs are exposed.
*/
export interface GraphRankOptions {
/** Return only the top-K nodes by score (default: all, descending). */
topK?: number
/** Also rank through `internal`-visibility nodes/edges (hidden by default). */
includeInternal?: boolean
/** Also rank through `system`-visibility nodes/edges (hidden by default). */
includeSystem?: boolean
}
/** One ranked entity from {@link GraphApi.rank}, descending by `score`. */
export interface GraphRankEntry {
/** The entity id. */
id: string
/** The importance score (relative; higher = more central). */
score: number
}
/**
* Options for {@link GraphApi.path} (best route between two entities).
*/
export interface GraphPathOptions {
/** Edge direction to follow (default `'both'`). */
direction?: 'in' | 'out' | 'both'
/** Optimize for fewest `'hops'` (default) or least summed edge `'weight'`. */
by?: 'hops' | 'weight'
/** Restrict the route to these relationship type(s). */
type?: VerbType | VerbType[]
/** Also route through `internal`-visibility nodes/edges (hidden by default). */
includeInternal?: boolean
/** Also route through `system`-visibility nodes/edges (hidden by default). */
includeSystem?: boolean
/** Abandon the search past this many hops. */
maxDepth?: number
}
/**
* Result of {@link GraphApi.path} the route from `from` to `to`, or `null` when
* unreachable.
*/
export interface GraphPathResult {
/** The entity ids along the route, `from` … `to` inclusive. */
nodes: string[]
/** The relationship (verb) ids traversed, one fewer than `nodes`. */
relationships: string[]
/** Route cost: number of hops, or summed edge weight when `by: 'weight'`. */
cost: number
}
/**
* The `brain.graph` namespace graph-shaped reads over the knowledge graph.
* Routes to a native {@link import('../plugin.js').GraphAccelerationProvider}
* when one is registered, otherwise serves the same results from Brainy's
* pure-TS adjacency (correct at small/medium scale).
*
* (Grows with the engine: `subgraph` + `export` ship first; `rank` / `path` /
* `communities` follow.)
*/
export interface GraphApi<T = any> {
/**
@ -874,6 +947,42 @@ export interface GraphApi<T = any> {
* }
*/
export(options?: GraphExportOptions): AsyncIterable<GraphView<T>>
/**
* @description Rank entities by graph importance (influence / centrality) the
* one-call answer to "which nodes matter most". The TS fallback uses PageRank;
* a native provider may use personalized PageRank / eigenvector centrality.
* @param options - `topK`, visibility opt-ins.
* @returns Entities with scores, DESCENDING (most important first).
* @example
* const top = await brain.graph.rank({ topK: 10 })
* top[0] // { id, score } — the most central entity
*/
rank(options?: GraphRankOptions): Promise<GraphRankEntry[]>
/**
* @description Partition the graph into connected communities (clusters of
* related entities) "which things group together".
* @param options - `directed`, visibility opt-ins.
* @returns The groups (member ids) + their count.
* @example
* const { groups, count } = await brain.graph.communities()
*/
communities(options?: GraphCommunitiesOptions): Promise<GraphCommunitiesResult>
/**
* @description Find the best route between two entities fewest hops (default)
* or least summed edge weight (`by: 'weight'`). The pathfinding ALGORITHM is
* the provider's choice (TS fallback: BFS for hops, Dijkstra for weight).
* @param from - Start entity id (natural keys are resolved).
* @param to - End entity id.
* @param options - Direction, `by`, type filter, `maxDepth`, visibility opt-ins.
* @returns The route (`nodes` + `relationships` + `cost`), or `null` if unreachable.
* @example
* const route = await brain.graph.path(aliceId, bobId)
* route?.nodes // [aliceId, …, bobId]
*/
path(from: string, to: string, options?: GraphPathOptions): Promise<GraphPathResult | null>
}
// ============= Batch Operations =============