Contributing¶
How to propose a change to canon, a record or the docs.
Contributing to canon¶
Thanks for helping. canon has two kinds of contribution: changes to the
engine (Go code) and changes to the canon itself (decision records, rules,
profiles and templates under canon/). Both go through the same checks.
Set up¶
You need Go 1.27 or later. For the full make check you also need
golangci-lint v2, shellcheck,
GoReleaser v2 and uv (for the docs site).
The external linters canon drives (hadolint, ruff, tflint, checkov,
yamllint) are optional for development: tests use a scripted runner, and
canon check reports a checker it cannot find as skipped.
Check your work¶
runs everything CI runs: build, lint, tests with the race detector and the
coverage gate, the one-command-per-file check, doc comments, canon lint,
benchmark and fuzz smoke runs, the strict docs build and goreleaser check.
make fmt fixes formatting, and make docs regenerates docs/reference/
after you change a command or a record.
Quality bars¶
- Coverage: at least 90% of hand-written code, measured by
make cover-gate. Generated files are excluded. Don't write tests for trivial getters to move the number. - Tests: table-driven,
t.Parallel()where safe. Commands are tested throughrootCmd.SetArgswith captured output (seecmd/cmd_test.go). - Doc comments: every function and method, exported or not, has one that
starts with its name, and every package has a package comment
(
make doccheck). - One cobra command per file in
cmd/, named after the command (make check-cmds). - Errors are wrapped with
%wand context where they cross a package. - Benchmarks and fuzzing: every critical path below has a
BenchmarkXxx; every one that reads external input has aFuzzXxxwith a real seed corpus that asserts properties. Commit failing fuzz inputs undertestdata/fuzz/.
Critical paths¶
These parse input, transform data, run in hot loops or resolve config.
Changes to them need a benchmark result (make bench, then compare with
benchstat) and, for parsers, a clean 30-second fuzz run.
| Function | Why it is critical | Benchmark | Fuzz |
|---|---|---|---|
records.Parse |
parses every record file | BenchmarkParse |
FuzzParse |
glob.Match |
runs for every file against every pattern | BenchmarkMatch |
FuzzMatch |
search.Tokenize, (*search.Index).Search |
every decide call |
BenchmarkSearch |
FuzzTokenize |
review.ParseDiff |
parses diffs from agents and git | BenchmarkParseDiff |
FuzzParseDiff |
review.Classify |
runs over every changed line | BenchmarkClassify |
via FuzzParseDiff |
checkers.ParseDockerfile and the Dockerfile checks |
parse user Dockerfiles | BenchmarkParseDockerfile |
FuzzParseDockerfile |
checkers.Pinned, checkers.FindUses |
parse workflow files | covered by tests | FuzzPinned |
checkers.ParseGolangci, ParseRuff, ParseHadolint, ParseTFLint, ParseCheckov, ParseYamllint |
parse linter output | BenchmarkParseGolangci |
FuzzParseToolOutput |
findings.Normalize |
sorts and dedupes every result | BenchmarkNormalize |
n/a |
hook.ParseInput |
parses Claude Code hook payloads | n/a | FuzzParseInput |
(*engine.Engine).Check, Decide |
the path every front door takes | BenchmarkCheckBuiltin, BenchmarkDecide |
n/a |
cmd.initConfig |
config precedence: flag > env > file | tested in cmd/cmd_test.go |
n/a |
Changing the canon¶
- One opinion per record, in
canon/records/<scope>/<id>.md. The file name matches theid. Required sections: Context, Decision, Consequences. - New records start as
status: proposed. Thecanon-new-recordMCP prompt drafts one in the right shape. - A record's
rules:and each rule'srecord:incanon/rules/rules.yamlmust point at each other. Every rule traces to a record; there are no orphan lint rules. - To retire advice, add a new record that lists the old one in
supersedes:and set the old one's status tosuperseded. - Templates under
canon/templates/must pass their own canon: the tests scaffold every kind and fail on any error. - Run
go run . lintandmake docs.
Commits and pull requests¶
Commit messages and PR titles follow Conventional Commits:
feat(checkers): add a built-in check for unpinned pip installs
fix(review): count renamed files as modified
docs(records): explain why distroless supersedes alpine
PRs are squash-merged, so the PR title becomes the commit. Every PR needs a
review from a code owner (see .github/CODEOWNERS) and green CI. Fill in
the pull request template's checklist.
Questions¶
Open an issue with the feature request form, or ask in the PR you're working on.