Files
contracts/docs/adr/ADR-002-file-per-type-naming.md

79 lines
3.4 KiB
Markdown
Raw Permalink Normal View History

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
# ADR-002: File-per-Type Naming Convention
- **Date:** 2026-05-27
- **Module:** `code.nochebuena.dev/einherjar/contracts`
- **Status:** Accepted
## Context
Framework-level ADR-004 establishes that `contracts` sub-packages contain one file
per interface or type (CT-6). It does not specify how those files are named. Without
an explicit convention, names tend to drift: `types.go`, `interfaces.go`, `models.go`,
or `common.go` — filenames that say nothing about what is inside.
The naming problem compounds in `security`, which has five declarations across five
files. A developer looking at the directory listing needs to know immediately which
file to open.
## Decision
Every source file in `contracts` is named after the single type or interface it
declares, converted to lowercase and snake_cased for multi-word names.
| Declaration | File |
|---|---|
| `Component` | `component.go` |
| `Checkable` | `checkable.go` |
| `Level` | `level.go` |
| `Logger` | `logger.go` |
| `CodedError` | `coded_error.go` |
| `ContextualError` | `contextual_error.go` |
| `Identity` | `identity.go` |
| `Permission` | `permission.go` |
| `PermissionMask` | `permission_mask.go` |
| `PermissionProvider` | `permission_provider.go` |
`doc.go` files are the sole exception — they carry the package-level doc comment and
export nothing.
Constants, package-level variables, and functions that are semantically part of a
type's API (constructors, context helpers, builder methods) coexist in the same file
as the type they serve. They are not counted as separate declarations for CT-6
purposes — CT-6 governs type declarations, not every exported symbol.
Examples:
- `level.go` declares `Level` and also defines `LevelCritical` and `LevelDegraded`
- `permission.go` declares `Permission` and also defines `MaxPermission`
- `identity.go` declares `Identity` and also defines `NewIdentity`, `WithTenant`,
`SetInContext`, and `FromContext`
## Alternatives Considered
**Single `types.go` per sub-package.** Rejected — a file named `types.go` provides
no information to a developer scanning a directory. They must open it to know what
is inside.
**Interface files named `interface.go` or `contract.go`.** Rejected — same reason.
The filename should answer "what contract does this file define?" not "what kind of
file is this?"
**Separate files for each function and constant.** Rejected — excessive fragmentation.
A constructor and the type it constructs are a single conceptual unit. Splitting them
forces a developer to open two files to understand one thing.
## Consequences
**Easier:** `find . -name "permission_mask.go"` returns exactly the file that defines
`PermissionMask`. A directory listing of `security/` reads as a vocabulary list for
that sub-package's domain. Code review diffs are unambiguous — a change to
`permission_provider.go` is a change to the `PermissionProvider` contract.
**Harder:** Multi-word type names require a deliberate naming decision at the file
level. The snake_case rule eliminates ambiguity (`permission_mask.go` is the only
valid name for `PermissionMask`) but must be applied consistently across all modules.
**New obligations:** Every Einherjar module inherits this convention. When a new type
is added to any module, its file is named after the type. This is not optional — the
compliance test enforces the one-type-per-file structural invariant, and the naming
convention is the human-readable complement to that mechanical check.