Files
flowgraph/docs/architecture/flowgraph-api.md
glm-5.1 907c33650f fix: architecture review - address 5 critical issues, 6 warnings, 3 suggestions
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
2026-05-21 19:40:45 +00:00

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 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:

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 timeaddEdge throws CycleError if the edge would create a cycle. hasCycles() should always return false after validated construction.
  • No parallel edgesaddEdge 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 constructionfromSpecs() 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 mutationaddCall, 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