AGPL-3.0-or-later · forever.

ADR-010

miette as the L4 diagnostic presentation layer

accepted · 2026-04-14 · L0 L4 · cites 5

0

ADR-010: miette as the L4 diagnostic presentation layer #

Context #

ADR-005 established NikaErrorCode trait + Box<dyn> unification at API boundaries — correct for routing, retry (is_transient()), and fingerprinting. It does NOT, however, solve rendering: when a user runs nika check workflow.nika.yaml and a template parse fails at line 14, they want source-span highlighting, a --explain NIKA-384, and coloured severity — not a single-line Error: NIKA-384: ... string.

The Rust 2026 ecosystem has converged on miette for this role (ruff, uv, cargo-dist, rye all use it or miette-class renderers). miette 7.6 is already pinned in [workspace.dependencies] and NikaError already carries impl miette::Diagnostic. This ADR locks the pattern and scope so downstream L4 crates inherit it consistently.

Decision #

  • nika-error keeps the NikaErrorCode trait as the structural / routing contract (ADR-005 unchanged).
  • nika-error additionally implements miette::Diagnostic for NikaError — this is the rendering contract.
  • L4 crates (nika-cli, nika-lsp, nika-serve, nika-catalog-verify) render all user-visible errors via miette::GraphicalReportHandler (or equivalent).
  • Source spans, severity, help strings, related diagnostics — all flow through miette's Diagnostic API.
  • --explain NIKA-NNN CLI subcommand uses the registered help string per NikaCode.
  • Library crates (L0, L0.5, L1, L2, L3) do NOT depend on miette at runtime — their errors implement only NikaErrorCode; miette is purely a presentation concern.

Consequences #

Positive #

  • User-facing errors are best-in-class (source underlines, colour, hyperlinked error codes).
  • Library crates stay lean — no miette surface in kernel/catalog/error public types beyond the optional Diagnostic impl on NikaError.
  • --explain works by registry lookup — zero per-error maintenance cost.

Negative #

  • L0 nika-error carries a miette dep (already paid — feature-gated via diagnostic).
  • Two contracts per error type (NikaErrorCode + Diagnostic). Mitigated by derive helpers.

Neutral #

  • If the community pivots to ariadne or a successor, we swap in nika-error only — library crates untouched.

Evidence #

  • crates/nika-error/Cargo.toml:14miette = { workspace = true, features = ["fancy-no-backtrace"] }
  • crates/nika-error/src/nika_error.rs:97impl miette::Diagnostic for NikaError
  • crates/nika-error/src/codes.rs:128 — per-code help strings
  • ADR-005 — the structural error hierarchy this extends

Alternatives considered #

Alt A — `anyhow` / `eyre` at CLI #

Rejected. Both produce flat strings — no spans, no --explain.

Alt B — Custom renderer #

Rejected. Reinvents miette. Not valuable.

Alt C — `error-stack` (HASH) with attachments #

Watching. Good for structured observability context but not focused on user-facing rendering. Revisit if miette stagnates.

  • ADR-005 — trait-based error hierarchy (structural contract this rendering layer wraps)
  • ADR-007 — forward-compat invariants (NikaError is #[non_exhaustive])
  • ADR-015 — expect-test inline snapshots (regression coverage for the rendering this ADR locks)

Notes #

When nika-cli is admitted, add a test-miette-render.rs golden test that pins the rendered output of 3 representative error codes. Prevents accidental UX regression.

read at v0.107.0 · the decision record ships with the engine