@@ -0,0 +1,100 @@
|
||||
# Conventions
|
||||
|
||||
The style floor. Do not ask about anything here — read it and comply. Disagreement is legitimate but
|
||||
goes through an ADR, not a diff.
|
||||
|
||||
## Language and dependencies
|
||||
- Go, current stable release. Stdlib first, every time.
|
||||
- `net/http`, `html/template`, `log/slog`, `os`, `io/fs`, `embed`. No web framework, ORM, router
|
||||
library, or config library — flags plus environment variables, parsed in one place. The site root
|
||||
(`-site`, `ATELIER_SITE`) is the only required setting; default templates are `embed`ded so a bare
|
||||
site root renders (ADR-0011).
|
||||
- New dependency = ADR + human approval + `scripts/allowed-deps.txt`. `verify.sh` enforces it.
|
||||
- Prefer 40 lines of obvious code over a dependency doing it in one call — unless the 40 lines would be
|
||||
subtly wrong in cases the author cannot predict, which is why YAML is a dependency (ADR-0020).
|
||||
|
||||
## Package layout
|
||||
```
|
||||
cmd/atelier/ main, flag parsing, explicit wiring — the only place things are assembled
|
||||
internal/content/ bundles, frontmatter, slugs, queries — knows the disk, not HTTP
|
||||
internal/render/ markdown, transforms, templates — knows content, not HTTP
|
||||
internal/web/ handlers, routing, headers, caching — knows both, exposes neither
|
||||
internal/ext/ extensions, one package each — earned at its counter (see extensions.md)
|
||||
```
|
||||
Dependencies point inward. `internal/content` imports nothing from the others; `cmd` imports everything
|
||||
and is imported by nothing. A feature under `internal/ext/` may import `internal/content` and
|
||||
`internal/render`, and must not import `internal/web`, `cmd/`, or **another feature** — sibling imports
|
||||
are what make an agent's read set compound (ADR-0027). Every `internal/ext/*` package carries a `doc.go`;
|
||||
`verify.sh` fails without one. Routes reach `web` by assembly in `cmd/atelier/wire.go`, so `web` never
|
||||
learns features exist. No `utils`, `helpers`, `common`, `shared`, `manager`, `base`,
|
||||
`impl`, `core` — a package name not describing a domain is a smell. Flat until a package exceeds
|
||||
`FILE_LOC_WARN`; do not pre-partition, and a single-file package therefore warns at the same point it
|
||||
wants splitting.
|
||||
|
||||
## Naming and shape
|
||||
- Functions under `FUNC_LOC_WARN`, ideally under 20. Nesting depth under 4. File length is not a
|
||||
target: one coherent file beats two split to satisfy a counter. Values: `scripts/budgets.env`.
|
||||
- No `init()`. No package-level mutable state. No singletons. Wire explicitly in `cmd`.
|
||||
- Accept interfaces only where a second implementation exists; return concrete types.
|
||||
- `ctx context.Context` first when a call can block or be cancelled — not decoratively.
|
||||
- Never call `time.Now()` outside a `clock.go`. Stages read the clock through an injected accessor and
|
||||
declare a validity window with it (ADR-0013); `verify.sh` fails on any other caller, because a
|
||||
forgotten window serves staleness silently.
|
||||
- Comments explain *why*, never *what*. Delete a comment narrating the next line. One stating a
|
||||
non-obvious invariant is worth ten describing control flow.
|
||||
|
||||
## Documentation
|
||||
|
||||
Written for a maintainer working alone, years from now, with no agent to explain anything. `verify.sh`
|
||||
enforces presence; only a human can enforce that it says something.
|
||||
|
||||
- **Every package has a package comment.** What it owns, what it does not, and which packages may import
|
||||
it. For `internal/ext/*` that is the four-line `doc.go` shape in `extensions.md`.
|
||||
- **Every exported identifier has a doc comment.** The exported surface of a 2000-line core is small, and
|
||||
`go doc ./...` is the only navigation tool that still works when nothing else does. A comment that
|
||||
restates the name (`// Load loads.`) is worse than none: say what it returns on absence, what it costs,
|
||||
what it assumes.
|
||||
- **Cite the ADR where the decision lives in the code.** `// Path shape: ADR-0008.` at the point that
|
||||
builds a URL, `// Visibility inherits: ADR-0024.` at the guard. Twenty-seven decisions are unreachable
|
||||
from code otherwise, and the next maintainer changes something whose reasoning they never saw.
|
||||
`verify.sh` fails on a citation naming an ADR that does not exist.
|
||||
- **Comment the trap, not the mechanism.** NFC normalisation, the settle window, parse order for template
|
||||
overrides, the `old ∪ new` membership rule — each is a line of code that looks arbitrary and is not.
|
||||
Those are the comments worth writing.
|
||||
|
||||
## Errors
|
||||
- Wrap with `%w` at package boundaries, with operation and path: `parse %s: %w`.
|
||||
- Never log and return the same error. Handle it, or return it.
|
||||
- Request-time render failure degrades: log, serve what exists, never 500 on a missing field.
|
||||
Startup failure is fatal and loud. Content authoring errors name the file and line.
|
||||
|
||||
## Tests
|
||||
- **Behaviour ships with a test.** A change to `cmd/` or `internal/` carries a `_test.go` change in
|
||||
the same commit; `verify.sh` fails otherwise. Test the observable contract, not coverage for its own
|
||||
sake — one test proving the new behaviour is enough, and a refactor with no behaviour change needs
|
||||
only the existing tests to still pass (touch them or say why not).
|
||||
- Table-driven. Golden files in `testdata/`, regenerated behind a `-update` flag.
|
||||
- Test the observable contract: URL in → bytes out; file on disk → page struct. Do not test private
|
||||
helpers, and do not add a seam solely to make something testable.
|
||||
- Needing a mock means the design is probably wrong — use a `testdata` directory with
|
||||
`os.DirFS`/`fstest.MapFS`.
|
||||
- One end-to-end test per route beats ten unit tests of render internals.
|
||||
|
||||
## Performance
|
||||
Correct and small first; fast where measured. The render path is the only hot path.
|
||||
- Write to `io.Writer`, never build pages by string concatenation.
|
||||
- Parse templates once at startup; never per request.
|
||||
- No `sync.Pool`, caching layer, or goroutines in the render path until a benchmark justifies it and
|
||||
the number goes in the commit message.
|
||||
- No reflection in the hot path. `Extra` lookups are map reads, not reflection.
|
||||
- Benchmarks live next to what they measure, added only when a decision depends on them.
|
||||
|
||||
## Frontend output
|
||||
Semantic HTML working with zero JavaScript. Progressive enhancement only. No build step for CSS.
|
||||
Page-specific styles and scripts come from the bundle (`styles`/`scripts` frontmatter). Respect
|
||||
`prefers-reduced-motion`. Every image gets width, height, and alt. Payload discipline is a feature of
|
||||
this project, not an optimisation.
|
||||
|
||||
## Git
|
||||
One feature per commit. Imperative subject under 72 characters; body says *why*. Doc updates ride in
|
||||
the same commit as the code that made them true.
|
||||
Reference in New Issue
Block a user