Choreo Requirement: Comprehensive Walkthrough
Mix.install([
{:choreo, "~> 0.14.1"},
# {:choreo, path: Path.expand("../..", __DIR__), force: true},
{:kino_vizjs, "~> 0.9.0"}
])
Section
Rendering diagrams: This livebook uses
Kino.VizJSto render DOT diagrams inline. You can also copy DOT output into PlantText or rundot -Tpng diagram.dot -o diagram.pnglocally. Since version 0.8.0, Choreo also supports Mermaid.js output — paste it into GitHub, GitLab, Notion, or the Mermaid Live Editor.
What is Choreo.Requirement?
Choreo.Requirement models requirements traceability as a directed graph. Instead of a spreadsheet or a wiki page nobody reads, your requirements and their links to code, tests, and stakeholders live as version-controlled, analysable data.
Common use cases:
- Track which features are implemented and which are still open
- Find test coverage gaps before a release
- Propagate risk from business-level requirements down to individual components
- Perform impact analysis before refactoring a service
Node Types
| Type | Shape | Purpose |
|---|---|---|
:requirement |
box (amber) | A need, feature, or constraint |
:component |
rounded box (blue) | A service, module, or library |
:test |
stadium (green) | A test suite or verification activity |
:stakeholder |
circle (purple) | A person, team, or requirements source |
Edge (Relationship) Types
| Type | Meaning |
|---|---|
:satisfies |
Component fulfills a requirement |
:verifies |
Test proves a requirement |
:refines |
Child requirement elaborates a parent |
:depends |
Requirement needs another requirement first |
:contains |
Parent requirement contains a child (structural grouping) |
:derives |
Requirement is derived from another |
:traces |
Generic traceability link |
API Approaches: Programmatic Pipe API vs Lab DSL
Choreo provides two complementary approaches to modeling requirements traceability:
- Programmatic Pipe API (
Choreo.Requirement): The canonical, explicit interface (Requirement.new() |> Requirement.add_requirement(...) |> Requirement.satisfies(...)). Ideal for CI/CD audit pipelines, programmatic model generation, and strict traceability checks. - Lab DSL (
Choreo.Lab.DSL.Requirement): An expressive sketch syntax usingrequirements "Name" do ... end, semantic requirement constructors (functional,performance,interface_requirement,design_constraint), actor/component constructors (component,test_case,stakeholder), direct connections (~>), typed edge verbs (satisfies,verifies,refines,depends,contains), and pipe modifiers (|> satisfies("implements")). Ideal for Livebooks, architectural documentation, and rapid requirement sketching.
The Legend below demonstrates the canonical programmatic pipe API, while subsequent examples showcase the concise Lab DSL.
import Choreo.Lab.DSL.Requirement
alias Choreo.Requirement
alias Choreo.Requirement.Analysis
legend =
Requirement.new("Node Type Legend")
|> Requirement.add_requirement(:req, id: "R0", text: "Example requirement", risk: :high)
|> Requirement.add_component(:svc, label: "Example Service")
|> Requirement.add_test(:suite, label: "Example Test Suite")
|> Requirement.add_stakeholder(:team, label: "Example Team")
|> Requirement.satisfies(:svc, :req)
|> Requirement.verifies(:suite, :req)
Kino.Layout.tabs(
Siren: Choreo.Lab.Siren.new(Requirement.to_mermaid(legend)),
Graphviz: Kino.VizJS.render(Requirement.to_dot(legend)),
Sketch: Choreo.Lab.Sketch.new(Requirement.to_mermaid(legend))
)
Example 1: Authentication Feature — The Minimum Useful Model
A product manager says "users must log in with MFA". Let's capture that requirement, the service that implements it, and the test that verifies it.
auth_model =
requirements "Auth v2" do
mfa = functional("Users must authenticate with MFA", id: "REQ-001", risk: :high, verification: :test)
session_timeout = functional("Sessions expire after 30 minutes of inactivity", id: "REQ-002", risk: :medium, verification: :test)
auth_service = component("Auth Service")
session_manager = component("Session Manager")
mfa_integration_test = test_case("MFA Integration Test")
session_expiry_test = test_case("Session Expiry Test")
security_team = stakeholder("Security Team")
# Who owns these requirements?
security_team ~> mfa |> traces()
security_team ~> session_timeout |> traces()
# What implements them?
auth_service ~> mfa |> satisfies()
session_manager ~> session_timeout |> satisfies()
# What verifies them?
mfa_integration_test ~> mfa |> verifies()
session_expiry_test ~> session_timeout |> verifies()
end
Kino.Layout.tabs(
Siren: Choreo.Lab.Siren.new(Requirement.to_mermaid(auth_model)),
Graphviz: Kino.VizJS.render(Requirement.to_dot(auth_model)),
Sketch: Choreo.Lab.Sketch.new(Requirement.to_mermaid(auth_model))
)
Color coding in the DOT diagram:
- Amber boxes — requirements, colored by risk level (green → yellow → orange → red)
- Blue rounded boxes — components / services
- Green stadiums — tests
- Purple circles — stakeholders
Coverage Check
Which requirements are fully covered (satisfied AND verified)?
cov = Analysis.coverage(auth_model)
IO.puts("Total requirements: #{cov.total}")
IO.puts("Satisfied: #{inspect(cov.satisfied)}")
IO.puts("Verified: #{inspect(cov.verified)}")
IO.puts("Orphaned: #{inspect(cov.orphan)}")
IO.puts("Satisfy ratio: #{Float.round(cov.ratios.satisfied * 100, 1)}%")
IO.puts("Verify ratio: #{Float.round(cov.ratios.verified * 100, 1)}%")
Traceability Matrix
Who implements what, and what tests cover which requirements?
matrix = Analysis.traceability_matrix(auth_model)
Enum.each(matrix, fn {req_id, links} ->
IO.puts("\n#{req_id}:")
IO.puts(" Components: #{inspect(Map.get(links, :components, []))}")
IO.puts(" Tests: #{inspect(Map.get(links, :tests, []))}")
end)
Example 2: Requirement Hierarchy — Breaking Down a Feature
Large requirements need decomposition. Use :refines or :contains to build a hierarchy. Risk propagates automatically from parent to child.
checkout_model =
requirements "Checkout v3" do
# High-level business requirement
checkout = functional("Users can purchase items in a single checkout flow", id: "BIZ-001", risk: :high)
# Functional sub-requirements
cart_validation = functional("Cart contents are validated before payment", id: "REQ-010", risk: :medium)
payment_processing = interface_requirement("Payment is processed via PCI-compliant gateway", id: "REQ-011", risk: :critical)
order_confirmation = performance("Customer receives a confirmation email within 60 seconds", id: "REQ-012", risk: :low)
# Dependency: payment can only happen after validation
stock_reservation = functional("Inventory is reserved for 15 minutes after add-to-cart", id: "REQ-013", risk: :high)
# Structural relationships
cart_validation ~> checkout |> refines()
payment_processing ~> checkout |> refines()
order_confirmation ~> checkout |> refines()
payment_processing ~> cart_validation |> depends()
payment_processing ~> stock_reservation |> depends()
# Components
cart_service = component("Cart Service")
payment_gateway = component("Payment Gateway")
email_service = component("Email Service")
inventory_service = component("Inventory Service")
cart_service ~> cart_validation |> satisfies()
payment_gateway ~> payment_processing |> satisfies()
email_service ~> order_confirmation |> satisfies()
inventory_service ~> stock_reservation |> satisfies()
# Tests
cart_test = test_case("Cart Validation Test")
payment_test = test_case("Payment Integration Test")
email_test = test_case("Email Delivery Test")
cart_test ~> cart_validation |> verifies()
payment_test ~> payment_processing |> verifies()
email_test ~> order_confirmation |> verifies()
end
Kino.Layout.tabs(
Siren: Choreo.Lab.Siren.new(Requirement.to_mermaid(checkout_model), height: "900px"),
Graphviz: Kino.VizJS.render(Requirement.to_dot(checkout_model), height: "900px"),
Sketch: Choreo.Lab.Sketch.new(Requirement.to_mermaid(checkout_model))
)
Risk Propagation
When a parent requirement is high-risk, its children inherit that risk level. The propagation rules:
:refines— child inherits parent risk if parent risk is higher:contains— contained child inherits container risk:derives— derived requirement inherits source risk
propagated = Analysis.risk_propagation(checkout_model)
propagated
|> Enum.sort_by(fn {_id, risk} -> risk end)
|> Enum.each(fn {id, risk} ->
data = Requirement.node(checkout_model, id)
original = data[:risk]
flag = if original != risk, do: " ← propagated from parent", else: ""
IO.puts("#{id}: #{original} → #{risk}#{flag}")
end)
Unmitigated Risks
High or critical risk requirements with no child refinements haven't been decomposed into manageable pieces. These are your architectural focus areas.
Analysis.unmitigated_risks(checkout_model)
|> IO.inspect(label: "Unmitigated high/critical risks")
High-Risk Gaps
Which high-risk requirements are neither satisfied by a component nor verified by a test?
Analysis.high_risk_gaps(checkout_model)
|> IO.inspect(label: "High-risk requirements with no implementation or test")
Example 3: Impact Analysis — What If We Change a Service?
Before refactoring the payment gateway, you want to know everything it touches.
Analysis.impact_of(checkout_model, :payment_gateway)
|> IO.inspect(label: "Nodes affected by changing :payment_gateway")
And from the requirement side — which components and tests are linked to the payment requirement?
Analysis.components_for(checkout_model, :payment_processing)
|> IO.inspect(label: "Components satisfying REQ-011")
Analysis.requirements_for(checkout_model, :payment_gateway)
|> IO.inspect(label: "Requirements that :payment_gateway touches")
Example 4: Finding Coverage Gaps
Let's model a system deliberately missing some test coverage and implementation to see the gap analysis tools in action.
gappy_model =
requirements "Gappy System" do
r_auth = functional("All API endpoints require authentication", id: "SEC-001", risk: :critical, verification: :test)
r_rate_limit = functional("Rate limiting is enforced at the gateway", id: "SEC-002", risk: :high, verification: :analysis)
r_audit_log = functional("All write operations are audit-logged", id: "SEC-003", risk: :high, verification: :inspection)
r_docs = requirement("API documentation is auto-generated from source", id: "SEC-004", risk: :low)
# Only auth is implemented and tested
api_gateway = component("API Gateway")
auth_middleware = component("Auth Middleware")
auth_test = test_case("Auth Middleware Test")
auth_middleware ~> r_auth |> satisfies()
api_gateway ~> r_rate_limit |> satisfies()
auth_test ~> r_auth |> verifies()
# r_audit_log and r_docs have no satisfies/verifies edges
end
Kino.Layout.tabs(
Siren: Choreo.Lab.Siren.new(Requirement.to_mermaid(gappy_model)),
Graphviz: Kino.VizJS.render(Requirement.to_dot(gappy_model)),
Sketch: Choreo.Lab.Sketch.new(Requirement.to_mermaid(gappy_model))
)
IO.puts("=== Unsatisfied (no component) ===")
Analysis.unsatisfied(gappy_model)
|> Enum.each(fn id ->
data = Requirement.node(gappy_model, id)
IO.puts(" #{data[:id]} — #{data[:text]}")
end)
IO.puts("\n=== Unverified (no test) ===")
Analysis.unverified(gappy_model)
|> Enum.each(fn id ->
data = Requirement.node(gappy_model, id)
IO.puts(" #{data[:id]} — #{data[:text]}")
end)
IO.puts("\n=== Orphan (no edges at all) ===")
Analysis.orphan_requirements(gappy_model)
|> Enum.each(fn id ->
data = Requirement.node(gappy_model, id)
IO.puts(" #{data[:id]} — #{data[:text]}")
end)
IO.puts("\n=== High-risk gaps ===")
Analysis.high_risk_gaps(gappy_model)
|> Enum.each(fn id ->
data = Requirement.node(gappy_model, id)
IO.puts(" ⚠️ #{data[:id]} [#{data[:risk]}] — #{data[:text]}")
end)
Example 5: Detecting Circular Dependencies
Circular requirement dependencies (A depends on B depends on A) are a design smell. Choreo detects them using a Tarjan SCC pass over the requirement-only subgraph.
circular_model =
requirements "Circular Example" do
a = requirement("Requirement A", id: "A", risk: :medium)
b = requirement("Requirement B", id: "B", risk: :medium)
c = requirement("Requirement C", id: "C", risk: :low)
# A depends on B, B depends on C, C depends back on A — a cycle
a ~> b |> depends()
b ~> c |> depends()
c ~> a |> depends()
end
Analysis.circular_dependencies(circular_model)
|> Enum.each(fn cycle ->
IO.puts("Cycle: #{Enum.join(cycle, " → ")}")
end)
Now with a clean model — no cycles expected:
Analysis.circular_dependencies(checkout_model)
|> IO.inspect(label: "Cycles in checkout_model (expect [])")
Example 6: Validation as a Checklist
Analysis.validate/1 runs all structural checks in one call and returns structured diagnostics.
incomplete =
requirements "Incomplete Model" do
# Missing :id and :text — these should error
bad_req = requirement("", node_id: :bad_req, id: "", risk: :high)
# Component with no label
component("", node_id: :unlabelled)
# High-risk, unimplemented
critical_gap =
requirement("This requirement has no component or test",
node_id: :critical_gap,
id: "GAP-001",
risk: :critical
)
end
Analysis.validate_messages(incomplete)
|> Enum.each(fn {t, msg} -> IO.puts("[#{t}] #{msg}") end)
Fix the model and validate again:
fixed =
requirements "Fixed Model" do
auth = functional("Users must authenticate", id: "REQ-001", risk: :high, verification: :test)
auth_service = component("Auth Service")
auth_test = test_case("Auth Test")
auth_service ~> auth |> satisfies()
auth_test ~> auth |> verifies()
end
IO.puts("Validation issues (expect none):")
Analysis.validate(fixed) |> IO.inspect()
Example 7: Requirements Across Multiple Kinds
Mermaid's requirementDiagram supports six requirement kinds. Here's all of them in one model.
all_kinds =
requirements "All Requirement Kinds" do
func = functional("System processes orders", id: "F-001", risk: :medium)
iface = interface_requirement("REST API follows OpenAPI 3.1 spec", id: "I-001", risk: :low)
perf = performance("p99 response time under 200ms", id: "P-001", risk: :high)
phys = physical("Service runs on a single t3.small instance", id: "PH-001", risk: :low)
design = design_constraint("All state is stored in Postgres; no in-memory state", id: "DC-001", risk: :medium)
generic = requirement("System complies with GDPR", id: "G-001", risk: :critical)
end
Kino.Layout.tabs(
Siren: Choreo.Lab.Siren.new(Requirement.to_mermaid(all_kinds)),
Graphviz: Kino.VizJS.render(Requirement.to_dot(all_kinds)),
Sketch: Choreo.Lab.Sketch.new(Requirement.to_mermaid(all_kinds))
)
Advanced: Custom Theming
The DOT renderer supports built-in themes: :default, :dark, :warm, :forest, :ocean.
Kino.Layout.tabs(
Default: Kino.VizJS.render(Requirement.to_dot(checkout_model, theme: :default)),
Dark: Kino.VizJS.render(Requirement.to_dot(checkout_model, theme: :dark)),
Warm: Kino.VizJS.render(Requirement.to_dot(checkout_model, theme: :warm)),
Forest: Kino.VizJS.render(Requirement.to_dot(checkout_model, theme: :forest)),
Ocean: Kino.VizJS.render(Requirement.to_dot(checkout_model, theme: :ocean))
)
Advanced: Zoom Levels via Choreo.View
Choreo.Viewable lets you filter down to subsets of the diagram at different zoom levels.
| Level | Nodes shown |
|---|---|
| 0 | Top-level requirements only (no :refines parent) |
| 1 | All requirements |
| 2 | Requirements + components + tests + stakeholders |
| 3+ | Everything |
# Build a rich model with hierarchy
zoomy =
requirements "Zoom Demo" do
epic = requirement("User account management", id: "E-001", risk: :high)
login = requirement("Users can log in", id: "R-001", risk: :medium)
logout = requirement("Users can log out", id: "R-002", risk: :low)
auth_svc = component("Auth Service")
login_test = test_case("Login Test")
login ~> epic |> refines()
logout ~> epic |> refines()
auth_svc ~> login |> satisfies()
login_test ~> login |> verifies()
end
Kino.Layout.tabs(
"Zoom 0 (top-level reqs)":
Kino.VizJS.render(Choreo.View.zoom(zoomy, level: 0) |> Requirement.to_dot()),
"Zoom 1 (all reqs)":
Kino.VizJS.render(Choreo.View.zoom(zoomy, level: 1) |> Requirement.to_dot()),
"Zoom 2 (full)":
Kino.VizJS.render(Choreo.View.zoom(zoomy, level: 2) |> Requirement.to_dot())
)
Cheat Sheet
Lab DSL Syntax (Choreo.Lab.DSL.Requirement)
| Syntax | Description |
|---|---|
requirements "Title" do ... end |
Define a requirements model block |
req = requirement("Text", id: "REQ-001", risk: :high) |
Generic requirement node |
f = functional("Text", id: "F-001") |
Functional requirement node |
i = interface_requirement("Text", id: "I-001") |
Interface requirement node |
p = performance("Text", id: "P-001") |
Performance requirement node |
ph = physical("Text", id: "PH-001") |
Physical requirement node |
dc = design_constraint("Text", id: "DC-001") |
Design constraint requirement node |
svc = component("Label") |
System component node |
tc = test_case("Label") |
Test case verification node |
sh = stakeholder("Label") |
Stakeholder node |
| `svc ~> req | > satisfies()/satisfies("implements")` |
| `test ~> req | > verifies()/verifies("e2e test")` |
| `child ~> parent | > refines()/refines("decomposes")` |
| `a ~> b | > depends()/depends("blocks")` |
| `a ~> b | > contains()/contains("subpart")` |
| `a ~> b | > derives()/derives("derived from")` |
| `a ~> b | > traces()/traces("traced to")` |
| `a ~> b | > on("label", type: :satisfies)` |
| `a ~> b | > docref("SPEC-12")` |
edge a ~> b, "label", type: :satisfies, docref: "..." |
Explicit edge statement with label and options |
edge a ~> b, type: :verifies |
Explicit edge statement with options |
Programmatic Pipe API & Analysis (Choreo.Requirement)
| Task / Feature | Command |
|---|---|
| Create Model | Requirement.new/1 |
| Add Requirements | Requirement.add_requirement/3 with kind: :requirement | :functional | :interface | :performance | :physical | :design_constraint |
| Add Structural Nodes | Requirement.add_component/3, add_test/3, add_stakeholder/3 |
| Connect Relationships | Requirement.satisfies/4, verifies/4, refines/4, depends/4, contains/4, derives/4, traces/4, relate/4 |
| Traceability & Coverage | Requirement.Analysis.coverage/1, traceability_matrix/1, unsatisfied/1, unverified/1, orphan_requirements/1 |
| Impact & Tracing | Requirement.Analysis.impact_of/2, components_for/2, requirements_for/2 |
| Risk Analysis | Requirement.Analysis.high_risk_gaps/1, unmitigated_risks/1, risk_propagation/1 |
| Integrity Checks | Requirement.Analysis.circular_dependencies/1, validate/1, validate_messages/1 |
| Render Mermaid | Requirement.to_mermaid/2 (Supports requirementDiagram) |
| Render DOT Graphviz | Requirement.to_dot/2 (Opts: :theme, :highlighted_nodes, :highlighted_edges) |
| Themes | Requirement.theme/2 (:default, :dark, :warm, :forest, :ocean) |
| Lens Views | Choreo.View.focus/3, Choreo.View.filter/3, Choreo.View.zoom/2 |
Summary
| Question | Function |
|---|---|
| "Which requirements have no implementation?" | Analysis.unsatisfied/1 |
| "Which requirements have no test?" | Analysis.unverified/1 |
| "Which requirements are completely isolated?" | Analysis.orphan_requirements/1 |
| "Show me the full coverage picture" | Analysis.coverage/1 |
| "What does requirement X link to?" | Analysis.traceability_matrix/1 |
| "Which components/tests relate to X?" | Analysis.components_for/2, requirements_for/2 |
| "If I change component Y, what breaks?" | Analysis.impact_of/2 |
| "Which high-risk reqs are unaddressed?" | Analysis.high_risk_gaps/1 |
| "What risk do child requirements inherit?" | Analysis.risk_propagation/1 |
| "Which high-risk reqs are undecomposed?" | Analysis.unmitigated_risks/1 |
| "Are there circular dependencies?" | Analysis.circular_dependencies/1 |
| "Is the model structurally sound?" | Analysis.validate/1, validate_messages/1 |
| "Render to DOT (Graphviz)" | Requirement.to_dot/2 |
| "Render to Mermaid" | Requirement.to_mermaid/2 |
Requirements as code means your traceability matrix is always in sync with your codebase, reviewed in pull requests, and queryable at runtime. When a requirement changes, you can immediately see which components and tests are affected — before anyone ships to production.