- 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)
162 lines
10 KiB
Markdown
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.
|