Calculation catalog › Spending and withdrawals
Spending total annual
All modeled expenses for a projection year before tax and penalties.
Field: Total expenses Unit: US dollars Basis: nominal dollars Computed by: Engine Status: Complete chain
Appears in: csv (totalExpenses); report (yearLedger); results-table (expenses); results-table (Expenses column); scenarios-page (Annual "Funded spending" and lifetime "Funded spending")
Calculation records
-
Annual net expense total Composition record, justified by derivation
Purpose: The year's actual net outflow, and which expense members compose it.
Statement: projection/internal/annualExpenseSummary.ts#annualExpenseSummary publishes projection/internal/types/yearLedger.ts#YearExpenses.total as baseSpending + oneTimeGoals + debtService + propertyCosts + healthcare + insurancePremiums + careCost - ltcBenefit, kept in that left-to-right association because regrouping the LTC pair can move the last bit. careCost is gross and ltcBenefit offsets it. guardrailFactor is descriptive: the cut is already inside baseSpending and is never multiplied in again. requiredSpending, targetSpending, idealSpending, excessSpending and intendedSpending are overlapping layer summaries, not additional members, and intendedSpending is the no-cut request and need not equal this total. Units: nominal USD per year. Rounding: none.
Formula:
total = baseSpending + oneTimeGoals + debtService + propertyCosts + healthcare + insurancePremiums + careCost - ltcBenefit-
baseSpending: Recurring lifestyle spending already net of the guardrail cut (unit: usd/year) (domain: nonnegative) -
oneTimeGoals: One-time goals funded this year (unit: usd/year) (domain: nonnegative) -
careCost, ltcBenefit: Gross care-episode cost and the policy benefit offsetting it (unit: usd/year) (domain: nonnegative; the benefit is subtracted) -
guardrailFactor: Discretionary multiplier already applied inside baseSpending (unit: 1) (domain: descriptive only)
Timing: once per projection year, at the expense summary
Rounding: none
Limits:
- Asserted at the annualExpenseSummary seam with the worksheet's eight member amounts, because the worksheet states them as published year-row components: a single real plan cannot present a chosen base spending, one-time goal, debt service, property cost, healthcare charge, insurance premium, gross care cost and LTC benefit at those exact values simultaneously
- Cross-checked on a real simulatePlan run: the published total of that year row equals the same row's own eight published members in the same association
- The guardrail factor and intended spending are asserted NOT to enter: the test drives the seam with a factor of 0.90 and an intended request of $100,000 and shows neither changes the total
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: spending-total-annual.md Mutation receipt: spending-total-annual.mutation.md
Evidence tests: annualExpenseSummary.evidence.test.ts
Implemented in: annualExpenseSummary.ts; yearLedger.ts
Is the number of: spending-total-annual
-
Records that feed this number
-
Annual base spending after the guardrail cut Composition record, justified by derivation
Purpose: Recurring lifestyle spending actually intended once the guardrail has rationed the target layer.
Statement: projection/internal/annualGuardrailFunding.ts#annualGuardrailFundingPlan computes targetLifestyleFunded as targetLifestyle x min(1, discretionaryMultiplier) while guardrails are active, and the full targetLifestyle otherwise; projection/internal/annualExpenseSummary.ts#annualExpenseSummary then publishes projection/internal/types/yearLedger.ts#YearExpenses.baseSpending as requiredLifestyle + targetLifestyleFunded + idealLifestyleFunded + excessLifestyleFunded. The cap keeps a multiplier above one from inflating the target layer; upside instead reaches the separately funded ideal and excess layers. One-time goals and the system-computed costs are excluded; expenses.total adds them. The same multiplier is published unchanged as expenses.guardrailFactor. Units: nominal USD per year. Rounding: none.
Formula:
baseSpending = requiredLifestyle + targetLifestyle * min(1, guardrailFactor) + idealLifestyleFunded + excessLifestyleFunded-
requiredLifestyle: Required-floor lifestyle layer, never cut (unit: usd/year) (domain: nonnegative) -
targetLifestyle: Full target lifestyle layer before the cut (unit: usd/year) (domain: nonnegative) -
guardrailFactor: This year's discretionary multiplier (unit: 1) (domain: nonnegative; capped at 1 for this term) -
idealLifestyleFunded, excessLifestyleFunded: Upside layers funded this year (unit: usd/year) (domain: nonnegative)
Timing: once per projection year, after the guardrail decision and before the funding fixed point
Rounding: none
Limits:
- The same published field is also the output of abw-annuity-due-payment, which computes it under the ABW spending policy; this record is the guardrail-policy composition of the same field, so the family carries two records rather than one
- The worksheet's two cases are states annualGuardrailFundingPlan produces, and the evidence runs the real phase for a live household: a cutting year holding an incoming 0.75 (its withdrawal rate at its starting rate, so the policy holds, as in any year after a cut), where the phase funds the target layer at the factor and no upside because the upside budget is max(0, multiplier - 1) x the guardrail step basis; and a year with no policy active, where the full target layer and both upside layers fund. The first derivation paired a 0.75 cut with $4,000 of funded ideal and $1,500 of funded excess, which the YearExpenses.baseSpending comment rules out; it was re-derived on 2026-09-22 and re-approved by the reviewer, and the min(1, factor) cap is also asserted at a held multiplier of 1.4
- The $8,000 one-time goal is passed to annualExpenseSummary beside the phase's funded layers and asserted to be absent from baseSpending while present in expenses.total, which is the worksheet's second wrong reading
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: spending-base-annual.md Mutation receipt: spending-base-annual.mutation.md
Evidence tests: annualExpenseSummary.evidence.test.ts
Implemented in: annualExpenseSummary.ts; annualGuardrailFunding.ts; yearLedger.ts
Is the number of: spending-base-annual
Feeds: spending-total-annual
-
-
Annual gross care cost and the LTC benefit against it Model record, justified by derivation
Purpose: The two published sides of one long-term-care year: what care costs, and what a policy pays.
Statement: projection/internal/types/yearLedger.ts#YearExpenses.careCost and #YearExpenses.ltcBenefit, planned by projection/internal/annualDebtAndLongTermCare.ts#annualLongTermCarePlan, publish the two sides of one rule. An active episode (the person is alive and 0 <= ageAttained - startAge < durationYears) contributes annualCost x the health-inflation factor to gross care cost. Each LTC policy the same person owns pays min(remaining cost, cap), where cap = benefitMonthly x 12 x (1 + riderPct/100)^(year - startYear), multiplied in the episode's first year by max(0, 1 - eliminationPeriodDays/365); a policy whose benefitPeriodYears are already used pays nothing. Units: nominal USD per year. Rounding: none.
Formula:
careCost = sum_e cost_e f_h; cap_p = 12 b_p (1 + q_p)^(y - y0) [x (1 - d_p/365) in the first episode year]; ltcBenefit = sum of min(remaining, cap_p)-
cost_e: Episode annual cost, today dollars (unit: usd/year) (domain: nonnegative) -
f_h: Cumulative health-inflation factor (unit: 1) (domain: positive) -
b_p: Policy monthly benefit base (unit: usd/month) (domain: nonnegative) -
q_p: Inflation rider (unit: 1) (domain: inflationRiderPct / 100) -
d_p: Elimination period (unit: days) (domain: 0..365)
Timing: once per projection year, per episode then per owned policy in plan order
Rounding: none
Limits:
- Asserted at the exported phase helper with the worksheet's episode, both policies, the years-used map, the health-inflation factor and the start and current years verbatim. Plan assumptions beyond the worksheet's inputs: both policies are paid-up (premium 0), which this plan does not read, and the person resolver reports the stated attained age and alive
- The benefit is published gross of the expense fold; expenses.total subtracts it, so the same dollars are never counted twice
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: spending-care-cost-gross-and-ltc-benefit-annual.md Mutation receipt: spending-care-cost-gross-and-ltc-benefit-annual.mutation.md
Evidence tests: annualDebtAndLongTermCare.evidence.test.ts
Implemented in: yearLedger.ts; annualDebtAndLongTermCare.ts
Is the number of: spending-care-cost-gross-annual, long-term-care-benefit-annual
Feeds: spending-total-annual
-
-
Annual debt service under the grow-then-pay convention Formula record, justified by derivation
Purpose: What the year's debts actually cost, once interest is charged before the payment.
Statement: projection/internal/types/yearLedger.ts#YearExpenses.debtService, planned by projection/internal/annualDebtAndLongTermCare.ts#annualDebtServiceRows, is the sum over debt accounts with a positive balance of the year's payment, where the opening balance first grows by 1 + interestPct/100 and the year then pays the whole grown balance when payoffYear is set and reached, else min(grown balance, monthlyPayment x 12). The level payment is never inflated and self-caps at the balance. Units: nominal USD per year. Rounding: none; the ordered fold is a binary float.
Formula:
grown_i = B_i (1 + r_i); pay_i = payoff ? grown_i : min(grown_i, 12 m_i); debtService = sum_i pay_i-
B_i: Opening balance of debt i (unit: usd) (domain: positive) -
r_i: Annual interest rate (unit: 1) (domain: interestPct / 100) -
m_i: Level monthly payment (unit: usd/month) (domain: nonnegative) -
payoff: Scheduled payoff reached (unit: boolean) (domain: year >= payoffYear)
Timing: once per projection year, before the funding solve
Rounding: none
Limits:
- Asserted twice: at the exported phase helper with the worksheet's two debts verbatim, and as the published expenses.debtService of a real simulatePlan 2030 row carrying the same two debts. Plan assumptions beyond the worksheet's inputs for that ledger row: a single 60-year-old filing single in KY at a zero state rate, zero general inflation, zero account returns, and one 500,000 cash account large enough that the payments are funded without a portfolio sale
- The worksheet's third wrong reading (ignoring payoffYear) coincides with the right answer at these inputs, so the test discriminates the first two numerically and states the third as a rule
- Restated 2026-09-29 under decision D-2027-ROLLOVER (review L4): a payoff year before the projection start year is reached in the first year, so the ledger pays the whole grown balance then, as it always did; nothing moves. The projection now names it (projection/preStartEvents.ts#preStartEvents) with the amount paid, the balance with a year of interest, and says to set the balance to $0 if the debt was paid. From a 2027 start the reviewer's mortgage with a 2026 payoff moved ending net worth by -$189,695.85 with nothing said
Provenance: derived by
codex, implemented byclaude.Independent review:
grokGates (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-debt-service-annual.md Mutation receipt: spending-debt-service-annual.mutation.md
Evidence tests: annualDebtAndLongTermCare.evidence.test.ts
Implemented in: yearLedger.ts; annualDebtAndLongTermCare.ts; preStartEvents.ts
Is the number of: spending-debt-service-annual
Feeds: spending-total-annual
-
-
Annual level insurance premiums Formula record, justified by derivation
Purpose: Which insurance policies are charged this year, and at what level nominal premium.
Statement: projection/internal/types/yearLedger.ts#YearExpenses.insurancePremiums, selected by projection/internal/annualInsurancePremiumRows.ts#annualInsurancePremiumRows, sums the fixed-nominal annualPremium of every policy whose subject is alive: lifetime charges every year, paidUp charges nothing, and untilAge charges only while the subject's attained age is below premiumEndAge, so nothing is charged in the year the subject attains premiumEndAge or afterward. No inflation factor is applied. Units: nominal USD per year. Rounding: none.
Formula:
insurancePremiums = sum over charged policies of annualPremium-
annualPremium: Level contractual premium (unit: usd/year) (domain: nonnegative) -
premiumMode: lifetime | paidUp | untilAge (unit: mode) (domain: closed enum) -
premiumEndAge: Subject age at which untilAge premiums stop (unit: years) (domain: required for untilAge)
Timing: once per projection year, per policy occurrence in plan order
Rounding: none
Limits:
- Asserted at the exported phase function with the worksheet's five policies (the primary case's four and the boundary case's Life E), their modes, premiums, subject ages and end ages verbatim; the subject resolver supplies the stated ages and alive flags. Plan assumptions beyond the worksheet's inputs: each policy carries zero benefit fields, which the premium selection does not read
- The first derivation read untilAge as charging through and including premiumEndAge, charged its Life C at attained age 65 against an end age of 65, and expected 1,800; that derivation was corrected on 2026-09-18 under decision D-PREMIUM-END-AGE. The worksheet now states the strict stop age production holds: a policy is skipped once subject.ageAttained >= policy.premiumEndAge, so its primary case totals 1,200 with Life C not charged, and its boundary case charges Life E 600 at attained age 64, one year below the same end age. Both are asserted, the 1,800 through-the-end-age reading is asserted as a wrong reading, and no production-versus-worksheet discrepancy remains. Decision D-PREMIUM-END-AGE (2026-09-25) kept this rule, since premiumEndAge is the field a user fills as the age premiums stop; the premiumModeSchema comment, which once read "charge annualPremium through premiumEndAge", now states the same stop age
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: spending-insurance-premiums-annual.md Mutation receipt: spending-insurance-premiums-annual.mutation.md
Evidence tests: annualInsurancePremiumRows.evidence.test.ts
Implemented in: yearLedger.ts; annualInsurancePremiumRows.ts
Is the number of: spending-insurance-premiums-annual
Feeds: spending-total-annual
-
-
Annual funded one-time goals Formula record, justified by derivation
Purpose: What this year's one-time goals actually cost, as distinct from what was intended.
Statement: projection/internal/types/yearLedger.ts#YearExpenses.oneTimeGoals, produced by projection/internal/annualOneTimeGoalFundingPhase.ts#annualOneTimeGoalFundingPhase, is the funded nominal amount of goals in the current year. With no guardrail scheduler every goal funds exactly in its target year at amount x the cumulative inflation factor; a skipped amount is an intended-but-unfunded miss tracked separately and is not in this field. Nothing funds when nobody in the household is alive. Units: nominal USD per year. Rounding: none.
Formula:
oneTimeGoals = sum over goals with targetYear = y of amount x f-
amount: Goal amount, today dollars (unit: usd) (domain: nonnegative) -
f: Cumulative general-inflation factor to this year (unit: 1) (domain: positive)
Timing: once per projection year, in goal order
Rounding: none
Limits:
- Asserted at the exported phase helper with the worksheet's three goals, target years, amounts, current year, null scheduler and alive flag verbatim; the helper receives the caller's already-resolved inflation factor, so the 1.10 is supplied rather than compounded from plan assumptions
- The worksheet's third wrong reading is a guardrail case: with no scheduler the phase reports four exact zero skip accumulators, which the test asserts, so no skipped amount can reach this field here
- Restated 2026-09-28 under decision D-2027-ROLLOVER: a goal whose target year (and, for a movable or skippable goal, whose whole window) is before the projection start year funds nothing, as before, and the projection now names it in a warning (projection/preStartEvents.ts#preStartEvents): the goal is dated before the plan starts, so it is not counted, and if it has not happened the household moves it to the start year or later. From a 2027 start the derivation's U1 household's $30,000 car goal dated 2026 dropped out with only an editor note to say so
Provenance: derived by
codex, implemented byclaude.Independent review:
grokGates (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-one-time-goals-annual.md Mutation receipt: spending-one-time-goals-annual.mutation.md
Evidence tests: annualOneTimeGoalFundingPhase.evidence.test.ts
Implemented in: yearLedger.ts; annualOneTimeGoalFundingPhase.ts; preStartEvents.ts
Is the number of: spending-one-time-goals-annual
Feeds: spending-total-annual
-
-
Annual property carrying costs on owned properties Formula record, justified by derivation
Purpose: Property tax and homeowner insurance on every property still owned this year.
Statement: projection/internal/types/yearLedger.ts#YearExpenses.propertyCosts, produced by projection/internal/annualPropertyCarryingCosts.ts#annualPropertyCarryingCosts, is the sum over property accounts that the household still owns of (propertyTaxAnnual + insuranceAnnual) x the cumulative general-inflation factor. A property contributes nothing from the year the ledger sells it onward: its planned sale year, or the projection start year when the planned year is earlier (projection/propertySaleYear.ts#effectivePropertySaleYear), the same year the sale itself runs. The charge continues after any mortgage is paid off, and no row is produced at all when nobody in the household is alive. Units: nominal USD per year. Rounding: none.
Formula:
propertyCosts = sum over owned p of (tax_p + insurance_p) x f-
tax_p: Annual property tax, today dollars (unit: usd/year) (domain: nonnegative; absent = 0) -
insurance_p: Annual homeowner insurance, today dollars (unit: usd/year) (domain: nonnegative; absent = 0) -
f: Cumulative general-inflation factor to this year (unit: 1) (domain: positive)
Timing: once per projection year, while the property is owned
Rounding: none
Limits:
- Asserted at the exported phase helper with the worksheet's two properties, current year, inflation factor and alive flag verbatim; the helper receives the caller's already-resolved inflation factor, so the 1.10 is supplied rather than compounded from plan assumptions. Plan assumptions beyond the worksheet's inputs: each property carries a 400,000 value and no HECM, neither of which the carrying-cost formula reads
- Ownership is decided by the effective sale year alone (the planned sale year, or the start year when that is earlier); a property with no sale year is owned for the whole horizon
- Restated 2026-09-29 under decision D-2027-ROLLOVER (review H1). No source governs a sale date that has passed; the decision reads a property still on the plan with a past sale year as a sale not yet reflected, so the ledger sells it in the first year (when it has a value; a property at $0 is not sold) and the carrying costs stop from that same year. Before, the costs stopped from the planned year while the sale ran only in a year equal to it, which a later start never reaches: the reviewer's case (the example couple's $420,000 home, $6,000 of property tax and $1,800 of insurance, sale dated 2026, run from 2027) kept the home to the end and dropped its costs, adding $1,017,839 to ending net worth. The projection names the sale (projection/preStartEvents.ts#preStartEvents; for a property at $0 it says only that the costs stop) and the editor notes the field
Provenance: derived by
codex, implemented byclaude.Independent review:
grokGates (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-property-costs-annual.md Mutation receipt: spending-property-costs-annual.mutation.md
Evidence tests: annualPropertyCarryingCosts.evidence.test.ts
Implemented in: yearLedger.ts; annualPropertyCarryingCosts.ts; propertySaleYear.ts
Is the number of: spending-property-costs-annual
Feeds: spending-total-annual
-
Engine source: engine/src/projection/internal/types/result.ts
(YearResult.expenses.total)
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.