# ADR-001: errs Sub-package — Formalising the Error Enrichment Contract - **Date:** 2026-05-27 - **Module:** `code.nochebuena.dev/einherjar/contracts` - **Status:** Accepted ## Context In micro-lib, `logz` and `xerrors` are decoupled through duck typing. `logz` defines two private interfaces internally: ```go // inside logz — never exported type errorWithCode interface { ErrorCode() string } type errorWithContext interface { ErrorContext() map[string]any } ``` `xerrors.Err` happens to satisfy both. `logz` detects this at runtime via `errors.As`. The contract exists — it just lives nowhere. Any type that implements `ErrorCode() string` and `ErrorContext() map[string]any` gets the enrichment behaviour, whether its author knew about `logz` or not. This works in a flat library collection where both packages are authored by the same team. It breaks down in a framework context for two reasons: 1. **No canonical definition.** A developer implementing a custom error type that wants logger enrichment has no interface to implement against — they must read `logz`'s source code to discover the implicit contract. 2. **Impossible to mock or test cleanly.** A test that wants to verify error enrichment behaviour cannot assert against a defined interface. It must rely on the duck-type resolution happening implicitly. ## Decision Introduce `contracts/errs` as a sub-package with two exported interfaces: ```go // errs/coded_error.go type CodedError interface { ErrorCode() string } // errs/contextual_error.go type ContextualError interface { ErrorContext() map[string]any } ``` `core/logz` imports `contracts/errs` and checks against these interfaces explicitly instead of defining private duck-typed equivalents. `core/xerrors` declares compile-time satisfaction: ```go var _ errs.CodedError = (*Err)(nil) var _ errs.ContextualError = (*Err)(nil) ``` The clean separation is preserved — `logz` still does not import `xerrors`, and `xerrors` still does not import `logz`. Both now depend on `contracts/errs`, which has zero dependencies of its own. ## Why Two Interfaces, Not One The interfaces are intentionally separate. ISP (Interface Segregation Principle) requires that no consumer is forced to implement methods it does not use. An error may carry a machine-readable code without structured context fields. An error may carry structured context fields without a machine-readable code. Forcing both into a single `RichError` interface would require implementors to provide both — a constraint that is not justified by the actual use cases. `logz` checks each independently via `errors.As`, which is exactly how the duck-typed version already worked. The two-interface shape matches the actual consumption pattern. ## Why in `contracts`, Not in `core` `contracts` is the only module guaranteed to have zero Einherjar dependencies. If `CodedError` and `ContextualError` lived in `core`, any module that wanted to implement them would be forced to import `core` — pulling the launcher, logger, and validator into its dependency graph. That violates the Separated Interface pattern that `contracts` exists to enforce. ## Consequences **Easier:** Custom error types have a clear interface to implement against. `go doc code.nochebuena.dev/einherjar/contracts/errs` is the single authoritative answer to "what does my error need to provide for logger enrichment to work?" Compile-time verification replaces implicit duck-type discovery. **Harder:** `errs` is a sub-package that did not exist in micro-lib. Developers migrating from micro-lib must adopt these interfaces explicitly rather than relying on the duck-type bridge. This is a one-time migration cost with no ongoing burden. **New obligations:** `CodedError.ErrorCode()` and `ContextualError.ErrorContext()` signatures are permanent from this release. They may not be changed without a major version bump on `contracts`.