Calculation catalog › Cash flow and summary
Projection summary average pre retirement savings rate pct
Average share of pre-retirement earnings saved across working years.
Field: averagePreRetirementSavingsRatePct Unit: percent Basis: no dollar basis Computed by: Engine Status: Complete chain
Appears in: results-headline (pre-retirement savings rate); report (headline-results block average-pre-retirement-savings-rate-pct and ReportPage KPI); mcp (run_projection.summary.averagePreRetirementSavingsRatePct); mcp (compare_scenarios.a.averagePreRetirementSavingsRatePct); mcp (compare_scenarios.b.averagePreRetirementSavingsRatePct)
Calculation records
-
Projection summary average pre retirement savings rate Composition record, justified by derivation
Purpose: Average the published working-year savings rates ahead of retirement.
Statement: projection/compare.ts#summarizeProjection computes the unweighted arithmetic mean, in percentage points, of the published savingsRates[].ratePct values over the years strictly before the household's later target retirement year (the household's later retirement, household-later-retirement: a retirement age gives birth year plus that age, or the first year without the person's wages when a wage stream's end age keeps paying past it, a person with none retires in the first year without their wages, else in the start year, and a person who never retires in the plan is left out; projection-summary-fi-spending-base), or over every year when nobody retires in the plan (the independent review's N3), with every qualifying year counted once regardless of income and 0 when no year qualifies.
Formula:
r_bar = (sum of r_i over years y < retirementYear) / n, over every year when retirementYear is null, and 0 when n = 0; retirementYear = householdRetirement(plan, startYear).retirement?.year ?? null-
retirementYear: The household's later retirement year (household-later-retirement), null when nobody retires in the plan (unit: calendar year) (domain: integer or null) -
r_i: One qualifying year's published savings rate (unit: percentage points) (domain: 0..100) -
n: Count of qualifying working years (unit: 1) (domain: integer >= 0)
Timing: years strictly before the target retirement year
Rounding: none stated
Limits:
- Unweighted: every represented working year counts once, so it is not a dollar-weighted savings ratio. Dividing by calendar boundaries rather than represented years, or reading the terminal working-year rate, is a different quantity. The target retirement year itself is not pre-retirement. An empty qualifying set publishes exactly 0 rather than an absent value.
Provenance: derived by
codex, implemented byclaude.Independent review:
cursorGates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass
Worksheet: projection-summary-average-pre-retirement-savings-rate.md Mutation receipt: projection-summary-average-pre-retirement-savings-rate.mutation.md
Evidence tests: compareSummary.evidence.test.ts
Implemented in: compare.ts
Is the number of: projection-summary-average-pre-retirement-savings-rate-pct
-
Records that feed this number
-
The household's later retirement, one rule for FI and the funded ratio Model record, justified by derivation
Purpose: The year the household stops earning, whose it is and which rule gave it, never whoever is listed first.
Statement: projection/householdRetirement.ts#personRetirement gives each person's retirement year, the first year without their work, and its rule. A wage stream of theirs with gross pay above 0 pays through birthYear + stopAge - 1, stopAge being its end age, else the retirement age (stopAge = endAge ?? retirementAge, as projection/internal/wageIncomeStreams.ts pays), and with neither through their last year alive at the planning age, never after it. With a retirement age the person retires in the later of the ISO birth year plus that age (retirementAge) and the year after their last wage year, when a stream's end age keeps paying past the retirement age (wagesPastRetirementAge); with none, in the year after their last wage year when that year is the start year or later (wagesEnd), otherwise in the start year (startYear). A person retires in the plan only when alive in max(year, startYear): one whose wages run through their last year alive works through the plan, a retirement age past the planning age is never reached, and a planning age ended before the start retires no one. projection/householdRetirement.ts#householdRetirement leaves those people out (notRetiring) and takes the latest of the rest, a tie going to the older person and then the smaller id by ordinal comparison, so list order never decides it; with nobody retiring in the plan there is no household retirement, and no FI figure, Coast-FIRE figure or funded ratio is priced (decision D-PEOPLE-ORDER; the independent review's M4 and N3). The FI figures (projection/compare.ts#summarizeProjection) and the funded ratio (ladder/fundedRatio.ts#fundedRatioStart) both read it, and projection/householdRetirement.ts#householdRetirementClause and #notRetiringClause word it for every page: they name the person, say which rule applied, and name anyone who works through the plan. Sam, born 1964 with no retirement age, planning age 95 and wages with no end age, works through the plan, so with Alex, born 1962 retiring at 66, the household retires in Alex's 2028; Gus, born 1966 with a retirement age of 65 and wages to age 75, is paid through 2040 and retires in 2041 (round-one review of #765). Units: calendar year. Rounding: none.
Formula:
year(p) = retirementAge !== null ? (lastWage(p) !== null ? max(birthYear + retirementAge, lastWage(p) + 1) : birthYear + retirementAge) : lastWage(p) !== null and lastWage(p) >= startYear ? lastWage(p) + 1 : startYear, lastWage(p) = max over p's paying wage streams of min(stopAge !== null ? birthYear + stopAge - 1 : birthYear + planningAge, birthYear + planningAge) with stopAge = endAge ?? retirementAge, and null when p has no paying wage stream; in(p) = max(year(p), startYear) <= birthYear + planningAge; household = argmax year over the people with in(p), ties to the earlier dob then the smaller id, none when nobody has in(p)-
year(p): A person's retirement year, the first year without their work (unit: calendar year) (domain: integer) -
lastWage(p): The last year one of their wage streams pays, null when none pays (unit: calendar year) (domain: integer or null) -
in(p): Whether the person is alive in the year their retirement would be priced (unit: boolean) (domain: true or false)
Timing: once per reading, from the plan (not a Monte Carlo path's sampled deaths)
Rounding: none
Limits:
- A convention, not a statute: a retirement age is the first year without wages unless a wage stream's end age runs past it, and then the wages decide; a person who never stops earning is said to work through the plan, not given a year
- A person who retires after the other person's planning age has ended still sets the household's retirement: the priced year is one they are alive in, whether or not their partner is
- Wages are the plan's only earned income; the rule reads the plan's wage streams and planning ages, not a Monte Carlo path's sampled death
Provenance: derived by
claude, implemented byclaude.Independent review:
codexGates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass
Worksheet: household-later-retirement.md Mutation receipt: household-later-retirement.mutation.md
Evidence tests: householdRetirement.evidence.test.ts
Implemented in: householdRetirement.ts
Feeds: projection-summary-fi-number, projection-summary-fi-age, projection-summary-coast-fire-number, projection-summary-fi-year, projection-summary-average-pre-retirement-savings-rate-pct, funded-ratio-result-essential-spending-pv, funded-ratio-result-guaranteed-income-pv, funded-ratio-result-funded-ratio-pct, funded-ratio-result-unfunded-pv
-
-
Which year and which outflows the FI figures price Composition record, justified by derivation
Purpose: The household's later retirement year, and that year's outflows without a Roth conversion's one-off tax.
Statement: projection/compare.ts#summarizeProjection prices the FI figures on the calendar year max(startYear, retirementYear), where retirementYear is the household's later retirement (household-later-retirement: a retirement age gives birth year plus that age, or the first year without the person's wages when a wage stream's end age keeps paying past it; a person with none retires in the first year without their wages, else in the start year; a person who never retires in the plan is left out and named in fiBasis.notRetiring; a tie goes to the older person, then the smaller id; decision D-PEOPLE-ORDER, rule R4), so list order never decides it; fiBasis.personId names that person, fiAge is their age, and fiBasis.retirementRule says which rule gave the year. When nobody retires in the plan, no FI figure is priced: fiNumber, fiYear, fiAge and coastFireNumber are null and fiBasis.spendingSource is noRetirementInPlan (the independent review's N3). The priced outflows are that year's published expenses.total + tax + penalties, except when the plan converts to Roth in any year (rothConversion > 0 or a named conversion request in some year): then they are the same year's figures from the plan run with its Roth conversions removed, so neither a conversion's one-off tax nor the costs it causes later (the IRMAA lookback, a drained taxable account) is priced as spending and converting more can never lower the figure (withoutRothConversions: the strategy set to none, named and legacy-aggregate conversion requests and the withdrawals they name as tax funding dropped), which the caller supplies as SummarizeProjectionOptions.conversionFreeRun and which runs at most once (decision D-FI-CONVERSION-TAX). fiBasis.spendingSource publishes projection, conversionFreeProjection, conversionTaxIncluded (the plan converts and no conversion-free run was supplied), baseAnnual (an empty ledger) or noRetirementInPlan. Pat (1980, retiring at 50) and Robin (1983, retiring at 49) price 2032, Robin's; with 80,000 of 2032 expenses, 30,000 of tax on a 100,000 conversion and 8,000 of tax without it, the FI number at 3 percent inflation and a 4 percent rate is 88,000 / 1.03^6 / 0.04 = 1,842,465.36, not 2,303,081.71. Units: calendar year; start-year USD. Rounding: none.
Formula:
retirementYear = householdRetirement(plan, startYear).retirement.year (no FI figure when null); y = max(startYear, retirementYear); base = (the conversion-free run's row y if any row converts, else row y): expenses.total + tax + penalties-
retirementYear: The household's later retirement year (unit: calendar year) (domain: integer) -
y: The priced year, else the first ledger row when absent (unit: calendar year) (domain: integer) -
base: That year's outflows without a conversion's tax (unit: nominal USD) (domain: nonnegative)
Timing: once per summary; the conversion-free run only when the plan converts
Rounding: none
Limits:
- The conversion-free run is a whole projection without conversions, so the priced year also differs by what earlier years' conversions did to the balances and withdrawals, not only by that year's conversion tax; it is the year the household would have had had it never converted
- The conversion-free choice is a required argument (the independent review's M3): the planner's projection, compareRothConversion and compareScenarios pass the run; Monte Carlo paths, the optimizer and decision candidates, the SWR, relocation, care, survivor and Social Security comparisons, the scenario plan comparison and the app's case runners pass null and show no FI figure, and a converting plan then publishes conversionTaxIncluded. RetireGolden-MCP chose in 0.11.0, and 0.12.0 keeps it: the summaries it publishes, run_projection's summary and compare_scenarios' a and b, pass the run, so their FI figures are the app's; batch_evaluate and solve_max_spending pass null for the summaries they only read (lifetime tax, ending balances, the after-tax estate), and no such summary reaches a response whole
- The later retirement is a household convention, the year the last earner stops; the program states it and the page names the person
Provenance: derived by
claude, implemented byclaude.Independent review:
codexGates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass
Worksheet: projection-summary-fi-spending-base.md Mutation receipt: projection-summary-fi-spending-base.mutation.md
Evidence tests: compareSummary.evidence.test.ts
Implemented in: compare.ts; householdRetirement.ts
Feeds: projection-summary-fi-number, projection-summary-fi-age, projection-summary-coast-fire-number, projection-summary-fi-year, projection-summary-average-pre-retirement-savings-rate-pct
-
-
Savings rate by year Formula record, justified by derivation
Purpose: What share of each year's gross income the household saves, as a percentage.
Statement: projection/compare.ts#summarizeProjection publishes ProjectionSummary.savingsRates, one entry per ledger row in ledger order. Its year is the row's year, and ratePct = min(100, max(0, S / I × 100)) when I > 0, else 0, where S = contributions + employerMatch + surplusInvested and I = incomes.total, all four from that row in that year's nominal dollars. The rate is in percentage points (20 means 20%). Rounding: none.
Formula:
ratePct_y = I_y > 0 ? min(100, max(0, (C_y + M_y + U_y) / I_y × 100)) : 0-
C_y: Contributions made in year y (YearResult.contributions) (unit: nominal USD) (domain: nonnegative) -
M_y: Employer match made in year y (YearResult.employerMatch) (unit: nominal USD) (domain: nonnegative) -
U_y: Surplus cash flow invested in year y (YearResult.surplusInvested) (unit: nominal USD) (domain: nonnegative) -
I_y: Gross cash income of year y (YearResult.incomes.total) (unit: nominal USD) (domain: nonnegative)
Timing: every projection year
Rounding: none
Limits:
- The denominator is the year's gross cash income as the ledger totals it (wages, Social Security, pensions, annuities, ladder income, recurring and one-time income, taxable yield and tax-exempt interest), not wages alone, so a retired year whose surplus is reinvested shows a positive rate with no wages.
- Savings count the employer match and the surplus the ledger invests as well as the household's own contributions. A year whose savings exceed its gross income is clamped to 100: for example a Social Security year whose surplus, reinvested from a required distribution larger than its spending, exceeds that year's income. The distribution is a withdrawal, not income (incomes.total), so it adds to the savings and not to the denominator. The floor at 0 binds only on a negative savings amount, which the ledger does not publish. A year with no gross income publishes 0 whatever it saves.
- Evidence: summarizeProjection on four rows of the early-career-match example (wages of $65,000 with $9,000 contributed and a $2,600 match, a retired year with no income, and two Social Security years with reinvested surpluses), entered as ledger rows; the rate reads only the row, so how a projection reaches those amounts does not enter it.
Provenance: derived by
claude, implemented byclaude.Independent review:
codexGates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass
Worksheet: projection-summary-savings-rate-annual.md Mutation receipt: projection-summary-savings-rate-annual.mutation.md
Evidence tests: compareSummary.breakdowns.evidence.test.ts
Implemented in: compare.ts
Is the number of: projection-summary-savings-rate-pct-annual
Feeds: projection-summary-average-pre-retirement-savings-rate-pct
-
Engine source: engine/src/projection/compare.ts
(ProjectionSummary.averagePreRetirementSavingsRatePct)
The evidence links on this page (each record's worksheet, mutation receipt, evidence tests
and implementation, and the source links) are pinned to engine commit
2624dbdba9fd, committed 2026-10-11.
The live calculation index and
output-family census links point at the repository's main
branch, so you can check this snapshot against them.