Thank you for considering a contribution. This guide explains how to set up the project, run tests, and submit changes.
Requirements: Go 1.26+, golangci-lint v2, gofumpt
git clone git@github.com:propifly/primkit.git
cd primkit
make all # tidy → fmt → lint → test → build → docs-check → check-registrationThe project uses a Go workspace (go.work) with five modules:
primkit/— shared library (config, auth, db, server, replicate)taskprim/— task management primitivestateprim/— state persistence primitiveknowledgeprim/— knowledge graph primitivequeueprim/— work queue primitive
make test # all modules, verbose, race detector
make build # compile all four binaries
make lint # golangci-lint across all modules
make fmt # gofumpt across all modules
make all # full pre-PR checklist (recommended before pushing)Individual modules:
cd taskprim && go test ./...
cd stateprim && go test ./...
cd knowledgeprim && go test ./...
cd queueprim && go test ./...
cd primkit && go test ./...Each prim has golden file tests that snapshot CLI output for regression testing. To update golden files after intentional output changes:
cd taskprim && go test -run TestGolden -update ./internal/cli/
cd stateprim && go test -run TestGolden -update ./internal/cli/
cd knowledgeprim && go test -run TestGolden -update ./internal/cli/
cd queueprim && go test -run TestGolden -update ./internal/cli/Review the diff in internal/cli/testdata/*.golden before committing — golden files should only change when output format changes intentionally.
The FTS5 query sanitizer in knowledgeprim has a fuzz test. Run it locally for deeper coverage:
cd knowledgeprim && go test -fuzz=FuzzSanitizeFTS5Query -fuzztime=60s ./internal/store/CI runs this for 30 seconds on every push.
- Formatter: gofumpt (stricter
gofmt). Runmake fmt. - Linter: golangci-lint v2 with 15 linters enabled. Run
make lint. See.golangci.ymlfor the full configuration. - Each CLI command lives in its own file (e.g.,
add.go,list.go) - Pattern: parse flags → call store → format output
- Tests use
testify/assertandtestify/require - Prefer table-driven tests:
func TestMyFunction(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{name: "basic case", input: "hello", want: "HELLO"},
{name: "empty", input: "", want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := MyFunction(tt.input)
assert.Equal(t, tt.want, got)
})
}
}- In-memory SQLite for all tests (
db.OpenInMemory())
Use Conventional Commits:
<type>(<scope>): <description>
Types: feat, fix, docs, style, refactor, test, ci, chore
Scopes: taskprim, stateprim, knowledgeprim, queueprim, primkit, or omit for cross-cutting changes.
Examples:
feat(taskprim): add restore command for point-in-time recoveryfix(knowledgeprim): quote hyphenated terms in FTS5 queriesci: add govulncheck to CI pipelinedocs: update CONTRIBUTING with conventional commits
One logical change per commit. Keep commits focused.
- Fork the repo and create a branch from
main - Write tests for new functionality
- Run
make all— it must pass with no failures - Open a PR with a clear description of what changed and why
Release binaries are signed with cosign via keyless signing (Sigstore OIDC). To verify a release:
# Download the release assets (checksums + signature + certificate)
cosign verify-blob \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp 'github\.com/propifly/primkit' \
checksums.txtBefore making significant changes, read docs/architecture.md to understand the layered design. The key constraint: store interfaces are the boundary — CLI, API, and MCP are sibling consumers that never depend on each other.
Use GitHub Issues. For bugs, include:
- What you expected to happen
- What actually happened
- Steps to reproduce
- Go version and OS