Calculation catalog › Cash flow and summary

Projection summary FI year

Calendar year in which modeled assets first reach the financial-independence target.

Field: fiYear Unit: calendar year Basis: no dollar basis Computed by: Engine Status: Complete chain

Appears in: report (financial independence year); results-headline (FI section "you reach FI in YEAR (age N)"); report (headline-results block fiYear); mcp (run_projection.summary.fiYear); mcp (compare_scenarios.a.fiYear); mcp (compare_scenarios.b.fiYear)

Calculation records

  • First financial-independence crossing year Formula record, justified by derivation

    Purpose: The first ledger year whose deflated investable total reaches the FI number.

    Statement: projection/compare.ts#summarizeProjection publishes fiYear as the first ledger year, in ledger order, whose published end-of-year investableTotal, deflated by (1 + inflationPct/100)^(year - startYear), is greater than or equal to fiNumber. It is null when no year crosses and when the ledger is empty. Units: calendar year or null. Rounding: none; the comparison is inclusive.

    Formula: fiYear = min { y : investableTotal_y / (1 + i)^(y - y0) >= fiNumber }, else null

    • investableTotal_y: Published end-of-year investable total (unit: usd) (domain: nonnegative)
    • i: General inflation (unit: 1) (domain: inflationPct / 100)
    • fiNumber: Upstream FI number in start-year dollars (unit: usd) (domain: positive)

    Timing: once per projection, over the ledger in order

    Rounding: none

    Limits:

    • Asserted at summarizeProjection with the worksheet's three-row crossing ledger and its three-row null ledger, both at zero inflation from a 2026 start so every deflator is exactly 1. The worksheet states its FI number as an input; the summary derives it, so the plan is built to derive exactly 500,000: a retirement age already attained puts the spending year at the start year, and 20,000 of funded outflows over the 4-percent lens is exactly 500,000
    • The empty-ledger null is asserted separately from the no-crossing null, because they reach the same published value by different paths

    Provenance: derived by codex, implemented by claude.

    Independent review: cursor

    Gates (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-year.md Mutation receipt: projection-summary-fi-year.mutation.md

    Evidence tests: compareSummary.evidence.test.ts

    Implemented in: compare.ts

    Is the number of: projection-summary-fi-year

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 by claude.

    Independent review: codex

    Gates (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 by claude.

    Independent review: codex

    Gates (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

Engine source: engine/src/projection/compare.ts (ProjectionSummary.fiYear)

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.