Supervision-topology conformance: diff the intended tree against the one the code actually produces, and report the drift.
Firebreak already projects each supervisor into a model bundle
(Firebreak.Model). The same projection doubles as a committable spec of the
intended topology: a team snapshots it on a known-good commit, commits the file,
and a later run compares the current topology to it. What changed — a strategy
flipped to :one_for_all, a child dropped out of a supervisor, the restart
intensity loosened — surfaces as topology_drift findings that gate in CI just
like any other finding.
This is the structural sibling of the finding baseline (Firebreak.Baseline):
the baseline pins the findings you've accepted; conformance pins the shape
of the tree you designed. The diff is the same idea Uppsala's process-structure
work framed as "compare the specification to the abstraction of the
implementation" — here the specification is just a prior projection of the
implementation.
Spec format
The committed file is a plain Elixir term (read with Code.eval_string, like
.firebreak.exs) — a list of per-supervisor maps:
[
%{
supervisor: MyApp.Sup,
strategy: :one_for_one,
max_restarts: 3,
max_seconds: 5,
children: [
%{module: MyApp.Cache, restart: :permanent, type: :worker},
%{module: MyApp.Pool, restart: :permanent, type: :supervisor}
]
}
]Generate it with mix firebreak --write-expected FILE; check against it with
mix firebreak --expect FILE.
Summary
Functions
Append topology_drift findings (current topology vs path's expected
topology) to the analysis. A missing/unreadable spec leaves the analysis
unchanged and returns {analysis, :no_spec} so the caller can warn.
Topology-drift findings comparing the current analysis to an expected projection.
Read an expected-topology file written by write/2. Returns the projection
list, or nil if the file is missing or unparseable (the caller notes that
rather than failing).
The committable projection of the current topology: one reduced bundle per supervisor (strategy, intensity, ordered children), sorted by supervisor name for a stable file.
Write the current topology projection to path as a pretty Elixir term.
Types
@type sup_spec() :: %{ supervisor: module(), strategy: atom() | nil, max_restarts: non_neg_integer() | nil, max_seconds: non_neg_integer() | nil, children: [%{module: module(), restart: atom(), type: atom()}] }
Functions
@spec check(Firebreak.Analysis.t(), Path.t()) :: {Firebreak.Analysis.t(), :ok | :no_spec}
Append topology_drift findings (current topology vs path's expected
topology) to the analysis. A missing/unreadable spec leaves the analysis
unchanged and returns {analysis, :no_spec} so the caller can warn.
@spec diff(Firebreak.Analysis.t(), [sup_spec()]) :: [Firebreak.Finding.t()]
Topology-drift findings comparing the current analysis to an expected projection.
Read an expected-topology file written by write/2. Returns the projection
list, or nil if the file is missing or unparseable (the caller notes that
rather than failing).
@spec projection(Firebreak.Analysis.t()) :: [sup_spec()]
The committable projection of the current topology: one reduced bundle per supervisor (strategy, intensity, ordered children), sorted by supervisor name for a stable file.
@spec write(Path.t(), Firebreak.Analysis.t()) :: :ok | {:error, term()}
Write the current topology projection to path as a pretty Elixir term.