add flowgraph architecture docs (Phase 1 SDD)

Draft architecture specification for @alkdev/flowgraph — a workflow graph library providing DAG-based orchestration over operations. Covers two graph types (operation graph, call graph), ujsx workflow templates, GraphologyHost and ReactiveHost configs, signal-driven execution, type-compatibility analysis, error hierarchy, and build/distribution. Includes 3 ADRs: ujsx as template IR, DAG-only enforcement, decoupled storage.
This commit is contained in:
2026-05-19 09:36:22 +00:00
parent 333dcd5ac1
commit d2253099ee
13 changed files with 2863 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
# ADR-002: Enforce DAG Invariants (No Cycles)
## Status
Proposed
## Context
Flowgraph represents two types of graphs: operation graphs (static type compatibility) and call graphs (dynamic call hierarchy). Both are directed acyclic graphs (DAGs) by nature:
- **Operation graphs** — type flow is acyclic. An operation's output feeding back into its own input is a design error.
- **Call graphs** — execution order is acyclic. A call being its own ancestor is physically impossible (you can't trigger yourself before you start).
- **Workflow templates** — rendered templates must be DAGs. Cycles in a template mean infinite loops in execution.
Taskgraph, the sibling package, allows cycles in its graph and detects them via `hasCycles()` and `findCycles()`. This makes sense because task dependencies can form cycles (e.g., iterative refinement where task A depends on task B which depends on task A's revised output).
## Decision
Flowgraph enforces acyclicity at construction time. Adding an edge that would create a cycle throws `CycleError`. `topologicalOrder()` can always produce a valid ordering without needing a cycle check first.
This is a stricter invariant than taskgraph's approach. The rationale:
1. **Cycles in operation graphs are design errors** — if operation A's output type is compatible with operation B's input, and B's output is compatible with A's input, that's circular type flow. It means infinite recursion is possible.
2. **Cycles in call graphs are physically impossible** — a call cannot be its own ancestor. The call protocol ensures this via `parentRequestId` chains.
3. **Cycles in templates are execution errors** — a cycle in a `<Sequential>` chain means infinite execution. This should be caught at template validation time, not at runtime.
4. **DAG algorithms are simpler**`topologicalOrder()` can always return a valid ordering. No need for `hasCycles()` + fallback path. `parallelGroups()` always produces a valid grouping. `reachableFrom()` never loops.
## Consequences
- **`addEdge()` validates before adding** — if adding the edge would create a cycle, it throws `CycleError` with the cycle paths.
- **`fromSpecs()` and `fromCallEvents()` cannot produce cyclic graphs** — cycles in the input data throw errors.
- **`topologicalOrder()` never throws** — it can always produce a valid ordering because the graph is guaranteed acyclic.
- **`hasCycles()` always returns `false`** — kept as a validation method for graphs loaded via `fromJSON()` (which doesn't enforce acyclicity during import).
- **This is different from taskgraph** — consumers familiar with taskgraph's `hasCycles()``findCycles()``topologicalOrder()` error-handling pattern need to adjust. In flowgraph, cycle prevention is at construction time, not query time.
## References
- Taskgraph cycle handling: `@alkdev/taskgraph_ts/docs/architecture/graph-model.md`
- Operation graph: [operation-graph.md](../operation-graph.md)
- Call graph: [call-graph.md](../call-graph.md)
- Error handling: [error-handling.md](../error-handling.md)