Phoenix-aware engineering-intelligence layer: call-graph + AST analysis.
Credo, Dialyzer, and mix xref cover the linting / type / module-graph layer
for Elixir, but nobody stitches their signals into a higher-level engineering
view. Pulsar builds a function-level call graph (via a compile tracer) and
joins it with source-AST structural metrics, then runs analyzers that
surface things a file-by-file linter structurally can't:
- Fat contexts — domain modules grown past size/public-API thresholds, reported with their fan-in (blast radius) from the call graph.
- Boundary violations — web-layer modules (controllers/LiveViews) calling
*.Repoor another context's internals directly, instead of through a context. Rules are configurable. - Dead public functions — public functions with zero callers anywhere in the project, excluding framework entrypoints (router-dispatched controller actions, LiveView/GenServer/Oban callbacks, Mix tasks).
Each run also produces a 0–100 health score per module and for the project, and a stable, versioned JSON document designed as the ingest feed for a future hosted "Observatory".
Add Pulsar as a dev/test dependency of your Phoenix app — the tracer must run
during your app's own mix compile:
def deps do
[
{:pulsar, "~> 0.1.0", only: [:dev, :test], runtime: false}
]
endmix pulsar # grouped, colorized terminal report
mix pulsar --format json # stable JSON schema (Observatory feed)
mix pulsar --only boundary # run a single analyzer (fat_context|boundary|dead_code)
mix pulsar --max-findings 0 # exit non-zero past a budget — a CI gateDrop a .pulsar.exs at your project root (see .pulsar.exs.example):
[
fat_context_public_fns: 25,
fat_context_loc: 1500,
boundary_rules: [
[from: "Web", to: "Repo", message: "web layer should go through a context, not Repo directly"]
],
ignore_modules: [],
max_findings_budget: :infinity
]A boundary rule fires on a call edge when some segment of the caller module
name contains from and some segment of the callee module name contains to.
- The call graph captures statically resolvable calls from
.exsources. Functions reached only via runtime dispatch (apply/3, framework-invoked behaviour callbacks) or from.heextemplates (Phoenix function components like<.table>) have no traceable caller, so dead-code analysis is strongest on an application's internal/context modules — callers that live only intest/, in templates, or behind dynamic dispatch are not edges. Framework entrypoints (controller actions, LiveView/OTP/Oban callbacks, Mix tasks,use-target helper modules) are all-listed to avoid false positives. - The JSON schema is versioned (
schema_version); treat it as the stable contract.