--- status: reviewed last_updated: 2026-05-22 --- # FlowGraph Public API Complete public API surface for the `FlowGraph` class — constructor, type parameters, methods, and the delegation model with graphology. ## Overview `FlowGraph` is the central class that wraps a graphology `DirectedGraph` and enforces DAG invariants. It is generic over node and edge attribute types, supporting three distinct graph modes: operation graph, call graph, and template DAG. The class delegates graph operations to graphology while providing flowgraph-specific methods for construction, mutation, queries, and analysis. It is NOT a subclass of `DirectedGraph` — it wraps one, exposing a curated API surface. ## Type Parameters ```typescript class FlowGraph< NodeAttrs extends TSchema = OperationNodeAttrs | CallNodeAttrs, EdgeAttrs extends TSchema = OperationEdgeAttrs | CallEdgeAttrs | TemplateEdgeAttrs, > { private _graph: DirectedGraph; // ... } ``` | Parameter | Default | Purpose | |-----------|---------|---------| | `NodeAttrs` | `OperationNodeAttrs` or `CallNodeAttrs` | TypeBox schema for node attributes | | `EdgeAttrs` | `OperationEdgeAttrs`, `CallEdgeAttrs`, or `TemplateEdgeAttrs` | TypeBox schema for edge attributes | Common instantiations: ```typescript // Operation graph (static type compatibility) type OperationGraph = FlowGraph; // Call graph (dynamic call events) type CallGraph = FlowGraph; // Template DAG (workflow structure) type TemplateDAG = FlowGraph; ``` ## Constructor and Factories ### `new FlowGraph()` ```typescript constructor(options?: FlowGraphOptions) ``` Creates an empty graph. Options: ```typescript interface FlowGraphOptions { type?: "directed"; // Always "directed" (default) multi?: false; // Always false (default) — no parallel edges allowSelfLoops?: false; // Always false (default) — no self-loops } ``` The options object is passed through to `new DirectedGraph()`. The DAG constraints (`multi: false`, `allowSelfLoops: false`) are enforced at the graphology level. ### `FlowGraph.fromSpecs(specs)` ```typescript static fromSpecs(specs: OperationSpec[]): OperationGraph ``` Constructs an operation graph from an array of `OperationSpec` objects. Creates nodes for each operation and type-compatibility edges via `buildTypeEdges()`. Throws `CycleError` if the resulting graph has cycles (shouldn't happen with valid operation specs, but validated defensively). ### `FlowGraph.fromCallEvents(events)` ```typescript static fromCallEvents(events: CallEventMapValue[]): CallGraph ``` Constructs a call graph from an array of call protocol events. Processes events in order, adding nodes and edges. Idempotent — duplicate events have no effect. ### `FlowGraph.fromJSON(data)` ```typescript static fromJSON(data: FlowGraphSerialized): FlowGraph ``` Deserializes from graphology native JSON format. Validates against the appropriate schema (`OperationGraphSerialized` or `CallGraphSerialized`). Throws `InvalidInputError` on validation failure. After schema validation, `fromJSON()` validates DAG invariants by running cycle detection on the deserialized graph. If cycles are found, it throws `CycleError` with the cycle paths. This enforces ADR-002's DAG-only invariant even for externally-provided data — a corrupted or adversarial JSON input cannot produce a cyclic graph that would violate downstream assumptions (e.g., that `topologicalOrder()` always succeeds). Round-trip guarantee: `fromSpecs()` → `export()` → `fromJSON()` is lossless. ## Mutation Methods ### Node Mutations | Method | Signature | Behavior | |--------|-----------|----------| | `addNode` | `(key: string, attrs: NodeAttrs): void` | Adds a node. Throws `DuplicateNodeError` if key exists. | | `removeNode` | `(key: string): void` | Removes a node and all attached edges. Throws `NodeNotFoundError` if key doesn't exist. | | `updateNode` | `(key: string, attrs: Partial): void` | Merges attributes into an existing node. Throws `NodeNotFoundError` if key doesn't exist. | | `hasNode` | `(key: string): boolean` | Checks if a node exists. | | `getNodeAttributes` | `(key: string): NodeAttrs` | Returns node attributes. Throws `NodeNotFoundError` if key doesn't exist. | ### Edge Mutations | Method | Signature | Behavior | |--------|-----------|----------| | `addEdge` | `(source: string, target: string, attrs?: EdgeAttrs): void` | Adds a directed edge. Throws `NodeNotFoundError` if either endpoint doesn't exist. Throws `CycleError` if the edge would create a cycle. Throws `DuplicateEdgeError` if an edge already exists between the same (source, target). | | `removeEdge` | `(source: string, target: string): void` | Removes an edge. No-op if the edge doesn't exist. | | `hasEdge` | `(source: string, target: string): boolean` | Checks if an edge exists between source and target. | | `getEdgeAttributes` | `(source: string, target: string): EdgeAttrs` | Returns edge attributes. Throws if edge doesn't exist. | ### Call Graph Mutations These are convenience methods specific to `FlowGraph`: | Method | Signature | Behavior | |--------|-----------|----------| | `addCall` | `(attrs: CallNodeAttrs): void` | Adds a call node. If `attrs.parentRequestId` is set, also creates a `triggered` edge from parent to child. | | `addDependency` | `(source: string, target: string): void` | Creates a `depends_on` edge. Validates both endpoints exist and the edge wouldn't create a cycle. | | `updateStatus` | `(requestId: string, status: CallStatus, extra?: Partial): void` | Updates call status. Throws `InvalidTransitionError` on invalid transitions. | | `updateCall` | `(requestId: string, attrs: Partial): void` | Partial merge of call attributes. | | `removeCall` | `(requestId: string): void` | Removes a call node and all attached edges. | ### Operation Graph Mutations | Method | Signature | Behavior | |--------|-----------|----------| | `addOperation` | `(spec: OperationSpec): void` | Adds an operation node. Key is `${spec.namespace}.${spec.name}`. | | `addTypedEdge` | `(source: string, target: string, attrs: { compatible: boolean; detail?: string }): void` | Adds a type-compatibility edge with `edgeType: "typed"`. | ## Query Methods ### Graph Traversal These methods delegate directly to graphology and graphology-dag: | Method | Returns | Delegated To | |--------|---------|-------------| | `topologicalOrder()` | `string[]` | `graphology-dag.topologicalSort` | | `hasCycles()` | `boolean` | `graphology-dag.hasCycle` (always `false` after validated construction) | | `findCycles()` | `string[][]` | `graphology-dag.findCycle` (debugging) | | `ancestors(nodeId)` | `string[]` | `graphology-dag.ancestors` | | `descendants(nodeId)` | `string[]` | `graphology-dag.descendants` | | `predecessors(nodeId)` | `string[]` | `graph.inNeighbors` | | `successors(nodeId)` | `string[]` | `graph.outNeighbors` | | `reachableFrom(nodeIds)` | `Set` | Custom BFS/DFS traversal | ### Call Graph Queries | Method | Returns | Description | |--------|---------|-------------| | `filterByStatus(status)` | `string[]` | Node keys with the given status | | `getRoots()` | `string[]` | Top-level call nodes (no `parentRequestId`) | | `children(requestId)` | `string[]` | Direct children via `triggered` edges | | `duration(requestId)` | `number` | `completedAt - startedAt` in ms | | `lineage(requestId)` | `string[]` | Ancestor chain from root to this call | ## Serialization | Method | Signature | Description | |--------|-----------|-------------| | `export()` | `FlowGraphSerialized` | Returns graphology native JSON format | | `toJSON()` | `FlowGraphSerialized` | Alias for `export()` | | `toString()` | `string` | JSON.stringify of `export()` | ## Analysis Convenience Methods The `FlowGraph` class exposes convenience methods that delegate to standalone analysis functions: ```typescript class FlowGraph { // Delegates to analysis/topologicalOrder topologicalOrder(): string[] { return _topologicalOrder(this._graph); } // Delegates to analysis/hasCycles hasCycles(): boolean { return _hasCycles(this._graph); } // Delegates to analysis/validate validate(): AnyValidationError[] { return _validate(this._graph); } // Delegates to analysis/typeCompat typeCompat(sourceKey: string, targetKey: string): TypeCompatResult { const source = this.getNodeAttributes(sourceKey); const target = this.getNodeAttributes(targetKey); return _typeCompat(source.outputSchema, target.inputSchema); } } ``` Standalone functions are also available from `@alkdev/flowgraph/analysis`: ```typescript import { topologicalOrder, hasCycles, validateGraph, typeCompat } from "@alkdev/flowgraph/analysis"; ``` ## Delegation Model `FlowGraph` wraps a graphology `DirectedGraph` instance. It does NOT extend `DirectedGraph`: ```typescript class FlowGraph { private _graph: DirectedGraph; // Construction constructor(options?: FlowGraphOptions) { this._graph = new DirectedGraph({ type: "directed", multi: false, allowSelfLoops: false, }); } // Internal access for delegation get graph(): DirectedGraph { return this._graph; } } ``` ### What FlowGraph delegates The `FlowGraph` class exposes only a subset of graphology's API — the methods that are meaningful for an enforced-DAG graph: | Category | What FlowGraph does | What raw graphology provides | |----------|---------------------|------------------------------| | Node ops | `addNode`, `removeNode`, `hasNode`, `getNodeAttributes`, `updateNode` | Full node CRUD + attributes | | Edge ops | `addEdge`, `removeEdge`, `hasEdge`, `getEdgeAttributes` | Full edge CRUD + attributes | | Traversal | `topologicalOrder`, `ancestors`, `descendants`, `predecessors`, `successors` | All graphology traversal methods | | Queries | `filterByStatus`, `getRoots`, `children`, `duration`, `lineage` | N/A (flowgraph-specific) | | Analysis | `typeCompat`, `validate`, `hasCycles` | N/A (flowgraph-specific) | ### What FlowGraph does NOT expose Methods that would violate DAG invariants or are unnecessary for flowgraph's use cases: - `addUndirectedEdge` — not applicable, all edges are directed - `addEdgeWithKey` — edge keys are deterministic (`${source}->${target}`), not user-specified - `merge` / `mergeEdge` — graph merging is not a supported operation (rebuild instead) - `import` — use `FlowGraph.fromJSON()` which validates schema - Any `multi: true` or `allowSelfLoops: true` options ### Direct graphology access Consumers who need graphology's full API can access the underlying graph via `flowGraph.graph`: ```typescript const graph = flowGraph.graph; graph.forEachNode((node, attrs) => { console.log(node, attrs); }); ``` This is an escape hatch. Direct graph mutation bypasses flowgraph's validation (cycle detection, duplicate checks). Use with caution. ## Immutability Guarantees | Method | Mutates? | Returns | |--------|----------|---------| | `fromSpecs()` | Creates new graph | `OperationGraph` | | `fromCallEvents()` | Creates new graph | `CallGraph` | | `fromJSON()` | Creates new graph | `FlowGraph` | | `addNode`, `addEdge`, etc. | **Yes — mutates** | `void` | | `removeNode`, `removeEdge` | **Yes — mutates** | `void` | | `updateNode`, `updateStatus` | **Yes — mutates** | `void` | | `export()`, `toJSON()` | No — reads | Serialized data | | `topologicalOrder()`, `ancestors()`, etc. | No — reads | Query results | | `validate()`, `hasCycles()` | No — reads | Validation results | | `typeCompat()` | No — reads | `TypeCompatResult` | **Key invariant**: The operation graph produced by `fromSpecs()` is conventionally immutable — consumers should not call mutation methods on it after construction. The mutation methods (`addNode`, `addEdge`, etc.) are available for incremental construction via `new FlowGraph()` + `addOperation()`/`addTypedEdge()`. If the registry changes, rebuild the graph. The call graph produced by `fromCallEvents()` supports incremental mutation via `addCall`, `updateStatus`, and `addDependency`. The initial events populate the graph, and subsequent events update it. ## Exports Map | Sub-path | Key exports | |-----------|-------------| | `@alkdev/flowgraph` | `FlowGraph`, all public types | | `@alkdev/flowgraph/graph` | `FlowGraph`, `FlowGraphOptions` | | `@alkdev/flowgraph/analysis` | `typeCompat`, `buildTypeEdges`, `validateGraph`, `validateTemplate`, `topologicalOrder`, `parallelGroups`, `criticalPath`, `reachableFrom` | | `@alkdev/flowgraph/schema` | `OperationNodeAttrs`, `CallNodeAttrs`, `OperationEdgeAttrs`, `CallEdgeAttrs`, `TemplateEdgeAttrs`, `CallResultSchema`, `CallResult`, `CallStatus`, `NodeStatus`, `EdgeType` | | `@alkdev/flowgraph/component` | `Operation`, `Sequential`, `Parallel`, `Conditional`, `Map` | | `@alkdev/flowgraph/host` | `GraphologyHostConfig`, `ReactiveHostConfig` | | `@alkdev/flowgraph/reactive` | `WorkflowReactiveRoot`, `WorkflowNode`, `ReactiveContext`, `EventLogProjection` | | `@alkdev/flowgraph/error` | `FlowgraphError`, `ConstructionError`, `CycleError`, `ValidationError`, `TypeIncompatError`, `InvalidTransitionError` | ## Constraints - **FlowGraph wraps, not extends, graphology** — direct `DirectedGraph` access is available via `.graph` but bypasses validation. - **DAG invariants enforced at construction time** — `addEdge` throws `CycleError` if the edge would create a cycle. `hasCycles()` should always return `false` after validated construction. - **No parallel edges** — `addEdge` throws `DuplicateEdgeError` if an edge already exists between the same (source, target) pair. - **No self-loops** — enforced at the graphology level (`allowSelfLoops: false`). - **Edge keys are deterministic** — `${source}->${target}` format. No user-specified edge keys. - **Operation graph is conventionally immutable after construction** — `fromSpecs()` produces a graph that consumers should treat as immutable. Mutation methods (`addOperation`, `addTypedEdge`) are available for incremental construction but should not be used on factory-produced graphs. If the registry changes, rebuild the graph. - **Call graph supports incremental mutation** — `addCall`, `updateStatus`, `addDependency` are the primary mutation paths. ## Open Questions 1. ~~**Should `FlowGraph` expose graphology's traversal methods directly or only via convenience methods?**~~ **Resolved (OQ-017)**: Option (c) — expose the most common traversal methods directly on `FlowGraph`, let `.graph` handle the rest. The directly exposed methods are: `forEachNode()`, `forEachEdge()`, `nodes()`, `edges()`, `order`, `size`, `inNeighbors()`, `outNeighbors()` (already exposed as `predecessors()`/`successors()`). Less common methods (degree, detailed attribute iteration, adjacency queries) remain accessible via `flowGraph.graph`. This is the 80/20 approach: consumers get a clean API for common operations, and power users get the escape hatch. The convenience delegation pattern is maintained — `FlowGraph.forEachNode()` delegates to `this._graph.forEachNode()`. 2. ~~**Should the operation graph's `addTypedEdge` be auto-populated or manual?**~~ **Resolved (OQ-018)**: Manual — `addOperation()` adds a node only, and `buildTypeEdges()` must be called separately after incremental construction. Auto-population would require O(n) comparisons on every `addOperation()`, which adds complexity for a rare use case (the operation graph is typically built once via `fromSpecs()`). If incremental construction is needed, the consumer can call `buildTypeEdges()` manually after adding operations. 3. ~~**Should `FlowGraph` support multiple graph instances sharing analysis functions?**~~ **Resolved (OQ-028)**: No — each `FlowGraph` instance owns its own `DirectedGraph`. Analysis functions are stateless pure functions that take a graph as input; there's nothing to pool or share. The `FlowGraph` convenience methods delegate to these standalone functions. This question conflates "sharing analysis functions" (already done — `typeCompat` is a standalone function) with "sharing graph data" (unnecessary since analysis doesn't cache state). ## References - Schema: [schema.md](schema.md) — TypeBox schemas for all node/edge attribute types - Operation graph: [operation-graph.md](operation-graph.md) — Static graph construction and queries - Call graph: [call-graph.md](call-graph.md) — Dynamic graph from call events - Analysis: [analysis.md](analysis.md) — Type compatibility, validation, ordering - Error handling: [error-handling.md](error-handling.md) — Error hierarchy - Build & distribution: [build-distribution.md](build-distribution.md) — Exports map and package structure