go-ratelimit/CLAUDE.md
Virgil 1ec0ea4d28 fix(ratelimit): align module metadata and repo guidance
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-27 04:23:34 +00:00

76 lines
4 KiB
Markdown

<!-- SPDX-License-Identifier: EUPL-1.2 -->
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
Provider-agnostic sliding window rate limiter for LLM API calls. Single Go package (no sub-packages) with two persistence backends: YAML (single-process, default) and SQLite (multi-process, WAL mode). Enforces RPM, TPM, and RPD quotas per model. Ships default profiles for Gemini, OpenAI, Anthropic, and Local providers.
Module: `dappco.re/go/core/go-ratelimit` — Go 1.26, no CGO required.
## Commands
```bash
go test ./... # run all tests
go test -race ./... # race detector (required before commit)
go test -v -run TestCanSend ./... # single test
go test -v -run "TestCanSend/RPM_at_exact_limit" ./... # single subtest
go test -bench=. -benchmem ./... # benchmarks
go vet ./... # vet check
golangci-lint run ./... # lint
```
Pre-commit gate: `go test -race ./...` and `go vet ./...` must both pass.
## Standards
- **UK English** everywhere: colour, organisation, serialise, initialise, behaviour
- **Conventional commits**: `type(scope): description` — scopes: `ratelimit`, `sqlite`, `persist`, `config`
- **Co-Author line** on every commit: `Co-Authored-By: Virgil <virgil@lethean.io>`
- **Coverage** must not drop below 95%
- **Error format**: `core.E("ratelimit.FunctionName", "what", err)` via `dappco.re/go/core` — lowercase, no trailing punctuation
- **No `init()` functions**, no global mutable state
- **Mutex discipline**: lock at the top of public methods, never inside helpers. Helpers that need the lock document "Caller must hold the lock". `prune()` mutates state, so even "read-only" methods that call it take the write lock. Never call a public method from another public method while holding the lock.
## Architecture
All code lives in the root package. Key files:
- `ratelimit.go` — core types (`RateLimiter`, `ModelQuota`, `UsageStats`, `Config`, `Provider`), sliding window logic (`prune`, `CanSend`, `RecordUsage`), YAML persistence, `CountTokens` (Gemini-specific), iterators (`Models`, `Iter`)
- `sqlite.go``sqliteStore` internal type, schema creation, load/save for quotas and state
Constructor matrix: `New()` / `NewWithConfig()` for YAML, `NewWithSQLite()` / `NewWithSQLiteConfig()` for SQLite. Always `defer rl.Close()` with SQLite.
### Sliding window
1-minute window pruned on every `CanSend`/`Stats`/`RecordUsage` call. Daily counter is a rolling 24h window from first request, not a calendar boundary. Empty state entries are garbage-collected by `prune()` to prevent memory leaks.
## Test Organisation
White-box tests (`package ratelimit`), all assertions via `testify` (`require` for fatal, `assert` for non-fatal). Do not use `t.Error`/`t.Fatal` directly.
| File | Scope |
|------|-------|
| `ratelimit_test.go` | Core logic, provider profiles, concurrency, benchmarks |
| `sqlite_test.go` | SQLite backend, migration, concurrent persistence |
| `error_test.go` | Error paths for SQLite and YAML |
| `iter_test.go` | Iterators, `CountTokens` edge cases |
SQLite tests use `_Good`/`_Bad`/`_Ugly` suffixes (happy path / expected errors / edge cases). Core tests use plain descriptive names with table-driven subtests. Use `t.TempDir()` for all file paths.
## Dependencies
Four direct dependencies — do not add more without justification:
- `dappco.re/go/core` — file I/O helpers, structured errors, JSON helpers, path/environment utilities
- `gopkg.in/yaml.v3` — YAML backend
- `modernc.org/sqlite` — pure Go SQLite (no CGO)
- `github.com/stretchr/testify` — test-only
## Docs
- `docs/architecture.md` — sliding window algorithm, provider quotas, YAML/SQLite backends, concurrency model
- `docs/development.md` — prerequisites, test patterns, coding standards
- `docs/history.md` — completed phases with commit hashes, known limitations