RNG.md
All posts

Code Quality Gates for Vibe-Coded Projects

My friend is vibe coding a “personal OS” for their new job. The whole thing is written in Go. He doesn’t know Go and hasn’t looked at the code. “But how will he know it works?” He’s using it and it works. That’s about it.

Less facetiously, he shared his CLAUDE.md with me recently. It’s pretty solid. It looks very similar to what I did when working on uniffi-bindgen-node-js. I talked a bit about this in simplifying AI-generated code with Lizard.

CLAUDE.md
## Code Quality Standards

### Pre-commit Hooks (enforced automatically)

Pre-commit is configured (`.pre-commit-config.yaml`). Every commit runs:
- `go fmt` + `go imports` — formatting
- `go build` — compilation check
- `go test` — unit tests must pass
- `golangci-lint` — expanded linter set (see below)
- `trailing-whitespace`, `end-of-file-fixer`, `check-yaml` — hygiene
- `detect-private-key` — no secrets in commits

### Linting (`.golangci.yml`)

The project uses an expanded linter set beyond defaults:
- **errcheck** — unchecked errors
- **govet**, **staticcheck**, **gosimple** — correctness
- **gocritic** — style, performance, diagnostics
- **gosec** — security vulnerabilities
- **goconst** — repeated magic strings
- **prealloc** — slice preallocation
- **nilerr** — returning nil when err is set
- **bodyclose** — unclosed HTTP response bodies
- **noctx** — HTTP requests without context
- **copyloopvar** — loop variable capture

Run: `make lint` or `golangci-lint run ./...`

### Test Coverage

**Target: 90%+ on `internal/` packages. This is a hard floor, not a goal.**

**Coverage must not regress.** Every new feature, handler, API endpoint, or store method MUST include tests in the same commit.

**Enforcement rule:** After writing any code, run `make cover` and check per-package coverage. If any package dropped below 90%, write tests to restore it before committing.

- Use table-driven tests for functions with multiple cases
- Use `httptest` for handler/API tests
- Use temp SQLite databases (`t.TempDir()`) for store tests
- Test edge cases: empty inputs, not-found, error paths
- New API endpoint → add request/response test in `api_test.go`
- New page handler → add 200-status test in `handlers_test.go`
- New store method → add CRUD test in `store_test.go`
- Smoke test against real vault: `make smoke`

### Complexity

**Max cyclomatic complexity per function: 15.** Check with `gocyclo -over 15 .` — should return empty.

When a function exceeds 15:
- **Extract helpers** — move cohesive blocks (filtering, scoring, parsing) into named functions
- **Use table-driven patterns** — replace repetitive if/switch blocks with data-driven loops
- **Accept structs, not long parameter lists** — if a function takes 5+ args of the same type, use a struct
- **Don't share helpers between callers with different needs** — two similar-looking loops that build different outputs are not duplication

### Verification Checklist (before any commit)

```bash
make check          # runs: lint → test → cover (all three)
gocyclo -over 15 .  # must return empty

Makefile Targets

Target Purpose
make build Compile binary to bin/harness
make test Run all tests
make lint Run golangci-lint
make cover Run tests with coverage report
make check lint + test + cover (full quality gate)
make smoke Smoke test against real vault
make security govulncheck + gosec
make run Build, stop existing, start in background
make restart Same as run (rebuild + restart)
make stop Kill running harness
make logs Tail the log file

Architecture Conventions

  • Vault as backend — the Obsidian vault is the source of truth for domain data. The harness reads .md files and builds an in-memory Index (protected by sync.RWMutex).
  • SQLite for operational state — jobs, schedules, alert acks, integration cache, metrics. NOT for vault data.
  • Skills via Claude CLI — external actions (Slack, Jira, Calendar) go through Claude Code skills, not native Go HTTP clients. Exception: 15Five uses a native client (no MCP tool available).
  • Dynamic skill registry — skills are loaded from $VAULT_PATH/.claude/commands/*.md at startup. Add a new .md file with description: and category: frontmatter → it appears on the Skills page without a restart.
  • Feature flags — integrations are gated by env vars (ENABLE_GCAL, ENABLE_JIRA, ENABLE_SLACK, ENABLE_15FIVE). Handlers check flags before reading cache.
  • Write-back pattern — the harness can write to vault files (task completion, project status, notes). Use os.ReadFile → transform → os.WriteFile. The file watcher detects changes and triggers index rebuild.
  • Catppuccin Mocha theme — all templates use ctp-* Tailwind classes. Colors defined in tailwind.config in layout.html.

File Organization

internal/vault/     — parsing, indexing, types (vault read-only)
internal/store/     — SQLite state (jobs, schedules, cache, queue)
internal/web/       — HTTP handlers, API, SSE, templates
internal/skills/    — skill runner, scheduler, registry, post-processor
internal/alerts/    — alert evaluation rules
internal/fifteenfive/ — 15Five API client
internal/metrics/   — code quality metric collector

Harness-Specific Recovery

Skills like /prep-bob-1on1, /weekly-rollup, and /morning query multiple MCP sources and can take 2+ minutes. If any single query hangs or times out, skip it and note “Data unavailable: [source]” in the output.

Adding New Features

  1. If it reads external data → create a Claude Code skill in ~/my-project/.claude/commands/ with description: and category: frontmatter. It auto-registers via the dynamic registry. Use the harness-cache post-processor pattern for caching.
  2. If it needs persistent state → add a SQLite table in store.go migrate()
  3. If it needs a UI → add handler in handlers.go, template in templates/pages/, route in server.go, nav link in nav.html
  4. If it modifies vault files → follow the write-back pattern (read → transform → write, watcher handles rebuild)

You run tests as the last step if you make any code changes.

The trick with vibe coded projects (that are actually meant to be used) is to come up with a very robust definition of quality:

  • Code works
  • Code is fast
  • Code does what I want
  • Code doesn’t fail

And so on. Then you tell the LLM ways to measure these things. It really comes down to the same stuff we’ve always been doing, just a lot more of it.

  • Unit tests
  • Integration tests
  • System tests
  • Performance tests
  • Soak tests
  • Leak tests
  • Chaos tests
  • Cyclomatic complexity
  • Lint
  • Code smells
  • Code style
  • Security tests
  • … and so on

Then you define acceptable criteria:

  • 90%+ unit test coverage on internal packages
  • No cyclomatic complexity over 15
  • No lint errors
  • No code smells
  • No security issues
  • Performance benchmarks must not regress
  • … and so on

Then you make sure your agent runs these checks, and you’re in pretty good shape.

Related posts