Ctrl K
ComunidadGuías…

docs/adr/0056-anonymous-trademark-budget.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# ADR-0056: Anonymous Trademark Budget Isolation

## Metadata

| Field | Value |
|-------|-------|
| **Status** | Accepted |
| **Date** | 2026-08-13 |
| **Authors** | AlessioBrillo |
| **Deciders** | AlessioBrillo |
| **Supersedes** | N/A |
| **Relates to** | ADR-0006, ADR-0030, ADR-0033, ADR-0043, ADR-0041 |
| **Project** | DOMINUS |

## Context

The trademark gate (ADR-0006) is mandatory: every candidate must pass the
USPTO/EUIPO check before any buy recommendation. On the public namespace
(ADR-0030) every anonymous valuation runs the same gate through
`AnonScoringService`, drawing from the **same** per-process USPTO/EUIPO rate
limit buckets as pipeline runs.

Two problems follow from sharing one budget:

1. **Starving the pipeline.** The public namespace is internet-facing and
   per-IP rate limited (ADR-0043), but aggregated anonymous traffic is
   unbounded. A valuation spike (a post going viral, a scraping wave) can
   drain the shared USPTO/EUIPO tokens for minutes at a time, delaying the
   trademark stage of pipeline runs — the revenue-critical path. In the
   multi-process Cloud deployment (ADR-0033) each process holds its own
   in-memory budget, so three processes can *multiply* anonymous demand by
   the replica count.
2. **Fail-closed leakage onto the public surface.** The trademark gate fails
   closed by design (ADR-0006): its deadline and circuit breakers produce an
   `unverified` verdict and strip the buy signal. That is correct for the
   pipeline, but on the public namespace an exhausted shared budget converts
   a *capacity* problem into degraded *quality* for visitors — without any
   signal telling the operator why.

The public namespace needs its own, bounded allowance for trademark checks:
anonymous traffic must be *contained* by design, not by luck, and the
operator must see when the containment engages.

## Decision Drivers

1. **Trademark gate is non-negotiable (ADR-0006)** — every candidate, public
   or pipeline, keeps a mandatory trademark verdict; the public surface may
   never bypass the gate.
2. **Pipeline integrity** — anonymous traffic must never delay or starve
   pipeline trademark checks (shared budget = hidden coupling).
3. **Public UX + cost discipline** — bounded wait, bounded provider spend;
   degradation must be graceful, visible to operators, and cheap (community
   edition stays at €0).
4. **Zero-cost community parity** — with defaults off, the community edition
   behaves byte-identically to today; enabling the budget costs nothing.
5. **Operator control** — the budget parameters (capacity, refill, acquire
   deadline) must be configurable and observable via existing telemetry.

## Considered Options

### Option A: Dedicated fail-open budget gate (chosen)

A dedicated token bucket (`anon-trademark` Redis namespace in Cloud, in-memory
otherwise) guards trademark checks initiated by anonymous valuations. The
acquire is bounded by `ANON_TRADEMARK_ACQUIRE_TIMEOUT_MS`; on timeout (or
limiter error) the valuation **fails open** to an `unverified` verdict — the
provider is never touched and the buy signal is stripped (ADR-0006). Feature
disabled by default (`ANON_TRADEMARK_BUDGET_ENABLED=false`).

**Advantages:**
- Anonymous demand is isolated: no path from the public surface to the
  pipeline's USPTO/EUIPO tokens, in any process topology.
- Provider cost is hard-bounded per interval (`TOKENS × replicas`); worst
  case is 2 USPTO + 2 EUIPO checks per second on defaults.
- Fail-open keeps the public surface responsive — the price is an
  `unverified` verdict, exactly the same output the gate already produces on
  provider errors, so public semantics are unchanged, just more frequent
  under load.
- Distributed namespace in Cloud shares one budget across api/worker/
  scheduler replicas (ADR-0041 fair-share pattern), so a fleet cannot
  multiply anonymous demand.
- Telemetry: `dominus_anon_trademark_{hits,blocked}_total` makes
  containment observable; blocked = visitors got `unverified` solely due to
  budget pressure.

**Disadvantages:**
- A new configuration surface (4 env vars) with a new default.
- Under sustained anonymous demand, visitors see `unverified` verdicts that
  are capacity artifacts, not trademark reality — the operator must raise
  the budget if they want public quality back.
- Fail-open is a deliberate softening of the gate's semantics for this one
  surface; it must never extend to the pipeline.

**Cost Implications:** ~1 day (gate + service wiring + telemetry + docs). No
infra or API cost.

**Risk Assessment:** Low — feature is off by default; when on, the pipeline's
fail-closed trademark semantics (ADR-0006) are untouched by construction
(separate budget, separate code path).

---

### Option B: Shared budget with public priority queue

Anonymous valuations wait behind pipeline checks in the shared USPTO/EUIPO
buckets (priority queuing), no separate budget.

**Advantages:**
- Single budget to reason about; no new config.

**Disadvantages:**
- The shared budget is per-process; a scraping wave still stalls pipeline
  checks until the wait timeout fires (and then *drains* provider calls on
  the way out — worse).
- No cost containment: anonymous traffic can still spend unlimited provider
  calls, just at lower priority.
- Queue-full rejections (RateLimiterQueueFullError) on the public surface
  surface as hard errors instead of graceful degradation.
- No operator signal distinguishing "gate failing" from "budget pressure".

**Cost Implications:** ~0.5 day. No infra cost.

**Risk Assessment:** Medium-high — introduces a priority mechanism into the
shared limiter used by the pipeline; the isolation the problem actually needs
is not achieved.

---

### Option C: Fail-closed budget (deny → error)

Like Option A but a denied budget returns an HTTP error to the anonymous
caller instead of an `unverified` verdict.

**Advantages:**
- Simplest semantics: no budget, no valuation.

**Disadvantages:**
- Converts capacity pressure into hard failures on the public surface
  (5xx storm under load — the failure mode public endpoints must avoid).
- The trademark gate's own behaviour on provider failure is `unverified`,
  not error; Option C introduces an inconsistent failure mode for a
  capacity problem.
- Worse UX and worse optics than a verdict that explains itself.

**Cost Implications:** ~0.5 day. No infra cost.

**Risk Assessment:** Medium — public availability degrades into error
responses exactly when traffic is highest.

## Decision

**Chosen option: Option A — dedicated fail-open budget gate.**

The public namespace gets its own trademark-check allowance with a bounded
acquire and fail-open semantics. This is the only option that achieves the
three non-negotiables at once: the pipeline's trademark capacity is isolated
by construction (no code path from anonymous traffic to the pipeline's
tokens), anonymous provider spend is hard-bounded, and the public surface
degrades into the same `unverified` verdict the gate already produces on
provider errors — no new failure mode.

The gate wraps a `RateLimiterLike` (in-memory `RateLimiter`, or
`RedisRateLimiter` on the `anon-trademark` namespace in Cloud) and exposes a
single `tryAcquire()` that never throws: grant → run the gate; denial →
`unverified`, provider untouched. Disabled by default so the community
edition ships byte-identical behaviour; operators who want public quality
under load raise `ANON_TRADEMARK_RATE_LIMIT_TOKENS` or disable the budget.

## Consequences

### Positive
- Anonymous valuations can never starve pipeline trademark checks, in any
  process topology (shared Cloud namespace via Redis).
- Provider spend from the public surface is bounded by configuration
  (2 tokens/s default); a traffic spike costs budget tokens, not money.
- Fail-open matches existing public semantics: an `unverified` verdict with
  `suggestedBuyMax` stripped is already the ADR-0006 behaviour on provider
  failure — visitors see the same quality degradation, not a new error.
- New Prometheus counters make containment visible: a jump in
  `dominus_anon_trademark_blocked_total` is a capacity signal with a config
  knob attached.
- ADR-0006 pipeline semantics untouched: the pipeline's trademark checks
  keep the shared buckets, fail-closed, as before.

### Negative
- Visitors under sustained anonymous demand may receive `unverified`
  verdicts that are capacity artifacts (mitigated by raising the budget).
- 4 new env vars and a new component (`AnonBudgetGate`) to learn.
- Fail-open is surface-specific by design; future maintainers must not
  extend it to the pipeline (the code path separation enforces this: the
  gate exists only inside `AnonScoringService`).

### Compliance and Security Implications
- None. No new data flows; no new attack surface — the budget adds a rate
  limit, it removes none. Public endpoints keep their existing per-IP
  limits (ADR-0043).

### Migration and Monitoring Plan
- Defaults off: existing deployments change nothing; enabling is a 1-line
  env change (`ANON_TRADEMARK_BUDGET_ENABLED=true`).
- Metrics to watch after enabling: `dominus_anon_trademark_hits_total`
  (rate of public gate checks), `dominus_anon_trademark_blocked_total`
  (share failing open — keep near 0; raise
  `ANON_TRADEMARK_RATE_LIMIT_TOKENS` if it grows), pipeline trademark stage
  latency (should be unaffected, by design).
- Rollback: set `ANON_TRADEMARK_BUDGET_ENABLED=false` — restores prior
  behaviour instantly.

### Validation
- Unit tests: gate grants/denies/fails open on queue-full, stall and limiter
  error; service returns `unverified` without touching the provider on
  denial; telemetry callback fires per attempt; config defaults/overrides;
  factory builds in-memory/Redis/disabled variants.
- Full backend suite (2241 tests) green with defaults off — community
  behaviour byte-identical.
- Timeline: 1 release cycle in Cloud before judging the default budget.

---

*This ADR was created following the MADR 4.0.0 standard. All DOMINUS ADRs should be consistent with the ADR series starting at `docs/adr/0001-project-architecture.md`.*