Metrics & analysis
Definitions in domain/metrics.md.
162. MetricDefinition#
Every metric card requires one. There is no variant without a definition.
Renders the formula with the actual input values substituted, plus cohort, period, source, and caveat:
Net Revenue Retention 104.6%
────────────────────────────────────────────────────────────────
(Starting MRR + Expansion − Contraction − Churn) ÷ Starting MRR
( 412,000 + 61,400 − 18,200 − 24,100 ) ÷ 412,000
Cohort: customers active on 1 Sep 2026
Period: 1–30 Sep 2026
Source: subscription ledger · excludes one-time fees
Note: new customers excluded by definition
"ARR" means four different things at four companies. This component makes sure the number here means one.
Definitions are versioned. When one changes, historical values computed under the old definition are marked. An unannounced definition change looks like a business change.
163. NonGaapReconciliation#
EBITDA, adjusted EBITDA, free cash flow, adjusted net income. Appears below net income in a separated block, never inside the P&L, with a line-by-line reconciliation and every adjustment named. "Adjusted EBITDA" with unnamed adjustments is not a number.
164. MrrBridge#
Opening MRR → new → expansion → contraction → churn → closing, as a waterfall. A single MRR number with a percentage change explains nothing; the bridge is the primary view.
Renders GRR and NRR together or neither, because NRR alone hides churn behind expansion. States that MRR is an operating metric that will not tie to the P&L.
165. BudgetVsActual#
Actual, budget, variance (absolute and relative) and forecast, by period and by dimension. Respects polarity: a favorable expense variance is under budget. Materiality thresholds are applied and displayed. Reforecast versions are tracked so you can see what was believed when.
The variance is a view, not a column. Actual, Δ and Δ% are the same figures asked three questions, and showing all of them at once turns a budget review into a wall of numbers nobody finishes. The comparison is declared, not positional: the actual column names budget as its base, so hiding the budget column to fit the screen doesn't silently re-point the variance at whatever column ends up to its left.
Two more things keep a budget review from going wrong.
The cut. A favorable total is often two departments canceling out. Engineering 1.6M over and sales 1.7M under reads at the entity line as 0.1M under, favorable, and says nothing about either. A report readable only at the entity level gets believed a quarter longer than it should. Members don't have to sum to the entity, since unallocated corporate spend sits outside them. A vintage that restates an entity budget says nothing about one department's share, so under a cut the member's own budget stands. The component doesn't invent an allocation nobody approved.
The vintage. The original plan and each reforecast are versions of one object, not "budget" plus "reforecasts." Model a reforecast as a different kind of thing and "what did we believe in February" stops having one answer. A vintage states only what it changed. The comparison base is chosen out loud in the toolbar, and the walk between versions sits under the grid, because a reforecast that quietly replaces the plan makes every past month look better than it was called at the time. September can beat the version in force and miss the one the board saw in November. Both readings are true, and a component that picked one silently would pick the flattering one.
166. ForecastTable#
Driver-based projections with scenarios. Projections render dashed on every chart and carry a "forecast" label on every figure. Actuals to date and forecast remaining look different within the same period.
The line between what happened and what's believed is drawn in three places.
Between periods. Everything after the last closed period is shaded, ruled with a dashed edge, set in the secondary tone, and headed by a column that says forecast under its label. Every projected figure sits beneath that word, not just near it.
Within a period. A month that has started but not finished is three columns, not one: what has posted, what's expected for the rest, and the landing they sum to. A part-elapsed month shown as one figure next to eight closed ones borrows their authority. That's the difference between "we're at 41.2 with nine days to go" and "we did 58.2," which nobody said and everybody quotes. The landing is computed from the other two, never supplied. A caller who provides all three will eventually provide three figures that don't add up, and the grid would render the contradiction without noticing.
Between scenarios. The scenario in force is named in the caption and the toolbar. The component won't rank scenarios or style one as the good case. An unlabeled forecast is indistinguishable from a claim, and a ranked one is a recommendation dressed as a forecast.
API change: ForecastRow.polarity is now required, no default (it previously fell back to standard).
167. CovenantMonitor#
Per-covenant: the contractual definition (lenders define EBITDA with specific add-backs), the computed value, the threshold, the headroom, and the next test date.
The number here may legitimately differ from the dashboard's EBITDA because the credit agreement defines it differently. That's not a bug, and the component says so on the card.
Headroom under 20% is --warning; a breach is --negative. The numeric headroom is always shown, never color alone.
168. KpiHealthStrip#
The dashboard's top row: liquidity, profitability, and cash metrics with polarity, comparison basis, and definitions. Metrics derived from unreconciled data are marked provisional. A KPI computed mid-close, before the bank is reconciled, is an estimate, and the label is what stops people quoting it externally.
174. RevenueTracker#
Revenue by stream, period by period, answering the four questions an operator asks: how much this period, versus what, from where, and how concentrated.
The summary tiles are computed from the same array as the grid cells. There's no second data path, because a header KPI that disagrees with the table under it loses the reader fast. For the same reason, total revenue is summed here, not accepted as a prop.
Month-on-month and year-on-year, together. Month-on-month on a seasonal business is noise; year-on-year on a fast-growing one is stale.
The run-rate tile is labeled as a projection from one period. A month times twelve isn't ARR: it ignores churn, seasonality and contract length. The tile says so before the figure travels into a deck.
Any stream over a third of revenue is flagged in words. Concentration is a fact about the business's risk, not about the month.
175. ExpenseTracker#
Spend by category on three bases (actual, budget, variance), because they're the same categories asked three questions, and nobody reads a nine-column grid past the third column.
Every row carries a polarity, and the component derives it. More spend isn't good news: under budget is favorable, over budget isn't. A grid that paints a rising cost green tells the reader the opposite of the truth.
Polarity describes the figure as stored, not the concept behind it, and that's the easiest thing
here to get backwards. This system holds costs as negatives, so a heavier month moves down and
the row wants standard. A model that holds the same costs as positive magnitudes wants
inverted. ExpenseTracker takes a sign and works it out, so the caller doesn't have to reason
about a storage convention to get the colors right.
Committed-but-unbilled sits beside the total, never inside it. A purchase order isn't an expense. Accruing it silently is a cutoff error, and hiding it is how a category with six figures of open POs looks under budget until the invoices land.
The movers list ranks by amount, not percentage. A line that doubled from 400 to 800 isn't the story of a month where another went from 180,000 to 240,000.
176. DriverModel#
A forecast's inputs and the outputs they produce, in one grid with the boundary between them visible. It keeps three things apart: actual from projection, input from output, and assumption from arithmetic.
Inputs are typed; outputs carry the ƒ mark and their formula. If you can't tell which is which, nobody can find the assumption that moved the answer.
Only projected inputs are editable. An actual is what the ledger posted, and a model that lets you type over it will eventually explain a month that never happened.
Named assumptions sit under the grid in words ("new logos hold at 14 a month"), because that's the sentence someone can disagree with, and disagreement is the point of a forecast review. A growth rate buried three rows into a projected column isn't.
API change: ModelDriver.polarity is now required, no default — computed outputs included (it previously fell back to standard).
177. ScenarioCompare#
Scenarios in columns, measures down, every column measured against the base. The comparison is declared, not positional, so it survives the reader reordering or hiding columns.
It won't rank the scenarios. No best-case styling, no sorting by outcome. A scenario is a set of assumptions, and calling the biggest number the good one turns a forecast review into a negotiation over which column the board sees.
It won't hide a breach. A measure with a limit (a covenant floor, a minimum cash balance) names, in words under the grid, the scenarios that cross it. A downside that breaches a covenant is the only thing on the page that matters, so it isn't left as a cell someone has to notice.
API change: ScenarioMeasure.polarity is now required, no default (it previously fell back to standard).
178. PlByDimension#
The P&L cut by department, entity, product or region instead of by period. Same lines, different question, and it's the question the department head came to ask.
Total is a column computed from the members beside it plus anything unallocated, never supplied separately. A total that doesn't equal its parts loses the room.
Unallocated is its own column, not a rounding and not an allocation. Some corporate costs belong to no department, and spreading them on a rule nobody agreed to gives the department head a P&L they can't argue with.
API change: PlDimensionLine.polarity is now required, no default — read it from the account type with polarityForAccountType (it previously fell back to standard).
187. RatioAnalysis#
Liquidity, leverage, coverage, efficiency and profitability ratios in one table, grouped in that order. Each row carries the ratio's name, its formula in words, and the two figures that produced it, so the value can be checked without opening the ledger.
The value is computed from its inputs, never passed in beside them. A ratio whose own formula line contradicts it is worse than no ratio.
- Kind is explicit:
times(1.84×),percent(34.2%) ordays(47 days). Adaysratio takes the length of the period its denominator covers — DSO on a quarter's revenue is × 91, not × 365 — so the component never guesses a year. - Polarity is required, no default. A current ratio rising is good; DSO or debt to equity rising is not.
- Change is in the ratio's own unit. A percent ratio moves in points, not percent of itself. The change is computed on the displayed precision, so it never shows a move where both readings print the same.
- Three gaps, three sentences, none of them zero: an input missing ("Input not available"), a zero denominator ("Not meaningful" — never Infinity), and inputs in two currencies ("Inputs in two currencies" — a ratio across currencies needs a rate nobody passed).
- Thresholds name their source — the credit agreement section, the board policy. A breach is
stated in words with an icon; the
--negativecolor repeats it, never replaces it. - The basis is a required prop: period, entity, currency, accounting basis. Ratios computed mid-close inherit the provisional state of their inputs; pass it in the basis line.
Not a covenant monitor: CovenantMonitor carries the contractual definition and the test date.
Use this for analysis and the covenant card for compliance.