@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,118 @@
1
+ /**
2
+ * Wire types between the Monte Carlo pool and its workers (roadmap V4).
3
+ * Everything here must survive structured clone: plain objects, arrays, and
4
+ * Float64Array only.
5
+ */
6
+
7
+ import type { Plan } from '@retiregolden/engine/model/plan'
8
+ import type { AnnuitizationSweep } from '@retiregolden/engine/decisions/annuitization'
9
+ import type { StochasticFrontierPoint } from '@retiregolden/engine/montecarlo/frontiers'
10
+ import type { HistoricalStressSuiteKind } from '@retiregolden/engine/montecarlo/historicalSuites'
11
+ import type { LtcShockParams } from '@retiregolden/engine/montecarlo/ltcShock'
12
+ import type { MarketModelConfig } from '@retiregolden/engine/montecarlo/marketModels'
13
+ import type { RiskBasedGuardrailSolution } from '@retiregolden/engine/montecarlo/riskBasedGuardrails'
14
+ import type { MonteCarloPathsResult } from '@retiregolden/engine/montecarlo/run'
15
+
16
+ export interface McWorkerRequest {
17
+ kind: 'monteCarlo'
18
+ plan: Plan
19
+ startYear: number
20
+ seed: number
21
+ model: MarketModelConfig
22
+ pathCount: number
23
+ /** Global index of this worker's first path (keeps seeding partition-independent). */
24
+ firstPathIndex: number
25
+ /** Post a progress message every N completed paths. */
26
+ progressEvery: number
27
+ /** Sample lifespans from the mortality table per path (roadmap V6). */
28
+ stochasticLongevity?: boolean
29
+ /** Probabilistic LTC care episode per path (roadmap V6); omitted/null = off. */
30
+ ltcShock?: LtcShockParams | null
31
+ }
32
+
33
+ export interface FrontierWorkerRequest {
34
+ kind: 'frontiers'
35
+ plan: Plan
36
+ startYear: number
37
+ seed: number
38
+ model: MarketModelConfig
39
+ pathCount: number
40
+ stochasticLongevity?: boolean
41
+ ltcShock?: LtcShockParams | null
42
+ }
43
+
44
+ export interface FrontierWorkerResult {
45
+ spending: StochasticFrontierPoint[]
46
+ retirement: StochasticFrontierPoint[]
47
+ /** Annuitization sweep (annuity-pension-and-home-equity, step 2). */
48
+ annuitization: AnnuitizationSweep
49
+ }
50
+
51
+ export interface RiskBasedWorkerRequest {
52
+ kind: 'riskBasedGuardrails'
53
+ plan: Plan
54
+ startYear: number
55
+ seed: number
56
+ model: MarketModelConfig
57
+ pathCount: number
58
+ stochasticLongevity?: boolean
59
+ ltcShock?: LtcShockParams | null
60
+ }
61
+
62
+ export interface HistoricalWorkerRequest {
63
+ kind: 'historicalSuites'
64
+ plan: Plan
65
+ startYear: number
66
+ equityWeightPct: number
67
+ classShocks?: boolean
68
+ worstWindowCount?: number
69
+ }
70
+
71
+ export interface HistoricalStressWindowView {
72
+ suite: HistoricalStressSuiteKind
73
+ label: string
74
+ startHistoricalYear: number
75
+ endHistoricalYear: number
76
+ reversed: boolean
77
+ success: boolean
78
+ projection: { depletionYear: number | null }
79
+ summary: { endingAfterTaxEstate: number }
80
+ totalShortfall: number
81
+ totalRequiredShortfall: number
82
+ totalTargetShortfall: number
83
+ }
84
+
85
+ export interface HistoricalStressSuiteView {
86
+ kind: HistoricalStressSuiteKind
87
+ name: string
88
+ windowLengthYears: number
89
+ worstByEndingAfterTaxEstate: HistoricalStressWindowView[]
90
+ worstByTotalShortfall: HistoricalStressWindowView[]
91
+ }
92
+
93
+ export interface HistoricalStressSuiteViewResult {
94
+ windowLengthYears: number
95
+ suites: HistoricalStressSuiteView[]
96
+ }
97
+
98
+ export type WorkerRequest = McWorkerRequest | FrontierWorkerRequest | HistoricalWorkerRequest | RiskBasedWorkerRequest
99
+
100
+ export type McWorkerResponse =
101
+ | { type: 'progress'; completed: number }
102
+ | { type: 'done'; result: MonteCarloPathsResult }
103
+ | { type: 'error'; message: string }
104
+
105
+ export type FrontierWorkerResponse =
106
+ | { type: 'frontiersDone'; result: FrontierWorkerResult }
107
+ | { type: 'error'; message: string }
108
+
109
+ export type HistoricalWorkerResponse =
110
+ | { type: 'historicalSuitesDone'; result: HistoricalStressSuiteViewResult }
111
+ | { type: 'error'; message: string }
112
+
113
+ export type RiskBasedWorkerResponse =
114
+ | { type: 'progress'; completed: number }
115
+ | { type: 'riskBasedDone'; result: RiskBasedGuardrailSolution }
116
+ | { type: 'error'; message: string }
117
+
118
+ export type WorkerResponse = McWorkerResponse | FrontierWorkerResponse | HistoricalWorkerResponse | RiskBasedWorkerResponse
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Web Worker entry for Monte Carlo paths (roadmap V4). One request per
3
+ * worker run: simulate the assigned slice of paths, stream progress, post
4
+ * the reduced per-path results back (Float64Array buffers transferred, not
5
+ * copied).
6
+ */
7
+
8
+ import type { WorkerRequest, WorkerResponse } from './messages'
9
+ import {
10
+ runFrontierRequest,
11
+ runHistoricalStressSuiteRequest,
12
+ runMcRequest,
13
+ runRiskBasedGuardrailRequest,
14
+ } from './runRequest'
15
+
16
+ const post = (msg: WorkerResponse, transfer: Transferable[] = []) =>
17
+ (self as unknown as Worker).postMessage(msg, transfer)
18
+
19
+ self.onmessage = (event: MessageEvent<WorkerRequest>) => {
20
+ const req = event.data
21
+ try {
22
+ if (req.kind === 'frontiers') {
23
+ post({ type: 'frontiersDone', result: runFrontierRequest(req) })
24
+ return
25
+ }
26
+ if (req.kind === 'historicalSuites') {
27
+ post({ type: 'historicalSuitesDone', result: runHistoricalStressSuiteRequest(req) })
28
+ return
29
+ }
30
+ if (req.kind === 'riskBasedGuardrails') {
31
+ post({ type: 'riskBasedDone', result: runRiskBasedGuardrailRequest(req, (completed) => post({ type: 'progress', completed })) })
32
+ return
33
+ }
34
+ const result = runMcRequest(req, (completed) => {
35
+ if (completed % req.progressEvery === 0) post({ type: 'progress', completed })
36
+ })
37
+ post(
38
+ { type: 'done', result },
39
+ result.paths.map((p) => p.investableByYear.buffer),
40
+ )
41
+ } catch (err) {
42
+ post({ type: 'error', message: err instanceof Error ? err.message : String(err) })
43
+ }
44
+ }
package/src/mc/pool.ts ADDED
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Worker-pool Monte Carlo runner (roadmap V4, feature catalog §11).
3
+ *
4
+ * Splits the requested paths across a pool of Web Workers (one per core by
5
+ * default, capped at 8) and merges the results. Per-path seeds derive from
6
+ * (seed, globalPathIndex) inside the engine, so the outcome is identical for
7
+ * any worker count — including the synchronous fallback used where Worker is
8
+ * unavailable (tests, very old browsers).
9
+ */
10
+
11
+ import type { Plan } from '@retiregolden/engine/model/plan'
12
+ import type { LtcShockParams } from '@retiregolden/engine/montecarlo/ltcShock'
13
+ import type { MarketModelConfig } from '@retiregolden/engine/montecarlo/marketModels'
14
+ import {
15
+ aggregateMonteCarlo,
16
+ mergePathResults,
17
+ type MonteCarloPathsResult,
18
+ type MonteCarloSummary,
19
+ } from '@retiregolden/engine/montecarlo/run'
20
+ import type { RiskBasedGuardrailSolution } from '@retiregolden/engine/montecarlo/riskBasedGuardrails'
21
+ import type {
22
+ FrontierWorkerRequest,
23
+ FrontierWorkerResponse,
24
+ FrontierWorkerResult,
25
+ HistoricalStressSuiteViewResult,
26
+ HistoricalWorkerRequest,
27
+ HistoricalWorkerResponse,
28
+ McWorkerRequest,
29
+ McWorkerResponse,
30
+ RiskBasedWorkerRequest,
31
+ RiskBasedWorkerResponse,
32
+ } from './messages'
33
+ import {
34
+ runFrontierRequest,
35
+ runHistoricalStressSuiteRequest,
36
+ runMcRequest,
37
+ runRiskBasedGuardrailRequest,
38
+ } from './runRequest'
39
+ import { runWorkerRequest } from '../workers/run'
40
+
41
+ export interface MonteCarloRunOptions {
42
+ startYear: number
43
+ /** 1,000 default; 10,000 on demand. */
44
+ pathCount: number
45
+ seed: number
46
+ model: MarketModelConfig
47
+ /** Default: hardwareConcurrency − 1, clamped to [1, 8]. */
48
+ workerCount?: number
49
+ /** Sample lifespans from the mortality table per path (roadmap V6). */
50
+ stochasticLongevity?: boolean
51
+ /** Probabilistic LTC care episode per path (roadmap V6); omitted/null = off. */
52
+ ltcShock?: LtcShockParams | null
53
+ onProgress?: (completedPaths: number, totalPaths: number) => void
54
+ }
55
+
56
+ export const DEFAULT_PATH_COUNT = 1000
57
+ export const ON_DEMAND_PATH_COUNT = 10_000
58
+
59
+ function defaultWorkerCount(): number {
60
+ const cores = typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency ?? 4) : 4
61
+ return Math.max(1, Math.min(8, cores - 1))
62
+ }
63
+
64
+ function makeRequest(plan: Plan, opts: MonteCarloRunOptions, firstPathIndex: number, pathCount: number): McWorkerRequest {
65
+ return {
66
+ kind: 'monteCarlo',
67
+ plan,
68
+ startYear: opts.startYear,
69
+ seed: opts.seed,
70
+ model: opts.model,
71
+ pathCount,
72
+ firstPathIndex,
73
+ progressEvery: Math.max(1, Math.floor(opts.pathCount / 100)),
74
+ stochasticLongevity: opts.stochasticLongevity,
75
+ ltcShock: opts.ltcShock,
76
+ }
77
+ }
78
+
79
+ export interface StochasticFrontierRunOptions {
80
+ startYear: number
81
+ pathCount: number
82
+ seed: number
83
+ model: MarketModelConfig
84
+ stochasticLongevity?: boolean
85
+ ltcShock?: LtcShockParams | null
86
+ }
87
+
88
+ export interface HistoricalStressSuiteRunOptions {
89
+ startYear: number
90
+ equityWeightPct: number
91
+ classShocks?: boolean
92
+ worstWindowCount?: number
93
+ }
94
+
95
+ export type { HistoricalStressSuiteViewResult } from './messages'
96
+
97
+ function runInWorker(
98
+ req: McWorkerRequest,
99
+ onProgress: (workerCompleted: number) => void,
100
+ onSpawn?: (worker: Worker) => void,
101
+ ): Promise<MonteCarloPathsResult> {
102
+ return runWorkerRequest<McWorkerRequest, McWorkerResponse, MonteCarloPathsResult>({
103
+ request: req,
104
+ createWorker: () => new Worker(new URL('./monteCarlo.worker.ts', import.meta.url), { type: 'module' }),
105
+ interpret: (msg) => {
106
+ if (msg.type === 'progress') {
107
+ onProgress(msg.completed)
108
+ return { kind: 'progress' }
109
+ }
110
+ if (msg.type === 'done') return { kind: 'done', result: msg.result }
111
+ return { kind: 'error', message: msg.message }
112
+ },
113
+ errorLabel: 'Monte Carlo worker failed',
114
+ onSpawn,
115
+ })
116
+ }
117
+
118
+ function runFrontiersInWorker(req: FrontierWorkerRequest): Promise<FrontierWorkerResult> {
119
+ return runWorkerRequest<FrontierWorkerRequest, FrontierWorkerResponse, FrontierWorkerResult>({
120
+ request: req,
121
+ createWorker: () => new Worker(new URL('./monteCarlo.worker.ts', import.meta.url), { type: 'module' }),
122
+ interpret: (msg) =>
123
+ msg.type === 'frontiersDone' ? { kind: 'done', result: msg.result } : { kind: 'error', message: msg.message },
124
+ errorLabel: 'Frontier worker failed',
125
+ })
126
+ }
127
+
128
+ function runHistoricalSuitesInWorker(req: HistoricalWorkerRequest): Promise<HistoricalStressSuiteViewResult> {
129
+ return runWorkerRequest<HistoricalWorkerRequest, HistoricalWorkerResponse, HistoricalStressSuiteViewResult>({
130
+ request: req,
131
+ createWorker: () => new Worker(new URL('./monteCarlo.worker.ts', import.meta.url), { type: 'module' }),
132
+ interpret: (msg) =>
133
+ msg.type === 'historicalSuitesDone'
134
+ ? { kind: 'done', result: msg.result }
135
+ : { kind: 'error', message: msg.message },
136
+ errorLabel: 'Historical stress worker failed',
137
+ })
138
+ }
139
+
140
+ /** Run the full Monte Carlo and aggregate it. Reproducible per (plan, seed, model). */
141
+ export async function runMonteCarlo(plan: Plan, opts: MonteCarloRunOptions): Promise<MonteCarloSummary> {
142
+ const total = opts.pathCount
143
+ if (typeof Worker === 'undefined') {
144
+ const result = runMcRequest(makeRequest(plan, opts, 0, total), (completed) => opts.onProgress?.(completed, total))
145
+ return aggregateMonteCarlo(result)
146
+ }
147
+
148
+ const workerCount = Math.max(1, Math.min(opts.workerCount ?? defaultWorkerCount(), total))
149
+ const base = Math.floor(total / workerCount)
150
+ const extra = total % workerCount
151
+ const completedByWorker = new Array<number>(workerCount).fill(0)
152
+ const reportProgress = () => {
153
+ opts.onProgress?.(
154
+ completedByWorker.reduce((a, b) => a + b, 0),
155
+ total,
156
+ )
157
+ }
158
+
159
+ const jobs: Promise<MonteCarloPathsResult>[] = []
160
+ const liveWorkers: Worker[] = []
161
+ let firstPathIndex = 0
162
+ for (let w = 0; w < workerCount; w++) {
163
+ const slice = base + (w < extra ? 1 : 0)
164
+ const req = makeRequest(plan, opts, firstPathIndex, slice)
165
+ firstPathIndex += slice
166
+ const workerIndex = w
167
+ jobs.push(
168
+ runInWorker(
169
+ req,
170
+ (completed) => {
171
+ completedByWorker[workerIndex] = completed
172
+ reportProgress()
173
+ },
174
+ (worker) => liveWorkers.push(worker),
175
+ ),
176
+ )
177
+ }
178
+ let parts: MonteCarloPathsResult[]
179
+ try {
180
+ parts = await Promise.all(jobs)
181
+ } catch (error) {
182
+ // One job failed: stop the siblings instead of letting them burn CPU on a
183
+ // run whose result will be discarded (terminate is a no-op once settled).
184
+ for (const worker of liveWorkers) worker.terminate()
185
+ // The killed siblings' promises still reject (their onerror fires after
186
+ // terminate); swallow those so only this rethrow surfaces.
187
+ for (const job of jobs) job.catch(() => {})
188
+ throw error
189
+ }
190
+ opts.onProgress?.(total, total)
191
+ return aggregateMonteCarlo(mergePathResults(parts))
192
+ }
193
+
194
+ export interface RiskBasedGuardrailRunOptions {
195
+ startYear: number
196
+ pathCount: number
197
+ seed: number
198
+ model: MarketModelConfig
199
+ stochasticLongevity?: boolean
200
+ ltcShock?: LtcShockParams | null
201
+ }
202
+
203
+ /**
204
+ * Solve the risk-based guardrail thresholds (dollar balance levels matching the
205
+ * plan's target success band) on a single worker. Deterministic per
206
+ * (plan, seed, model, pathCount).
207
+ */
208
+ export async function runRiskBasedGuardrailSolve(
209
+ plan: Plan,
210
+ opts: RiskBasedGuardrailRunOptions,
211
+ ): Promise<RiskBasedGuardrailSolution> {
212
+ const req: RiskBasedWorkerRequest = {
213
+ kind: 'riskBasedGuardrails',
214
+ plan,
215
+ startYear: opts.startYear,
216
+ seed: opts.seed,
217
+ model: opts.model,
218
+ pathCount: opts.pathCount,
219
+ stochasticLongevity: opts.stochasticLongevity,
220
+ ltcShock: opts.ltcShock,
221
+ }
222
+ if (typeof Worker === 'undefined') return runRiskBasedGuardrailRequest(req)
223
+ return runWorkerRequest<RiskBasedWorkerRequest, RiskBasedWorkerResponse, RiskBasedGuardrailSolution>({
224
+ request: req,
225
+ createWorker: () => new Worker(new URL('./monteCarlo.worker.ts', import.meta.url), { type: 'module' }),
226
+ interpret: (msg) => {
227
+ if (msg.type === 'progress') return { kind: 'progress' }
228
+ if (msg.type === 'riskBasedDone') return { kind: 'done', result: msg.result }
229
+ return { kind: 'error', message: msg.message }
230
+ },
231
+ errorLabel: 'Risk-based guardrail solver failed',
232
+ })
233
+ }
234
+
235
+ export async function runStochasticFrontiers(
236
+ plan: Plan,
237
+ opts: StochasticFrontierRunOptions,
238
+ ): Promise<FrontierWorkerResult> {
239
+ const req: FrontierWorkerRequest = {
240
+ kind: 'frontiers',
241
+ plan,
242
+ startYear: opts.startYear,
243
+ seed: opts.seed,
244
+ model: opts.model,
245
+ pathCount: opts.pathCount,
246
+ stochasticLongevity: opts.stochasticLongevity,
247
+ ltcShock: opts.ltcShock,
248
+ }
249
+ if (typeof Worker === 'undefined') return runFrontierRequest(req)
250
+ return runFrontiersInWorker(req)
251
+ }
252
+
253
+ export async function runHistoricalStressSuiteViews(
254
+ plan: Plan,
255
+ opts: HistoricalStressSuiteRunOptions,
256
+ ): Promise<HistoricalStressSuiteViewResult> {
257
+ const req: HistoricalWorkerRequest = {
258
+ kind: 'historicalSuites',
259
+ plan,
260
+ startYear: opts.startYear,
261
+ equityWeightPct: opts.equityWeightPct,
262
+ classShocks: opts.classShocks,
263
+ worstWindowCount: opts.worstWindowCount,
264
+ }
265
+ if (typeof Worker === 'undefined') return runHistoricalStressSuiteRequest(req)
266
+ return runHistoricalSuitesInWorker(req)
267
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Executes one Monte Carlo work unit. Shared by the Web Worker entry and the
3
+ * pool's synchronous fallback so both paths run the identical tax stack:
4
+ * the federal engine plus the per-state engine (with the plan's flat rate as
5
+ * an override).
6
+ */
7
+
8
+ import type { Plan } from '@retiregolden/engine/model/plan'
9
+ import { buildAnnuitizationSweep } from '@retiregolden/engine/decisions/annuitization'
10
+ import { buildRetirementAgeSuccessFrontier, buildSpendingSuccessFrontier } from '@retiregolden/engine/montecarlo/frontiers'
11
+ import {
12
+ runHistoricalStressSuites,
13
+ type HistoricalStressWindow,
14
+ } from '@retiregolden/engine/montecarlo/historicalSuites'
15
+ import { createMarketModel } from '@retiregolden/engine/montecarlo/marketModels'
16
+ import {
17
+ solveRiskBasedGuardrails,
18
+ type RiskBasedGuardrailSolution,
19
+ } from '@retiregolden/engine/montecarlo/riskBasedGuardrails'
20
+ import { runMonteCarloPaths, type MonteCarloPathsResult } from '@retiregolden/engine/montecarlo/run'
21
+ import { combineTaxCalculators, createFederalTaxCalculator } from '@retiregolden/engine/tax/federalTax'
22
+ import { createStateTaxCalculator } from '@retiregolden/engine/tax/stateTax'
23
+ import type {
24
+ FrontierWorkerRequest,
25
+ FrontierWorkerResult,
26
+ HistoricalStressSuiteViewResult,
27
+ HistoricalStressWindowView,
28
+ HistoricalWorkerRequest,
29
+ McWorkerRequest,
30
+ RiskBasedWorkerRequest,
31
+ } from './messages'
32
+
33
+ function taxCalculatorForPlan(plan: Plan) {
34
+ return combineTaxCalculators(
35
+ createFederalTaxCalculator(),
36
+ createStateTaxCalculator({
37
+ overridePct: plan.assumptions.stateEffectiveTaxPct,
38
+ localPct: plan.assumptions.localIncomeTaxPct,
39
+ }),
40
+ )
41
+ }
42
+
43
+ export function runMcRequest(req: McWorkerRequest, onPathDone?: (completed: number) => void): MonteCarloPathsResult {
44
+ const taxCalculator = taxCalculatorForPlan(req.plan)
45
+ return runMonteCarloPaths(req.plan, {
46
+ startYear: req.startYear,
47
+ taxCalculator,
48
+ model: createMarketModel(req.model),
49
+ seed: req.seed,
50
+ pathCount: req.pathCount,
51
+ firstPathIndex: req.firstPathIndex,
52
+ stochasticLongevity: req.stochasticLongevity,
53
+ ltcShock: req.ltcShock,
54
+ onPathDone,
55
+ })
56
+ }
57
+
58
+ export function runRiskBasedGuardrailRequest(
59
+ req: RiskBasedWorkerRequest,
60
+ onProbeDone?: (completed: number, total: number) => void,
61
+ ): RiskBasedGuardrailSolution {
62
+ return solveRiskBasedGuardrails(req.plan, {
63
+ startYear: req.startYear,
64
+ taxCalculator: taxCalculatorForPlan(req.plan),
65
+ model: req.model,
66
+ pathCount: req.pathCount,
67
+ seed: req.seed,
68
+ stochasticLongevity: req.stochasticLongevity,
69
+ ltcShock: req.ltcShock,
70
+ onProbeDone,
71
+ })
72
+ }
73
+
74
+ export function runFrontierRequest(req: FrontierWorkerRequest): FrontierWorkerResult {
75
+ const opts = {
76
+ startYear: req.startYear,
77
+ taxCalculator: taxCalculatorForPlan(req.plan),
78
+ model: req.model,
79
+ pathCount: req.pathCount,
80
+ seed: req.seed,
81
+ stochasticLongevity: req.stochasticLongevity,
82
+ ltcShock: req.ltcShock,
83
+ }
84
+ return {
85
+ spending: buildSpendingSuccessFrontier(req.plan, opts),
86
+ retirement: buildRetirementAgeSuccessFrontier(req.plan, opts),
87
+ annuitization: buildAnnuitizationSweep(req.plan, opts),
88
+ }
89
+ }
90
+
91
+ function windowView(window: HistoricalStressWindow): HistoricalStressWindowView {
92
+ return {
93
+ suite: window.suite,
94
+ label: window.label,
95
+ startHistoricalYear: window.startHistoricalYear,
96
+ endHistoricalYear: window.endHistoricalYear,
97
+ reversed: window.reversed,
98
+ success: window.success,
99
+ projection: { depletionYear: window.projection.depletionYear },
100
+ summary: { endingAfterTaxEstate: window.summary.endingAfterTaxEstate },
101
+ totalShortfall: window.totalShortfall,
102
+ totalRequiredShortfall: window.totalRequiredShortfall,
103
+ totalTargetShortfall: window.totalTargetShortfall,
104
+ }
105
+ }
106
+
107
+ export function runHistoricalStressSuiteRequest(req: HistoricalWorkerRequest): HistoricalStressSuiteViewResult {
108
+ const result = runHistoricalStressSuites(req.plan, {
109
+ startYear: req.startYear,
110
+ taxCalculator: taxCalculatorForPlan(req.plan),
111
+ equityWeightPct: req.equityWeightPct,
112
+ classShocks: req.classShocks,
113
+ worstWindowCount: req.worstWindowCount,
114
+ })
115
+ return {
116
+ windowLengthYears: result.windowLengthYears,
117
+ suites: result.suites.map((suite) => ({
118
+ kind: suite.kind,
119
+ name: suite.name,
120
+ windowLengthYears: suite.windowLengthYears,
121
+ worstByEndingAfterTaxEstate: suite.worstByEndingAfterTaxEstate.map(windowView),
122
+ worstByTotalShortfall: suite.worstByTotalShortfall.map(windowView),
123
+ })),
124
+ }
125
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Wire types between the Optimize UI and its worker (roadmap V8). Everything
3
+ * here must survive structured clone — note the tax calculator is NOT passed
4
+ * (it's a closure); the worker rebuilds the standard stack from the plan, just
5
+ * like the Monte Carlo worker (see src/mc/runRequest.ts).
6
+ */
7
+
8
+ import type { ObjectivePolicyId } from '@retiregolden/engine/decisions'
9
+ import type { Plan } from '@retiregolden/engine/model/plan'
10
+ import type {
11
+ ClaimAgeCoOptimization,
12
+ ExactLedgerConvergenceDiagnostics,
13
+ ExactLedgerRecommendationSchedule,
14
+ ExactLedgerScheduleAdjustment,
15
+ ExactLedgerTournament,
16
+ ExactLedgerValidation,
17
+ } from '@retiregolden/engine/projection/optimizePlan'
18
+ import type { OptimizedSchedule } from '@retiregolden/engine/strategies/optimizer'
19
+
20
+ export interface OptimizeRequest {
21
+ plan: Plan
22
+ startYear: number
23
+ /** Haircut on leftover traditional; defaults to the plan's heirTaxRatePct. */
24
+ liquidationRatePct?: number
25
+ /** HiGHS time limit (seconds). */
26
+ timeLimitSec?: number
27
+ /**
28
+ * Exact-ledger local-search budget for refining winning schedules. Omitted =
29
+ * default 96 simulations. This is a PER-SEED budget: search refines the top
30
+ * two candidates (and a winning MILP schedule), so total search simulations
31
+ * can reach ~2–3× this value. 0 or negative disables search.
32
+ */
33
+ searchSimulationBudget?: number
34
+ /**
35
+ * Objective policy the tournament ranks by. Omitted = `max-after-tax-estate`
36
+ * (the original behavior). Floor-style policies pick up the plan's bequest
37
+ * target inside the worker (`objectivePolicyForPlan`).
38
+ */
39
+ objectivePolicyId?: ObjectivePolicyId
40
+ /**
41
+ * Exact-ledger convergence-loop iteration cap (optimizer-exact-ledger-convergence Track 1, Step 1).
42
+ * Omitted = worker default; `≤ 1` disables the loop (single solve = the
43
+ * pre-convergence schedule).
44
+ */
45
+ convergenceIterations?: number
46
+ /**
47
+ * Co-optimize the SS claim age with the conversion optimum
48
+ * (optimizer-exact-ledger-convergence Track 1, Step 5). Off by default: it runs a bounded alternate-minimization over the
49
+ * claim grid (a small multiple of a single optimize) and can recommend a claim
50
+ * change alongside the schedule, so the caller must surface `claimAge` when on.
51
+ */
52
+ coOptimizeClaimAge?: boolean
53
+ }
54
+
55
+ export interface OptimizeResult {
56
+ schedule: OptimizedSchedule
57
+ postProcessed: OptimizePostProcessing | null
58
+ /** Bounded simple-candidate tournament; contains no ProjectionResult, so it is structured-clone safe as-is. */
59
+ tournament: ExactLedgerTournament
60
+ /** Convergence-loop diagnostics (Step 1); plain scalars, structured-clone safe. */
61
+ convergence: ExactLedgerConvergenceDiagnostics
62
+ /**
63
+ * Claim-age co-optimization diagnostics (Step 5); null unless requested. The
64
+ * `winningClaimPatch` (a plain incomes array) is structured-clone safe.
65
+ */
66
+ claimAge: ClaimAgeCoOptimization | null
67
+ }
68
+
69
+ export interface OptimizePostProcessing {
70
+ rawSchedule: OptimizedSchedule
71
+ cleanedSchedule: OptimizedSchedule
72
+ rawValidation: ExactLedgerValidation
73
+ cleanedValidation: ExactLedgerValidation
74
+ adjustments: ExactLedgerScheduleAdjustment[]
75
+ stabilized: boolean
76
+ iterationCount: number
77
+ pruneIterationCount: number
78
+ recommendationSchedule: ExactLedgerRecommendationSchedule
79
+ cleanedExecutionByYear: { year: number; rothConversion: number }[]
80
+ }
81
+
82
+ export type OptimizeResponse =
83
+ | { type: 'done'; result: OptimizeResult }
84
+ | { type: 'error'; message: string }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Web Worker entry for the V8 optimizer (roadmap V8). One request per run:
3
+ * solve the plan's conversion MILP off the main thread and post the schedule
4
+ * back. HiGHS-WASM is loaded lazily inside the worker — the ~3 MB wasm is
5
+ * emitted as a separate Vite asset (`?url`) and only fetched when Optimize is
6
+ * actually invoked, never in the main bundle.
7
+ */
8
+
9
+ // `highs/runtime` is the package's exported alias for build/highs.wasm; `?url`
10
+ // makes Vite emit it as a separate asset fetched lazily by the worker.
11
+ import wasmUrl from 'highs/runtime?url'
12
+ import type { OptimizeRequest, OptimizeResponse } from './messages'
13
+ import { runOptimizeRequest } from './runOptimize'
14
+
15
+ // Deployment marker: keeps the hashed worker asset moving when worker-level
16
+ // serving headers need to be refreshed through immutable CDN caches.
17
+ ;(self as unknown as { __retiregoldenOptimizerWorkerBuild?: string }).__retiregoldenOptimizerWorkerBuild =
18
+ 'optimizer-worker-csp-2026-06-25'
19
+
20
+ const post = (msg: OptimizeResponse) => (self as unknown as Worker).postMessage(msg)
21
+
22
+ self.onmessage = async (event: MessageEvent<OptimizeRequest>) => {
23
+ try {
24
+ const result = await runOptimizeRequest(event.data, () => wasmUrl)
25
+ post({ type: 'done', result })
26
+ } catch (err) {
27
+ post({ type: 'error', message: err instanceof Error ? err.message : String(err) })
28
+ }
29
+ }