feat(core): initial implementation — launcher, logz, xerrors, valid

Introduces `code.nochebuena.dev/einherjar/core` — the foundational implementation
module of the Einherjar framework. Provides four sub-packages that together cover
every service's baseline needs: lifecycle management, structured logging, typed
errors, and struct validation.

- launcher: Launcher interface — three-phase managed lifecycle (OnInit → BeforeStart
  hooks → OnStart → OS signal wait → OnStop in reverse). Accepts
  lifecycle.Component and logging.Logger from contracts. Prints an ASCII art banner
  at startup (EINHERJAR_BANNER=off to suppress). Banner includes core version via
  runtime/debug.ReadBuildInfo() and a loaded-module list for every registered
  component that implements observability.Identifiable. Config struct with
  EINHERJAR_COMPONENT_STOP_TIMEOUT env tag (caarlos0/env syntax, default 15s).

- logz: Logger implementation backed by log/slog. Returns contracts/logging.Logger.
  Detects errs.CodedError and errs.ContextualError (from contracts/errs) to enrich
  log records automatically — replaces the private duck-typed bridge from micro-lib.
  Context helpers: WithRequestID, WithField, WithFields, GetRequestID. Config struct
  with EINHERJAR_LOG_LEVEL (default INFO) and EINHERJAR_LOG_JSON (default false) env
  tags (caarlos0/env syntax); programmatic-only fields StaticArgs and Writer carry no
  tags.

- xerrors: Typed error codes with context enrichment. Complete gRPC canonical set
  (16 codes) plus HTTP 410 ErrGone. Adds ErrOutOfRange, ErrAborted, ErrDataLoss
  over micro-lib. One convenience constructor per code. *Err declares compile-time
  satisfaction of errs.CodedError and errs.ContextualError.

- valid: Struct validation wrapping go-playground/validator/v10. Validator interface
  + MessageProvider interface with full built-in tag coverage (~150 tags) in both
  DefaultMessages (English) and SpanishMessages (Spanish). Backend fully hidden;
  returns *xerrors.Err with ErrInvalidInput or ErrInternal. FieldLevel interface
  abstracts the backend's field-level access for custom validators.
  WithCustomValidator registers custom validation tags at construction time;
  OverrideProvider chains a tag→handler map with a fallback MessageProvider for
  custom tag messages without re-implementing built-ins.

Compliance test enforces CT-6 (at most one exported TypeSpec per file via AST) and
verifies behavioural correctness of all four sub-packages, including custom validator
registration and OverrideProvider composition. Compile-time var _ assertions prove
interface satisfaction.

docs: ADR-001 (core module composition), ADR-002 (logz contracts/errs adoption),
ADR-003 (Config naming convention and caarlos0/env tag standard)
This commit is contained in:
2026-05-29 15:45:12 +00:00
commit 38a415c2ab
33 changed files with 3868 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
# ADR-001: core module composition — four sub-packages in one Go module
- **Date:** 2026-05-28
- **Module:** `code.nochebuena.dev/einherjar/core`
- **Status:** Accepted
## Context
In micro-lib, the four packages that become `core` are independent Go modules:
- `code.nochebuena.dev/go/launcher`
- `code.nochebuena.dev/go/logz`
- `code.nochebuena.dev/go/xerrors`
- `code.nochebuena.dev/go/valid`
Each has its own `go.mod`, version tag, and release cycle. A dependency update to
`xerrors` requires a version bump on `xerrors`, then updating `valid` to consume the
new version, then informing all callers of both. Four packages → four coordinated
version bumps per change.
In the Einherjar module structure, each entry in the module inventory is one
independently versionable unit. The Spring Boot starter model places the
distribution boundary at the starter level (`db-postgres`, `cache-valkey`, `auth`),
not at the foundational implementation level.
## Decision
`launcher`, `logz`, `xerrors`, and `valid` are sub-packages of a single
`code.nochebuena.dev/einherjar/core` Go module.
## Rationale
**They always ship together.** Every Einherjar service needs all four:
- `launcher` requires a `logging.Logger` — it calls `logz.New` in the same main function
- `valid` returns `*xerrors.Err` — they are semantically coupled at the type level
- No known use case requires `logz` without `xerrors`, or `launcher` without `logz`
**Version coordination cost is real.** With four separate modules, a one-line fix to
`xerrors` becomes four coordinated operations: tag xerrors, update valid's go.mod,
tag valid, inform consumers. With one module, it is one operation.
**The Spring Boot analogy holds at the right level.** Spring Boot's parent POM bundles
dozens of interdependent libraries into a single versioned unit. Starters (`spring-boot-starter-data-jpa`) are the distribution boundary, not the individual `spring-data-commons` jar inside them. Einherjar's `core` is the parent; the starters are the distribution units.
## Alternatives Considered
**Four separate modules (`core-launcher`, `core-logz`, etc.).** Rejected — over-engineering at the foundational layer. There is no consumer that needs `logz` but not `xerrors`, or `launcher` but not `logz`. The additional version coordination overhead has no compensating benefit.
**One flat `core` package (no sub-packages).** Rejected — importing `core` would pull all four concerns into any module that only needs errors or validation. Sub-packages preserve the ability to import only what is needed.
## Consequences
**Easier:** A single `go get code.nochebuena.dev/einherjar/core@v1.x.x` installs
everything. A single version bump covers all four sub-packages. Dependency graph for
downstream starters stays simple.
**Harder:** A change to any sub-package bumps the entire `core` version, even if the
change is isolated to `valid`. This is the accepted trade-off — the coupling is real;
the version increment reflects it.
**New obligations:** Sub-packages within `core` may import each other in one direction:
`valid` may import `xerrors`; nothing else crosses package boundaries within `core`.
Circular imports between sub-packages are prohibited. `launcher` uses
`contracts/logging` — not `core/logz` directly — to preserve the contracts layer.