All notable changes to Firebreak are documented here. The format is based on Keep a Changelog, and the project follows semantic versioning.

[0.1.0]

Initial public release.

Firebreak builds two graphs from an Elixir/OTP project — the declared supervision tree and the actual process-to-process coupling — and reports where coupling crosses a supervision boundary the tree treats as "contained": a synchronous call from one branch into another, which a restart turns into :noproc/:timeout for the caller. No app boot, no LLM, CI-friendly.

Analysis

  • Two-graph model. The supervision forest is read the way OTP reads it — by calling each supervisor's init/1 (child specs are runtime data; init/1 returns them without starting anything) — with static AST parsing as the fallback for code it can't load. The coupling graph is always static: it resolves GenServer.call/cast, :gen_server/:gen_statem, registered names, Registry, :global, Process.whereis, :ets, Phoenix.PubSub, and :pg to the owning module, following wrapper functions transitively.
  • Synchronous vs async weighting. Only a synchronous crossing makes a caller block on :noproc, so severities are gated on it — async-only coupling rates lower.
  • Confidence. Findings are tagged exact (read via init/1) or best-effort (static), so you know which is which.

Checks

  • Coupling / correctness: cross_tree_coupling, crash_cascade (failure simulation over the restart closure), cyclic_coupling, boot_order_cycle (an in-init/1 synchronous cycle means an unbootable tree), missing_trap_exit, boot_order_dependency, start_link_in_callback.
  • Blast radius / structural: one_for_all_blast_radius, supervisor_subtree_blast, dynamic_supervisor_restart_blast, orphaned_stateful_process, dynamic_supervisor_registry_race, lookup_or_create_race, unhandled_port_exit, shutdown_exceeds_intensity_window, default_restart_intensity.

Runtime observation (--observe)

Attach to a live node over distributed Erlang and fold its real shape into the analysis: live DynamicSupervisor children join the forest, registered names are recovered, runtime_fanout reports supervisors running more children than the source models, and runtime_mailbox_backlog flags a deep mailbox on a synchronously-called process. Reads use standard-library :rpc only — the target needs nothing installed.

Output formats (mix firebreak --format)

text (default; leads with primary findings, collapses advisories), json, dot, mermaid, github (PR annotations), html, model (the supervision-model IR), score (a structural risk score + per-supervisor ranking), failure (a Mermaid diagram of the cross-tree failure modes), and overlay (static crossings annotated with a live node's observed state; needs --observe).

The model IR and its backends

mix firebreak --format model emits a versioned, documented contract (notes/model-ir-contract.md) — every other artifact is a pure function of it:

CI integration

  • --fail-on <severity> gate; --min-severity filter.
  • --write-baseline / --baseline to gate only on new findings; a .firebreak.exs allowlist for accepted findings.
  • --write-expected / --expect topology conformance: snapshot the intended tree and report topology_drift (strategy flips, dropped children, intensity changes).
  • A composite GitHub Action (action.yml).

Implementation

Pure Elixir, zero runtime dependencies (hand-rolled JSON encoder; ex_doc is dev-only).