Repository conventions
The rules this repository follows, and why β an audit of the codebase once found seven kinds of drift that all traced back to the same cause: nobody had written the rule down, so each new feature picked a reasonable answer and the answers disagreed. Every rule below carries its reason, because a rule without one is a rule the next contributor is entitled to ignore.
Where code goes
Repo root β package breeze. The HTTP core and nothing else: router, Context, request/response, the Breeze server, worker pool, WebSocket
engine, template engine, i18n, the error type, Auto-MCP route registration.
A file belongs at root only if it is part of serving an HTTP request or is
required to construct the server β everything else is a subpackage, even
when that means a package with three files. The root package is what import "github.com/nelthaarion/breeze/v2" gets, and every symbol in it is
in every user's namespace: adding to it costs everyone, adding a subpackage
costs only its importers.
Subdirectories of root β public subsystems. One directory per
subsystem, importable directly, and each can be left out of an application
entirely: binding, client, dashboard, diag, events, fleet, mcp, middlewares, migrate, observability, rpc, scalar, video, workflow. Nesting one level deeper is allowed only when the nested
packages are alternatives, not layers β fleet/transport/{httptransport,wstransport,eventtransport,gnettransport} are four implementations of one interface, chosen by configuration.
Nesting to express "part of" is what files in a package are for instead.
internal/ β machinery with no public API. internal/generator (the
CLI's implementation), internal/mcp (the MCP server's implementation), internal/mcpcmd (flag parsing shared by two binaries). Go enforces that
nothing outside the module can import them β exactly the point, since
these packages change shape whenever a tool grows a feature, and no user
should be able to depend on that shape. The public faΓ§ade for anything in internal/ is a thin wrapper: mcp/ exports ServeInProcess over internal/mcp.
cmd/ β one directory per binary, never a loose .go file directly in cmd/:
| Pattern | Meaning | Current members |
|---|---|---|
cmd/<tool> | a shipped binary a user installs | breeze, breeze-mcp |
cmd/<subsystem>-example | a runnable demonstration of one subsystem | dashboard-example, workflow-example, video-example, fleet-example, templates-example, events-example, automcp-example |
cmd/<name> (neither) | a deployable that is not the CLI | fleet-aggregator |
The -example suffix is load-bearing: it's how a reader scanning cmd/ tells "this is how you use the framework" from "this is the framework".
Nested modules. A package may carry its own go.mod only to keep a
heavy third-party dependency out of the framework's dependency graph.
Currently one does β fleet/transport/eventtransport/backends/kafka, so kafka-go is opt-in. The cost: go build ./... and CI don't see it, so a
nested module must be built explicitly by CI or it rots invisibly.
File naming within a package
| File | Holds | Required when |
|---|---|---|
doc.go | the package doc comment, nothing else | public and more than a trivial API |
config.go | the Config struct, its defaults, its validation | the package has a Config |
types.go | wire/DTO types with no behaviour | there are enough to make a home worthwhile |
errors.go | sentinel errors and error types | the package exports either |
diag.go | the diag probe and its registration | the package registers a probe |
<concern>.go | one concern | always |
Two file-name styles coexist and both stay: internal/generator and internal/mcp use snake_case with a shared prefix grouping related files
(tools_*.go, generate_*.go) because both packages have 40+ files and the
prefix is what makes ls readable; everything else uses single flat words
(dispatch.go, tracer.go) because under 20 files a prefix is noise. Do
not mix within one package.
Tests: <file>_test.go next to the code it tests; one bench_test.go per package, not zzperf_bench_test.go or a per-subsystem name, so go test -bench is predictable β though Benchmark**ZZ**β¦ function names stay, since every recorded baseline in CHANGELOG.md is keyed to
them, and renaming would make a benchstat comparison report "no
benchmarks" rather than a regression. Fixtures live inline with t.TempDir(); testdata/ is only for assets a Go test cannot construct.
Known deviation, kept deliberately: middlewares/ declares package middleware (singular); every import site aliases it. Renaming
either breaks every existing user's import line β it stays.
Naming inside code
- Receivers: one name per type, everywhere β
*Breezeiss,*Routerisr,*Collectorisc,*Tracerist. - Config variables:
cfgβ notconfig(shadows the package name ininternal/generator), notc(collides with the collector and the context receiver). - Abbreviations:
ctx,cfg,req,res,err,buf,n,i. Spell out anything not on that list. - Sentinel errors:
ErrFoo. One exception,events.Stopβ exported API a listener returns, and its call sites read as English:return events.Stop. - Never shadow a package name with a local β
config,template,url,path,timeas variable names are all bugs waiting to happen.
Error handling
Wrap with %w when returning an error caused by another error:
if err := os.WriteFile(path, body, 0o644); err != nil {
return fmt.Errorf("write %s: %w", path, err)
} errors.Is and errors.As are the only way a caller can react to a
specific failure, and both walk the %w chain β fmt.Errorf("...: %v", err) produces the same string and silently deletes that ability.
Use a bare sentinel when the error is the whole fact: var ErrBusClosed = errors.New("events: bus is closed").
Don't wrap when the error text is the product, not a diagnosis of a
cause β binding's validation errors are shown to a client as an RFC 9457
body; there's no underlying error to unwrap. binding and middlewares are documented as such deliberately, so the next contributor doesn't "fix"
them into inconsistency with themselves.
Prefix with the package name in errors that escape the package: "events: bus is closed", "video: malformed range header".
Never swallow. If an error genuinely cannot be handled, assign it to _ with a comment saying why:
_ = conn.Close() // response already written; a close error changes nothing Example projects
Every example is cmd/<subsystem>-example/:
cmd/<subsystem>-example/
βββ main.go # package main, with a package doc comment
βββ README.md # required
βββ <name>_test.go # if the example demonstrates a claim, assert it
βββ docker-compose.yml # only if it needs more than one process
βββ views/ etc. # only if it needs assets README.md has three sections, in order: what this demonstrates (subsystem features exercised, which framework APIs to look at); how to
run it (the exact, copy-pasteable command); what to look for (the
URLs, the curl calls, what the output should show β an example whose
output nobody described is an example nobody can tell is broken). cmd/fleet-example/README.md is the reference.
An example that is not compiled is an example that rots β all of cmd/... is covered by go build ./... and go vet ./... in CI. An example that makes a claim should test it β compiling proves the
API still exists, not that the behaviour the README describes still holds. cmd/automcp-example is the reference: four claims, four separately named
tests, so a failure says which guarantee broke.
Documentation
Every public subsystem has a doc, reachable from docs/README.md β the
index is the contract: a subsystem not in it is undocumented regardless of
what files exist. docs/<topic>.md for anything spanning packages or
describing an operational workflow; <package>/README.md for a single
package's guide; <package>/doc.go for the package comment, always, in
addition to the above. Minimum content: what it does and what problem it
solves that the standard library doesn't; a quick-start example that
compiles; a configuration reference β every field, its default, what it
affects; how it's exposed through the CLI and MCP, or an explicit
statement that it isn't.
Linting
.golangci.yml at the repo root, run in CI alongside go vet ./... and go test ./.... Enabled beyond the defaults: staticcheck and unused β
the two that found every drift finding that motivated this document, all
of which had sat in the tree for months while CI reported green.
Suppressions carry a reason and live at the site, not in the config: //lint:ignore <Check> <why>, or //nolint:staticcheck // <why>. A bare nolint is not acceptable β the point of a suppression is to record a
decision, and a decision recorded in a config file is attached to a path
rather than to the line that made it. .golangci.yml currently has no
exclusions at all, which is the target state, not an oversight.
gofmt -l .
go vet ./...
golangci-lint run
staticcheck ./... # the bare binary does not read .golangci.yml
go test ./... When these rules and the code disagree
The code is wrong, unless the deviation is listed as deliberate β add to that list in the same change that introduces the deviation, with the reason. A deviation nobody wrote down is indistinguishable from a mistake, which is how this repository accumulated the drift that produced these rules in the first place.
| Deviation | Reason |
|---|---|
middlewares/ declares package middleware | renaming either breaks every existing user's import line |
events.Stop is not ErrStop | exported API a listener returns; return events.Stop reads as English where ErrStop would claim something failed |
the kafka transport is a nested module | pulls a heavyweight client that must not appear in the root go.sum for users who don't want it |
breeze.go:OnTraffic and video/handler.go:serve stay long | measured hot paths β splitting risks the inlining and allocation behaviour the benchmark numbers verify |
Benchmark**ZZ**β¦ function names keep the zz prefix | every recorded baseline is keyed to these identifiers |
env/envInt duplicated across example main packages | they're separate package main programs by design; sharing them would mean a library package existing only for example glue |