template/CLAUDE.md
djmil 13f6a6812a result.Wrap - propagate a failed Expect into a new type U
- public API streamline
- Failf[T]("msg") -	originate a failure from a message; embed a cause with %w
- Err[T](err) - sets .err verbatim (the return zero, err / sentinel case)
2026-06-14 11:43:23 +00:00

162 lines
10 KiB
Markdown

# CLAUDE.md — Agent Instructions
This file is read automatically by Claude Code at the start of every session.
Keep it concise — the agent needs signal, not essays.
---
## Project overview
Go 1.25 template for PoC, hobby projects, and small publishable packages.
Stack: structured logging (slog), config (flag), consumer-defined interfaces + manual fakes,
result type (happy-path error handling), linting (golangci-lint), security scanning (gosec, govulncheck).
Key constraint: `go.mod` stays free of dev tool deps (tools are pinned in `tools.versions` and run via
`go run tool@version`) so packages published from this repo have a clean module graph for consumers.
---
## Design philosophy
**Form over mechanism** — call sites express *what* the code is doing, not *how* it achieves it.
Mechanism belongs in constructors and infrastructure; intent belongs at the call site.
When you see `fmt.Fprintln(os.Stderr, ...)` or `if err != nil { return }` scattered through
business logic, that is mechanism leaking into intent — move it to the edges.
**[The Twelve-Factor App](https://12factor.net) is the primary reference for default implementation choices.**
Follow its recommendations unless circumstances force a deviation — and document the reason when you do.
Key factors that shape this codebase most directly:
- **[Logs (factor XI)](https://12factor.net/logs)** — the app writes log events to `stderr` as an unbuffered stream.
It never opens log files or manages rotation. The execution environment (shell, systemd, Docker, k8s)
routes and stores the stream. Human-readable program output goes to `stdout` (`fmt.Print*`).
- **[Config (factor III)](https://12factor.net/config)** — all configuration comes from the environment
(flags in this template; env vars are the 12-factor ideal). No hard-coded values in logic packages.
- **[Dependencies (factor II)](https://12factor.net/dependencies)** — explicit declaration; dev tools are
pinned in `tools.versions` and kept out of `go.mod` so published packages have a clean module graph.
---
## Project structure
```
cmd/app/ CLI entrypoint: parses flags, wires dependencies, and expresses
what the program does in high-level readable steps — calls internal/
and pkg/ packages; no domain logic lives here
internal/ domain-specific packages; logical grouping of substantial code into
digestible types and interfaces; not importable outside this module
pkg/ generic, publishable packages reusable across projects; no
assumptions about the calling application
tools.versions Pinned tool versions (sourced by Makefile and pre-push hook)
.golangci.yml Linter rules
.githooks/pre-push Runs gofmt + go vet + golangci-lint + gosec before push
```
---
## Project rules
- **Module imports** — always use the full module path from `go.mod` (never relative imports)
- **Packages** — `cmd/` owns CLI parsing, dependency wiring, and high-level orchestration; domain logic belongs in `internal/`
- **Types** — expose concrete types from constructors (`New(...) *Type`); never wrap in an interface at the implementation site. Consumers define their own interfaces if they need one (Go's implicit satisfaction makes this free)
- **Errors** — `pkg/result` is a convenience tool for removing error-threading clutter from application logic; use it as follows:
- `pkg/` libraries **only return** `result.Expect[T]` — never call `.Expect()`, `.Must()`, or `.Expectf()` inside library code; those methods exit the goroutine via `runtime.Goexit` and are only safe in application-layer code protected by a boundary
- application code (`cmd/`, HTTP handlers, etc.) chains `.Expect("context")` freely — each call exits the goroutine on failure and is caught at the entry point
- top-level entry points defer `result.Catch(&err)` (or use `result.Run(...)`) to convert any result exit into a normal Go error; genuine runtime panics (nil-deref, etc.) are re-panicked
- **`result.Catch` is incompatible with `-tags result_goexit`**: it relies on `recover()` which cannot intercept `runtime.Goexit`; prefer `result.Run`/`result.Go` which work in both builds
- bridge existing `(T, error)` stdlib/third-party calls with `result.Of(...)`: `result.Of(os.ReadFile("cfg.json")).Expect("read config")`
- construct failures with these constructors, each a distinct intent — don't invent more:
- `result.Ok[T](v)` / `result.Err[T](err)` — the two field constructors: a success value, or a bare error boxed verbatim (use `Err` for `return zero, err` and sentinels)
- `result.Failf[T]("msg")`*originate* a failure from a message; embed a cause with `%w` (`result.Failf[T]("load config: %w", err)`)
- `result.Wrap[U](r, "msg")`*propagate* an already-failed `Expect` into a new type `U`, optionally adding context; only valid on a failed result (panics on a success), so guard with `if r.Err() != nil`. Use this instead of pulling the error out with `r.Err()` by hand
- use `result.StackTrace(err)` to retrieve the capture-site stack from a caught error
- still use `fmt.Errorf("context: %w", err)` when wrapping errors *before* constructing a `result.Err`
- **Logging** — logs go to `stderr` per 12-factor XI; human output goes to `stdout` via `fmt.Print*`.
Use `logger.NewCLI(level, debugFile)` for CLI apps: auto-detects TTY → human text on terminal,
JSON when piped. Use `logger.New(level)` for headless services that always want JSON.
Passing a non-empty `debugFile` enables debug mode: INFO/WARN/ERROR on screen plus a full
JSON DEBUG trace to file for post-run investigation.
Use `log.WithField("key", val)` for structured context; never `fmt.Sprintf` in log messages
- **Config** — all configuration parsed in `cmd/app/config.go` (flags); no hard-coded values in `internal/` or `pkg/` packages
---
## Code style
- Follow `gofmt` + `goimports` formatting (enforced by linter and git hook)
- Imports: stdlib → blank line → external → blank line → internal (goimports handles this)
- Error variables: `err` for local, `ErrFoo` for package-level sentinels
- Constructors: `New(deps...) *Type` pattern
- Comment every exported symbol (golangci-lint will warn if missing)
- Max line length: 120 chars (configured in `.golangci.yml`)
---
## Testing rules
- Tests use only the standard `testing` package — no third-party assertion libraries
- Test files: `package foo_test` (black-box) unless white-box access is needed
- Fake dependencies with **manual fakes** (implement the interface inline in `_test.go`)
- Use `logger.NewNop()` when the test doesn't care about log output
- Use `logger.NewWriter(&buf, "debug")` in acceptance / integration tests that need to assert on log content
- Table-driven tests with `t.Run("description", ...)` for multiple cases
- The race detector is enabled in CI (`make test-race`); don't introduce data races
- Never use `time.Sleep` in tests; use channels or `t.Cleanup`
- Use `gitea.djmil.dev/go/template/pkg/testutil` helpers instead of manual checks — `ResultOk`, `ResultOkNotNil`, `ResultErr` for `result.Expect[T]`; `NoError`, `Error`, `ErrorContains`, `Equal` for plain values
---
## Development commands
```bash
make init # first-time setup: fetch deps, configure git hooks
make tools # install tool binaries to GOPATH/bin (versions from tools.versions)
make build # compile to ./bin/app
make run # go run with default flags
make test # run all tests
make test-race # tests + race detector
make lint # go vet + golangci-lint
make lint-fix # go fix + golangci-lint auto-fix
make security # gosec + govulncheck
make release # list releases, or tag+push after full checks (make release VERSION=v0.1.0)
make clean # remove bin/
```
---
## Adding new features (checklist)
1. Write the implementation in `internal/<domain>/` — return a concrete `*Type`, no interface at the implementation site
2. In the *consumer* package (or `_test.go`), declare a minimal interface covering only the methods you call
3. Write unit tests using a manual fake that satisfies that interface
4. Wire the concrete type in `cmd/app/main.go`
5. Run `make lint test` before committing
---
## Known pitfalls
- `govulncheck` makes network calls; excluded from pre-push hook (run manually)
- Tool versions live in `tools.versions` — edit that file to upgrade, both Makefile and hook pick it up
- `go run tool@version` is used in lint/security targets; Go caches downloads so subsequent runs are fast
- `make tools` installs binaries to `GOPATH/bin` for IDE integration (e.g. dlv for the debugger)
- `go fix` rewrites source files; run `make lint-fix` before committing after a Go version bump
---
## Recent work
<!-- Agent: append a dated bullet when completing a significant chunk of work.
Keep this section to ~10 entries; remove stale items.
Format: YYYY-MM-DD — what was done and why. -->
- 2026-03-29 — Stripped to stdlib-only: removed testify/mockery→manual fakes, zap→slog, viper→flag.
- 2026-03-29 — Pre-commit hook moved to pre-push; go vet + go fix added to lint pipeline.
- 2026-03-29 — Fixed stale docs and .golangci.yml local-prefixes; .vscode launch configs use CLI flags.
- 2026-04-01 — Replaced tools.go/go.mod pinning with tools.versions + go run tool@version; go.mod is now free of dev tool deps.
- 2026-04-01 — Added make release: lists tags with no args; validates semver, runs test-race+lint+security, then tags+pushes.
- 2026-04-23 — Documented result layering rule: pkg/ libraries only return Expect[T]; .Expect()/.Must() calls belong in application-layer code.
- 2026-06-03 — pkg/logger v0.4.0: replaced NewDevelopment with NewCLI(level, debugFile); two-mode model (human text on TTY / JSON when piped); debug file mode; IsInteractive() helper. Established "form over mechanism" as core design principle.
- 2026-06-13 — Build stamping + multi-binary build: internal/buildinfo (Version, Commit, BuildTime injected via -ldflags); make build discovers all cmd/* via find and produces named binaries in ./bin/; make run replaced with make run/<name> pattern; devcontainer adds ./bin to PATH via ${containerWorkspaceFolder}.
- 2026-06-14 — pkg/result: reworked the failure surface into four intent-split constructors — Ok/Err (field constructors: value / bare error), Failf (originate from a message, %w for a cause), Wrap[U](r, "msg") (propagate a failed Expect into a new type; panics on success). Removed Errf/Errw. Wrap eliminates the r.Err() unwrap-rewrap dance; behavioral guarantees covered in pkg/result/wrap_test.go.