ADORSYS-GIS / ADORSYS-GIS/lightbridge-governance
[Story]: Total AI spend per engineer across all four sources
- Dominant language
- Rust
- Stars
- 1
- Forks
- 2
- Avg merge
- 13h 13m
- Merged PRs (30d)
- 110
Description
### Story Statement
As an **engineering manager**,
I want **one view of total AI spend per engineer across the gateway, Copilot, Claude Code and Codex**,
so that **I can answer "what does this person's AI usage cost" without stitching four systems together by hand.**
### Real Intent
This is the payoff, and it is the reason every other story insists on integer micro-USD and a Keycloak `sub`. Four sources that each measure honestly but cannot be added together are four dashboards nobody opens.
It is also where partial coverage becomes dangerous. A per-engineer total that silently omits a source reads as authoritative and is wrong in the direction that matters — too low.
### Background and Context
The platform already reads Postgres directly from Grafana (ai-helm ADR-0063, `uid: keycloak`), which is what lets this join on columns instead of fighting metric cardinality. Gateway spend is in Mimir keyed on `user_id`; Copilot, Claude Code and Codex land in the governance store keyed on the Keycloak `sub`.
### Source of truth (links)
- Epic: #30
- Depends on: #32, #33, #35 (the data), ai-helm#879 (the Grafana Postgres datasource)
- Pattern precedent: ai-helm ADR-0063, `docs/patterns/per-user-observability.md`
- Related: the Copilot dashboards, ai-helm#880
### Acceptance Criteria
#### Functional
- [ ] Given a developer, when the view loads, then their spend is shown **broken out by source** and totalled.
- [ ] Given all four sources, when totalled, then they are in the same unit and the total is arithmetically the sum — no conversion at query time.
- [ ] Given a time range, when it changes, then every source respects it consistently. ⚠️ Gateway spend is a live counter; the connectors are daily/near-real-time. A range shorter than a connector's granularity must not silently show zero for it.
#### Negative / Edge Cases
- [ ] ⚠️ Given a source has **no data** for a developer, when the view loads, then it renders as **"no data"**, distinct from **"zero spend"**. These mean opposite things — one is "they didn't use it", the other is "we can't see it" — and a total that conflates them is worse than no total.
- [ ] Given a developer appears in one source and not others (no ingest token, no Copilot seat), when the view loads, then that is **visible as a coverage gap**, not rendered as thrift.
- [ ] Given spend that could not be attributed to any person, when the view loads, then it appears in an **unattributed** bucket rather than being dropped. Unattributed spend is a finding, not a rounding error.
#### Non-Functional
- [ ] Reads the governance Postgres datasource for connector data; Mimir only for gateway spend.
- [ ] Dashboard source lives in `tools/dashboards/` (ai-helm); ⚠️ `uv run dashboards build` must be run and the regenerated JSON committed or `dashboards-drift` CI fails.
- [ ] ⚠️ Every `GrafanaFolder` sets `resyncPeriod` — Grafana runs stateless here, so a pod roll wipes operator-pushed folders and `folderRef` dashboards then fail `[400] folder not found` until the operator restarts.
### Out of Scope
- Any new collection. This story only joins what #32/#33 and the Copilot connector already store.
- Budget enforcement or alerting on spend. Visibility first.
- Per-team or per-cost-centre rollups — a later story once per-person is trusted.
### Dependencies and Blockers
- Depends on #32, #33, #35 and ai-helm#879.
- ⚠️ Genuinely blocked on real data. Building this against seeded data risks a view that looks right and is wrong; sequence it after at least one source is live.
### Assumptions
1. **All four sources resolve to the same Keycloak `sub`.** That is what #35 exists to guarantee for the two new ones; the gateway and Copilot already do.
2. **Integer micro-USD throughout** (ADR-0008), so the sum needs no conversion.
3. Grafana can reach the governance database (ai-helm#879). ⚠️ Needs its Cilium egress — every Postgres datasource here has hit that.
### Implementation Notes
- Make coverage a first-class element, not a footnote. A small "sources reporting for this developer: 3 of 4" beats a confident wrong total.
- `joinByField` on the identity key, the way the ADR-0063 user-directory dashboard does.
- Prefer an outer join so a developer missing from one source still appears, with that source blank rather than the row vanishing.
### Test Expectations
- Manual against real data, per source and combined.
- A developer present in only one source renders correctly.
- Unattributed spend appears in its bucket.
- Roll the Grafana pod and confirm folders and dashboards survive.
### Verification evidence
*To be completed on delivery.* Required: screenshots with real data showing the four-source breakdown, a partial-coverage developer, and the unattributed bucket; `uv run dashboards check` clean; evidence the dashboard survives a Grafana pod roll.
### Human accountable owner
@stephane-segning
### AI Usage Declaration
Drafting this story, Refining acceptance criteria
### Human verification completed
- [x] I checked the story against the source of truth
- [x] I confirmed the acceptance criteria
- [x] I documented remaining risks
- [x] I am the accountable owner and accept responsibility for this story.
Contributor guide
Assessment
This issue has not been assessed yet.