Everything we learned about a single module: what kind of OTP citizen it is, its supervision configuration (if a supervisor), the names it registers itself under, and its outgoing coupling edges.
The supervision-tree fields (strategy, max_restarts, max_seconds,
children) can come from two places, recorded in tree_source:
:exact— read at runtime by callingMod.init/1, the same call OTP'ssupervisormakes. Authoritative: resolves child lists assembled dynamically (pipes, conditionals, builder calls) that no static pass can.:static— recovered from the AST without running the code. Best-effort; a child spec or option assembled at runtime is reported as unresolved rather than guessed.
The coupling fields (edges, registers, unsupervised_spawns, traps_exit?)
are always static — they describe who-calls-whom, which init/1 does not reveal.
Dynamic children
A DynamicSupervisor returns no children from init/1 — they are added at
runtime via DynamicSupervisor.start_child/2, so the static (and even exact)
tree shows the supervisor as empty. Two fields recover that runtime shape
statically:
dynamic_starts— everystart_childcall site found in this module, as{sup_target, child}pairs (the supervisor the call targets and the best-effortChildparsed from the spec). The call may target a supervisor defined in a different module.dynamic_children— resolved inFirebreak.Coupling: the dynamic children attributed to this module as their supervisor (gathered from every module'sdynamic_startswhosesup_targetresolves here). Best-effort.
Supervision evidence (orphan-check inputs)
Two fields feed the orphan check the evidence that a stateful process which appears in no supervisor's subtree is nonetheless meant to be (or is) started a particular way — so the finding can be relabelled or sharpened rather than left as a vague "we couldn't see it":
child_spec_builds— modules this one builds a child spec for (Mod.child_spec(...)), scanned from the full body includingquoteblocks, since achild_speccall inside a__using__macro is exactly how a base module declares that itsusers will superviseMod. A module that something builds a child spec for is supervised through an indirection we can't attribute to a specific supervisor — not an orphan.manual_starts—{module, line}for every directMod.start_link(...)call site in this module (excluding the OTP behaviour helpers and self). A stateful module started this way, outside any supervisor, genuinely won't be restarted on crash — the orphan finding can name the exact call site.
Wrapper-call coupling (inter-procedural inputs)
Most apps don't litter GenServer.call(Server, …) across the codebase; they
expose a public API (Server.fetch(id)) that wraps the call. A naive edge scan
only sees the direct call shapes, so it misses the dependency from the API
caller onto the server process. Two fields recover the first level of that:
api_entries— this module's public functions that themselves perform a coupling call, asfun => [{kind, target, sync?}]. A self-call (GenServer.cast(__MODULE__, …)indef notify/1) is recorded with this module as the target, so a caller ofnotify/1can be wired to it. OTP callbacks (handle_call,init, …) are excluded — they aren't an external API.api_calls—{module, fun, in_init?, line}for every remote callMod.fun(...)this module makes.Firebreak.Couplingkeeps only those that match some module'sapi_entriesand synthesises the resulting edge onto the wrapped process.api_fun_calls—public_fun => [{module, fun}]: the remote calls each public function makes. This letsFirebreak.Couplingchain wrappers transitively (A callsB.f,B.fcallsC.g,C.gcouples to a process ⇒ A depends on it), bounded by a depth cap and cycle-guarded. Only chains through remote public-function calls, not private helpers.
Concurrency evidence
lookup_or_create— functions performing a non-atomic registry test-and-set: a single body that both reads the registry (whereis/Registry.lookup/:global.whereis_name/registered) and creates a registration (register/start_link/start_child/spawn).{fun, line, mechanism}per such function;mechanismis:register_raises(a duplicateregistercrashes the loser) or:register_soft(a duplicate start returns{:error, {:already_started, _}}). The classic lookup-or-create race (Christakis & Sagonas, PADL 2010): two callers both miss the read and both create, and the loser's just-spawned process leaks as a ghost.opens_port— lines at which this module opens a port (Port.open/:erlang.open_port), i.e. bridges to an external OS process.handles_port_exit— whether anyhandle_infoclause handles a port's termination (a head mentioning:exit_status/:EXIT/:eof). A module that opens a port but never handles its exit can't notice the external program dying (or is taken down by the linked exit).
Summary
Types
How a start_child call names its supervisor: a literal module (__MODULE__
is pre-resolved to the enclosing module), a registered name to look up in the
global name index, or unresolvable.
Functions
Default restart intensity for a supervisor when not explicitly set.
Was the supervision tree read at runtime (exact) rather than parsed (static)?
Does this module hold process state worth losing on restart? GenStage stages carry buffered demand/events, so they count alongside GenServer/gen_statem/Agent.
Is this module a supervisor of any flavour? Broadway counts: it boots an internal supervision subtree (producers/processors/batchers), so its declared producer is a real subtree member for blast-radius purposes.
Types
How a start_child call names its supervisor: a literal module (__MODULE__
is pre-resolved to the enclosing module), a registered name to look up in the
global name index, or unresolvable.
@type kind() ::
:supervisor
| :dynamic_supervisor
| :genserver
| :gen_statem
| :gen_stage
| :broadway
| :application
| :task
| :agent
| :other
@type t() :: %Firebreak.ModuleInfo{ api_calls: [{module(), atom(), boolean(), non_neg_integer() | nil}], api_entries: %{ optional(atom()) => [ {Firebreak.Edge.kind(), Firebreak.Edge.target(), boolean()} ] }, api_fun_calls: %{optional(atom()) => [{module(), atom()}]}, behaviours: [module()], callback_starts: [{module(), atom(), non_neg_integer() | nil}], child_spec_builds: [module()], children: [Firebreak.Child.t()], dynamic_children: [Firebreak.Child.t()], dynamic_start?: boolean(), dynamic_starts: [{dynamic_target(), Firebreak.Child.t() | nil}], edges: [Firebreak.Edge.t()], ets_tables: [term()], file: String.t() | nil, handles_port_exit: boolean(), init_arg_struct: module() | nil, kind: kind(), line: non_neg_integer() | nil, lookup_or_create: [ {atom(), non_neg_integer() | nil, :register_raises | :register_soft} ], manual_starts: [{module(), non_neg_integer() | nil}], max_restarts: non_neg_integer() | nil, max_seconds: non_neg_integer() | nil, name: module(), opens_port: [non_neg_integer() | nil], own_restart: Firebreak.Child.restart(), own_shutdown: term(), registers: [term()], strategy: atom() | nil, traps_exit?: boolean(), tree_source: tree_source(), unsupervised_spawns: [{atom(), non_neg_integer() | nil}], uses: [module()] }
@type tree_source() :: :static | :exact
Functions
Default restart intensity for a supervisor when not explicitly set.
Was the supervision tree read at runtime (exact) rather than parsed (static)?
Does this module hold process state worth losing on restart? GenStage stages carry buffered demand/events, so they count alongside GenServer/gen_statem/Agent.
Is this module a supervisor of any flavour? Broadway counts: it boots an internal supervision subtree (producers/processors/batchers), so its declared producer is a real subtree member for blast-radius purposes.