AGENTS.md — pixe
Build & Run
make build # GoReleaser snapshot → ./pixe (production build)
make build-debug # go build with debug symbols (for dlv)
make run ARGS="sort -h" # build + run with arguments
make clean # remove binary, dist/, coverage files
Test Commands
make test # unit tests only (excludes integration/)
make test-all # all tests including integration
make test-integration # integration tests only
# Single test by name:
go test -race -timeout 120s ./internal/pipeline/ -run TestRun_basicSort -v
# Single package:
go test -race -timeout 120s ./internal/archivedb/...
# With coverage:
make test-cover # prints coverage summary
make test-cover-html # opens HTML report in browser
Always use -race when running tests. The Makefile sets TEST_FLAGS := -race -timeout 120s.
Lint & Format
make lint # golangci-lint run ./...
make vet # go vet ./...
make fmt # gofmt -w -s .
make fmt-check # check formatting without modifying (CI gate)
make check # fmt-check + vet + unit tests (fast pre-commit gate)
CI runs golangci-lint and go test -v -short ./... on every push/PR to main.
Code Style
File Header
Every .go file starts with the Apache 2.0 copyright block:
// Copyright 2026 Chris Wells <chris@rhza.org>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// ...
Package Comments
Every package has a doc comment on the package line or in a doc.go. Use // style, not /* */. Describe what the package does and any key design decisions:
// Package archivedb provides SQLite-backed persistence for the Pixe archive
// database. It replaces the earlier JSON manifest with a cumulative registry
// that tracks all files ever sorted into a destination archive across all runs.
package archivedb
Imports
Three groups separated by blank lines: stdlib, external, internal. Sorted alphabetically within each group. Use named imports only when there's a collision:
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/cwlls/pixe/internal/archivedb"
copypkg "github.com/cwlls/pixe/internal/copy" // named: avoids collision with builtin
jpeghandler "github.com/cwlls/pixe/internal/handler/jpeg" // named: handler packages share type names
)
Handler packages use the convention <format>handler for their import alias (e.g., jpeghandler, heichandler, dnghandler).
Naming
- Packages: lowercase, single word when possible (
archivedb,discovery,pathbuilder). - Exported types:
PascalCase. Structs use noun names (Registry,Hasher,DB). - Constructors:
New()returns the primary type.NewFoo()if the package has multiple types. - Methods: receiver name is 1–2 letters derived from the type (
db,h,r,m,sw). - Constants:
PascalCasefor exported,camelCasefor unexported. Status enums useStatusprefix:StatusPending,StatusComplete,StatusFailed. - Test helpers: unexported, use
t.Helper(). Common pattern:openTestDB(t),copyFixture(t, dir, name),writeFile(t, path, content).
Types & Structs
- Use
*string,*time.Time,*int64for nullable DB fields (maps tosql.NullStringetc. in scan). - Config structs (
AppConfig,SortOptions) are passed by pointer. - Domain types live in
internal/domain/— shared across packages without import cycles. - The
FileTypeHandlerinterface is the extension point for new formats (Section 6 of the architecture).
Error Handling
- Wrap errors with
fmt.Errorf("context: %w", err)— always include the package or function as context prefix. - Use
errors.Is()for sentinel checks (e.g.,errors.Is(err, os.ErrNotExist)). - DB scan errors: return
(nil, nil)forsql.ErrNoRowswhen "not found" is a valid result. - Non-fatal errors (e.g., tag failure): log a warning to
outand continue. Do not return an error. - Fatal errors: return the error up the call chain. The CLI layer (
cmd/) prints it and exits.
// Pattern: wrap with package prefix
return nil, fmt.Errorf("archivedb: insert run: %w", err)
// Pattern: nil-nil for not-found
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
Testing
- Tests use the stdlib
testingpackage only — no testify or other assertion libraries. - Test files are in the same package (white-box testing):
package pipelinenotpackage pipeline_test. - Use
t.TempDir()for filesystem tests — auto-cleaned. - Use
t.Helper()on all helper functions. - Test names:
TestTypeName_methodOrBehavior(e.g.,TestRun_basicSort,TestCheckDuplicate_notFound). - Table-driven tests use anonymous struct slices with
want/gotpattern. - Integration tests live in
internal/integration/and are excluded frommake test. - Locale-sensitive tests pin locale via
TestMain:pathbuilder.SetLocaleForTesting(language.English).
SQL
- SQL strings use backtick-quoted
constblocks with the query name as the const name. - Use
?placeholders (SQLite positional params). - All DDL uses
IF NOT EXISTSfor idempotency. - Schema migrations are additive (
ALTER TABLE ADD COLUMN), never destructive.
Concurrency
- Worker pool pattern: coordinator goroutine owns DB writes; workers own I/O.
syncWriterwrapsio.Writerwith a mutex for concurrent stdout access.- Per-worker assignment channels (buffered 1) for coordinator→worker communication.
context.Contextthreaded through concurrent paths for cancellation.
Project Layout
cmd/ CLI commands (Cobra). No business logic here.
internal/
archivedb/ SQLite database (schema, CRUD, queries)
benchmark/ Centralized benchmark suite (hash, copy, db, discovery, pathbuilder)
cli/ Bubble Tea progress bar model and Lip Gloss styles
config/ AppConfig struct (populated by cmd/, read by pipeline/)
copy/ File copy + verify
dblocator/ Database path resolution (local vs network mount)
discovery/ File walking + handler registry
docgen/ Development-time doc generation tool (marker-based injection)
domain/ Shared types (FileStatus, Ledger, Manifest, FileTypeHandler)
fileutil/ Shared file-path utilities (extension normalization)
handler/ FileTypeHandler implementations (jpeg/, heic/, mp4/, tiffraw/, dng/, ...)
hash/ Configurable hasher (MD5, SHA-1, SHA-256, BLAKE3, xxHash)
ignore/ Glob-based file ignore matching with .pixeignore support
integration/ End-to-end tests (excluded from `make test`)
manifest/ JSON ledger/manifest persistence
migrate/ Legacy JSON manifest → SQLite migration
pathbuilder/ Destination path construction (date-based, locale-aware)
pipeline/ Sort orchestrator (sequential + concurrent)
progress/ Pipeline event bus (typed events, non-blocking channel, plain text writer)
tagging/ Metadata tag injection (dispatch via handler capability)
verify/ Post-sort verification
xmp/ XMP sidecar file generation (Adobe-compatible)
Key Architectural Rules
dirAis read-only. Only.pixe_ledger.jsonis written there. Never modify source files.- No external binaries. No
os/exec. All parsing is pure Go. - Copy-then-verify. Every file is re-hashed after copy. Mismatches are flagged, never silent.
- No Viper below
cmd/. Onlycmd/reads Viper. Everything else receives*config.AppConfig. - Version from git tags. No version literals in source. Injected via ldflags at build time.