Contributing to Stave
Thank you for considering contributing to Stave. This document explains how to set up your development environment, run tests, and submit changes.
Development Environment
Option 1 — Coder workspace (recommended)
Launch a pre-configured workspace with the right Go toolchain,
Steampipe, every test fixture, and pre-built stave/stave-mcp
binaries already in place:
# From a checkout of this repo:
coder templates push stave --directory stave-workspace
coder create my-stave --template stave
Inside the workspace:
make test-fast # < 2 min, the dev-loop test tier
make lint # golangci-lint (pre-installed)
make build # rebuild ./stave after changes
make mcp # rebuild ./stave-mcp after changes
See stave-workspace/README.md for
what the image includes, how to customize it, and the honest
caveats (no code-server, no AWS creds, no extra Steampipe plugins).
The workspace is the lowest-friction contributor path: no Go install
to manage, no toolchain-version drift between contributors, no
"works on my machine" — the image pins everything.
Option 2 — Local setup
If you'd rather work in your own environment:
Prerequisites
- Go 1.26.5 or later
- golangci-lint (optional, for linting)
- Make (for convenience targets)
Setup
# Clone the repository
git clone https://github.com/sufield/stave.git
cd stave
# Verify Go version
go version
# Download dependencies
go mod download
# Build the binary
make build
# Run tests
make test
The local path also works for adopters who only want the CLI on
their host (see README Option 3
for go install instructions that skip the clone).
Running Tests
# Fast dev loop — unit tests only, skips e2e / golden / profile suites.
# Designed to finish under 30 seconds.
make test-fast
# Full local suite — runs everything assuming goldens are current.
# Use this before opening a PR if you want a final local check.
make test
# CI entry point — regenerates goldens fresh, then runs the full suite.
# Goldens are regenerated in the CI workspace and discarded; nothing is
# committed back. You should not need to run this locally.
make test-ci
# Run tests with verbose output
go test -v ./...
# Run tests with coverage
make test-coverage
# Run a specific test
go test -v -run TestEvaluator ./internal/core/...
# Run startup benchmark (informational performance budget)
go test -run '^$' -bench BenchmarkCLIStartupHelp -benchmem ./cmd
Startup target for lightweight commands is approximately <500ms (see BenchmarkCLIStartupHelp in cmd/startup_benchmark_test.go).
Why three test targets
Adding a control changes the policy fingerprint embedded in 2000+ golden fixtures. Regenerating those goldens locally on every control addition is slow churn that adds no signal — the diffs are catalog growth, not behavior. So:
make test-fastis the dev feedback loop. It usesgo test -shortand excludes./e2e/, so any test that gates ontesting.Short()(e2e, profile, fixture-binary determinism) self-skips.make test-ciis what CI runs. It regenerates goldens fresh and then runs the full suite, so behavior regressions are still caught but fingerprint churn never blocks a PR.make testis unchanged — full suite, assumes goldens are current.
If you write a new test that builds the CLI binary, executes it, or
compares against a golden, gate it with if testing.Short() { t.Skip(...) }
so it stays out of the dev fast path.
Parallelizing slow packages
When a package's serial test cost starts dominating CI wall time,
add t.Parallel() to every Test function in the package. The
mechanical edit is scripted:
make parallelize PKG=./internal/core/enginetest
The script (scripts/add-parallel.sh) inserts t.Parallel() as
the first line of every Test* function and skips files that
mutate process-wide state (t.Setenv, os.Setenv, os.Chdir)
to avoid races. The original 6-package rollout (commit 4c7170cf0)
took enginetest from 349s → 33s standalone (10.6×).
After running, verify with go test -race ./<package> to confirm
no races surfaced.
Test Prerequisites
E2E tests (make e2e, which drives go test ./e2e/...) require:
- jq — JSON processor for comparing evaluation output
- diff — standard Unix diff for golden-file comparison
- bash — example invocations use bash-specific features
These are not needed for unit tests (make test), only for E2E validation.
Regenerating fixture goldens
When a change modifies control YAML (metadata, predicate, add/remove
control) the e2e goldens under testdata/e2e/*/expected.* and
testdata/e2e/*/golden.json go stale. Regenerate them with:
make regenerate-goldens
The tool:
- Walks every fixture under
testdata/e2e/. - Picks the correct invocation shape (default
apply,command.txtoverride, or profile-styleapply --profile aws-s3/hipaa). - Writes the updated goldens (
expected.out.json,expected.summary.json,expected.findings.count,expected.exit,expected.input_hashes.json,expected.source_evidence.json,expected.out.sarif,golden.json). - Prints a report bucketed as CLEAN / FINGERPRINT-ONLY / METADATA-ONLY / BEHAVIORAL / MIXED.
Flags are passed via the ARGS variable:
make regenerate-goldens ARGS="-dry-run" # preview, no writes
make regenerate-goldens ARGS="-filter s3-public" # limit to regex match
Interpreting the diff categories:
| Category | What it means | Safe to commit? |
|---|---|---|
| CLEAN | Fixture output unchanged. | Yes — nothing to commit. |
| FINGERPRINT-ONLY | Only run.policy_fingerprint shifted (a new control joined the catalog and changed the per-profile hash; detection behavior is identical). | Yes. |
| METADATA-ONLY | Only projected metadata changed: control_name, control_description, control_compliance*, remediation.*, exposure.*. Detection identical. | Yes. |
| BEHAVIORAL | Findings identity, count, severity, evidence, or summary changed. Detection behavior shifted. | Investigate first. Confirm the shift matches the intended change. |
| MIXED | Both metadata and behavioral paths diffed in the same fixture. | Investigate first. |
The target does not run as part of make check or CI. It is a
developer tool — run it explicitly, review the report, then commit.
Automatic regeneration is what masked the drift-cleanup series bugs.
Code Quality
Before submitting changes, ensure your code passes all checks:
# Run all checks (format, vet, lint, test)
make check
# Individual checks
make fmt # Format code with gofmt
make vet # Run go vet
make lint # Run golangci-lint (if installed)
For Go modernization and dead-code cleanup requirements, follow gofixer.md before opening a PR.
Code Style
Stave follows standard Go conventions:
- Format code with
gofmt(runmake fmt) - Follow Effective Go guidelines
- Use meaningful variable and function names
- Write Godoc comments for all exported identifiers
- Start comments with the identifier name (e.g.,
// Evaluator computes...)
CLI output and command UX conventions are documented in docs/cli-style-guide.md.
CLI Command Conventions
New commands must use the NewCmd() factory pattern:
func NewCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "mycommand",
Short: "...",
RunE: run,
}
cmd.Flags().StringVar(&opts.Flag, "flag", "default", "help text")
return cmd
}
Do not use package-level var Cmd = &cobra.Command{...} with init() for new commands. Existing commands that use this pattern should not be retrofitted unless they are being substantially modified for other reasons.
Package Organization
internal/
├── domain/ # Core business logic, no external dependencies
├── app/ # Use case orchestration
└── adapters/ # Input/output adapters (JSON, YAML loaders)
- Keep domain logic in
internal/corewithout I/O concerns - Use interfaces (ports) for external dependencies
- Implement adapters in
internal/adapters
Submitting Changes
Branch Naming
Use descriptive branch names:
feature/add-sarif-outputfix/episode-duration-calculationdocs/improve-readme
Commit Messages
Write clear commit messages:
Add SARIF output format support
- Implement SARIF 2.1.0 writer in adapters/output/sarif
- Add --format flag to apply command
- Update documentation with SARIF examples
Pull Request Process
- Create a feature branch from
main - Make your changes with tests
- Run
make checkto verify all checks pass - Push your branch and open a pull request
- Describe what the PR does and why
- Link any related issues
PR Checklist
- Tests pass (
make test) - Code is formatted (
make fmt) - No vet warnings (
make vet) - Lint passes (
make lint) - New features have tests
- Documentation updated for all user-visible changes (required)
- If CLI commands/flags/help changed, regenerate CLI reference docs (
cd ../publisher && make docs-gen)
Docs-as-Code rules
Documentation is treated as a first-class artifact:
- User-visible behavior changes must ship with docs updates in the same PR.
- CLI usage reference generation is owned by sibling
../publishertooling, not hand-edited per-command pages. - Stave CI runs link checks; publisher workflows own docs generation.
Adding Controls
To add new controls:
- Create a YAML file in the appropriate pack directory
- Use DSL version
ctrl.v1 - Define clear
unsafe_predicateconditions - Add per-control tests in the YAML's own
tests:block (run viastave test) and cover the predicate logic at the call site underinternal/core/...
Example control:
dsl_version: ctrl.v1
id: CTL.EXP.DURATION.002
name: Descriptive Name
description: What this control checks.
type: unsafe_duration
unsafe_predicate:
any:
- field: "properties.some_field"
op: "eq"
value: true
Note: Control IDs must follow the format CTL.<DOMAIN>.<CATEGORY>.<SEQ> where:
- DOMAIN: EXP, ID, TP, PROC, or META
- CATEGORY: STATE, DURATION, RECURRENCE, AUTHZ, JUSTIFICATION, OWNERSHIP, or VISIBILITY
- SEQ: 3-digit sequence number
Security review before merge
Every PR that changes controls/ must get a security-focused review of the
diff before merge:
- Run the
differential-reviewClaude Code skill on the control diff — focus on false negatives and edge-case coverage. Check thedifferential-reviewbox in the PR description to attest it was done. - Run
fp-checkon any new control's false-positive trap to confirm the control does not fire on its safe scenario.
The Control Security Review workflow
(.github/workflows/control-security-review.yml) is path-filtered on
controls/** and fails until the differential-review box is checked. Make it a
required check in branch protection to enforce the gate.
Secret Scanning
The repository uses gitleaks to prevent accidental credential leaks. Configuration is in .gitleaks.toml at the repo root.
Local Setup (pre-commit)
# Install pre-commit (once)
pip install pre-commit
# Install hooks (once, from repo root)
cd /path/to/bizacademy
pre-commit install
# Run manually against all files
pre-commit run --all-files
Run gitleaks Directly
# Install gitleaks: https://github.com/gitleaks/gitleaks#installing
gitleaks detect --source . --config .gitleaks.toml
Allowlist
Known false positives (AWS example keys, Visa test numbers, educational fixtures) are allowlisted in .gitleaks.toml. If you add test fixtures containing synthetic credentials, either:
- Use clearly fake formats (e.g.,
AKIAIOSFODNN7EXAMPLE,sk_live_EXAMPLE_NOT_A_REAL_KEY) - Add a path-scoped allowlist entry in
.gitleaks.toml
CI
The secret-scan GitHub Actions workflow runs gitleaks on every push and PR to main.
Synthetic Test Data
All AWS account IDs, ARNs, and bucket names under testdata/ and case-studies/ are synthetic placeholders. They do not correspond to real AWS accounts. See testdata/README.md for details.
Reporting Bugs
When filing a bug report, include a minimal, deterministic reproduction. See the Bug Reproduction Guide for how to write one, and the Bug Reproduction Template for a copy-paste starting point.
Getting Help
- Open an issue for bugs or feature requests
- Check existing issues before creating new ones
- Provide minimal reproduction steps for bugs
Scope note
Stave MVP scope is AWS S3 public exposure only.
Vocabulary
Stave uses one canonical term per concept in user-facing surfaces
(CLI help, docs, MCP descriptions, external articles): control,
finding, catalog, evaluation, verdict, chain, observation.
The canonical → deprecated mapping, the "why control not invariant"
rationale, and the carve-outs where invariant deliberately stays
(internal type names, the solver-import JSON contract, research and
explanation docs) are in TERMINOLOGY.md. The
Docs Drift CI check enforces the phrase-level renames on the
user-facing surfaces.