Calculation catalog › Spending and withdrawals
Spending shape delta vs flat
How much more or less baseline spending each spending shape sustains than the flat shape.
Field: Spending-shape solved delta versus flat Unit: US dollars Basis: today's dollars (real) Computed by: UI-native Status: Complete chain
Appears in: solver-page (Per-shape table "vs. flat" column)
Calculation records
-
Spending shapes compared with constant-real spending Composition record, justified by derivation
Purpose: Each spending shape's sustainable amount and its difference from the constant-real shape, as the spending page's shape table shows them.
Statement: decisions/spendingShapes.ts#planWithSpendingShape builds the plan solved for each shape in SPENDING_SHAPE_COMPARISON (flat, smile, smirk): the plan's own phases replaced by the shape's rows on the retirement age of the person the phases follow (expenses.phasesAgeOf, the person listed first when a two-person plan names none, which the plan then names; 65 when unset), and amortized (ABW) spending removed; every other field, a guardrail policy included, is unchanged. spendingShapeRows publishes, for each shape, the solver's published maxBaseAnnual and deltaVsFlatDollars, that amount minus the flat shape's, null on the flat row and when either amount is null (owner decision R5), so the difference is always the gap between the two amounts shown. It refuses an input without exactly one flat row, a shape listed twice, and any amount that is not a published answer (a rounded amount that is not a whole multiple of $100, or an amount without its rounding). With passing levels of $50,050 (flat) and $50,149 (smile) the published amounts are $50,000 and $50,100 and the difference is +$100, where subtracting the passing levels printed +$99. Units: today's dollars per year. Rounding: none beyond the published amounts.
Formula:
delta_s = M_s − M_flat-
M_s: The published answer of the solve for shape s (unit: today USD per year) (domain: nonnegative) -
M_flat: The published answer of the constant-real (flat) solve (unit: today USD per year) (domain: nonnegative)
Timing: once per comparison
Rounding: none
Limits:
- Each shape is a separate solve that resolves to about $500, so a difference smaller than that is not meaningful.
- Shapes use the retirement age of the person the phases follow (65 when unset); a plan with amortized spending is compared as fixed-target versions of itself.
- Under guardrail spending a row can publish the exact amount that passed rather than the rounded one (see solved-spending-rounding); its difference is still taken between the amounts shown.
Provenance: derived by
claude, implemented byclaude-subagent.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: spending-shape-delta-vs-flat.md Mutation receipt: spending-shape-delta-vs-flat.mutation.md
Evidence tests: spendingShapes.evidence.test.ts
Implemented in: spendingShapes.ts
Is the number of: spending-shape-delta-vs-flat
-
Records that feed this number
-
Sustainable spending rounded down to the hundred Formula record, justified by derivation
Purpose: The one amount every surface shows, applies and measures slack from once the spending solver has found the highest level that passed.
Statement: decisions/spendingSolver.ts#roundSolvedSpending floors a nonnegative amount to a whole multiple of SOLVED_SPENDING_STEP_DOLLARS (100): floor(x / 100) × 100, refusing an amount that is negative or not finite. The solver publishes feasibleBaseAnnual, the highest level that passed (a whole number of dollars), and maxBaseAnnual, that level rounded down to $100, and measures spendingSlackDollars as maxBaseAnnual minus the current base (owner decision R4: one slack everywhere, from the amount shown). Under fixed-target spending the rounded amount is published without a run of its own, on the decision's assumption that a lower base does not fail where a higher one passed (measured, not proven; see the limits). Under withdrawal-rate or risk-based guardrails a lower base can fail, so the rounded amount is published only on a run at it that passed: the search's own probe at that level when it made one, else one more run after the search; otherwise the solver publishes feasibleBaseAnnual itself, sets maxBaseAnnualRounding to none and adds a sentence to its diagnostics saying why. A rounded amount below the plan's required spending is never published. Whether today's spending is sustained is sustainsCurrentBase, the verdict of the first probe (today's base rounded to a whole dollar, or the required floor rounded up when that is higher), never the sign of the slack. When the rounded amount is published, bestEvaluation and the unpriced premium-credit years still describe the run at feasibleBaseAnnual. For the worksheet's case A (levels up to $62,850 pass, current base $40,000) the probes end at $62,813, the published amount is $62,800 and the slack $22,800; under guardrails, with the rounded amount failing, it is $62,813 and $22,813. Units: today's dollars per year. Rounding: down to a whole $100.
Formula:
M = floor(F / 100) · 100 when that level is known to pass, else M = F; slack = M − current-
F: feasibleBaseAnnual, the highest level that passed (unit: today USD per year) (domain: nonnegative whole number) -
M: maxBaseAnnual, the published answer (unit: today USD per year) (domain: nonnegative; a multiple of 100 unless published as F) -
current: The base spending the solve started from, after any base patch (unit: today USD per year) (domain: nonnegative)
Timing: once per solve, after the search
Rounding: down to a whole $100; the page prints whole dollars
Limits:
- Under fixed-target spending the rounded amount is not run on its own. That it passes is the decision's assumption, measured and not proven: every one of the 109 published amounts of the example plans and their shape solves re-simulates feasible, as do 210 fixed-target balance variants (0.70 to 1.30 times) of ten Marketplace-heavy or early-retirement examples, and a scan of every $10 level in the $3,000 below the answer on six Marketplace examples found none that fails. The mechanism it does not exclude is a lower spend dropping the household's income below 100 percent of the poverty line, where the premium tax credit is lost and the full premium is budgeted.
- When the rounded amount is published, the evidence figures on the spending page and the unpriced premium-credit years describe the run at feasibleBaseAnnual, not a run at the published amount (under guardrails that run also passed, but its own evidence is not published).
- Under guardrails a check of the rounded amount that the search did not already make is one more simulation after the search, outside maxSimulations, and it is counted in simulationCount.
- A published amount under the current base by less than $100 can belong to a plan that sustains its own spending (the current base passed and is the answer, rounded down); the spending page judges that on feasibleBaseAnnual, and so should any other reader of the slack.
- The search resolves to about $500, so answers less than $100 apart can publish the same amount, and a comparison of two plans can tie there.
Provenance: derived by
claude, implemented byclaude-subagent.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: solved-spending-rounded-to-hundred.md Mutation receipt: solved-spending-rounded-to-hundred.mutation.md
Evidence tests: spendingSolver.rounding.evidence.test.ts
Implemented in: spendingSolver.ts
Is the number of: solved-spending-rounded-to-hundred, sustainable-spending-result-max-base-annual
Feeds: sustainable-spending-result-spending-slack-dollars, solved-initial-withdrawal-rate-pct, spending-shape-delta-vs-flat
-
-
Annual real spending drift compiled to five-year phase rows Model record, justified by derivation
Purpose: Turns a steady %/yr real spending change into the editable phase multipliers a plan stores.
Statement: deltaPct = 0 returns no phases. startAge = min(max(round(retirementAge), 40), 105). For age = startAge + 5, startAge + 10, ... while age <= 100: multiplier = (1 + deltaPct/100)^(age - startAge), rounded to two decimals as Math.round(x x 100)/100 and clamped into [0, 3]; each row is { fromAge: age, multiplier }. Units: fromAge in years of the person the phases follow (expenses.phasesAgeOf; spending-phase-person); multiplier a factor on baseAnnual. Rounding: two decimals per row, as stated.
Formula:
m(age) = clamp(round2((1 + d/100)^(age - a0)), 0, 3) for age = a0 + 5k <= 100, k >= 1, a0 = clamp(round(retirementAge), 40, 105)-
d: Annual real spending change (unit: percent/year) (domain: finite; 0 emits no rows) -
a0: Rounded, clamped retirement age the steps count from (unit: years) (domain: 40 <= a0 <= 105) -
age: A phase row's fromAge (unit: years) (domain: a0 + 5, a0 + 10, ... <= 100) -
m(age): Multiplier on baseAnnual from that age (unit: 1) (domain: 0 <= m <= 3, two decimals)
Timing: compiled once when the shape is chosen; the rows then hold flat between step ages
Rounding: two decimals per multiplier (Math.round(x x 100)/100)
Limits:
- On the amortization-based-withdrawal path the compiled phases do not reach spending-base-annual: annualLifestyleLayers scales the base by the phase multiplier and then, when ABW is active, replaces it with abwAnnualPayment and zeroes the other lifestyle layers. The feed holds on the ordinary lifestyle path, for spending-shape-delta-vs-flat and for the sustainable-spending solver, which run shapes as plan data.
- Five-year steps hold the fully compounded multiplier flat between step ages, an editable approximation to annual drift
- The first row is one step after retirement, never at retirement age itself
- Retirement age is rounded and clamped into [40, 105] so every row satisfies the phase schema (fromAge 40..110, multiplier 0..3)
- The rows are plan data: the planner-ui SpendingSection writes them into expenses.phases and the solver page runs the sustainable-spending solver per shape; no engine module calls this function, so the named families are fed through the plan the UI writes
Provenance: derived by
codex, implemented byclaude-subagent.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: spending-shape-annual-delta-phases.md Mutation receipt: spending-shape-annual-delta-phases.mutation.md
Evidence tests: shapePresets.evidence.test.ts
Implemented in: shapePresets.ts
Feeds: spending-base-annual, spending-shape-delta-vs-flat, sustainable-spending-result-max-base-annual, sustainable-spending-result-spending-slack-dollars
-
-
Named spending shapes compiled to phase rows Model record, justified by derivation
Purpose: What phase rows the flat, smile, front-loaded and smirk presets write into a plan.
Statement: "flat" returns no phases. "smile" returns [{ fromAge: 75, multiplier: 0.9 }, { fromAge: 85, multiplier: 0.8 }] independent of retirement age. "smirk" returns annualDeltaPhases(SMIRK_ANNUAL_REAL_DELTA_PCT = -1, retirementAge). "frontLoaded": boostFrom = min(max(retirementAge, 40), 100); boostFrom >= 75 returns no phases, else [{ fromAge: boostFrom, multiplier: 1.1 }, { fromAge: 75, multiplier: 1 }]. Units: fromAge in years; multiplier a factor on baseAnnual. Rounding: none here; the smirk rows carry annualDeltaPhases's two-decimal rounding.
Formula:
flat: []; smile: [(75, 0.9), (85, 0.8)]; smirk: annualDeltaPhases(-1, retirementAge); frontLoaded: [(clamp(retirementAge, 40, 100), 1.1), (75, 1)] when that age < 75, else []-
shape: The preset id (unit: enum) (domain: flat | smile | frontLoaded | smirk) -
retirementAge: The retirement age of the person the phases follow (expenses.phasesAgeOf) (unit: years) (domain: finite) -
rows: The phase rows written into expenses.phases (unit: rows) (domain: each within the phase schema)
Timing: compiled once when the preset is chosen; never re-evaluated for a saved plan
Rounding: none (smirk inherits the two-decimal rows)
Limits:
- On the amortization-based-withdrawal path the compiled phases do not reach spending-base-annual: annualLifestyleLayers scales the base by the phase multiplier and then, when ABW is active, replaces it with abwAnnualPayment and zeroes the other lifestyle layers. The feed holds on the ordinary lifestyle path, for spending-shape-delta-vs-flat and for the sustainable-spending solver, which run shapes as plan data.
- Presets compile to visible phase rows at creation time and never live-link a saved plan to the research constants; changing SMIRK_ANNUAL_REAL_DELTA_PCT changes only newly compiled plans
- The smile calibration is a two-step approximation of Blanchett's average path without an explicit late-life step-up
- frontLoaded passes the retirement age through unrounded (clamped into [40, 100]) while the smirk path rounds it
- No engine module calls this function; the UI writes the rows into expenses.phases and the solver page evaluates each shape, so the named families are fed through plan data
Provenance: derived by
codex, implemented byclaude-subagent.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: spending-shape-preset-compilation.md Mutation receipt: spending-shape-preset-compilation.mutation.md
Evidence tests: shapePresets.evidence.test.ts
Implemented in: shapePresets.ts
-
Transformations
- Each shape's published maxBaseAnnual minus the flat shape's, in decisions/spendingShapes.ts#spendingShapeRows (R5), on the plans decisions/spendingShapes.ts#planWithSpendingShape builds.
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.