feat(rate_limit): Limiter API redesign per reprazent review (Spec 8)

Summary

Addresses all 10 unresolved threads from MR !270 (merged) review and Bob Van Landuyt's spec update (2026-04-28T09:03). Implements Spec 8 (#28792).

  • Limiter class — holds Evaluator at init; no per-request Evaluator allocation (Scenario A)
  • Configuration classLabkit::RateLimit.configure { |c| c.redis = ...; c.logger = ... } (Scenarios M, N)
  • Result objectcheck returns Result with matched?, exceeded?, action, rule, error? (Scenarios I-K, S)
  • Compound Redis key — all characteristics joined into one key per rule; one incr per call (Scenario B)
  • First-match-wins — evaluation stops at the first matching rule; caller controls priority by rule order (Scenarios E, F)
  • _unknown_ sentinel — nil/empty characteristic values use "_unknown_" in the key (Scenario C)
  • Rule name: field — added as required field; used in Redis key instead of positional index (Scenario Q)
  • Callable limit/period — lambdas resolved at check-time, not init (Scenario D)
  • Remove KNOWN_CHARACTERISTICS — any identifier key is valid as a characteristic (Scenario O)
  • Endpoint normalisation in Identifier#initialize — removed from Evaluator (Scenario P)
  • Name validation at Limiter.new — raises ArgumentError eagerly on invalid names (Scenario U)

Test plan

  • bundle exec rspec spec/labkit/rate_limit_spec.rb spec/labkit/rate_limit/ --format documentation -- 77 examples, 0 failures
  • bundle exec rubocop on all changed files -- no offenses

Closes #28792

/cc @reprazent for primary review (these are your unresolved threads)

Merge request reports

Loading