Skip to content

code-shoily/choreo

Repository files navigation

Choreo

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.

Question Choreo 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)

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 =
  new()
  |> add_database(:db, name: "Postgres", kind: :postgres)
  |> add_cache(:cache, name: "Redis")
  |> add_service(:api, name: "API Gateway")
  |> connect(:api, :cache, cost: 5)
  |> connect(:api, :db, cost: 10)

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

# Render
dot = 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: NFA simulation via subset construction, 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.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:

Module Node types Shapes
Choreo database, cache, service, queue, ... cylinder, diamond, box3d, cloud, folder
Choreo.FSM initial, normal, final circle, doublecircle
Choreo.Dataflow source, sink, transform, buffer, conditional, merge house, invhouse, box3d, cylinder, diamond, trapezium
Choreo.Dependency application, library, module, interface, test box3d, cylinder, box, diamond, note
Choreo.DecisionTree root, decision, outcome diamond (double), diamond, rounded box
Choreo.ThreatModel external_entity, process, data_store box (double), circle, cylinder
Choreo.Workflow start, end, task, decision, fork, join, compensation, event circle, doublecircle, box3d, diamond, invhouse, house, note, cloud

Testing

mix test

All modules ship with comprehensive ExUnit test suites:

Module Tests
Choreo 32
Choreo.FSM 40
Choreo.Dataflow 44
Choreo.Dependency 36
Choreo.DecisionTree 34
Choreo.ThreatModel 33
Choreo.Workflow 48
Total 267

Roadmap

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

License

MIT

About

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

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages