@retiregolden/planner-ui 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 (298) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +181 -0
  3. package/package.json +77 -0
  4. package/src/App.tsx +246 -0
  5. package/src/RouteErrorBoundary.tsx +45 -0
  6. package/src/assets/hero.png +0 -0
  7. package/src/assets/react.svg +1 -0
  8. package/src/assets/vite.svg +1 -0
  9. package/src/data/fedInvestClient.ts +113 -0
  10. package/src/data/localStore.ts +42 -0
  11. package/src/data/planOrigin.ts +24 -0
  12. package/src/data/planStore.ts +165 -0
  13. package/src/data/v2Backup.ts +101 -0
  14. package/src/import/ImportPage.tsx +347 -0
  15. package/src/import/ReviewChecklistView.tsx +38 -0
  16. package/src/import/brokerCsv.ts +395 -0
  17. package/src/import/csv.ts +133 -0
  18. package/src/import/genericCsv.ts +224 -0
  19. package/src/import/projectionLab.ts +350 -0
  20. package/src/import/reviewChecklist.ts +33 -0
  21. package/src/import/tenForty.ts +275 -0
  22. package/src/index.css +630 -0
  23. package/src/index.ts +16 -0
  24. package/src/learn/ArticleBody.tsx +78 -0
  25. package/src/learn/ArticlePage.tsx +57 -0
  26. package/src/learn/GlossaryPage.tsx +33 -0
  27. package/src/learn/LearnAboutScreen.tsx +41 -0
  28. package/src/learn/LearnCards.tsx +41 -0
  29. package/src/learn/LearnLink.tsx +91 -0
  30. package/src/learn/LearningCenterPage.tsx +114 -0
  31. package/src/learn/SourcesPage.tsx +98 -0
  32. package/src/learn/components/ArticleFigure.tsx +34 -0
  33. package/src/learn/components/ArticleShell.tsx +86 -0
  34. package/src/learn/components/ComparisonTable.tsx +42 -0
  35. package/src/learn/components/FormulaBlock.tsx +34 -0
  36. package/src/learn/components/PurchasingPowerChart.tsx +41 -0
  37. package/src/learn/components/RelatedArticles.tsx +27 -0
  38. package/src/learn/components/ScenarioCard.tsx +24 -0
  39. package/src/learn/components/SourceList.tsx +23 -0
  40. package/src/learn/components/charts.tsx +21 -0
  41. package/src/learn/content/about-retiregolden.ts +100 -0
  42. package/src/learn/content/aca-premium-tax-credits-and-magi.ts +103 -0
  43. package/src/learn/content/account-types-overview.ts +106 -0
  44. package/src/learn/content/after-tax-estate.ts +111 -0
  45. package/src/learn/content/agi-magi-and-taxable-income.ts +112 -0
  46. package/src/learn/content/appealing-irmaa-ssa-44.ts +95 -0
  47. package/src/learn/content/assumption-general-inflation.ts +82 -0
  48. package/src/learn/content/assumption-healthcare-inflation.ts +85 -0
  49. package/src/learn/content/assumption-heir-tax-rate.ts +79 -0
  50. package/src/learn/content/assumption-investment-returns.ts +90 -0
  51. package/src/learn/content/assumption-longevity-planning-age.ts +78 -0
  52. package/src/learn/content/assumption-recent-magi.ts +83 -0
  53. package/src/learn/content/assumption-social-security-cola.ts +89 -0
  54. package/src/learn/content/assumption-social-security-trust-fund.ts +83 -0
  55. package/src/learn/content/assumption-state-tax-override.ts +79 -0
  56. package/src/learn/content/beneficiaries-and-account-titling.ts +99 -0
  57. package/src/learn/content/break-even-useful-lens.ts +94 -0
  58. package/src/learn/content/building-a-retirement-spending-budget.ts +100 -0
  59. package/src/learn/content/cola-and-inflation-protection.ts +102 -0
  60. package/src/learn/content/divorced-spousal-and-survivor-records.ts +104 -0
  61. package/src/learn/content/dynamic-spending-guardrails.ts +90 -0
  62. package/src/learn/content/earnings-test-before-fra.ts +100 -0
  63. package/src/learn/content/employer-match-and-contribution-order.ts +104 -0
  64. package/src/learn/content/examplePlanArticles.ts +525 -0
  65. package/src/learn/content/fees-expense-ratios-and-compounding-drag.ts +98 -0
  66. package/src/learn/content/fi-number-and-four-percent-rule.ts +64 -0
  67. package/src/learn/content/filling-a-tax-bracket-with-roth-conversions.ts +98 -0
  68. package/src/learn/content/funded-ratio.ts +70 -0
  69. package/src/learn/content/healthcare-after-65.ts +103 -0
  70. package/src/learn/content/healthcare-before-65.ts +104 -0
  71. package/src/learn/content/historical-vs-random-return-models.ts +101 -0
  72. package/src/learn/content/how-assumptions-change-the-answer.ts +105 -0
  73. package/src/learn/content/how-much-can-i-spend.ts +105 -0
  74. package/src/learn/content/how-social-security-is-taxed.ts +95 -0
  75. package/src/learn/content/how-the-optimizer-thinks.ts +102 -0
  76. package/src/learn/content/how-the-optimizer-values-after-tax-estate.ts +97 -0
  77. package/src/learn/content/how-to-model-accumulation.ts +67 -0
  78. package/src/learn/content/how-to-read-a-retirement-projection.ts +115 -0
  79. package/src/learn/content/hsas-and-qualified-medical-expenses.ts +108 -0
  80. package/src/learn/content/hsas-as-retirement-accounts.ts +101 -0
  81. package/src/learn/content/inflation-risk.ts +98 -0
  82. package/src/learn/content/inherited-ira-10-year-rule.ts +105 -0
  83. package/src/learn/content/insurance-in-your-retirement-plan.ts +103 -0
  84. package/src/learn/content/irmaa-two-year-lookback.ts +99 -0
  85. package/src/learn/content/long-term-care-costs-and-insurance.ts +103 -0
  86. package/src/learn/content/long-term-care-insurance-as-risk-transfer.ts +98 -0
  87. package/src/learn/content/longevity-risk.ts +99 -0
  88. package/src/learn/content/marginal-vs-effective-tax-rate.ts +98 -0
  89. package/src/learn/content/medicare-part-b-vs-part-d-irmaa.ts +102 -0
  90. package/src/learn/content/mortality-weighted-social-security.ts +113 -0
  91. package/src/learn/content/moving-to-retiregolden.ts +86 -0
  92. package/src/learn/content/niit-high-income-investment-tax.ts +98 -0
  93. package/src/learn/content/ordinary-income-vs-capital-gains.ts +103 -0
  94. package/src/learn/content/paying-conversion-taxes-taxable-vs-ira.ts +102 -0
  95. package/src/learn/content/pensions-and-annuities.ts +101 -0
  96. package/src/learn/content/permanent-life-insurance-in-a-plan.ts +106 -0
  97. package/src/learn/content/pia-aime-and-bend-points.ts +103 -0
  98. package/src/learn/content/planner-overview.ts +106 -0
  99. package/src/learn/content/planning-for-couples-and-survivor-years.ts +108 -0
  100. package/src/learn/content/privacy-what-stays-in-your-browser.ts +99 -0
  101. package/src/learn/content/qcds-qualified-charitable-distributions.ts +101 -0
  102. package/src/learn/content/reading-the-results-page.ts +96 -0
  103. package/src/learn/content/reading-the-social-security-analysis-page.ts +106 -0
  104. package/src/learn/content/real-estate-home-equity-and-debt.ts +100 -0
  105. package/src/learn/content/reports-csv-exports-and-sharing.ts +101 -0
  106. package/src/learn/content/risk-based-guardrails.ts +100 -0
  107. package/src/learn/content/rmds-required-minimum-distributions.ts +100 -0
  108. package/src/learn/content/roth-conversion-basics.ts +104 -0
  109. package/src/learn/content/rsus-and-espp.ts +101 -0
  110. package/src/learn/content/rule-of-55-and-72t.ts +107 -0
  111. package/src/learn/content/savings-rate-biggest-lever.ts +66 -0
  112. package/src/learn/content/seed-your-plan-from-your-tax-return.ts +93 -0
  113. package/src/learn/content/sensitivity-testing-what-changes-the-answer.ts +104 -0
  114. package/src/learn/content/sequence-of-returns-risk.ts +98 -0
  115. package/src/learn/content/social-security-bridge.ts +67 -0
  116. package/src/learn/content/social-security-claiming-age-basics.ts +113 -0
  117. package/src/learn/content/social-security-taxes-vs-benefits.ts +76 -0
  118. package/src/learn/content/spending-profiles-and-the-retirement-smile.ts +92 -0
  119. package/src/learn/content/spousal-and-survivor-benefits.ts +120 -0
  120. package/src/learn/content/ssdi-and-retirement-planning.ts +72 -0
  121. package/src/learn/content/standard-deduction-senior-deduction-and-itemizing.ts +97 -0
  122. package/src/learn/content/state-income-taxes-in-retirement.ts +97 -0
  123. package/src/learn/content/step-up-in-basis.ts +102 -0
  124. package/src/learn/content/survivor-planning-for-couples.ts +110 -0
  125. package/src/learn/content/survivor-spending-in-couple-plans.ts +98 -0
  126. package/src/learn/content/tax-cliffs-and-bracket-edges.ts +105 -0
  127. package/src/learn/content/tax-loss-and-gain-harvesting.ts +99 -0
  128. package/src/learn/content/taxable-brokerage-basis-and-capital-gains.ts +99 -0
  129. package/src/learn/content/three-big-questions-spending-time-risk.ts +103 -0
  130. package/src/learn/content/tips-ladders.ts +92 -0
  131. package/src/learn/content/todays-dollars-vs-future-dollars.ts +107 -0
  132. package/src/learn/content/traditional-vs-roth-contributions.ts +113 -0
  133. package/src/learn/content/troubleshooting-surprising-results.ts +105 -0
  134. package/src/learn/content/trust-fund-haircut-scenarios.ts +101 -0
  135. package/src/learn/content/understanding-monte-carlo-success-rate.ts +118 -0
  136. package/src/learn/content/understanding-your-plan-assumptions.ts +134 -0
  137. package/src/learn/content/using-assumptions-and-provenance.ts +98 -0
  138. package/src/learn/content/using-scenarios-to-compare-choices.ts +99 -0
  139. package/src/learn/content/what-changes-when-you-move-states.ts +141 -0
  140. package/src/learn/content/what-is-fire.ts +65 -0
  141. package/src/learn/content/what-monte-carlo-proves.ts +98 -0
  142. package/src/learn/content/what-retiregolden-models.ts +103 -0
  143. package/src/learn/content/what-retirement-healthcare-really-costs.ts +117 -0
  144. package/src/learn/content/why-95-percent-is-not-a-guarantee.ts +98 -0
  145. package/src/learn/content/why-roth-conversions-raise-other-costs.ts +106 -0
  146. package/src/learn/content/why-small-tax-cliffs-can-matter.ts +109 -0
  147. package/src/learn/content/widows-penalty-and-survivor-brackets.ts +106 -0
  148. package/src/learn/content/withdrawal-order-basics.ts +105 -0
  149. package/src/learn/glossary.ts +191 -0
  150. package/src/learn/inlineMarkdown.tsx +54 -0
  151. package/src/learn/learn.css +537 -0
  152. package/src/learn/learningRegistry.ts +502 -0
  153. package/src/longevity/LongevityResults.tsx +85 -0
  154. package/src/longevity/LongevityWizard.tsx +305 -0
  155. package/src/longevity/constants.ts +15 -0
  156. package/src/longevity/factors.ts +125 -0
  157. package/src/longevity/model.ts +31 -0
  158. package/src/longevity/persistedGuard.ts +129 -0
  159. package/src/longevity/storage.ts +40 -0
  160. package/src/mc/messages.ts +118 -0
  161. package/src/mc/monteCarlo.worker.ts +44 -0
  162. package/src/mc/pool.ts +267 -0
  163. package/src/mc/runRequest.ts +125 -0
  164. package/src/optimize/messages.ts +84 -0
  165. package/src/optimize/optimize.worker.ts +29 -0
  166. package/src/optimize/runOptimize.ts +92 -0
  167. package/src/optimize/runSpendingSolve.ts +47 -0
  168. package/src/optimize/runner.ts +21 -0
  169. package/src/optimize/spendingMessages.ts +44 -0
  170. package/src/optimize/spendingRunner.ts +21 -0
  171. package/src/optimize/spendingSolve.worker.ts +18 -0
  172. package/src/planner/AssumptionsCardPage.tsx +136 -0
  173. package/src/planner/BucketLensCard.tsx +114 -0
  174. package/src/planner/ComparePlansPage.tsx +219 -0
  175. package/src/planner/DisclaimerPage.tsx +88 -0
  176. package/src/planner/HowTestedPage.tsx +159 -0
  177. package/src/planner/LiveStatus.tsx +15 -0
  178. package/src/planner/LongevityModal.tsx +55 -0
  179. package/src/planner/Modal.tsx +97 -0
  180. package/src/planner/MonteCarloPage.tsx +907 -0
  181. package/src/planner/OptimizePage.tsx +611 -0
  182. package/src/planner/PlanContext.tsx +198 -0
  183. package/src/planner/PlanPickerPage.tsx +124 -0
  184. package/src/planner/PlanWorkspace.tsx +290 -0
  185. package/src/planner/ProvenancePanel.tsx +45 -0
  186. package/src/planner/RelocationComparePage.tsx +485 -0
  187. package/src/planner/ReportPage.tsx +375 -0
  188. package/src/planner/ResultsPage.tsx +817 -0
  189. package/src/planner/ScenariosPage.tsx +285 -0
  190. package/src/planner/SocialSecuritySection.tsx +556 -0
  191. package/src/planner/SpendingSolverPage.tsx +512 -0
  192. package/src/planner/SsAnalysisPage.tsx +1134 -0
  193. package/src/planner/SurvivalPercentileModal.tsx +161 -0
  194. package/src/planner/SurvivorTransitionPage.tsx +286 -0
  195. package/src/planner/assumptionsExport.ts +371 -0
  196. package/src/planner/bucketLens.ts +89 -0
  197. package/src/planner/chartFrame.ts +8 -0
  198. package/src/planner/chartStyle.ts +11 -0
  199. package/src/planner/dialogViews.tsx +184 -0
  200. package/src/planner/dialogs.tsx +133 -0
  201. package/src/planner/examples/ExampleLibrary.tsx +189 -0
  202. package/src/planner/examples/ExamplePreviewBanner.tsx +55 -0
  203. package/src/planner/examples/ExamplesPage.tsx +25 -0
  204. package/src/planner/examples/OpenExampleButton.tsx +61 -0
  205. package/src/planner/examples/buildAggressiveSaver.ts +102 -0
  206. package/src/planner/examples/buildAnnuityEstate.ts +137 -0
  207. package/src/planner/examples/buildBaristaFire.ts +115 -0
  208. package/src/planner/examples/buildBracketFillRoth.ts +65 -0
  209. package/src/planner/examples/buildBridgeEarlyRetirement.ts +94 -0
  210. package/src/planner/examples/buildBrokerageNoHsa.ts +109 -0
  211. package/src/planner/examples/buildCoastFire.ts +88 -0
  212. package/src/planner/examples/buildContext.ts +20 -0
  213. package/src/planner/examples/buildEarlyCareerMatch.ts +93 -0
  214. package/src/planner/examples/buildEarlyRetireeAca.ts +61 -0
  215. package/src/planner/examples/buildExampleCouple.ts +103 -0
  216. package/src/planner/examples/buildFixedTargetSpending.ts +74 -0
  217. package/src/planner/examples/buildGlidepathAllocation.ts +131 -0
  218. package/src/planner/examples/buildGuardrailsFlex.ts +120 -0
  219. package/src/planner/examples/buildHsaPropertyDepth.ts +109 -0
  220. package/src/planner/examples/buildHsaStealthRetirement.ts +97 -0
  221. package/src/planner/examples/buildLeanFatFire.ts +109 -0
  222. package/src/planner/examples/buildLtcShock.ts +62 -0
  223. package/src/planner/examples/buildMovingStateTax.ts +53 -0
  224. package/src/planner/examples/buildNoAnnuityBrokerage.ts +92 -0
  225. package/src/planner/examples/buildRmdIrmaa.ts +55 -0
  226. package/src/planner/examples/buildSalaryGrowthEscalation.ts +96 -0
  227. package/src/planner/examples/buildStaticAllocationControl.ts +96 -0
  228. package/src/planner/examples/buildSurvivorYears.ts +62 -0
  229. package/src/planner/examples/buildUnderSavedSingle.ts +51 -0
  230. package/src/planner/examples/exampleCopy.ts +23 -0
  231. package/src/planner/examples/loadExample.ts +90 -0
  232. package/src/planner/examples/registry.ts +313 -0
  233. package/src/planner/explainPanels.tsx +233 -0
  234. package/src/planner/fields.tsx +381 -0
  235. package/src/planner/format.ts +33 -0
  236. package/src/planner/home/DataAndPrivacyCard.tsx +56 -0
  237. package/src/planner/home/GettingStartedPaths.tsx +46 -0
  238. package/src/planner/home/GettingStartedReopener.tsx +32 -0
  239. package/src/planner/home/StartHereLinks.tsx +22 -0
  240. package/src/planner/home/WelcomeHero.tsx +39 -0
  241. package/src/planner/home/YourPlans.tsx +72 -0
  242. package/src/planner/home/importErrorMessage.ts +22 -0
  243. package/src/planner/home/startHereSlugs.ts +7 -0
  244. package/src/planner/home/useHomeData.ts +190 -0
  245. package/src/planner/home/useHomeMode.ts +47 -0
  246. package/src/planner/householdActions.ts +22 -0
  247. package/src/planner/insights/InsightCardView.tsx +340 -0
  248. package/src/planner/insights/InsightsPage.tsx +204 -0
  249. package/src/planner/insights/categoryLabels.ts +11 -0
  250. package/src/planner/learnLinks.ts +85 -0
  251. package/src/planner/marketModelPicker.ts +172 -0
  252. package/src/planner/optimizePageChart.ts +40 -0
  253. package/src/planner/optimizePageClaim.ts +64 -0
  254. package/src/planner/planCompleteness.ts +27 -0
  255. package/src/planner/planContextCore.ts +26 -0
  256. package/src/planner/planner.css +2304 -0
  257. package/src/planner/provenanceLinks.ts +25 -0
  258. package/src/planner/sections/AccountFields.tsx +872 -0
  259. package/src/planner/sections/AccountsSection.tsx +89 -0
  260. package/src/planner/sections/AllocationPanel.tsx +261 -0
  261. package/src/planner/sections/AssumptionsSection.tsx +256 -0
  262. package/src/planner/sections/HouseholdSection.tsx +243 -0
  263. package/src/planner/sections/IncomeFloorSection.tsx +418 -0
  264. package/src/planner/sections/IncomeSection.tsx +170 -0
  265. package/src/planner/sections/InsuranceSection.tsx +362 -0
  266. package/src/planner/sections/SpendingSection.tsx +904 -0
  267. package/src/planner/sections/StrategySection.tsx +349 -0
  268. package/src/planner/sections/UpdateBalancesPanel.tsx +182 -0
  269. package/src/planner/sections/sectionHelpers.ts +48 -0
  270. package/src/planner/sections/shared.tsx +15 -0
  271. package/src/planner/sections.tsx +15 -0
  272. package/src/planner/ssAnalysis.ts +325 -0
  273. package/src/planner/successBand.ts +20 -0
  274. package/src/planner/survivorAnalysis.ts +277 -0
  275. package/src/planner/usStates.ts +19 -0
  276. package/src/planner/useMcSuccessRate.ts +77 -0
  277. package/src/planner/useProjection.ts +63 -0
  278. package/src/relocation/messages.ts +21 -0
  279. package/src/relocation/relocation.worker.ts +18 -0
  280. package/src/relocation/runRelocation.ts +17 -0
  281. package/src/relocation/runner.ts +22 -0
  282. package/src/report/brandingContext.ts +15 -0
  283. package/src/report/downloadReport.ts +34 -0
  284. package/src/report/reportHtml.ts +547 -0
  285. package/src/routes/LearnRoutes.tsx +46 -0
  286. package/src/routes/PlanRoutes.tsx +55 -0
  287. package/src/routes/RouteFallback.tsx +9 -0
  288. package/src/socialSecurity/breakEven.ts +107 -0
  289. package/src/socialSecurity/expectedPv.ts +164 -0
  290. package/src/socialSecurity/explain.ts +92 -0
  291. package/src/socialSecurity/ficaReturn.ts +81 -0
  292. package/src/socialSecurity/persistedSsGuard.ts +138 -0
  293. package/src/socialSecurity/ssFormUtils.ts +48 -0
  294. package/src/socialSecurity/ssaStatementXml.ts +156 -0
  295. package/src/socialSecurity/storage.ts +69 -0
  296. package/src/socialSecurity/survivorSwitching.ts +153 -0
  297. package/src/testSupport/samplePlan.ts +2 -0
  298. package/src/workers/run.ts +45 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Executes one optimize request. Shared by the Web Worker entry and the
3
+ * synchronous fallback (tests / no-Worker environments) so both run the
4
+ * identical tax stack — federal engine + per-state engine with the plan's flat
5
+ * rate as an override, matching src/mc/runRequest.ts.
6
+ */
7
+
8
+ import { objectivePolicyForPlan } from '@retiregolden/engine/decisions'
9
+ import {
10
+ optimizePlan,
11
+ optimizePlanCoOptimizingClaimAge,
12
+ type ExactLedgerPostProcessing,
13
+ type OptimizePlanOptions,
14
+ } from '@retiregolden/engine/projection/optimizePlan'
15
+ import { combineTaxCalculators, createFederalTaxCalculator } from '@retiregolden/engine/tax/federalTax'
16
+ import { createStateTaxCalculator } from '@retiregolden/engine/tax/stateTax'
17
+ import type { OptimizePostProcessing, OptimizeRequest, OptimizeResult } from './messages'
18
+
19
+ function summarizePostProcessing(postProcessed: ExactLedgerPostProcessing | null): OptimizePostProcessing | null {
20
+ if (!postProcessed) return null
21
+ return {
22
+ rawSchedule: postProcessed.rawSchedule,
23
+ cleanedSchedule: postProcessed.cleanedSchedule,
24
+ rawValidation: postProcessed.rawValidation,
25
+ cleanedValidation: postProcessed.cleanedValidation,
26
+ adjustments: postProcessed.adjustments,
27
+ stabilized: postProcessed.stabilized,
28
+ iterationCount: postProcessed.iterationCount,
29
+ pruneIterationCount: postProcessed.pruneIterationCount,
30
+ recommendationSchedule: postProcessed.recommendationSchedule,
31
+ cleanedExecutionByYear: postProcessed.cleanedResult.years.map((year) => ({
32
+ year: year.year,
33
+ rothConversion: year.rothConversion,
34
+ })),
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Production defaults for the optimize pipeline. Exported so the dev-only Owl
40
+ * parity harness (src/cases/owlParity.ts) measures exactly what the worker
41
+ * ships — a retune here automatically retunes the parity gate.
42
+ */
43
+ export const DEFAULT_OPTIMIZE_SEARCH_BUDGET = 96
44
+ export const DEFAULT_OPTIMIZE_CONVERGENCE_ITERATIONS = 6
45
+
46
+ export async function runOptimizeRequest(
47
+ req: OptimizeRequest,
48
+ locateFile?: (file: string) => string,
49
+ ): Promise<OptimizeResult> {
50
+ const taxCalculator = combineTaxCalculators(
51
+ createFederalTaxCalculator(),
52
+ createStateTaxCalculator({
53
+ overridePct: req.plan.assumptions.stateEffectiveTaxPct,
54
+ localPct: req.plan.assumptions.localIncomeTaxPct,
55
+ }),
56
+ )
57
+ // Phase 4 local search: refine the winning schedules on the exact ledger
58
+ // under a bounded, deterministic per-seed simulation budget (enough for the
59
+ // coarse+fine sweeps to also explore the bounded taper-extension years past
60
+ // the seed schedule); zero or negative disables search entirely (the seed
61
+ // evaluation would otherwise still cost one simulation).
62
+ const searchBudget = req.searchSimulationBudget ?? DEFAULT_OPTIMIZE_SEARCH_BUDGET
63
+ // Exact-ledger convergence loop (Step 1): re-solve against exogenous inputs
64
+ // recaptured from the incumbent schedule so the recommendation is optimal on
65
+ // the real projection to tolerance. Bounded iteration cap; the tournament
66
+ // still prices and gates whatever the loop produces. `≤ 1` disables it.
67
+ const convergenceIterations = req.convergenceIterations ?? DEFAULT_OPTIMIZE_CONVERGENCE_ITERATIONS
68
+ const planOptions: OptimizePlanOptions = {
69
+ startYear: req.startYear,
70
+ taxCalculator,
71
+ liquidationRatePct: req.liquidationRatePct,
72
+ solver: { timeLimitSec: req.timeLimitSec, locateFile },
73
+ search: searchBudget > 0 ? { maxSimulations: searchBudget } : false,
74
+ convergence: convergenceIterations > 1 ? { maxIterations: convergenceIterations } : false,
75
+ // Resolved here (not passed as a function) so the policy survives the
76
+ // structured-clone worker boundary; floor policies read the plan's bequest target.
77
+ policy: req.objectivePolicyId ? objectivePolicyForPlan(req.objectivePolicyId, req.plan) : undefined,
78
+ }
79
+
80
+ // Step 5: co-optimize the SS claim age when requested (opt-in; bounded grid).
81
+ // The returned schedule may assume a claim change reported in `claimAge`.
82
+ if (req.coOptimizeClaimAge) {
83
+ const { schedule, postProcessed, tournament, convergence, claimAge } = await optimizePlanCoOptimizingClaimAge(
84
+ req.plan,
85
+ planOptions,
86
+ )
87
+ return { schedule, postProcessed: summarizePostProcessing(postProcessed), tournament, convergence, claimAge }
88
+ }
89
+
90
+ const { schedule, postProcessed, tournament, convergence } = await optimizePlan(req.plan, planOptions)
91
+ return { schedule, postProcessed: summarizePostProcessing(postProcessed), tournament, convergence, claimAge: null }
92
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Executes one sustainable-spending solve. Shared by the Web Worker entry and
3
+ * the synchronous fallback (tests / no-Worker environments) so both run the
4
+ * identical tax stack — federal engine + per-state engine with the plan's flat
5
+ * rate as an override, matching ./runOptimize.ts.
6
+ */
7
+
8
+ import { createDecisionContext, solveMaxSustainableSpending, SPENDING_SOLVER_UI_BUDGET } from '@retiregolden/engine/decisions'
9
+ import { combineTaxCalculators, createFederalTaxCalculator } from '@retiregolden/engine/tax/federalTax'
10
+ import { createStateTaxCalculator } from '@retiregolden/engine/tax/stateTax'
11
+ import type { SpendingSolveRequest, SpendingSolveResult } from './spendingMessages'
12
+
13
+ export function runSpendingSolveRequest(req: SpendingSolveRequest): SpendingSolveResult {
14
+ const taxCalculator = combineTaxCalculators(
15
+ createFederalTaxCalculator(),
16
+ createStateTaxCalculator({
17
+ overridePct: req.plan.assumptions.stateEffectiveTaxPct,
18
+ localPct: req.plan.assumptions.localIncomeTaxPct,
19
+ }),
20
+ )
21
+ const ctx = createDecisionContext(req.plan, { startYear: req.startYear, taxCalculator })
22
+ const estateFloorTodayDollars = req.plan.expenses.bequestTargetDollars ?? 0
23
+ const solved = solveMaxSustainableSpending(ctx, {
24
+ maxSimulations: req.maxSimulations ?? SPENDING_SOLVER_UI_BUDGET,
25
+ estateFloorTodayDollars,
26
+ })
27
+ const summary = solved.bestEvaluation?.candidateSummary ?? null
28
+ return {
29
+ maxBaseAnnual: solved.maxBaseAnnual,
30
+ spendingSlackDollars: solved.spendingSlackDollars,
31
+ currentBaseAnnual: req.plan.expenses.baseAnnual,
32
+ estateFloorTodayDollars,
33
+ converged: solved.converged,
34
+ limitingConstraint: solved.limitingConstraint,
35
+ simulationCount: solved.simulationCount,
36
+ diagnostics: solved.diagnostics,
37
+ evidence: summary
38
+ ? {
39
+ endingAfterTaxEstate: summary.endingAfterTaxEstate,
40
+ endingNetWorth: summary.endingNetWorth,
41
+ lifetimeTaxesAndPenalties: summary.lifetimeTaxesAndPenalties,
42
+ depletionYear: solved.bestEvaluation!.candidateResult.depletionYear,
43
+ endYear: solved.bestEvaluation!.candidateResult.endYear,
44
+ }
45
+ : null,
46
+ }
47
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Spawns the optimizer in a Web Worker (lazy-loaded only here, so the wasm
3
+ * chunk is fetched only when Optimize is invoked) and resolves the schedule.
4
+ * Falls back to a synchronous in-process solve where Worker is unavailable
5
+ * (tests, very old browsers), mirroring src/mc/pool.ts.
6
+ */
7
+
8
+ import type { OptimizeRequest, OptimizeResponse, OptimizeResult } from './messages'
9
+ import { runOptimizeRequest } from './runOptimize'
10
+ import { runWorkerRequest } from '../workers/run'
11
+
12
+ export function runOptimize(req: OptimizeRequest): Promise<OptimizeResult> {
13
+ if (typeof Worker === 'undefined') return runOptimizeRequest(req)
14
+ return runWorkerRequest<OptimizeRequest, OptimizeResponse, OptimizeResult>({
15
+ request: req,
16
+ createWorker: () => new Worker(new URL('./optimize.worker.ts', import.meta.url), { type: 'module' }),
17
+ interpret: (msg) =>
18
+ msg.type === 'done' ? { kind: 'done', result: msg.result } : { kind: 'error', message: msg.message },
19
+ errorLabel: 'Optimizer worker failed',
20
+ })
21
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Wire types between the "How much can I spend?" surface and its worker
3
+ * (sustainable-spending plan, Step 4). Same rules as ./messages.ts: everything
4
+ * must survive structured clone, so the solver's `bestEvaluation` (which holds
5
+ * a full ProjectionResult) is summarized into `evidence` before posting.
6
+ */
7
+
8
+ import type { Plan } from '@retiregolden/engine/model/plan'
9
+
10
+ export interface SpendingSolveRequest {
11
+ plan: Plan
12
+ startYear: number
13
+ /** Exact-ledger simulation budget; defaults to SPENDING_SOLVER_UI_BUDGET. */
14
+ maxSimulations?: number
15
+ }
16
+
17
+ /** Exact-ledger evidence for the plan run at the solved spending level. */
18
+ export interface SpendingSolveEvidence {
19
+ endingAfterTaxEstate: number
20
+ endingNetWorth: number
21
+ lifetimeTaxesAndPenalties: number
22
+ depletionYear: number | null
23
+ endYear: number
24
+ }
25
+
26
+ export interface SpendingSolveResult {
27
+ /** Highest feasible annual base spending (today's dollars), or null. */
28
+ maxBaseAnnual: number | null
29
+ /** maxBaseAnnual − current base spending (negative ⇒ overspending today). */
30
+ spendingSlackDollars: number | null
31
+ /** The plan's own base spending the slack is measured against. */
32
+ currentBaseAnnual: number
33
+ /** The bequest target the solve enforced (today's dollars; 0 = none). */
34
+ estateFloorTodayDollars: number
35
+ converged: boolean
36
+ limitingConstraint: 'depletion' | 'estate-floor' | null
37
+ simulationCount: number
38
+ diagnostics: string[]
39
+ evidence: SpendingSolveEvidence | null
40
+ }
41
+
42
+ export type SpendingSolveResponse =
43
+ | { type: 'done'; result: SpendingSolveResult }
44
+ | { type: 'error'; message: string }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Spawns the sustainable-spending solver in a Web Worker (lazy-loaded only
3
+ * here) and resolves the summarized result. Falls back to a synchronous
4
+ * in-process solve where Worker is unavailable (tests, very old browsers),
5
+ * mirroring ./runner.ts.
6
+ */
7
+
8
+ import type { SpendingSolveRequest, SpendingSolveResponse, SpendingSolveResult } from './spendingMessages'
9
+ import { runSpendingSolveRequest } from './runSpendingSolve'
10
+ import { runWorkerRequest } from '../workers/run'
11
+
12
+ export function runSpendingSolve(req: SpendingSolveRequest): Promise<SpendingSolveResult> {
13
+ if (typeof Worker === 'undefined') return Promise.resolve(runSpendingSolveRequest(req))
14
+ return runWorkerRequest<SpendingSolveRequest, SpendingSolveResponse, SpendingSolveResult>({
15
+ request: req,
16
+ createWorker: () => new Worker(new URL('./spendingSolve.worker.ts', import.meta.url), { type: 'module' }),
17
+ interpret: (msg) =>
18
+ msg.type === 'done' ? { kind: 'done', result: msg.result } : { kind: 'error', message: msg.message },
19
+ errorLabel: 'Spending solver worker failed',
20
+ })
21
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Web Worker entry for the sustainable-spending solver: one request per run,
3
+ * ~25 exact-ledger simulations off the main thread, summarized result posted
4
+ * back. No wasm — the solver is pure `simulatePlan` bisection.
5
+ */
6
+
7
+ import type { SpendingSolveRequest, SpendingSolveResponse } from './spendingMessages'
8
+ import { runSpendingSolveRequest } from './runSpendingSolve'
9
+
10
+ const post = (msg: SpendingSolveResponse) => (self as unknown as Worker).postMessage(msg)
11
+
12
+ self.onmessage = (event: MessageEvent<SpendingSolveRequest>) => {
13
+ try {
14
+ post({ type: 'done', result: runSpendingSolveRequest(event.data) })
15
+ } catch (err) {
16
+ post({ type: 'error', message: err instanceof Error ? err.message : String(err) })
17
+ }
18
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * "Your assumptions" card (trust-and-transparency-layer, step 1): one screen
3
+ * enumerating every live assumption behind this plan's numbers — economy,
4
+ * account returns, longevity, law toggles, and the parameter pack — each
5
+ * tagged user-set / app default / published source, with a copy-export so the
6
+ * run can be replicated in another tool. This is the direct answer to "why do
7
+ * different planners give different answers?": they differ here.
8
+ */
9
+
10
+ import { useEffect, useMemo, useRef, useState } from 'react'
11
+ import { Link } from 'react-router-dom'
12
+
13
+ import { PARAMETER_PROVENANCE } from '@retiregolden/engine/params'
14
+ import { usePlan } from './planContextCore'
15
+ import { currentStartYear } from './useProjection'
16
+ import {
17
+ assumptionsExportJson,
18
+ assumptionsExportText,
19
+ buildAssumptionsSnapshot,
20
+ type AssumptionProvenance,
21
+ } from './assumptionsExport'
22
+
23
+ const PROVENANCE_CHIP: Record<AssumptionProvenance, { label: string; title: string }> = {
24
+ 'user-set': { label: 'You set this', title: 'Entered or changed by you — other tools need this value to match your run.' },
25
+ 'app-default': { label: 'App default', title: "RetireGolden's shipped default — you have not changed it." },
26
+ 'published-source': { label: 'Published source', title: 'Comes from a cited publication (statute, agency figure, or documented dataset).' },
27
+ }
28
+
29
+ function CopyButton({ label, copiedLabel, text }: { label: string; copiedLabel: string; text: () => string }) {
30
+ const [state, setState] = useState<'idle' | 'copied' | 'failed'>('idle')
31
+ const resetTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
32
+ useEffect(() => () => clearTimeout(resetTimer.current), [])
33
+ const flash = (next: 'copied' | 'failed') => {
34
+ setState(next)
35
+ clearTimeout(resetTimer.current)
36
+ resetTimer.current = setTimeout(() => setState('idle'), 2500)
37
+ }
38
+ const copy = () => {
39
+ // The Clipboard API is absent in insecure contexts and some browsers; say
40
+ // so instead of throwing (or faking success).
41
+ if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
42
+ flash('failed')
43
+ return
44
+ }
45
+ navigator.clipboard
46
+ .writeText(text())
47
+ .then(() => flash('copied'))
48
+ .catch(() => flash('failed'))
49
+ }
50
+ return (
51
+ <button type="button" className="btn btn-secondary btn-small" onClick={copy}>
52
+ {state === 'copied' ? copiedLabel : state === 'failed' ? 'Clipboard unavailable — copy manually' : label}
53
+ </button>
54
+ )
55
+ }
56
+
57
+ export function AssumptionsCardPage() {
58
+ const { plan } = usePlan()
59
+ const startYear = currentStartYear()
60
+ const snapshot = useMemo(() => buildAssumptionsSnapshot(plan, startYear), [plan, startYear])
61
+ const sourceById = useMemo(() => new Map(PARAMETER_PROVENANCE.map((s) => [s.id, s])), [])
62
+
63
+ return (
64
+ <section>
65
+ <div className="card">
66
+ <h2>Your assumptions, on one card</h2>
67
+ <p className="card-hint">
68
+ Every number RetireGolden shows follows from the assumptions below — the same plan run through tools with
69
+ different assumptions (or hidden ones) will disagree. Each value is tagged with where it comes from, and the
70
+ copy buttons export the whole card so you can hand your exact assumptions to another tool or a professional
71
+ and see where the inputs differ. (The rest of the plan — incomes, expenses, goals — travels in a plan backup
72
+ from the planner home.)
73
+ </p>
74
+ <div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
75
+ <CopyButton label="Copy as text" copiedLabel="Copied ✓" text={() => assumptionsExportText(snapshot)} />
76
+ <CopyButton label="Copy as JSON" copiedLabel="Copied ✓" text={() => assumptionsExportJson(snapshot)} />
77
+ </div>
78
+ <p className="field-hint" style={{ marginTop: '0.5rem' }}>
79
+ Applies to the {snapshot.packYear} tax year · parameter figures compiled {snapshot.dataAsOf} ·{' '}
80
+ <Link to="/disclaimer">full source list</Link> · <Link to="/how-tested">how RetireGolden is tested</Link>
81
+ </p>
82
+ </div>
83
+
84
+ {snapshot.groups.map((group) => (
85
+ <div className="card" key={group.id}>
86
+ <h3 style={{ marginTop: 0 }}>{group.label}</h3>
87
+ <table className="provenance-table">
88
+ <thead>
89
+ <tr>
90
+ <th scope="col">Assumption</th>
91
+ <th scope="col">Where it comes from</th>
92
+ </tr>
93
+ </thead>
94
+ <tbody>
95
+ {group.rows.map((row) => {
96
+ const source = row.sourceId ? sourceById.get(row.sourceId) : undefined
97
+ const chip = PROVENANCE_CHIP[row.provenance]
98
+ return (
99
+ <tr key={row.id}>
100
+ <th scope="row">
101
+ <span className="provenance-label">{row.label}</span>
102
+ <span className="provenance-figures muted small">{row.value}</span>
103
+ </th>
104
+ <td>
105
+ <span className="muted small" title={chip.title}>
106
+ {chip.label}
107
+ </span>
108
+ {source ? (
109
+ <>
110
+ {' · '}
111
+ <a href={source.url} target="_blank" rel="noopener noreferrer">
112
+ {source.publisher} ↗
113
+ </a>
114
+ </>
115
+ ) : null}
116
+ </td>
117
+ </tr>
118
+ )
119
+ })}
120
+ </tbody>
121
+ </table>
122
+ </div>
123
+ ))}
124
+
125
+ <div className="card">
126
+ <p className="field-hint" style={{ margin: 0 }}>
127
+ Monte Carlo results also depend on the market model, path count, and seed chosen on the{' '}
128
+ <Link to={`/plan/${plan.id}/monte-carlo`}>Monte Carlo page</Link> — your expected returns and inflation above
129
+ stay the center of every model's distribution. To change anything on this card, use{' '}
130
+ <Link to={`/plan/${plan.id}/assumptions`}>Assumptions</Link>, <Link to={`/plan/${plan.id}/accounts`}>Accounts</Link>,
131
+ or <Link to={`/plan/${plan.id}/household`}>Household</Link>.
132
+ </p>
133
+ </div>
134
+ </section>
135
+ )
136
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Bucket reporting lens card (spending-paths & SWR-lenses plan, Goal 5): an
3
+ * opt-in re-reading of the same ledger balances as time-segmented buckets —
4
+ * "the next N years of net spending", then growth. Reporting only: nothing
5
+ * feeds back into the engine, and the honest evidence note stays attached
6
+ * because the bucket literature (Estrada; Kitces) finds no systematic benefit
7
+ * over total-return investing — the comfort is the product, not extra return.
8
+ */
9
+
10
+ import { useMemo, useState } from 'react'
11
+ import {
12
+ Area,
13
+ AreaChart,
14
+ CartesianGrid,
15
+ Legend,
16
+ ResponsiveContainer,
17
+ Tooltip,
18
+ XAxis,
19
+ YAxis,
20
+ } from 'recharts'
21
+
22
+ import type { ProjectionResult } from '@retiregolden/engine/projection/types'
23
+ import { bucketLens, BUCKET_PRESETS } from './bucketLens'
24
+ import { chartTooltipStyle } from './chartStyle'
25
+ import { frameH } from './chartFrame'
26
+ import { fmtMoney, fmtMoneyCompact } from './format'
27
+ import { SelectField } from './fields'
28
+
29
+ const BUCKET_COLORS = ['var(--chart-5)', 'var(--chart-2)', 'var(--chart-1)']
30
+
31
+ export function BucketLensCard({
32
+ result,
33
+ adj,
34
+ }: {
35
+ result: ProjectionResult
36
+ /** Dollar-mode adapter from ResultsPage: (year, nominal) → displayed dollars. */
37
+ adj: (year: number, v: number) => number
38
+ }) {
39
+ const [presetId, setPresetId] = useState<'off' | 'three' | 'two'>('off')
40
+ const preset = BUCKET_PRESETS.find((p) => p.id === presetId) ?? null
41
+
42
+ const rows = useMemo(() => {
43
+ if (!preset) return []
44
+ return bucketLens(result, preset.spans).map((row) => ({
45
+ year: row.year,
46
+ ...Object.fromEntries(row.buckets.map((b, i) => [`b${i}`, adj(row.year, b)])),
47
+ }))
48
+ }, [result, preset, adj])
49
+
50
+ return (
51
+ <div className="chart-card">
52
+ <h2>Bucket view (a reporting lens)</h2>
53
+ <p className="card-hint">
54
+ Many retirees think in buckets: a couple of years of spending in cash, several more in stable assets, the
55
+ rest invested for growth. This view reads the same projected balances that way — bucket 1 holds the next
56
+ years of <em>net</em> spending (spending plus taxes, minus that year&apos;s income), and so on — without
57
+ changing how the plan is invested or simulated. The buckets sum exactly to the investable total every year.
58
+ </p>
59
+ <div className="form-grid">
60
+ <SelectField
61
+ label="Bucket definition"
62
+ help="Purely presentational: pick how the projected balances are segmented. Bucket studies (Estrada; Kitces) find no systematic return or safety benefit from actually managing money as buckets versus a rebalanced total-return portfolio — the value is legibility, so RetireGolden reports buckets but never invests by them."
63
+ value={presetId}
64
+ options={[
65
+ { value: 'off', label: 'Off' },
66
+ ...BUCKET_PRESETS.map((p) => ({ value: p.id, label: p.label })),
67
+ ]}
68
+ onCommit={(v) => setPresetId(v as 'off' | 'three' | 'two')}
69
+ />
70
+ </div>
71
+ {preset ? (
72
+ <>
73
+ <div className="chart-frame" style={frameH(280)}>
74
+ <ResponsiveContainer width="100%" height="100%">
75
+ <AreaChart
76
+ data={rows}
77
+ margin={{ left: 12, right: 8, top: 8 }}
78
+ aria-label="Bucket view: projected balances segmented into spending buckets, year by year"
79
+ >
80
+ <CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
81
+ <XAxis dataKey="year" interval="equidistantPreserveStart" tick={{ fill: 'var(--muted)', fontSize: 12 }} />
82
+ <YAxis tickFormatter={fmtMoneyCompact} tick={{ fill: 'var(--muted)', fontSize: 12 }} width={70} />
83
+ <Legend />
84
+ {preset.bucketLabels.map((label, i) => (
85
+ <Area
86
+ key={label}
87
+ dataKey={`b${i}`}
88
+ stackId="buckets"
89
+ name={label}
90
+ stroke={BUCKET_COLORS[i % BUCKET_COLORS.length]}
91
+ fill={BUCKET_COLORS[i % BUCKET_COLORS.length]}
92
+ fillOpacity={0.55}
93
+ />
94
+ ))}
95
+ <Tooltip
96
+ formatter={(v: unknown) => fmtMoney(Number(v))}
97
+ contentStyle={chartTooltipStyle}
98
+ wrapperStyle={{ zIndex: 2 }}
99
+ />
100
+ </AreaChart>
101
+ </ResponsiveContainer>
102
+ </div>
103
+ <p className="field-hint" style={{ marginTop: '0.5rem' }}>
104
+ Honest labeling: the research record (Estrada&apos;s bucket studies; Kitces) finds bucket{' '}
105
+ <em>management</em> adds no systematic benefit over a rebalanced total-return portfolio — refilling rules
106
+ are market-timing in disguise. RetireGolden therefore simulates your plan total-return and offers buckets
107
+ only as this view. Years where income covers spending show an empty bucket 1; near the end of the plan
108
+ the leading buckets drain because fewer spending years remain.
109
+ </p>
110
+ </>
111
+ ) : null}
112
+ </div>
113
+ )
114
+ }