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

79 lines
3.4 KiB
Markdown
Raw Normal View History

# 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.