Skip to content
411 changes: 411 additions & 0 deletions .formatter.exs

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,34 @@ Keep `CHANGELOG.md` up to date. Use the `[Unreleased]` section at the top with t

Group related bullets. Mention new modules, significant behavior changes, new guides/livebooks, and new skills.

## API and DSL Philosophy

Choreo's stable API should remain pipe-first and explicit. Builders such as
`Choreo.C4.add_container/3`, `Choreo.Dataflow.add_source/3`, and
`Choreo.Infrastructure.connect/4` are the canonical programmatic interface.

Macro DSLs are valuable for sketches, examples, tests, and Livebook ergonomics, but they should incubate under
`Choreo.Lab.DSL.*` unless there is a strong reason to make them part of a stable domain module. Prefer this maturity path:

```text
Choreo.Lab.DSL.<Domain>
experimental / Livebook-friendly / sketch syntax

→ maybe later

Choreo.<Domain>.DSL or Choreo.<NewDomain>
stable public API, only after the syntax and semantics prove durable
```

DSL design guardrails:

- DSLs should compile down to existing stable builders and return ordinary Choreo structs.
- Do not replace or obscure the pipe API; document DSLs as convenience syntax.
- Keep DSL grammars small and strict: fail on unknown constructors or variables instead of silently creating odd models.
- Prefer variable-bound semantic nodes in Lab DSLs when labels/metadata matter, e.g. `api = service("API")` then `api ~> db`.
- Use pipe modifiers for edge metadata when possible, e.g. `api ~> db |> on("reads")`, and explicit forms like `edge api ~> db, label: "reads"` when clarity is needed.
- Avoid adding a generic cross-domain DSL engine to core Choreo unless multiple Lab DSLs converge on the same proven grammar.

## Style Guidelines

- Prefer small, coherent models over exhaustive ones.
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,26 @@

### Added

- Added incubating `Choreo.Lab.DSL.Infrastructure` syntax for Livebook-friendly infrastructure sketches over the stable `Choreo` builders, including bindable cluster constructors for VPCs and subnets.
- Added incubating `Choreo.Lab.DSL.FSM` syntax for variable-bound finite-state-machine sketches over the stable `Choreo.FSM` builders.
- Added incubating `Choreo.Lab.DSL.MindMap` syntax for Livebook-friendly mind-map sketches over the stable `Choreo.MindMap` builders, including typed branch and association edges.
- Added incubating `Choreo.Lab.DSL.ERD` syntax for Livebook-friendly schema sketches over the stable `Choreo.ERD` builders, including block-based table columns and typed cardinality edges.
- Added incubating `Choreo.Lab.DSL.UML` syntax for Livebook-friendly class/struct sketches over the stable `Choreo.UML` builders, including block-based fields/functions and typed relationship edges.
- Added incubating `Choreo.Lab.DSL.Dataflow` syntax for Livebook-friendly pipeline sketches over the stable `Choreo.Dataflow` builders, including stage clusters and typed normal/error/retry/dead-letter paths.
- Added incubating `Choreo.Lab.DSL.Sequence` syntax for Livebook-friendly sequence sketches over the stable `Choreo.Sequence` builders, including ordered messages, activations, notes, and block fragments.
- Added incubating `Choreo.Lab.DSL.C4` syntax for Livebook-friendly C4 sketches over the stable `Choreo.C4` builders, including hierarchy-aware constructors, scope statements, clusters, and relationship verbs.
- Added incubating `Choreo.Lab.DSL.DecisionTree` syntax for Livebook-friendly decision policy sketches over the stable `Choreo.DecisionTree` builders, including condition-labeled branch helpers.
- Added incubating `Choreo.Lab.DSL.Workflow` syntax for Livebook-friendly process sketches over the stable `Choreo.Workflow` builders, including swimlanes, typed execution edges, conditions, and Saga compensation helpers.
- Added incubating `Choreo.Lab.DSL.Dependency` syntax for Livebook-friendly software dependency sketches over the stable `Choreo.Dependency` builders, including clusters and typed dependency edges.
- Added incubating `Choreo.Lab.DSL.Requirement` syntax for Livebook-friendly requirements traceability sketches over the stable `Choreo.Requirement` builders, including requirement-kind constructors and typed traceability edges.
- Added incubating `Choreo.Lab.DSL.Domain` syntax for Livebook-friendly DDD and event-storming sketches over the stable `Choreo.Domain` builders, including context maps, tactical domain flows, and named scenarios.
- Added `Choreo.Lab.View` pipe-friendly helpers for Livebook zoom, focus, filter, path, and collapse exploration over `Choreo.View`.
- Added `Choreo.Lab.Compose` pipe-friendly helpers for Livebook cluster, embed, connect, and trace composition over `Choreo`.

### Changed

- Added `taxonomy/0` as the preferred Livebook discovery helper for `Choreo.Lab.View`, `Choreo.Lab.Compose`, and current `Choreo.Lab.DSL.*` modules; kept `verbs/0` as a compatibility alias.

### Fixed

## [0.11.0] - 2026-07-17
Expand Down
166 changes: 166 additions & 0 deletions lib/choreo/lab/compose.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
defmodule Choreo.Lab.Compose do
@moduledoc """
Pipe-friendly composition helpers for assembling Choreo models in Livebook.

This module is ordinary Elixir, not a macro DSL. It is a thin convenience
layer over the stable composition primitives in `Choreo`: clusters, embedding,
normal graph connections, and semantic trace links.

`Choreo` remains the primitive composition API. `Choreo.Lab.Compose` is the
ergonomic exploration API for building larger composed models from smaller
Choreo diagrams.

Prefer alias-qualified usage for better editor autocomplete:

alias Choreo.Lab.Compose

Choreo.new()
|> Compose.cluster("system", label: "System")
|> Compose.embed(infra, into: "system", as: :infra)
|> Compose.embed(auth_fsm, into: "system", as: :auth)
|> Compose.trace(:infra_auth, :auth_unauthenticated, :executes)

`connect/4` creates a normal visible relationship. `trace/4` creates a
semantic cross-model relationship that `Choreo.Analysis.Tracing` and
`Choreo.View.focus_trace/4` can follow.
"""

@type system :: Choreo.t()
@type node_id :: Yog.node_id()
@type cluster_id :: String.t() | atom()

@doc """
Returns the composition helper vocabulary for Livebook discovery.

iex> taxonomy = Choreo.Lab.Compose.taxonomy()
iex> :embed in taxonomy.structure
true
iex> :trace in taxonomy.links
true
iex> :as in taxonomy.options
true
"""
@spec taxonomy() :: %{
structure: [atom()],
links: [atom()],
options: [atom()]
}
def taxonomy do
%{
structure: [:cluster, :embed],
links: [:connect, :trace],
options: [:into, :as, :prefix, :label, :type]
}
end

@doc """
Compatibility alias for `taxonomy/0`.
"""
@spec verbs() :: %{
structure: [atom()],
links: [atom()],
options: [atom()]
}
def verbs, do: taxonomy()

@doc """
Adds a visual cluster/grouping boundary to a composed system.

iex> system = Choreo.new() |> Choreo.Lab.Compose.cluster(:auth, label: "Auth")
iex> Map.has_key?(system.clusters, "cluster_auth")
true
"""
@spec cluster(system(), cluster_id(), keyword()) :: system()
def cluster(%Choreo{} = system, name, opts \\ []) do
Choreo.add_cluster(system, name, opts)
end

@doc """
Embeds a child diagram into a cluster of the parent system.

Required options:

* `:into` — target cluster name/id

Prefix options:

* `:as` — friendly alias converted to a prefix with a trailing underscore
* `:prefix` — explicit prefix passed through to `Choreo.embed/4`

`:prefix` wins over `:as` when both are supplied.

iex> child = Choreo.new() |> Choreo.add_service(:api)
iex> system =
...> Choreo.new()
...> |> Choreo.Lab.Compose.cluster(:system)
...> |> Choreo.Lab.Compose.embed(child, into: :system, as: :child)
iex> Map.has_key?(Choreo.nodes(system), :child_api)
true
"""
@spec embed(system(), struct(), keyword()) :: system()
def embed(%Choreo{} = system, child, opts) when is_struct(child) and is_list(opts) do
into = Keyword.fetch!(opts, :into)
prefix = embed_prefix(opts)
Choreo.embed(system, child, into, prefix: prefix)
end

@doc """
Connects two nodes with a normal visible relationship.

The third argument may be a label string, a keyword list, or omitted.

iex> system =
...> Choreo.new()
...> |> Choreo.add_service(:api)
...> |> Choreo.add_database(:db)
...> |> Choreo.Lab.Compose.connect(:api, :db, "reads")
iex> [{:api, :db, _weight, meta}] = Choreo.edges_with_meta(system)
iex> meta.label
"reads"
"""
@spec connect(system(), node_id(), node_id(), String.t() | keyword()) :: system()
def connect(%Choreo{} = system, from, to, label_or_opts \\ []) do
Choreo.connect(system, from, to, normalize_label_or_opts(label_or_opts))
end

@doc """
Adds a semantic trace relationship between two nodes.

The fourth argument may be a trace type atom, a keyword list, or omitted.

iex> system =
...> Choreo.new()
...> |> Choreo.add_service(:api)
...> |> Choreo.add_service(:auth)
...> |> Choreo.Lab.Compose.trace(:api, :auth, :executes)
iex> [{:api, :auth, _weight, meta}] = Choreo.edges_with_meta(system)
iex> {meta.edge_type, meta.type, meta.label}
{:trace, :executes, "executes"}
"""
@spec trace(system(), node_id(), node_id(), atom() | keyword()) :: system()
def trace(%Choreo{} = system, from, to, type_or_opts \\ []) do
Choreo.trace(system, from, to, normalize_type_or_opts(type_or_opts))
end

defp embed_prefix(opts) do
cond do
prefix = Keyword.get(opts, :prefix) ->
to_string(prefix)

as = Keyword.get(opts, :as) ->
as
|> to_string()
|> String.trim_trailing("_")
|> Kernel.<>("_")

true ->
"sub_"
end
end

defp normalize_label_or_opts(label) when is_binary(label), do: [label: label]
defp normalize_label_or_opts(opts) when is_list(opts), do: opts

defp normalize_type_or_opts(type) when is_atom(type), do: [type: type]
defp normalize_type_or_opts(opts) when is_list(opts), do: opts
end
Loading
Loading