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

311 lines
17 KiB
Markdown

---
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<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()`
```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<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:
```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<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`:
```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