@retiregolden/engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +79 -0
  3. package/dist/allocation/assetClasses.d.ts +94 -0
  4. package/dist/allocation/assetClasses.js +215 -0
  5. package/dist/decisions/annuitization.d.ts +72 -0
  6. package/dist/decisions/annuitization.js +184 -0
  7. package/dist/decisions/decisionFixtures.d.ts +48 -0
  8. package/dist/decisions/decisionFixtures.js +253 -0
  9. package/dist/decisions/evaluateCandidate.d.ts +61 -0
  10. package/dist/decisions/evaluateCandidate.js +193 -0
  11. package/dist/decisions/generators.d.ts +110 -0
  12. package/dist/decisions/generators.js +667 -0
  13. package/dist/decisions/index.d.ts +17 -0
  14. package/dist/decisions/index.js +17 -0
  15. package/dist/decisions/insightsAdapter.d.ts +27 -0
  16. package/dist/decisions/insightsAdapter.js +51 -0
  17. package/dist/decisions/objectives.d.ts +96 -0
  18. package/dist/decisions/objectives.js +319 -0
  19. package/dist/decisions/pensionElection.d.ts +112 -0
  20. package/dist/decisions/pensionElection.js +239 -0
  21. package/dist/decisions/search.d.ts +51 -0
  22. package/dist/decisions/search.js +110 -0
  23. package/dist/decisions/spendingSolver.d.ts +70 -0
  24. package/dist/decisions/spendingSolver.js +180 -0
  25. package/dist/decisions/spiaQuotes.d.ts +21 -0
  26. package/dist/decisions/spiaQuotes.js +43 -0
  27. package/dist/decisions/stochastic.d.ts +11 -0
  28. package/dist/decisions/stochastic.js +66 -0
  29. package/dist/decisions/swrComparator.d.ts +54 -0
  30. package/dist/decisions/swrComparator.js +89 -0
  31. package/dist/decisions/tournament.d.ts +53 -0
  32. package/dist/decisions/tournament.js +104 -0
  33. package/dist/decisions/types.d.ts +125 -0
  34. package/dist/decisions/types.js +12 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.js +12 -0
  37. package/dist/insights/detectors/annuitizationHeadroom.d.ts +11 -0
  38. package/dist/insights/detectors/annuitizationHeadroom.js +77 -0
  39. package/dist/insights/detectors/assetLocation.d.ts +9 -0
  40. package/dist/insights/detectors/assetLocation.js +86 -0
  41. package/dist/insights/detectors/hecmBufferCandidate.d.ts +12 -0
  42. package/dist/insights/detectors/hecmBufferCandidate.js +76 -0
  43. package/dist/insights/detectors/incomeFloorFunded.d.ts +8 -0
  44. package/dist/insights/detectors/incomeFloorFunded.js +52 -0
  45. package/dist/insights/detectors/irmaaTierEdge.d.ts +2 -0
  46. package/dist/insights/detectors/irmaaTierEdge.js +102 -0
  47. package/dist/insights/detectors/pensionElectionPending.d.ts +11 -0
  48. package/dist/insights/detectors/pensionElectionPending.js +62 -0
  49. package/dist/insights/detectors/qcdEfficiency.d.ts +2 -0
  50. package/dist/insights/detectors/qcdEfficiency.js +62 -0
  51. package/dist/insights/detectors/rothBridgeHeadroom.d.ts +2 -0
  52. package/dist/insights/detectors/rothBridgeHeadroom.js +77 -0
  53. package/dist/insights/detectors/spendingGuardrails.d.ts +2 -0
  54. package/dist/insights/detectors/spendingGuardrails.js +77 -0
  55. package/dist/insights/detectors/spendingHeadroom.d.ts +12 -0
  56. package/dist/insights/detectors/spendingHeadroom.js +92 -0
  57. package/dist/insights/detectors/ssBridgeGap.d.ts +9 -0
  58. package/dist/insights/detectors/ssBridgeGap.js +92 -0
  59. package/dist/insights/detectors/stateRelocation.d.ts +12 -0
  60. package/dist/insights/detectors/stateRelocation.js +99 -0
  61. package/dist/insights/detectors/widowsPenalty.d.ts +12 -0
  62. package/dist/insights/detectors/widowsPenalty.js +145 -0
  63. package/dist/insights/registry.d.ts +4 -0
  64. package/dist/insights/registry.js +62 -0
  65. package/dist/insights/runInsights.d.ts +6 -0
  66. package/dist/insights/runInsights.js +15 -0
  67. package/dist/insights/types.d.ts +70 -0
  68. package/dist/insights/types.js +1 -0
  69. package/dist/ladder/bridge.d.ts +72 -0
  70. package/dist/ladder/bridge.js +59 -0
  71. package/dist/ladder/fedInvest.d.ts +55 -0
  72. package/dist/ladder/fedInvest.js +77 -0
  73. package/dist/ladder/fundedRatio.d.ts +41 -0
  74. package/dist/ladder/fundedRatio.js +46 -0
  75. package/dist/ladder/ladderMath.d.ts +88 -0
  76. package/dist/ladder/ladderMath.js +140 -0
  77. package/dist/longevity/ssaPeriod2022.d.ts +10 -0
  78. package/dist/longevity/ssaPeriod2022.js +51 -0
  79. package/dist/longevity/types.d.ts +39 -0
  80. package/dist/longevity/types.js +1 -0
  81. package/dist/model/migrations.d.ts +20 -0
  82. package/dist/model/migrations.js +67 -0
  83. package/dist/model/plan.d.ts +3108 -0
  84. package/dist/model/plan.js +1514 -0
  85. package/dist/montecarlo/frontiers.d.ts +22 -0
  86. package/dist/montecarlo/frontiers.js +61 -0
  87. package/dist/montecarlo/historicalReturns.d.ts +27 -0
  88. package/dist/montecarlo/historicalReturns.js +124 -0
  89. package/dist/montecarlo/historicalSuites.d.ts +45 -0
  90. package/dist/montecarlo/historicalSuites.js +102 -0
  91. package/dist/montecarlo/ltcShock.d.ts +42 -0
  92. package/dist/montecarlo/ltcShock.js +62 -0
  93. package/dist/montecarlo/marketModels.d.ts +248 -0
  94. package/dist/montecarlo/marketModels.js +672 -0
  95. package/dist/montecarlo/mortality.d.ts +31 -0
  96. package/dist/montecarlo/mortality.js +61 -0
  97. package/dist/montecarlo/riskBasedGuardrails.d.ts +93 -0
  98. package/dist/montecarlo/riskBasedGuardrails.js +200 -0
  99. package/dist/montecarlo/rng.d.ts +18 -0
  100. package/dist/montecarlo/rng.js +44 -0
  101. package/dist/montecarlo/run.d.ts +210 -0
  102. package/dist/montecarlo/run.js +353 -0
  103. package/dist/montecarlo/sharedPaths.d.ts +43 -0
  104. package/dist/montecarlo/sharedPaths.js +28 -0
  105. package/dist/montecarlo/survival.d.ts +49 -0
  106. package/dist/montecarlo/survival.js +126 -0
  107. package/dist/params/data/realYieldCurve2026.d.ts +17 -0
  108. package/dist/params/data/realYieldCurve2026.js +26 -0
  109. package/dist/params/data/year2026.d.ts +14 -0
  110. package/dist/params/data/year2026.js +199 -0
  111. package/dist/params/index.d.ts +63 -0
  112. package/dist/params/index.js +134 -0
  113. package/dist/params/provenance.d.ts +29 -0
  114. package/dist/params/provenance.js +123 -0
  115. package/dist/params/state/data/year2026.d.ts +18 -0
  116. package/dist/params/state/data/year2026.js +581 -0
  117. package/dist/params/state/index.d.ts +13 -0
  118. package/dist/params/state/index.js +26 -0
  119. package/dist/params/state/types.d.ts +73 -0
  120. package/dist/params/state/types.js +10 -0
  121. package/dist/params/types.d.ts +201 -0
  122. package/dist/params/types.js +7 -0
  123. package/dist/projection/annuityForms.d.ts +62 -0
  124. package/dist/projection/annuityForms.js +87 -0
  125. package/dist/projection/compare.d.ts +96 -0
  126. package/dist/projection/compare.js +216 -0
  127. package/dist/projection/flatTax.d.ts +7 -0
  128. package/dist/projection/flatTax.js +14 -0
  129. package/dist/projection/optimizePlan.d.ts +328 -0
  130. package/dist/projection/optimizePlan.js +1039 -0
  131. package/dist/projection/relocation.d.ts +149 -0
  132. package/dist/projection/relocation.js +317 -0
  133. package/dist/projection/simulate.d.ts +61 -0
  134. package/dist/projection/simulate.js +2754 -0
  135. package/dist/projection/types.d.ts +381 -0
  136. package/dist/projection/types.js +9 -0
  137. package/dist/rmd/jointLifeTable.d.ts +8 -0
  138. package/dist/rmd/jointLifeTable.js +663 -0
  139. package/dist/rmd/rmd.d.ts +29 -0
  140. package/dist/rmd/rmd.js +34 -0
  141. package/dist/scenarios/scenarios.d.ts +70 -0
  142. package/dist/scenarios/scenarios.js +130 -0
  143. package/dist/socialSecurity/benefitFactor.d.ts +13 -0
  144. package/dist/socialSecurity/benefitFactor.js +40 -0
  145. package/dist/socialSecurity/claimFactor.d.ts +33 -0
  146. package/dist/socialSecurity/claimFactor.js +58 -0
  147. package/dist/socialSecurity/disability.d.ts +36 -0
  148. package/dist/socialSecurity/disability.js +42 -0
  149. package/dist/socialSecurity/familyMaximum.d.ts +38 -0
  150. package/dist/socialSecurity/familyMaximum.js +48 -0
  151. package/dist/socialSecurity/maritalBenefits.d.ts +57 -0
  152. package/dist/socialSecurity/maritalBenefits.js +87 -0
  153. package/dist/socialSecurity/nra.d.ts +30 -0
  154. package/dist/socialSecurity/nra.js +83 -0
  155. package/dist/socialSecurity/piaFromEarnings.d.ts +100 -0
  156. package/dist/socialSecurity/piaFromEarnings.js +202 -0
  157. package/dist/socialSecurity/ssaWageData.d.ts +68 -0
  158. package/dist/socialSecurity/ssaWageData.js +306 -0
  159. package/dist/socialSecurity/survivorBenefit.d.ts +61 -0
  160. package/dist/socialSecurity/survivorBenefit.js +61 -0
  161. package/dist/spending/abw.d.ts +57 -0
  162. package/dist/spending/abw.js +80 -0
  163. package/dist/spending/flexibleGoals.d.ts +62 -0
  164. package/dist/spending/flexibleGoals.js +136 -0
  165. package/dist/spending/guardrails.d.ts +78 -0
  166. package/dist/spending/guardrails.js +100 -0
  167. package/dist/spending/layers.d.ts +81 -0
  168. package/dist/spending/layers.js +67 -0
  169. package/dist/spending/shapePresets.d.ts +42 -0
  170. package/dist/spending/shapePresets.js +77 -0
  171. package/dist/strategies/accountEligibility.d.ts +81 -0
  172. package/dist/strategies/accountEligibility.js +98 -0
  173. package/dist/strategies/inheritedIra.d.ts +45 -0
  174. package/dist/strategies/inheritedIra.js +43 -0
  175. package/dist/strategies/iraBasis.d.ts +48 -0
  176. package/dist/strategies/iraBasis.js +46 -0
  177. package/dist/strategies/optimizer.d.ts +307 -0
  178. package/dist/strategies/optimizer.js +551 -0
  179. package/dist/strategies/rothBasis.d.ts +67 -0
  180. package/dist/strategies/rothBasis.js +83 -0
  181. package/dist/strategies/rothConversion.d.ts +55 -0
  182. package/dist/strategies/rothConversion.js +83 -0
  183. package/dist/strategies/sepp.d.ts +38 -0
  184. package/dist/strategies/sepp.js +53 -0
  185. package/dist/tax/aca.d.ts +40 -0
  186. package/dist/tax/aca.js +82 -0
  187. package/dist/tax/federalTax.d.ts +110 -0
  188. package/dist/tax/federalTax.js +249 -0
  189. package/dist/tax/medicare.d.ts +20 -0
  190. package/dist/tax/medicare.js +36 -0
  191. package/dist/tax/propertySale.d.ts +46 -0
  192. package/dist/tax/propertySale.js +35 -0
  193. package/dist/tax/stateTax.d.ts +66 -0
  194. package/dist/tax/stateTax.js +198 -0
  195. package/dist/testing/money.d.ts +7 -0
  196. package/dist/testing/money.js +17 -0
  197. package/dist/testing/planFixtures.d.ts +29 -0
  198. package/dist/testing/planFixtures.js +141 -0
  199. package/package.json +75 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Per-state income tax parameters (V5, "big levers"). One pack per tax year;
3
+ * a data-only refresh each fall. Models brackets, standard deduction, whether
4
+ * the state taxes Social Security, and the major age-based retirement-income
5
+ * exclusion. Per-state credits, local/city taxes, and income phase-outs of
6
+ * exclusions are out of scope.
7
+ *
8
+ * @see DOCS/features/taxes.md
9
+ */
10
+ import type { FilingStatus, PerStatus } from '../types.js';
11
+ /** Marginal bracket: `ratePct` applies to taxable income above `lowerBound`. */
12
+ export interface StateTaxBracket {
13
+ lowerBound: number;
14
+ ratePct: number;
15
+ }
16
+ /**
17
+ * Exclusion of retirement income from state taxable income.
18
+ * - none: the state taxes retirement income like other ordinary income.
19
+ * - full: retirement income is entirely exempt (subject to minAge if set).
20
+ * - capped: each age-eligible person excludes up to capPerPerson.
21
+ */
22
+ export interface StateRetirementExclusion {
23
+ kind: 'none' | 'full' | 'capped';
24
+ /** Annual cap per eligible person (kind 'capped'), in pack-year dollars. */
25
+ capPerPerson?: number;
26
+ /** Exclusion applies only to people at or above this age. */
27
+ minAge?: number;
28
+ }
29
+ export interface StateTaxParams {
30
+ /** Two-letter code, e.g. 'KY'. */
31
+ code: string;
32
+ name: string;
33
+ /** Nine states levy no broad income tax → everything below is ignored. */
34
+ hasIncomeTax: boolean;
35
+ /** ~9 states tax Social Security benefits (to the federally taxable extent). */
36
+ taxesSocialSecurity: boolean;
37
+ /** Most states tax long-term gains as ordinary income. */
38
+ capitalGainsAsOrdinary: boolean;
39
+ /**
40
+ * Percent of modeled realized/net capital gain included in the state base.
41
+ * Defaults to 100 when `capitalGainsAsOrdinary` is true, else 0.
42
+ */
43
+ capitalGainsTaxablePct?: number;
44
+ /**
45
+ * Whether the state follows the federal capital-loss carryforward netting
46
+ * already applied by the ledger, or taxes only current-year realized gains
47
+ * and ignores prior-year carryforward offsets (PA-style planning case).
48
+ */
49
+ capitalLossCarryforwardConformity?: 'federal' | 'currentYearOnly';
50
+ /** Source notes for state capital-gain/conformity treatment. */
51
+ capitalGainsNotes?: string;
52
+ capitalGainsSources?: string[];
53
+ standardDeduction: PerStatus<number>;
54
+ brackets: PerStatus<StateTaxBracket[]>;
55
+ /** Private pensions, annuities, traditional IRA/401(k), RMD, SEPP, and inherited distributions. */
56
+ retirementPrivate: StateRetirementExclusion;
57
+ /** Public civil-service / military pensions, where state law separates them. */
58
+ retirementPublic: StateRetirementExclusion;
59
+ /**
60
+ * True when the state has one all-retirement rule copied into both buckets
61
+ * (no separate public-pension law): a capped exclusion then applies once to
62
+ * the combined retirement income, never once per bucket.
63
+ */
64
+ retirementRuleShared?: boolean;
65
+ /** Citation / modeled simplifications for the data-refresh workstream. */
66
+ notes?: string;
67
+ }
68
+ export interface StateTaxPack {
69
+ year: number;
70
+ /** Keyed by two-letter code. Absent states fall back to the flat override. */
71
+ states: Record<string, StateTaxParams>;
72
+ }
73
+ export type { FilingStatus };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Per-state income tax parameters (V5, "big levers"). One pack per tax year;
3
+ * a data-only refresh each fall. Models brackets, standard deduction, whether
4
+ * the state taxes Social Security, and the major age-based retirement-income
5
+ * exclusion. Per-state credits, local/city taxes, and income phase-outs of
6
+ * exclusions are out of scope.
7
+ *
8
+ * @see DOCS/features/taxes.md
9
+ */
10
+ export {};
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Annual parameter pack shape. One pack per tax year; values come from IRS,
3
+ * SSA, CMS, and HHS releases each fall. Refreshing a year is a data-only PR.
4
+ *
5
+ * Sources and the meaning of each figure: DOCS/domain/domain-rules-reference.md.
6
+ */
7
+ export type FilingStatus = 'single' | 'marriedFilingJointly';
8
+ /** One point on the TIPS par real-yield curve. */
9
+ export interface RealYieldCurvePoint {
10
+ maturityYears: number;
11
+ /** Par real yield, percent per year (above inflation). */
12
+ realYieldPct: number;
13
+ }
14
+ /**
15
+ * TIPS real-yield curve snapshot: the discount curve for ladder quotes and
16
+ * essential-spending present values. Points sorted ascending by maturity;
17
+ * the ladder engine interpolates linearly and holds the endpoints flat.
18
+ */
19
+ export interface RealYieldCurve {
20
+ /** Publication date of the yields (shown as "curve as of" in the UI). */
21
+ asOfIso: string;
22
+ /** Human-readable provenance of the snapshot. */
23
+ source: string;
24
+ points: RealYieldCurvePoint[];
25
+ }
26
+ /** Marginal bracket: `ratePct` applies to taxable income above `lowerBound`. */
27
+ export interface TaxBracket {
28
+ lowerBound: number;
29
+ ratePct: number;
30
+ }
31
+ export interface PerStatus<T> {
32
+ single: T;
33
+ marriedFilingJointly: T;
34
+ }
35
+ export interface IrmaaTier {
36
+ /** Tier applies when MAGI (2-year lookback) exceeds this. */
37
+ magiOver: PerStatus<number>;
38
+ /**
39
+ * Statutory share of program cost (35/50/65/80/85). Standard premium is 25%,
40
+ * so Part B total = partBStandardMonthly * applicablePct / 25.
41
+ */
42
+ applicablePct: number;
43
+ /** Published monthly Part D surcharge. null = not yet verified for this year. */
44
+ partDSurchargeMonthly: number | null;
45
+ }
46
+ export interface ParameterPack {
47
+ year: number;
48
+ federalTax: {
49
+ brackets: PerStatus<TaxBracket[]>;
50
+ standardDeduction: PerStatus<number>;
51
+ /** Additional standard deduction at 65+ (per qualifying person for MFJ). */
52
+ age65Addition: PerStatus<number>;
53
+ /**
54
+ * SALT (state + local + property tax) itemized-deduction cap. OBBBA raised it
55
+ * to $40k for 2025 (+1%/yr through 2029, reverting to $10k in 2030). The
56
+ * high-income (>$500k MAGI) phase-out toward a $10k floor is not modeled.
57
+ */
58
+ saltCap: number;
59
+ /**
60
+ * Net capital loss deductible against ordinary income per year ($3,000;
61
+ * $1,500 MFS — not modeled). Statutory, never indexed since 1978; the
62
+ * remainder carries forward indefinitely.
63
+ */
64
+ capitalLossOrdinaryOffsetLimit: number;
65
+ /**
66
+ * IRC §121 primary-residence gain exclusion caps ($250k single / $500k
67
+ * joint). Statutory, unchanged since 1997 and never inflation-indexed —
68
+ * do not apply limitScale/inflation projection to these.
69
+ */
70
+ section121Exclusion: PerStatus<number>;
71
+ /** OBBBA senior deduction (2025–2028): per person 65+, MAGI phase-out. */
72
+ seniorDeduction: {
73
+ amountPerPerson: number;
74
+ magiPhaseOutStart: PerStatus<number>;
75
+ phaseOutRatePct: number;
76
+ lastApplicableYear: number;
77
+ } | null;
78
+ /** Planning-grade individual AMT screen (Form 6251 thresholds). */
79
+ amt: {
80
+ exemption: PerStatus<number>;
81
+ exemptionPhaseOutStart: PerStatus<number>;
82
+ exemptionPhaseOutRatePct: number;
83
+ rate28StartsAbove: number;
84
+ rate26Pct: number;
85
+ rate28Pct: number;
86
+ };
87
+ };
88
+ capitalGains: {
89
+ /** Taxable-income thresholds where the 15% and 20% LTCG rates begin. */
90
+ rate15StartsAbove: PerStatus<number>;
91
+ rate20StartsAbove: PerStatus<number>;
92
+ };
93
+ niit: {
94
+ ratePct: number;
95
+ /** MAGI thresholds; statutorily unindexed. */
96
+ magiThreshold: PerStatus<number>;
97
+ };
98
+ /** Provisional-income thresholds for Social Security benefit taxation (unindexed). */
99
+ ssBenefitTaxation: {
100
+ tier50Start: PerStatus<number>;
101
+ tier85Start: PerStatus<number>;
102
+ };
103
+ contributionLimits: {
104
+ employee401k: number;
105
+ catchUp50: number;
106
+ /** SECURE 2.0 ages 60–63 "super" catch-up (replaces catchUp50). */
107
+ superCatchUp60to63: number;
108
+ /** Prior-year FICA wages above this force catch-ups to be Roth. */
109
+ rothCatchUpWageThreshold: number;
110
+ ira: number;
111
+ iraCatchUp50: number;
112
+ hsaSelfOnly: number;
113
+ hsaFamily: number;
114
+ hsaCatchUp55: number;
115
+ /** IRS Section 415(c)(1)(A) defined contribution total additions limit. */
116
+ section415cLimit: number;
117
+ };
118
+ rmd: {
119
+ /** IRS Uniform Lifetime Table (Pub 590-B, 2022+): age -> divisor. */
120
+ uniformLifetimeTable: Record<number, number>;
121
+ /** Annual QCD exclusion limit. */
122
+ qcdAnnualLimit: number;
123
+ };
124
+ /** Fixed-annuity purchase planning parameters (guaranteed-income depth). */
125
+ annuities: {
126
+ /**
127
+ * QLAC (Qualified Longevity Annuity Contract) maximum premium that a
128
+ * qualified purchase may exclude from the owner's RMD balances. SECURE 2.0
129
+ * removed the 25%-of-balance limit, leaving a flat, inflation-indexed dollar
130
+ * cap. Premium above the cap is not QLAC-eligible.
131
+ */
132
+ qlacPremiumCap: number;
133
+ /**
134
+ * IRS Pub 939 Table V (ordinary life annuity, one life) expected-return
135
+ * multiples: age at the annuity starting date → remaining life expectancy
136
+ * in years. Used to derive the non-qualified exclusion ratio
137
+ * (investment-in-contract ÷ expected return). Ages between table entries
138
+ * interpolate linearly; ages outside the table clamp to the endpoints.
139
+ */
140
+ expectedReturnMultiples: Record<number, number>;
141
+ };
142
+ /** HECM (reverse mortgage) planning parameters (annuity-pension-and-home-equity decisions). */
143
+ hecm: {
144
+ /**
145
+ * Published principal-limit factors (percent of home value available as
146
+ * the initial principal limit) by age of the youngest borrower, at the
147
+ * expected rate below. Used only as the planning default when the user
148
+ * has not entered a lender-quoted line size; ages between entries
149
+ * interpolate linearly and ages outside clamp to the endpoints.
150
+ */
151
+ principalLimitFactorPctByAge: Record<number, number>;
152
+ /** Expected interest rate the factor table was published at (context for the UI). */
153
+ plfExpectedRatePct: number;
154
+ /** Planning default for line/loan growth (note rate + annual MIP). */
155
+ defaultGrowthRatePct: number;
156
+ };
157
+ medicare: {
158
+ partBStandardMonthly: number;
159
+ /** Ascending by threshold. */
160
+ irmaaTiers: IrmaaTier[];
161
+ };
162
+ socialSecurity: {
163
+ colaPct: number;
164
+ taxableWageBase: number;
165
+ /** Annual exempt amounts for the retirement earnings test. */
166
+ earningsTestBelowFraAnnual: number;
167
+ earningsTestFraYearAnnual: number;
168
+ /**
169
+ * Substantial Gainful Activity (SGA) monthly earnings limit for SSDI: wages
170
+ * above this generally stop disability benefits. (Statutorily blind SGA is
171
+ * higher; not modeled.) @see DOCS/domain/domain-rules-reference.md §4 SSDI
172
+ */
173
+ sgaMonthlyNonBlind: number;
174
+ /**
175
+ * Employee-side OASDI payroll-tax rate (%). The employer pays the same again;
176
+ * the self-employed pay double (12.4%). Used only for the "what you paid in
177
+ * vs. what you get back" education readout (no working-years tax is modeled in
178
+ * the projection). Statutory since 1990; applied uniformly over the career
179
+ * (historical rate drift pre-1990 is a documented simplification).
180
+ */
181
+ oasdiEmployeeRatePct: number;
182
+ };
183
+ /** HHS poverty guideline (48 contiguous states) used for ACA in this coverage year. */
184
+ federalPovertyLine: {
185
+ firstPerson: number;
186
+ perAdditionalPerson: number;
187
+ };
188
+ /** ACA premium-tax-credit scale (post-2025: enhanced credits expired, 400% FPL cliff restored). */
189
+ aca: {
190
+ /**
191
+ * Expected-contribution percentage of MAGI, piecewise-linear between
192
+ * breakpoints sorted by fplPct. Income above maxFplPctForCredit gets no
193
+ * credit at all (the cliff).
194
+ */
195
+ applicablePctBreakpoints: Array<{
196
+ fplPct: number;
197
+ applicablePct: number;
198
+ }>;
199
+ maxFplPctForCredit: number;
200
+ };
201
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Annual parameter pack shape. One pack per tax year; values come from IRS,
3
+ * SSA, CMS, and HHS releases each fall. Refreshing a year is a data-only PR.
4
+ *
5
+ * Sources and the meaning of each figure: DOCS/domain/domain-rules-reference.md.
6
+ */
7
+ export {};
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Annuity payout forms: payment continuation and exclusion-ratio inputs per
3
+ * form (annuity-pension-and-home-equity decisions, step 1).
4
+ *
5
+ * The IRS Pub 939 General Rule prices a non-qualified annuity's tax-free share
6
+ * as investment-in-contract ÷ expected return, where expected return = annual
7
+ * payment × an expected-return multiple. The multiple depends on the payout
8
+ * form:
9
+ * - life-only: Pub 939 Table V by age at the starting date (embedded in the
10
+ * parameter pack — the shipped guaranteed-income behavior);
11
+ * - life with N years certain: Pub 939 adjusts the investment for the value
12
+ * of the refund/period-certain feature (Tables III/VII). We approximate the
13
+ * same economics from the payment side: the expected payment stream can
14
+ * never be shorter than the guarantee, so the multiple is
15
+ * max(Table V multiple, N). Documented planning-grade approximation;
16
+ * - joint & survivor: Pub 939 uses Tables VI/VIA (joint last-survivor and
17
+ * joint-life multiples). We decompose by expectation instead — exactly the
18
+ * stream the ledger pays: the full payment for the owner's expected
19
+ * lifetime (Table V) plus the survivor share for the years the joint
20
+ * annuitant is expected to outlive the owner (joint last-survivor
21
+ * expectancy from the SSA-derived mortality model, per-sex where Pub 939's
22
+ * tables are unisex). Same General-Rule method, sourced from the tables
23
+ * already in the repo.
24
+ *
25
+ * In all forms the exclusion ratio is fixed at the starting date and the
26
+ * excludable share of each payment continues to a survivor/beneficiary until
27
+ * the investment is recovered (IRS Pub 575/939 treatment).
28
+ */
29
+ import type { Account, AnnuityPayoutForm, Person } from '../model/plan.js';
30
+ import type { ParameterPack } from '../params/types.js';
31
+ type AnnuityAccount = Extract<Account, {
32
+ type: 'annuity';
33
+ }>;
34
+ /** The payout form in effect; absent = life-only (legacy behavior). */
35
+ export declare function annuityPayoutForm(account: AnnuityAccount): AnnuityPayoutForm;
36
+ /**
37
+ * Expected-return multiple (years of the FULL annual payment) for the
38
+ * exclusion ratio under the account's payout form. `owner` is the annuitant;
39
+ * `joint` is the other household member for a joint-and-survivor form (the
40
+ * form is validated to require a two-person household).
41
+ */
42
+ export declare function annuityExclusionMultiple(pack: ParameterPack, account: AnnuityAccount, owner: Person, joint: Person | undefined): number;
43
+ /**
44
+ * Fraction of the full payment paid this year under the payout form.
45
+ * - life-only: 1 while the owner is alive, else 0;
46
+ * - period certain: 1 while the owner is alive; after the owner's death the
47
+ * remaining guaranteed years continue to the household (any member alive).
48
+ * Documented simplification: if the whole household dies inside the
49
+ * guarantee window, the remaining certain payments (which a real contract
50
+ * would keep paying to a beneficiary or the estate) are not modeled — the
51
+ * ledger has no post-household cash-flow path, so a long guarantee's
52
+ * estate value is understated in that case (domain rules §19);
53
+ * - joint & survivor: 1 while the owner is alive, survivorPct% while the
54
+ * joint annuitant survives them.
55
+ */
56
+ export declare function annuityPayoutFraction(form: AnnuityPayoutForm, state: {
57
+ ownerAlive: boolean;
58
+ otherAlive: boolean;
59
+ anyAlive: boolean;
60
+ yearsSinceStart: number;
61
+ }): number;
62
+ export {};
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Annuity payout forms: payment continuation and exclusion-ratio inputs per
3
+ * form (annuity-pension-and-home-equity decisions, step 1).
4
+ *
5
+ * The IRS Pub 939 General Rule prices a non-qualified annuity's tax-free share
6
+ * as investment-in-contract ÷ expected return, where expected return = annual
7
+ * payment × an expected-return multiple. The multiple depends on the payout
8
+ * form:
9
+ * - life-only: Pub 939 Table V by age at the starting date (embedded in the
10
+ * parameter pack — the shipped guaranteed-income behavior);
11
+ * - life with N years certain: Pub 939 adjusts the investment for the value
12
+ * of the refund/period-certain feature (Tables III/VII). We approximate the
13
+ * same economics from the payment side: the expected payment stream can
14
+ * never be shorter than the guarantee, so the multiple is
15
+ * max(Table V multiple, N). Documented planning-grade approximation;
16
+ * - joint & survivor: Pub 939 uses Tables VI/VIA (joint last-survivor and
17
+ * joint-life multiples). We decompose by expectation instead — exactly the
18
+ * stream the ledger pays: the full payment for the owner's expected
19
+ * lifetime (Table V) plus the survivor share for the years the joint
20
+ * annuitant is expected to outlive the owner (joint last-survivor
21
+ * expectancy from the SSA-derived mortality model, per-sex where Pub 939's
22
+ * tables are unisex). Same General-Rule method, sourced from the tables
23
+ * already in the repo.
24
+ *
25
+ * In all forms the exclusion ratio is fixed at the starting date and the
26
+ * excludable share of each payment continues to a survivor/beneficiary until
27
+ * the investment is recovered (IRS Pub 575/939 treatment).
28
+ */
29
+ import { annuityExpectedReturnMultiple } from '../params/index.js';
30
+ import { jointLastSurvivorExpectancy } from '../montecarlo/mortality.js';
31
+ /** The payout form in effect; absent = life-only (legacy behavior). */
32
+ export function annuityPayoutForm(account) {
33
+ return account.payoutForm ?? { kind: 'lifeOnly' };
34
+ }
35
+ /**
36
+ * Expected-return multiple (years of the FULL annual payment) for the
37
+ * exclusion ratio under the account's payout form. `owner` is the annuitant;
38
+ * `joint` is the other household member for a joint-and-survivor form (the
39
+ * form is validated to require a two-person household).
40
+ */
41
+ export function annuityExclusionMultiple(pack, account, owner, joint) {
42
+ const form = annuityPayoutForm(account);
43
+ const lifeMultiple = annuityExpectedReturnMultiple(pack, account.startAge);
44
+ if (form.kind === 'lifeOnly')
45
+ return lifeMultiple;
46
+ if (form.kind === 'periodCertain') {
47
+ // The guarantee floors the expected payment years at the certain period.
48
+ return Math.max(lifeMultiple, form.certainYears);
49
+ }
50
+ if (!joint)
51
+ return lifeMultiple;
52
+ // Joint & survivor: full payment for the owner's expected lifetime, then
53
+ // survivorPct of it for the expected years the joint annuitant survives the
54
+ // owner. Ages at the starting date: the joint annuitant's age when the owner
55
+ // attains startAge.
56
+ const ownerBirthYear = Number(owner.dob.slice(0, 4));
57
+ const jointBirthYear = Number(joint.dob.slice(0, 4));
58
+ const jointAgeAtStart = account.startAge + (ownerBirthYear - jointBirthYear);
59
+ const jointExpectancy = jointLastSurvivorExpectancy(account.startAge, owner.sex, Math.max(0, jointAgeAtStart), joint.sex);
60
+ const survivorYears = Math.max(0, jointExpectancy - lifeMultiple);
61
+ return lifeMultiple + (form.survivorPct / 100) * survivorYears;
62
+ }
63
+ /**
64
+ * Fraction of the full payment paid this year under the payout form.
65
+ * - life-only: 1 while the owner is alive, else 0;
66
+ * - period certain: 1 while the owner is alive; after the owner's death the
67
+ * remaining guaranteed years continue to the household (any member alive).
68
+ * Documented simplification: if the whole household dies inside the
69
+ * guarantee window, the remaining certain payments (which a real contract
70
+ * would keep paying to a beneficiary or the estate) are not modeled — the
71
+ * ledger has no post-household cash-flow path, so a long guarantee's
72
+ * estate value is understated in that case (domain rules §19);
73
+ * - joint & survivor: 1 while the owner is alive, survivorPct% while the
74
+ * joint annuitant survives them.
75
+ */
76
+ export function annuityPayoutFraction(form, state) {
77
+ if (form.kind === 'lifeOnly')
78
+ return state.ownerAlive ? 1 : 0;
79
+ if (form.kind === 'periodCertain') {
80
+ if (state.ownerAlive)
81
+ return 1;
82
+ return state.yearsSinceStart < form.certainYears && state.anyAlive ? 1 : 0;
83
+ }
84
+ if (state.ownerAlive)
85
+ return 1;
86
+ return state.otherAlive ? form.survivorPct / 100 : 0;
87
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Projection summaries and the convert-vs-don't comparison (roadmap V3).
3
+ * Scenario diffing generalizes in V4; this covers the headline question.
4
+ */
5
+ import type { Plan } from '../model/plan.js';
6
+ import { type SimulateOptions } from './simulate.js';
7
+ import type { ProjectionResult } from './types.js';
8
+ export type EstateDestination = 'spouse' | 'nonSpouse' | 'charity';
9
+ /** How one account's ending balance is discounted on the way to heirs/charity. */
10
+ export interface EstateAccountBreakdown {
11
+ accountId: string;
12
+ name: string;
13
+ category: 'cash' | 'taxable' | 'traditional' | 'roth' | 'hsa';
14
+ grossBalance: number;
15
+ destination: EstateDestination;
16
+ /** Pre-tax portion exposed to heir income tax (traditional net of 8606 basis; non-spouse HSA). */
17
+ taxablePretaxBase: number;
18
+ heirTaxRatePct: number;
19
+ /** Portion passing to charity, untaxed. */
20
+ charityAmount: number;
21
+ heirTax: number;
22
+ /** grossBalance − charityAmount − heirTax. */
23
+ netToHeirs: number;
24
+ }
25
+ export interface ProjectionSummary {
26
+ /** Sum of taxes and penalties over the whole projection. */
27
+ lifetimeTaxesAndPenalties: number;
28
+ lifetimeRothConversions: number;
29
+ endingInvestable: number;
30
+ endingNetWorth: number;
31
+ /**
32
+ * Ending net worth net of the income tax heirs owe on inherited pre-tax
33
+ * (traditional) balances, at the plan's assumed heir tax rate. Nondeductible
34
+ * (after-tax) IRA basis remaining at the horizon is excluded — the heir
35
+ * inherits it tax-free and files a separate Form 8606. Roth, taxable
36
+ * (stepped-up at death), cash, and property are treated as passing through
37
+ * untaxed — the standard simplification for an estate comparison. An HSA
38
+ * left to a non-spouse beneficiary is fully taxable in the death year (IRC
39
+ * §223(f)(8)(B)), so its ending balance is also taxed at the heir rate; an
40
+ * HSA inherited by a spouse (or with no beneficiary set) passes untaxed.
41
+ */
42
+ endingAfterTaxEstate: number;
43
+ /** Total heir income tax discounted from the estate (sum of the breakdown's heirTax). */
44
+ endingEstateHeirTax: number;
45
+ /** Total passing to charity untaxed (sum of the breakdown's charityAmount). */
46
+ endingEstateToCharity: number;
47
+ /** Per-account estate discounting, so the summary can explain each class's haircut. */
48
+ estateBreakdown: EstateAccountBreakdown[];
49
+ /** End-of-plan balance by account category. */
50
+ endingByCategory: {
51
+ cash: number;
52
+ taxable: number;
53
+ traditional: number;
54
+ roth: number;
55
+ hsa: number;
56
+ };
57
+ depletionYear: number | null;
58
+ warnings: string[];
59
+ savingsRates: Array<{
60
+ year: number;
61
+ ratePct: number;
62
+ }>;
63
+ averagePreRetirementSavingsRatePct: number;
64
+ fiNumber: number;
65
+ fiYear: number | null;
66
+ fiAge: number | null;
67
+ coastFireNumber: number;
68
+ }
69
+ export declare function summarizeProjection(plan: Plan, result: ProjectionResult): ProjectionSummary;
70
+ export interface RothComparison {
71
+ withConversions: ProjectionSummary;
72
+ withoutConversions: ProjectionSummary;
73
+ }
74
+ /** Runs the plan as-is and with conversions disabled, for side-by-side display. */
75
+ export declare function compareRothConversion(plan: Plan, opts: SimulateOptions): RothComparison;
76
+ export interface LtcStressComparison {
77
+ /** No care episode, LTC policies held out — the premium-neutral baseline. */
78
+ noCare: ProjectionSummary;
79
+ /** The care episode occurs and is fully self-funded (LTC policies removed). */
80
+ careUninsured: ProjectionSummary;
81
+ /** The care episode occurs with the plan's LTC policies (premiums + benefits). */
82
+ careInsured: ProjectionSummary;
83
+ hasCareEvents: boolean;
84
+ hasLtcPolicy: boolean;
85
+ }
86
+ /**
87
+ * The "LTC stress" comparison: how a deterministic care episode hits the plan,
88
+ * and what the LTC policy does about it.
89
+ *
90
+ * noCare and careUninsured both hold LTC out, so they differ only by the care
91
+ * episode — `noCare − careUninsured` is the raw care shock, free of premium
92
+ * effects. careInsured adds the policies back, so `careInsured − careUninsured`
93
+ * is the policy's value *net of its premiums* (it can be negative if lifetime
94
+ * premiums exceed the benefits paid). The UI labels it as such.
95
+ */
96
+ export declare function compareLtcStress(plan: Plan, opts: SimulateOptions): LtcStressComparison;