Powered by AppSignal & Oban Pro

Choreo Requirement: Comprehensive Walkthrough

requirement_walkthrough.livemd

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.VizJS to render DOT diagrams inline. You can also copy DOT output into PlantText or run dot -Tpng diagram.dot -o diagram.png locally. 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:

  1. 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.
  2. Lab DSL (Choreo.Lab.DSL.Requirement): An expressive sketch syntax using requirements "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.