mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
fix(calculator): honest burn-multiple and runway labels, surface model assumptions (#274)
* fix(calculator): honest burn-multiple and runway labels, surface model assumptions - Burn Multiple now reports Graham's metric (net burn / net new ARR); the net burn / MRR ratio is reported separately as Burn to Revenue. The qualifier (efficient/healthy/warning/critical) is derived from the real burn multiple, so DEAD verdicts no longer print 'efficient'. - ALIVE verdicts no longer print a misleading 'Runway: 120 months' (projection cap); output now shows 'Projected cash-out' with 'none within the 10-year projection' when the company never runs out. - Model assumptions (fixed/variable burn split, variable burn ratio, growth decay, projection cap, safety buffer) are now surfaced in the human report and in JSON model_assumptions. - SKILL.md: fix dead paulgraham.com/default.html source URL to aord.html; update output-field docs and examples to real model output. - Add regression tests (tests/integration/test_default_alive.py). Fixes #272 Fixes #273 * docs(calculator): add When Not to Use boundary (validator requirement)
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"""Regression tests for yc-default-alive-calculator output labeling.
|
||||
|
||||
Covers issues #272 (Burn Multiple mislabel) and #273 (runway display and
|
||||
unsurfaced model assumptions): the canonical Burn Multiple is Graham's
|
||||
net burn / net new ARR, the MRR ratio has an honest name, ALIVE verdicts
|
||||
no longer print a misleading projection-cap runway, and model assumptions
|
||||
are surfaced in the output.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[2] / "yc-default-alive-calculator" / "scripts" / "default-alive.py"
|
||||
|
||||
_spec = importlib.util.spec_from_file_location("default_alive", SCRIPT)
|
||||
assert _spec is not None and _spec.loader is not None
|
||||
MOD = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(MOD)
|
||||
|
||||
|
||||
def calc(revenue, burn, cash, growth):
|
||||
return MOD.project_trajectory(
|
||||
monthly_revenue=revenue,
|
||||
monthly_burn=burn,
|
||||
cash_on_hand=cash,
|
||||
monthly_growth_pct=growth,
|
||||
growth_decay_pct=0.5,
|
||||
)
|
||||
|
||||
|
||||
def test_lean_saas_alive_diagnostics():
|
||||
d = calc(12000, 18000, 220000, 7)
|
||||
diag = d["diagnostics"]
|
||||
assert d["verdict"] == "ALIVE"
|
||||
assert diag["burn_multiple"] == 0.6 # Graham: net burn / net new ARR
|
||||
assert diag["burn_to_revenue_ratio"] == 0.5 # net burn / MRR
|
||||
assert diag["projected_cashout_month"] is None
|
||||
assert "none within the 10-year projection" in d["explanation"]
|
||||
assert "Runway:" not in d["explanation"]
|
||||
|
||||
|
||||
def test_dead_verdict_labels_are_honest():
|
||||
d = calc(12000, 26000, 220000, 3)
|
||||
diag = d["diagnostics"]
|
||||
assert d["verdict"] == "DEAD"
|
||||
assert diag["burn_multiple"] == 3.24
|
||||
assert diag["burn_to_revenue_ratio"] == 1.17
|
||||
assert diag["projected_cashout_month"] == 18
|
||||
assert "critical" in d["explanation"]
|
||||
assert "efficient" not in d["explanation"]
|
||||
assert "net burn / net new ARR" in d["explanation"]
|
||||
|
||||
|
||||
def test_model_assumptions_surfaced():
|
||||
d = calc(12000, 26000, 220000, 3)
|
||||
a = d["model_assumptions"]
|
||||
assert a["fixed_burn_pct"] == 70.0
|
||||
assert a["variable_burn_ratio"] == 0.65
|
||||
assert a["growth_decay_pct"] == 0.5
|
||||
assert a["projection_cap_months"] == 120
|
||||
assert a["safety_buffer_months"] == 3
|
||||
|
||||
|
||||
def test_pre_revenue_has_no_burn_multiple():
|
||||
d = calc(0, 80000, 400000, 0)
|
||||
assert d["verdict"] == "DEAD"
|
||||
assert d["diagnostics"]["burn_multiple"] is None
|
||||
assert d["diagnostics"]["burn_to_revenue_ratio"] is None
|
||||
@@ -13,7 +13,7 @@ metadata:
|
||||
spec-version: '1.0'
|
||||
tags: startup-finance, runway-analysis, ycombinator, paul-graham, fundraising, financial-modeling,
|
||||
default-alive, burn-rate, startup-metrics
|
||||
sources: https://paulgraham.com/default.html, https://paulgraham.com/die.html, https://www.ycombinator.com/about
|
||||
sources: https://paulgraham.com/aord.html, https://paulgraham.com/die.html, https://www.ycombinator.com/about
|
||||
skills: yc-weekly-growth-compass
|
||||
requires-toolsets: terminal
|
||||
---
|
||||
@@ -35,6 +35,14 @@ A startup is "default alive" if its current revenue trajectory will reach profit
|
||||
| "How long until we break even?" | Trajectory check |
|
||||
| "Default alive/dead analysis" | Explicit framework request |
|
||||
|
||||
## When Not to Use
|
||||
|
||||
- **Detailed financial planning.** This is a diagnostic, not a budget. It projects one growth curve with fixed assumptions; it does not model hiring plans, contract timing, seasonality, or capital expenditures. Use a proper financial model for those.
|
||||
- **Fundraising valuation.** The verdict tells you whether raising money is existential; it does not compute a valuation, cap table, or round size.
|
||||
- **Cost structures the default assumptions don't describe.** The model splits burn into fixed and variable components (default 70/30). Hardware, R&D, or manufacturing companies with very different cost structures will get a misleading projection.
|
||||
- **Unknown or unstable growth.** The model assumes a steady monthly growth rate with a small decay. A company in launch mode or product-market-fit search does not produce a meaningful projection.
|
||||
- **As the only input to a board decision.** Treat the verdict as a conversation starter with your CFO or accountant, not as financial advice.
|
||||
|
||||
## How to Use
|
||||
|
||||
### Quick Answer (No Script)
|
||||
@@ -64,7 +72,7 @@ python scripts/default-alive.py \
|
||||
--monthly-growth 8
|
||||
```
|
||||
|
||||
Output shows: runway (months), burn multiple, months to breakeven extrapolated, default alive/dead verdict, and the key levers available.
|
||||
Output shows: the verdict, burn multiple (net burn ÷ net new ARR), burn-to-revenue ratio (net burn ÷ MRR), projected cash-out month, months to breakeven, cash gap coverage at current spend, the model's assumptions, and the key levers available.
|
||||
|
||||
#### Required inputs
|
||||
|
||||
@@ -87,12 +95,15 @@ Output shows: runway (months), burn multiple, months to breakeven extrapolated,
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `runway_months` | Months until cash runs out (at current burn) |
|
||||
| `burn_multiple` | Net burn ÷ net new ARR |
|
||||
| `projected_cashout_month` | Month cash runs out under the model; `null` if never within the 10-year projection |
|
||||
| `burn_multiple` | Graham's burn multiple: net burn ÷ net new ARR |
|
||||
| `burn_to_revenue_ratio` | Net burn ÷ MRR (secondary diagnostic, not Graham's metric) |
|
||||
| `months_to_breakeven` | Months until revenue ≥ expenses (extrapolated) |
|
||||
| `default_verdict` | `ALIVE`, `DEAD`, or `MARGINAL` |
|
||||
| `revenue_at_breakeven` | Projected revenue when/if breakeven reached |
|
||||
| `gap_to_breakeven` | Monthly shortfall remaining |
|
||||
| `months_of_gap_remaining` | Static runway at current spend: cash ÷ monthly gap |
|
||||
| `model_assumptions` | `fixed_burn_pct`, `variable_burn_ratio`, `growth_decay_pct`, `projection_cap_months`, `safety_buffer_months` |
|
||||
| `levers` | What can change the outcome (increase price, cut costs, etc.) |
|
||||
|
||||
## Methodology
|
||||
@@ -147,48 +158,47 @@ When the verdict is DEAD or MARGINAL, evaluate these levers (in rough order of i
|
||||
|
||||
## Examples
|
||||
|
||||
### YC Typical Profile (Default Alive)
|
||||
### YC Typical Profile (Default Dead under the model)
|
||||
|
||||
```bash
|
||||
python scripts/default-alive.py \
|
||||
python3 scripts/default-alive.py \
|
||||
--monthly-revenue 30000 \
|
||||
--monthly-burn 75000 \
|
||||
--cash-on-hand 500000 \
|
||||
--monthly-growth 10
|
||||
```
|
||||
|
||||
- Runway: ~11 months
|
||||
- Burn multiple: 1.5x
|
||||
- Breakeven: ~14 months (3 months short on runway → MARGINAL)
|
||||
- Verdict: **MARGINAL** — needs faster growth, cost cuts, or funding
|
||||
- Projected cash-out: month 14
|
||||
- Burn multiple: 1.25x (net burn / net new ARR)
|
||||
- Breakeven would require 22 months, after cash runs out
|
||||
- Verdict: **DEAD** — needs faster growth, cost cuts, or funding
|
||||
|
||||
### Pre-Revenue Startup (Default Dead)
|
||||
|
||||
```bash
|
||||
python scripts/default-alive.py \
|
||||
python3 scripts/default-alive.py \
|
||||
--monthly-revenue 0 \
|
||||
--monthly-burn 80000 \
|
||||
--cash-on-hand 400000 \
|
||||
--monthly-growth 0
|
||||
```
|
||||
|
||||
- Runway: 5 months
|
||||
- Projected cash-out: month 8 (the model burns only the fixed 70% of burn at zero revenue)
|
||||
- Burn multiple: undefined (no revenue)
|
||||
- Verdict: **DEAD** — fundraising is existential
|
||||
|
||||
### Capital-Efficient SaaS (Default Alive)
|
||||
|
||||
```bash
|
||||
python scripts/default-alive.py \
|
||||
python3 scripts/default-alive.py \
|
||||
--monthly-revenue 150000 \
|
||||
--monthly-burn 180000 \
|
||||
--cash-on-hand 2000000 \
|
||||
--monthly-growth 7
|
||||
```
|
||||
|
||||
- Runway: ~66 months (effectively infinite)
|
||||
- Burn multiple: 0.2x
|
||||
- Breakeven: ~3 months
|
||||
- Projected cash-out: none within the 10-year projection
|
||||
- Burn multiple: 0.24x (net burn / net new ARR)
|
||||
- Verdict: **ALIVE**
|
||||
|
||||
## References
|
||||
|
||||
@@ -16,9 +16,6 @@ See SKILL.md for full documentation and methodology.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core calculation
|
||||
@@ -62,8 +59,8 @@ def project_trajectory(
|
||||
current_growth = growth_rate
|
||||
|
||||
trajectory = []
|
||||
breakeven_month: Optional[int] = None
|
||||
cashout_month: Optional[int] = None
|
||||
breakeven_month: int | None = None
|
||||
cashout_month: int | None = None
|
||||
|
||||
for month in range(1, MAX_PROJECTION_MONTHS + 1):
|
||||
# Revenue grows (or decays) at current growth rate
|
||||
@@ -117,29 +114,27 @@ def project_trajectory(
|
||||
# Diagnostics
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
runway_months = cashout_month if cashout_month else MAX_PROJECTION_MONTHS
|
||||
runway_months = cashout_month if cashout_month else None # None = never runs out within projection
|
||||
net_burn = monthly_burn - monthly_revenue
|
||||
burn_multiple = round(net_burn / max(monthly_revenue, 1), 2) if monthly_revenue > 0 else None
|
||||
|
||||
# Net new ARR (monthly growth in ARR terms)
|
||||
last_month_revenue = monthly_revenue
|
||||
# Secondary diagnostic: net burn / MRR. This is NOT Graham's burn multiple.
|
||||
burn_to_revenue_ratio = round(net_burn / max(monthly_revenue, 1), 2) if monthly_revenue > 0 else None
|
||||
|
||||
# Graham's burn multiple: net burn / net new ARR (annualized new recurring revenue added this month)
|
||||
current_arr = monthly_revenue * 12
|
||||
if monthly_growth_pct > 0 and monthly_revenue > 0:
|
||||
next_arr = (monthly_revenue * (1 + monthly_growth_pct / 100)) * 12
|
||||
net_new_arr = next_arr - current_arr
|
||||
arr_burn_multiple = round(net_burn / max(net_new_arr, 1), 2) if net_new_arr > 0 else None
|
||||
burn_multiple = round(net_burn / max(net_new_arr, 1), 2) if net_new_arr > 0 else None
|
||||
else:
|
||||
net_new_arr = 0
|
||||
arr_burn_multiple = None
|
||||
burn_multiple = None
|
||||
|
||||
# Verdict
|
||||
if breakeven_month is not None and cashout_month is not None:
|
||||
if breakeven_month < cashout_month:
|
||||
post_breakeven_runway = cashout_month - breakeven_month
|
||||
if post_breakeven_runway >= SAFETY_BUFFER_MONTHS:
|
||||
verdict = "ALIVE"
|
||||
else:
|
||||
verdict = "MARGINAL"
|
||||
verdict = "ALIVE" if post_breakeven_runway >= SAFETY_BUFFER_MONTHS else "MARGINAL"
|
||||
else:
|
||||
verdict = "DEAD"
|
||||
elif breakeven_month is not None and cashout_month is None:
|
||||
@@ -194,20 +189,27 @@ def project_trajectory(
|
||||
"diagnostics": {
|
||||
"net_monthly_burn": round(net_burn, 2),
|
||||
"burn_multiple": burn_multiple,
|
||||
"arr_burn_multiple": arr_burn_multiple,
|
||||
"burn_to_revenue_ratio": burn_to_revenue_ratio,
|
||||
"current_arr": round(current_arr, 2),
|
||||
"net_new_arr": round(net_new_arr, 2),
|
||||
"runway_months": runway_months,
|
||||
"projected_cashout_month": runway_months,
|
||||
"months_to_breakeven": breakeven_month,
|
||||
"cashout_month": cashout_month,
|
||||
"gap_to_breakeven": round(gap_to_breakeven, 2),
|
||||
"months_of_gap_remaining": months_of_gap,
|
||||
"months_projected": len(trajectory),
|
||||
},
|
||||
"model_assumptions": {
|
||||
"fixed_burn_pct": fixed_burn_pct,
|
||||
"variable_burn_ratio": round(variable_burn_ratio, 4),
|
||||
"growth_decay_pct": growth_decay_pct,
|
||||
"projection_cap_months": MAX_PROJECTION_MONTHS,
|
||||
"safety_buffer_months": SAFETY_BUFFER_MONTHS,
|
||||
},
|
||||
"verdict": verdict,
|
||||
"explanation": _generate_explanation(
|
||||
verdict, runway_months, breakeven_month, cashout_month,
|
||||
burn_multiple, arr_burn_multiple, monthly_revenue, monthly_burn,
|
||||
burn_multiple, burn_to_revenue_ratio, monthly_revenue, monthly_burn,
|
||||
cash_on_hand,
|
||||
),
|
||||
"levers": levers,
|
||||
@@ -219,11 +221,11 @@ def project_trajectory(
|
||||
|
||||
def _generate_explanation(
|
||||
verdict: str,
|
||||
runway_months: int,
|
||||
breakeven_month: Optional[int],
|
||||
cashout_month: Optional[int],
|
||||
burn_multiple: Optional[float],
|
||||
arr_burn_multiple: Optional[float],
|
||||
runway_months: int | None,
|
||||
breakeven_month: int | None,
|
||||
cashout_month: int | None,
|
||||
burn_multiple: float | None,
|
||||
burn_to_revenue_ratio: float | None,
|
||||
monthly_revenue: float,
|
||||
monthly_burn: float,
|
||||
cash_on_hand: float,
|
||||
@@ -232,27 +234,31 @@ def _generate_explanation(
|
||||
lines = []
|
||||
|
||||
if verdict == "ALIVE":
|
||||
lines.append(f"✅ DEFAULT ALIVE — You will reach profitability before running out of cash.")
|
||||
lines.append("✅ DEFAULT ALIVE — You will reach profitability before running out of cash.")
|
||||
if breakeven_month:
|
||||
lines.append(f" Breakeven projected at month {breakeven_month}.")
|
||||
elif verdict == "DEAD":
|
||||
lines.append(f"❌ DEFAULT DEAD — You will run out of cash before reaching profitability.")
|
||||
lines.append("❌ DEFAULT DEAD — You will run out of cash before reaching profitability.")
|
||||
if cashout_month:
|
||||
lines.append(f" Cash runs out at month {cashout_month}.")
|
||||
if breakeven_month:
|
||||
lines.append(f" Breakeven would require {breakeven_month} months — too late.")
|
||||
else:
|
||||
lines.append(f"⚠️ MARGINAL — Breakeven is possible but dangerously close to cash-out.")
|
||||
lines.append("⚠️ MARGINAL — Breakeven is possible but dangerously close to cash-out.")
|
||||
if breakeven_month and cashout_month:
|
||||
lines.append(f" Breakeven at month {breakeven_month}, cash-out at month {cashout_month}.")
|
||||
lines.append(f" Only {cashout_month - breakeven_month} months of post-breakeven buffer (need {SAFETY_BUFFER_MONTHS}+).")
|
||||
|
||||
lines.append("")
|
||||
if burn_multiple is not None:
|
||||
lines.append(f" Burn Multiple: {burn_multiple}x ({'efficient' if burn_multiple < 2 else 'inefficient' if burn_multiple < 3 else 'critical'})")
|
||||
if arr_burn_multiple is not None:
|
||||
lines.append(f" ARR Burn Multiple: {arr_burn_multiple}x (net burn / net new ARR)")
|
||||
lines.append(f" Runway: {runway_months} months")
|
||||
qualifier = "efficient" if burn_multiple < 1 else "healthy" if burn_multiple < 2 else "warning" if burn_multiple < 3 else "critical"
|
||||
lines.append(f" Burn Multiple: {burn_multiple}x ({qualifier}, net burn / net new ARR)")
|
||||
if burn_to_revenue_ratio is not None:
|
||||
lines.append(f" Burn to Revenue: {burn_to_revenue_ratio}x (net burn / MRR)")
|
||||
if runway_months is None:
|
||||
lines.append(" Projected cash-out: none within the 10-year projection")
|
||||
else:
|
||||
lines.append(f" Projected cash-out (model): month {runway_months}")
|
||||
lines.append(f" Monthly gap: ${monthly_burn - monthly_revenue:,.0f}")
|
||||
lines.append(f" Cash: ${cash_on_hand:,.0f}")
|
||||
|
||||
@@ -295,18 +301,30 @@ def format_output(result: dict, verbose: bool) -> str:
|
||||
lines.append("── Diagnostics ─────────────────────────────────────────")
|
||||
lines.append(f" Net monthly burn: ${diag['net_monthly_burn']:>8,.0f}")
|
||||
if diag["burn_multiple"] is not None:
|
||||
lines.append(f" Burn Multiple: {diag['burn_multiple']:>8.2f}x")
|
||||
if diag["arr_burn_multiple"] is not None:
|
||||
lines.append(f" ARR Burn Multiple: {diag['arr_burn_multiple']:>8.2f}x (net burn ÷ net new ARR)")
|
||||
lines.append(f" Burn Multiple: {diag['burn_multiple']:>8.2f}x (net burn ÷ net new ARR)")
|
||||
if diag["burn_to_revenue_ratio"] is not None:
|
||||
lines.append(f" Burn to Revenue: {diag['burn_to_revenue_ratio']:>8.2f}x (net burn ÷ MRR)")
|
||||
lines.append(f" Current ARR: ${diag['current_arr']:>8,.0f}")
|
||||
if diag["net_new_arr"]:
|
||||
lines.append(f" Net new ARR/month: ${diag['net_new_arr']:>8,.0f}")
|
||||
lines.append(f" Runway: {diag['runway_months']:>8} months")
|
||||
cashout = diag["projected_cashout_month"]
|
||||
cashout_label = "none within 10y projection" if cashout is None else f"month {cashout}"
|
||||
lines.append(f" Projected cash-out: {cashout_label:>8}")
|
||||
lines.append(f" Months to breakeven: {diag['months_to_breakeven'] or 'never':>8}")
|
||||
if diag["months_of_gap_remaining"]:
|
||||
lines.append(f" Cash gap coverage: {diag['months_of_gap_remaining']:>8.1f} months at current spend")
|
||||
lines.append("")
|
||||
|
||||
# Model assumptions
|
||||
if "model_assumptions" in result:
|
||||
a = result["model_assumptions"]
|
||||
lines.append("── Model Assumptions ──────────────────────────────")
|
||||
lines.append(f" Fixed burn: {a['fixed_burn_pct']:.0f}% of burn fixed; {100 - a['fixed_burn_pct']:.0f}% scales with revenue")
|
||||
lines.append(f" Variable burn ratio: {a['variable_burn_ratio']:.2f} per $1 of revenue")
|
||||
lines.append(f" Growth decay: {a['growth_decay_pct']:.1f}% per month")
|
||||
lines.append(f" Projection cap: {a['projection_cap_months']} months | Safety buffer: {a['safety_buffer_months']} months")
|
||||
lines.append("")
|
||||
|
||||
# Levers
|
||||
if result["levers"]:
|
||||
lines.append("── Levers ───────────────────────────────────────────────")
|
||||
|
||||
Reference in New Issue
Block a user