Each CLI sub-command has its own file in a sub-package under cmd/ledgerctl/:
cmd/ledgerctl/ledgers/command.go- Parent commandcmd/ledgerctl/ledgers/create.go-ledgers createcmd/ledgerctl/ledgers/list.go-ledgers listcmd/ledgerctl/ledgers/get.go-ledgers get
Each HTTP handler has its own file in internal/adapter/http/:
handlers_create_ledger.go-POST /{ledgerName}handlers_get_ledger.go-GET /{ledgerName}handlers_create_transaction.go-POST /{ledgerName}/transactions
All methods (receiver functions) for a given struct must be defined in the same file as the struct definition. Standalone functions (constructors, helpers) may live in separate files within the same package. If a file grows large because its struct has many methods, extract sub-types (composition) rather than splitting methods across files.
- Packages: lowercase, single word
- Types: PascalCase
- Public functions: PascalCase
- Private functions: camelCase
- JSON properties: camelCase (e.g.,
ledgerName,nextLogId,createdAt)
Group variable declarations in a block:
// Good
var (
address string
metadataJSON string
volumesJSON string
)
// Bad
var address string
var metadataJSON string
var volumesJSON stringAlways handle errors explicitly:
// Good
data, err := json.Marshal(obj)
if err != nil {
return fmt.Errorf("failed to marshal: %w", err)
}
// Good - explicit discard with reason
_ = file.Close() // Best effort cleanup
// Bad - silent ignore
data, _ := json.Marshal(obj)Prefer adding parameters to existing methods over creating separate methods. When adding a boolean mode (dry run, force, preview), add it as a parameter to the existing method rather than creating a new method.
Never use type aliases (type X = Y). Always use the original type directly.
Avoid global variables for command flags. Use a struct to hold options and extract values in the RunE function.
Before completing a PR candidate, run the base-sensitive local selector:
AI_REVIEW_BASE_SHA=<base-sha> bash scripts/agent-check-prFor relevant inputs this runs the repository's straightforward pre-commit
recipe once, followed by focused validation. Documentation-only changes skip
Go normalization. The pinned Nix entry point clears inherited GOROOT and does
not use a login shell. See Local validation.
After any change to interfaces annotated with //go:generate mockgen, regenerate mocks:
go generate ./...Do not maintain a static interface inventory here; annotations are the source of truth and new interfaces are added regularly. Discover them with:
rg -n '^//go:generate mockgen' --glob '*.go'Always run go generate ./... rather than generating only the interface you
remember changing, so every annotated package stays synchronized.
Use mockgen source mode (-source <file>) for interfaces declared in this
repository, including interfaces from another package (-source ../../infra/backup/storage.go). Package mode (an import path or . followed
by interface names) type-checks the whole dependency graph from source on every
call and costs roughly a second per directive; source mode parses one file
in about 0.1 seconds.
Source mode mocks every interface in the file and ignores trailing interface
names. Exclude the others with -exclude_interfaces, for example the server
interfaces in a *_grpc.pb.go file. Keep package mode only where source mode
cannot produce the mock: interfaces from external modules, such as
google.golang.org/grpc streams, and type aliases of generic instantiations,
such as readstore.EntityIterator. Generate several interfaces from one
package-mode import path in a single directive so the package loads once.
-
Create
cmd/ledgerctl/<group>/<action>.gowith aNew<Action>Command() *cobra.Commandconstructor returning a fresh command. SetUse, help text, an explicitArgsvalidator, andRunEfor execution. Keep flags and options local to the constructor or an options struct; do not use global command or flag variables. -
Define positional completion with
ValidArgsFunction(usecobra.NoFileCompletionswhen appropriate), and register completion for flags that offer known choices or resource names. Reusecmdutilhelpers for shared flags, connections, completion, and error handling. Return errors fromRunE; follow the existingcmdutil.CLIErrorconvention for errors already displayed so the root does not print them twice. -
Explicitly call
cmd.AddCommand(New<Action>Command())in the parent'sNewCommand()incmd/ledgerctl/<group>/command.go. For a new top-level group, also register its constructor innewRootCommand()incmd/ledgerctl/main.go. Constructors andAddCommandassemble the tree; there is no automaticinit()registration. -
Test the command's behavior and add root-level contract coverage where registration matters: resolve the full command path, check argument validation and relevant flags/completion, and exercise error propagation through the real command tree. Keep assertions independent of exact help or error wording.
-
Update
docs/ops/cli.mdwhen commands, flags, or behavior change, and regenerate the relevant demo GIFs when applicable.
- Create
internal/adapter/http/handlers_<action>.gowith the handler function - Register the route in
internal/adapter/http/handler.go
- Code compiles without errors
- Tests pass (unit, integration, e2e)
- Documentation updated if necessary
- OpenAPI updated if new API
- Protobufs regenerated if modified
- No
time.Sleepin tests outside atesting/synctestbubble - Errors handled appropriately