Contributing
Aceryx is an open-source project licensed under the MIT License (Community Edition). We welcome contributions from the community. This guide explains how to contribute code, documentation, and feedback.
License
By contributing to Aceryx, you agree to license your contributions under the MIT License. See LICENSE file for details.
Code of Conduct
Be respectful, inclusive, and constructive. We do not tolerate harassment or discrimination.
Getting Started
- Fork the repository on GitHub
- Clone your fork locally
- Create a branch for your feature (
git checkout -b feature/my-feature) - Make changes and test thoroughly
- Commit with clear messages (see Commit Guidelines below)
- Push to your fork and open a pull request
Architecture & Design
Before making significant changes, understand the core principles:
Package Boundaries
Each package owns its domain completely:
- Own tables exclusively — No other package queries your tables directly
- Export public API — Other packages use your public functions
- Never import internals — Import only types and functions from the
package.gofile - No cross-package queries — If you need another domain’s data, use their public API
Forbidden:
// BAD: Importing internal implementation details
import "aceryx/internal/cases/store"
func (t *Task) getCase() {
return store.GetCase(t.CaseID) // Direct database access
}
// BAD: Cross-package SQL queries
func GetTasksForCase(caseID string) {
return db.Query("SELECT * FROM tasks WHERE case_id = ?")
}Encouraged:
// GOOD: Using public API
import "aceryx/internal/cases"
func (t *Task) getCase(ctx context.Context) {
return cases.Get(ctx, t.CaseID) // Through public API
}Verify Boundaries
Run this before committing:
qp check:importsThis verifies:
- No package imports another’s internals
- No circular dependencies
- All cross-package calls use public APIs
Failing this check blocks merging.
Development Workflow
1. Code Changes
Make changes following the code style guide below.
2. Unit Testing
Write unit tests for business logic. Test files live alongside code:
// internal/cases/service.go
func (s *Service) ValidateData(data map[string]interface{}) error {
// Business logic
}
// internal/cases/service_test.go
func TestValidateData_ValidInput(t *testing.T) {
s := cases.NewService(nil) // Mock DB
err := s.ValidateData(map[string]interface{}{
"name": "Test",
})
assert.NoError(t, err)
}
func TestValidateData_InvalidInput(t *testing.T) {
s := cases.NewService(nil)
err := s.ValidateData(map[string]interface{}{
"name": 123, // Wrong type
})
assert.Error(t, err)
}Run tests:
go test ./internal/... -v -race3. Integration Testing
For cross-package interactions, write integration tests in tests/integration/:
// tests/integration/cases_test.go
func TestCaseWithWorkflow(t *testing.T) {
db := setupTestDB(t)
caseService := cases.NewService(db)
engineService := engine.NewEngine(db)
// Create case
c, _ := caseService.Create(ctx, &cases.CreateInput{...})
// Simulate workflow execution
engineService.ProcessCase(ctx, c.ID)
// Verify final state
updated, _ := caseService.Get(ctx, c.ID)
assert.Equal(t, "completed", updated.Status)
}Run integration tests:
go test ./tests/integration/... -v -race -tags=integration4. Code Quality Checks
Before committing, run:
qp guardsThis sequentially runs:
- Format:
gofmt -wall Go files - Lint:
golangci-lint run(static analysis) - Test:
go test ./internal/... -race(unit tests)
All three must pass. Fix any issues and rerun.
5. Commit & Push
Commit with clear, descriptive messages (see Commit Guidelines). Push to your fork.
6. Pull Request
Open a PR with:
- Title: Short description (under 70 chars)
- Description: Why this change? What problem does it solve?
- Testing: How did you test? (link to test files)
- Related issues: Reference any issues this addresses
Example:
## Description
Add support for case data validation against JSON schema.
## Why
Currently, case data is stored without validation, allowing invalid data
to corrupt workflows. This adds upstream validation in the cases service.
## Changes
- Add `ValidateData()` method to cases.Service
- Add JSON schema support in case_types
- Add integration test for validation
## Testing
- Unit tests: `internal/cases/service_test.go`
- Integration tests: `tests/integration/cases_test.go`
Closes #123Code Style Guide
Go
Follow Effective Go and use gofmt:
gofmt -w ./internal/...Naming conventions:
- Package names: lowercase, single word (
cases,tasks,audit) - Functions: PascalCase (
GetCase,CreateTask,ValidateData) - Constants: UPPER_SNAKE_CASE (
MAX_RETRIES,DEFAULT_TIMEOUT) - Interfaces: Verb + “er” naming (
Reader,Writer,Storer) - Error variables:
ErrXxxprefix (ErrNotFound,ErrInvalidData)
Examples:
// Good: Clear, concise
type Store interface {
Create(ctx context.Context, case *Case) error
Get(ctx context.Context, id string) (*Case, error)
Update(ctx context.Context, case *Case) error
}
// Good: Error naming
var ErrCaseNotFound = errors.New("case not found")
// Good: Constant naming
const DefaultPageSize = 25
// Bad: Ambiguous naming
type S interface {
C(c *Case) error
}
// Bad: Package prefix in name (redundant)
func cases.GetCase() {}
// Bad: Inconsistent error naming
var NotFound = errors.New("not found")No ORM
Use raw SQL with pgx. This gives explicit control and better performance:
// Good: Raw SQL with pgx
rows, _ := db.Query(ctx, "SELECT id, name FROM cases WHERE tenant_id = ? AND status = ?", tenantID, "active")
defer rows.Close()
for rows.Next() {
var id, name string
rows.Scan(&id, &name)
// ...
}
// Bad: Using an ORM
db.Where("tenant_id = ?", tenantID).Where("status = ?", "active").Find(&cases)Structured Logging
Use slog (Go 1.21+ standard library):
import "log/slog"
// Good: Structured logging with context
logger := slog.With(
slog.String("case_id", caseID),
slog.String("step_name", stepName),
)
logger.Info("Step activated", slog.String("status", "active"))
// Output (JSON format):
// {"time":"2026-04-04T10:00:00Z","level":"INFO","msg":"Step activated","case_id":"uuid","step_name":"intake","status":"active"}
// Bad: String concatenation
log.Printf("Step %s activated for case %s", stepName, caseID)Error Handling
Always handle errors explicitly:
// Good: Explicit error handling
case, err := s.Get(ctx, caseID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, fmt.Errorf("case not found: %w", err)
}
return nil, fmt.Errorf("failed to get case: %w", err)
}
// Bad: Ignoring errors
case, _ := s.Get(ctx, caseID)
// Bad: Generic error message
if err != nil {
return errors.New("error")
}Database Transactions
Use transactions for atomic operations:
// Good: Explicit transaction
tx, _ := db.Begin(ctx)
defer tx.Rollback(ctx)
tx.Exec(ctx, "UPDATE cases SET status = ?", "closed")
tx.Exec(ctx, "INSERT INTO case_events (case_id, action) VALUES (?, ?)", caseID, "closed")
tx.Commit(ctx)
// Bad: No transaction
db.Exec(ctx, "UPDATE cases SET status = ?", "closed")
db.Exec(ctx, "INSERT INTO case_events ...") // Could fail, leaving case in inconsistent stateFrontend (Vue 3 + TypeScript)
Use Vue 3 Composition API with <script setup>:
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useCases } from '@/stores/cases'
interface Case {
id: string
status: 'active' | 'closed'
}
const casesStore = useCases()
const selectedId = ref<string | null>(null)
const selectedCase = computed(() => {
if (!selectedId.value) return null
return casesStore.cases.find(c => c.id === selectedId.value)
})
const closeCase = async () => {
if (!selectedCase.value) return
await casesStore.closeCase(selectedCase.value.id)
}
</script>
<template>
<div>
<select v-model="selectedId">
<option v-for="case in casesStore.cases" :key="case.id" :value="case.id">
{{ case.id }} - {{ case.status }}
</option>
</select>
<button @click="closeCase" :disabled="!selectedCase">Close Case</button>
</div>
</template>Rules:
- Use
<script setup>syntax (simpler, more readable) - Use TypeScript with strict mode (
"strict": truein tsconfig) - No
anytypes (use generics or better typing) - Use PrimeVue components for consistency
- Import types with
typekeyword to avoid circular dependencies
// Good
import type { Case } from '@/types'
import { useCases } from '@/stores/cases'
// Bad
import { Case, useCases } from '@/types' // Types and values mixed
const data: any = {} // No type safety
Audit Trail Requirements
Any state-changing operation must record an audit event within the same transaction:
// Good: Audit event recorded in transaction
func (s *Service) CloseCase(ctx context.Context, id string, reason string) error {
// All mutations in a single transaction
tx, err := s.db.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
// Make the change
case, _ := s.Get(ctx, id)
case.Status = "closed"
tx.Exec(ctx, "UPDATE cases SET status = $1 WHERE id = $2", "closed", id)
// Record audit event in same transaction
tx.Exec(ctx, `
INSERT INTO case_events (id, case_id, tenant_id, action, actor, changes, hash, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
`, eventID, id, tenantID, "case:closed", userID, changesJSON, hash)
return tx.Commit(ctx)
}
// Bad: No audit trail
func (s *Service) CloseCase(ctx context.Context, id string) error {
case, _ := s.Get(ctx, id)
case.Status = "closed"
return s.store.Update(ctx, case) // No event recorded
}Submitting Changes
Before You Submit
Run the full quality check:
qp guards # Format + lint + test
qp guards:full # Full suite (fmt + lint + test:all + vet + build + arch-check + vulncheck)All must pass before merge.
Pull Request Checklist
Guardian checklist (from AGENTS.md — all 12 must be satisfied):
-
gofmtproduces no changes -
golangci-lintreports zero issues - All unit tests pass (
go test ./internal/...) - All integration tests pass (
go test ./tests/integration/...) - All BDD scenarios pass (
go test ./tests/bdd/...) - New code has corresponding tests
- No cross-package internal imports (verified by
qp check:imports) - No direct cross-package table queries (all queries use public APIs)
- All DB writes are within a single transaction per step transition
- RBAC check on every new API endpoint
- Audit event recorded for every state-changing operation
- Every tenant-scoped query includes
WHERE tenant_id = $1
Additional checks:
- Documentation updated (if user-facing)
- Commit messages are clear and descriptive
- No breaking API changes (or documented)
Commit Guidelines
Use clear, imperative commit messages:
Capitalize the first line (50 chars max)
- Use bullet points for multiple changes
- Reference issues: "Fixes #123"
- Explain the "why", not the "what"
- Keep line length under 72 chars
Fixes #123Good examples:
Add JSON schema validation for case data
Create database migration for new case_types table
Fix race condition in step dispatch
Document expressions featureBad examples:
Fix bug
Update code
wip
changed thingsTesting Expectations
Code Coverage
Aim for >80% coverage on business logic. Use:
go test ./internal/... -cover
go test ./internal/... -coverprofile=coverage.out
go tool cover -html=coverage.out # View in browserTest Levels
- Unit tests (80%): Business logic, validators, transformations
- Integration tests (15%): Database interactions, cross-package flows
- E2E tests (5%): Critical user workflows (via Playwright)
Questions?
- Open an issue for bugs or feature requests
- Open a discussion for questions or ideas
- Check existing issues before creating duplicates
Thank you for contributing to Aceryx!