Skip to content

Token-ROI Phase 1 Spec — core + turn report

Read docs/METRICS-STANDARD.md first (formulas §2, savings §5, turn line §10). Phase 1 delivers the per-turn pipeline end-to-end: parse Claude session JSONL → compute ETC + savings → append unified ledger → print one-line turn report. Attribution/gate/report verbs are Phase 2 — do NOT build them.

Solution layout (repo root)

TokenRoi.sln
src/TokenRoi.Core/           netstandard-free, net8.0 (or highest installed SDK; check dotnet --list-sdks)
src/TokenRoi.Cli/            console, System.CommandLine or manual arg parse (keep deps minimal)
tests/TokenRoi.Tests/        xunit + Bogus (mocked-coverage mandate: every Core class tested, no I/O in unit tests — inject readers)
pricing.json                 rate card (see below)

Core classes

  1. UsageBlock record: InputTokens, OutputTokens, CacheCreationInputTokens, CacheReadInputTokens, Model, Timestamp.
  2. ClaudeSessionParser
  3. Input: path to a session .jsonl (Claude Code format: one JSON object per line; assistant events have type:"assistant", message.usage{input_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens}, message.model, plus top-level cwd, gitBranch, sessionId, timestamp).
  4. ParseLastTurn(stream): a "turn" = contiguous tail of events from the LAST type:"user" event that is a real user prompt (not tool_result — user events whose message.content is an array containing tool_result items are NOT prompt boundaries) through end of file; aggregate all assistant usage blocks in that span (main thread; sidechain/subagent files are separate — ignore).
  5. ParseSession(stream): all turns + session metadata (cwd, gitBranch, sessionId).
  6. Must be resilient: skip malformed lines, missing usage blocks, unknown fields (forward-compat).
  7. EtCalculator
  8. Et(UsageBlock) = 1.0·input + 1.0·cacheCreate + 0.1·cacheRead + 4.0·output
  9. CostUsd(UsageBlock, PricingTable) = input×inRate + cacheCreate×cacheWriteRate + cacheRead×cacheReadRate + output×outRate (per MTok). Unknown model → nearest prefix match (e.g. claude-sonnet-*), else confidence:"estimated" with fallback rate + warning.
  10. PricingTable loaded from pricing.json. Seed rates ($/MTok, in/cacheWrite/cacheRead/out):
  11. claude-fable-5 & claude-opus-*: 15 / 18.75 / 1.5 / 75
  12. claude-sonnet-*: 3 / 3.75 / 0.3 / 15
  13. claude-haiku-*: 1 / 1.25 / 0.1 / 5
  14. gpt-5: 1.25 / — / 0.125 / 10 ; gemini-: 2 / — / 0.5 / 12 ; grok-: 3 / — / 0.75 / 15 ; ollama/: 0 (tracked tokens only)
  15. Comment in file: VERIFY before trusting dollar outputs; rates change.
  16. GovernorReader: parse C:\Users\fives\.claude\usage-governor\state.json → { tier, pctUsed, projectedPct }. Weekly pct: if a weekly field exists use it, else null → print wk —. Tolerate file missing (all null).
  17. CavemanReader: latest entry for a sessionId from .caveman-history.jsonl (path configurable) → estSavedTokens/estSavedUsd. MODELED bucket.
  18. SavingsCalculator:
  19. MEASURED: cacheReadSavings = (inRate − cacheReadRate) × cacheReadTokens.
  20. MODELED: caveman est (from reader).
  21. Output: two decimals, separate fields — never summed.
  22. LedgerWriter: append JSONL to %LOCALAPPDATA%\TokenRoi\ledger.jsonl: {ts, machine, tool:"claude-code", model, in, out, cacheRead, cacheCreate, costUsd, confidence:"exact", sessionId, cwd, branch, ticket?}. Ticket = regex \b([A-Z]{2,6}-\d+)\b over gitBranch, else null. Create dir if missing. Append-only, one File.AppendAllText with newline (atomic enough for single writer).

CLI verbs

  • token-roi turn [--session <path>] [--project-dir <dir>] [--no-ledger]
  • No --session: find newest *.jsonl under C:\Users\fives\.claude\projects\<encoded-project-dir>\ for --project-dir (encode: full path, :-(drop), \-, i.e. C:\Users\fives\source\repos → C--Users-fives-source-repos), else newest across all projects.
  • Prints EXACTLY (suppress zero/null segments): ⛽ $0.19 ET 12.4k (in 8.1k cr 3.2k out 1.1k) | 5h 34% YEL wk 61% | cache 78% | saved $0.04 meas + ~$0.11 est | WW-42
    • cr = cache_read; cache % = cacheRead/(input+cacheRead); tier 3-letter (GRN/YEL/RED/BLK); k-format 1 decimal.
  • Also appends ledger row unless --no-ledger.
  • token-roi scan --session <path>: whole-session totals, same line format prefixed Σ session.

Tests (xunit + Bogus)

  • Parser: turn boundary correctness (tool_result user events NOT boundaries), malformed line resilience, empty file, usage aggregation across multi-assistant-message turn. Use inline JSONL string fixtures built with Bogus for field values.
  • EtCalculator: formula exactness, prefix model matching, unknown model fallback.
  • SavingsCalculator: measured vs modeled never summed (assert distinct fields).
  • LedgerWriter: ticket regex extraction (WW-42, ABC-123, no match), row schema.
  • GovernorReader/CavemanReader: missing file → nulls, not throw.
  • Turn line formatter: golden-string tests incl. zero-suppression cases.

Build gates (must pass before you finish)

dotnet build clean, dotnet test green. Run token-roi turn against a REAL session file from C:\Users\fives.claude\projects\C--Users-fives-source-repos\ (pick newest) and include its output line in your report. Handle UTF-8 BOM and very large session files (stream, don't ReadAllText).

Style: file-scoped namespaces, nullable enabled, no external deps beyond System.CommandLine (optional), xunit, Bogus. README.md stub linking docs/METRICS-STANDARD.md.