Summary
The gateway's audit pipeline emits the same tier_denied outcome string for two semantically distinct failure modes:
- Tier restriction —
gateway.ts:1322, via enforceTierRestriction (e.g., free user requests a premium quality tier that their plan doesn't cover).
- Quota exhaustion —
gateway.ts:1350, via reserveQuota returning allowed: false (user has credits available in principle but has burned their monthly budget).
These are different operational signals. "Plan feature gate denied" should not roll up with "user hit their credit ceiling" — ops queries like "how often are tenants hitting their credit limit this week?" or "which users are repeatedly requesting tiers their plan can't cover?" can't be answered from audit today because both collapse to the same string.
Proposed fix
Split the outcome enum: keep tier_denied for the plan-feature path and add quota_exceeded for the budget-exhaustion path. audit.ts owns the enum; gateway.ts:1350 is the only call site that needs to switch.
Context
Surfaced during PR #26 review. Not a blocker for ship (the denial itself is correct in both cases), but the audit drift rots if it isn't captured now — every downstream dashboard or alert that keys off outcome will get built on top of the overloaded string and be harder to fix later.
Summary
The gateway's audit pipeline emits the same
tier_deniedoutcome string for two semantically distinct failure modes:gateway.ts:1322, viaenforceTierRestriction(e.g., free user requests apremiumquality tier that their plan doesn't cover).gateway.ts:1350, viareserveQuotareturningallowed: false(user has credits available in principle but has burned their monthly budget).These are different operational signals. "Plan feature gate denied" should not roll up with "user hit their credit ceiling" — ops queries like "how often are tenants hitting their credit limit this week?" or "which users are repeatedly requesting tiers their plan can't cover?" can't be answered from audit today because both collapse to the same string.
Proposed fix
Split the outcome enum: keep
tier_deniedfor the plan-feature path and addquota_exceededfor the budget-exhaustion path.audit.tsowns the enum;gateway.ts:1350is the only call site that needs to switch.Context
Surfaced during PR #26 review. Not a blocker for ship (the denial itself is correct in both cases), but the audit drift rots if it isn't captured now — every downstream dashboard or alert that keys off outcome will get built on top of the overloaded string and be harder to fix later.