TIER docs

Interpreting the TIER Number

Read this before you trust a TIER score. TIER is honest about its own limits. The single most common way to misread the headline number is to trust a recent or short window -- and the reason is a measurement-timing artifact, not anything about how productive the work was.

Table of contents


Series breaks. Before comparing periods, check the score-series change register and apply its comparison rule; name the applicable changes or state that the definition history is unknown.

The one-sentence version

TIER's headline is

TIER = Σ(outcome_weight × quality_multiplier) / (cost_usd / $1,000)

computed over a half-open window [since, until). The numerator (outcome) and the denominator (cost) are windowed independently, each by its own timestamp -- and those two timestamps mean different things in time. Cost is timestamped when the tokens were spent; outcome is timestamped when the work closed. Because closing happens days or weeks after the spend that produced it, a recent or short window shows cost that has already landed against outcomes that have not been credited yet. The ratio reads artificially low early, then rises as outcomes close.


Why the skew exists: cost is forward, outcome is a close-time event

Two different clocks feed the two halves of the ratio.

Half of the ratio What is summed Timestamp used to window it When that timestamp lands
Denominator (cost) token_events.cost_micro token_events.ts Continuously, in real time, as tokens are spent
Numerator (outcome) outcomes.weight × outcomes.quality outcomes.ts At the instant the work closes (see below)

The scores handler runs DeveloperCostsWindow(since, until) and AllOutcomesWindow(since, until) as two separate windowed reads over the same [since, until). It then joins them by identity (developer, and for per-type segments the (developer, issue) pair) -- not by time. So a cost row and the outcome it eventually produces only both appear in a window if both of their independent timestamps happen to fall inside it.

What "outcome timestamp" precisely means (verified against the code)

This is the crux, so it is worth stating exactly. The outcome's ts is not backdated to the cost window. It is a point event stamped at close time:

So the outcome is credited all at once, at the close/commit instant (or the fallback time above) -- it is never spread back across the days when the cost was actually spent. The cost for that same issue, by contrast, was dribbled in continuously over those earlier days. The numerator is a spike at the end; the denominator is a ramp leading up to it. Window them independently and the two rarely line up.

On the word "backfilled": it is fair to say the outcome credit appears only once the work closes. But do not picture it as filling in the earlier cost days -- it does not. It lands as a single dated event at close, which is exactly why the trailing edge of a window sees the cost but misses the outcome.


A worked mini-example

A developer spends $40 of AI budget on issue #412 across Monday through Wednesday. The PR merges and closes #412 on Friday.

Now read that same work through three different windows:

gantt
    title Issue #412 -- cost is a ramp, outcome is a Friday spike
    dateFormat  YYYY-MM-DD
    axisFormat  %a

    section Cost ($40 total)
    $13 Mon        :done, c1, 2026-07-06, 1d
    $14 Tue        :done, c2, 2026-07-07, 1d
    $13 Wed        :done, c3, 2026-07-08, 1d

    section Outcome
    Closes #412 (weight credited here) :milestone, m1, 2026-07-10, 0d
Window [since, until) Cost seen Outcome seen TIER reads Reality
Mon 00:00 -> Thu 00:00 $40 0 (close is Friday) 0 Looks catastrophic. Nothing has closed yet.
Mon 00:00 -> Sat 00:00 $40 full weight of #412 correct The window is wide enough to contain both the spend and the close.
Fri 00:00 -> Sat 00:00 ~$0 full weight of #412 huge The close is inside; its cost fell before since. Flattering and equally wrong.

The Mon->Thu window is the day-1 trap: the $40 is real, the score of 0 is a lie of timing. Wait for Friday and the same work scores fine. Nothing about the work changed between Thursday and Friday -- only the measurement caught up.

A note on the "huge" cell: the number blows up because a little residual cost (a Friday tweak) still lands under a large closed-outcome weight. If a leading-edge window contains literally $0 of cost, the engine guards the ratio on a positive denominator (internal/scoring/engine.go) and reports TIER 0, not infinity -- so the failure surfaces as a suspicious zero rather than a divide-by-zero. Either way the reading is an artifact of the window edge, not of the work.


Both edges of every window are wrong -- in opposite directions

The example generalizes. Every window has two suspect edges:

In a wide, settled window these two edge effects are small relative to the bulk of fully-contained work. In a short window they can dominate the number entirely.

The leading edge is not small on a default install

The paragraph above holds only when your window starts after TIER began capturing cost. That is the case worth checking first, because on a default install it is usually false.

Every installation has a cost horizon: the time of the oldest cost event in its store. Outcomes do not respect it -- issues and PRs are read from your tracker and backfill freely, including work that closed long before you installed TIER. Cost exists only from the horizon forward.

So if since reaches back past the horizon, the leading edge is not a thin sliver of boundary-crossing work. It is every outcome in the uncovered stretch, divided by no cost at all -- and it pushes the number up. On one real multi-repo installation, measured on 2026-07-26, the API's default 90-day window gave a number about twice as high as the same installation reported once the window was narrowed to the trailing 30 days, close to the period cost capture actually covered. Most of the cost fell inside those 30 days; well under half of the outcomes did. The overstatement is in the flattering direction, which is the direction nobody questions.

This is not log retention, and no retention setting fixes it. TIER keeps the cost events it extracts, even after the provider deletes the session log they came from. Many installations hold events whose original logs no longer exist. The horizon is simply the oldest cost the store holds, so what you load moves it. Loading history moves it earlier: tierd ship stamps each event with the time of the original session (by default it reaches back 90 days). On tierd serve, the Codex rollout collector (--codex-rollout), the Opencode collector (--opencode) and the Muse Code collector (--muse) read every log already on disk on their first pass, and the Anthropic Admin and OpenAI Usage pollers write cost back to the first day of the previous month. Which Claude Code session files the watcher reads, and from where, is described in how-it-works.md: resuming an old session can move the horizon earlier, and the rest of your Claude Code history loads with tierd ship. Erasing a developer (DELETE /api/v1/developer/{id}) moves it later if their rows held the oldest events. A fresh install with nothing loaded has no horizon yet.

How to check it. A /api/v1/scores response carries the answer in its data_quality block:

Two cases emit no horizon fields at all, and neither means "covered": a store holding no cost yet (there is no horizon to compare against), and a failure of the horizon query itself (logged server-side). Absence is never a clean bill of health -- that is the whole reason the covered case emits an explicit false rather than staying quiet.

The dashboard renders the same signal as a banner, and tierd doctor reports it as a named cost horizon check. When the flag is true, set since to cost_coverage_safe_since before comparing anything to anything.

Note that this bound is about coverage in time only. A window that clears the horizon can still have most of its spend attached to no issue -- that is attribution coverage, a separate measurement with its own surface.


Practical guidance

Rule of thumb: check the cost horizon first, then do not trust a TIER number until the window is wide enough that most of the work started inside it has also closed inside it.

The horizon comes first because it is a hard bound, not a tendency: no amount of widening, waiting or settling improves a window that starts before your cost data does. Widening makes it worse.


This is a different problem from low attribution coverage

It is easy to conflate the windowing skew with TIER's other honesty caveat, attribution coverage. They are independent, and a window can be perfectly clean on one and badly skewed on the other:

A window can show 100% attribution coverage and still read a skewed, too-low TIER, because every dollar maps to an issue but half those issues have not closed. No single honesty surface below catches the timing skew on its own -- which is precisely why this document exists. (The cost-horizon signal is the one exception, and it catches only the leading-edge case: a window that starts before capture did.)


The honesty surfaces that DO exist -- and what each one catches

TIER ships several trust signals. Each is real and each catches a different distortion. Read them together; none is a substitute for choosing a sane window.

Surface Where What it catches What it does NOT catch
attributed_cost_share + the attribution-coverage banner data_quality.attributed_cost_share on GET /scores; dashboard banner (renderAttributionCoverage). Renders a red warning below a 50% coverage threshold. In team and division mode the share is not published and the banner says coverage is not shown, a calm note rather than a warning. Windows where a large share of spend maps to no issue. The timing skew -- attributed spend whose issue simply has not closed yet still counts as "attributed".
tierd doctor --min-attribution cmd/tierd/doctor.go. Attribution-coverage floor (default 0.5; "healthy" at >= 0.8). Below the floor the check is a hard FAIL (non-zero exit), not a warning. An install whose branch/label hygiene is too poor for TIER to attribute cost at all -- refuses to green-light it. Same blind spot: it measures coverage, not close-timing.
cost_per_point internal/scoring/engine.go (TotalCostUSD / weighted_points). The dollars-per-weighted-point unit; a higher TIER is a lower cost_per_point. No distortion that TIER itself misses. It is TIER in dollars (1000/TIER): the same number, read as money a CFO knows, and the unit to trend a team against its own past when the rubric and price-table versions match. The timing skew, and labeling habits -- it is the same ratio inverted, so a skewed window or generous size labels move cost_per_point exactly as much as they move TIER, in the opposite direction.
Zero-outcome tripwire (#189) tierd serve WARN log + tier_zero_outcome_tripwire gauge. The extreme end of this skew: cost accrued but zero outcomes in the window (broken webhook, or trunk team with no push capture). Partial skew -- a window with some but not-yet-all outcomes closed does not trip it.
window_predates_cost_capture + the cost-horizon banner data_quality on GET /scores and on both windows of GET /scores/compare; dashboard banner (renderCostHorizon); tierd doctor cost horizon check. Emits an explicit false when covered, so silence is never mistaken for clean. The leading-edge case: a window starting before this installation captured any cost, so outcomes are divided by cost that does not exist. Reads too HIGH. The trailing edge, and attribution. A window can clear the horizon and still be mostly unattributed or mostly unclosed.

Guiding principle. Correctness over marketing; dollar value, not token count. This page is TIER being candid about a real limitation of its own headline metric. A number that flatters a short window is not a number worth shipping.


Summary

flowchart TD
    A[You are reading a TIER number] --> H{Does the window start<br/>before your cost horizon?<br/>window_predates_cost_capture}
    H -->|"Yes -- or no horizon field at all"| I[Leading-edge over-count:<br/>outcomes divided by cost<br/>that was never captured]
    I --> J[Number reads artificially HIGH.<br/>Set since to cost_coverage_safe_since<br/>and read it again. Widening makes this worse.]
    H -->|No -- window is covered| B{Is the window recent<br/>or short?}
    B -->|Yes| C[Trailing-edge under-count:<br/>cost has landed, outcomes<br/>have not closed yet]
    C --> D[Number reads artificially LOW.<br/>Do not treat as a productivity signal.]
    B -->|"No -- wide and settled"| E{Has the number<br/>stopped drifting up<br/>as the window ages?}
    E -->|No| F[Still draining the artifact.<br/>Wait for in-flight work to close.]
    E -->|Yes| G[Interior-dominated window,<br/>inside the cost horizon.<br/>This number you can trust.]

    style J fill:#fde8e8,stroke:#c0392b
    style D fill:#fde8e8,stroke:#c0392b
    style F fill:#fef5e7,stroke:#e67e22
    style G fill:#eafaf1,stroke:#27ae60

See also: How It Works -- section 6, the formula and section 4, outcome attribution.