Thank you for considering contributing to Stave. This document explains how to set up your development environment, run tests, and submit changes.
- Go 1.26.2 or later
- golangci-lint (optional, for linting)
- Make (for convenience targets)
# 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# Run all tests
make test
# 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/domain
# Run startup benchmark (informational performance budget)
go test -run '^$' -bench BenchmarkCLIStartupHelp -benchmem ./cmd/stave/cmdStartup target for lightweight commands is approximately <500ms (see BenchmarkCLIStartupHelp in cmd/stave/cmd/startup_benchmark_test.go).
E2E tests (scripts/e2e.sh, scripts/e2e-counterfactual.sh) require:
- jq — JSON processor for comparing evaluation output
- diff — standard Unix diff for golden-file comparison
- bash — scripts use bash-specific features (process substitution)
These are not needed for unit tests (make test), only for E2E validation.
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-goldensThe 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 matchInterpreting 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.
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.
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.
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.
internal/
├── domain/ # Core business logic, no external dependencies
├── app/ # Use case orchestration
└── adapters/ # Input/output adapters (JSON, YAML loaders)
- Keep domain logic in
internal/domainwithout I/O concerns - Use interfaces (ports) for external dependencies
- Implement adapters in
internal/adapters
Use descriptive branch names:
feature/add-sarif-outputfix/episode-duration-calculationdocs/improve-readme
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
- 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
- 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)
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.
To add new controls:
- Create a YAML file in the appropriate pack directory
- Use DSL version
ctrl.v1 - Define clear
unsafe_predicateconditions - Add tests in
internal/domain/control_test.go
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: trueNote: 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
The repository uses gitleaks to prevent accidental credential leaks. Configuration is in .gitleaks.toml at the repo root.
# 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# Install gitleaks: https://github.com/gitleaks/gitleaks#installing
gitleaks detect --source . --config .gitleaks.tomlKnown 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
The secret-scan GitHub Actions workflow runs gitleaks on every push and PR to main.
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.
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.
- Open an issue for bugs or feature requests
- Check existing issues before creating new ones
- Provide minimal reproduction steps for bugs
Stave MVP scope is AWS S3 public exposure only.