Skip to content

feat(net): RateKey + RateLimitConfig + boot-time coverage (Slice 0 PR 4)#72

Merged
NotAProfDev merged 5 commits into
mainfrom
feat/net-http-rate-coverage
Jul 4, 2026
Merged

feat(net): RateKey + RateLimitConfig + boot-time coverage (Slice 0 PR 4)#72
NotAProfDev merged 5 commits into
mainfrom
feat/net-http-rate-coverage

Conversation

@NotAProfDev

@NotAProfDev NotAProfDev commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Closes #69

Slice 0 PR 4 of the net-http construction surface (spec: docs/superpowers/specs/2026-06-30-net-http-construction-surface-design.md) — closes Slice 0.

  • RateKey — the adapter's endpoint key with a finite universe (fn all() -> &'static [Self]), kept generic so the coverage check can iterate every variant.
  • LimitPolicy (TokenBucket { rate, burst } / Concurrency { max }) + LimitDecl (Policy / GlobalOnly) — explicit per-endpoint classification; there is no "absent" arm.
  • RateLimitConfig<K> — a total map (required global + local over K::all()).
  • BuildError + validate_coverage — the pure construction-time check: an unclassified key → UndeclaredKey; an out-of-range rate/burst/maxInvalidPolicy. A missing bucket is a boot failure, not a 15-minute IBKR penalty box (ADR-0034 §3).

No new dependency; no runtime; no layer. Slice 2's stack()/build() will call validate_coverage before assembling the stack.

Next: Slice 1 — the resilience layers (Retry, RateLimit (consumes this config + constructs Guarded), CircuitBreaker, Tracing), needing MockTimer-driven timing tests and the per-request Scope/RateLimit<K> extension.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a boot-time rate-limiting coverage validator for the HTTP API, ensuring all rate keys are declared up front.
    • Introduced a rate-limiting configuration vocabulary (global + per-endpoint/local policies) supporting token-bucket and concurrency limits.
  • Bug Fixes

    • Startup now fails fast on undeclared endpoint keys or invalid rate-limit parameters, preventing late runtime pacing issues.
  • Tests

    • Added unit tests covering coverage completeness and policy validation edge cases.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ce1ce7d-a842-4c6f-8db2-1e58a8c80b26

📥 Commits

Reviewing files that changed from the base of the PR and between 0d3a4d6 and 049f70d.

📒 Files selected for processing (1)
  • CHANGELOG.md
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

📝 Walkthrough

Walkthrough

Adds a new rate module to oath-adapter-net-http-api with boot-time rate-limit coverage validation types and checks, exposes the new API from lib.rs, and records the change in the changelog.

Changes

Rate limit coverage validation

Layer / File(s) Summary
RateKey trait and pacing model types
crates/adapter/net/http/api/src/rate.rs
Defines RateKey with a finite all() universe, plus LimitPolicy and LimitDecl for token-bucket, concurrency, explicit, and global-only pacing classification.
RateLimitConfig and validate_coverage implementation
crates/adapter/net/http/api/src/rate.rs
Adds RateLimitConfig<K>, LimitPolicy::validate, BuildError, and validate_coverage, with unit tests covering total coverage, missing keys, invalid policy parameters, and GlobalOnly.
Module wiring and documentation
crates/adapter/net/http/api/src/lib.rs, CHANGELOG.md
Adds pub mod rate;, re-exports the new rate API, updates crate docs, and adds the changelog entry for the boot-time coverage check.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

  • NotAProfDev/oath#58: Also changes crates/adapter/net/http/api/src/lib.rs public module wiring and re-exports in the same crate API surface.

Suggested labels: enhancement

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is Conventional Commits-compliant and accurately describes the rate-limit coverage feature.
Linked Issues check ✅ Passed The changes implement the requested RateKey, policy types, total config, build errors, and standalone boot-time validation.
Out of Scope Changes check ✅ Passed The PR only adds the requested rate module plus related exports and docs/changelog updates, with no unrelated code.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/net-http-rate-coverage

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/adapter/net/http/api/src/rate.rs (1)

137-150: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

validate_coverage doesn't flag stale keys not in K::all().

The loop only checks that every K::all() variant is present in cfg.local; it never rejects extra entries in local that no longer correspond to a current variant (e.g., after a RateKey variant is removed/renamed, an orphaned entry silently survives). Since the module's contract is "total over K::all()," consider also asserting cfg.local.len() == K::all().len() (or diffing keys) to catch drift in both directions.

♻️ Optional strengthening of the total-coverage check
 pub fn validate_coverage<K>(cfg: &RateLimitConfig<K>) -> Result<(), BuildError>
 where
     K: RateKey + fmt::Debug,
 {
     cfg.global.validate()?;
+    if cfg.local.len() != K::all().len() {
+        // an entry in `local` does not correspond to any current `K::all()` variant
+    }
     for key in K::all() {
         match cfg.local.get(key) {
             None => return Err(BuildError::UndeclaredKey(format!("{key:?}"))),
             Some(LimitDecl::Policy(policy)) => policy.validate()?,
             Some(LimitDecl::GlobalOnly) => {},
         }
     }
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/adapter/net/http/api/src/rate.rs` around lines 137 - 150,
`validate_coverage` only verifies that every `K::all()` key exists in
`RateLimitConfig::local`, but it does not reject stale or orphaned entries that
are no longer part of the current `RateKey` set. Update `validate_coverage` to
check coverage in both directions by comparing the configured local keys against
`K::all()` (for example by validating the lengths or diffing the key sets) so
removed or renamed variants are flagged as `BuildError` instead of silently
surviving.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/adapter/net/http/api/src/rate.rs`:
- Around line 137-150: `validate_coverage` only verifies that every `K::all()`
key exists in `RateLimitConfig::local`, but it does not reject stale or orphaned
entries that are no longer part of the current `RateKey` set. Update
`validate_coverage` to check coverage in both directions by comparing the
configured local keys against `K::all()` (for example by validating the lengths
or diffing the key sets) so removed or renamed variants are flagged as
`BuildError` instead of silently surviving.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bedb1658-563d-41d0-91aa-b5d03304cdfa

📥 Commits

Reviewing files that changed from the base of the PR and between 4adeac1 and 0d3a4d6.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • crates/adapter/net/http/api/src/lib.rs
  • crates/adapter/net/http/api/src/rate.rs

@NotAProfDev

Copy link
Copy Markdown
Owner Author

Re: CodeRabbit nitpick on validate_coverage (stale/orphaned local keys) — validated, declining as by-design:

  1. The contract is "total over K::all()" = the map is defined for every domain element (forward coverage). An extra entry doesn't break totality and is inert — the Slice-1 RateLimit layer looks up by current K values, so a stale entry is never read. This PR guards the under-coverage (safety) direction; an orphaned entry is the harmless one.
  2. For a typed-enum K, an orphaned entry is essentially unconstructible: enum-literal configs can't name a removed variant (compile error), and serde rejects unknown variants. The only way |local| > |all()| with forward-coverage passing is a buggy all() that under-reports.
  3. That case is deliberately the adapter's responsibility (spec §"all() must not drift" — strum::VariantArray or an exhaustive-match test), not validate_coverage's. A len check here would fire on an all() bug while pointing at the wrong culprit, and would force a third BuildError variant against the deliberate two-variant design.

No code change.

@NotAProfDev NotAProfDev merged commit ff0b5db into main Jul 4, 2026
5 checks passed
@NotAProfDev NotAProfDev deleted the feat/net-http-rate-coverage branch July 4, 2026 10:35
NotAProfDev added a commit that referenced this pull request Jul 5, 2026
* docs(net): Slice 2 stack()/HttpConfig design + Slice 0 PR4 plan

Adds the runtime-free assembly design (stack() + HttpConfig) derived from the
construction-surface spec and ADR-0031, and commits the previously-untracked
Slice 0 PR4 (rate-coverage) implementation plan that shipped as #72.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(net): Slice 2 implementation plan + spec refinements

Adds the task-by-task stack()/HttpConfig plan and folds in the design
refinements surfaced while planning against the shipped layer APIs:
HttpConfig gains rate_limit_max_wait (RateLimitLayer::new's 3rd arg),
validation is delegated to RateLimitLayer::new (no separate call), and
tests use an inline recording leaf (not MockClient, a dev-dep cycle).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(net): HttpConfig + stack() - canonical resilience assembly

Add the non-generic HttpConfig aggregate and the stack() function that
validates pacing coverage (RateLimitLayer::new) before composing the
ADR-0031 canonical resilience order over any leaf:
Tracing(CircuitBreaker(Retry(RateLimit(Timeout(SetHeaders(Auth(leaf))))))).

Includes a transparency smoke test (request flows through all layers,
body untouched) and a boot-coverage test proving a non-total
RateLimitConfig fails closed as a BuildError before anything is built.

Deviations from the brief, needed to compile/lint clean:
- Added `S::Body: Send` to stack()'s where clause: RateLimit's Service
  impl requires the body to be Send, which the bare HttpClient bound
  does not imply.
- Test module imports `oath_adapter_net_api::Timer` (needed for
  ScriptLeaf's Step::Hang arm to call `timer.sleep`).
- `#[allow(clippy::needless_pass_by_value)]` on stack(): rate_limits is
  only borrowed internally, but the public signature intentionally
  takes ownership to mirror HttpConfig's "consumed once at boot" shape.
- Second test uses `let-else` instead of `.unwrap_err()`, which would
  require Debug on the opaque `impl HttpClient + ...` Ok type.
- `#[allow(dead_code)]` on Step::Err/Hang, ScriptLeaf::calls/seen_auth,
  and CounterAuth: scaffolding consumed by Task 2's full-stack tests
  landing later in this same file.

Closes #87

* test(net): stack ordering, fail-closed, and per-attempt Auth tests

* docs(net): record stack()/HttpConfig amendment (ADR-0034) + changelog

* docs(net): note S::Body: Send bound in ADR-0034 amendment #11

Final whole-branch review flagged the amendment enumerated the bound
refinements but omitted S::Body: Send (transitively required by
Retry/RateLimit's B: Send). Records it so the ADR matches the shipped
stack() signature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(net): sync stack() signature bounds in Slice 2 spec

CodeRabbit (PR #88) flagged the spec's stack() where-clause still showed
the pre-implementation sketch bounds. Sync it to the shipped signature:
S::Body: Send, T/A + 'static, K + Debug (already in ADR-0034 #11).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(net): RateKey + RateLimitConfig + boot-time coverage (Slice 0, PR 4)

1 participant