@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,172 @@
1
+ /**
2
+ * Market-model picker wiring: the plain-language presets shown up front, the
3
+ * full advanced catalog behind the disclosure, and the mapping from a picked
4
+ * kind to a MarketModelConfig. Pure UI-level mapping — every config value
5
+ * written here already existed when the picker was a flat <select>, so a
6
+ * given (kind, seed) pair produces byte-identical Monte Carlo results.
7
+ */
8
+
9
+ import { planUsesAssetAllocation, resolveAssetClassParams } from '@retiregolden/engine/allocation/assetClasses'
10
+ import { ASSET_CLASS_IDS, type AssetClassId } from '@retiregolden/engine/model/plan'
11
+ import type { Plan } from '@retiregolden/engine/model/plan'
12
+ import { buildLognormalModelConfigForPlan, type ClassShockConfig, type MarketModelConfig } from '@retiregolden/engine/montecarlo/marketModels'
13
+
14
+ export type ModelKind =
15
+ | 'lognormal'
16
+ | 'hist-iid'
17
+ | 'hist-block'
18
+ | 'hist-sequence'
19
+ | 'student-t'
20
+ | 'regime-switch'
21
+ | 'cape-conditioned'
22
+ | 'stationary'
23
+ | 'empirical'
24
+ | 'garch'
25
+ | 'inflation-regime'
26
+ | 'reversed-history'
27
+ | 'user-shock'
28
+ | 'gaussian'
29
+ | 'ar1'
30
+
31
+ export type ModelPresetId = 'smooth' | 'history' | 'stress'
32
+
33
+ export interface ModelPreset {
34
+ id: ModelPresetId
35
+ label: string
36
+ description: string
37
+ kind: ModelKind
38
+ }
39
+
40
+ /** The three plain-language choices shown before the advanced disclosure. */
41
+ export const MODEL_PRESETS: ReadonlyArray<ModelPreset> = [
42
+ {
43
+ id: 'smooth',
44
+ label: 'Smooth randomness (recommended)',
45
+ description: 'Each year varies around your expected return, like a bell curve.',
46
+ kind: 'lognormal',
47
+ },
48
+ {
49
+ id: 'history',
50
+ label: 'Replay real history',
51
+ description: 'Your retirement starts in a random past year and history unfolds from there.',
52
+ kind: 'hist-sequence',
53
+ },
54
+ {
55
+ id: 'stress',
56
+ label: 'Stress test',
57
+ description: 'A 25% market drop hits in year one, then markets vary normally.',
58
+ kind: 'user-shock',
59
+ },
60
+ ]
61
+
62
+ export const PRESET_FAMILY_LABELS: Record<ModelPresetId, string> = {
63
+ smooth: 'Smooth randomness',
64
+ history: 'Replay real history',
65
+ stress: 'Stress test',
66
+ }
67
+
68
+ /** The full advanced catalog. Every kind the old flat <select> offered stays reachable. */
69
+ export const MODEL_CATALOG: ReadonlyArray<{ kind: ModelKind; label: string; family: ModelPresetId }> = [
70
+ { kind: 'lognormal', label: 'Lognormal (correlated inflation)', family: 'smooth' },
71
+ { kind: 'hist-iid', label: 'Historical bootstrap — independent years', family: 'history' },
72
+ { kind: 'hist-block', label: 'Historical bootstrap — 5-year blocks', family: 'history' },
73
+ { kind: 'hist-sequence', label: 'Historical full-sequence replay', family: 'history' },
74
+ { kind: 'student-t', label: 'Student-t (fat tails)', family: 'smooth' },
75
+ { kind: 'regime-switch', label: 'Regime-switching (bull/bear Markov)', family: 'smooth' },
76
+ { kind: 'cape-conditioned', label: 'CAPE-conditioned mean reversion', family: 'smooth' },
77
+ { kind: 'stationary', label: 'Stationary bootstrap (variable blocks)', family: 'history' },
78
+ { kind: 'empirical', label: 'Empirical historical (mean-preserving)', family: 'history' },
79
+ { kind: 'garch', label: 'GARCH(1,1) vol clustering', family: 'smooth' },
80
+ { kind: 'inflation-regime', label: 'Inflation regime (fat/high-infl)', family: 'smooth' },
81
+ { kind: 'reversed-history', label: 'Reversed-history windows', family: 'stress' },
82
+ { kind: 'user-shock', label: 'User shock scenario (year-1 crash)', family: 'stress' },
83
+ { kind: 'gaussian', label: 'Gaussian (additive normal)', family: 'smooth' },
84
+ { kind: 'ar1', label: 'AR(1) mean-reverting shocks', family: 'smooth' },
85
+ ]
86
+
87
+ export function presetFamilyOf(kind: ModelKind): ModelPresetId {
88
+ return MODEL_CATALOG.find((entry) => entry.kind === kind)?.family ?? 'smooth'
89
+ }
90
+
91
+ export function catalogLabelOf(kind: ModelKind): string {
92
+ return MODEL_CATALOG.find((entry) => entry.kind === kind)?.label ?? kind
93
+ }
94
+
95
+ function buildClassShocks(plan: Plan): ClassShockConfig | undefined {
96
+ if (!planUsesAssetAllocation(plan)) return undefined
97
+ const params = resolveAssetClassParams(plan.assumptions.assetClassParams)
98
+ const volatilityPctByClass = Object.fromEntries(
99
+ ASSET_CLASS_IDS.map((id) => [id, params[id].volatilityPct]),
100
+ ) as Record<AssetClassId, number>
101
+ return { volatilityPctByClass }
102
+ }
103
+
104
+ export function buildModel(
105
+ kind: ModelKind,
106
+ inflationMeanPct: number,
107
+ returnVolPct: number,
108
+ equityWeightPct: number,
109
+ plan: Plan,
110
+ ): MarketModelConfig {
111
+ // Plans with allocated accounts get per-class correlated shocks sharing the
112
+ // deterministic ledger's allocation schema; single-return plans keep the
113
+ // single-factor model (and their exact seeded paths) unchanged.
114
+ const classShocks = buildClassShocks(plan)
115
+ if (kind === 'lognormal') {
116
+ return {
117
+ ...buildLognormalModelConfigForPlan(plan, returnVolPct),
118
+ inflationMeanPct,
119
+ }
120
+ }
121
+ if (kind === 'hist-iid' || kind === 'hist-block' || kind === 'hist-sequence') {
122
+ return {
123
+ type: 'historical',
124
+ mode: kind === 'hist-iid' ? 'iid' : kind === 'hist-block' ? 'block' : 'sequence',
125
+ equityWeightPct,
126
+ classShocks: !!classShocks,
127
+ }
128
+ }
129
+ if (kind === 'student-t') {
130
+ return { type: 'student-t', inflationMeanPct, returnVolPct, classShocks }
131
+ }
132
+ if (kind === 'regime-switch') {
133
+ return { type: 'regime-switch', inflationMeanPct, classShocks }
134
+ }
135
+ if (kind === 'cape-conditioned') {
136
+ return { type: 'cape-conditioned', inflationMeanPct, returnVolPct, startingCape: 28, classShocks }
137
+ }
138
+ if (kind === 'stationary') {
139
+ return { type: 'stationary', equityWeightPct, classShocks: !!classShocks }
140
+ }
141
+ if (kind === 'empirical') {
142
+ // Default to centered (mean-preserving) for correctness. Non-centered (raw historical
143
+ // as shock) can overstate results because simulate always does expected + shock.
144
+ // Non-centered kept in config for advanced/experimental use.
145
+ return { type: 'empirical', equityWeightPct, centered: true, classShocks: !!classShocks }
146
+ }
147
+ if (kind === 'garch') {
148
+ return { type: 'garch', inflationMeanPct, returnVolScalePct: returnVolPct, classShocks }
149
+ }
150
+ if (kind === 'inflation-regime') {
151
+ return { type: 'inflation-regime', baseInflationMeanPct: inflationMeanPct, classShocks }
152
+ }
153
+ if (kind === 'reversed-history') {
154
+ return { type: 'reversed-history', equityWeightPct, windowLengthYears: 10, classShocks: !!classShocks }
155
+ }
156
+ if (kind === 'user-shock') {
157
+ return { type: 'user-shock', inflationMeanPct, shockYear: 1, shockPct: -25, baseReturnVolPct: returnVolPct, classShocks }
158
+ }
159
+ if (kind === 'gaussian') {
160
+ return { type: 'gaussian', inflationMeanPct, returnVolPct, classShocks }
161
+ }
162
+ if (kind === 'ar1') {
163
+ return { type: 'ar1', inflationMeanPct, returnVolPct, phi: 0.3, classShocks }
164
+ }
165
+ // fallback
166
+ return {
167
+ type: 'historical',
168
+ mode: 'iid',
169
+ equityWeightPct,
170
+ classShocks: !!classShocks,
171
+ }
172
+ }
@@ -0,0 +1,40 @@
1
+ import type { OptimizedSchedule } from '@retiregolden/engine/strategies/optimizer'
2
+ import type { OptimizePostProcessing } from '../optimize/messages'
3
+
4
+ export interface OptimizeChartRow {
5
+ year: number
6
+ requested: number
7
+ cleaned: number
8
+ executed: number
9
+ }
10
+
11
+ export function buildOptimizeChartRows({
12
+ schedule,
13
+ recommendedConversions,
14
+ postProcessed,
15
+ candidateWins,
16
+ }: {
17
+ schedule: OptimizedSchedule | null
18
+ recommendedConversions: { year: number; amount: number }[]
19
+ postProcessed: OptimizePostProcessing | null
20
+ candidateWins: boolean
21
+ }): OptimizeChartRow[] {
22
+ const rawByYear = new Map(schedule?.conversions.map((c) => [c.year, c.amount]) ?? [])
23
+ const cleanedByYear = new Map(recommendedConversions.map((c) => [c.year, c.amount]))
24
+ // A winning candidate's schedule is its exact-ledger execution; the MILP path
25
+ // reports the cleaned re-run's executed amounts.
26
+ const executedByYear = candidateWins
27
+ ? cleanedByYear
28
+ : new Map(postProcessed?.cleanedExecutionByYear.map((year) => [year.year, year.rothConversion]) ?? [])
29
+ const years = [...new Set([...rawByYear.keys(), ...cleanedByYear.keys()])].sort((a, b) => a - b)
30
+ return years.map((year) => ({
31
+ year,
32
+ requested: Math.round(rawByYear.get(year) ?? 0),
33
+ cleaned: Math.round(cleanedByYear.get(year) ?? 0),
34
+ executed: Math.round(executedByYear.get(year) ?? 0),
35
+ }))
36
+ }
37
+
38
+ export function shouldShowRecommendedScheduleBars(candidateWins: boolean, hasExecutionMismatch: boolean): boolean {
39
+ return candidateWins || hasExecutionMismatch
40
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Claim-age co-optimization helpers for the Optimize page
3
+ * (optimizer-exact-ledger-convergence Track 1, Step 5). Pure functions so the load-bearing apply contract is unit-testable
4
+ * without rendering: when the co-optimizer recommends a claim change, the
5
+ * returned conversion schedule was computed against the CLAIM-PATCHED plan, so
6
+ * the claim change and the schedule must always be installed together —
7
+ * conversions applied to the unpatched plan are a different (wrong) plan.
8
+ */
9
+
10
+ import type { Plan } from '@retiregolden/engine/model/plan'
11
+ import type { ClaimAgeCoOptimization } from '@retiregolden/engine/projection/optimizePlan'
12
+
13
+ /** Exact-estate improvement of the joint optimum over the current-claim optimum. */
14
+ export function claimEstateGain(claimAge: ClaimAgeCoOptimization | null): number {
15
+ if (!claimAge?.winningClaimPatch) return 0
16
+ return claimAge.jointExactEstate - claimAge.currentClaimExactEstate
17
+ }
18
+
19
+ /**
20
+ * The plan the recommendation was actually computed against: the current plan
21
+ * with the winning claim change installed, or the plan itself when the current
22
+ * claim ages won (or co-optimization did not run). Monte Carlo and the report
23
+ * must consume this plan, never the unpatched one.
24
+ *
25
+ * ASSUMPTION: a claim patch touches ONLY `incomes`. That mirrors the engine's
26
+ * capture (`winningPatch = { incomes: patchedPlan.incomes }` in
27
+ * optimizePlanCoOptimizingClaimAge) and the `winningClaimPatch` wire type, but
28
+ * the claim generator flows through the general `applyScenarioPatch`, so
29
+ * nothing at compile time enforces it. If the engine ever widens the patch,
30
+ * this helper AND `applyOptimizeRecommendation` below must widen with it (or
31
+ * better: ship the engine's `optimizedPlan` across the worker boundary).
32
+ */
33
+ export function planWithWinningClaim(plan: Plan, claimAge: ClaimAgeCoOptimization | null): Plan {
34
+ if (!claimAge?.winningClaimPatch) return plan
35
+ return { ...plan, incomes: claimAge.winningClaimPatch.incomes }
36
+ }
37
+
38
+ /**
39
+ * Install the joint recommendation on a plan draft: the winning claim change
40
+ * (when one won) and the conversion schedule, atomically. An empty conversions
41
+ * list leaves the existing conversion strategy in place — that is the correct
42
+ * joint recommendation when the incumbent schedule holds under the new claim.
43
+ */
44
+ export function applyOptimizeRecommendation(
45
+ draft: Plan,
46
+ args: {
47
+ claimAge: ClaimAgeCoOptimization | null
48
+ conversions: { year: number; amount: number }[]
49
+ mode: 'optimized' | 'manual'
50
+ nowIso?: string
51
+ },
52
+ ): void {
53
+ if (args.claimAge?.winningClaimPatch) draft.incomes = args.claimAge.winningClaimPatch.incomes
54
+ if (args.conversions.length > 0) {
55
+ draft.strategies.rothConversion =
56
+ args.mode === 'manual'
57
+ ? { mode: 'manual', conversions: args.conversions }
58
+ : {
59
+ mode: 'optimized',
60
+ conversions: args.conversions,
61
+ optimizedAtIso: args.nowIso ?? new Date().toISOString(),
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Display-layer completeness: a plan with no income sources and nothing that
3
+ * can actually fund spending is still being set up, so verdict framing (red
4
+ * "portfolio depletes") would just read as doom while the user types.
5
+ * Debts never count as funding (their balance is money owed), and
6
+ * payment/value account types must carry a positive amount.
7
+ */
8
+
9
+ import type { Account, Plan } from '@retiregolden/engine/model/plan'
10
+
11
+ function accountProvidesFunding(account: Account): boolean {
12
+ switch (account.type) {
13
+ case 'debt':
14
+ return false
15
+ case 'pension':
16
+ case 'annuity':
17
+ return account.monthlyAmount > 0
18
+ case 'property':
19
+ return account.value > 0
20
+ default:
21
+ return account.balance > 0
22
+ }
23
+ }
24
+
25
+ export function isPlanIncomplete(plan: Pick<Plan, 'incomes' | 'accounts'>): boolean {
26
+ return plan.incomes.length === 0 && !plan.accounts.some(accountProvidesFunding)
27
+ }
@@ -0,0 +1,26 @@
1
+ /** Context + hook for the plan workspace (separate file keeps fast refresh happy). */
2
+
3
+ import { createContext, useContext } from 'react'
4
+
5
+ import type { Plan } from '@retiregolden/engine/model/plan'
6
+
7
+ export type SaveState = 'loading' | 'saved' | 'saving' | 'dirty' | 'invalid' | 'error'
8
+
9
+ export interface PlanContextValue {
10
+ plan: Plan
11
+ /** Mutate a draft clone of the plan; the result is validated and autosaved. */
12
+ update: (mutator: (draft: Plan) => void) => void
13
+ /** Cancel pending autosave so an unmount flush cannot resurrect a deleted demo. */
14
+ discardPendingSave: () => void
15
+ saveState: SaveState
16
+ /** Zod issues for the current (possibly unsaved) state; empty when valid. */
17
+ issues: string[]
18
+ }
19
+
20
+ export const PlanCtx = createContext<PlanContextValue | null>(null)
21
+
22
+ export function usePlan(): PlanContextValue {
23
+ const v = useContext(PlanCtx)
24
+ if (!v) throw new Error('usePlan must be used inside <PlanProvider>')
25
+ return v
26
+ }