From c186c2afb4eb7772855f41a03102892ecb97c9b1 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Tue, 24 Feb 2026 10:44:51 -0500 Subject: [PATCH] Add JSDoc with usage examples to all exported functions Co-Authored-By: Claude Opus 4.6 --- .changeset/add-jsdoc-examples.md | 5 + src/algorithms.ts | 486 ++++++++++++++++++++++++++- src/diff.ts | 97 +++++- src/formats/adjacency-list/index.ts | 33 ++ src/formats/converter/index.ts | 38 ++- src/formats/cytoscape/index.ts | 50 ++- src/formats/d3/index.ts | 48 ++- src/formats/dot/index.ts | 55 ++- src/formats/edge-list/index.ts | 32 ++ src/formats/gml/index.ts | 57 +++- src/formats/jgf/index.ts | 51 ++- src/formats/mermaid/block.ts | 30 +- src/formats/mermaid/class-diagram.ts | 31 +- src/formats/mermaid/er-diagram.ts | 28 +- src/formats/mermaid/flowchart.ts | 28 +- src/formats/mermaid/mindmap.ts | 31 +- src/formats/mermaid/sequence.ts | 30 +- src/formats/mermaid/state.ts | 29 +- src/formats/tgf/index.ts | 46 ++- src/graph.ts | 202 ++++++++++- src/indexing.ts | 33 +- src/queries.ts | 358 +++++++++++++++++++- src/transforms.ts | 19 ++ 23 files changed, 1772 insertions(+), 45 deletions(-) create mode 100644 .changeset/add-jsdoc-examples.md diff --git a/.changeset/add-jsdoc-examples.md b/.changeset/add-jsdoc-examples.md new file mode 100644 index 0000000..9d1041c --- /dev/null +++ b/.changeset/add-jsdoc-examples.md @@ -0,0 +1,5 @@ +--- +"@statelyai/graph": patch +--- + +Add JSDoc with usage examples to all exported functions diff --git a/src/algorithms.ts b/src/algorithms.ts index ce8f847..d2ac959 100644 --- a/src/algorithms.ts +++ b/src/algorithms.ts @@ -15,6 +15,23 @@ import { createGraph } from './graph'; // --- Traversal generators --- +/** + * Breadth-first traversal generator yielding nodes level by level. + * + * @example + * ```ts + * import { createGraph, bfs } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }, { id: 'bc', sourceId: 'b', targetId: 'c' }], + * }); + * + * for (const node of bfs(graph, 'a')) { + * console.log(node.id); // 'a', 'b', 'c' + * } + * ``` + */ export function* bfs( graph: Graph, startId: string, @@ -39,6 +56,23 @@ export function* bfs( } } +/** + * Depth-first traversal generator yielding nodes as visited. + * + * @example + * ```ts + * import { createGraph, dfs } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }, { id: 'bc', sourceId: 'b', targetId: 'c' }], + * }); + * + * for (const node of dfs(graph, 'a')) { + * console.log(node.id); // 'a', 'b', 'c' + * } + * ``` + */ export function* dfs( graph: Graph, startId: string, @@ -88,6 +122,21 @@ function getSuccessorIds(graph: Graph, nodeId: string): string[] { // --- Graph properties --- +/** + * Checks whether the graph contains no cycles. + * + * @example + * ```ts + * import { createGraph, isAcyclic } from '@statelyai/graph'; + * + * const dag = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }], + * }); + * + * isAcyclic(dag); // true + * ``` + */ export function isAcyclic(graph: Graph): boolean { if (graph.type === 'undirected') { return isAcyclicUndirected(graph); @@ -152,6 +201,23 @@ function isAcyclicUndirected(graph: Graph): boolean { return true; } +/** + * Returns connected components as arrays of nodes. + * Treats all edges as undirected for connectivity. + * + * @example + * ```ts + * import { createGraph, getConnectedComponents } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }], + * }); + * + * const components = getConnectedComponents(graph); + * // [[nodeA, nodeB], [nodeC]] + * ``` + */ export function getConnectedComponents(graph: Graph): GraphNode[][] { const idx = getIndex(graph); const visited = new Set(); @@ -195,6 +261,25 @@ export function getConnectedComponents(graph: Graph): GraphNode[][] { return components; } +/** + * Returns a topological ordering of nodes, or `null` if the graph is cyclic. + * + * @example + * ```ts + * import { createGraph, getTopologicalSort } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * ], + * }); + * + * const sorted = getTopologicalSort(graph); + * // [nodeA, nodeB, nodeC] + * ``` + */ export function getTopologicalSort(graph: Graph): GraphNode[] | null { const idx = getIndex(graph); const inDeg = new Map(); @@ -228,6 +313,22 @@ export function getTopologicalSort(graph: Graph): GraphNode[] | null { return result; } +/** + * Checks whether a path exists between two nodes. + * + * @example + * ```ts + * import { createGraph, hasPath } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }], + * }); + * + * hasPath(graph, 'a', 'b'); // true + * hasPath(graph, 'a', 'c'); // false + * ``` + */ export function hasPath( graph: Graph, sourceId: string, @@ -236,12 +337,45 @@ export function hasPath( return getShortestPaths(graph, { from: sourceId, to: targetId }).length > 0; } +/** + * Checks whether the graph is connected (all nodes reachable from any node). + * + * @example + * ```ts + * import { createGraph, isConnected } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'ab', sourceId: 'a', targetId: 'b' }], + * }); + * + * isConnected(graph); // true + * ``` + */ export function isConnected(graph: Graph): boolean { if (graph.nodes.length === 0) return true; const components = getConnectedComponents(graph); return components.length <= 1; } +/** + * Checks whether the graph is a tree (connected and acyclic). + * + * @example + * ```ts + * import { createGraph, isTree } from '@statelyai/graph'; + * + * const tree = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'ac', sourceId: 'a', targetId: 'c' }, + * ], + * }); + * + * isTree(tree); // true + * ``` + */ export function isTree(graph: Graph): boolean { return isConnected(graph) && isAcyclic(graph); } @@ -403,6 +537,24 @@ function* reconstructPaths( /** * Lazily yields all shortest paths from a source node. * Use `getShortestPaths` for the full array. + * + * @example + * ```ts + * import { createGraph, genShortestPaths } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * for (const path of genShortestPaths(graph)) { + * console.log(path.steps.map(s => s.node.id)); + * } + * ``` */ export function* genShortestPaths( graph: Graph, @@ -431,6 +583,27 @@ export function* genShortestPaths( } } +/** + * Returns all shortest paths from a source node as an array. + * Delegates to `genShortestPaths` internally. + * + * @example + * ```ts + * import { createGraph, getShortestPaths } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * const paths = getShortestPaths(graph); + * // paths to 'b' and 'c' from 'a' + * ``` + */ export function getShortestPaths( graph: Graph, opts?: PathOptions, @@ -439,7 +612,24 @@ export function getShortestPaths( } /** - * Returns a single shortest path from source to target, or undefined if unreachable. + * Returns a single shortest path from source to target, or `undefined` if unreachable. + * + * @example + * ```ts + * import { createGraph, getShortestPath } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * const path = getShortestPath(graph, { to: 'c' }); + * // path.steps -> [{node: nodeB, edge: ...}, {node: nodeC, edge: ...}] + * ``` */ export function getShortestPath( graph: Graph, @@ -452,8 +642,26 @@ export function getShortestPath( } /** - * Returns all simple (acyclic) paths from a source node. - * Uses DFS with backtracking. + * Returns all simple (acyclic) paths from a source node as an array. + * Delegates to `genSimplePaths` internally. + * + * @example + * ```ts + * import { createGraph, getSimplePaths } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * { id: 'ac', sourceId: 'a', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * const paths = getSimplePaths(graph, { to: 'c' }); + * // two paths: a->b->c and a->c + * ``` */ export function getSimplePaths( graph: Graph, @@ -463,8 +671,28 @@ export function getSimplePaths( } /** - * Lazily yields all simple (acyclic) paths from a source node. + * Lazily yields all simple (acyclic) paths from a source node via DFS backtracking. * Use `getSimplePaths` for the full array. + * + * @example + * ```ts + * import { createGraph, genSimplePaths } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * { id: 'ac', sourceId: 'a', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * for (const path of genSimplePaths(graph, { to: 'c' })) { + * console.log(path.steps.map(s => s.node.id)); + * // ['b', 'c'] or ['c'] + * } + * ``` */ export function* genSimplePaths( graph: Graph, @@ -519,7 +747,24 @@ export function* genSimplePaths( } /** - * Returns a single simple (acyclic) path from source to target, or undefined if unreachable. + * Returns a single simple (acyclic) path from source to target, or `undefined` if unreachable. + * + * @example + * ```ts + * import { createGraph, getSimplePath } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * const path = getSimplePath(graph, { to: 'c' }); + * // path.steps -> [{node: nodeB, edge: ...}, {node: nodeC, edge: ...}] + * ``` */ export function getSimplePath( graph: Graph, @@ -536,6 +781,27 @@ export function getSimplePath( // Strongly connected components (Tarjan's) // --------------------------------------------------------------------------- +/** + * Returns strongly connected components using Tarjan's algorithm. + * Only meaningful for directed graphs. + * + * @example + * ```ts + * import { createGraph, getStronglyConnectedComponents } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'ba', sourceId: 'b', targetId: 'a' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * ], + * }); + * + * const sccs = getStronglyConnectedComponents(graph); + * // [[nodeA, nodeB], [nodeC]] + * ``` + */ export function getStronglyConnectedComponents( graph: Graph, ): GraphNode[][] { @@ -591,13 +857,50 @@ export function getStronglyConnectedComponents( // Cycle detection — all elementary cycles // --------------------------------------------------------------------------- +/** + * Returns all elementary cycles as an array of paths. + * Delegates to `genCycles` internally. + * + * @example + * ```ts + * import { createGraph, getCycles } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'ba', sourceId: 'b', targetId: 'a' }, + * ], + * }); + * + * const cycles = getCycles(graph); + * // one cycle: a -> b -> a + * ``` + */ export function getCycles(graph: Graph): GraphPath[] { return [...genCycles(graph)]; } /** - * Lazily yields cycles one at a time. + * Lazily yields elementary cycles one at a time. * Use `getCycles` for the full array. + * + * @example + * ```ts + * import { createGraph, genCycles } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'ba', sourceId: 'b', targetId: 'a' }, + * ], + * }); + * + * for (const cycle of genCycles(graph)) { + * console.log(cycle.steps.map(s => s.node.id)); // ['b', 'a'] + * } + * ``` */ export function* genCycles( graph: Graph, @@ -749,6 +1052,23 @@ function getNeighborEdgesAll( /** * Returns a single canonical preorder (DFS visit-order) sequence. * Visits neighbors in the order they appear in the adjacency list. + * + * @example + * ```ts + * import { createGraph, getPreorder } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * const order = getPreorder(graph); + * // [nodeA, nodeB, nodeC] + * ``` */ export function getPreorder( graph: Graph, @@ -784,6 +1104,23 @@ export function getPreorder( /** * Returns a single canonical postorder (DFS finish-order) sequence. * Visits neighbors in the order they appear in the adjacency list. + * + * @example + * ```ts + * import { createGraph, getPostorder } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * const order = getPostorder(graph); + * // [nodeC, nodeB, nodeA] + * ``` */ export function getPostorder( graph: Graph, @@ -820,7 +1157,26 @@ export function getPostorder( // Traversal order enumeration — all possible DFS orderings (generators) // --------------------------------------------------------------------------- -/** Returns all possible preorder sequences as an array. Can be exponential — prefer `genPreorders`. */ +/** + * Returns all possible preorder sequences as an array. Can be exponential -- prefer `genPreorders`. + * + * @example + * ```ts + * import { createGraph, getPreorders } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'ac', sourceId: 'a', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * const allOrders = getPreorders(graph); + * // [[nodeA, nodeB, nodeC], [nodeA, nodeC, nodeB]] + * ``` + */ export function getPreorders( graph: Graph, opts?: TraversalOptions, @@ -828,7 +1184,26 @@ export function getPreorders( return [...genPreorders(graph, opts)]; } -/** Returns all possible postorder sequences as an array. Can be exponential — prefer `genPostorders`. */ +/** + * Returns all possible postorder sequences as an array. Can be exponential -- prefer `genPostorders`. + * + * @example + * ```ts + * import { createGraph, getPostorders } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'ac', sourceId: 'a', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * const allOrders = getPostorders(graph); + * // [[nodeB, nodeC, nodeA], [nodeC, nodeB, nodeA]] + * ``` + */ export function getPostorders( graph: Graph, opts?: TraversalOptions, @@ -840,6 +1215,25 @@ export function getPostorders( * Lazily yields all possible preorder (DFS visit-order) sequences. * Different neighbor exploration orders yield different sequences. * Use `getPreorder()` for a single canonical ordering. + * + * @example + * ```ts + * import { createGraph, genPreorders } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'ac', sourceId: 'a', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * for (const order of genPreorders(graph)) { + * console.log(order.map(n => n.id)); + * // ['a', 'b', 'c'] or ['a', 'c', 'b'] + * } + * ``` */ export function* genPreorders( graph: Graph, @@ -906,6 +1300,25 @@ export function* genPreorders( * Lazily yields all possible postorder (DFS finish-order) sequences. * Different neighbor exploration orders yield different sequences. * Use `getPostorder()` for a single canonical ordering. + * + * @example + * ```ts + * import { createGraph, genPostorders } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'ac', sourceId: 'a', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * for (const order of genPostorders(graph)) { + * console.log(order.map(n => n.id)); + * // ['b', 'c', 'a'] or ['c', 'b', 'a'] + * } + * ``` */ export function* genPostorders( graph: Graph, @@ -974,6 +1387,26 @@ export function* genPostorders( * Returns a minimum spanning tree of the graph. * Only meaningful for connected undirected graphs (or the component reachable * from an arbitrary start node in directed graphs). + * + * @example + * ```ts + * import { createGraph, getMinimumSpanningTree } from '@statelyai/graph'; + * + * const graph = createGraph({ + * type: 'undirected', + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b', data: { weight: 1 } }, + * { id: 'bc', sourceId: 'b', targetId: 'c', data: { weight: 2 } }, + * { id: 'ac', sourceId: 'a', targetId: 'c', data: { weight: 3 } }, + * ], + * }); + * + * const mst = getMinimumSpanningTree(graph, { + * getWeight: (e) => e.data.weight, + * }); + * // mst has edges 'ab' and 'bc' (total weight 3) + * ``` */ export function getMinimumSpanningTree( graph: Graph, @@ -1122,7 +1555,23 @@ function kruskalMST( /** * Returns shortest paths between all pairs of nodes. * Algorithm 'dijkstra' (default): runs getShortestPaths per source node. - * Algorithm 'floyd-warshall': classic O(V³) dynamic programming. + * Algorithm 'floyd-warshall': classic O(V^3) dynamic programming. + * + * @example + * ```ts + * import { createGraph, getAllPairsShortestPaths } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * ], + * }); + * + * const allPaths = getAllPairsShortestPaths(graph); + * // paths for every reachable (source, target) pair + * ``` */ export function getAllPairsShortestPaths( graph: Graph, @@ -1293,6 +1742,25 @@ function fwReconstruct( * * Steps are concatenated: head.steps ++ tail.steps (tail already starts * from the overlap node, so no slicing is needed). + * + * @example + * ```ts + * import { createGraph, getShortestPath, joinPaths } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'ab', sourceId: 'a', targetId: 'b' }, + * { id: 'bc', sourceId: 'b', targetId: 'c' }, + * ], + * initialNodeId: 'a', + * }); + * + * const ab = getShortestPath(graph, { to: 'b' })!; + * const bc = getShortestPath(graph, { from: 'b', to: 'c' })!; + * const ac = joinPaths(ab, bc); + * // ac: a -> b -> c + * ``` */ export function joinPaths( headPath: GraphPath, diff --git a/src/diff.ts b/src/diff.ts index 5c72a63..6b24ded 100644 --- a/src/diff.ts +++ b/src/diff.ts @@ -94,7 +94,21 @@ const EDGE_COMPARE_KEYS = [ // Diff functions // --------------------------------------------------------------------------- -/** Compute a structured diff from graph `a` to graph `b` by matching IDs. */ +/** + * Compute a structured diff from graph `a` to graph `b` by matching IDs. + * + * @example + * ```ts + * import { createGraph, getDiff } from '@statelyai/graph'; + * + * const a = createGraph({ nodes: [{ id: 'n1' }], edges: [] }); + * const b = createGraph({ nodes: [{ id: 'n1', label: 'hello' }, { id: 'n2' }], edges: [] }); + * + * const diff = getDiff(a, b); + * // diff.nodes.added → [{ id: 'n2' }] + * // diff.nodes.updated → [{ id: 'n1', old: { label: '' }, new: { label: 'hello' } }] + * ``` + */ export function getDiff(a: Graph, b: Graph): GraphDiff { const aNodeMap = new Map(a.nodes.map((n) => [n.id, n])); const bNodeMap = new Map(b.nodes.map((n) => [n.id, n])); @@ -159,7 +173,18 @@ export function getDiff(a: Graph, b: Graph): GraphDiff { return diff; } -/** Check if a diff has zero changes. */ +/** + * Check if a diff has zero changes. + * + * @example + * ```ts + * import { createGraph, getDiff, isEmptyDiff } from '@statelyai/graph'; + * + * const g = createGraph({ nodes: [{ id: 'n1' }], edges: [] }); + * const diff = getDiff(g, g); + * isEmptyDiff(diff); // true + * ``` + */ export function isEmptyDiff(diff: GraphDiff): boolean { return ( diff.nodes.added.length === 0 && @@ -171,7 +196,22 @@ export function isEmptyDiff(diff: GraphDiff): boolean { ); } -/** Invert a diff: swap added↔removed, swap old↔new in updates. */ +/** + * Invert a diff: swap added/removed, swap old/new in updates. + * + * @example + * ```ts + * import { createGraph, getDiff, invertDiff } from '@statelyai/graph'; + * + * const a = createGraph({ nodes: [{ id: 'n1' }], edges: [] }); + * const b = createGraph({ nodes: [{ id: 'n2' }], edges: [] }); + * + * const diff = getDiff(a, b); + * const inv = invertDiff(diff); + * // inv.nodes.added contains n1 (was removed) + * // inv.nodes.removed contains n2 (was added) + * ``` + */ export function invertDiff(diff: GraphDiff): GraphDiff { return { nodes: { @@ -202,6 +242,17 @@ export function invertDiff(diff: GraphDiff): GraphDiff { /** * Compute an ordered patch list from graph `a` to graph `b`. * Order: delete edges → delete nodes → add nodes → add edges → update nodes → update edges. + * + * @example + * ```ts + * import { createGraph, getPatches } from '@statelyai/graph'; + * + * const a = createGraph({ nodes: [{ id: 'n1' }], edges: [] }); + * const b = createGraph({ nodes: [{ id: 'n1' }, { id: 'n2' }], edges: [] }); + * + * const patches = getPatches(a, b); + * // patches → [{ op: 'addNode', node: { id: 'n2' } }] + * ``` */ export function getPatches( a: Graph, @@ -214,6 +265,18 @@ export function getPatches( /** * **Mutable.** Apply patches to a graph in order. * Delegates to addNode/deleteNode/updateNode/addEdge/deleteEdge/updateEdge. + * + * @example + * ```ts + * import { createGraph, getPatches, applyPatches } from '@statelyai/graph'; + * + * const a = createGraph({ nodes: [{ id: 'n1' }], edges: [] }); + * const b = createGraph({ nodes: [{ id: 'n1' }, { id: 'n2' }], edges: [] }); + * + * const patches = getPatches(a, b); + * applyPatches(a, patches); + * // a now contains both n1 and n2 + * ``` */ export function applyPatches( graph: Graph, @@ -253,6 +316,18 @@ export function applyPatches( * Order: add nodes → update edges → delete edges → delete nodes → add edges → update nodes. * This avoids cascading deletes removing edges that are being updated, * and ensures new nodes exist before edges reference them. + * + * @example + * ```ts + * import { createGraph, getDiff, toPatches } from '@statelyai/graph'; + * + * const a = createGraph({ nodes: [{ id: 'n1' }], edges: [] }); + * const b = createGraph({ nodes: [{ id: 'n2' }], edges: [] }); + * + * const diff = getDiff(a, b); + * const patches = toPatches(diff); + * // patches → [{ op: 'addNode', ... }, { op: 'deleteNode', ... }] + * ``` */ export function toPatches(diff: GraphDiff): GraphPatch[] { const patches: GraphPatch[] = []; @@ -298,7 +373,21 @@ export function toPatches(diff: GraphDiff): GraphPatch[] { return patches; } -/** Group a patch list into a structured diff. */ +/** + * Group a patch list into a structured diff. + * + * @example + * ```ts + * import { createGraph, getPatches, toDiff } from '@statelyai/graph'; + * + * const a = createGraph({ nodes: [{ id: 'n1' }], edges: [] }); + * const b = createGraph({ nodes: [{ id: 'n1' }, { id: 'n2' }], edges: [] }); + * + * const patches = getPatches(a, b); + * const diff = toDiff(patches); + * // diff.nodes.added → [{ id: 'n2' }] + * ``` + */ export function toDiff(patches: GraphPatch[]): GraphDiff { const diff: GraphDiff = { nodes: { added: [], removed: [], updated: [] }, diff --git a/src/formats/adjacency-list/index.ts b/src/formats/adjacency-list/index.ts index a790941..5be7812 100644 --- a/src/formats/adjacency-list/index.ts +++ b/src/formats/adjacency-list/index.ts @@ -1,5 +1,21 @@ import type { Graph } from '../../types'; +/** + * Converts a graph to an adjacency list representation. + * + * @example + * ```ts + * import { createGraph, toAdjacencyList } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: { a: {}, b: {}, c: {} }, + * edges: [{ source: 'a', target: 'b' }, { source: 'a', target: 'c' }], + * }); + * + * toAdjacencyList(graph); + * // { a: ['b', 'c'], b: [], c: [] } + * ``` + */ export function toAdjacencyList(graph: Graph): Record { const adj: Record = {}; @@ -17,6 +33,23 @@ export function toAdjacencyList(graph: Graph): Record { return adj; } +/** + * Parses an adjacency list into a graph. + * + * @example + * ```ts + * import { fromAdjacencyList } from '@statelyai/graph'; + * + * const graph = fromAdjacencyList({ + * a: ['b', 'c'], + * b: ['c'], + * c: [], + * }); + * + * graph.nodes; // [{id: 'a', ...}, {id: 'b', ...}, {id: 'c', ...}] + * graph.edges; // [{sourceId: 'a', targetId: 'b', ...}, ...] + * ``` + */ export function fromAdjacencyList( adj: Record, options?: { directed?: boolean; id?: string }, diff --git a/src/formats/converter/index.ts b/src/formats/converter/index.ts index 09e784c..73f4686 100644 --- a/src/formats/converter/index.ts +++ b/src/formats/converter/index.ts @@ -25,11 +25,45 @@ export function createFormatConverter( return { to, from }; } -/** Bidirectional converter for adjacency-list format (`Record`). */ +/** + * Bidirectional converter for adjacency-list format (`Record`). + * + * @example + * ```ts + * import { adjacencyListConverter, createGraph } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: { a: {}, b: {} }, + * edges: [{ source: 'a', target: 'b' }], + * }); + * + * const adj = adjacencyListConverter.to(graph); + * // { a: ['b'], b: [] } + * + * const roundTripped = adjacencyListConverter.from(adj); + * ``` + */ export const adjacencyListConverter: GraphFormatConverter< Record > = createFormatConverter(toAdjacencyList, fromAdjacencyList); -/** Bidirectional converter for edge-list format (`[source, target][]`). */ +/** + * Bidirectional converter for edge-list format (`[source, target][]`). + * + * @example + * ```ts + * import { edgeListConverter, createGraph } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: { a: {}, b: {} }, + * edges: [{ source: 'a', target: 'b' }], + * }); + * + * const edges = edgeListConverter.to(graph); + * // [['a', 'b']] + * + * const roundTripped = edgeListConverter.from(edges); + * ``` + */ export const edgeListConverter: GraphFormatConverter<[string, string][]> = createFormatConverter(toEdgeList, fromEdgeList); diff --git a/src/formats/cytoscape/index.ts b/src/formats/cytoscape/index.ts index 2e0562c..7005cdf 100644 --- a/src/formats/cytoscape/index.ts +++ b/src/formats/cytoscape/index.ts @@ -22,6 +22,23 @@ export interface CytoscapeJSON { // --- Conversion --- +/** + * Converts a graph to Cytoscape.js JSON format. + * + * @example + * ```ts + * import { createGraph } from '@statelyai/graph'; + * import { toCytoscapeJSON } from '@statelyai/graph/formats/cytoscape'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e0', sourceId: 'a', targetId: 'b' }], + * }); + * + * const cyto = toCytoscapeJSON(graph); + * // { elements: { nodes: [...], edges: [...] } } + * ``` + */ export function toCytoscapeJSON(graph: Graph): CytoscapeJSON { const graphData: Record = {}; if (graph.id) graphData.id = graph.id; @@ -66,6 +83,21 @@ export function toCytoscapeJSON(graph: Graph): CytoscapeJSON { }; } +/** + * Parses a Cytoscape.js JSON object into a graph. + * + * @example + * ```ts + * import { fromCytoscapeJSON } from '@statelyai/graph/formats/cytoscape'; + * + * const graph = fromCytoscapeJSON({ + * elements: { + * nodes: [{ data: { id: 'a' } }, { data: { id: 'b' } }], + * edges: [{ data: { id: 'e0', source: 'a', target: 'b' } }], + * }, + * }); + * ``` + */ export function fromCytoscapeJSON(cyto: CytoscapeJSON): Graph { if (!cyto || typeof cyto !== 'object') { throw new Error('Cytoscape: expected an object'); @@ -110,6 +142,22 @@ export function fromCytoscapeJSON(cyto: CytoscapeJSON): Graph { }; } -/** Bidirectional converter for Cytoscape.js JSON format. */ +/** + * Bidirectional converter for Cytoscape.js JSON format. + * + * @example + * ```ts + * import { createGraph } from '@statelyai/graph'; + * import { cytoscapeConverter } from '@statelyai/graph/formats/cytoscape'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e0', sourceId: 'a', targetId: 'b' }], + * }); + * + * const cyto = cytoscapeConverter.to(graph); + * const roundTripped = cytoscapeConverter.from(cyto); + * ``` + */ export const cytoscapeConverter: GraphFormatConverter = createFormatConverter(toCytoscapeJSON, fromCytoscapeJSON); diff --git a/src/formats/d3/index.ts b/src/formats/d3/index.ts index 14b60a3..40ffe06 100644 --- a/src/formats/d3/index.ts +++ b/src/formats/d3/index.ts @@ -21,6 +21,23 @@ export interface D3Graph { // --- Conversion --- +/** + * Converts a graph to D3.js force-directed format. + * + * @example + * ```ts + * import { createGraph } from '@statelyai/graph'; + * import { toD3Graph } from '@statelyai/graph/formats/d3'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e0', sourceId: 'a', targetId: 'b' }], + * }); + * + * const d3 = toD3Graph(graph); + * // { nodes: [{ id: 'a' }, { id: 'b' }], links: [{ source: 'a', target: 'b' }] } + * ``` + */ export function toD3Graph(graph: Graph): D3Graph { return { nodes: graph.nodes.map((n) => { @@ -47,6 +64,19 @@ export function toD3Graph(graph: Graph): D3Graph { }; } +/** + * Parses a D3.js force-directed JSON object into a graph. + * + * @example + * ```ts + * import { fromD3Graph } from '@statelyai/graph/formats/d3'; + * + * const graph = fromD3Graph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * links: [{ source: 'a', target: 'b' }], + * }); + * ``` + */ export function fromD3Graph(d3: D3Graph): Graph { if (!d3 || typeof d3 !== 'object') { throw new Error('D3: expected an object'); @@ -86,6 +116,22 @@ export function fromD3Graph(d3: D3Graph): Graph { }; } -/** Bidirectional converter for D3.js force-directed JSON format. */ +/** + * Bidirectional converter for D3.js force-directed JSON format. + * + * @example + * ```ts + * import { createGraph } from '@statelyai/graph'; + * import { d3Converter } from '@statelyai/graph/formats/d3'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e0', sourceId: 'a', targetId: 'b' }], + * }); + * + * const d3 = d3Converter.to(graph); + * const roundTripped = d3Converter.from(d3); + * ``` + */ export const d3Converter: GraphFormatConverter = createFormatConverter(toD3Graph, fromD3Graph); diff --git a/src/formats/dot/index.ts b/src/formats/dot/index.ts index 844cbd2..833a576 100644 --- a/src/formats/dot/index.ts +++ b/src/formats/dot/index.ts @@ -32,6 +32,26 @@ const SHAPE_TO_DOT: Record = { parallelogram: 'parallelogram', }; +/** + * Converts a graph to a DOT (Graphviz) format string. + * + * @example + * ```ts + * import { createGraph, toDOT } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: { a: {}, b: {} }, + * edges: [{ source: 'a', target: 'b' }], + * }); + * + * const dot = toDOT(graph); + * // digraph "" { + * // a; + * // b; + * // a -> b; + * // } + * ``` + */ export function toDOT(graph: Graph): string { const isDirected = graph.type === 'directed'; const keyword = isDirected ? 'digraph' : 'graph'; @@ -130,6 +150,24 @@ function nodeFromAttrs( }; } +/** + * Parses a DOT (Graphviz) format string into a graph. + * + * @example + * ```ts + * import { fromDOT } from '@statelyai/graph'; + * + * const graph = fromDOT(` + * digraph { + * a -> b; + * b -> c; + * } + * `); + * + * graph.nodes; // [{id: 'a', ...}, {id: 'b', ...}, {id: 'c', ...}] + * graph.edges; // [{sourceId: 'a', targetId: 'b', ...}, ...] + * ``` + */ export function fromDOT(dot: string): Graph { if (typeof dot !== 'string') { throw new Error('DOT: expected a string'); @@ -326,6 +364,21 @@ export function fromDOT(dot: string): Graph { }; } -/** Bidirectional converter for DOT (Graphviz) format. */ +/** + * Bidirectional converter for DOT (Graphviz) format. + * + * @example + * ```ts + * import { dotConverter, createGraph } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: { a: {}, b: {} }, + * edges: [{ source: 'a', target: 'b' }], + * }); + * + * const dot = dotConverter.to(graph); + * const roundTripped = dotConverter.from(dot); + * ``` + */ export const dotConverter: GraphFormatConverter = createFormatConverter(toDOT, fromDOT); diff --git a/src/formats/edge-list/index.ts b/src/formats/edge-list/index.ts index bbcf361..8628028 100644 --- a/src/formats/edge-list/index.ts +++ b/src/formats/edge-list/index.ts @@ -1,9 +1,41 @@ import type { Graph } from '../../types'; +/** + * Converts a graph to an edge list of `[source, target]` tuples. + * + * @example + * ```ts + * import { createGraph, toEdgeList } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: { a: {}, b: {}, c: {} }, + * edges: [{ source: 'a', target: 'b' }, { source: 'b', target: 'c' }], + * }); + * + * toEdgeList(graph); + * // [['a', 'b'], ['b', 'c']] + * ``` + */ export function toEdgeList(graph: Graph): [string, string][] { return graph.edges.map((e) => [e.sourceId, e.targetId]); } +/** + * Parses an edge list of `[source, target]` tuples into a graph. + * + * @example + * ```ts + * import { fromEdgeList } from '@statelyai/graph'; + * + * const graph = fromEdgeList([ + * ['a', 'b'], + * ['b', 'c'], + * ]); + * + * graph.nodes; // [{id: 'a', ...}, {id: 'b', ...}, {id: 'c', ...}] + * graph.edges; // [{sourceId: 'a', targetId: 'b', ...}, ...] + * ``` + */ export function fromEdgeList( edges: [string, string][], options?: { directed?: boolean; id?: string }, diff --git a/src/formats/gml/index.ts b/src/formats/gml/index.ts index 5e9b267..48d30a0 100644 --- a/src/formats/gml/index.ts +++ b/src/formats/gml/index.ts @@ -3,6 +3,28 @@ import { createFormatConverter } from '../converter'; // --- GML serializer --- +/** + * Converts a graph to GML (Graph Modelling Language) string. + * + * @example + * ```ts + * import { createGraph } from '@statelyai/graph'; + * import { toGML } from '@statelyai/graph/formats/gml'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e0', sourceId: 'a', targetId: 'b' }], + * }); + * + * const gml = toGML(graph); + * // graph [ + * // directed 1 + * // node [ id "a" ] + * // node [ id "b" ] + * // edge [ id "e0" source "a" target "b" ] + * // ] + * ``` + */ export function toGML(graph: Graph): string { const lines: string[] = []; lines.push('graph ['); @@ -80,6 +102,23 @@ function gmlString(s: string): string { // --- GML parser --- +/** + * Parses a GML (Graph Modelling Language) string into a graph. + * + * @example + * ```ts + * import { fromGML } from '@statelyai/graph/formats/gml'; + * + * const graph = fromGML(` + * graph [ + * directed 1 + * node [ id "a" ] + * node [ id "b" ] + * edge [ source "a" target "b" ] + * ] + * `); + * ``` + */ export function fromGML(gml: string): Graph { if (typeof gml !== 'string') { throw new Error('GML: expected a string'); @@ -292,6 +331,22 @@ function tryParseJSON(str: any): any { } } -/** Bidirectional converter for GML (Graph Modelling Language) format. */ +/** + * Bidirectional converter for GML (Graph Modelling Language) format. + * + * @example + * ```ts + * import { createGraph } from '@statelyai/graph'; + * import { gmlConverter } from '@statelyai/graph/formats/gml'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e0', sourceId: 'a', targetId: 'b' }], + * }); + * + * const gml = gmlConverter.to(graph); + * const roundTripped = gmlConverter.from(gml); + * ``` + */ export const gmlConverter: GraphFormatConverter = createFormatConverter(toGML, fromGML); diff --git a/src/formats/jgf/index.ts b/src/formats/jgf/index.ts index 011c849..c066cfa 100644 --- a/src/formats/jgf/index.ts +++ b/src/formats/jgf/index.ts @@ -29,6 +29,23 @@ export interface JGFGraph { // --- Conversion --- +/** + * Converts a graph to JSON Graph Format (JGF). + * + * @example + * ```ts + * import { createGraph } from '@statelyai/graph'; + * import { toJGF } from '@statelyai/graph/formats/jgf'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e0', sourceId: 'a', targetId: 'b' }], + * }); + * + * const jgf = toJGF(graph); + * // { graph: { directed: true, nodes: [...], edges: [...] } } + * ``` + */ export function toJGF(graph: Graph): JGFGraph { const metadata: Record = {}; if (graph.initialNodeId !== null) metadata.initialNodeId = graph.initialNodeId; @@ -73,6 +90,22 @@ export function toJGF(graph: Graph): JGFGraph { }; } +/** + * Parses a JSON Graph Format (JGF) object into a graph. + * + * @example + * ```ts + * import { fromJGF } from '@statelyai/graph/formats/jgf'; + * + * const graph = fromJGF({ + * graph: { + * directed: true, + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ source: 'a', target: 'b' }], + * }, + * }); + * ``` + */ export function fromJGF(jgf: JGFGraph): Graph { if (!jgf || typeof jgf !== 'object') { throw new Error('JGF: expected an object'); @@ -119,6 +152,22 @@ export function fromJGF(jgf: JGFGraph): Graph { }; } -/** Bidirectional converter for JSON Graph Format. */ +/** + * Bidirectional converter for JSON Graph Format. + * + * @example + * ```ts + * import { createGraph } from '@statelyai/graph'; + * import { jgfConverter } from '@statelyai/graph/formats/jgf'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e0', sourceId: 'a', targetId: 'b' }], + * }); + * + * const jgf = jgfConverter.to(graph); + * const roundTripped = jgfConverter.from(jgf); + * ``` + */ export const jgfConverter: GraphFormatConverter = createFormatConverter(toJGF, fromJGF); diff --git a/src/formats/mermaid/block.ts b/src/formats/mermaid/block.ts index 19087a2..cc7e18b 100644 --- a/src/formats/mermaid/block.ts +++ b/src/formats/mermaid/block.ts @@ -54,6 +54,17 @@ function parseBlockNode(text: string): { id: string; label: string; shape?: stri return null; } +/** + * Parses a Mermaid block diagram string into a Graph. + * + * @example + * const graph = fromMermaidBlock(` + * block-beta + * columns 2 + * a["Task A"] b["Task B"] + * a --> b + * `); + */ export function fromMermaidBlock(input: string): BlockGraph { validateInput(input, 'Mermaid block'); const { lines } = prepareLines(input); @@ -178,6 +189,13 @@ export function fromMermaidBlock(input: string): BlockGraph { // --- Serializer --- +/** + * Converts a block diagram Graph to a Mermaid block diagram string. + * + * @example + * const mermaid = toMermaidBlock(graph); + * // "block-beta\n columns 2\n a[\"Task A\"]\n ..." + */ export function toMermaidBlock(graph: BlockGraph): string { const lines: string[] = ['block-beta']; @@ -225,7 +243,17 @@ export function toMermaidBlock(graph: BlockGraph): string { return lines.join('\n'); } -/** Bidirectional converter for Mermaid block diagram format. */ +/** + * Bidirectional converter for Mermaid block diagram format. + * + * @example + * const graph = mermaidBlockConverter.from(` + * block-beta + * columns 2 + * a b + * `); + * const str = mermaidBlockConverter.to(graph); + */ export const mermaidBlockConverter: GraphFormatConverter = createFormatConverter( toMermaidBlock as (graph: Graph) => string, diff --git a/src/formats/mermaid/class-diagram.ts b/src/formats/mermaid/class-diagram.ts index d27ecd7..5921d77 100644 --- a/src/formats/mermaid/class-diagram.ts +++ b/src/formats/mermaid/class-diagram.ts @@ -185,6 +185,19 @@ function parseMember(line: string): { // --- Parser --- +/** + * Parses a Mermaid class diagram string into a Graph. + * + * @example + * const graph = fromMermaidClass(` + * classDiagram + * class Animal { + * +String name + * +eat() void + * } + * Animal <|-- Dog + * `); + */ export function fromMermaidClass(input: string): ClassGraph { validateInput(input, 'Mermaid class'); const { lines } = prepareLines(input); @@ -339,6 +352,13 @@ const VISIBILITY_SYMBOLS: Record = { '~': '~', }; +/** + * Converts a class diagram Graph to a Mermaid class diagram string. + * + * @example + * const mermaid = toMermaidClass(graph); + * // "classDiagram\n class Animal {\n ..." + */ export function toMermaidClass(graph: ClassGraph): string { const lines: string[] = ['classDiagram']; @@ -388,7 +408,16 @@ export function toMermaidClass(graph: ClassGraph): string { return lines.join('\n'); } -/** Bidirectional converter for Mermaid class diagram format. */ +/** + * Bidirectional converter for Mermaid class diagram format. + * + * @example + * const graph = mermaidClassConverter.from(` + * classDiagram + * Animal <|-- Dog + * `); + * const str = mermaidClassConverter.to(graph); + */ export const mermaidClassConverter: GraphFormatConverter = createFormatConverter( toMermaidClass as (graph: Graph) => string, diff --git a/src/formats/mermaid/er-diagram.ts b/src/formats/mermaid/er-diagram.ts index 8c6d961..0966997 100644 --- a/src/formats/mermaid/er-diagram.ts +++ b/src/formats/mermaid/er-diagram.ts @@ -100,6 +100,16 @@ function parseERRelationship(symbol: string): { const ER_LINE_RE = /^(\S+)\s+([|}{o.][|}{o.][-.][-.][|}{o.][|}{o.])\s+(\S+)\s*:\s*"?([^"]*)"?\s*$/; +/** + * Parses a Mermaid ER diagram string into a Graph. + * + * @example + * const graph = fromMermaidER(` + * erDiagram + * CUSTOMER ||--o{ ORDER : places + * ORDER ||--|{ LINE_ITEM : contains + * `); + */ export function fromMermaidER(input: string): ERGraph { validateInput(input, 'Mermaid ER'); const { lines } = prepareLines(input); @@ -211,6 +221,13 @@ export function fromMermaidER(input: string): ERGraph { // --- Serializer --- +/** + * Converts an ER diagram Graph to a Mermaid ER diagram string. + * + * @example + * const mermaid = toMermaidER(graph); + * // "erDiagram\n CUSTOMER ||--o{ ORDER : \"places\"\n ..." + */ export function toMermaidER(graph: ERGraph): string { const lines: string[] = ['erDiagram']; @@ -243,7 +260,16 @@ export function toMermaidER(graph: ERGraph): string { return lines.join('\n'); } -/** Bidirectional converter for Mermaid ER diagram format. */ +/** + * Bidirectional converter for Mermaid ER diagram format. + * + * @example + * const graph = mermaidERConverter.from(` + * erDiagram + * CUSTOMER ||--o{ ORDER : places + * `); + * const str = mermaidERConverter.to(graph); + */ export const mermaidERConverter: GraphFormatConverter = createFormatConverter( toMermaidER as (graph: Graph) => string, diff --git a/src/formats/mermaid/flowchart.ts b/src/formats/mermaid/flowchart.ts index 716b4b3..8246a23 100644 --- a/src/formats/mermaid/flowchart.ts +++ b/src/formats/mermaid/flowchart.ts @@ -196,6 +196,16 @@ function findEdge(line: string): { // --- Parser --- +/** + * Parses a Mermaid flowchart string into a Graph. + * + * @example + * const graph = fromMermaidFlowchart(` + * flowchart TD + * A[Start] --> B{Decision} + * B -->|Yes| C[End] + * `); + */ export function fromMermaidFlowchart(input: string): FlowchartGraph { validateInput(input, 'Mermaid flowchart'); const { lines } = prepareLines(input); @@ -452,6 +462,13 @@ export function fromMermaidFlowchart(input: string): FlowchartGraph { // --- Serializer --- +/** + * Converts a flowchart Graph to a Mermaid flowchart string. + * + * @example + * const mermaid = toMermaidFlowchart(graph); + * // "flowchart TD\n A[Start] --> B{Decision}\n ..." + */ export function toMermaidFlowchart(graph: FlowchartGraph): string { const dir = DIRECTION_TO_MERMAID[graph.direction ?? 'down'] ?? 'TD'; const lines: string[] = [`flowchart ${dir}`]; @@ -561,7 +578,16 @@ export function toMermaidFlowchart(graph: FlowchartGraph): string { return lines.join('\n'); } -/** Bidirectional converter for Mermaid flowchart format. */ +/** + * Bidirectional converter for Mermaid flowchart format. + * + * @example + * const graph = mermaidFlowchartConverter.from(` + * flowchart TD + * A --> B + * `); + * const str = mermaidFlowchartConverter.to(graph); + */ export const mermaidFlowchartConverter: GraphFormatConverter = createFormatConverter( toMermaidFlowchart as (graph: Graph) => string, diff --git a/src/formats/mermaid/mindmap.ts b/src/formats/mermaid/mindmap.ts index e4d5d25..d8a4ce2 100644 --- a/src/formats/mermaid/mindmap.ts +++ b/src/formats/mermaid/mindmap.ts @@ -54,6 +54,18 @@ function parseNodeText(text: string): { label: string; shape?: string } { // --- Parser --- +/** + * Parses a Mermaid mindmap string into a Graph. + * + * @example + * const graph = fromMermaidMindmap(` + * mindmap + * Root + * Child A + * Grandchild + * Child B + * `); + */ export function fromMermaidMindmap(input: string): MindmapGraph { validateInput(input, 'Mermaid mindmap'); const { lines } = prepareLines(input); @@ -140,6 +152,13 @@ export function fromMermaidMindmap(input: string): MindmapGraph { // --- Serializer --- +/** + * Converts a mindmap Graph to a Mermaid mindmap string. + * + * @example + * const mermaid = toMermaidMindmap(graph); + * // "mindmap\n Root\n Child A\n ..." + */ export function toMermaidMindmap(graph: MindmapGraph): string { const lines: string[] = ['mindmap']; @@ -177,7 +196,17 @@ export function toMermaidMindmap(graph: MindmapGraph): string { return lines.join('\n'); } -/** Bidirectional converter for Mermaid mindmap format. */ +/** + * Bidirectional converter for Mermaid mindmap format. + * + * @example + * const graph = mermaidMindmapConverter.from(` + * mindmap + * Root + * Branch + * `); + * const str = mermaidMindmapConverter.to(graph); + */ export const mermaidMindmapConverter: GraphFormatConverter = createFormatConverter( toMermaidMindmap as (graph: Graph) => string, diff --git a/src/formats/mermaid/sequence.ts b/src/formats/mermaid/sequence.ts index b596603..e63ad18 100644 --- a/src/formats/mermaid/sequence.ts +++ b/src/formats/mermaid/sequence.ts @@ -99,6 +99,18 @@ const MESSAGE_RE = // --- Parser --- +/** + * Parses a Mermaid sequence diagram string into a Graph. + * + * @example + * const graph = fromMermaidSequence(` + * sequenceDiagram + * participant Alice + * participant Bob + * Alice->>Bob: Hello + * Bob-->>Alice: Hi back + * `); + */ export function fromMermaidSequence(input: string): SequenceGraph { validateInput(input, 'Mermaid sequence'); const { lines } = prepareLines(input); @@ -476,6 +488,13 @@ const ARROW_MAP: Record> = { }, }; +/** + * Converts a sequence diagram Graph to a Mermaid sequence diagram string. + * + * @example + * const mermaid = toMermaidSequence(graph); + * // "sequenceDiagram\n participant Alice\n ..." + */ export function toMermaidSequence(graph: SequenceGraph): string { const lines: string[] = ['sequenceDiagram']; const gd = graph.data; @@ -531,7 +550,16 @@ export function toMermaidSequence(graph: SequenceGraph): string { return lines.join('\n'); } -/** Bidirectional converter for Mermaid sequence diagram format. */ +/** + * Bidirectional converter for Mermaid sequence diagram format. + * + * @example + * const graph = mermaidSequenceConverter.from(` + * sequenceDiagram + * Alice->>Bob: Hello + * `); + * const str = mermaidSequenceConverter.to(graph); + */ export const mermaidSequenceConverter: GraphFormatConverter = createFormatConverter( toMermaidSequence as (graph: Graph) => string, diff --git a/src/formats/mermaid/state.ts b/src/formats/mermaid/state.ts index 74ca891..2c54284 100644 --- a/src/formats/mermaid/state.ts +++ b/src/formats/mermaid/state.ts @@ -31,6 +31,17 @@ type StateEdge = GraphEdge; // --- Parser --- +/** + * Parses a Mermaid state diagram string into a Graph. + * + * @example + * const graph = fromMermaidState(` + * stateDiagram-v2 + * [*] --> Idle + * Idle --> Running : start + * Running --> [*] + * `); + */ export function fromMermaidState(input: string): StateGraph { validateInput(input, 'Mermaid state'); const { lines } = prepareLines(input); @@ -210,6 +221,13 @@ export function fromMermaidState(input: string): StateGraph { // --- Serializer --- +/** + * Converts a state diagram Graph to a Mermaid state diagram string. + * + * @example + * const mermaid = toMermaidState(graph); + * // "stateDiagram-v2\n [*] --> Idle\n ..." + */ export function toMermaidState(graph: StateGraph): string { const lines: string[] = ['stateDiagram-v2']; @@ -281,7 +299,16 @@ export function toMermaidState(graph: StateGraph): string { return lines.join('\n'); } -/** Bidirectional converter for Mermaid state diagram format. */ +/** + * Bidirectional converter for Mermaid state diagram format. + * + * @example + * const graph = mermaidStateConverter.from(` + * stateDiagram-v2 + * [*] --> Active + * `); + * const str = mermaidStateConverter.to(graph); + */ export const mermaidStateConverter: GraphFormatConverter = createFormatConverter( toMermaidState as (graph: Graph) => string, diff --git a/src/formats/tgf/index.ts b/src/formats/tgf/index.ts index 1be9606..2021005 100644 --- a/src/formats/tgf/index.ts +++ b/src/formats/tgf/index.ts @@ -3,6 +3,23 @@ import { createFormatConverter } from '../converter'; // --- TGF (Trivial Graph Format) --- +/** + * Converts a graph to TGF (Trivial Graph Format) string. + * + * @example + * ```ts + * import { createGraph } from '@statelyai/graph'; + * import { toTGF } from '@statelyai/graph/formats/tgf'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }], + * edges: [{ id: 'e0', sourceId: 'a', targetId: 'b', label: 'go' }], + * }); + * + * const tgf = toTGF(graph); + * // "a A\nb B\n#\na b go" + * ``` + */ export function toTGF(graph: Graph): string { const lines: string[] = []; @@ -21,6 +38,17 @@ export function toTGF(graph: Graph): string { return lines.join('\n'); } +/** + * Parses a TGF (Trivial Graph Format) string into a graph. + * + * @example + * ```ts + * import { fromTGF } from '@statelyai/graph/formats/tgf'; + * + * const graph = fromTGF('a A\nb B\n#\na b go'); + * // graph.nodes = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }] + * ``` + */ export function fromTGF(tgf: string): Graph { if (typeof tgf !== 'string') { throw new Error('TGF: expected a string'); @@ -75,6 +103,22 @@ export function fromTGF(tgf: string): Graph { }; } -/** Bidirectional converter for TGF (Trivial Graph Format). */ +/** + * Bidirectional converter for TGF (Trivial Graph Format). + * + * @example + * ```ts + * import { createGraph } from '@statelyai/graph'; + * import { tgfConverter } from '@statelyai/graph/formats/tgf'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e0', sourceId: 'a', targetId: 'b' }], + * }); + * + * const tgf = tgfConverter.to(graph); + * const roundTripped = tgfConverter.from(tgf); + * ``` + */ export const tgfConverter: GraphFormatConverter = createFormatConverter(toTGF, fromTGF); diff --git a/src/graph.ts b/src/graph.ts index 3ec4a99..efc90a0 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -64,7 +64,17 @@ function resolveEdge(config: EdgeConfig): GraphEdge { // Factory // --------------------------------------------------------------------------- -/** Create a graph from a config. Resolves defaults for all fields. */ +/** + * Create a graph from a config. Resolves defaults for all fields. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * ``` + */ export function createGraph( config?: GraphConfig, ): Graph { @@ -81,7 +91,19 @@ export function createGraph( return graph; } -/** Create a visual graph with required position/size on all nodes and edges. */ +/** + * Create a visual graph with required position/size on all nodes and edges. + * + * @example + * ```ts + * const graph = createVisualGraph({ + * nodes: [{ id: 'a', x: 0, y: 0, width: 100, height: 50 }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'a', x: 0, y: 0, width: 0, height: 0 }], + * }); + * // graph.nodes[0].x === 0 + * // graph.nodes[0].shape === 'rectangle' + * ``` + */ export function createVisualGraph( config?: VisualGraphConfig, ): VisualGraph { @@ -113,11 +135,30 @@ export function createVisualGraph( /** * Create a graph by BFS exploration of a transition function. - * Each unique state becomes a node; each (state, event) → nextState becomes an edge. + * Each unique state becomes a node; each (state, event) -> nextState becomes an edge. * * - Node IDs are determined by `serializeState` (default: `JSON.stringify`). * - Edge IDs use the format `sourceId|serializedEvent|targetId` for uniqueness * and debuggability. Edge labels are just the serialized event string. + * + * @example + * ```ts + * const graph = createGraphFromTransition( + * (state, event) => { + * if (state === 'green' && event === 'TIMER') return 'yellow'; + * if (state === 'yellow' && event === 'TIMER') return 'red'; + * if (state === 'red' && event === 'TIMER') return 'green'; + * return state; + * }, + * { + * initialState: 'green', + * events: ['TIMER'], + * serializeState: (s) => s, + * serializeEvent: (e) => e, + * }, + * ); + * // graph.nodes.length === 3 + * ``` */ export function createGraphFromTransition( transition: (state: TState, event: TEvent) => TState, @@ -194,26 +235,68 @@ export function createGraphFromTransition( // Lookup helpers // --------------------------------------------------------------------------- -/** Get a node by id, or `undefined` if not found. */ +/** + * Get a node by id, or `undefined` if not found. + * + * @example + * ```ts + * const graph = createGraph({ nodes: [{ id: 'a' }] }); + * const node = getNode(graph, 'a'); // GraphNode + * const missing = getNode(graph, 'z'); // undefined + * ``` + */ export function getNode(graph: Graph, id: string): GraphNode | undefined { const idx = getIndex(graph); const arrayIdx = idx.nodeById.get(id); return arrayIdx !== undefined ? graph.nodes[arrayIdx] : undefined; } -/** Get an edge by id, or `undefined` if not found. */ +/** + * Get an edge by id, or `undefined` if not found. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * const edge = getEdge(graph, 'e1'); // GraphEdge + * const missing = getEdge(graph, 'z'); // undefined + * ``` + */ export function getEdge(graph: Graph, id: string): GraphEdge | undefined { const idx = getIndex(graph); const arrayIdx = idx.edgeById.get(id); return arrayIdx !== undefined ? graph.edges[arrayIdx] : undefined; } -/** Check if a node exists in the graph. */ +/** + * Check if a node exists in the graph. + * + * @example + * ```ts + * const graph = createGraph({ nodes: [{ id: 'a' }] }); + * hasNode(graph, 'a'); // true + * hasNode(graph, 'z'); // false + * ``` + */ export function hasNode(graph: Graph, id: string): boolean { return getIndex(graph).nodeById.has(id); } -/** Check if an edge exists in the graph. */ +/** + * Check if an edge exists in the graph. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * hasEdge(graph, 'e1'); // true + * hasEdge(graph, 'z'); // false + * ``` + */ export function hasEdge(graph: Graph, id: string): boolean { return getIndex(graph).edgeById.has(id); } @@ -225,6 +308,13 @@ export function hasEdge(graph: Graph, id: string): boolean { /** * **Mutable.** Add a node to the graph. Mutates `graph.nodes` in place. * @returns The resolved node that was added. + * + * @example + * ```ts + * const graph = createGraph(); + * const node = addNode(graph, { id: 'a', label: 'Node A' }); + * // graph.nodes.length === 1 + * ``` */ export function addNode(graph: Graph, config: NodeConfig): GraphNode { const idx = getIndex(graph); @@ -243,6 +333,13 @@ export function addNode(graph: Graph, config: NodeConfig): GraphNode /** * **Mutable.** Add an edge to the graph. Mutates `graph.edges` in place. * @returns The resolved edge that was added. + * + * @example + * ```ts + * const graph = createGraph({ nodes: [{ id: 'a' }, { id: 'b' }] }); + * const edge = addEdge(graph, { id: 'e1', sourceId: 'a', targetId: 'b' }); + * // graph.edges.length === 1 + * ``` */ export function addEdge(graph: Graph, config: EdgeConfig): GraphEdge { const idx = getIndex(graph); @@ -267,6 +364,16 @@ export function addEdge(graph: Graph, config: EdgeConfig): GraphEd * * By default, children are deleted recursively. * With `{ reparent: true }`, children are re-parented to the deleted node's parent. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * deleteNode(graph, 'a'); + * // graph.nodes.length === 1, edge e1 also removed + * ``` */ export function deleteNode( graph: Graph, @@ -302,6 +409,16 @@ export function deleteNode( /** * **Mutable.** Delete an edge. Mutates `graph.edges` in place. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * deleteEdge(graph, 'e1'); + * // graph.edges.length === 0 + * ``` */ export function deleteEdge(graph: Graph, id: string): void { if (!hasEdge(graph, id)) { @@ -314,6 +431,13 @@ export function deleteEdge(graph: Graph, id: string): void { /** * **Mutable.** Update a node in place. * @returns The updated node. + * + * @example + * ```ts + * const graph = createGraph({ nodes: [{ id: 'a', label: 'old' }] }); + * const updated = updateNode(graph, 'a', { label: 'new' }); + * // updated.label === 'new' + * ``` */ export function updateNode( graph: Graph, @@ -352,6 +476,16 @@ export function updateNode( /** * **Mutable.** Update an edge in place. * @returns The updated edge. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b', label: 'old' }], + * }); + * const updated = updateEdge(graph, 'e1', { label: 'new' }); + * // updated.label === 'new' + * ``` */ export function updateEdge( graph: Graph, @@ -396,6 +530,16 @@ export function updateEdge( /** * **Mutable.** Add multiple nodes and edges to the graph. * Nodes are added first, then edges (so edges can reference new nodes). + * + * @example + * ```ts + * const graph = createGraph(); + * addEntities(graph, { + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * // graph.nodes.length === 2, graph.edges.length === 1 + * ``` */ export function addEntities( graph: Graph, @@ -412,6 +556,16 @@ export function addEntities( /** * **Mutable.** Delete entities by id(s). Automatically detects whether each id * is a node or edge. Node deletions cascade to children and connected edges. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * deleteEntities(graph, ['a', 'e1']); + * // graph.nodes.length === 1, graph.edges.length === 0 + * ``` */ export function deleteEntities( graph: Graph, @@ -435,6 +589,18 @@ export function deleteEntities( /** * **Mutable.** Update multiple nodes and edges in place. * Each entry must include an `id` to identify which entity to update. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a', label: 'old' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'a', label: 'old' }], + * }); + * updateEntities(graph, { + * nodes: [{ id: 'a', label: 'new' }], + * edges: [{ id: 'e1', label: 'new' }], + * }); + * ``` */ export function updateEntities( graph: Graph, @@ -457,6 +623,17 @@ export function updateEntities( /** * OOP wrapper around a plain `Graph` object. * Delegates to the standalone mutable functions. + * + * @example + * ```ts + * const instance = new GraphInstance({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * instance.addNode({ id: 'c' }); + * instance.hasNode('c'); // true + * instance.toJSON(); // plain Graph object + * ``` */ export class GraphInstance { public graph: Graph; @@ -465,7 +642,16 @@ export class GraphInstance { this.graph = createGraph(config); } - /** Wrap an existing plain graph object. */ + /** + * Wrap an existing plain graph object. + * + * @example + * ```ts + * const graph = createGraph({ nodes: [{ id: 'a' }] }); + * const instance = GraphInstance.from(graph); + * instance.hasNode('a'); // true + * ``` + */ static from( graph: Graph, ): GraphInstance { diff --git a/src/indexing.ts b/src/indexing.ts index 857634c..9746835 100644 --- a/src/indexing.ts +++ b/src/indexing.ts @@ -30,7 +30,24 @@ const indexes = new WeakMap(); // Public API // --------------------------------------------------------------------------- -/** Get or lazily build the index for a graph. */ +/** + * Get or lazily build the index for a graph. + * Auto-rebuilds when node/edge count changes. + * + * @example + * ```ts + * import { createGraph, getIndex } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * + * const idx = getIndex(graph); + * idx.nodeById.get('a'); // 0 + * idx.outEdges.get('a'); // ['e1'] + * ``` + */ export function getIndex(graph: Graph): GraphIndex { let idx = indexes.get(graph); if ( @@ -44,7 +61,19 @@ export function getIndex(graph: Graph): GraphIndex { return idx; } -/** Clear the cached index. Call this if you mutate graph.nodes/edges directly. */ +/** + * Clear the cached index. Call this if you mutate graph.nodes/edges directly. + * + * @example + * ```ts + * import { createGraph, invalidateIndex, getIndex } from '@statelyai/graph'; + * + * const graph = createGraph({ nodes: [{ id: 'a' }], edges: [] }); + * // manually mutate nodes array + * graph.nodes.push({ type: 'node', id: 'b', parentId: null, initialNodeId: null, label: '', data: undefined }); + * invalidateIndex(graph); // forces rebuild on next getIndex() + * ``` + */ export function invalidateIndex(graph: Graph): void { indexes.delete(graph); } diff --git a/src/queries.ts b/src/queries.ts index 53d1f17..ae9c841 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -3,6 +3,22 @@ import { getIndex } from './indexing'; // --- Edge queries --- +/** + * Returns all edges (incoming + outgoing) connected to a node. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'e1', sourceId: 'a', targetId: 'b' }, + * { id: 'e2', sourceId: 'c', targetId: 'b' }, + * ], + * }); + * getEdgesOf(graph, 'b'); + * // => [edge e1, edge e2] + * ``` + */ export function getEdgesOf(graph: Graph, nodeId: string): GraphEdge[] { const idx = getIndex(graph); const outIds = idx.outEdges.get(nodeId) ?? []; @@ -23,18 +39,64 @@ export function getEdgesOf(graph: Graph, nodeId: string): GraphEdge [edge e1] + * getInEdges(graph, 'a'); + * // => [] + * ``` + */ export function getInEdges(graph: Graph, nodeId: string): GraphEdge[] { const idx = getIndex(graph); const edgeIds = idx.inEdges.get(nodeId) ?? []; return edgeIds.map((eid) => graph.edges[idx.edgeById.get(eid)!]); } +/** + * Returns outgoing edges from a node. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * getOutEdges(graph, 'a'); + * // => [edge e1] + * getOutEdges(graph, 'b'); + * // => [] + * ``` + */ export function getOutEdges(graph: Graph, nodeId: string): GraphEdge[] { const idx = getIndex(graph); const edgeIds = idx.outEdges.get(nodeId) ?? []; return edgeIds.map((eid) => graph.edges[idx.edgeById.get(eid)!]); } +/** + * Returns the edge from `sourceId` to `targetId`, or `undefined` if none exists. + * For undirected graphs, checks both directions. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * getEdgeBetween(graph, 'a', 'b'); + * // => edge e1 + * getEdgeBetween(graph, 'b', 'a'); + * // => undefined (directed graph) + * ``` + */ export function getEdgeBetween( graph: Graph, sourceId: string, @@ -60,6 +122,22 @@ export function getEdgeBetween( // --- Neighbor queries --- +/** + * Returns direct successor nodes (targets of outgoing edges). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'e1', sourceId: 'a', targetId: 'b' }, + * { id: 'e2', sourceId: 'a', targetId: 'c' }, + * ], + * }); + * getSuccessors(graph, 'a'); + * // => [node b, node c] + * ``` + */ export function getSuccessors(graph: Graph, nodeId: string): GraphNode[] { const idx = getIndex(graph); const edgeIds = idx.outEdges.get(nodeId) ?? []; @@ -76,6 +154,22 @@ export function getSuccessors(graph: Graph, nodeId: string): GraphNode[ return result; } +/** + * Returns direct predecessor nodes (sources of incoming edges). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'e1', sourceId: 'a', targetId: 'c' }, + * { id: 'e2', sourceId: 'b', targetId: 'c' }, + * ], + * }); + * getPredecessors(graph, 'c'); + * // => [node a, node b] + * ``` + */ export function getPredecessors(graph: Graph, nodeId: string): GraphNode[] { const idx = getIndex(graph); const edgeIds = idx.inEdges.get(nodeId) ?? []; @@ -92,6 +186,22 @@ export function getPredecessors(graph: Graph, nodeId: string): GraphNode [node a, node c] + * ``` + */ export function getNeighbors(graph: Graph, nodeId: string): GraphNode[] { const idx = getIndex(graph); const ids = new Set(); @@ -106,6 +216,23 @@ export function getNeighbors(graph: Graph, nodeId: string): GraphNode[] // --- Degree queries --- +/** + * Returns the total degree of a node (inDegree + outDegree). + * For undirected graphs, each edge is counted once. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'e1', sourceId: 'a', targetId: 'b' }, + * { id: 'e2', sourceId: 'c', targetId: 'b' }, + * ], + * }); + * getDegree(graph, 'b'); // => 2 + * getDegree(graph, 'a'); // => 1 + * ``` + */ export function getDegree(graph: Graph, nodeId: string): number { const idx = getIndex(graph); if (graph.type === 'undirected') { @@ -118,16 +245,61 @@ export function getDegree(graph: Graph, nodeId: string): number { return (idx.inEdges.get(nodeId)?.length ?? 0) + (idx.outEdges.get(nodeId)?.length ?? 0); } +/** + * Returns the in-degree of a node (number of incoming edges). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * getInDegree(graph, 'b'); // => 1 + * getInDegree(graph, 'a'); // => 0 + * ``` + */ export function getInDegree(graph: Graph, nodeId: string): number { return getIndex(graph).inEdges.get(nodeId)?.length ?? 0; } +/** + * Returns the out-degree of a node (number of outgoing edges). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }], + * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }], + * }); + * getOutDegree(graph, 'a'); // => 1 + * getOutDegree(graph, 'b'); // => 0 + * ``` + */ export function getOutDegree(graph: Graph, nodeId: string): number { return getIndex(graph).outEdges.get(nodeId)?.length ?? 0; } // --- Hierarchy queries --- +/** + * Returns direct children of a node in the hierarchy. + * Pass `null` to get root-level nodes. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [ + * { id: 'parent' }, + * { id: 'child1', parentId: 'parent' }, + * { id: 'child2', parentId: 'parent' }, + * ], + * }); + * getChildren(graph, 'parent'); + * // => [node child1, node child2] + * getChildren(graph, null); + * // => [node parent] + * ``` + */ export function getChildren( graph: Graph, nodeId: string | null, @@ -137,6 +309,23 @@ export function getChildren( return childIds.map((id) => graph.nodes[idx.nodeById.get(id)!]).filter(Boolean); } +/** + * Returns the parent node in the hierarchy, or `undefined` if root-level. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [ + * { id: 'parent' }, + * { id: 'child', parentId: 'parent' }, + * ], + * }); + * getParent(graph, 'child'); + * // => node parent + * getParent(graph, 'parent'); + * // => undefined + * ``` + */ export function getParent( graph: Graph, nodeId: string, @@ -150,6 +339,22 @@ export function getParent( return pi !== undefined ? graph.nodes[pi] : undefined; } +/** + * Returns all ancestors from the node up to the root (nearest parent first). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [ + * { id: 'root' }, + * { id: 'mid', parentId: 'root' }, + * { id: 'leaf', parentId: 'mid' }, + * ], + * }); + * getAncestors(graph, 'leaf'); + * // => [node mid, node root] + * ``` + */ export function getAncestors( graph: Graph, nodeId: string, @@ -169,6 +374,22 @@ export function getAncestors( return result; } +/** + * Returns all descendants recursively (depth-first). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [ + * { id: 'root' }, + * { id: 'child', parentId: 'root' }, + * { id: 'grandchild', parentId: 'child' }, + * ], + * }); + * getDescendants(graph, 'root'); + * // => [node child, node grandchild] + * ``` + */ export function getDescendants( graph: Graph, nodeId: string, @@ -189,24 +410,85 @@ export function getDescendants( return result; } +/** + * Returns all root nodes (nodes with no parent, i.e. `parentId === null`). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [ + * { id: 'root1' }, + * { id: 'root2' }, + * { id: 'child', parentId: 'root1' }, + * ], + * }); + * getRoots(graph); + * // => [node root1, node root2] + * ``` + */ export function getRoots(graph: Graph): GraphNode[] { const idx = getIndex(graph); return idx.childNodes.get(null)?.map((id) => graph.nodes[idx.nodeById.get(id)!]).filter(Boolean) ?? []; } -/** Whether a node has children (is a compound/group node). */ +/** + * Whether a node has children (is a compound/group node). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [ + * { id: 'parent' }, + * { id: 'child', parentId: 'parent' }, + * ], + * }); + * isCompound(graph, 'parent'); // => true + * isCompound(graph, 'child'); // => false + * ``` + */ export function isCompound(graph: Graph, nodeId: string): boolean { const idx = getIndex(graph); const childIds = idx.childNodes.get(nodeId) ?? []; return childIds.length > 0; } -/** Whether a node has no children (is a leaf/atomic node). */ +/** + * Whether a node has no children (is a leaf/atomic node). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [ + * { id: 'parent' }, + * { id: 'child', parentId: 'parent' }, + * ], + * }); + * isLeaf(graph, 'child'); // => true + * isLeaf(graph, 'parent'); // => false + * ``` + */ export function isLeaf(graph: Graph, nodeId: string): boolean { return !isCompound(graph, nodeId); } -/** Depth of a node in the hierarchy (root = 0). */ +/** + * Depth of a node in the hierarchy (root = 0). + * Returns -1 if the node is not found. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [ + * { id: 'root' }, + * { id: 'child', parentId: 'root' }, + * { id: 'grandchild', parentId: 'child' }, + * ], + * }); + * getDepth(graph, 'root'); // => 0 + * getDepth(graph, 'child'); // => 1 + * getDepth(graph, 'grandchild'); // => 2 + * ``` + */ export function getDepth(graph: Graph, nodeId: string): number { const idx = getIndex(graph); let d = 0; @@ -222,7 +504,23 @@ export function getDepth(graph: Graph, nodeId: string): number { return d; } -/** Sibling nodes (same parentId, excluding the node itself). */ +/** + * Sibling nodes (same parentId, excluding the node itself). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [ + * { id: 'parent' }, + * { id: 'a', parentId: 'parent' }, + * { id: 'b', parentId: 'parent' }, + * { id: 'c', parentId: 'parent' }, + * ], + * }); + * getSiblings(graph, 'a'); + * // => [node b, node c] + * ``` + */ export function getSiblings( graph: Graph, nodeId: string, @@ -239,8 +537,24 @@ export function getSiblings( } /** - * Least Common Ancestor — deepest proper ancestor of all given nodes. + * Least Common Ancestor -- deepest proper ancestor of all given nodes. * A proper ancestor excludes the input nodes themselves. + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [ + * { id: 'root' }, + * { id: 'a', parentId: 'root' }, + * { id: 'b', parentId: 'root' }, + * { id: 'a1', parentId: 'a' }, + * ], + * }); + * getLCA(graph, 'a1', 'b'); + * // => node root + * getLCA(graph, 'a', 'b'); + * // => node root + * ``` */ export function getLCA( graph: Graph, @@ -428,13 +742,43 @@ export function getRelativeDistance( // --- Graph-level queries --- -/** Nodes with no incoming edges (inDegree 0). */ +/** + * Nodes with no incoming edges (inDegree 0). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'e1', sourceId: 'a', targetId: 'b' }, + * { id: 'e2', sourceId: 'b', targetId: 'c' }, + * ], + * }); + * getSources(graph); + * // => [node a] + * ``` + */ export function getSources(graph: Graph): GraphNode[] { const idx = getIndex(graph); return graph.nodes.filter((n) => (idx.inEdges.get(n.id)?.length ?? 0) === 0); } -/** Nodes with no outgoing edges (outDegree 0). */ +/** + * Nodes with no outgoing edges (outDegree 0). + * + * @example + * ```ts + * const graph = createGraph({ + * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + * edges: [ + * { id: 'e1', sourceId: 'a', targetId: 'b' }, + * { id: 'e2', sourceId: 'b', targetId: 'c' }, + * ], + * }); + * getSinks(graph); + * // => [node c] + * ``` + */ export function getSinks(graph: Graph): GraphNode[] { const idx = getIndex(graph); return graph.nodes.filter((n) => (idx.outEdges.get(n.id)?.length ?? 0) === 0); diff --git a/src/transforms.ts b/src/transforms.ts index 91349f2..314fb70 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -9,6 +9,25 @@ import { createGraph } from './graph'; * - Edges originating from a compound node expand to all leaf descendants. * - Only leaf nodes (nodes with no children) appear in the result. * - Duplicate edges (same source + target) are deduplicated. + * + * @example + * ```ts + * import { createGraph, flatten } from '@statelyai/graph'; + * + * const graph = createGraph({ + * nodes: [ + * { id: 'parent', initialNodeId: 'child1' }, + * { id: 'child1', parentId: 'parent' }, + * { id: 'child2', parentId: 'parent' }, + * { id: 'other' }, + * ], + * edges: [{ id: 'e1', sourceId: 'other', targetId: 'parent' }], + * }); + * + * const flat = flatten(graph); + * // flat.nodes → [child1, child2, other] (leaf nodes only) + * // flat.edges → edge from 'other' → 'child1' (resolved via initialNodeId) + * ``` */ export function flatten(graph: Graph): Graph { const idx = getIndex(graph);