Pure diagnostic module that builds Sigra's nine-feature optional-dependency matrix, validates boot-time wiring for configured features, and returns a structured diagnosis result.
Design
Sigra.Doctor is a pure library module — no IO calls, no Mix.Task dependency,
no side effects. All diagnostic logic lives here so that security-sensitive
checks ship via mix deps.update rather than being locked in a generator or
task layer.
The Mix task Mix.Tasks.Sigra.Doctor is a thin formatter + exit shell that
calls run/1 here, formats the structured result, and handles exit codes. All
IO formatting stays in the task.
Injection Seam (D-04)
diagnose/1 accepts injected inputs so the matrix and wiring logic are
unit-testable without toggling the ambient dep tree (since Code.ensure_loaded?
cannot be toggled per-test). The injection keys are:
:predicates— a map of predicate name atom → boolean, overriding individualSigra.OptionalDepscalls.:host_sigra— the raw keyword config list (same shape as the two-hop config read inSigra.Application).:oban_running— a boolean override for both async-email and audit-forwarder Oban supervision checks (replaces the real internaloban_running?/1call inSigra.Audit.Forwarders). When not provided, the real function is called.:module_loaded?— a(module :: atom() -> boolean())function override forCode.ensure_loaded?/1used in the D-09 #4 forwarder-module-loaded check. Allows deterministic unit-testing of the not-loaded branch without relying on a module name that happens not to be defined in the runtime.
When injection keys are absent, the real functions are called:
Sigra.OptionalDeps.*_available?/0for each of the nine availability predicatesSigra.OptionalDeps.encryption_active?/1for encryption posture- the internal
oban_running?/1inSigra.Audit.Forwardersfor supervised-Oban checks Code.ensure_loaded?/1for dynamic forwarder-module existence checks
OptionalDeps SOT (D-06)
This module consumes Sigra.OptionalDeps predicates directly and must NOT call
Code.ensure_loaded? itself to re-implement those checks — preserving the single
source of truth established in Phase 137.
The one narrow exception is the dynamic forwarder-module check: configured
forwarder modules are host-provided dynamic atoms that cannot be listed in
OptionalDeps, so Code.ensure_loaded?(module) is called directly for that
check (mirrors the internal attach_forwarders/0 in Sigra.Application).
Deliberate Omissions (D-08)
This module deliberately does NOT call:
- the internal
verify_vault!/1inSigra.Application— it raises, which would abort the report before the full matrix prints. - the internal
attach_forwarders/0inSigra.Application— it raisesArgumentErroron async-without-Oban, which would abort.
Instead, doctor uses the non-raising mirrors: Sigra.OptionalDeps.encryption_active?/1
and the internal oban_running?/1 in Sigra.Audit.Forwarders.
Summary
Functions
Diagnoses the optional-dependency wiring for all nine Sigra features.
Runs the diagnostic and returns the same structured map as diagnose/1.
Types
@type diagnosis() :: %{ rows: [feature_row()], wiring: [String.t()], verdict: :ok | :fail }
Functions
Diagnoses the optional-dependency wiring for all nine Sigra features.
Returns a structured map with:
:rows— list of nine feature row maps, each with:feature,:deps,:state, and:hintkeys.:wiring— list of wiring check failure strings (empty when all wiring is OK).:verdict—:okwhen no configured features are misconfigured;:failwhen any configured feature has broken wiring or a missing required dependency.
Injection Options (for testing; all optional)
:predicates— map of predicate atom → boolean. Keys::oban,:bcrypt,:eqrcode,:threadline,:assent,:swoosh,:joken,:hammer,:req,:encryption_active.:host_sigra— raw keyword list of host application Sigra config.:oban_running— boolean override for the Oban supervision check.:module_loaded?— a(module :: atom() -> boolean())function override forCode.ensure_loaded?/1used in the D-09 #4 forwarder-module-loaded check. Allows deterministic unit-testing of the not-loaded branch without relying on a module name that happens not to be defined.
Runs the diagnostic and returns the same structured map as diagnose/1.
This is the entrypoint called by Mix.Tasks.Sigra.Doctor. All IO formatting
and exit logic lives in the Mix task, not here.
Accepts the same injection options as diagnose/1. Output verbosity is handled
by Mix.Tasks.Sigra.Doctor; this function always returns the full structured
diagnosis.