Critical fixes: - C1: Create standalone ADR-006 file (edge type consistency), extract from open-questions.md inline content - C2: Convert CallResult from plain interface to TypeBox schema, aligning with 'TypeBox as single source of truth' constraint - C3: Add fromJSON() cycle detection specification - enforce ADR-002 DAG invariant even on deserialized input - C4: Rewrite consumer-integration.md Phase 4 to use ADR-005 event-append pattern instead of direct signal mutation - C5: Fix operator precedence bug in consumer-integration.md (missing parentheses around OR condition) Warnings addressed: - W1: Fix immutability claim - operation graph is 'conventionally immutable', not prevented by API - W2: Add EventLogProjection to reactive exports map - W3: Add CallResult/CallResultSchema to schema exports map - W4: Fix reactive-execution.md Level 1 error handling to use event-append pattern instead of direct signal mutation - W5: Remove duplicate dataFlow inference description in schema.md - W6: Clarify ADR-006 project context (flowgraph vs taskgraph) Suggestions implemented: - S1: Add 'reviewed' document lifecycle status between draft/stable, update all docs to reviewed status - S2: Add carve-out note for analysis result types in schema.md constraints (they are ephemeral, not serialized) - S3: Add isComplete() and getAggregateStatus() convenience methods to WorkflowReactiveRoot specification
17 KiB
status, last_updated
| status | last_updated |
|---|---|
| reviewed | 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
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:
// Operation graph (static type compatibility)
type OperationGraph = FlowGraph<OperationNodeAttrs, OperationEdgeAttrs>;
// Call graph (dynamic call events)
type CallGraph = FlowGraph<CallNodeAttrs, CallEdgeAttrs>;
// Template DAG (workflow structure)
type TemplateDAG = FlowGraph<TemplateNodeAttrs, TemplateEdgeAttrs>;
Constructor and Factories
new FlowGraph()
constructor(options?: FlowGraphOptions)
Creates an empty graph. Options:
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)
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)
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)
static fromJSON(data: FlowGraphSerialized<NodeAttrs, EdgeAttrs>): FlowGraph<NodeAttrs, EdgeAttrs>
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<NodeAttrs>): 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<CallNodeAttrs, CallEdgeAttrs>:
| 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<CallNodeAttrs>): void |
Updates call status. Throws InvalidTransitionError on invalid transitions. |
updateCall |
(requestId: string, attrs: Partial<CallNodeAttrs>): 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<string> |
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<NodeAttrs, EdgeAttrs> |
Returns graphology native JSON format |
toJSON() |
FlowGraphSerialized<NodeAttrs, EdgeAttrs> |
Alias for export() |
toString() |
string |
JSON.stringify of export() |
Analysis Convenience Methods
The FlowGraph class exposes convenience methods that delegate to standalone analysis functions:
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:
import { topologicalOrder, hasCycles, validateGraph, typeCompat } from "@alkdev/flowgraph/analysis";
Delegation Model
FlowGraph wraps a graphology DirectedGraph instance. It does NOT extend DirectedGraph:
class FlowGraph<NodeAttrs, EdgeAttrs> {
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 directedaddEdgeWithKey— edge keys are deterministic (${source}->${target}), not user-specifiedmerge/mergeEdge— graph merging is not a supported operation (rebuild instead)import— useFlowGraph.fromJSON()which validates schema- Any
multi: trueorallowSelfLoops: trueoptions
Direct graphology access
Consumers who need graphology's full API can access the underlying graph via flowGraph.graph:
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
DirectedGraphaccess is available via.graphbut bypasses validation. - DAG invariants enforced at construction time —
addEdgethrowsCycleErrorif the edge would create a cycle.hasCycles()should always returnfalseafter validated construction. - No parallel edges —
addEdgethrowsDuplicateEdgeErrorif 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,addDependencyare the primary mutation paths.
Open Questions
-
ShouldResolved (OQ-017): Option (c) — expose the most common traversal methods directly onFlowGraphexpose graphology's traversal methods directly or only via convenience methods?FlowGraph, let.graphhandle the rest. The directly exposed methods are:forEachNode(),forEachEdge(),nodes(),edges(),order,size,inNeighbors(),outNeighbors()(already exposed aspredecessors()/successors()). Less common methods (degree, detailed attribute iteration, adjacency queries) remain accessible viaflowGraph.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 tothis._graph.forEachNode(). -
Should the operation graph'sResolved (OQ-018): Manual —addTypedEdgebe auto-populated or manual?addOperation()adds a node only, andbuildTypeEdges()must be called separately after incremental construction. Auto-population would require O(n) comparisons on everyaddOperation(), which adds complexity for a rare use case (the operation graph is typically built once viafromSpecs()). If incremental construction is needed, the consumer can callbuildTypeEdges()manually after adding operations. -
ShouldResolved (OQ-028): No — eachFlowGraphsupport multiple graph instances sharing analysis functions?FlowGraphinstance owns its ownDirectedGraph. Analysis functions are stateless pure functions that take a graph as input; there's nothing to pool or share. TheFlowGraphconvenience methods delegate to these standalone functions. This question conflates "sharing analysis functions" (already done —typeCompatis a standalone function) with "sharing graph data" (unnecessary since analysis doesn't cache state).
References
- Schema: schema.md — TypeBox schemas for all node/edge attribute types
- Operation graph: operation-graph.md — Static graph construction and queries
- Call graph: call-graph.md — Dynamic graph from call events
- Analysis: analysis.md — Type compatibility, validation, ordering
- Error handling: error-handling.md — Error hierarchy
- Build & distribution: build-distribution.md — Exports map and package structure