Hex Version Hex Docs CI Coverage Status License

Domain-specific diagram builders and graph analyzers on top of Yog.

Choreo is a family of Elixir libraries that let you model, analyze, and render complex systems as graphs. Instead of drawing boxes and arrows by hand, you write code. Instead of static pictures, you get live analysis — reachability, cycles, bottlenecks, threat generation, and more.

alias Choreo.Dataflow

# A dataflow pipeline with one line of analysis
pipeline =
  Dataflow.new()
  |> Dataflow.add_source(:sensor)
  |> Dataflow.add_transform(:parse)
  |> Dataflow.add_sink(:db)
  |> Dataflow.connect(:sensor, :parse)
  |> Dataflow.connect(:parse, :db)

Dataflow.Analysis.cyclic?(pipeline)      #=> false
Dataflow.to_dot(pipeline)                #=> DOT string

Table of Contents


Why Choreo?

Most diagramming tools are visualization-only: you describe a picture, you get a picture.

Choreo is analysis-first: you describe a system, you get answers.

QuestionChoreo answer
"Can this state machine accept input X?"Choreo.FSM.Analysis.accepts?(fsm, ["X"])
"Is my pipeline cyclic?"Choreo.Dataflow.Analysis.cyclic?(pipeline)
"What breaks if I change :auth?"Choreo.Dependency.Analysis.affected_by(deps, :auth)
"What's the slowest path end-to-end?"Choreo.Dataflow.Analysis.longest_path(pipeline)
"Are there circular dependencies?"Choreo.Dependency.Analysis.cyclic_dependencies(deps)
"What threats exist in my architecture?"Choreo.ThreatModel.Analysis.stride_threats(model)
"Which feature drives the most splits?"Choreo.DecisionTree.Analysis.feature_importance(tree)
"How deep is this mind map?"Choreo.MindMap.Analysis.depth(map)
"Which ideas are orphaned?"Choreo.MindMap.Analysis.orphan_nodes(map)
"What's the shortest path from A to B?"Choreo.View.focus_between(map, :a, :b)
"Collapse these nodes into one?"Choreo.View.collapse(map, pred, :agg)

Everything renders to DOT (Graphviz) for publication-quality output with built-in :default, :dark, and custom themes.


Installation

Add choreo to your mix.exs:

def deps do
  [
    {:choreo, "~> 0.5"}
  ]
end

Modules

Choreo — Infrastructure Architecture

Model systems with typed infrastructure nodes: databases, caches, services, queues, load balancers, networks, users, and storage.

alias Choreo

system =
  Choreo.new()
  |> Choreo.add_database(:db, name: "Postgres", kind: :postgres)
  |> Choreo.add_cache(:cache, name: "Redis")
  |> Choreo.add_service(:api, name: "API Gateway")
  |> Choreo.connect(:api, :cache, cost: 5)
  |> Choreo.connect(:api, :db, cost: 10)

# Analysis
{:ok, mst} = Choreo.Analysis.mst(system)
{:ok, order} = Choreo.Analysis.topological_sort(system)

# Render
dot = Choreo.to_dot(system, theme: :dark)

Features: clusters with nesting, dataflow edges, cost-weighted edges, MST, topological sort, SCC, theming.

  digraph {
    rankdir=LR; splines=spline; nodesep=0.6; ranksep=1.0;
    node [shape=box, style=filled, fontname="Helvetica", fontsize=12];
    edge [fontname="Helvetica", fontsize=10, color="#64748b"];
    api [label="API Gateway", shape=box3d, fillcolor="#3b82f6", fontcolor=white];
    cache [label="Redis", shape=diamond, fillcolor="#f59e0b", fontcolor=white];
    db [label="Postgres", shape=cylinder, fillcolor="#10b981", fontcolor=white];
    api -> cache [label="cost: 5"];
    api -> db [label="cost: 10", headport=n];
  }

Choreo.FSM — Finite State Machines

Classic state machines with initial states, final states, and labeled transitions.

alias Choreo.FSM

fsm =
  FSM.new()
  |> FSM.add_initial_state(:idle)
  |> FSM.add_state(:running)
  |> FSM.add_final_state(:done)
  |> FSM.add_transition(:idle, :running, label: "start")
  |> FSM.add_transition(:running, :done, label: "finish")

# Analysis
FSM.Analysis.accepts?(fsm, ["start", "finish"])  #=> true
FSM.Analysis.deterministic?(fsm)                  #=> true
FSM.Analysis.shortest_accepting_path(fsm)         #=> {:ok, ["start", "finish"]}

# Transforms
pruned = FSM.prune(fsm)

Features: Deterministic execution, reachability, dead-state detection, determinism check, complement, product construction, equivalence checking.

digraph {
  rankdir=LR; splines=spline; nodesep=0.5; ranksep=1.0;
  node [shape=circle, style=filled, fontname="Helvetica", fontsize=12];
  edge [fontname="Helvetica", fontsize=10, color="#64748b"];
  idle [label="idle", fillcolor="#10b981", fontcolor=white];
  running [label="running", fillcolor="#e2e8f0"];
  done [label="done", shape=doublecircle, fillcolor="#e2e8f0", penwidth=2];
  __start_idle [shape=point, width=0.15, height=0.15, style=filled, fillcolor=black];
  __start_idle -> idle;
  idle -> running [label="start"];
  running -> done [label="finish"];
}

Choreo.Dataflow — Pipeline Diagrams

Model stream-processing and ETL pipelines. Nodes are sources, transforms, buffers, conditionals, merges, and sinks.

alias Choreo.Dataflow

pipeline =
  Dataflow.new()
  |> Dataflow.add_source(:sensor, label: "IoT Sensor", rate: 1000)
  |> Dataflow.add_transform(:parse, label: "JSON Parser", latency_ms: 50)
  |> Dataflow.add_buffer(:kafka, label: "Events", capacity: 10_000)
  |> Dataflow.add_sink(:db, label: "TimescaleDB")
  |> Dataflow.connect(:sensor, :parse, data_type: "raw bytes")
  |> Dataflow.connect(:parse, :kafka, data_type: "event")
  |> Dataflow.connect(:kafka, :db, data_type: "metrics")

# Analysis
Dataflow.Analysis.cyclic?(pipeline)           #=> false
{:ok, order} = Dataflow.Analysis.topological_sort(pipeline)
Dataflow.Analysis.orphan_nodes(pipeline)       #=> []
Dataflow.Analysis.bottlenecks(pipeline)        #=> [:kafka]
Dataflow.Analysis.simulate(pipeline)           #=> throughput map
{:ok, path, latency} = Dataflow.Analysis.longest_path(pipeline)

Features: error/retry/dead-letter path types, sub-pipeline clusters, throughput simulation, backpressure detection, critical-path analysis.

digraph {
  rankdir=LR; splines=spline; nodesep=0.6; ranksep=1.2;
  node [shape=box, style=filled, fontname="Helvetica", fontsize=12];
  edge [fontname="Helvetica", fontsize=10, color="#64748b"];
  sensor [label="IoT Sensor\n1000 evt/s", shape=house, fillcolor="#10b981", fontcolor=white];
  parse [label="JSON Parser", shape=box3d, fillcolor="#3b82f6", fontcolor=white];
  kafka [label="Events\n(cap: 10000)", shape=cylinder, fillcolor="#f59e0b", fontcolor=white];
  db [label="TimescaleDB", shape=invhouse, fillcolor="#f43f5e", fontcolor=white];
  sensor -> parse [label="raw bytes"];
  parse -> kafka [label="event"];
  kafka -> db [label="metrics"];
}

Choreo.Dependency — Software Dependency Graphs

Map modules, libraries, applications, interfaces, and tests. Detect circular dependencies, layering violations, and impact zones.

alias Choreo.Dependency

deps =
  Dependency.new()
  |> Dependency.add_application(:api, label: "API Gateway")
  |> Dependency.add_module(:auth, label: "Auth")
  |> Dependency.add_library(:phoenix)
  |> Dependency.depends_on(:api, :auth, type: :calls)
  |> Dependency.depends_on(:auth, :phoenix, type: :uses)

# Analysis
Dependency.Analysis.cyclic_dependencies(deps)     #=> []
Dependency.Analysis.affected_by(deps, :auth)       #=> [:api]
Dependency.Analysis.depends_on(deps, :api)         #=> [:auth, :phoenix]

# Layer enforcement
layers = %{repo: 1, service: 2, api: 3}
Dependency.Analysis.layer_violations(deps, layers)

Features: cycle path extraction (not just boolean), transitive impact analysis, layer violation detection, centrality ranking, longest dependency chain, cycle edge highlighting in DOT.

digraph {
  rankdir=TB; splines=spline; nodesep=0.5; ranksep=1.0;
  node [shape=box, style=filled, fontname="Helvetica", fontsize=12];
  edge [fontname="Helvetica", fontsize=9, color="#64748b"];
  api [label="API Gateway", shape=box3d, fillcolor="#3b82f6", fontcolor=white];
  auth [label="Auth", shape=box, fillcolor="#10b981", fontcolor=white];
  phoenix [label="Phoenix", shape=cylinder, fillcolor="#f59e0b", fontcolor=white];
  api -> auth [label="calls"];
  auth -> phoenix [label="uses", style=dashed];
}

Choreo.DecisionTree — Classification Trees

Build decision trees with enforced tree invariants (single root, single parent, no cycles).

alias Choreo.DecisionTree
alias Choreo.DecisionTree.Analysis

tree =
  DecisionTree.new()
  |> DecisionTree.set_root(:weather, feature: "weather")
  |> DecisionTree.add_decision(:wind, feature: "wind")
  |> DecisionTree.add_outcome(:play, label: "Play", class: "yes")
  |> DecisionTree.add_outcome(:stay, label: "Stay", class: "no")
  |> DecisionTree.branch(:weather, :wind, "cloudy")
  |> DecisionTree.branch(:weather, :play, "sunny")
  |> DecisionTree.branch(:wind, :play, "calm")
  |> DecisionTree.branch(:wind, :stay, "stormy")

# Evaluation
Analysis.decide(tree, %{"weather" => "cloudy", "wind" => "calm"})
#=> {:ok, [:weather, :wind, :play], "Play"}

# Metrics
Analysis.paths(tree)               #=> all root-to-leaf paths
Analysis.depth(tree)               #=> 2
Analysis.feature_importance(tree)  #=> %{"weather" => 1, "wind" => 1}

# Optimization
pruned = Analysis.prune_redundant(tree)

Features: exact-match decision evaluation, path enumeration with conditions, redundant-branch pruning, feature-importance counting, tree validation.

digraph {
  rankdir=TB; splines=spline; nodesep=0.7; ranksep=1.2;
  node [shape=box, style=filled, fontname="Helvetica", fontsize=12];
  edge [fontname="Helvetica", fontsize=10, color="#64748b"];
  weather [label="weather", shape=diamond, fillcolor="#8b5cf6", fontcolor=white, penwidth=2];
  wind [label="wind", shape=diamond, fillcolor="#3b82f6", fontcolor=white];
  play [label="Play", shape=box, style="rounded,filled", fillcolor="#10b981", fontcolor=white];
  stay [label="Stay", shape=box, style="rounded,filled", fillcolor="#10b981", fontcolor=white];
  weather -> wind [label="cloudy"];
  weather -> play [label="sunny"];
  wind -> play [label="calm"];
  wind -> stay [label="stormy"];
}

Choreo.Workflow — Task Orchestration

Model automated task orchestration with Saga-pattern compensations, timeouts, retries, and conditional branching.

alias Choreo.Workflow
alias Choreo.Workflow.Analysis

workflow =
  Workflow.new()
  |> Workflow.add_start(:order_received)
  |> Workflow.add_task(:charge_card, timeout_ms: 5000, retry: 3)
  |> Workflow.add_task(:reserve_inventory, timeout_ms: 3000)
  |> Workflow.add_decision(:sufficient_stock)
  |> Workflow.add_task(:pack_items, timeout_ms: 10_000)
  |> Workflow.add_task(:ship_order, timeout_ms: 5000)
  |> Workflow.add_compensation(:refund_payment, for: :charge_card)
  |> Workflow.add_end(:done)
  |> Workflow.connect(:order_received, :charge_card)
  |> Workflow.connect(:charge_card, :reserve_inventory)
  |> Workflow.connect(:reserve_inventory, :sufficient_stock)
  |> Workflow.connect(:sufficient_stock, :pack_items, condition: "yes")
  |> Workflow.connect(:sufficient_stock, :refund_payment, condition: "no", edge_type: :compensation)
  |> Workflow.connect(:pack_items, :ship_order)
  |> Workflow.connect(:ship_order, :done)

# Analysis
Analysis.critical_path(workflow)
#=> {:ok, [:order_received, :charge_card, :reserve_inventory, :sufficient_stock, :pack_items, :ship_order, :done], 23000}

Analysis.parallelizable_tasks(workflow)
Analysis.missing_compensations(workflow)
Analysis.validate(workflow)

Features: critical-path analysis with latency weights, parallelizable-task grouping, failure-scenario detection, missing-compensation detection, bottleneck detection, execution simulation.

digraph {
  rankdir=TB; splines=spline; nodesep=0.6; ranksep=1.2;
  node [shape=box, style=filled, fontname="Helvetica", fontsize=12];
  edge [fontname="Helvetica", fontsize=10, color="#64748b"];
  order_received [label="order_received", shape=circle, fillcolor="#10b981", fontcolor=white, penwidth=2];
  charge_card [label="charge_card\n(5000ms)\nretry: 3", shape=box3d, fillcolor="#3b82f6", fontcolor=white];
  reserve_inventory [label="reserve_inventory\n(3000ms)", shape=box3d, fillcolor="#3b82f6", fontcolor=white];
  sufficient_stock [label="sufficient_stock", shape=diamond, fillcolor="#8b5cf6", fontcolor=white];
  pack_items [label="pack_items\n(10000ms)", shape=box3d, fillcolor="#3b82f6", fontcolor=white];
  ship_order [label="ship_order\n(5000ms)", shape=box3d, fillcolor="#3b82f6", fontcolor=white];
  done [label="done", shape=doublecircle, fillcolor="#ef4444", fontcolor=white, penwidth=2];
  refund_payment [label="refund_payment", shape=note, fillcolor="#f87171", fontcolor=white, style="filled,dashed", color="#ef4444"];
  order_received -> charge_card;
  charge_card -> reserve_inventory;
  reserve_inventory -> sufficient_stock;
  sufficient_stock -> pack_items [label="yes"];
  sufficient_stock -> refund_payment [label="no", color="#ef4444", penwidth=1.5, style=dashed];
  pack_items -> ship_order;
  ship_order -> done;
}

Choreo.MindMap — Concept Mapping

Model hierarchical concept maps with a central root, branching topics and subtopics, and associative cross-links.

alias Choreo.MindMap
alias Choreo.MindMap.Analysis

map =
  MindMap.new()
  |> MindMap.set_root(:elixir, label: "Elixir")
  |> MindMap.add_topic(:concurrency, label: "Concurrency")
  |> MindMap.add_topic(:ecosystem, label: "Ecosystem")
  |> MindMap.add_subtopic(:processes, label: "Processes")
  |> MindMap.add_note(:beam, label: "BEAM VM")
  |> MindMap.branch(:elixir, :concurrency)
  |> MindMap.branch(:elixir, :ecosystem)
  |> MindMap.branch(:concurrency, :processes)
  |> MindMap.branch(:ecosystem, :beam)
  |> MindMap.associate(:processes, :beam, label: "runs on")

# Analysis
Analysis.depth(map)          #=> 2
Analysis.breadth(map)        #=> 2
Analysis.orphan_nodes(map)   #=> []
Analysis.paths(map)          #=> [[:elixir, :concurrency, :processes], [:elixir, :ecosystem, :beam]]
Analysis.validate(map)       #=> []

Features: single-root invariant, branch and associate edge types, depth/breadth/width metrics, root-to-leaf path enumeration, orphan detection, cycle detection, type-frequency analysis, validation.

digraph {
  rankdir=TB; splines=spline; nodesep=0.6; ranksep=1.2;
  node [shape=ellipse, style=filled, fontname="Helvetica", fontsize=12];
  edge [fontname="Helvetica", fontsize=10, color="#64748b"];
  elixir [label="Elixir", shape=doublecircle, fillcolor="#8b5cf6", fontcolor=white, penwidth=2];
  concurrency [label="Concurrency", shape=ellipse, fillcolor="#3b82f6", fontcolor=white];
  ecosystem [label="Ecosystem", shape=ellipse, fillcolor="#10b981", fontcolor=white];
  processes [label="Processes", shape=box, style="rounded,filled", fillcolor="#06b6d4", fontcolor=white];
  beam [label="BEAM VM", shape=note, fillcolor="#f59e0b", fontcolor=white];
  elixir -> concurrency;
  elixir -> ecosystem;
  concurrency -> processes;
  ecosystem -> beam;
  processes -> beam [label="runs on", style=dashed, color="#94a3b8", dir=none];
}

Choreo.ThreatModel — STRIDE Threat Modeling

Extend dataflow diagrams with security semantics. Auto-generate STRIDE threats based on element types, trust boundaries, and encryption status.

alias Choreo.ThreatModel
alias Choreo.ThreatModel.Analysis

model =
  ThreatModel.new()
  |> ThreatModel.add_trust_boundary("internet", level: 0)
  |> ThreatModel.add_trust_boundary("app", level: 2)
  |> ThreatModel.add_external_entity(:user, boundary: "internet")
  |> ThreatModel.add_process(:api, boundary: "app", privilege: :admin)
  |> ThreatModel.add_data_store(:db, boundary: "app", sensitivity: :confidential)
  |> ThreatModel.data_flow(:user, :api)
  |> ThreatModel.data_flow(:api, :db, encrypted: true)

# Auto-generated threats
threats = Analysis.stride_threats(model)
#=> [%{id: "T1", category: :spoofing, target: :user, severity: :high, ...}, ...]

# Security analysis
Analysis.exposed_data_stores(model)
Analysis.high_risk_processes(model)
Analysis.unencrypted_boundary_flows(model)

Features: automated STRIDE threat generation with severity scoring, trust-boundary crossing detection, exposed-data-store identification, high-risk process detection, encrypted-flow detection.

digraph {
  rankdir=LR; splines=spline; nodesep=0.6; ranksep=1.2;
  node [shape=box, style=filled, fontname="Helvetica", fontsize=12];
  edge [fontname="Helvetica", fontsize=10];
  user [label="User", shape=box, fillcolor="#64748b", fontcolor=white, penwidth=2];
  api [label="API\n(admin)", shape=circle, fillcolor="#3b82f6", fontcolor=white];
  db [label="DB\n[confidential]", shape=cylinder, fillcolor="#f59e0b", fontcolor=white];
  user -> api [color="#ef4444", penwidth=2, style=dashed];
  api -> db [color="#f59e0b", penwidth=1.5];
}

Themes & Rendering

All modules render to DOT (Graphviz) via a shared theming pipeline.

# Built-in themes
Choreo.to_dot(system, theme: :default)
Choreo.to_dot(system, theme: :dark)
Choreo.to_dot(system, theme: :minimal)

# Custom theme
theme = Choreo.Theme.custom(
  colors: %{database: "#ff0000", service: "#00ff00"},
  graph_bgcolor: "#0f172a",
  node_fontcolor: "white"
)
Choreo.to_dot(system, theme: theme)

Every module has type-specific shapes and colours:

ModuleNode typesShapes
Choreodatabase, cache, service, queue, ...cylinder, diamond, box3d, cloud, folder
Choreo.FSMinitial, normal, finalcircle, doublecircle
Choreo.Dataflowsource, sink, transform, buffer, conditional, mergehouse, invhouse, box3d, cylinder, diamond, trapezium
Choreo.Dependencyapplication, library, module, interface, testbox3d, cylinder, box, diamond, note
Choreo.DecisionTreeroot, decision, outcomediamond (double), diamond, rounded box
Choreo.MindMaproot, topic, subtopic, notedoublecircle, ellipse, rounded box, note
Choreo.ThreatModelexternal_entity, process, data_storebox (double), circle, cylinder
Choreo.Workflowstart, end, task, decision, fork, join, compensation, eventcircle, doublecircle, box3d, diamond, invhouse, house, note, cloud

Testing

mix test

All modules ship with comprehensive ExUnit test suites:

ModuleTests
Choreo32
Choreo.FSM40
Choreo.Dataflow44
Choreo.Dependency36
Choreo.DecisionTree34
Choreo.MindMap43
Choreo.ThreatModel33
Choreo.Workflow48
Total310

Roadmap

  • [x] Infrastructure architecture diagrams (Choreo)
  • [x] Finite state machines with analysis (Choreo.FSM)
  • [x] Dataflow / pipeline diagrams (Choreo.Dataflow)
  • [x] Software dependency graphs (Choreo.Dependency)
  • [x] Decision trees (Choreo.DecisionTree)
  • [x] STRIDE threat modeling (Choreo.ThreatModel)
  • [x] Task orchestration workflows (Choreo.Workflow)
  • [x] Mind maps (Choreo.MindMap)
  • [x] Schema validation for dataflow edges
  • [x] Custom theme presets and per-node style overrides
  • [ ] Deeper analysis: centrality metrics, graph colouring, cut vertices
  • [x] Cross-module composition (e.g. embed a Dataflow inside a Choreo cluster)
  • [x] Validation framework across all modules

License

MIT