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
311 lines
17 KiB
Markdown
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 |