Resolve all 19 remaining open questions across the architecture. Every question now has a documented resolution with rationale: - OQ-004/OQ-029: edgeType is a universal required attribute on all edges, single graph per FlowGraph instance (ADR-006) - OQ-011: No OR preconditions for v1; preconditionMode as v2 extension - OQ-012: maxConcurrency enforced via reactive counting semaphore - OQ-014: Unknown operationId creates node with pending status - OQ-017: Expose common graphology traversal methods on FlowGraph (80/20) - OQ-020: condition as Type.Unknown() with string/function documentation - OQ-022: Identity imported from @alkdev/operations peer dep - All other questions resolved with documented rationale Fix three critical issues found by architecture review: 1. edgeType serialization/validation gap: document two-step validation 2. CallEdgeAttrs runtime discrimination: edgeType as runtime discriminant, depends_on edges clarified as observability-only (not execution) 3. ADR-005 signal mutation inconsistency: explicitly distinguish call-level statuses (event-log-driven) from workflow-derived statuses (signal-mutation) Additional clarifications: - dataFlow inference uses conservative strategy (defaults false) - Conditional.test string resolution: operationName → status === completed - Add negated field to TemplateEdgeAttrs for else-branch conditions - Document edge key priority convention for composite keys - Add maxConcurrency semaphore design to reactive-execution.md
16 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 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.
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 immutable after construction — no mutation methods are exposed. 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, CallStatus, NodeStatus, EdgeType |
@alkdev/flowgraph/component |
Operation, Sequential, Parallel, Conditional, Map |
@alkdev/flowgraph/host |
GraphologyHostConfig, ReactiveHostConfig |
@alkdev/flowgraph/reactive |
WorkflowReactiveRoot, WorkflowNode, ReactiveContext |
@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 immutable after construction — no mutation methods are exposed after
fromSpecs(). 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