Skip to content

Add --tag and --by-tag filtering to burn summary#369

Merged
willwashburn merged 1 commit intomainfrom
claude/add-tag-filtering-eRlmw
May 8, 2026
Merged

Add --tag and --by-tag filtering to burn summary#369
willwashburn merged 1 commit intomainfrom
claude/add-tag-filtering-eRlmw

Conversation

@willwashburn
Copy link
Copy Markdown
Member

Summary

burn summary couldn't slice or compare by the user-supplied tags written by burn run --tag k=v. This adds two surface changes that reuse the existing stamp/enrichment plumbing:

  • Filter: --tag k=v (repeatable, AND-combined) narrows turns to the stamped enrichment value. Rides alongside the existing --workflow sugar; both land in q.enrichment so the ledger walk and SQLite archive both filter at query time.
  • Compare: --by-tag <key> is a new mode flag that groups rows by an enrichment value the same way --by-provider/--by-model group today. Turns missing the key bucket as (unset) so coverage isn't silently dropped.

JSON output emits byTag: { key, rows: [{ value, turns, usage, cost }] } and the per-cell fidelity block uses groupBy: 'tag'. --by-tag is mutually exclusive with the other --by-* modes; combining them or passing it without a key exits non-zero.

Example:

burn run claude --tag workflow=refactor -- --resume
burn run claude --tag workflow=review   -- --resume
burn summary --tag workflow=refactor --since 7d
burn summary --by-tag workflow --since 30d

Test plan

  • pnpm run build — clean
  • pnpm -w run test — 875/875 pass, including 5 new --tag / --by-tag cases (filter narrowing, group-by-value with (unset) bucket, missing-key error, mode-exclusivity error, text header tag:<key>)
  • Manual: run burn summary --by-tag workflow against a ledger with mixed --tag workflow=... stamps

Notes

  • SDK (@relayburn/sdk@1.x) is intentionally untouched — it's in maintenance-only mode per the existing CHANGELOG. New programmatic surface for tags would land in @relayburn/sdk@2.x (Rust napi) once that path opens.
  • No schema changes — Enrichment is already Record<string,string> and the archive's json_extract(enrichment_json, ...) filter already covers arbitrary keys.

https://claude.ai/code/session_01TSeLhFCGujBx73mw2DpMt3


Generated by Claude Code

`--tag k=v` filters turns by stamped enrichment values (multiple flags
AND together; rides alongside the existing `--workflow` sugar). `--by-tag
<key>` adds a new mode that groups rows by an enrichment value, with
turns missing the key bucketed as `(unset)`. JSON output emits
`byTag: { key, rows: [...] }` and the per-cell fidelity block uses
`groupBy: 'tag'`.

https://claude.ai/code/session_01TSeLhFCGujBx73mw2DpMt3
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds tag-based filtering and grouping capabilities to the burn summary command. Users can now filter turns by enrichment stamp values using --tag k=v (supporting multiple filters via AND logic) and group summary rows by enrichment keys using --by-tag <key>, with JSON and table output formats updated accordingly.

Changes

Tag Filtering and Grouping for Burn Summary

Layer / File(s) Summary
Type and Contract Updates
packages/cli/src/commands/summary.ts
PerCellFidelityBlock type extended to include groupBy: 'tag'; buildPerCellFidelity signature updated to accept tag dimension.
Core Tag Enrichment and Aggregation
packages/cli/src/commands/summary.ts
enrichmentFilter constructed from args.tags with --workflow mapping; --by-tag validation ensures key is provided; mode exclusivity prevents combination with other aggregation modes; TAG_UNSET_LABEL constant and aggregateByTag function added for grouping by enrichment values.
Output Generation for Tag Aggregation
packages/cli/src/commands/summary.ts
Aggregate row computation generalized to include tag mode; JSON output now groups by tag value with per-cell fidelity under model|provider|tag axis; table header adjusted to show tag:<key> column and stamped values.
Test Infrastructure Update
packages/cli/src/commands/summary.test.ts
captureSummary test helper accepts typed tags parameter and forwards it to runSummary.
Tag Filtering and Grouping Tests
packages/cli/src/commands/summary.test.ts
Tests added for --tag k=v filtering, --by-tag <key> grouping with (unset) bucket, error when tag key missing, rejection when combined with --by-tool, and text-mode rendering with tag column.
Documentation and Help Text
packages/cli/src/cli.ts, packages/cli/CHANGELOG.md
CLI help text extended with example invocations; CHANGELOG documents new options, JSON output shape, and per-cell fidelity grouping behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

A rabbit hops through tags today,
Grouping turns in every way,
Enrichment values, set or bare,
Summary filters with utmost care. ✨🏷️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and concisely summarizes the main changes: adding --tag and --by-tag filtering options to the burn summary command, which is the core focus of the PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the filtering and grouping features, JSON output changes, usage examples, and test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/add-tag-filtering-eRlmw

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/cli/src/commands/summary.test.ts (1)

1416-1420: ⚡ Quick win

Add an explicit empty-key test case (--by-tag=) to lock validation.

Current missing-key test covers boolean form (--by-tag), but not empty-string form. Adding it prevents regressions if parser shape changes.

✅ Suggested test addition
   it('--by-tag without a key exits non-zero', async () => {
     const out = await captureSummary({ 'by-tag': true });
     assert.equal(out.code, 2);
     assert.match(out.stderr, /--by-tag requires a tag key/);
   });
+
+  it('--by-tag= (empty key) exits non-zero', async () => {
+    const out = await captureSummary({ 'by-tag': '' });
+    assert.equal(out.code, 2);
+    assert.match(out.stderr, /--by-tag requires a tag key/);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/summary.test.ts` around lines 1416 - 1420, Add a
new test case that calls captureSummary with the explicit empty key form (e.g.,
{'by-tag': ''} or invoking the CLI with '--by-tag=') to ensure the parser
rejects an empty tag key; it should mirror the existing test that uses the
boolean form by asserting out.code is 2 and out.stderr matches /--by-tag
requires a tag key/. Place the new case alongside the existing "it('--by-tag
without a key exits non-zero')" test so it validates the empty-string form and
prevents regressions in captureSummary and the CLI option parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/summary.ts`:
- Around line 73-78: The check only rejects the boolean form of --by-tag but
allows an empty string (--by-tag=) to pass; update the handling around
byTagFlag/byTagKey so that byTagKey is set only when byTagFlag is a non-empty
string, and treat an empty string the same as the boolean true: write the same
error to stderr and return 2. Specifically, change the logic that assigns
byTagKey and the subsequent if (byTagFlag === true) branch to detect typeof
byTagFlag === 'string' && byTagFlag.length > 0 for a valid key, and otherwise
print "burn: --by-tag requires a tag key..." and return 2 when byTagFlag is true
or an empty string.

---

Nitpick comments:
In `@packages/cli/src/commands/summary.test.ts`:
- Around line 1416-1420: Add a new test case that calls captureSummary with the
explicit empty key form (e.g., {'by-tag': ''} or invoking the CLI with
'--by-tag=') to ensure the parser rejects an empty tag key; it should mirror the
existing test that uses the boolean form by asserting out.code is 2 and
out.stderr matches /--by-tag requires a tag key/. Place the new case alongside
the existing "it('--by-tag without a key exits non-zero')" test so it validates
the empty-string form and prevents regressions in captureSummary and the CLI
option parsing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 27db3f47-e126-4a25-9a32-ad4e2158f278

📥 Commits

Reviewing files that changed from the base of the PR and between 8391297 and 811f4e3.

📒 Files selected for processing (4)
  • packages/cli/CHANGELOG.md
  • packages/cli/src/cli.ts
  • packages/cli/src/commands/summary.test.ts
  • packages/cli/src/commands/summary.ts

Comment on lines +73 to +78
const byTagFlag = args.flags['by-tag'];
const byTagKey = typeof byTagFlag === 'string' ? byTagFlag : undefined;
if (byTagFlag === true) {
process.stderr.write('burn: --by-tag requires a tag key (e.g. --by-tag workflow)\n');
return 2;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject empty --by-tag keys, not just bare boolean form.

Line 75 only catches --by-tag (boolean). --by-tag= (empty string) can slip through and should also return code 2 per the command contract.

🔧 Suggested fix
   const byTagFlag = args.flags['by-tag'];
-  const byTagKey = typeof byTagFlag === 'string' ? byTagFlag : undefined;
-  if (byTagFlag === true) {
+  const byTagKey = typeof byTagFlag === 'string' ? byTagFlag.trim() : undefined;
+  if (byTagFlag === true || (typeof byTagFlag === 'string' && byTagKey.length === 0)) {
     process.stderr.write('burn: --by-tag requires a tag key (e.g. --by-tag workflow)\n');
     return 2;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const byTagFlag = args.flags['by-tag'];
const byTagKey = typeof byTagFlag === 'string' ? byTagFlag : undefined;
if (byTagFlag === true) {
process.stderr.write('burn: --by-tag requires a tag key (e.g. --by-tag workflow)\n');
return 2;
}
const byTagFlag = args.flags['by-tag'];
const byTagKey = typeof byTagFlag === 'string' ? byTagFlag.trim() : undefined;
if (byTagFlag === true || (typeof byTagFlag === 'string' && byTagKey.length === 0)) {
process.stderr.write('burn: --by-tag requires a tag key (e.g. --by-tag workflow)\n');
return 2;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/summary.ts` around lines 73 - 78, The check only
rejects the boolean form of --by-tag but allows an empty string (--by-tag=) to
pass; update the handling around byTagFlag/byTagKey so that byTagKey is set only
when byTagFlag is a non-empty string, and treat an empty string the same as the
boolean true: write the same error to stderr and return 2. Specifically, change
the logic that assigns byTagKey and the subsequent if (byTagFlag === true)
branch to detect typeof byTagFlag === 'string' && byTagFlag.length > 0 for a
valid key, and otherwise print "burn: --by-tag requires a tag key..." and return
2 when byTagFlag is true or an empty string.

@willwashburn willwashburn merged commit d8af6ac into main May 8, 2026
3 checks passed
@willwashburn willwashburn deleted the claude/add-tag-filtering-eRlmw branch May 8, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants