Files
flowgraph/docs/architecture/decisions/002-dag-only-graph.md
glm-5.1 c5e649cc9f resolve mechanical architecture review issues (C-01,C-02,C-03,W-01,W-09,W-10,W-12)
- C-01: fix broken README link (call-graph-runtime.md → call-graph.md)
- C-02: add CallEdgeAttrs union type alias in schema.md
- C-03/W-12: rename TypedEdgeAttrs → OperationEdgeAttrs for consistent
  {GraphType}EdgeAttrs naming pattern, update all references
- W-01: standardize terminology — prerequisites=structural/graph,
  preconditions=reactive/computed, rename WorkflowNode.prerequisites
  to preconditions, rename computePrerequisites to computePreconditions
- W-09: update ADR-001/002/003 status from Proposed to Accepted
- W-10: clarify call graph mutation API — addCall creates triggered
  edges automatically, addDependency creates depends_on edges
- update review checklist with resolved items
2026-05-19 11:09:06 +00:00

3.0 KiB

ADR-002: Enforce DAG Invariants (No Cycles)

Status

Accepted

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 simplertopologicalOrder() 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