@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,340 @@
1
+ import { useState } from 'react'
2
+ import { Link, useNavigate } from 'react-router-dom'
3
+ import type { Plan } from '@retiregolden/engine/model/plan'
4
+ import { usePlan } from '../planContextCore'
5
+ import { useProjection, taxCalculatorFor, seedFromPlanId } from '../useProjection'
6
+ import { packForYear } from '@retiregolden/engine/params'
7
+ import { applyScenarioPatch } from '@retiregolden/engine/scenarios/scenarios'
8
+ import { createDecisionContext, evaluateInsightAction } from '@retiregolden/engine/decisions'
9
+ import { runMonteCarlo } from '../../mc/pool'
10
+ import { registry } from '@retiregolden/engine/insights/registry'
11
+ import type { InsightAction, InsightCard, InsightImpact } from '@retiregolden/engine/insights/types'
12
+ import { LearnLink } from '../../learn/LearnLink'
13
+ import { fmtMoney, fmtMoneyCompact } from '../format'
14
+
15
+ function makeScenarioId(): string {
16
+ return typeof crypto !== 'undefined' && 'randomUUID' in crypto
17
+ ? `scenario-${crypto.randomUUID()}`
18
+ : `scenario-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
19
+ }
20
+
21
+ function uniqueScenarioName(baseName: string, plan: Plan): string {
22
+ const names = new Set(plan.scenarios.map((scenario) => scenario.name))
23
+ if (!names.has(baseName)) return baseName
24
+
25
+ let suffix = 2
26
+ while (names.has(`${baseName} (${suffix})`)) {
27
+ suffix += 1
28
+ }
29
+ return `${baseName} (${suffix})`
30
+ }
31
+
32
+ export function InsightCardView({ card, onDismiss }: { card: InsightCard; onDismiss: () => void }) {
33
+ const { plan, update } = usePlan()
34
+ const projectionView = useProjection(plan)
35
+ const navigate = useNavigate()
36
+
37
+ const [expanded, setExpanded] = useState(false)
38
+ const [exactImpact, setExactImpact] = useState<InsightImpact | null>(null)
39
+ // Detectors may refine their action during evaluate() (e.g. the spending
40
+ // headroom card solves the exact level); Add-as-scenario must use that one.
41
+ const [exactAction, setExactAction] = useState<InsightAction | null>(null)
42
+ const [mcDelta, setMcDelta] = useState<number | null>(null)
43
+ const [loadingExact, setLoadingExact] = useState(false)
44
+ const [loadingMc, setLoadingMc] = useState(false)
45
+ const [previewError, setPreviewError] = useState<string | null>(null)
46
+ const [undoPlan, setUndoPlan] = useState<Plan | null>(null)
47
+
48
+ const handleToggleExpand = async () => {
49
+ const nextExpanded = !expanded
50
+ setExpanded(nextExpanded)
51
+ setPreviewError(null)
52
+
53
+ if (nextExpanded && card.action.kind === 'preview-scenario' && !exactImpact && !loadingExact) {
54
+ setLoadingExact(true)
55
+ try {
56
+ const detector = registry.find((d) => d.id === card.id)
57
+ if (!detector || !detector.evaluate) {
58
+ setPreviewError('This insight cannot be previewed yet.')
59
+ return
60
+ }
61
+
62
+ const paramsLookup = packForYear(projectionView.startYear)
63
+ const ctx = {
64
+ plan,
65
+ projection: {
66
+ result: projectionView.result,
67
+ summary: projectionView.summary,
68
+ startYear: projectionView.startYear,
69
+ deflate: projectionView.deflate,
70
+ },
71
+ params: paramsLookup.pack,
72
+ }
73
+
74
+ const evalResult = detector.evaluate(ctx)
75
+ if (evalResult.action.kind === 'preview-scenario') {
76
+ const patch = evalResult.action.patch
77
+ const applied = applyScenarioPatch(plan, patch)
78
+ if (applied.ok) {
79
+ // Shared exact-ledger evaluator (decision engine): the same core
80
+ // the Roth & Tax Optimizer tournament uses, so this card's exact
81
+ // deltas and recommendation state match that surface. Baseline is
82
+ // reused from the memoized projection — one extra simulate() only.
83
+ const decisionCtx = createDecisionContext(
84
+ plan,
85
+ { startYear: projectionView.startYear, taxCalculator: taxCalculatorFor(plan) },
86
+ { result: projectionView.result, summary: projectionView.summary },
87
+ // Per-candidate tax stacks: a preview patch may change tax
88
+ // assumptions (e.g. relocation clearing the flat override).
89
+ taxCalculatorFor,
90
+ )
91
+ const { evaluation, impact } = evaluateInsightAction(decisionCtx, card, evalResult.action)
92
+ if (evaluation.recommendationState === 'diagnostic') {
93
+ setPreviewError(evaluation.diagnostics.join(' ') || 'This insight could not be compared against the base plan.')
94
+ } else {
95
+ // Keep the detector's own evaluated summary line (e.g. the solved
96
+ // spending level) alongside the shared evaluator's exact deltas.
97
+ setExactImpact(
98
+ evalResult.impact?.qualitative ? { ...impact, qualitative: evalResult.impact.qualitative } : impact,
99
+ )
100
+ setExactAction(evalResult.action)
101
+
102
+ // If it impacts Monte Carlo success rate, run async MC pool query
103
+ if (card.impact.successRateDeltaPct !== undefined) {
104
+ setLoadingMc(true)
105
+ const seed = seedFromPlanId(plan.id)
106
+ const model = { type: 'historical' as const, mode: 'iid' as const, equityWeightPct: 60 }
107
+ const mcOpts = {
108
+ startYear: projectionView.startYear,
109
+ pathCount: 250, // fast preview size
110
+ seed,
111
+ model,
112
+ }
113
+ const [baseMc, patchMc] = await Promise.all([
114
+ runMonteCarlo(plan, mcOpts),
115
+ runMonteCarlo(applied.plan, mcOpts),
116
+ ])
117
+ setMcDelta((patchMc.successRate - baseMc.successRate) * 100)
118
+ setLoadingMc(false)
119
+ }
120
+ }
121
+ } else {
122
+ setPreviewError(`This suggestion can't be applied to your plan: ${applied.issues.join('; ')}`)
123
+ }
124
+ }
125
+ } catch (err) {
126
+ setPreviewError(err instanceof Error ? err.message : 'Failed to evaluate this insight.')
127
+ setLoadingMc(false)
128
+ } finally {
129
+ setLoadingExact(false)
130
+ }
131
+ }
132
+ }
133
+
134
+ const handleAddScenario = () => {
135
+ const action = exactAction?.kind === 'preview-scenario' ? exactAction : card.action
136
+ if (action.kind !== 'preview-scenario') return
137
+ const applied = applyScenarioPatch(plan, action.patch)
138
+ if (!applied.ok) {
139
+ setPreviewError(`This suggestion can't be applied to your plan: ${applied.issues.join('; ')}`)
140
+ return
141
+ }
142
+ const name = uniqueScenarioName(action.scenarioName, plan)
143
+ update((d) => {
144
+ d.scenarios.push({
145
+ id: makeScenarioId(),
146
+ name,
147
+ patch: action.patch,
148
+ })
149
+ })
150
+ navigate(`/plan/${plan.id}/scenarios`)
151
+ }
152
+
153
+ const handleApplyToggle = () => {
154
+ if (card.action.kind !== 'apply-toggle') return
155
+ const applied = applyScenarioPatch(plan, card.action.patch)
156
+ if (!applied.ok) {
157
+ setPreviewError(`This change can't be applied to your plan: ${applied.issues.join('; ')}`)
158
+ return
159
+ }
160
+ setUndoPlan(plan)
161
+ update((d) => {
162
+ Object.assign(d, applied.plan)
163
+ })
164
+ }
165
+
166
+ const handleUndoToggle = () => {
167
+ if (!undoPlan) return
168
+ const previous = undoPlan
169
+ setUndoPlan(null)
170
+ update((d) => {
171
+ Object.assign(d, previous)
172
+ })
173
+ }
174
+
175
+ // Formatting helpers
176
+ const formatDelta = (val: number, isGoodPositive = true) => {
177
+ if (val === 0) return 'no change'
178
+ const sign = val > 0 ? '+' : ''
179
+ const good = (val > 0 && isGoodPositive) || (val < 0 && !isGoodPositive)
180
+ return (
181
+ <span className={good ? 'delta-pos' : 'delta-neg'}>
182
+ {sign}
183
+ {fmtMoney(val)}
184
+ </span>
185
+ )
186
+ }
187
+
188
+ const confidenceChips = {
189
+ high: { className: 'type-chip type-chip--good', label: 'High Confidence' },
190
+ medium: { className: 'type-chip type-chip--warn', label: 'Medium Confidence' },
191
+ low: { className: 'type-chip type-chip--muted', label: 'Low Confidence' },
192
+ }
193
+ const confidence = confidenceChips[card.confidence]
194
+
195
+ return (
196
+ <div className="card insight-card">
197
+ <button
198
+ type="button"
199
+ className="btn-ghost insight-dismiss"
200
+ onClick={onDismiss}
201
+ aria-label="Dismiss this recommendation"
202
+ title="Dismiss this recommendation"
203
+ >
204
+ <svg
205
+ viewBox="0 0 24 24"
206
+ width="16"
207
+ height="16"
208
+ fill="none"
209
+ stroke="currentColor"
210
+ strokeWidth="2"
211
+ strokeLinecap="round"
212
+ aria-hidden="true"
213
+ >
214
+ <line x1="6" y1="6" x2="18" y2="18" />
215
+ <line x1="18" y1="6" x2="6" y2="18" />
216
+ </svg>
217
+ </button>
218
+
219
+ {/* Cards always render under their category's group heading, so the
220
+ category chip would just repeat it — only the confidence chip earns
221
+ its place. */}
222
+ <div className="insight-badges">
223
+ <span className={confidence.className}>{confidence.label}</span>
224
+ </div>
225
+
226
+ <h3 className="insight-card-title">{card.title}</h3>
227
+ <p className="insight-rationale">{card.rationale}</p>
228
+
229
+ {/* Impact Section */}
230
+ <div className="insight-impact-box">
231
+ {expanded && exactImpact ? (
232
+ <div>
233
+ {exactImpact.qualitative ? <p>{exactImpact.qualitative}</p> : null}
234
+ <div className="insight-impact-grid">
235
+ {exactImpact.endingAfterTaxEstateDelta !== undefined && (
236
+ <div>
237
+ <span className="muted">Ending estate delta:</span>{' '}
238
+ {formatDelta(exactImpact.endingAfterTaxEstateDelta, true)}
239
+ </div>
240
+ )}
241
+ {exactImpact.lifetimeTaxDelta !== undefined && (
242
+ <div>
243
+ <span className="muted">Lifetime tax delta:</span>{' '}
244
+ {formatDelta(exactImpact.lifetimeTaxDelta, false)}
245
+ </div>
246
+ )}
247
+ {card.impact.successRateDeltaPct !== undefined && (
248
+ <div>
249
+ <span className="muted">Monte Carlo success:</span>{' '}
250
+ {loadingMc ? (
251
+ <span className="muted">calculating…</span>
252
+ ) : mcDelta !== null ? (
253
+ <span className={mcDelta >= 0 ? 'delta-pos' : 'delta-neg'}>
254
+ {mcDelta > 0 ? '+' : ''}
255
+ {mcDelta.toFixed(1)}%
256
+ </span>
257
+ ) : (
258
+ '--'
259
+ )}
260
+ </div>
261
+ )}
262
+ </div>
263
+ <div className="insight-impact-note">
264
+ * Calculated by running a full plan re-simulation side-by-side.
265
+ </div>
266
+ </div>
267
+ ) : (
268
+ <div>
269
+ <strong>Estimated impact:</strong>{' '}
270
+ {card.impact.qualitative ? (
271
+ <span>{card.impact.qualitative}</span>
272
+ ) : (
273
+ <span>
274
+ {card.impact.endingAfterTaxEstateDelta !== undefined && (
275
+ <span>≈ {fmtMoneyCompact(card.impact.endingAfterTaxEstateDelta)} estate delta </span>
276
+ )}
277
+ {card.impact.lifetimeTaxDelta !== undefined && (
278
+ <span>≈ {fmtMoneyCompact(card.impact.lifetimeTaxDelta)} tax savings </span>
279
+ )}
280
+ </span>
281
+ )}
282
+ </div>
283
+ )}
284
+ </div>
285
+
286
+ {previewError && (
287
+ <div className="callout callout--warn insight-error" role="alert">
288
+ {previewError}
289
+ </div>
290
+ )}
291
+
292
+ {/* Actions Row */}
293
+ <div className="insight-actions">
294
+ <div className="insight-actions-links">
295
+ {card.learnSlug && <LearnLink slug={card.learnSlug} label="Learn more" />}
296
+ {card.plannerRoute && (
297
+ <Link to={`/plan/${plan.id}/${card.plannerRoute}`} className="learn-link">
298
+ Go to screen
299
+ </Link>
300
+ )}
301
+ </div>
302
+
303
+ <div className="insight-actions-buttons">
304
+ {card.action.kind === 'preview-scenario' && (
305
+ <>
306
+ <button
307
+ type="button"
308
+ className="btn btn-secondary btn-small"
309
+ onClick={handleToggleExpand}
310
+ disabled={loadingExact}
311
+ >
312
+ {loadingExact ? 'Loading…' : expanded ? 'Hide preview' : 'Preview impact'}
313
+ </button>
314
+ {expanded && exactImpact && (
315
+ <button type="button" className="btn btn-primary btn-small" onClick={handleAddScenario}>
316
+ Add as scenario
317
+ </button>
318
+ )}
319
+ </>
320
+ )}
321
+ {card.action.kind === 'advisory' && (
322
+ <span className="insight-advisory-note">Advisory only</span>
323
+ )}
324
+ {card.action.kind === 'apply-toggle' && (
325
+ <>
326
+ <button type="button" className="btn btn-primary btn-small" onClick={handleApplyToggle}>
327
+ Apply to plan
328
+ </button>
329
+ {undoPlan && (
330
+ <button type="button" className="btn btn-secondary btn-small" onClick={handleUndoToggle}>
331
+ Undo
332
+ </button>
333
+ )}
334
+ </>
335
+ )}
336
+ </div>
337
+ </div>
338
+ </div>
339
+ )
340
+ }
@@ -0,0 +1,204 @@
1
+ import { useState, useMemo } from 'react'
2
+ import { Link } from 'react-router-dom'
3
+ import { usePlan } from '../planContextCore'
4
+ import { useProjection } from '../useProjection'
5
+ import { packForYear } from '@retiregolden/engine/params'
6
+ import { runScreen } from '@retiregolden/engine/insights/runInsights'
7
+ import type { InsightCard, InsightCategory } from '@retiregolden/engine/insights/types'
8
+ import { readLocal, STORAGE_KEYS, writeLocal } from '../../data/localStore'
9
+ import { CATEGORY_LABELS } from './categoryLabels'
10
+ import { InsightCardView } from './InsightCardView'
11
+ import { LiveStatus } from '../LiveStatus'
12
+
13
+ function insightRenderKey(card: InsightCard): string {
14
+ return `${card.id}:${card.rationale}:${JSON.stringify(card.action)}`
15
+ }
16
+
17
+ export function InsightsPage() {
18
+ const { plan } = usePlan()
19
+ const projectionView = useProjection(plan)
20
+
21
+ // Manage dismissed cards state loaded from localStorage
22
+ const [dismissedMap, setDismissedMap] = useState<Record<string, string[]>>(() => {
23
+ try {
24
+ const stored = readLocal(STORAGE_KEYS.insightsDismissed)
25
+ return stored ? JSON.parse(stored) : {}
26
+ } catch {
27
+ return {}
28
+ }
29
+ })
30
+
31
+ // State for collapsible category groups (default expanded: true)
32
+ const [collapsedCategories, setCollapsedCategories] = useState<Record<string, boolean>>({})
33
+
34
+ // Screen-reader announcement for dismiss/restore (visual change is otherwise silent).
35
+ const [liveMessage, setLiveMessage] = useState('')
36
+
37
+ // Save dismissed cards to localStorage
38
+ const dismissCard = (cardId: string) => {
39
+ const nextMap = {
40
+ ...dismissedMap,
41
+ [plan.id]: [...(dismissedMap[plan.id] ?? []), cardId],
42
+ }
43
+ setDismissedMap(nextMap)
44
+ writeLocal(STORAGE_KEYS.insightsDismissed, JSON.stringify(nextMap))
45
+ setLiveMessage(`Insight dismissed. ${nextMap[plan.id]!.length} dismissed on this plan.`)
46
+ }
47
+
48
+ // Restore all dismissed cards for this plan
49
+ const restoreDismissed = () => {
50
+ const restoredCount = dismissedMap[plan.id]?.length ?? 0
51
+ const nextMap = { ...dismissedMap }
52
+ delete nextMap[plan.id]
53
+ setDismissedMap(nextMap)
54
+ writeLocal(STORAGE_KEYS.insightsDismissed, JSON.stringify(nextMap))
55
+ setLiveMessage(
56
+ `Restored ${restoredCount} dismissed insight${restoredCount === 1 ? '' : 's'}.`,
57
+ )
58
+ }
59
+
60
+ // Run detectors synchronously
61
+ const allCards = useMemo(() => {
62
+ const paramsLookup = packForYear(projectionView.startYear)
63
+ const ctx = {
64
+ plan,
65
+ projection: {
66
+ result: projectionView.result,
67
+ summary: projectionView.summary,
68
+ startYear: projectionView.startYear,
69
+ deflate: projectionView.deflate,
70
+ },
71
+ params: paramsLookup.pack,
72
+ }
73
+ return runScreen(ctx)
74
+ }, [plan, projectionView])
75
+
76
+ // Filter out dismissed cards
77
+ const planDismissed = useMemo(() => dismissedMap[plan.id] ?? [], [dismissedMap, plan.id])
78
+ const visibleCards = useMemo(() => {
79
+ return allCards.filter((card) => !planDismissed.includes(card.id))
80
+ }, [allCards, planDismissed])
81
+
82
+ // Group visible cards by category
83
+ const groupedCards = useMemo(() => {
84
+ const groups: Partial<Record<InsightCategory, InsightCard[]>> = {}
85
+ for (const card of visibleCards) {
86
+ if (!groups[card.category]) {
87
+ groups[card.category] = []
88
+ }
89
+ groups[card.category]!.push(card)
90
+ }
91
+ return groups
92
+ }, [visibleCards])
93
+
94
+ const toggleCategory = (cat: InsightCategory) => {
95
+ setCollapsedCategories((prev) => ({
96
+ ...prev,
97
+ [cat]: !prev[cat],
98
+ }))
99
+ }
100
+
101
+ return (
102
+ <section>
103
+ <LiveStatus message={liveMessage} />
104
+ <div className="card">
105
+ <h2>Insights & Recommendations</h2>
106
+ <p className="card-hint">
107
+ RetireGolden scans your plan to identify personalized planning opportunities. These recommendations update live as you edit.
108
+ </p>
109
+ <div className="callout callout--info">
110
+ <strong>Educational only.</strong> These suggestions are for educational purposes based on the rules and assumptions modeled. They do not constitute financial, tax, or legal advice.
111
+ </div>
112
+ </div>
113
+
114
+ <div className="insight-optimizer-row">
115
+ <div className="card">
116
+ <h2>Roth & Tax Optimizer</h2>
117
+ <p className="card-hint">
118
+ Compare candidate Roth-conversion schedules on your full year-by-year projection and see the best result found for your plan.
119
+ </p>
120
+ <Link to={`/plan/${plan.id}/optimize`} className="btn btn-primary btn-small">
121
+ Run optimizer →
122
+ </Link>
123
+ </div>
124
+
125
+ {plan.incomes.some((i) => i.type === 'socialSecurity') && (
126
+ <div className="card">
127
+ <h2>Social Security Optimizer</h2>
128
+ <p className="card-hint">
129
+ Analyze every claiming age combination side-by-side to find the schedule that maximizes your ending estate.
130
+ </p>
131
+ <Link to={`/plan/${plan.id}/social-security-analysis`} className="btn btn-secondary btn-small">
132
+ Run SS sweep →
133
+ </Link>
134
+ </div>
135
+ )}
136
+ </div>
137
+
138
+ {visibleCards.length === 0 ? (
139
+ <div className="card empty-state">
140
+ <h2>No opportunities found right now</h2>
141
+ <p className="muted insight-empty-note">
142
+ Your plan already covers the big levers or no new opportunities were detected. Try adjusting your strategy or assumptions.
143
+ </p>
144
+ {planDismissed.length > 0 && (
145
+ <button type="button" className="btn btn-secondary btn-small" onClick={restoreDismissed}>
146
+ Restore dismissed insights
147
+ </button>
148
+ )}
149
+ </div>
150
+ ) : (
151
+ <div>
152
+ {Object.entries(groupedCards).map(([cat, cards]) => {
153
+ const category = cat as InsightCategory
154
+ const isCollapsed = collapsedCategories[category] ?? false
155
+ return (
156
+ <div key={category} className="insight-category-group">
157
+ <h2 className="insight-category-heading">
158
+ <button
159
+ type="button"
160
+ className="insight-category-header"
161
+ aria-expanded={!isCollapsed}
162
+ onClick={() => toggleCategory(category)}
163
+ >
164
+ <span>
165
+ {CATEGORY_LABELS[category]} ({cards!.length})
166
+ </span>
167
+ <svg
168
+ viewBox="0 0 24 24"
169
+ width="16"
170
+ height="16"
171
+ fill="none"
172
+ stroke="currentColor"
173
+ strokeWidth="2"
174
+ strokeLinecap="round"
175
+ strokeLinejoin="round"
176
+ aria-hidden="true"
177
+ >
178
+ <polyline points={isCollapsed ? '6 9 12 15 18 9' : '6 15 12 9 18 15'} />
179
+ </svg>
180
+ </button>
181
+ </h2>
182
+ {!isCollapsed && (
183
+ <div className="insight-cards-list">
184
+ {cards!.map((card) => (
185
+ <InsightCardView key={insightRenderKey(card)} card={card} onDismiss={() => dismissCard(card.id)} />
186
+ ))}
187
+ </div>
188
+ )}
189
+ </div>
190
+ )
191
+ })}
192
+
193
+ {planDismissed.length > 0 && (
194
+ <div className="insight-restore-row">
195
+ <button type="button" className="btn btn-secondary btn-small" onClick={restoreDismissed}>
196
+ Restore dismissed insights ({planDismissed.length})
197
+ </button>
198
+ </div>
199
+ )}
200
+ </div>
201
+ )}
202
+ </section>
203
+ )
204
+ }
@@ -0,0 +1,11 @@
1
+ import type { InsightCategory } from '@retiregolden/engine/insights/types'
2
+
3
+ /** Human-readable names for insight categories, shared by the page grouping and card badges. */
4
+ export const CATEGORY_LABELS: Record<InsightCategory, string> = {
5
+ 'tax-brackets': 'Tax Brackets & Headroom',
6
+ 'accounts-contributions': 'Account Contributions',
7
+ 'withdrawals-charitable': 'Withdrawals & Charitable Giving',
8
+ 'sequence-risk': 'Sequence Risk & Spending',
9
+ 'social-security': 'Social Security & Spousal',
10
+ 'longevity-insurance-geography': 'Longevity & Geography',
11
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Canonical Learning Center targets used by planner field help (V9 PR3).
3
+ *
4
+ * Centralizing the planner→article wiring here gives the contextual "Learn
5
+ * more" links a single source of truth and a broken-slug test (every hook must
6
+ * resolve to a `ready` article). Field help should reference these hooks rather
7
+ * than inlining raw slugs, so a typo or a not-yet-written article is caught by
8
+ * a test instead of shipping a dead link.
9
+ *
10
+ * Only add an entry once the target article is readable. Screen-level clusters
11
+ * use {@link articlesForRoute} instead and need no entry here.
12
+ */
13
+
14
+ import type { LearnHook } from '../learn/LearnLink'
15
+
16
+ export const LEARN = {
17
+ exampleCouple: { slug: 'example-couple', label: 'Learn about this example' },
18
+ exampleUnderSavedSingle: { slug: 'example-under-saved-single', label: 'Learn about this example' },
19
+ exampleBracketFillRoth: { slug: 'example-bracket-fill-roth', label: 'Learn about this example' },
20
+ exampleEarlyRetireeAca: { slug: 'example-early-retiree-aca', label: 'Learn about this example' },
21
+ exampleRmdIrmaa: { slug: 'example-rmd-irmaa', label: 'Learn about this example' },
22
+ exampleSurvivorYears: { slug: 'example-survivor-years', label: 'Learn about this example' },
23
+ exampleMovingStateTax: { slug: 'example-moving-state-tax', label: 'Learn about this example' },
24
+ exampleLtcShock: { slug: 'example-ltc-shock', label: 'Learn about this example' },
25
+ exampleGuardrailsFlex: { slug: 'example-guardrails-flex-goals', label: 'Learn about this example' },
26
+ exampleAnnuityEstate: { slug: 'example-annuity-purchases-estate', label: 'Learn about this example' },
27
+ exampleGlidepathAllocation: { slug: 'example-glidepath-allocation', label: 'Learn about this example' },
28
+ exampleHsaPropertyDepth: { slug: 'example-hsa-property-depth', label: 'Learn about this example' },
29
+ exampleFixedTargetSpending: { slug: 'example-fixed-target-spending', label: 'Learn about this example' },
30
+ exampleNoAnnuityBrokerage: { slug: 'example-no-annuity-brokerage', label: 'Learn about this example' },
31
+ exampleStaticAllocationControl: { slug: 'example-static-allocation-control', label: 'Learn about this example' },
32
+ exampleBrokerageNoHsa: { slug: 'example-brokerage-no-hsa', label: 'Learn about this example' },
33
+ withdrawalOrder: { slug: 'withdrawal-order-basics', label: 'Learn about withdrawal order' },
34
+ qcd: { slug: 'qcds-qualified-charitable-distributions', label: 'Learn about QCDs' },
35
+ itemizedDeductions: {
36
+ slug: 'standard-deduction-senior-deduction-and-itemizing',
37
+ label: 'Learn: standard vs. itemized deductions',
38
+ },
39
+ spendingBudget: { slug: 'building-a-retirement-spending-budget', label: 'Learn about retirement spending budgets' },
40
+ spendingProfiles: {
41
+ slug: 'spending-profiles-and-the-retirement-smile',
42
+ label: 'Learn about spending profiles',
43
+ },
44
+ survivorSpending: { slug: 'survivor-spending-in-couple-plans', label: 'Learn about survivor spending' },
45
+ sustainableSpending: { slug: 'how-much-can-i-spend', label: 'Learn: how much can I spend?' },
46
+ dynamicSpendingGuardrails: { slug: 'dynamic-spending-guardrails', label: 'Learn about spending flexibility' },
47
+ riskBasedGuardrails: { slug: 'risk-based-guardrails', label: 'Learn about risk-based guardrails' },
48
+ tipsLadders: { slug: 'tips-ladders', label: 'Learn about TIPS ladders' },
49
+ socialSecurityBridge: { slug: 'social-security-bridge', label: 'Learn about the Social Security bridge' },
50
+ fundedRatio: { slug: 'funded-ratio', label: 'Learn about the funded ratio' },
51
+ healthcareBefore65: { slug: 'healthcare-before-65', label: 'Learn about healthcare before 65' },
52
+ healthcareAfter65: { slug: 'healthcare-after-65', label: 'Learn about healthcare after 65' },
53
+ acaCredit: { slug: 'aca-premium-tax-credits-and-magi', label: 'Learn about ACA premium credits' },
54
+ ssa44: { slug: 'appealing-irmaa-ssa-44', label: 'Learn about SSA-44 IRMAA appeals' },
55
+ retirementHealthcareCosts: { slug: 'what-retirement-healthcare-really-costs', label: 'Learn what healthcare really costs' },
56
+ insuranceOverview: { slug: 'insurance-in-your-retirement-plan', label: 'Learn how insurance fits your plan' },
57
+ permanentLife: { slug: 'permanent-life-insurance-in-a-plan', label: 'Learn about permanent life insurance' },
58
+ ltcInsurance: { slug: 'long-term-care-insurance-as-risk-transfer', label: 'Learn about LTC insurance' },
59
+ ltcCosts: { slug: 'long-term-care-costs-and-insurance', label: 'Learn about long-term-care costs' },
60
+ rothConversionBasics: { slug: 'roth-conversion-basics', label: 'Learn about Roth conversions' },
61
+ fillingTaxBracket: { slug: 'filling-a-tax-bracket-with-roth-conversions', label: 'Learn: filling a tax bracket' },
62
+ marginalVsEffective: { slug: 'marginal-vs-effective-tax-rate', label: 'Learn: marginal vs effective rate' },
63
+ whyConversionsRaiseCosts: {
64
+ slug: 'why-roth-conversions-raise-other-costs',
65
+ label: 'Learn how conversions raise other costs',
66
+ },
67
+ todaysDollars: { slug: 'todays-dollars-vs-future-dollars', label: "Learn: today's vs future dollars" },
68
+ lossHarvesting: { slug: 'tax-loss-and-gain-harvesting', label: 'Learn about tax-loss harvesting' },
69
+ fiNumber: { slug: 'fi-number-and-four-percent-rule', label: 'Learn about FI number and SWR' },
70
+ accumulation: { slug: 'how-to-model-accumulation', label: 'Learn how to model accumulation' },
71
+ employerMatch: { slug: 'employer-match-and-contribution-order', label: 'Learn about employer match' },
72
+ ssTaxesVsBenefits: { slug: 'social-security-taxes-vs-benefits', label: 'Learn: Social Security taxes vs. benefits' },
73
+ generalInflation: { slug: 'assumption-general-inflation', label: 'Learn about general inflation' },
74
+ healthcareInflation: { slug: 'assumption-healthcare-inflation', label: 'Learn about healthcare inflation' },
75
+ investmentReturns: { slug: 'assumption-investment-returns', label: 'Learn about investment returns' },
76
+ ssCola: { slug: 'assumption-social-security-cola', label: 'Learn about Social Security COLA' },
77
+ ssTrustFund: { slug: 'assumption-social-security-trust-fund', label: 'Learn about the Social Security trust fund' },
78
+ longevity: { slug: 'assumption-longevity-planning-age', label: 'Learn about longevity planning age' },
79
+ stateTaxOverride: { slug: 'assumption-state-tax-override', label: 'Learn about the state tax override' },
80
+ stateIncomeTaxes: { slug: 'state-income-taxes-in-retirement', label: 'Learn about state income taxes in retirement' },
81
+ stateRelocation: { slug: 'what-changes-when-you-move-states', label: 'Learn what changes when you move states' },
82
+ recentMagi: { slug: 'assumption-recent-magi', label: 'Learn about recent MAGI' },
83
+ heirTaxRate: { slug: 'assumption-heir-tax-rate', label: 'Learn about the heir tax rate' },
84
+ optimizerObjectives: { slug: 'how-the-optimizer-thinks', label: 'Learn about optimizer objectives' },
85
+ } as const satisfies Record<string, LearnHook>