---
phase: 09-osint-infrastructure
plan: 02
subsystem: recon
tags: [recon, rate-limiting, stealth, infrastructure]
requires:
  - golang.org/x/time/rate
provides:
  - pkg/recon.LimiterRegistry
  - pkg/recon.NewLimiterRegistry
  - LimiterRegistry.For
  - LimiterRegistry.Wait
affects: []
tech-stack:
  added: []
  patterns:
    - per-source rate.Limiter map guarded by sync.Mutex
    - idempotent registry lookup (first registration wins)
    - optional stealth jitter (100ms-1s) gated by flag
key-files:
  created:
    - pkg/recon/limiter.go
    - pkg/recon/limiter_test.go
  modified: []
decisions:
  - Jitter range fixed at 100ms + rand[0,900)ms per plan spec (max ~1000ms)
  - First For() call wins; later rate/burst arguments are ignored to preserve token state
  - Added TestJitterDisabled as extra guard (Rule 2: correctness)
metrics:
  completed: 2026-04-05
  duration: ~5 min
  tasks: 1/1
requirements: [RECON-INFRA-05]
---

# Phase 09 Plan 02: LimiterRegistry Summary

Per-source rate limiting foundation using `golang.org/x/time/rate` with optional stealth jitter, keyed by source name via a mutex-guarded registry.

## What Was Built

- **`pkg/recon/limiter.go`** — `LimiterRegistry` type with:
  - `NewLimiterRegistry()` constructor (empty map)
  - `For(name, r, burst) *rate.Limiter` — idempotent lookup; creates on first call, returns the same pointer on subsequent calls
  - `Wait(ctx, name, r, burst, stealth) error` — acquires limiter (via `For`), blocks on `limiter.Wait(ctx)`, then sleeps a random 100ms–1000ms jitter if `stealth==true`, respecting ctx cancellation during the sleep
- **`pkg/recon/limiter_test.go`** — 5 tests:
  - `TestLimiterPerSourceIsolation` — different names → distinct `*rate.Limiter`
  - `TestLimiterIdempotent` — same name → same pointer
  - `TestWaitRespectsContext` — pre-cancelled ctx returns error
  - `TestJitterRange` — stealth=true elapsed ∈ [90ms, 1200ms]
  - `TestJitterDisabled` — stealth=false elapsed < 50ms

## Satisfies

- **RECON-INFRA-05** — "each source owns its own limiter — no centralization". Verified via `TestLimiterPerSourceIsolation`.
- Partial foundation for **RECON-INFRA-06** (stealth jitter) — fully wired into the engine in plan 09-03.

## Verification

```
go test -run 'TestLimiter|TestWait|TestJitter' -count=1 -v \
  ./pkg/recon/limiter.go ./pkg/recon/limiter_test.go ./pkg/recon/source.go
```

Result: 5/5 PASS.

Tests were executed via explicit file list because sibling plans in the same wave (09-03, 09-04) have landed test files (`engine_test.go`, `robots_test.go`) whose implementation files are still in flight. This is expected parallel-wave behavior and does not affect the correctness of this plan's code. Once all wave-1 plans merge, `go test ./pkg/recon/...` will run the full suite.

`go vet ./pkg/recon/limiter.go ./pkg/recon/source.go` is clean.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 2 — Missing coverage] Added `TestJitterDisabled`**
- **Found during:** Task 1
- **Issue:** Plan tested jitter upper/lower bounds with stealth on, but did not assert the negative case — a regression that applied jitter even when stealth=false would go undetected.
- **Fix:** Added `TestJitterDisabled` asserting elapsed < 50ms when stealth=false.
- **Files modified:** `pkg/recon/limiter_test.go`
- **Commit:** 590fc33

No architectural deviations. No blockers.

## Commits

- `590fc33` — feat(09-02): add LimiterRegistry with per-source rate limiters and jitter

## Self-Check: PASSED

- FOUND: pkg/recon/limiter.go
- FOUND: pkg/recon/limiter_test.go
- FOUND commit: 590fc33
- All plan tasks (1/1) complete
- All plan success criteria met:
  - LimiterRegistry exported with `For` and `Wait` ✓
  - Each source receives its own `*rate.Limiter` ✓
  - Stealth jitter range 100ms–1s enforced ✓
