Roth conversion annual
Traditional-to-Roth dollars actually converted in a projection year.
Field: Roth conversion Unit: US dollars Basis: nominal dollars Computed by: Engine Status: Complete chain
Appears in: csv (rothConversion); optimize-page (conversion schedule); report (yearLedger); results-table (Conversion column); scenarios-page (Annual "Roth conversion" and lifetime "Roth conversions"); cash-flow-drilldown (namedRothConversion and aggregateRothConversion transfer lines); mcp (run_projection.years[].rothConversion)
Calculation records
-
Annual Roth conversion: the bracket headroom a fill-to-target strategy converts Model record, justified by derivation
Purpose: Dollars moved traditional-to-Roth in one year by a fill-to-bracket conversion strategy.
Statement: For strategies.rothConversion in fillToTarget/topOfBracket mode, strategies/rothConversion.ts#sizeRothConversion bisects the federal engine for the largest conversion keeping federal taxable income at or under the selected bracket's ceiling: the NEXT bracket's lowerBound, indexed to the projected year. Without Social Security benefits taxable income rises one dollar per converted dollar, so the answer is that ceiling minus taxable income before the conversion, i.e. upper bound − (ordinary income − deduction). With benefits it is not that subtraction: taxable Social Security phases in at up to 1.85x per converted dollar, so only the bisection's root is the answer. YearResult.rothConversion then publishes the gross movement actually executed, capped by the traditional balance left after the RMD reserve. Units: nominal dollars. Rounding: the bisection stops at a bracket width of $0.01 and returns its lower bound; with two or more convertible owners the amount is then split between them in exact cents, so when every share converts the published total is that amount rounded half up to the cent, which can sit up to half a cent above the root.
Formula:
amount = max{ c : taxableIncome(ordinary + c) <= bracketUpperBound }; without benefits = upperBound − (ordinary − deduction)-
upperBound: Selected bracket's ceiling: the next bracket's lowerBound for the year (unit: usd taxable income) (domain: finite; null for the open-ended top bracket) -
ordinary: Ordinary income before the conversion (unit: usd) (domain: finite) -
deduction: Standard or itemized deduction the federal engine applies (unit: usd) (domain: nonnegative) -
amount: Conversion sized for the year (unit: usd) (domain: nonnegative)
Timing: annual, inside the strategy window [startYear, endYear]
Rounding: bisection to a $0.01 bracket, returning its lower bound; with one convertible owner the published figure is that bound, and with two or more it is split in exact cents, so when every share converts the published total is the amount rounded half up to the cent and can sit up to half a cent above the root
Limits:
- Beyond the worksheet's inputs the evidence plan fixes: a 63-year-old single filer (over 59.5 so no early-distribution exposure, under 65 so the deduction is the base standard deduction with no age addition), the $70,000 as an uninflated recurring ordinary stream, zero returns and zero inflation so the 2026 bracket ladder is unindexed, a cash account large enough to pay the conversion tax, and no base spending
- The published year figure is the executed gross movement: the sized amount is the ceiling (to within half a cent: with two or more convertible owners it is first split in exact cents, so the executed total is the sized amount rounded half up to the cent), and a traditional balance smaller than it (or an RMD reserve) lowers what the row shows
- The bisection's $0.01 stopping width is a real error bar on the published dollar; the worksheet's $51,800 is reached exactly here only because the first midpoint of the expanded bracket lands on the root
- The benefits branch is asserted as a branch, not as a number: the worksheet states no closed form for it, so the evidence shows only that a positive benefit makes the answer differ from the no-benefit subtraction
- The amount is sized once for the household and then split between the owners of traditional balances in proportion to those balances after any unsatisfied RMD is reserved, in exact cents, and each share converts only into that owner's own Roth IRA. An owner with no Roth IRA of their own loses the share, with a warning naming them, so the published figure is the converting owners' shares and can be well below the sized amount, which is published beside it as YearResult.aggregateRothConversionAllocationDesired (decision D-BRACKET-FILL-ROTH-EXAMPLE)
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: roth-conversion-annual.md Mutation receipt: roth-conversion-annual.mutation.md
Evidence tests: annualAggregateRothConversionTargetPlan.evidence.test.ts
Implemented in: rothConversion.ts; annualAggregateRothConversionTargetPlan.ts
Is the number of: roth-conversion-annual
-
Records that feed this number
-
Exact cent largest remainder slices Formula record, justified by derivation
Purpose: Divide an exact-cent total without losing cents or changing positional tie order.
Statement: actions/exactCentProRata.ts#exactCentLargestRemainderSlices allocates a nonnegative exact-cent total across nonnegative integer weights, reconciles independently rounded shares to the exact total by largest fractional remainder, and breaks ties by input position.
Formula:
share_i = A w_i / sum(w); settle rounded-share residual by fractional remainder, ties by original position-
A: Total amount (unit: cents) (domain: nonnegative integer) -
w_i: Allocation weight (unit: integer weight) (domain: nonnegative integer; positive sum)
Timing: one allocation
Rounding: nearest half up, then exact residual settlement
Limits:
- No direct census family: actions/aggregateRothConversionOwnerAllocation.ts consumes the slices as owner allocations feeding annual Roth conversions. The test discriminates positional ties, not every surplus-residual case.
Provenance: derived by
codex, implemented bycodex.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: exact-cent-largest-remainder-slices.md Mutation receipt: exact-cent-largest-remainder-slices.mutation.md
Evidence tests: exactCentProRata.evidence.test.ts
Implemented in: exactCentProRata.ts
Feeds: roth-conversion-annual
-
-
Projection money tolerance thresholds Data record, justified by derivation
Purpose: Keep the half-cent funding tolerance distinct from the one-cent conversion floor.
Statement: projection/moneyTolerance.ts declares two distinct plan-dollar thresholds: an annual funding residual tolerance of $0.005 and an aggregate Roth-conversion action epsilon of $0.01, with comparisons performed in unrounded plan dollars.
Limits:
- No direct census family: projection/internal/annualAggregateRothConversionPhase.ts consumes both constants for funding and conversion materiality; the nearest family is roth-conversion-annual. These are product numerical conventions, not tax law.
Provenance: derived by
codex, implemented bycodex.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-money-tolerance-thresholds.md Mutation receipt: projection-money-tolerance-thresholds.mutation.md
Evidence tests: moneyTolerance.evidence.test.ts
Implemented in: moneyTolerance.ts
Feeds: roth-conversion-annual
Engine source: engine/src/projection/internal/types/result.ts
(YearResult.rothConversion)
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.