ADORSYS-GIS / ADORSYS-GIS/lightbridge-authz
[Story]: Phase 5 — automatic budget augmentation on threshold crossing
- Ngôn ngữ chính
- Rust
- Star
- 0
- Fork
- 1
- Merge trung bình
- 6 giờ 42 phút
- Pull request đã merge (30 ngày)
- 246
Mô tả
### Story Statement
As the **AI governance platform**,
I want **budget augmentation to fire automatically when a configured threshold is crossed, exactly once per threshold crossing**,
so that **a user who stays above a rung's danger line gets refilled without a human or the user having to ask, and never gets refilled twice for the same crossing.**
### Real Intent
"Never by granting on every inference request" is the load-bearing constraint (RFC-0001). This is a trigger, not a limiter: a state transition or scheduled reconciliation notices the threshold is crossed and grants once; nothing in the inference request path evaluates this.
Outline only — filed now so the epic's delivery-plan PR breakdown has an issue to reference; not scheduled ahead of #189/#190/#191.
### Background and Context
RFC-0001 specifies the idempotency mechanism precisely: a deterministic trigger key with a UNIQUE constraint,
```text
auto:{policy_revision}:{period}:{budget_account_id}:{rule_id}:{threshold}
```
⚠️ **This story's threshold source is `usage_events` in `lightbridge-authz-usage` (already ingesting OTLP and computing `total_cost` per account/project/api-key since `20260320000001_total_cost.sql`), read directly from Timescale — not a Mimir/Redis round-trip.** That adapter is planned as its own PR (1.5) ahead of this story, because it has a sharp edge that must not leak into the trigger: `SUM(total_cost)` over zero matching rows is SQL `NULL`, and the existing usage-query repo already collapses that to `0.0`. A threshold trigger must never evaluate against a collapsed zero — an account with no data (broken ingest, aged out of retention, or the account never sent any traffic yet) is **unknown**, not **spent nothing**, and a trigger firing on "unknown" would grant against a fact that isn't true. This story consumes a `Spend::Known(_) | Spend::Unavailable` value from that adapter and must **suppress the trigger entirely on `Unavailable`** rather than treating it as zero.
Also relevant: the usage database's retention policy is 30 days where TimescaleDB is present and unbounded where it is not (an environment-dependent silent divergence, not a bug this story needs to fix, but one it must not be surprised by) — so "last period's consumption" style thresholds may need a rollup that survives retention. Decide this alongside PR 1.5, not from scratch in this story.
### Source of truth (links)
- Epic: #188
- Depends on: #189 (ledger — the grant this trigger produces), #190 (policy — the threshold/rule that decides to trigger)
- RFC: docs/rfc/0001-budget-refill.md, "Automatic augmentation" section (trigger key, idempotency)
- Runbook: docs/runbooks/stuck-augmentation-request.md (already written against this domain's failure modes)
### Acceptance Criteria
#### Functional
- [ ] Given an account whose spend crosses a configured threshold, when the trigger fires (state transition or scheduled reconciliation), then exactly one grant is recorded, keyed by the deterministic trigger key.
- [ ] Given the same threshold crossing evaluated across many reconciliation cycles, when the trigger key already has an entry, then no further grant is issued — the UNIQUE constraint is what stops it, not application-level "did I already do this" logic.
#### Negative / Edge Cases
- [ ] Given the spend adapter reports `Spend::Unavailable` for an account/period, when the reconciliation job runs, then the trigger is **suppressed**, not evaluated against zero.
- [ ] Given the policy engine is unavailable, when a threshold crossing is detected, then no grant is issued (fail closed, matching #190's contract).
#### Non-Functional
- [ ] Amounts integer micro-USD throughout.
- [ ] `cargo clippy --all-targets --all-features -- -D warnings` clean.
### Out of Scope
- The spend-reading adapter itself (planned as PR 1.5 ahead of this story) — this story consumes its typed output, it does not build the Timescale read path.
- The ledger and policy engine — #189/#190.
- Any gateway enforcement change.
### Dependencies and Blockers
- Depends on #189, #190, and the spend adapter (PR 1.5 in the epic's delivery plan).
- RFC open question: do automatic triggers need a per-period cap of their own, separate from the self-service rung cap? Needs an answer before this story is implementable in full.
### Assumptions
1. Automatic augmentation is a trigger, never a per-request check.
2. `Spend::Unavailable` is a legitimate, expected state (not an error to alert on by itself) and must resolve to "do nothing" here.
### Implementation Notes
Guidance, not gospel: worker + scheduled reconciliation, both going through the same trigger-key insert path so there is one place the UNIQUE constraint is relied on, not two.
### Test Expectations
- **The load-bearing test, verbatim from the RFC:** a budget held above a threshold across many evaluation cycles produces exactly **one** automatic grant.
- A named test that `Spend::Unavailable` suppresses the trigger rather than being read as zero.
### Verification evidence
*To be completed on delivery.*
### Human accountable owner
@stephane-segning — filed at outline depth on the epic owner's behalf per the #188 backlog comment (2026-08-01); needs their confirmation before entering sprint planning.
### AI Usage Declaration
Drafting this story
### Human verification completed
- [ ] I checked the story against the source of truth
- [ ] I confirmed the acceptance criteria
- [ ] I am the accountable owner and accept responsibility for this story.
> Filed by an AI planning session as part of decomposing epic #188 (see the delivery plan discussion on that issue). None of the boxes above are ticked because the session that filed this is not the accountable owner — @stephane-segning, please review and tick what you've verified.
Hướng dẫn đóng góp
Đánh giá
Issue này chưa được đánh giá.