Files
contracts/docs/adr/ADR-001-errs-sub-package.md
Rene Nochebuena 098a2098f8 feat(contracts): initial implementation (v1.0.0)
Introduces code.nochebuena.dev/einherjar/contracts — the zero-dependency
foundation of the Einherjar framework. Defines the interfaces and minimal
types consumed by every starter. Zero external dependencies. Zero Einherjar
dependencies. Nothing is above it in the dependency graph.

lifecycle:
- Component — OnInit, OnStart, OnStop three-phase lifecycle hooks

observability:
- Level (LevelCritical=0, LevelDegraded); zero value is the safe default
- Checkable — HealthCheck, Name, Priority
- Identifiable — ModulePath, ModuleVersion; implemented by all starters to
  surface module identity and version in the startup banner

logging:
- Logger — Debug, Info, Warn, Error, With, WithContext

errs:
- CodedError — ErrorCode() string; satisfied by core/xerrors.Err
- ContextualError — ErrorContext() map[string]any; satisfied by core/xerrors.Err

security:
- Identity value type — UID, TenantID, DisplayName, Email; NewIdentity, WithTenant
- Permission (int64), MaxPermission=62, PermissionMask — Has, Grant
- PermissionProvider — ResolveMask(ctx, uid, resource) (PermissionMask, error)
- SecurityBag value type — immutable request-scoped security context; carries
  Identity and arbitrary typed attributes (hardware IDs, grant codes, etc.);
  With copies the attribute map on every call to preserve receiver-invariant behaviour
- NewSecurityBag, Identity, WithIdentity, Get, With
- SetBagInContext / BagFromContext — full bag context storage
- SetInContext / FromContext — backed by SecurityBag; all four cross-function
  combinations (SetInContext+BagFromContext, SetBagInContext+FromContext) are valid

One file per type; CT-6 enforced by compliance test AST walk.
2026-05-29 15:43:08 +00:00

3.9 KiB

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:

// 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:

// 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:

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.