@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,77 @@
1
+ /**
2
+ * Background Monte Carlo success rate for headline surfaces (the KPI bar and
3
+ * the Results verdict). Runs the MC page's exact default configuration — same
4
+ * seed, model, and path count — so headline numbers always match what the
5
+ * Monte Carlo page shows on arrival. Debounced well past the autosave window;
6
+ * failures stay silent here (the MC page owns the full error/retry surface).
7
+ *
8
+ * Concurrent subscribers share one run per plan object via the in-flight map,
9
+ * so the KPI bar and the verdict never trigger two identical 1,000-path
10
+ * simulations for the same plan.
11
+ */
12
+
13
+ import { useEffect, useRef, useState } from 'react'
14
+
15
+ import type { Plan } from '@retiregolden/engine/model/plan'
16
+ import { DEFAULT_PATH_COUNT, runMonteCarlo } from '../mc/pool'
17
+ import { buildModel } from './marketModelPicker'
18
+ import { currentStartYear, seedFromPlanId } from './useProjection'
19
+
20
+ const MC_DEBOUNCE_MS = 1200
21
+
22
+ const inflight = new WeakMap<Plan, Promise<number>>()
23
+
24
+ function successRateOf(plan: Plan): Promise<number> {
25
+ const existing = inflight.get(plan)
26
+ if (existing !== undefined) return existing
27
+ const model = buildModel('lognormal', plan.assumptions.inflationPct, 12, 60, plan)
28
+ const run = runMonteCarlo(plan, {
29
+ startYear: currentStartYear(),
30
+ pathCount: DEFAULT_PATH_COUNT,
31
+ seed: seedFromPlanId(plan.id),
32
+ model,
33
+ }).then((s) => s.successRate)
34
+ // Successful runs stay cached (later subscribers reuse the result), but a
35
+ // rejection is evicted so the next subscriber retries instead of replaying
36
+ // a transient worker failure forever for this plan object.
37
+ run.catch(() => {
38
+ inflight.delete(plan)
39
+ })
40
+ inflight.set(plan, run)
41
+ return run
42
+ }
43
+
44
+ export function useMcSuccessRate(plan: Plan, enabled: boolean): number | null {
45
+ // The rate is stored WITH the plan it was computed for, and derived to null
46
+ // whenever the current plan differs — so a headline number can never show a
47
+ // previous plan's rate through the debounce + recompute, and a silently
48
+ // failed re-run can never leave a stale rate up (edits produce a new plan
49
+ // object via structuredClone, so reference identity is the right key).
50
+ const [snapshot, setSnapshot] = useState<{ plan: Plan; rate: number } | null>(null)
51
+ const runToken = useRef(0)
52
+ useEffect(() => {
53
+ if (!enabled) return undefined
54
+ const token = ++runToken.current
55
+ const attach = () => {
56
+ successRateOf(plan)
57
+ .then((rate) => {
58
+ if (token === runToken.current) setSnapshot({ plan, rate })
59
+ })
60
+ .catch(() => {
61
+ /* silent — the Monte Carlo page carries the error state and retry */
62
+ })
63
+ }
64
+ // A run for this plan already exists (typically started by the KPI bar):
65
+ // attach immediately. The debounce only guards against launching fresh
66
+ // simulations mid-edit, and attaching to an existing run starts none.
67
+ if (inflight.get(plan) !== undefined) {
68
+ attach()
69
+ return undefined
70
+ }
71
+ const t = window.setTimeout(attach, MC_DEBOUNCE_MS)
72
+ return () => {
73
+ window.clearTimeout(t)
74
+ }
75
+ }, [plan, enabled])
76
+ return enabled && snapshot !== null && snapshot.plan === plan ? snapshot.rate : null
77
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Live deterministic projection: memoized simulatePlan over the standard tax
3
+ * stack (federal engine + flat state rate — same stack the Monte Carlo
4
+ * workers use, see src/mc/runRequest.ts). Fast enough (<5 ms typical) to run
5
+ * on every committed keystroke.
6
+ */
7
+
8
+ import { useMemo } from 'react'
9
+
10
+ import type { Plan } from '@retiregolden/engine/model/plan'
11
+ import { summarizeProjection, type ProjectionSummary } from '@retiregolden/engine/projection/compare'
12
+ import { simulatePlan } from '@retiregolden/engine/projection/simulate'
13
+ import type { ProjectionResult } from '@retiregolden/engine/projection/types'
14
+ import { combineTaxCalculators, createFederalTaxCalculator } from '@retiregolden/engine/tax/federalTax'
15
+ import { createStateTaxCalculator } from '@retiregolden/engine/tax/stateTax'
16
+
17
+ export function taxCalculatorFor(plan: Plan) {
18
+ return combineTaxCalculators(
19
+ createFederalTaxCalculator(),
20
+ createStateTaxCalculator({
21
+ overridePct: plan.assumptions.stateEffectiveTaxPct,
22
+ localPct: plan.assumptions.localIncomeTaxPct,
23
+ }),
24
+ )
25
+ }
26
+
27
+ export function currentStartYear(): number {
28
+ return new Date().getFullYear()
29
+ }
30
+
31
+ export interface ProjectionView {
32
+ result: ProjectionResult
33
+ summary: ProjectionSummary
34
+ startYear: number
35
+ /** Divide a nominal amount in `year` by this to get today's dollars. */
36
+ deflate: (year: number, amount: number) => number
37
+ }
38
+
39
+ export function projectPlan(plan: Plan, startYear = currentStartYear()): ProjectionView {
40
+ const result = simulatePlan(plan, { startYear, taxCalculator: taxCalculatorFor(plan) })
41
+ const summary = summarizeProjection(plan, result)
42
+ const r = 1 + plan.assumptions.inflationPct / 100
43
+ return {
44
+ result,
45
+ summary,
46
+ startYear,
47
+ deflate: (year, amount) => amount / Math.pow(r, year - startYear),
48
+ }
49
+ }
50
+
51
+ export function useProjection(plan: Plan): ProjectionView {
52
+ return useMemo(() => projectPlan(plan), [plan])
53
+ }
54
+
55
+ /** Stable Monte Carlo seed per plan (re-rollable in the UI). */
56
+ export function seedFromPlanId(id: string): number {
57
+ let h = 2166136261
58
+ for (let i = 0; i < id.length; i++) {
59
+ h ^= id.charCodeAt(i)
60
+ h = Math.imul(h, 16777619)
61
+ }
62
+ return h >>> 0
63
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Wire types between the Relocation Compare page and its worker. Same rules
3
+ * as src/mc/messages.ts: everything must survive structured clone — the
4
+ * comparison rows are plain data (numbers, strings, small arrays).
5
+ */
6
+
7
+ import type { Plan } from '@retiregolden/engine/model/plan'
8
+ import type { MarketModelConfig } from '@retiregolden/engine/montecarlo/marketModels'
9
+ import type { RelocationCandidate, RelocationComparison } from '@retiregolden/engine/projection/relocation'
10
+
11
+ export interface RelocationCompareRequest {
12
+ plan: Plan
13
+ candidates: RelocationCandidate[]
14
+ startYear: number
15
+ /** When present, rows get a Monte Carlo success rate on shared market paths. */
16
+ monteCarlo?: { model: MarketModelConfig; pathCount: number; seed: number } | null
17
+ }
18
+
19
+ export type RelocationCompareResponse =
20
+ | { type: 'done'; result: RelocationComparison }
21
+ | { type: 'error'; message: string }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Web Worker entry for the relocation compare: one request per run — up to
3
+ * six deterministic full-plan simulations plus the optional shared-path
4
+ * Monte Carlo — posted back as one summarized result.
5
+ */
6
+
7
+ import type { RelocationCompareRequest, RelocationCompareResponse } from './messages'
8
+ import { runRelocationCompareRequest } from './runRelocation'
9
+
10
+ const post = (msg: RelocationCompareResponse) => (self as unknown as Worker).postMessage(msg)
11
+
12
+ self.onmessage = (event: MessageEvent<RelocationCompareRequest>) => {
13
+ try {
14
+ post({ type: 'done', result: runRelocationCompareRequest(event.data) })
15
+ } catch (err) {
16
+ post({ type: 'error', message: err instanceof Error ? err.message : String(err) })
17
+ }
18
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Executes one relocation-compare sweep. Shared by the Web Worker entry and
3
+ * the synchronous fallback (tests / no-Worker environments). The engine
4
+ * builds the app-standard per-candidate tax stacks itself (federal + modeled
5
+ * state packs with the plan's flat rate as an override), so both paths run
6
+ * identical numbers.
7
+ */
8
+
9
+ import { compareRelocationCandidates, type RelocationComparison } from '@retiregolden/engine/projection/relocation'
10
+ import type { RelocationCompareRequest } from './messages'
11
+
12
+ export function runRelocationCompareRequest(req: RelocationCompareRequest): RelocationComparison {
13
+ return compareRelocationCandidates(req.plan, req.candidates, {
14
+ startYear: req.startYear,
15
+ monteCarlo: req.monteCarlo ?? null,
16
+ })
17
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Spawns the relocation-compare sweep in a Web Worker (lazy-loaded only here)
3
+ * and resolves the comparison. Falls back to a synchronous in-process run
4
+ * where Worker is unavailable (tests, very old browsers), mirroring
5
+ * src/optimize/spendingRunner.ts.
6
+ */
7
+
8
+ import type { RelocationComparison } from '@retiregolden/engine/projection/relocation'
9
+ import { runWorkerRequest } from '../workers/run'
10
+ import type { RelocationCompareRequest, RelocationCompareResponse } from './messages'
11
+ import { runRelocationCompareRequest } from './runRelocation'
12
+
13
+ export function runRelocationCompare(req: RelocationCompareRequest): Promise<RelocationComparison> {
14
+ if (typeof Worker === 'undefined') return Promise.resolve(runRelocationCompareRequest(req))
15
+ return runWorkerRequest<RelocationCompareRequest, RelocationCompareResponse, RelocationComparison>({
16
+ request: req,
17
+ createWorker: () => new Worker(new URL('./relocation.worker.ts', import.meta.url), { type: 'module' }),
18
+ interpret: (msg) =>
19
+ msg.type === 'done' ? { kind: 'done', result: msg.result } : { kind: 'error', message: msg.message },
20
+ errorLabel: 'Relocation compare worker failed',
21
+ })
22
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Threads the host's `reportBranding` prop (see `PlannerApp`) from the app
3
+ * shell down to the pages with "Download HTML report" buttons, so a host can
4
+ * brand downloaded reports without touching the planner tree. Null means "no
5
+ * branding supplied" — the report renders its RetireGolden defaults.
6
+ */
7
+ import { createContext, useContext } from 'react'
8
+
9
+ import type { ReportBranding } from './reportHtml'
10
+
11
+ export const ReportBrandingContext = createContext<ReportBranding | null>(null)
12
+
13
+ export function useReportBranding(): ReportBranding | null {
14
+ return useContext(ReportBrandingContext)
15
+ }
@@ -0,0 +1,34 @@
1
+ import type { Plan } from '@retiregolden/engine/model/plan'
2
+ import type { ProjectionSummary } from '@retiregolden/engine/projection/compare'
3
+ import type { ProjectionResult } from '@retiregolden/engine/projection/types'
4
+ import { buildStandaloneReportHtml, type ReportBranding, type ReportRecommendationEvidence } from './reportHtml'
5
+
6
+ function slug(name: string): string {
7
+ return name
8
+ .trim()
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9]+/g, '-')
11
+ .replace(/^-+|-+$/g, '')
12
+ }
13
+
14
+ function fileStem(name: string): string {
15
+ return slug(name) || 'retiregolden-plan'
16
+ }
17
+
18
+ export function downloadStandaloneReport(args: {
19
+ plan: Plan
20
+ result: ProjectionResult
21
+ summary: ProjectionSummary
22
+ startYear: number
23
+ recommendationEvidence?: ReportRecommendationEvidence | null
24
+ branding?: ReportBranding | null
25
+ }): void {
26
+ const html = buildStandaloneReportHtml(args)
27
+ const brandStem = slug(args.branding?.productName ?? '') || 'retiregolden'
28
+ const blob = new Blob([html], { type: 'text/html' })
29
+ const a = document.createElement('a')
30
+ a.href = URL.createObjectURL(blob)
31
+ a.download = `${fileStem(args.plan.name)}-${brandStem}-report.html`
32
+ a.click()
33
+ URL.revokeObjectURL(a.href)
34
+ }