ADR-11: Unified error taxonomy¶
Status: accepted (2026-05-28) Blocks: workstream A (core errors), workstream D (Python bindings), workstream E (bridge), workstream G (cross-cutting infra).
Context¶
Rust returns Result<T, DagMlError>; the C ABI uses integer return codes; Python bindings raise exceptions; nirs4all has its own exception hierarchy. Without a unified taxonomy, a single failure becomes a different opaque error in every binding layer. Users can’t write robust catch-blocks; debugging crosses three layers.
Decision¶
One stable error code surface that maps deterministically across all layers:
Error code structure¶
error_code = (category, code, severity):
category: one ofvalidation,runtime,data,controller,bundle,lineage,replay,security,compatibility,internal.code: short stable identifier (e.g.oof_leakage,envelope_version_unsupported,schema_fingerprint_mismatch,controller_dispatch_failed,repetition_leakage).severity:fatal | error | warning.warningdoesn’t fail by default but flips to error under--strict.
Per-layer mapping¶
Rust —
DagMlErrorenum with one variant per(category, code). Carries structured context: stable code, category, severity, optionalcause: Box<dyn Error>, optionalremediation_hint: &'static str, and acontext: BTreeMap<&'static str, serde_json::Value>for query-time debug fields.C ABI — returns
int32_t:0on success, otherwise(category << 16) | code. A thread-local last-error buffer holds the JSON-serializedDagMlErrorso callers can fetch the structured payload viadagml_last_error_json.Python —
DagMlErrorbase class with one subclass per category (DagMlValidationError,DagMlRuntimeError, etc.). Each subclass exposes.code,.category,.severity,.remediation_hint,.context. Catch hierarchy mirrors the Rust enum.nirs4all — bridge maps
DagMlError.categoryto nirs4all’s existing exception classes (PipelineError,DataError,ValidationErrorfromnirs4all/core/exceptions.py). The mapping is exhaustive and tested.
Remediation hints¶
Every error variant carries a remediation_hint — a single sentence telling the user the next thing to do. Hints are part of the public ABI; CI fails if a new variant ships without one. Examples:
oof_leakage→ “Refit the offending edge withrequires_oof = trueor remove the leakage path; see ADR-05.”envelope_version_unsupported→ “Re-export your bundle withnirs4all workspace migrate; see ADR-02.”repetition_leakage→ “Passrespect_repetition=Trueto your splitter; see ADR-05.”
Consequences¶
Workstream A task 6 introduces the
DagMlErrorenum + builders.Workstream G task 1 lands the C ABI side and the Python exception hierarchy.
CONTRIBUTING.md (workstream C task 1) documents how to add a new error variant, including the remediation-hint requirement.
The bridge logs every refusal with
code,category,context— feeds the observability hooks (ADR-12).
Risk¶
An exhaustive enum is a stable surface; adding variants is non-breaking but renaming/removing one is. CHANGELOG entries flag any new variants under “added” and removals under “breaking change” with the deprecation cycle from ADR-14.