Context
burn compare today calls queryAll() and then runs buildCompareTable() over the resulting EnrichedTurn[] — a full ledger scan plus an in-memory grouped reduce by (model, activity) for every invocation (packages/cli/src/commands/compare.ts:8,86, packages/analyze/src/compare.ts). The compare table is exactly the kind of grouped aggregate SQL was designed for, and the archive landed in PR #78 already has the right indexes (idx_turns_model, idx_turns_activity, idx_turns_workflow, idx_turns_project_key).
PR #78 explicitly defers this rewire:
Rewiring burn summary / compare / plans / @relayburn/mcp to read from the archive (each command is a self-contained migration that keeps the in-memory fallback intact).
Issue #40 calls this out specifically as well:
burn compare (#38) and burn plans (#39) can be implemented as SQL-style grouped queries rather than ad hoc in-memory scans.
Proposal
- Add a
compareFromArchive(query, opts) helper to @relayburn/ledger (or @relayburn/analyze) that issues SELECT model, activity, COUNT(*) AS turns, SUM(...) AS tokens, ... FROM turns WHERE ... GROUP BY model, activity.
- Cost-per-turn, 1-shot rate, cache-hit-rate, and median retries either materialize as part of the query (where SQLite supports it) or do a tiny per-cell post-process over already-grouped rows. Median retries needs either a stored quantile or a small per-(model, activity) follow-up query.
--min-sample filtering happens in the SQL HAVING clause or as a post-filter on the cell list.
- Keep the
queryAll() + buildCompareTable() path behind a fallback flag for parity validation.
Tests:
- Parity test: same fixture, both code paths, same
CompareTable (deep-equal via --json) and same text/CSV output.
- Filter coverage:
--workflow, --agent, --project, --session, --since, --models all work through SQL.
- Empty / single-cell edge cases.
Acceptance criteria
Out of scope
- Compare-specific schema additions to the archive.
- Coverage / fidelity columns.
- Rewiring
summary, plans, or MCP tools.
Refs
Context
burn comparetoday callsqueryAll()and then runsbuildCompareTable()over the resultingEnrichedTurn[]— a full ledger scan plus an in-memory grouped reduce by(model, activity)for every invocation (packages/cli/src/commands/compare.ts:8,86,packages/analyze/src/compare.ts). The compare table is exactly the kind of grouped aggregate SQL was designed for, and the archive landed in PR #78 already has the right indexes (idx_turns_model,idx_turns_activity,idx_turns_workflow,idx_turns_project_key).PR #78 explicitly defers this rewire:
Issue #40 calls this out specifically as well:
Proposal
compareFromArchive(query, opts)helper to@relayburn/ledger(or@relayburn/analyze) that issuesSELECT model, activity, COUNT(*) AS turns, SUM(...) AS tokens, ... FROM turns WHERE ... GROUP BY model, activity.--min-samplefiltering happens in the SQLHAVINGclause or as a post-filter on the cell list.queryAll()+buildCompareTable()path behind a fallback flag for parity validation.Tests:
CompareTable(deep-equal via--json) and same text/CSV output.--workflow,--agent,--project,--session,--since,--modelsall work through SQL.Acceptance criteria
burn compareissues a single grouped SQL query for the cell aggregates rather than streaming the full ledger.--jsonoutputs are byte-identical to the pre-migration implementation for the parity fixture.--models,--since,--project,--session,--workflow,--agent,--min-sample) continue to work.burn compare --since 30dis at least 5x faster than the current implementation.Out of scope
summary,plans, or MCP tools.Refs
burn archive(#40) #78 (Derived analytics archive: materialize the ledger into a local queryable store #40 follow-up)