@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,48 @@
1
+ /**
2
+ * Named fixture plans for the ledger-native decision engine test suite
3
+ * (enhancement doc "Testing Strategy"). Test-only helper — not exported from
4
+ * the module index, so it never reaches the app bundle.
5
+ *
6
+ * Fixtures deliberately use flat inflation/returns so tests can reason about
7
+ * bracket, taxability, and basis effects without market noise.
8
+ */
9
+ import { type Plan } from '../model/plan.js';
10
+ import type { SimulateOptions } from '../projection/simulate.js';
11
+ export declare const testIds: () => string;
12
+ export declare const fixedNow: () => Date;
13
+ export declare function validate(plan: Plan): Plan;
14
+ export declare function simOptions(): SimulateOptions;
15
+ /** Mostly traditional with a cash/taxable bridge: ripe for bracket-fill conversions. */
16
+ export declare function tradHeavyPlan(): Plan;
17
+ /** No traditional at all — every conversion candidate should execute $0 and stay non-beneficial. */
18
+ export declare function noTraditionalPlan(): Plan;
19
+ /** Inherited-only traditional: distributable under the 10-year rule but never convertible. */
20
+ export declare function inheritedOnlyPlan(): Plan;
21
+ /** Own plus inherited traditional: only the own balance is convertible. */
22
+ export declare function mixedTraditionalPlan(): Plan;
23
+ /** Late-career accumulator with scheduled contributions and an employer match. */
24
+ export declare function accumulatorPlan(): Plan;
25
+ /**
26
+ * Early retiree bridging on a taxable brokerage: spending is funded by selling
27
+ * shares, so candidate evaluation must price the realized gains implied by the
28
+ * account's basis ratio.
29
+ */
30
+ export declare function taxableBridgePlan(basis: 'high' | 'low'): Plan;
31
+ /**
32
+ * Social Security taxability edge: SS starts mid-horizon (claim at 70), so an
33
+ * identical conversion is priced differently before vs during benefit years —
34
+ * the provisional-income feedback only the exact ledger knows.
35
+ */
36
+ export declare function ssTaxabilityPlan(): Plan;
37
+ /** Lumpy one-time ordinary income in 2028 that stacks under any conversion that year. */
38
+ export declare function oneTimeIncomePlan(): Plan;
39
+ /**
40
+ * Asset-location fixture (asset-allocation-and-return-model-v2, step 5): a
41
+ * bond-heavy allocated brokerage next to a stock-heavy-capable traditional and
42
+ * Roth, where *where* classes are held changes the after-tax estate — bonds
43
+ * throw ordinary interest in taxable and inflate the heir-taxed traditional
44
+ * balance when they grow there instead.
45
+ */
46
+ export declare function assetLocationPlan(): Plan;
47
+ /** Married couple where the second spouse's earlier planning age creates survivor years. */
48
+ export declare function survivorPlan(): Plan;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Named fixture plans for the ledger-native decision engine test suite
3
+ * (enhancement doc "Testing Strategy"). Test-only helper — not exported from
4
+ * the module index, so it never reaches the app bundle.
5
+ *
6
+ * Fixtures deliberately use flat inflation/returns so tests can reason about
7
+ * bracket, taxability, and basis effects without market noise.
8
+ */
9
+ import { createEmptyPlan, parsePlan } from '../model/plan.js';
10
+ import { createFederalTaxCalculator } from '../tax/federalTax.js';
11
+ let counter = 0;
12
+ export const testIds = () => `dec-${++counter}`;
13
+ export const fixedNow = () => new Date('2026-06-11T00:00:00.000Z');
14
+ export function validate(plan) {
15
+ const parsed = parsePlan(plan);
16
+ if (!parsed.ok)
17
+ throw new Error(parsed.issues.join('; '));
18
+ return parsed.plan;
19
+ }
20
+ export function simOptions() {
21
+ return { startYear: 2026, taxCalculator: createFederalTaxCalculator() };
22
+ }
23
+ function base(personOverrides = {}) {
24
+ const plan = createEmptyPlan({ newId: testIds, now: fixedNow });
25
+ plan.household.people[0] = {
26
+ id: 'p1',
27
+ name: 'Pat',
28
+ dob: '1958-06-15', // 68 in 2026; RMDs from 73
29
+ sex: 'average',
30
+ retirementAge: 65,
31
+ longevity: { planningAge: 85, source: 'manual' },
32
+ ...personOverrides,
33
+ };
34
+ plan.assumptions.inflationPct = 0;
35
+ plan.assumptions.healthcareExtraInflationPct = 0;
36
+ plan.assumptions.defaultReturnPct = 0;
37
+ plan.assumptions.stateEffectiveTaxPct = 0;
38
+ plan.assumptions.heirTaxRatePct = 25;
39
+ plan.expenses.baseAnnual = 0;
40
+ plan.expenses.healthcare = { pre65MonthlyPremiumPerPerson: 0, applyAcaCredit: false, medicareExtrasMonthlyPerPerson: 0 };
41
+ return plan;
42
+ }
43
+ /** Mostly traditional with a cash/taxable bridge: ripe for bracket-fill conversions. */
44
+ export function tradHeavyPlan() {
45
+ const plan = base();
46
+ plan.assumptions.defaultReturnPct = 4;
47
+ plan.expenses.baseAnnual = 45_000;
48
+ plan.accounts = [
49
+ { type: 'traditional', id: testIds(), name: '401k', ownerPersonId: 'p1', annualReturnPct: null, kind: 'ira', balance: 800_000, annualContribution: 0 },
50
+ { type: 'roth', id: testIds(), name: 'Roth', ownerPersonId: 'p1', annualReturnPct: null, kind: 'ira', balance: 0, annualContribution: 0 },
51
+ { type: 'taxable', id: testIds(), name: 'Brokerage', ownerPersonId: null, annualReturnPct: null, balance: 150_000, costBasis: 150_000, annualContribution: 0 },
52
+ { type: 'cash', id: testIds(), name: 'Cash', ownerPersonId: null, annualReturnPct: null, balance: 60_000, annualContribution: 0 },
53
+ ];
54
+ return validate(plan);
55
+ }
56
+ /** No traditional at all — every conversion candidate should execute $0 and stay non-beneficial. */
57
+ export function noTraditionalPlan() {
58
+ const plan = base();
59
+ plan.expenses.baseAnnual = 30_000;
60
+ plan.accounts = [
61
+ { type: 'roth', id: testIds(), name: 'Roth', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 250_000, annualContribution: 0 },
62
+ { type: 'taxable', id: testIds(), name: 'Brokerage', ownerPersonId: null, annualReturnPct: 0, balance: 400_000, costBasis: 400_000, annualContribution: 0 },
63
+ { type: 'cash', id: testIds(), name: 'Cash', ownerPersonId: null, annualReturnPct: 0, balance: 50_000, annualContribution: 0 },
64
+ ];
65
+ return validate(plan);
66
+ }
67
+ /** Inherited-only traditional: distributable under the 10-year rule but never convertible. */
68
+ export function inheritedOnlyPlan() {
69
+ const plan = base({ dob: '1960-01-01', longevity: { planningAge: 70, source: 'manual' } });
70
+ plan.accounts = [
71
+ {
72
+ type: 'traditional',
73
+ id: testIds(),
74
+ name: 'Inherited IRA',
75
+ ownerPersonId: 'p1',
76
+ annualReturnPct: 0,
77
+ kind: 'ira',
78
+ balance: 300_000,
79
+ annualContribution: 0,
80
+ inherited: { ownerDeathYear: 2024, decedentHadStartedRmds: false },
81
+ },
82
+ { type: 'roth', id: testIds(), name: 'Roth', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 0, annualContribution: 0 },
83
+ { type: 'cash', id: testIds(), name: 'Cash', ownerPersonId: null, annualReturnPct: 0, balance: 40_000, annualContribution: 0 },
84
+ ];
85
+ return validate(plan);
86
+ }
87
+ /** Own plus inherited traditional: only the own balance is convertible. */
88
+ export function mixedTraditionalPlan() {
89
+ const plan = inheritedOnlyPlan();
90
+ return validate({
91
+ ...plan,
92
+ accounts: [
93
+ { type: 'traditional', id: testIds(), name: 'Own IRA', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 200_000, annualContribution: 0 },
94
+ ...plan.accounts,
95
+ ],
96
+ });
97
+ }
98
+ /** Late-career accumulator with scheduled contributions and an employer match. */
99
+ export function accumulatorPlan() {
100
+ const plan = base({ dob: '1971-01-01', retirementAge: 62, longevity: { planningAge: 80, source: 'manual' } });
101
+ plan.expenses.baseAnnual = 50_000;
102
+ plan.accounts = [
103
+ {
104
+ type: 'traditional',
105
+ id: testIds(),
106
+ name: '401k',
107
+ ownerPersonId: 'p1',
108
+ annualReturnPct: 0,
109
+ kind: 'employer',
110
+ balance: 100_000,
111
+ annualContribution: 20_000,
112
+ employerMatch: { matchPct: 50, capPctOfPay: 6 },
113
+ },
114
+ { type: 'roth', id: testIds(), name: 'Roth', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 0, annualContribution: 0 },
115
+ { type: 'cash', id: testIds(), name: 'Cash', ownerPersonId: null, annualReturnPct: 0, balance: 50_000, annualContribution: 0 },
116
+ ];
117
+ plan.incomes = [
118
+ { type: 'wages', id: testIds(), personId: 'p1', annualGross: 150_000, endAge: null, realGrowthPct: 0 },
119
+ { type: 'socialSecurity', id: testIds(), personId: 'p1', piaMonthly: 2_500, earnings: null, claimAge: { years: 67, months: 0 } },
120
+ ];
121
+ return validate(plan);
122
+ }
123
+ /**
124
+ * Early retiree bridging on a taxable brokerage: spending is funded by selling
125
+ * shares, so candidate evaluation must price the realized gains implied by the
126
+ * account's basis ratio.
127
+ */
128
+ export function taxableBridgePlan(basis) {
129
+ const plan = base({ dob: '1966-01-01', retirementAge: 60, longevity: { planningAge: 80, source: 'manual' } });
130
+ plan.expenses.baseAnnual = 60_000;
131
+ plan.accounts = [
132
+ { type: 'traditional', id: testIds(), name: 'IRA', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 400_000, annualContribution: 0 },
133
+ { type: 'roth', id: testIds(), name: 'Roth', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 0, annualContribution: 0 },
134
+ {
135
+ type: 'taxable',
136
+ id: testIds(),
137
+ name: 'Brokerage',
138
+ ownerPersonId: null,
139
+ annualReturnPct: 0,
140
+ balance: 600_000,
141
+ costBasis: basis === 'high' ? 580_000 : 120_000,
142
+ annualContribution: 0,
143
+ },
144
+ { type: 'cash', id: testIds(), name: 'Cash', ownerPersonId: null, annualReturnPct: 0, balance: 5_000, annualContribution: 0 },
145
+ ];
146
+ plan.incomes = [
147
+ { type: 'socialSecurity', id: testIds(), personId: 'p1', piaMonthly: 2_800, earnings: null, claimAge: { years: 70, months: 0 } },
148
+ ];
149
+ return validate(plan);
150
+ }
151
+ /**
152
+ * Social Security taxability edge: SS starts mid-horizon (claim at 70), so an
153
+ * identical conversion is priced differently before vs during benefit years —
154
+ * the provisional-income feedback only the exact ledger knows.
155
+ */
156
+ export function ssTaxabilityPlan() {
157
+ const plan = base({ dob: '1964-01-01', retirementAge: 62, longevity: { planningAge: 85, source: 'manual' } });
158
+ plan.expenses.baseAnnual = 30_000;
159
+ plan.accounts = [
160
+ { type: 'traditional', id: testIds(), name: 'IRA', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 400_000, annualContribution: 0 },
161
+ { type: 'roth', id: testIds(), name: 'Roth', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 0, annualContribution: 0 },
162
+ { type: 'cash', id: testIds(), name: 'Cash', ownerPersonId: null, annualReturnPct: 0, balance: 500_000, annualContribution: 0 },
163
+ ];
164
+ plan.incomes = [
165
+ { type: 'socialSecurity', id: testIds(), personId: 'p1', piaMonthly: 3_000, earnings: null, claimAge: { years: 70, months: 0 } },
166
+ ];
167
+ return validate(plan);
168
+ }
169
+ /** Lumpy one-time ordinary income in 2028 that stacks under any conversion that year. */
170
+ export function oneTimeIncomePlan() {
171
+ const plan = base({ dob: '1960-01-01', longevity: { planningAge: 72, source: 'manual' } });
172
+ plan.accounts = [
173
+ { type: 'traditional', id: testIds(), name: 'IRA', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 300_000, annualContribution: 0 },
174
+ { type: 'roth', id: testIds(), name: 'Roth', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 0, annualContribution: 0 },
175
+ { type: 'cash', id: testIds(), name: 'Cash', ownerPersonId: null, annualReturnPct: 0, balance: 250_000, annualContribution: 0 },
176
+ ];
177
+ plan.incomes = [
178
+ { type: 'oneTime', id: testIds(), label: 'Deferred comp payout', year: 2028, amount: 80_000, taxTreatment: 'ordinary' },
179
+ ];
180
+ return validate(plan);
181
+ }
182
+ /**
183
+ * Asset-location fixture (asset-allocation-and-return-model-v2, step 5): a
184
+ * bond-heavy allocated brokerage next to a stock-heavy-capable traditional and
185
+ * Roth, where *where* classes are held changes the after-tax estate — bonds
186
+ * throw ordinary interest in taxable and inflate the heir-taxed traditional
187
+ * balance when they grow there instead.
188
+ */
189
+ export function assetLocationPlan() {
190
+ const plan = base({ dob: '1961-06-15', longevity: { planningAge: 90, source: 'manual' } });
191
+ plan.expenses.baseAnnual = 40_000;
192
+ const weights5050 = { usStocks: 50, intlStocks: 0, bonds: 50, cash: 0 };
193
+ plan.accounts = [
194
+ {
195
+ type: 'taxable',
196
+ id: testIds(),
197
+ name: 'Brokerage',
198
+ ownerPersonId: null,
199
+ annualReturnPct: null,
200
+ balance: 500_000,
201
+ costBasis: 350_000,
202
+ annualContribution: 0,
203
+ allocation: { mode: 'static', rebalancing: 'annual', weights: weights5050 },
204
+ },
205
+ {
206
+ type: 'traditional',
207
+ id: testIds(),
208
+ name: 'IRA',
209
+ ownerPersonId: 'p1',
210
+ annualReturnPct: null,
211
+ kind: 'ira',
212
+ balance: 500_000,
213
+ annualContribution: 0,
214
+ allocation: { mode: 'static', rebalancing: 'annual', weights: weights5050 },
215
+ },
216
+ {
217
+ type: 'roth',
218
+ id: testIds(),
219
+ name: 'Roth',
220
+ ownerPersonId: 'p1',
221
+ annualReturnPct: null,
222
+ kind: 'ira',
223
+ balance: 200_000,
224
+ annualContribution: 0,
225
+ allocation: { mode: 'static', rebalancing: 'annual', weights: weights5050 },
226
+ },
227
+ { type: 'cash', id: testIds(), name: 'Cash', ownerPersonId: null, annualReturnPct: 0, balance: 60_000, annualContribution: 0 },
228
+ ];
229
+ plan.incomes = [
230
+ { type: 'socialSecurity', id: testIds(), personId: 'p1', piaMonthly: 2_400, earnings: null, claimAge: { years: 67, months: 0 } },
231
+ ];
232
+ return validate(plan);
233
+ }
234
+ /** Married couple where the second spouse's earlier planning age creates survivor years. */
235
+ export function survivorPlan() {
236
+ const plan = base({ dob: '1960-01-01', longevity: { planningAge: 90, source: 'manual' } });
237
+ plan.household.filingStatus = 'marriedFilingJointly';
238
+ plan.household.people.push({
239
+ id: 'p2',
240
+ name: 'Sam',
241
+ dob: '1958-01-01',
242
+ sex: 'average',
243
+ retirementAge: 65,
244
+ longevity: { planningAge: 78, source: 'manual' },
245
+ });
246
+ plan.expenses.baseAnnual = 40_000;
247
+ plan.accounts = [
248
+ { type: 'traditional', id: testIds(), name: 'IRA', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 600_000, annualContribution: 0 },
249
+ { type: 'roth', id: testIds(), name: 'Roth', ownerPersonId: 'p1', annualReturnPct: 0, kind: 'ira', balance: 0, annualContribution: 0 },
250
+ { type: 'cash', id: testIds(), name: 'Cash', ownerPersonId: null, annualReturnPct: 0, balance: 700_000, annualContribution: 0 },
251
+ ];
252
+ return validate(plan);
253
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Shared exact-ledger candidate evaluator (ledger-native decision engine,
3
+ * Phase 1).
4
+ *
5
+ * One evaluation = one deterministic `simulatePlan` run of the candidate plan,
6
+ * compared against a shared baseline run. All recommendation surfaces (Roth &
7
+ * Tax Optimizer validation, tournaments, Insights previews, local search) go
8
+ * through here so income, capital gains, Social Security taxability, ACA,
9
+ * IRMAA, RMD, and inherited-IRA effects are priced by the ledger exactly once
10
+ * — never re-derived in candidate logic.
11
+ */
12
+ import type { Plan } from '../model/plan.js';
13
+ import { type ProjectionSummary } from '../projection/compare.js';
14
+ import type { ProjectionResult } from '../projection/types.js';
15
+ import type { DecisionCandidate, DecisionContext, ExactDecisionEvaluation } from './types.js';
16
+ export interface EvaluateCandidateOptions {
17
+ /** Dollars around zero treated as matching the baseline. */
18
+ neutralToleranceDollars?: number;
19
+ /** Minimum total requested conversions before execution-ratio diagnostics matter. */
20
+ minimumRequestedConversionDollars?: number;
21
+ /** Absolute shortfall before requested-vs-executed mismatch is material. */
22
+ materialConversionShortfallDollars?: number;
23
+ /** Percent shortfall before requested-vs-executed mismatch is material. */
24
+ materialConversionShortfallPct?: number;
25
+ /**
26
+ * Reuse an already-simulated candidate result instead of re-running the
27
+ * ledger (e.g. the optimizer post-processor simulates schedules itself).
28
+ * The caller is responsible for it matching the candidate exactly.
29
+ */
30
+ candidateResult?: ProjectionResult;
31
+ }
32
+ export declare const DECISION_NEUTRAL_TOLERANCE_DOLLARS = 1;
33
+ export declare const DECISION_MINIMUM_REQUESTED_CONVERSION_DOLLARS = 1;
34
+ export declare const DECISION_MATERIAL_SHORTFALL_DOLLARS = 1000;
35
+ export declare const DECISION_MATERIAL_SHORTFALL_PCT = 0.05;
36
+ /** Years the money lasts: depletion year, or one past the horizon when it never depletes. */
37
+ export declare function lastsThroughYear(result: ProjectionResult): number;
38
+ /** Build a fresh decision context, running the shared baseline once (or reusing a caller's run). */
39
+ export declare function createDecisionContext(plan: Plan, simulateOptions: DecisionContext['simulateOptions'], baseline?: {
40
+ result: ProjectionResult;
41
+ summary?: ProjectionSummary;
42
+ }, taxCalculatorForPlan?: DecisionContext['taxCalculatorForPlan']): DecisionContext;
43
+ /**
44
+ * Materialize the concrete plan a candidate describes: scenario patch first
45
+ * (validated through the plan schema), then any explicit conversion schedule
46
+ * installed as an `optimized` Roth strategy.
47
+ */
48
+ export declare function planForCandidate(plan: Plan, candidate: Pick<DecisionCandidate, 'planPatch' | 'conversions'>): {
49
+ ok: true;
50
+ plan: Plan;
51
+ } | {
52
+ ok: false;
53
+ error: string;
54
+ };
55
+ /** First year the plan's own (non-inherited) traditional balance is exhausted, or null. */
56
+ export declare function findTraditionalDepletionYear(plan: Plan, result: ProjectionResult, toleranceDollars: number): number | null;
57
+ /**
58
+ * Run one candidate through the exact ledger and compare it with the shared
59
+ * baseline. Deterministic: same plan + candidate + options ⇒ same evaluation.
60
+ */
61
+ export declare function evaluateCandidate(ctx: DecisionContext, candidate: DecisionCandidate, options?: EvaluateCandidateOptions): ExactDecisionEvaluation;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Shared exact-ledger candidate evaluator (ledger-native decision engine,
3
+ * Phase 1).
4
+ *
5
+ * One evaluation = one deterministic `simulatePlan` run of the candidate plan,
6
+ * compared against a shared baseline run. All recommendation surfaces (Roth &
7
+ * Tax Optimizer validation, tournaments, Insights previews, local search) go
8
+ * through here so income, capital gains, Social Security taxability, ACA,
9
+ * IRMAA, RMD, and inherited-IRA effects are priced by the ledger exactly once
10
+ * — never re-derived in candidate logic.
11
+ */
12
+ import { applyScenarioPatch } from '../scenarios/scenarios.js';
13
+ import { summarizeProjection } from '../projection/compare.js';
14
+ import { simulatePlan } from '../projection/simulate.js';
15
+ export const DECISION_NEUTRAL_TOLERANCE_DOLLARS = 1;
16
+ export const DECISION_MINIMUM_REQUESTED_CONVERSION_DOLLARS = 1;
17
+ export const DECISION_MATERIAL_SHORTFALL_DOLLARS = 1_000;
18
+ export const DECISION_MATERIAL_SHORTFALL_PCT = 0.05;
19
+ /** Years the money lasts: depletion year, or one past the horizon when it never depletes. */
20
+ export function lastsThroughYear(result) {
21
+ return result.depletionYear ?? result.endYear + 1;
22
+ }
23
+ /** Build a fresh decision context, running the shared baseline once (or reusing a caller's run). */
24
+ export function createDecisionContext(plan, simulateOptions, baseline, taxCalculatorForPlan) {
25
+ const baselineResult = baseline?.result ?? simulatePlan(plan, simulateOptions);
26
+ return {
27
+ plan,
28
+ baselineResult,
29
+ baselineSummary: baseline?.summary ?? summarizeProjection(plan, baselineResult),
30
+ simulateOptions,
31
+ taxCalculatorForPlan,
32
+ };
33
+ }
34
+ /**
35
+ * Materialize the concrete plan a candidate describes: scenario patch first
36
+ * (validated through the plan schema), then any explicit conversion schedule
37
+ * installed as an `optimized` Roth strategy.
38
+ */
39
+ export function planForCandidate(plan, candidate) {
40
+ let candidatePlan = plan;
41
+ if (candidate.planPatch) {
42
+ const applied = applyScenarioPatch(plan, candidate.planPatch);
43
+ if (!applied.ok)
44
+ return { ok: false, error: `This candidate can't be applied to the plan: ${applied.issues.join('; ')}` };
45
+ candidatePlan = applied.plan;
46
+ }
47
+ if (candidate.conversions) {
48
+ candidatePlan = {
49
+ ...candidatePlan,
50
+ strategies: {
51
+ ...candidatePlan.strategies,
52
+ rothConversion: { mode: 'optimized', conversions: candidate.conversions },
53
+ },
54
+ };
55
+ }
56
+ return { ok: true, plan: candidatePlan };
57
+ }
58
+ function aggregateByYear(conversions) {
59
+ const byYear = new Map();
60
+ for (const conversion of conversions) {
61
+ byYear.set(conversion.year, (byYear.get(conversion.year) ?? 0) + conversion.amount);
62
+ }
63
+ return byYear;
64
+ }
65
+ function buildConversionExecution(requested, candidateResult, options) {
66
+ const requestedByYear = aggregateByYear(requested);
67
+ const requestedTotal = requested.reduce((sum, conversion) => sum + conversion.amount, 0);
68
+ const executedTotal = candidateResult.years.reduce((sum, year) => sum + year.rothConversion, 0);
69
+ let firstMateriallyUnexecutedYear = null;
70
+ for (const year of [...requestedByYear.keys()].sort((a, b) => a - b)) {
71
+ const requestedAmount = requestedByYear.get(year) ?? 0;
72
+ const executedAmount = candidateResult.years.find((y) => y.year === year)?.rothConversion ?? 0;
73
+ const materialShortfall = Math.max(options.materialConversionShortfallDollars, requestedAmount * options.materialConversionShortfallPct);
74
+ if (requestedAmount - executedAmount > materialShortfall) {
75
+ firstMateriallyUnexecutedYear = year;
76
+ break;
77
+ }
78
+ }
79
+ return {
80
+ requestedTotal,
81
+ executedTotal,
82
+ executedRatio: requestedTotal > 0 ? Math.min(1, executedTotal / requestedTotal) : 1,
83
+ firstMateriallyUnexecutedYear,
84
+ executedByYear: candidateResult.years
85
+ .filter((year) => year.rothConversion > 1)
86
+ .map((year) => ({ year: year.year, amount: Math.round(year.rothConversion * 100) / 100 })),
87
+ };
88
+ }
89
+ /** First year the plan's own (non-inherited) traditional balance is exhausted, or null. */
90
+ export function findTraditionalDepletionYear(plan, result, toleranceDollars) {
91
+ const ownTraditionalIds = new Set(plan.accounts.filter((account) => account.type === 'traditional' && !account.inherited).map((account) => account.id));
92
+ if (ownTraditionalIds.size === 0)
93
+ return null;
94
+ for (const year of result.years) {
95
+ let balance = 0;
96
+ for (const accountId of ownTraditionalIds)
97
+ balance += year.balances[accountId] ?? 0;
98
+ if (balance <= toleranceDollars)
99
+ return year.year;
100
+ }
101
+ return null;
102
+ }
103
+ function classifyRecommendationState(args) {
104
+ if (args.afterTaxEstateDelta > args.neutralToleranceDollars)
105
+ return 'beneficial';
106
+ if (args.conversionExecution) {
107
+ const { requestedTotal, executedTotal } = args.conversionExecution;
108
+ const materialShortfall = Math.max(args.materialConversionShortfallDollars, requestedTotal * args.materialConversionShortfallPct);
109
+ if (requestedTotal >= args.minimumRequestedConversionDollars && requestedTotal - executedTotal > materialShortfall) {
110
+ return 'diagnostic';
111
+ }
112
+ }
113
+ if (args.afterTaxEstateDelta < -args.neutralToleranceDollars)
114
+ return 'rejected';
115
+ return 'neutral';
116
+ }
117
+ /** Diagnostic-only evaluation for a candidate whose patch failed plan validation. */
118
+ function invalidCandidateEvaluation(ctx, candidate, error) {
119
+ return {
120
+ candidate,
121
+ baselineSummary: ctx.baselineSummary,
122
+ candidateSummary: ctx.baselineSummary,
123
+ candidateResult: ctx.baselineResult,
124
+ deltas: { endingAfterTaxEstate: 0, endingNetWorth: 0, lifetimeTax: 0, moneyLastsYears: 0 },
125
+ conversionExecution: null,
126
+ traditionalDepletionYear: null,
127
+ diagnostics: [error],
128
+ recommendationState: 'diagnostic',
129
+ };
130
+ }
131
+ /**
132
+ * Run one candidate through the exact ledger and compare it with the shared
133
+ * baseline. Deterministic: same plan + candidate + options ⇒ same evaluation.
134
+ */
135
+ export function evaluateCandidate(ctx, candidate, options = {}) {
136
+ const neutralToleranceDollars = options.neutralToleranceDollars ?? DECISION_NEUTRAL_TOLERANCE_DOLLARS;
137
+ const minimumRequestedConversionDollars = options.minimumRequestedConversionDollars ?? DECISION_MINIMUM_REQUESTED_CONVERSION_DOLLARS;
138
+ const materialConversionShortfallDollars = options.materialConversionShortfallDollars ?? DECISION_MATERIAL_SHORTFALL_DOLLARS;
139
+ const materialConversionShortfallPct = options.materialConversionShortfallPct ?? DECISION_MATERIAL_SHORTFALL_PCT;
140
+ const built = planForCandidate(ctx.plan, candidate);
141
+ if (!built.ok)
142
+ return invalidCandidateEvaluation(ctx, candidate, built.error);
143
+ // A patch may change tax assumptions (e.g. a relocation candidate clearing
144
+ // the flat state-rate override); when the context carries a per-plan tax
145
+ // stack factory, price the candidate with its own stack.
146
+ const candidateSimulateOptions = ctx.taxCalculatorForPlan
147
+ ? { ...ctx.simulateOptions, taxCalculator: ctx.taxCalculatorForPlan(built.plan) }
148
+ : ctx.simulateOptions;
149
+ const candidateResult = options.candidateResult ?? simulatePlan(built.plan, candidateSimulateOptions);
150
+ const candidateSummary = summarizeProjection(built.plan, candidateResult);
151
+ const conversionExecution = candidate.conversions
152
+ ? buildConversionExecution(candidate.conversions, candidateResult, {
153
+ materialConversionShortfallDollars,
154
+ materialConversionShortfallPct,
155
+ })
156
+ : null;
157
+ const deltas = {
158
+ endingAfterTaxEstate: candidateSummary.endingAfterTaxEstate - ctx.baselineSummary.endingAfterTaxEstate,
159
+ endingNetWorth: candidateSummary.endingNetWorth - ctx.baselineSummary.endingNetWorth,
160
+ lifetimeTax: candidateSummary.lifetimeTaxesAndPenalties - ctx.baselineSummary.lifetimeTaxesAndPenalties,
161
+ moneyLastsYears: lastsThroughYear(candidateResult) - lastsThroughYear(ctx.baselineResult),
162
+ };
163
+ const diagnostics = [];
164
+ if (conversionExecution && conversionExecution.firstMateriallyUnexecutedYear !== null) {
165
+ diagnostics.push(`Your plan could not execute the requested conversion in ${conversionExecution.firstMateriallyUnexecutedYear}: ` +
166
+ `requested $${Math.round(conversionExecution.requestedTotal).toLocaleString()} in total, ` +
167
+ `executed $${Math.round(conversionExecution.executedTotal).toLocaleString()}.`);
168
+ }
169
+ if (deltas.moneyLastsYears < 0) {
170
+ diagnostics.push(`Money lasts ${-deltas.moneyLastsYears} year(s) less than the baseline.`);
171
+ }
172
+ if (candidateResult.depletionYear !== null && ctx.baselineResult.depletionYear === null) {
173
+ diagnostics.push(`Introduces portfolio depletion in ${candidateResult.depletionYear}.`);
174
+ }
175
+ return {
176
+ candidate,
177
+ baselineSummary: ctx.baselineSummary,
178
+ candidateSummary,
179
+ candidateResult,
180
+ deltas,
181
+ conversionExecution,
182
+ traditionalDepletionYear: findTraditionalDepletionYear(built.plan, candidateResult, neutralToleranceDollars),
183
+ diagnostics,
184
+ recommendationState: classifyRecommendationState({
185
+ afterTaxEstateDelta: deltas.endingAfterTaxEstate,
186
+ conversionExecution,
187
+ neutralToleranceDollars,
188
+ minimumRequestedConversionDollars,
189
+ materialConversionShortfallDollars,
190
+ materialConversionShortfallPct,
191
+ }),
192
+ };
193
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Normalized candidate generators (ledger-native decision engine, Phase 2).
3
+ *
4
+ * Every recommendation source is a `CandidateGenerator`: pure, bounded, and
5
+ * fast — no simulate() calls, just concrete plan patches or conversion
6
+ * schedules. Generation never decides whether something is recommended; the
7
+ * exact-ledger evaluation does.
8
+ */
9
+ import type { OptimizedSchedule } from '../strategies/optimizer.js';
10
+ import type { CandidateGenerator, DecisionCandidate } from './types.js';
11
+ /**
12
+ * The simple fill-to-target Roth strategies of the bounded tournament: bracket
13
+ * fills, the ACA cliff cap, and the first IRMAA tier cap over the whole
14
+ * horizon, plus *windowed* bracket fills that stop at income boundaries (SS
15
+ * claim / RMD start) — under a sequential withdrawal order, spending draws fill
16
+ * the cheap brackets themselves once cash/taxable deplete or income starts, so
17
+ * "fill high early, then stop" beats any whole-horizon fill on bridge-shaped
18
+ * plans. Generated unconditionally — a plan with no convertible traditional
19
+ * simply executes $0 and evaluates neutral.
20
+ */
21
+ export declare const simpleRothConversionGenerator: CandidateGenerator;
22
+ /** The do-nothing alternative, only meaningful when the plan currently converts. */
23
+ export declare const noConversionGenerator: CandidateGenerator;
24
+ /** Withdrawal-order alternatives to whatever the plan currently uses. */
25
+ export declare const withdrawalOrderGenerator: CandidateGenerator;
26
+ export interface ProbabilityBandSpendingGuardrailOptions {
27
+ /** Floor as a percent of target annual spending when the plan has no floor yet. */
28
+ requiredFloorPct?: number;
29
+ /** Lower Monte Carlo success band the candidate is meant to defend. */
30
+ lowerSuccessPct?: number;
31
+ /** Upper Monte Carlo success band where raises/upside become available. */
32
+ upperSuccessPct?: number;
33
+ }
34
+ /**
35
+ * Probability-band safe-spend guardrail candidates. Generation is deliberately
36
+ * pure: the candidate carries the success band as metadata and a concrete
37
+ * ledger-native guardrail patch; exact/stochastic surfaces can then evaluate it
38
+ * through the shared decision and Monte Carlo runners without private models.
39
+ */
40
+ export declare function probabilityBandSpendingGuardrailGenerator(options?: ProbabilityBandSpendingGuardrailOptions): CandidateGenerator;
41
+ /**
42
+ * Bounded Social Security claim-age candidates: for up to two SS streams, the
43
+ * three canonical claim ages (62 / FRA / 70) that differ from the current
44
+ * claim. Each candidate replaces the whole incomes array (scenario patches
45
+ * replace arrays wholesale, keeping the patch order-safe).
46
+ */
47
+ export declare const socialSecurityClaimGenerator: CandidateGenerator;
48
+ /**
49
+ * Asset-location candidates (asset-allocation-and-return-model-v2, step 5).
50
+ *
51
+ * Proposes swapping *where* classes are held while leaving the household's
52
+ * total dollars in each class unchanged: bonds move toward traditional
53
+ * (sheltering their ordinary-income yield and slowing the RMD/heir-taxed
54
+ * balance), US stocks move toward taxable (qualified dividends + basis
55
+ * machinery) or Roth (sheltering the highest expected growth). Swaps pair the
56
+ * largest eligible static-allocation accounts and are expressed as plan
57
+ * patches over the accounts array; the exact ledger prices every candidate —
58
+ * generation never decides. Bounded: at most 3 candidates.
59
+ */
60
+ export declare const assetLocationGenerator: CandidateGenerator;
61
+ /**
62
+ * Full Social Security claiming grid for the optimizer page: every whole-year
63
+ * claim combination from 62-70 (respecting current age) for up to two claiming
64
+ * streams. When a stream's current claim age is a whole year within range, that
65
+ * combination reproduces the current plan; a current claim age with months, below
66
+ * 62, above 70, or already past is not emitted here. Bounded: 9 single-person
67
+ * candidates or 81 couple candidates.
68
+ */
69
+ export declare const socialSecurityClaimGridGenerator: CandidateGenerator;
70
+ /**
71
+ * Wrap MILP optimizer output (raw and post-processed schedules) as candidates.
72
+ * The solver is one candidate generator among several — never the authority.
73
+ */
74
+ export declare function milpScheduleGenerator(schedules: {
75
+ raw?: OptimizedSchedule | null;
76
+ cleanedConversions?: Array<{
77
+ year: number;
78
+ amount: number;
79
+ }> | null;
80
+ }): CandidateGenerator;
81
+ /**
82
+ * Annuity purchase candidates (guaranteed-income-and-estate-depth, step 6;
83
+ * ladders added by annuity-pension-and-home-equity, step 1): no purchase /
84
+ * cover-the-floor SPIA / a laddered version of the same SPIA (three dated
85
+ * tranches, so rising age-payout rates and shorter rate lock-in are priced) /
86
+ * QLAC at the cap. Each is a concrete, bounded plan patch that adds (or
87
+ * removes) annuity purchases; the exact ledger prices the liquidity ↓ /
88
+ * durability ↑ / estate Δ tradeoff — generation never decides. Bounded: at
89
+ * most four candidates. Payout rates come from the sourced default table in
90
+ * ./spiaQuotes (user-entered purchases carry their own quotes).
91
+ */
92
+ export declare const annuityPurchaseGenerator: CandidateGenerator;
93
+ /**
94
+ * Social Security bridge / TIPS-ladder candidates
95
+ * (social-security-bridge-and-tips-ladder, step 3): fund the gap years between
96
+ * retirement and each claimant's chosen claim age with a TIPS bridge ladder
97
+ * paying the forgone age-62 benefit, purchased from the largest liquid
98
+ * account; plus the remove-the-ladders alternative when the plan already has
99
+ * some. Bounded (≤ one bridge per person + one removal); the exact ledger
100
+ * prices the liquidity ↓ / durability ↑ tradeoff — generation never decides.
101
+ */
102
+ export declare const bridgeLadderGenerator: CandidateGenerator;
103
+ /** Wrap a modelable Insights detector action as a decision candidate generator. */
104
+ export declare function insightActionGenerator(args: {
105
+ id: string;
106
+ category: DecisionCandidate['category'];
107
+ label: string;
108
+ explanation: string;
109
+ planPatch: Record<string, unknown>;
110
+ }): CandidateGenerator;