ADORSYS-GIS / ADORSYS-GIS/lightbridge-authz

[Story]: Phase 5 — automatic budget augmentation on threshold crossing

Đang mở
#195 1 bình luận 0 reaction 0 người được giao Xem trên GitHub
governance user-story
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

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.