# ADR-002: logz adopts contracts/errs instead of private duck typing - **Date:** 2026-05-28 - **Module:** `code.nochebuena.dev/einherjar/core` - **Status:** Accepted ## Context In micro-lib, `logz` enriches log records when the error passed to `Logger.Error` carries a machine-readable code or structured fields. It detects this at runtime using two private interfaces defined inside `logz`: ```go // inside logz — never exported type errorWithCode interface { ErrorCode() string } type errorWithContext interface { ErrorContext() map[string]any } ``` `xerrors.Err` implements both via its `ErrorCode()` and `ErrorContext()` methods. `logz` detects this with `errors.As` — without importing `xerrors`. The decoupling is preserved. But the contract is invisible: a developer who wants their custom error type to receive log enrichment must read `logz`'s internal source to discover what methods are required. This approach is acceptable in micro-lib (single team, single repository, total visibility). In a framework distributed to third-party authors, it is a maintenance trap. ## Decision `core/logz` imports `contracts/errs` and checks `errs.CodedError` and `errs.ContextualError` instead of defining private duck-typed equivalents. ```go // core/logz/logger.go var ec errs.CodedError if errors.As(err, &ec) { attrs = append(attrs, "error_code", ec.ErrorCode()) } var ectx errs.ContextualError if errors.As(err, &ectx) { for k, v := range ectx.ErrorContext() { attrs = append(attrs, k, v) } } ``` `core/xerrors` declares compile-time satisfaction: ```go // core/xerrors/err.go var _ errs.CodedError = (*Err)(nil) var _ errs.ContextualError = (*Err)(nil) ``` ## Why the `contracts/errs` Sub-package Exists `contracts` is the only Einherjar module guaranteed to have zero dependencies. If `CodedError` and `ContextualError` lived in `core/xerrors`, any module implementing a custom error type would be forced to import `core` — pulling the launcher, logger, and validator into its dependency graph. That defeats the Separated Interface pattern. `contracts/errs` is a zero-dependency home for these two 1-method interfaces. Any module can implement them without taking on any Einherjar dependencies. ## Clean Separation Preserved `logz` still does not import `xerrors`. Both import `contracts/errs`. The decoupling is maintained; the contract is now visible. ``` contracts/errs (zero deps) ↑ ↑ core/logz core/xerrors (imports) (implements) ``` A third-party error type implements `errs.CodedError` by satisfying one interface from `contracts` — a zero-dependency import. It receives full log enrichment automatically, without any wiring code. ## Alternatives Considered **Keep private duck typing.** Rejected — invisible to third-party implementors. A framework that requires developers to read its internal source code to understand what their types must implement is hostile to the developers it serves. **Export the interfaces from `core/logz`.** Rejected — would force custom error types to import `core`, violating the Separated Interface pattern. An error type should not need to know about logging infrastructure. **One combined `RichError` interface.** Rejected — ISP. An error may carry a machine-readable code without structured fields, or structured fields without a code. Forcing both into one interface imposes unnecessary constraints on implementors. ## Consequences **Easier:** Third-party error types have a clear, discoverable interface to implement: `go doc code.nochebuena.dev/einherjar/contracts/errs`. Compile-time verification replaces runtime discovery. The framework's own `*xerrors.Err` is provably correct at compile time. **Harder:** `logz` now imports `contracts`, adding one dependency to its import graph. This dependency is zero-cost in practice — `contracts` has no external dependencies and will not change its interfaces without a major version bump. **New obligations:** The `errs.CodedError.ErrorCode()` and `errs.ContextualError.ErrorContext()` signatures are permanent from `contracts v1.0.0`. Any implementor of these interfaces is guaranteed that the signatures will not change without a major version bump on `contracts`.