@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.
- package/LICENSE +661 -0
- package/README.md +181 -0
- package/package.json +77 -0
- package/src/App.tsx +246 -0
- package/src/RouteErrorBoundary.tsx +45 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/data/fedInvestClient.ts +113 -0
- package/src/data/localStore.ts +42 -0
- package/src/data/planOrigin.ts +24 -0
- package/src/data/planStore.ts +165 -0
- package/src/data/v2Backup.ts +101 -0
- package/src/import/ImportPage.tsx +347 -0
- package/src/import/ReviewChecklistView.tsx +38 -0
- package/src/import/brokerCsv.ts +395 -0
- package/src/import/csv.ts +133 -0
- package/src/import/genericCsv.ts +224 -0
- package/src/import/projectionLab.ts +350 -0
- package/src/import/reviewChecklist.ts +33 -0
- package/src/import/tenForty.ts +275 -0
- package/src/index.css +630 -0
- package/src/index.ts +16 -0
- package/src/learn/ArticleBody.tsx +78 -0
- package/src/learn/ArticlePage.tsx +57 -0
- package/src/learn/GlossaryPage.tsx +33 -0
- package/src/learn/LearnAboutScreen.tsx +41 -0
- package/src/learn/LearnCards.tsx +41 -0
- package/src/learn/LearnLink.tsx +91 -0
- package/src/learn/LearningCenterPage.tsx +114 -0
- package/src/learn/SourcesPage.tsx +98 -0
- package/src/learn/components/ArticleFigure.tsx +34 -0
- package/src/learn/components/ArticleShell.tsx +86 -0
- package/src/learn/components/ComparisonTable.tsx +42 -0
- package/src/learn/components/FormulaBlock.tsx +34 -0
- package/src/learn/components/PurchasingPowerChart.tsx +41 -0
- package/src/learn/components/RelatedArticles.tsx +27 -0
- package/src/learn/components/ScenarioCard.tsx +24 -0
- package/src/learn/components/SourceList.tsx +23 -0
- package/src/learn/components/charts.tsx +21 -0
- package/src/learn/content/about-retiregolden.ts +100 -0
- package/src/learn/content/aca-premium-tax-credits-and-magi.ts +103 -0
- package/src/learn/content/account-types-overview.ts +106 -0
- package/src/learn/content/after-tax-estate.ts +111 -0
- package/src/learn/content/agi-magi-and-taxable-income.ts +112 -0
- package/src/learn/content/appealing-irmaa-ssa-44.ts +95 -0
- package/src/learn/content/assumption-general-inflation.ts +82 -0
- package/src/learn/content/assumption-healthcare-inflation.ts +85 -0
- package/src/learn/content/assumption-heir-tax-rate.ts +79 -0
- package/src/learn/content/assumption-investment-returns.ts +90 -0
- package/src/learn/content/assumption-longevity-planning-age.ts +78 -0
- package/src/learn/content/assumption-recent-magi.ts +83 -0
- package/src/learn/content/assumption-social-security-cola.ts +89 -0
- package/src/learn/content/assumption-social-security-trust-fund.ts +83 -0
- package/src/learn/content/assumption-state-tax-override.ts +79 -0
- package/src/learn/content/beneficiaries-and-account-titling.ts +99 -0
- package/src/learn/content/break-even-useful-lens.ts +94 -0
- package/src/learn/content/building-a-retirement-spending-budget.ts +100 -0
- package/src/learn/content/cola-and-inflation-protection.ts +102 -0
- package/src/learn/content/divorced-spousal-and-survivor-records.ts +104 -0
- package/src/learn/content/dynamic-spending-guardrails.ts +90 -0
- package/src/learn/content/earnings-test-before-fra.ts +100 -0
- package/src/learn/content/employer-match-and-contribution-order.ts +104 -0
- package/src/learn/content/examplePlanArticles.ts +525 -0
- package/src/learn/content/fees-expense-ratios-and-compounding-drag.ts +98 -0
- package/src/learn/content/fi-number-and-four-percent-rule.ts +64 -0
- package/src/learn/content/filling-a-tax-bracket-with-roth-conversions.ts +98 -0
- package/src/learn/content/funded-ratio.ts +70 -0
- package/src/learn/content/healthcare-after-65.ts +103 -0
- package/src/learn/content/healthcare-before-65.ts +104 -0
- package/src/learn/content/historical-vs-random-return-models.ts +101 -0
- package/src/learn/content/how-assumptions-change-the-answer.ts +105 -0
- package/src/learn/content/how-much-can-i-spend.ts +105 -0
- package/src/learn/content/how-social-security-is-taxed.ts +95 -0
- package/src/learn/content/how-the-optimizer-thinks.ts +102 -0
- package/src/learn/content/how-the-optimizer-values-after-tax-estate.ts +97 -0
- package/src/learn/content/how-to-model-accumulation.ts +67 -0
- package/src/learn/content/how-to-read-a-retirement-projection.ts +115 -0
- package/src/learn/content/hsas-and-qualified-medical-expenses.ts +108 -0
- package/src/learn/content/hsas-as-retirement-accounts.ts +101 -0
- package/src/learn/content/inflation-risk.ts +98 -0
- package/src/learn/content/inherited-ira-10-year-rule.ts +105 -0
- package/src/learn/content/insurance-in-your-retirement-plan.ts +103 -0
- package/src/learn/content/irmaa-two-year-lookback.ts +99 -0
- package/src/learn/content/long-term-care-costs-and-insurance.ts +103 -0
- package/src/learn/content/long-term-care-insurance-as-risk-transfer.ts +98 -0
- package/src/learn/content/longevity-risk.ts +99 -0
- package/src/learn/content/marginal-vs-effective-tax-rate.ts +98 -0
- package/src/learn/content/medicare-part-b-vs-part-d-irmaa.ts +102 -0
- package/src/learn/content/mortality-weighted-social-security.ts +113 -0
- package/src/learn/content/moving-to-retiregolden.ts +86 -0
- package/src/learn/content/niit-high-income-investment-tax.ts +98 -0
- package/src/learn/content/ordinary-income-vs-capital-gains.ts +103 -0
- package/src/learn/content/paying-conversion-taxes-taxable-vs-ira.ts +102 -0
- package/src/learn/content/pensions-and-annuities.ts +101 -0
- package/src/learn/content/permanent-life-insurance-in-a-plan.ts +106 -0
- package/src/learn/content/pia-aime-and-bend-points.ts +103 -0
- package/src/learn/content/planner-overview.ts +106 -0
- package/src/learn/content/planning-for-couples-and-survivor-years.ts +108 -0
- package/src/learn/content/privacy-what-stays-in-your-browser.ts +99 -0
- package/src/learn/content/qcds-qualified-charitable-distributions.ts +101 -0
- package/src/learn/content/reading-the-results-page.ts +96 -0
- package/src/learn/content/reading-the-social-security-analysis-page.ts +106 -0
- package/src/learn/content/real-estate-home-equity-and-debt.ts +100 -0
- package/src/learn/content/reports-csv-exports-and-sharing.ts +101 -0
- package/src/learn/content/risk-based-guardrails.ts +100 -0
- package/src/learn/content/rmds-required-minimum-distributions.ts +100 -0
- package/src/learn/content/roth-conversion-basics.ts +104 -0
- package/src/learn/content/rsus-and-espp.ts +101 -0
- package/src/learn/content/rule-of-55-and-72t.ts +107 -0
- package/src/learn/content/savings-rate-biggest-lever.ts +66 -0
- package/src/learn/content/seed-your-plan-from-your-tax-return.ts +93 -0
- package/src/learn/content/sensitivity-testing-what-changes-the-answer.ts +104 -0
- package/src/learn/content/sequence-of-returns-risk.ts +98 -0
- package/src/learn/content/social-security-bridge.ts +67 -0
- package/src/learn/content/social-security-claiming-age-basics.ts +113 -0
- package/src/learn/content/social-security-taxes-vs-benefits.ts +76 -0
- package/src/learn/content/spending-profiles-and-the-retirement-smile.ts +92 -0
- package/src/learn/content/spousal-and-survivor-benefits.ts +120 -0
- package/src/learn/content/ssdi-and-retirement-planning.ts +72 -0
- package/src/learn/content/standard-deduction-senior-deduction-and-itemizing.ts +97 -0
- package/src/learn/content/state-income-taxes-in-retirement.ts +97 -0
- package/src/learn/content/step-up-in-basis.ts +102 -0
- package/src/learn/content/survivor-planning-for-couples.ts +110 -0
- package/src/learn/content/survivor-spending-in-couple-plans.ts +98 -0
- package/src/learn/content/tax-cliffs-and-bracket-edges.ts +105 -0
- package/src/learn/content/tax-loss-and-gain-harvesting.ts +99 -0
- package/src/learn/content/taxable-brokerage-basis-and-capital-gains.ts +99 -0
- package/src/learn/content/three-big-questions-spending-time-risk.ts +103 -0
- package/src/learn/content/tips-ladders.ts +92 -0
- package/src/learn/content/todays-dollars-vs-future-dollars.ts +107 -0
- package/src/learn/content/traditional-vs-roth-contributions.ts +113 -0
- package/src/learn/content/troubleshooting-surprising-results.ts +105 -0
- package/src/learn/content/trust-fund-haircut-scenarios.ts +101 -0
- package/src/learn/content/understanding-monte-carlo-success-rate.ts +118 -0
- package/src/learn/content/understanding-your-plan-assumptions.ts +134 -0
- package/src/learn/content/using-assumptions-and-provenance.ts +98 -0
- package/src/learn/content/using-scenarios-to-compare-choices.ts +99 -0
- package/src/learn/content/what-changes-when-you-move-states.ts +141 -0
- package/src/learn/content/what-is-fire.ts +65 -0
- package/src/learn/content/what-monte-carlo-proves.ts +98 -0
- package/src/learn/content/what-retiregolden-models.ts +103 -0
- package/src/learn/content/what-retirement-healthcare-really-costs.ts +117 -0
- package/src/learn/content/why-95-percent-is-not-a-guarantee.ts +98 -0
- package/src/learn/content/why-roth-conversions-raise-other-costs.ts +106 -0
- package/src/learn/content/why-small-tax-cliffs-can-matter.ts +109 -0
- package/src/learn/content/widows-penalty-and-survivor-brackets.ts +106 -0
- package/src/learn/content/withdrawal-order-basics.ts +105 -0
- package/src/learn/glossary.ts +191 -0
- package/src/learn/inlineMarkdown.tsx +54 -0
- package/src/learn/learn.css +537 -0
- package/src/learn/learningRegistry.ts +502 -0
- package/src/longevity/LongevityResults.tsx +85 -0
- package/src/longevity/LongevityWizard.tsx +305 -0
- package/src/longevity/constants.ts +15 -0
- package/src/longevity/factors.ts +125 -0
- package/src/longevity/model.ts +31 -0
- package/src/longevity/persistedGuard.ts +129 -0
- package/src/longevity/storage.ts +40 -0
- package/src/mc/messages.ts +118 -0
- package/src/mc/monteCarlo.worker.ts +44 -0
- package/src/mc/pool.ts +267 -0
- package/src/mc/runRequest.ts +125 -0
- package/src/optimize/messages.ts +84 -0
- package/src/optimize/optimize.worker.ts +29 -0
- package/src/optimize/runOptimize.ts +92 -0
- package/src/optimize/runSpendingSolve.ts +47 -0
- package/src/optimize/runner.ts +21 -0
- package/src/optimize/spendingMessages.ts +44 -0
- package/src/optimize/spendingRunner.ts +21 -0
- package/src/optimize/spendingSolve.worker.ts +18 -0
- package/src/planner/AssumptionsCardPage.tsx +136 -0
- package/src/planner/BucketLensCard.tsx +114 -0
- package/src/planner/ComparePlansPage.tsx +219 -0
- package/src/planner/DisclaimerPage.tsx +88 -0
- package/src/planner/HowTestedPage.tsx +159 -0
- package/src/planner/LiveStatus.tsx +15 -0
- package/src/planner/LongevityModal.tsx +55 -0
- package/src/planner/Modal.tsx +97 -0
- package/src/planner/MonteCarloPage.tsx +907 -0
- package/src/planner/OptimizePage.tsx +611 -0
- package/src/planner/PlanContext.tsx +198 -0
- package/src/planner/PlanPickerPage.tsx +124 -0
- package/src/planner/PlanWorkspace.tsx +290 -0
- package/src/planner/ProvenancePanel.tsx +45 -0
- package/src/planner/RelocationComparePage.tsx +485 -0
- package/src/planner/ReportPage.tsx +375 -0
- package/src/planner/ResultsPage.tsx +817 -0
- package/src/planner/ScenariosPage.tsx +285 -0
- package/src/planner/SocialSecuritySection.tsx +556 -0
- package/src/planner/SpendingSolverPage.tsx +512 -0
- package/src/planner/SsAnalysisPage.tsx +1134 -0
- package/src/planner/SurvivalPercentileModal.tsx +161 -0
- package/src/planner/SurvivorTransitionPage.tsx +286 -0
- package/src/planner/assumptionsExport.ts +371 -0
- package/src/planner/bucketLens.ts +89 -0
- package/src/planner/chartFrame.ts +8 -0
- package/src/planner/chartStyle.ts +11 -0
- package/src/planner/dialogViews.tsx +184 -0
- package/src/planner/dialogs.tsx +133 -0
- package/src/planner/examples/ExampleLibrary.tsx +189 -0
- package/src/planner/examples/ExamplePreviewBanner.tsx +55 -0
- package/src/planner/examples/ExamplesPage.tsx +25 -0
- package/src/planner/examples/OpenExampleButton.tsx +61 -0
- package/src/planner/examples/buildAggressiveSaver.ts +102 -0
- package/src/planner/examples/buildAnnuityEstate.ts +137 -0
- package/src/planner/examples/buildBaristaFire.ts +115 -0
- package/src/planner/examples/buildBracketFillRoth.ts +65 -0
- package/src/planner/examples/buildBridgeEarlyRetirement.ts +94 -0
- package/src/planner/examples/buildBrokerageNoHsa.ts +109 -0
- package/src/planner/examples/buildCoastFire.ts +88 -0
- package/src/planner/examples/buildContext.ts +20 -0
- package/src/planner/examples/buildEarlyCareerMatch.ts +93 -0
- package/src/planner/examples/buildEarlyRetireeAca.ts +61 -0
- package/src/planner/examples/buildExampleCouple.ts +103 -0
- package/src/planner/examples/buildFixedTargetSpending.ts +74 -0
- package/src/planner/examples/buildGlidepathAllocation.ts +131 -0
- package/src/planner/examples/buildGuardrailsFlex.ts +120 -0
- package/src/planner/examples/buildHsaPropertyDepth.ts +109 -0
- package/src/planner/examples/buildHsaStealthRetirement.ts +97 -0
- package/src/planner/examples/buildLeanFatFire.ts +109 -0
- package/src/planner/examples/buildLtcShock.ts +62 -0
- package/src/planner/examples/buildMovingStateTax.ts +53 -0
- package/src/planner/examples/buildNoAnnuityBrokerage.ts +92 -0
- package/src/planner/examples/buildRmdIrmaa.ts +55 -0
- package/src/planner/examples/buildSalaryGrowthEscalation.ts +96 -0
- package/src/planner/examples/buildStaticAllocationControl.ts +96 -0
- package/src/planner/examples/buildSurvivorYears.ts +62 -0
- package/src/planner/examples/buildUnderSavedSingle.ts +51 -0
- package/src/planner/examples/exampleCopy.ts +23 -0
- package/src/planner/examples/loadExample.ts +90 -0
- package/src/planner/examples/registry.ts +313 -0
- package/src/planner/explainPanels.tsx +233 -0
- package/src/planner/fields.tsx +381 -0
- package/src/planner/format.ts +33 -0
- package/src/planner/home/DataAndPrivacyCard.tsx +56 -0
- package/src/planner/home/GettingStartedPaths.tsx +46 -0
- package/src/planner/home/GettingStartedReopener.tsx +32 -0
- package/src/planner/home/StartHereLinks.tsx +22 -0
- package/src/planner/home/WelcomeHero.tsx +39 -0
- package/src/planner/home/YourPlans.tsx +72 -0
- package/src/planner/home/importErrorMessage.ts +22 -0
- package/src/planner/home/startHereSlugs.ts +7 -0
- package/src/planner/home/useHomeData.ts +190 -0
- package/src/planner/home/useHomeMode.ts +47 -0
- package/src/planner/householdActions.ts +22 -0
- package/src/planner/insights/InsightCardView.tsx +340 -0
- package/src/planner/insights/InsightsPage.tsx +204 -0
- package/src/planner/insights/categoryLabels.ts +11 -0
- package/src/planner/learnLinks.ts +85 -0
- package/src/planner/marketModelPicker.ts +172 -0
- package/src/planner/optimizePageChart.ts +40 -0
- package/src/planner/optimizePageClaim.ts +64 -0
- package/src/planner/planCompleteness.ts +27 -0
- package/src/planner/planContextCore.ts +26 -0
- package/src/planner/planner.css +2304 -0
- package/src/planner/provenanceLinks.ts +25 -0
- package/src/planner/sections/AccountFields.tsx +872 -0
- package/src/planner/sections/AccountsSection.tsx +89 -0
- package/src/planner/sections/AllocationPanel.tsx +261 -0
- package/src/planner/sections/AssumptionsSection.tsx +256 -0
- package/src/planner/sections/HouseholdSection.tsx +243 -0
- package/src/planner/sections/IncomeFloorSection.tsx +418 -0
- package/src/planner/sections/IncomeSection.tsx +170 -0
- package/src/planner/sections/InsuranceSection.tsx +362 -0
- package/src/planner/sections/SpendingSection.tsx +904 -0
- package/src/planner/sections/StrategySection.tsx +349 -0
- package/src/planner/sections/UpdateBalancesPanel.tsx +182 -0
- package/src/planner/sections/sectionHelpers.ts +48 -0
- package/src/planner/sections/shared.tsx +15 -0
- package/src/planner/sections.tsx +15 -0
- package/src/planner/ssAnalysis.ts +325 -0
- package/src/planner/successBand.ts +20 -0
- package/src/planner/survivorAnalysis.ts +277 -0
- package/src/planner/usStates.ts +19 -0
- package/src/planner/useMcSuccessRate.ts +77 -0
- package/src/planner/useProjection.ts +63 -0
- package/src/relocation/messages.ts +21 -0
- package/src/relocation/relocation.worker.ts +18 -0
- package/src/relocation/runRelocation.ts +17 -0
- package/src/relocation/runner.ts +22 -0
- package/src/report/brandingContext.ts +15 -0
- package/src/report/downloadReport.ts +34 -0
- package/src/report/reportHtml.ts +547 -0
- package/src/routes/LearnRoutes.tsx +46 -0
- package/src/routes/PlanRoutes.tsx +55 -0
- package/src/routes/RouteFallback.tsx +9 -0
- package/src/socialSecurity/breakEven.ts +107 -0
- package/src/socialSecurity/expectedPv.ts +164 -0
- package/src/socialSecurity/explain.ts +92 -0
- package/src/socialSecurity/ficaReturn.ts +81 -0
- package/src/socialSecurity/persistedSsGuard.ts +138 -0
- package/src/socialSecurity/ssFormUtils.ts +48 -0
- package/src/socialSecurity/ssaStatementXml.ts +156 -0
- package/src/socialSecurity/storage.ts +69 -0
- package/src/socialSecurity/survivorSwitching.ts +153 -0
- package/src/testSupport/samplePlan.ts +2 -0
- 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
|
+
}
|