Go Rig
Process and review discipline for Go work in this repo.
Scope split (do not duplicate across layers):
CLAUDE.md — toolchain, commands, env vars, exit codes, style pointers
.claude/rules/go-idioms.md — Go 1.26 idioms and modernizers
.claude/rules/go-patterns.md — style, API, docs, and testing patterns
- This skill — ATDD/TDD workflow, DI discipline, coverage floors, schema-first, review gate, reject list
If CLAUDE.md is stricter on a shared point, follow CLAUDE.md.
When to use
- implementing a new feature or behavior increment
- refactoring for clearer ownership or testability
- reviewing package boundaries or dependency flow
- replacing hidden collaborator construction with explicit injection
- tightening tests around user-visible or integration behavior
ATDD/TDD workflow
Test-first is a design tool, not an afterthought. Every meaningful behavior change follows red → green → refactor.
- ATDD (acceptance-first) for any user- or agent-visible change — drive from
cmd/command_test.go or cmd/transport_test.go so the test speaks in "what the CLI does", not "what the function does"
- TDD (unit-first) for internal invariants, validation rules, and edge cases — drive from a package-local
*_test.go
- Define the boundary behavior you want
- Add/update the closest consumer-level test (ATDD when user-visible; unit otherwise)
- Write the smallest failing assertion for the next increment
- Minimal implementation to pass
- Refactor while green; commit often
- Repeat
If repo policy blocks automatic test execution, still design test-first and ask before running. "Test everything you write" — no behavior ships without at least one consumer-level test pointing at it.
Coverage floors
Measured via go test -cover ./.... Touched packages must meet the floor or the PR must explicitly note the gap.
internal/api, internal/auth, internal/output, cmd/ ≥ 75%
internal/config, internal/mcp ≥ 70%
- Every branch of
cmd.mapTransportError (401/403/404/422/429/5xx/unknown) has a test
- Every
Transport method called by the CLI or MCP has an httptest-backed case in internal/api/official_test.go
main.go exempt (pure wiring)
Schema-first contract changes
schemas/golink-output.schema.json is the source of truth for every --json envelope. Order of operations when a shape changes:
- Edit the schema (
$defs entry + the oneOf entry if it's a new envelope)
- Update or add the fixture in
internal/output/schema_test.go
- Run
go test ./internal/output/... and watch it fail the new expectation
- Then edit the Go struct and command/tool handler
- Run the full suite to prove the envelope flows end-to-end
This turns schema drift into a test failure instead of silent runtime mismatch.
Definition of done
- Acceptance behavior specified at the right boundary
- Smallest relevant unit behavior covered
- Failure and edge paths covered
- Code refactored back to clarity while green
make ci (or the equivalent vet + lint + test + race + vuln gate) passes locally
- Coverage floors met for any touched package, or gap explicitly noted
- Schema and fixture moved first if a
--json shape changed
Design principles
Apply without ceremony — these guide decisions, not generate boilerplate.
- SRP — one reason to change per package, type, function
- DRY — extract repeated rules/validation/mapping, but not incidental similarity
- OCP — a concrete type with a small seam at the consumer beats an abstract framework
When these conflict, fix boundaries before introducing interfaces.
Abstraction and function discipline
- Start concrete; add an interface only when a real consumer needs substitution
- A function does one thing: validate, transform, orchestrate, persist, or render
- Split when business rules get mixed with transport/storage/logging
- Early returns over nested pyramids; whitespace separates phases
- Options struct when params > 3; named types when booleans muddy intent
Dependency injection
- Constructors for long-lived collaborators; parameters for short-lived/pure logic
- Never construct HTTP clients, keyring stores, loggers, or browsers inside domain methods
- No DI framework. No hidden globals. No package-level mutable state.
- Pass dependencies from the composition root (
main → cmd.ExecuteContext → cmd.Dependencies)
- Inject seams for: time (
Now), randomness, HTTP (HTTPClient), filesystem, browser launch, interactive TTY check, transport selection (TransportFactory), session storage (SessionStore)
// constructor injection for long-lived deps
func NewOrderService(store OrderStore, clock Clock) *OrderService {
return &OrderService{store: store, clock: clock}
}
// function parameter for short-lived / pure logic
func ValidateOrder(order Order, now time.Time) error {
if order.ExpiresAt.Before(now) {
return fmt.Errorf("order %s expired: %w", order.ID, ErrExpired)
}
return nil
}
Hardcoding
- No URLs, ports, credentials, file paths, timeouts, feature flags, or collaborator selection in core logic
- Exceptions: protocol-mandated constants and explicit contract/test fixtures
- Operational values come from typed config structs validated at startup
Review checklist
Before finishing any change — treat a match in Reject these patterns as a blocker.
Reject these patterns
- Interface-per-struct without a consumer need
- Functions mixing validation, orchestration, and persistence
- Hardcoded configuration or collaborator selection
- Weakly typed domain data (raw maps / generic blobs) when a struct would do
- Comments that restate code
- Brittle mock-only tests (prefer fakes with real behavior)
- Transport concerns embedded in core domain logic
- Production design distorted to satisfy a mocking framework
- Refactors that add indirection without improving correctness, ownership, or testability
- Schema drift — Go struct changed without the matching schema fixture
Success criteria
The skill is being followed correctly when:
- changes are small, test-backed, and easy to review
- dependency flow is explicit from
main down
- package responsibilities get cleaner, not blurrier
- tests speak in behavior terms, not implementation vocabulary
- the resulting code reads clearly without comments explaining control flow
1---2name: go-rig-43description: Use this skill when building, reviewing, or refactoring Go code that must follow strict design discipline — ATDD/TDD workflow, explicit dependency injection, package-boundary discipline, and structured code review. Complements CLAUDE.md by focusing on process and design judgment rather than version-specific Go features.4---56# Go Rig78Process and review discipline for Go work in this repo.910Scope split (do not duplicate across layers):1112- `CLAUDE.md` — toolchain, commands, env vars, exit codes, style pointers13- `.claude/rules/go-idioms.md` — Go 1.26 idioms and modernizers14- `.claude/rules/go-patterns.md` — style, API, docs, and testing patterns15- This skill — ATDD/TDD workflow, DI discipline, coverage floors, schema-first, review gate, reject list1617If `CLAUDE.md` is stricter on a shared point, follow `CLAUDE.md`.1819## When to use2021- implementing a new feature or behavior increment22- refactoring for clearer ownership or testability23- reviewing package boundaries or dependency flow24- replacing hidden collaborator construction with explicit injection25- tightening tests around user-visible or integration behavior2627## ATDD/TDD workflow2829Test-first is a design tool, not an afterthought. Every meaningful behavior change follows red → green → refactor.3031- **ATDD** (acceptance-first) for any user- or agent-visible change — drive from `cmd/command_test.go` or `cmd/transport_test.go` so the test speaks in "what the CLI does", not "what the function does"32- **TDD** (unit-first) for internal invariants, validation rules, and edge cases — drive from a package-local `*_test.go`33341. Define the boundary behavior you want352. Add/update the closest consumer-level test (ATDD when user-visible; unit otherwise)363. Write the smallest failing assertion for the next increment374. Minimal implementation to pass385. Refactor while green; commit often396. Repeat4041If repo policy blocks automatic test execution, still design test-first and ask before running. "Test everything you write" — no behavior ships without at least one consumer-level test pointing at it.4243## Coverage floors4445Measured via `go test -cover ./...`. Touched packages must meet the floor or the PR must explicitly note the gap.4647- `internal/api`, `internal/auth`, `internal/output`, `cmd/` ≥ 75%48- `internal/config`, `internal/mcp` ≥ 70%49- Every branch of `cmd.mapTransportError` (401/403/404/422/429/5xx/unknown) has a test50- Every `Transport` method called by the CLI or MCP has an httptest-backed case in `internal/api/official_test.go`51- `main.go` exempt (pure wiring)5253## Schema-first contract changes5455`schemas/golink-output.schema.json` is the source of truth for every `--json` envelope. Order of operations when a shape changes:56571. Edit the schema (`$defs` entry + the `oneOf` entry if it's a new envelope)582. Update or add the fixture in `internal/output/schema_test.go`593. Run `go test ./internal/output/...` and watch it fail the new expectation604. Then edit the Go struct and command/tool handler615. Run the full suite to prove the envelope flows end-to-end6263This turns schema drift into a test failure instead of silent runtime mismatch.6465## Definition of done6667- Acceptance behavior specified at the right boundary68- Smallest relevant unit behavior covered69- Failure and edge paths covered70- Code refactored back to clarity while green71- `make ci` (or the equivalent vet + lint + test + race + vuln gate) passes locally72- Coverage floors met for any touched package, or gap explicitly noted73- Schema and fixture moved first if a `--json` shape changed7475## Design principles7677Apply without ceremony — these guide decisions, not generate boilerplate.7879- **SRP** — one reason to change per package, type, function80- **DRY** — extract repeated rules/validation/mapping, but not incidental similarity81- **OCP** — a concrete type with a small seam at the consumer beats an abstract framework8283When these conflict, fix boundaries before introducing interfaces.8485## Abstraction and function discipline8687- Start concrete; add an interface only when a real consumer needs substitution88- A function does one thing: validate, transform, orchestrate, persist, or render89- Split when business rules get mixed with transport/storage/logging90- Early returns over nested pyramids; whitespace separates phases91- Options struct when params > 3; named types when booleans muddy intent9293## Dependency injection9495- Constructors for long-lived collaborators; parameters for short-lived/pure logic96- Never construct HTTP clients, keyring stores, loggers, or browsers inside domain methods97- No DI framework. No hidden globals. No package-level mutable state.98- Pass dependencies from the composition root (`main` → `cmd.ExecuteContext` → `cmd.Dependencies`)99- Inject seams for: time (`Now`), randomness, HTTP (`HTTPClient`), filesystem, browser launch, interactive TTY check, transport selection (`TransportFactory`), session storage (`SessionStore`)100101```go102// constructor injection for long-lived deps103func NewOrderService(store OrderStore, clock Clock) *OrderService {104 return &OrderService{store: store, clock: clock}105}106107// function parameter for short-lived / pure logic108func ValidateOrder(order Order, now time.Time) error {109 if order.ExpiresAt.Before(now) {110 return fmt.Errorf("order %s expired: %w", order.ID, ErrExpired)111 }112 return nil113}114```115116## Hardcoding117118- No URLs, ports, credentials, file paths, timeouts, feature flags, or collaborator selection in core logic119- Exceptions: protocol-mandated constants and explicit contract/test fixtures120- Operational values come from typed config structs validated at startup121122## Review checklist123124Before finishing any change — treat a match in *Reject these patterns* as a blocker.125126- [ ] Package boundaries coherent; no cross-domain leaks127- [ ] Interfaces have real consumers; no interface-per-struct128- [ ] Dependencies injected from the composition root129- [ ] No hardcoded URLs/ports/credentials/timeouts130- [ ] Types explicit where they protect domain correctness131- [ ] Functions readable in one pass, one responsibility each132- [ ] Errors wrapped with `%w`; no swallowed errors; no panic on expected failures133- [ ] Tests cover happy path, validation, edges, errors; concurrency where relevant134- [ ] Coverage floors met for touched packages135- [ ] JSON schema and Go struct in sync; fixture exists for every envelope change136- [ ] Nil-vs-empty behavior intentional for slices/maps/pointers/JSON137- [ ] Goroutines have a shutdown path and observable ownership138- [ ] Exported docs updated when public behavior changed139- [ ] `make ci` passes locally140141## Reject these patterns142143- Interface-per-struct without a consumer need144- Functions mixing validation, orchestration, and persistence145- Hardcoded configuration or collaborator selection146- Weakly typed domain data (raw maps / generic blobs) when a struct would do147- Comments that restate code148- Brittle mock-only tests (prefer fakes with real behavior)149- Transport concerns embedded in core domain logic150- Production design distorted to satisfy a mocking framework151- Refactors that add indirection without improving correctness, ownership, or testability152- Schema drift — Go struct changed without the matching schema fixture153154## Success criteria155156The skill is being followed correctly when:157158- changes are small, test-backed, and easy to review159- dependency flow is explicit from `main` down160- package responsibilities get cleaner, not blurrier161- tests speak in behavior terms, not implementation vocabulary162- the resulting code reads clearly without comments explaining control flow