Skip to content

Coding Standards

This reference is for contributors writing or reviewing HoloMUSH code. It covers Go conventions, testing requirements, build commands, and code style — the rules that task lint enforces and that reviewers check. For the broader development workflow (spec-first, PR process, issue tracking), see the Pull Request Guide.

Write tests before implementation:

  1. Write a failing test
  2. Implement the minimum code to pass
  3. Refactor while keeping tests green
  4. Repeat

All tests must pass before any task is considered complete.

Work must be guided by specifications:

  • Specs define requirements (docs/specs/)
  • Plans define implementation approach (docs/plans/)
  • Both use RFC2119 keywords for requirement levels

Documentation uses these keywords to indicate requirement levels:

KeywordMeaning
MUSTAbsolute requirement
MUST NOTAbsolute prohibition
SHOULDRecommended, may ignore with justification
SHOULD NOTNot recommended
MAYOptional

Follow standard Go idioms:

  • Accept interfaces, return structs
  • Errors are values - handle them explicitly
  • Use context for cancellation and timeouts
  • Prefer composition over inheritance

Use oops for structured errors with context:

// Wrap existing error with context
return oops.With("plugin", name).With("operation", "load").Wrap(err)
// Create new error
return oops.Errorf("validation failed").With("field", fieldName)
// At API boundaries, add error code
return oops.Code("PLUGIN_LOAD_FAILED").With("plugin", name).Wrap(err)

For logging oops errors, use pkg/errutil:

errutil.LogError(logger, "operation failed", err)

When internal errors occur during plugin host function calls, the error is sanitized before returning to the plugin. To help operators debug issues reported by plugins, a correlation ID (ULID) is included in both the log entry and the sanitized error message.

Plugin receives:

internal error (ref: 01KFSM8W2JKVD441APK779B4PN)

Server log contains:

ERROR internal error in plugin query error_id=01KFSM8W2JKVD441APK779B4PN plugin=my-plugin ...

Correlation workflow:

  1. Plugin reports error to user (e.g., “internal error (ref: 01KFSM8W…)”)
  2. User contacts server operator with the reference ID
  3. Operator searches logs for error_id=01KFSM8W...
  4. Full error details including stack trace are available in the log entry

This pattern ensures internal details (database errors, stack traces) are never exposed to plugins while still enabling effective debugging.

Use structured logging with slog. When a context.Context is in scope, use the context-carrying variant — it threads trace_id/span_id into the log entry, which is how Loki and Sentry correlate logs to traces.

// CORRECT — ctx is available, use InfoContext
slog.InfoContext(ctx, "plugin loaded",
"plugin", name,
"version", version,
"load_time_ms", loadTime.Milliseconds())
// WRONG — ctx is available but dropped; log line is orphaned
slog.Info("plugin loaded", "plugin", name)

Bare slog.Info/slog.Warn are only acceptable in main, init, or bare goroutines where no context is reachable. The sloglint linter (run by task lint) enforces this and will flag violations.

Use clear, descriptive names:

  • Avoid abbreviations except well-known ones (ID, URL, HTTP)
  • Package names are lowercase, single words when possible
  • Use MixedCaps (PascalCase for exported, camelCase for unexported)

All source files must include SPDX license headers:

Apache-2.0
// Copyright 2026 HoloMUSH Contributors
// Package foo provides ...
package foo

Headers are auto-added by the pre-commit hook, or manually with:

Terminal window
task license:add
RequirementThreshold
Minimum per-package coverage80%
Target for core packages90%+

Verify coverage before completing work:

Terminal window
task test:cover
  • Unit tests: foo.go -> foo_test.go (same directory)
  • Integration tests: *_integration_test.go with build tag
//go:build integration
package foo_test

Use table-driven tests for comprehensive coverage:

func TestEventType_String(t *testing.T) {
tests := []struct {
name string
input EventType
expected string
}{
{"say event", EventTypeSay, "say"},
{"pose event", EventTypePose, "pose"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, tt.input.String())
})
}
}

Use testify for assertions:

// Equality
assert.Equal(t, expected, got)
// Error checking
require.NoError(t, err)
assert.Error(t, err)
// Contains
assert.Contains(t, slice, element)

Use mockery for generating mocks:

Terminal window
mockery # Uses .mockery.yaml config

Use generated mocks in tests:

store := mocks.NewMockEventStore(t)
store.EXPECT().Append(mock.Anything, mock.Anything).Return(nil)

Integration tests use Ginkgo/Gomega for BDD-style specs:

//go:build integration
var _ = Describe("Feature Name", func() {
Describe("User story or capability", func() {
It("expected behavior in plain English", func() {
// Given/When/Then pattern
Expect(result).To(Equal(expected))
})
})
})

Run integration tests:

Terminal window
task test:int

Use task for all build operations. Do not run Go commands directly:

Terminal window
task lint # Run all linters
task fmt # Format all files
task test # Run tests
task build # Build binary
task dev # Run dev server

These commands ensure consistent tooling and configuration.

Prefer readable code over clever solutions:

  • Write code that’s easy to understand
  • Avoid premature optimization
  • Keep functions focused and small
  • Do not add comments that restate what code does
  • Comment the “why”, not the “what”
  • Use doc comments for exported functions/types

Match existing project patterns:

  • Follow the conventions in nearby code
  • Use consistent formatting (enforced by task fmt)
  • Match naming patterns used elsewhere
api/ # Protocol definitions (protobuf)
cmd/holomush/ # Server entry point
docs/
plans/ # Implementation plans
specs/ # Design specifications
internal/ # Private implementation
core/ # Event system, sessions
grpc/ # gRPC server implementation
logging/ # Structured logging setup
observability/ # Metrics and health endpoints
plugin/ # Plugin host (Lua & Go)
store/ # PostgreSQL implementations
telnet/ # Telnet protocol adapter
tls/ # TLS certificate management
web/ # WebSocket adapter
world/ # World engine, characters, locations
xdg/ # XDG base directory support
pkg/ # Public plugin API
plugins/ # Core plugins (Lua for customization, Go for performance)
scripts/ # Build and utility scripts
test/integration/ # End-to-end test suites

Core interfaces that implementations must satisfy:

type EventStore interface {
Append(ctx context.Context, event Event) error
Replay(ctx context.Context, stream string, afterID ulid.ULID, limit int) ([]Event, error)
LastEventID(ctx context.Context, stream string) (ulid.ULID, error)
Subscribe(ctx context.Context, stream string) (eventCh <-chan ulid.ULID, errCh <-chan error, err error)
}
type AccessControl interface {
Check(ctx context.Context, subject, action, resource string) bool
}
  • All game actions produce events
  • Events are immutable and ordered
  • World state is canonical in PostgreSQL; the event feed is an append-only audit and notification log (the decided model — see ADR docs/adr/holomush-i4784-world-state-model-decision.md)
  • Accept interfaces in constructors
  • Create implementations at composition root
  • Enables testing with mocks
  • Pass context through call chains
  • Use context for cancellation and timeouts
  • Include tracing spans in context