Files
contracts/docs/adr/ADR-002-file-per-type-naming.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.4 KiB

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.