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/1returns them without starting anything) — with static AST parsing as the fallback for code it can't load. The coupling graph is always static: it resolvesGenServer.call/cast,:gen_server/:gen_statem, registered names,Registry,:global,Process.whereis,:ets,Phoenix.PubSub, and:pgto 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 viainit/1) orbest-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/1synchronous 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:
mix firebreak.spec— a TLA+ lifecycle spec per supervisor (restart-intensity budget + escalation, and the cross-tree caller's permanent:noproc), runnable with TLC.--lang quintemits the same model as Quint.mix firebreak.lockstep— a lockstep regression-test scaffold per synchronous crossing.Firebreak.WhatIf— simulate a refactor (move/3,set_strategy/3) and diff the crossings before touching code.Firebreak.RiskScore,Firebreak.FailureViz,Firebreak.RuntimeOverlay— a risk score, a failure-mode diagram, and a live overlay, all from the IR.
CI integration
--fail-on <severity>gate;--min-severityfilter.--write-baseline/--baselineto gate only on new findings; a.firebreak.exsallowlist for accepted findings.--write-expected/--expecttopology conformance: snapshot the intended tree and reporttopology_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).