---
id: w8/m4/t005
title: Period query reads usage_monthly outside the hot window, usage_hourly inside it
worker: worker8
status: done
estimate: 40m
depends_on: [w8/m4/t002]
---

## Objective

Make `monthToDateAt` (and the `period=` historical path built on it) transparently read from `usage_monthly` for months outside the hot window and `usage_hourly` for months inside it, returning the identical `Summary` shape either way — REST/GraphQL/MCP all consume this one method, so fixing it here fixes all three adapters at once.

## Context

- `lego/backend/internal/usage/service.go:126` `monthToDateAt` is the single read path all three adapters call through — matches the "one Core, three thin adapters" design (`docs/ADR006-bex-api.md`).
- Must stay correct for the in-progress current month (always hourly — it hasn't been compacted yet) and behave identically whether or not compaction has run yet for a given past month (defensive: fall back to `usage_hourly` if `usage_monthly` has no row for that month, so query correctness never depends on compaction having already run).

## Steps

1. In `monthToDateAt`, branch on whether the requested month is inside or outside the hot window (or, more robustly: outside the hot window, use `usage_monthly`; if it's the current month or has no `usage_monthly` row yet, fall back to `usage_hourly`).
2. Ensure `summarise` produces the same `Summary` shape from either source.
3. No adapter-level changes needed — REST/GraphQL/MCP already call through this one method.

## Files

- `lego/backend/internal/usage/service.go`
- `lego/backend/internal/store/usage.go`

## Acceptance criteria

- [ ] A period query for a compacted month returns totals sourced from `usage_monthly`.
- [ ] A period query for a month not yet compacted (or still hot) returns totals sourced from `usage_hourly`, identical to today's behavior.
- [ ] Totals match exactly across the compaction boundary (query the same month before and after compaction — same numbers).

## Out of scope

- Any REST/GraphQL/MCP adapter code changes — this task is Core-only by design.
