Add --bucket time-series to summary and compare

Merged
WI
4mo ago
+749-21

Summary

Adds --bucket <DURATION> to the read verbs that take --since, turning a single windowed total into a time-series: the verb's report computed per fixed-width time bucket. Built for a live spend chart that needs selectable ranges (last 5m / 1h / 12h / 1d / 7d) sourced from one indexed query rather than N client-side round-trips.

JSON shape (both verbs): { "bucketSeconds": N, "buckets": [ { "start": ISO, "end": ISO, ...the verb's normal report... } ] }.

What's included

  • Shared primitive (query_verbs/mod.rs): parse_bucket (grammar 30s/5m/1h/12h/1d/7d), iso_z_to_epoch_secs, and a Buckets partition anchored on the normalized --since (or the earliest turn), with a MAX_BUCKETS guard. Reuses the existing fetch (query_turns + idx_turns_ts), cost_for_turn, load_pricing, and provider_for.
  • summary: summary_timeseries — turn-level partition (the grouped model/provider/tag path is a pure per-turn fold). Rejected with a clear error for the --by-tool/subagent/relationship attribution modes.
  • compare: compare_timeseries — per-bucket compare table.
  • CLI: --bucket on summary and compare; JSON emits the series, human output prints a per-bucket line.
  • Tests (5): parser grammar, ISO↔epoch round-trip, Buckets edges/indices, summary_timeseries placement reconciling exactly to the un-bucketed total, attribution-mode rejection.

Note: m = minutes for --bucket

--since treats m as month, but month-sized buckets are meaningless and the consumer wants minute buckets ("last 5 minutes"). So --bucket uses m = minutes and adds s = seconds. Documented in the flag help and code.

Deferred: hotspots / overhead

Both also take --since, but their aggregation isn't a pure per-turn fold — hotspots attributes turn N's cost to turn N−1's tool_use (ordering/cross-turn + session-scoped side queries) and overhead computes window-amortized per-session denominators (and does filesystem work). Correct bucketing for these needs session-granular partitioning (assign whole sessions to a bucket by earliest ts). Rather than ship incorrect attribution, --bucket is simply absent on those two; it's a clean follow-up.

Verification

cargo build --workspace ✅ · cargo test --workspace ✅ (all suites pass; 5 new). Smoke vs a real ledger: summary --provider anthropic --since 1h --bucket 5m --json → bucketSeconds=300, 12 buckets, and bucket-sum tokens == the plain --since 1h total exactly.

🤖 Generated with Claude Code