|
| 1 | +# M01: Graph Visualization |
| 2 | + |
| 3 | +**Status:** Not Started |
| 4 | +**Priority:** Medium |
| 5 | +**Effort:** 1 week |
| 6 | +**Dependencies:** None |
| 7 | + |
| 8 | +## Motivation |
| 9 | + |
| 10 | +Complex workflows with branches, parallel blocks, and compensation edges are hard to reason about from code alone. Graph visualization provides a structural view of workflow definitions and, with execution state overlay, lets developers debug running/failed workflows visually. This also feeds into M06 (Phoenix Dashboard) as the primary workflow visualization component. |
| 11 | + |
| 12 | +## Scope |
| 13 | + |
| 14 | +### In Scope |
| 15 | + |
| 16 | +- Graph data structure (`%Graph{nodes, edges}`) from workflow definitions |
| 17 | +- Builder that walks `Durable.Definition` structs |
| 18 | +- Export to DOT (Graphviz), Mermaid, and Cytoscape.js JSON formats |
| 19 | +- Execution state overlay (join graph nodes with `step_executions` data) |
| 20 | +- Support for all node types: step, decision, branch, parallel (fork/join), compensation |
| 21 | + |
| 22 | +### Out of Scope |
| 23 | + |
| 24 | +- Live/real-time graph updates (deferred to M05/M06 — needs message bus) |
| 25 | +- Visual layout engine (consumers handle layout — DOT/Mermaid/Cytoscape all have their own) |
| 26 | +- Phoenix LiveView rendering (M06) |
| 27 | +- WebSocket streaming (M06) |
| 28 | + |
| 29 | +## Architecture & Design |
| 30 | + |
| 31 | +### Module Layout |
| 32 | + |
| 33 | +``` |
| 34 | +lib/durable/graph.ex # Public API: generate/2, with_execution_state/3 |
| 35 | +lib/durable/graph/builder.ex # Walks Definition structs → %Graph{} |
| 36 | +lib/durable/graph/export/dot.ex # DOT format export |
| 37 | +lib/durable/graph/export/mermaid.ex # Mermaid format export |
| 38 | +lib/durable/graph/export/cytoscape.ex # Cytoscape.js JSON export |
| 39 | +``` |
| 40 | + |
| 41 | +### Data Structures |
| 42 | + |
| 43 | +```elixir |
| 44 | +defmodule Durable.Graph do |
| 45 | + defstruct [:workflow_name, :workflow_module, nodes: [], edges: []] |
| 46 | + |
| 47 | + defmodule Node do |
| 48 | + defstruct [:id, :type, :label, :metadata] |
| 49 | + # type: :start | :end | :step | :decision | :branch_fork | :branch_join |
| 50 | + # | :parallel_fork | :parallel_join | :compensation |
| 51 | + end |
| 52 | + |
| 53 | + defmodule Edge do |
| 54 | + defstruct [:from, :to, :label, :type] |
| 55 | + # type: :normal | :branch | :compensation | :parallel |
| 56 | + end |
| 57 | +end |
| 58 | +``` |
| 59 | + |
| 60 | +### Builder Logic |
| 61 | + |
| 62 | +The builder walks the list of `%Durable.Definition.Step{}` structs returned by `Module.__workflow_definition__/1`: |
| 63 | + |
| 64 | +1. Add `:start` node |
| 65 | +2. Iterate steps in order: |
| 66 | + - **Regular step** → add node + edge from previous |
| 67 | + - **Decision** → add diamond node, edges for each `{:goto, target}` |
| 68 | + - **Branch** → add fork node, walk each branch's steps, add join node |
| 69 | + - **Parallel** → add fork node, one edge per parallel step, add join node |
| 70 | + - **Step with `compensate:`** → add dashed compensation edge back to compensate target |
| 71 | +3. Add `:end` node |
| 72 | + |
| 73 | +### Execution State Overlay |
| 74 | + |
| 75 | +```elixir |
| 76 | +Durable.Graph.with_execution_state(graph, workflow_id, opts) |
| 77 | +``` |
| 78 | + |
| 79 | +Queries `step_executions` via `Durable.Query` and merges status/timing into node metadata: |
| 80 | + |
| 81 | +```elixir |
| 82 | +%Node{ |
| 83 | + id: :charge_payment, |
| 84 | + type: :step, |
| 85 | + metadata: %{ |
| 86 | + execution: %{ |
| 87 | + status: :completed, |
| 88 | + attempt: 2, |
| 89 | + duration_ms: 1234, |
| 90 | + started_at: ~U[...], |
| 91 | + completed_at: ~U[...] |
| 92 | + } |
| 93 | + } |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +### Export Format Details |
| 98 | + |
| 99 | +**DOT**: Standard Graphviz digraph. Nodes styled by type (diamonds for decisions, boxes for steps, etc.). Execution state colors: green=completed, blue=running, red=failed, gray=pending. |
| 100 | + |
| 101 | +**Mermaid**: `flowchart TD` format. Branch/parallel rendered as subgraphs. Compatible with GitHub markdown rendering. |
| 102 | + |
| 103 | +**Cytoscape.js**: JSON elements array with `{ data: { id, label, type, ... } }` for nodes and `{ data: { source, target, label } }` for edges. Ready for frontend consumption. |
| 104 | + |
| 105 | +## Implementation Plan |
| 106 | + |
| 107 | +1. **Graph struct & public API** — `lib/durable/graph.ex` |
| 108 | + - Define `%Graph{}`, `%Node{}`, `%Edge{}` structs |
| 109 | + - Public functions: `generate/2`, `with_execution_state/3` |
| 110 | + - Delegate to builder/query internally |
| 111 | + |
| 112 | +2. **Graph builder** — `lib/durable/graph/builder.ex` |
| 113 | + - `build/1` takes a workflow module, returns `%Graph{}` |
| 114 | + - Walk `__workflow_definition__/1` step list |
| 115 | + - Handle each step type (step, decision, branch, parallel, compensation) |
| 116 | + - Pure function — no side effects, no DB access |
| 117 | + |
| 118 | +3. **DOT exporter** — `lib/durable/graph/export/dot.ex` |
| 119 | + - `to_dot/1` takes `%Graph{}`, returns DOT string |
| 120 | + - Node shape mapping: step→box, decision→diamond, start/end→circle |
| 121 | + - Optional execution state coloring |
| 122 | + |
| 123 | +4. **Mermaid exporter** — `lib/durable/graph/export/mermaid.ex` |
| 124 | + - `to_mermaid/1` takes `%Graph{}`, returns Mermaid string |
| 125 | + - Use `flowchart TD` direction |
| 126 | + - Subgraphs for parallel blocks |
| 127 | + |
| 128 | +5. **Cytoscape exporter** — `lib/durable/graph/export/cytoscape.ex` |
| 129 | + - `to_cytoscape/1` takes `%Graph{}`, returns JSON-encodable map |
| 130 | + - Standard Cytoscape elements format |
| 131 | + |
| 132 | +6. **Execution overlay** — integrate into `lib/durable/graph.ex` |
| 133 | + - Query step executions for a workflow_id |
| 134 | + - Merge execution data into node metadata |
| 135 | + |
| 136 | +## Testing Strategy |
| 137 | + |
| 138 | +- `test/durable/graph/builder_test.exs` — test graph generation for each workflow pattern: |
| 139 | + - Linear steps |
| 140 | + - Decision with goto |
| 141 | + - Branch with multiple paths |
| 142 | + - Parallel block |
| 143 | + - Compensation edges |
| 144 | + - Mixed (branch + parallel + compensation) |
| 145 | +- `test/durable/graph/export/dot_test.exs` — validate DOT output format |
| 146 | +- `test/durable/graph/export/mermaid_test.exs` — validate Mermaid output format |
| 147 | +- `test/durable/graph/export/cytoscape_test.exs` — validate Cytoscape JSON structure |
| 148 | +- `test/durable/graph/execution_state_test.exs` — test overlay with DB fixtures (use DataCase) |
| 149 | +- Define test workflow modules in `test/support/` for predictable graph structures |
| 150 | + |
| 151 | +## Acceptance Criteria |
| 152 | + |
| 153 | +- [ ] `Durable.Graph.generate(MyWorkflow)` returns `%Graph{}` with correct nodes/edges |
| 154 | +- [ ] All step types produce expected graph structures (step, decision, branch, parallel, compensation) |
| 155 | +- [ ] DOT export produces valid Graphviz syntax (verify with `dot -Tsvg` if available) |
| 156 | +- [ ] Mermaid export produces valid Mermaid syntax |
| 157 | +- [ ] Cytoscape export produces valid JSON elements |
| 158 | +- [ ] Execution state overlay merges step status into node metadata |
| 159 | +- [ ] All exports handle execution state coloring/annotation |
| 160 | +- [ ] No duplicated nodes or edges for converging branches |
| 161 | +- [ ] `mix credo --strict` passes |
| 162 | +- [ ] All new tests pass |
| 163 | + |
| 164 | +## Open Questions |
| 165 | + |
| 166 | +- Should `generate/2` accept a workflow name filter for modules with multiple workflows? |
| 167 | +- Should execution overlay include step logs summary (log count, last error)? |
| 168 | +- Do we need a `to_ascii/1` exporter for terminal output in mix tasks (M02)? |
| 169 | + |
| 170 | +## References |
| 171 | + |
| 172 | +- `agents/arch.md` — "Graph Visualization" section for target API and data structures |
| 173 | +- `lib/durable/definition.ex` — `%Step{}` struct that builder walks |
| 174 | +- `lib/durable/query.ex` — query functions for execution state overlay |
| 175 | +- `lib/durable/executor.ex` — understanding of step type handling |
0 commit comments