@retiregolden/engine 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 +79 -0
- package/dist/allocation/assetClasses.d.ts +94 -0
- package/dist/allocation/assetClasses.js +215 -0
- package/dist/decisions/annuitization.d.ts +72 -0
- package/dist/decisions/annuitization.js +184 -0
- package/dist/decisions/decisionFixtures.d.ts +48 -0
- package/dist/decisions/decisionFixtures.js +253 -0
- package/dist/decisions/evaluateCandidate.d.ts +61 -0
- package/dist/decisions/evaluateCandidate.js +193 -0
- package/dist/decisions/generators.d.ts +110 -0
- package/dist/decisions/generators.js +667 -0
- package/dist/decisions/index.d.ts +17 -0
- package/dist/decisions/index.js +17 -0
- package/dist/decisions/insightsAdapter.d.ts +27 -0
- package/dist/decisions/insightsAdapter.js +51 -0
- package/dist/decisions/objectives.d.ts +96 -0
- package/dist/decisions/objectives.js +319 -0
- package/dist/decisions/pensionElection.d.ts +112 -0
- package/dist/decisions/pensionElection.js +239 -0
- package/dist/decisions/search.d.ts +51 -0
- package/dist/decisions/search.js +110 -0
- package/dist/decisions/spendingSolver.d.ts +70 -0
- package/dist/decisions/spendingSolver.js +180 -0
- package/dist/decisions/spiaQuotes.d.ts +21 -0
- package/dist/decisions/spiaQuotes.js +43 -0
- package/dist/decisions/stochastic.d.ts +11 -0
- package/dist/decisions/stochastic.js +66 -0
- package/dist/decisions/swrComparator.d.ts +54 -0
- package/dist/decisions/swrComparator.js +89 -0
- package/dist/decisions/tournament.d.ts +53 -0
- package/dist/decisions/tournament.js +104 -0
- package/dist/decisions/types.d.ts +125 -0
- package/dist/decisions/types.js +12 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/insights/detectors/annuitizationHeadroom.d.ts +11 -0
- package/dist/insights/detectors/annuitizationHeadroom.js +77 -0
- package/dist/insights/detectors/assetLocation.d.ts +9 -0
- package/dist/insights/detectors/assetLocation.js +86 -0
- package/dist/insights/detectors/hecmBufferCandidate.d.ts +12 -0
- package/dist/insights/detectors/hecmBufferCandidate.js +76 -0
- package/dist/insights/detectors/incomeFloorFunded.d.ts +8 -0
- package/dist/insights/detectors/incomeFloorFunded.js +52 -0
- package/dist/insights/detectors/irmaaTierEdge.d.ts +2 -0
- package/dist/insights/detectors/irmaaTierEdge.js +102 -0
- package/dist/insights/detectors/pensionElectionPending.d.ts +11 -0
- package/dist/insights/detectors/pensionElectionPending.js +62 -0
- package/dist/insights/detectors/qcdEfficiency.d.ts +2 -0
- package/dist/insights/detectors/qcdEfficiency.js +62 -0
- package/dist/insights/detectors/rothBridgeHeadroom.d.ts +2 -0
- package/dist/insights/detectors/rothBridgeHeadroom.js +77 -0
- package/dist/insights/detectors/spendingGuardrails.d.ts +2 -0
- package/dist/insights/detectors/spendingGuardrails.js +77 -0
- package/dist/insights/detectors/spendingHeadroom.d.ts +12 -0
- package/dist/insights/detectors/spendingHeadroom.js +92 -0
- package/dist/insights/detectors/ssBridgeGap.d.ts +9 -0
- package/dist/insights/detectors/ssBridgeGap.js +92 -0
- package/dist/insights/detectors/stateRelocation.d.ts +12 -0
- package/dist/insights/detectors/stateRelocation.js +99 -0
- package/dist/insights/detectors/widowsPenalty.d.ts +12 -0
- package/dist/insights/detectors/widowsPenalty.js +145 -0
- package/dist/insights/registry.d.ts +4 -0
- package/dist/insights/registry.js +62 -0
- package/dist/insights/runInsights.d.ts +6 -0
- package/dist/insights/runInsights.js +15 -0
- package/dist/insights/types.d.ts +70 -0
- package/dist/insights/types.js +1 -0
- package/dist/ladder/bridge.d.ts +72 -0
- package/dist/ladder/bridge.js +59 -0
- package/dist/ladder/fedInvest.d.ts +55 -0
- package/dist/ladder/fedInvest.js +77 -0
- package/dist/ladder/fundedRatio.d.ts +41 -0
- package/dist/ladder/fundedRatio.js +46 -0
- package/dist/ladder/ladderMath.d.ts +88 -0
- package/dist/ladder/ladderMath.js +140 -0
- package/dist/longevity/ssaPeriod2022.d.ts +10 -0
- package/dist/longevity/ssaPeriod2022.js +51 -0
- package/dist/longevity/types.d.ts +39 -0
- package/dist/longevity/types.js +1 -0
- package/dist/model/migrations.d.ts +20 -0
- package/dist/model/migrations.js +67 -0
- package/dist/model/plan.d.ts +3108 -0
- package/dist/model/plan.js +1514 -0
- package/dist/montecarlo/frontiers.d.ts +22 -0
- package/dist/montecarlo/frontiers.js +61 -0
- package/dist/montecarlo/historicalReturns.d.ts +27 -0
- package/dist/montecarlo/historicalReturns.js +124 -0
- package/dist/montecarlo/historicalSuites.d.ts +45 -0
- package/dist/montecarlo/historicalSuites.js +102 -0
- package/dist/montecarlo/ltcShock.d.ts +42 -0
- package/dist/montecarlo/ltcShock.js +62 -0
- package/dist/montecarlo/marketModels.d.ts +248 -0
- package/dist/montecarlo/marketModels.js +672 -0
- package/dist/montecarlo/mortality.d.ts +31 -0
- package/dist/montecarlo/mortality.js +61 -0
- package/dist/montecarlo/riskBasedGuardrails.d.ts +93 -0
- package/dist/montecarlo/riskBasedGuardrails.js +200 -0
- package/dist/montecarlo/rng.d.ts +18 -0
- package/dist/montecarlo/rng.js +44 -0
- package/dist/montecarlo/run.d.ts +210 -0
- package/dist/montecarlo/run.js +353 -0
- package/dist/montecarlo/sharedPaths.d.ts +43 -0
- package/dist/montecarlo/sharedPaths.js +28 -0
- package/dist/montecarlo/survival.d.ts +49 -0
- package/dist/montecarlo/survival.js +126 -0
- package/dist/params/data/realYieldCurve2026.d.ts +17 -0
- package/dist/params/data/realYieldCurve2026.js +26 -0
- package/dist/params/data/year2026.d.ts +14 -0
- package/dist/params/data/year2026.js +199 -0
- package/dist/params/index.d.ts +63 -0
- package/dist/params/index.js +134 -0
- package/dist/params/provenance.d.ts +29 -0
- package/dist/params/provenance.js +123 -0
- package/dist/params/state/data/year2026.d.ts +18 -0
- package/dist/params/state/data/year2026.js +581 -0
- package/dist/params/state/index.d.ts +13 -0
- package/dist/params/state/index.js +26 -0
- package/dist/params/state/types.d.ts +73 -0
- package/dist/params/state/types.js +10 -0
- package/dist/params/types.d.ts +201 -0
- package/dist/params/types.js +7 -0
- package/dist/projection/annuityForms.d.ts +62 -0
- package/dist/projection/annuityForms.js +87 -0
- package/dist/projection/compare.d.ts +96 -0
- package/dist/projection/compare.js +216 -0
- package/dist/projection/flatTax.d.ts +7 -0
- package/dist/projection/flatTax.js +14 -0
- package/dist/projection/optimizePlan.d.ts +328 -0
- package/dist/projection/optimizePlan.js +1039 -0
- package/dist/projection/relocation.d.ts +149 -0
- package/dist/projection/relocation.js +317 -0
- package/dist/projection/simulate.d.ts +61 -0
- package/dist/projection/simulate.js +2754 -0
- package/dist/projection/types.d.ts +381 -0
- package/dist/projection/types.js +9 -0
- package/dist/rmd/jointLifeTable.d.ts +8 -0
- package/dist/rmd/jointLifeTable.js +663 -0
- package/dist/rmd/rmd.d.ts +29 -0
- package/dist/rmd/rmd.js +34 -0
- package/dist/scenarios/scenarios.d.ts +70 -0
- package/dist/scenarios/scenarios.js +130 -0
- package/dist/socialSecurity/benefitFactor.d.ts +13 -0
- package/dist/socialSecurity/benefitFactor.js +40 -0
- package/dist/socialSecurity/claimFactor.d.ts +33 -0
- package/dist/socialSecurity/claimFactor.js +58 -0
- package/dist/socialSecurity/disability.d.ts +36 -0
- package/dist/socialSecurity/disability.js +42 -0
- package/dist/socialSecurity/familyMaximum.d.ts +38 -0
- package/dist/socialSecurity/familyMaximum.js +48 -0
- package/dist/socialSecurity/maritalBenefits.d.ts +57 -0
- package/dist/socialSecurity/maritalBenefits.js +87 -0
- package/dist/socialSecurity/nra.d.ts +30 -0
- package/dist/socialSecurity/nra.js +83 -0
- package/dist/socialSecurity/piaFromEarnings.d.ts +100 -0
- package/dist/socialSecurity/piaFromEarnings.js +202 -0
- package/dist/socialSecurity/ssaWageData.d.ts +68 -0
- package/dist/socialSecurity/ssaWageData.js +306 -0
- package/dist/socialSecurity/survivorBenefit.d.ts +61 -0
- package/dist/socialSecurity/survivorBenefit.js +61 -0
- package/dist/spending/abw.d.ts +57 -0
- package/dist/spending/abw.js +80 -0
- package/dist/spending/flexibleGoals.d.ts +62 -0
- package/dist/spending/flexibleGoals.js +136 -0
- package/dist/spending/guardrails.d.ts +78 -0
- package/dist/spending/guardrails.js +100 -0
- package/dist/spending/layers.d.ts +81 -0
- package/dist/spending/layers.js +67 -0
- package/dist/spending/shapePresets.d.ts +42 -0
- package/dist/spending/shapePresets.js +77 -0
- package/dist/strategies/accountEligibility.d.ts +81 -0
- package/dist/strategies/accountEligibility.js +98 -0
- package/dist/strategies/inheritedIra.d.ts +45 -0
- package/dist/strategies/inheritedIra.js +43 -0
- package/dist/strategies/iraBasis.d.ts +48 -0
- package/dist/strategies/iraBasis.js +46 -0
- package/dist/strategies/optimizer.d.ts +307 -0
- package/dist/strategies/optimizer.js +551 -0
- package/dist/strategies/rothBasis.d.ts +67 -0
- package/dist/strategies/rothBasis.js +83 -0
- package/dist/strategies/rothConversion.d.ts +55 -0
- package/dist/strategies/rothConversion.js +83 -0
- package/dist/strategies/sepp.d.ts +38 -0
- package/dist/strategies/sepp.js +53 -0
- package/dist/tax/aca.d.ts +40 -0
- package/dist/tax/aca.js +82 -0
- package/dist/tax/federalTax.d.ts +110 -0
- package/dist/tax/federalTax.js +249 -0
- package/dist/tax/medicare.d.ts +20 -0
- package/dist/tax/medicare.js +36 -0
- package/dist/tax/propertySale.d.ts +46 -0
- package/dist/tax/propertySale.js +35 -0
- package/dist/tax/stateTax.d.ts +66 -0
- package/dist/tax/stateTax.js +198 -0
- package/dist/testing/money.d.ts +7 -0
- package/dist/testing/money.js +17 -0
- package/dist/testing/planFixtures.d.ts +29 -0
- package/dist/testing/planFixtures.js +141 -0
- package/package.json +75 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Period life expectancy e(x) — **remaining years** at exact integer age x.
|
|
3
|
+
* Source: SSA Table 4C6, 2022 (2025 TR), male/female columns.
|
|
4
|
+
* @see https://www.ssa.gov/oact/STATS/table4c6.html
|
|
5
|
+
*/
|
|
6
|
+
export const MALE = [
|
|
7
|
+
/* 0–17 */ 74.74, 74.2, 73.23, 72.25, 71.27, 70.29, 69.3, 68.31, 67.32, 66.32, 65.33,
|
|
8
|
+
64.34, 63.35, 62.36, 61.37, 60.39, 59.42, 58.46,
|
|
9
|
+
/* 18 */ 57.5, 56.56, 55.63, 54.7, 53.78, 52.86, 51.94, 51.03, 50.12, 49.21, 48.31, 47.41,
|
|
10
|
+
46.51, 45.62, 44.73, 43.84, 42.96, 42.08, 41.19, 40.31, 39.43, 38.55, 37.67, 36.8, 35.93,
|
|
11
|
+
35.05, 34.19, 33.32, 32.46, 31.6, 30.75, 29.9, 29.05, 28.22, 27.39, 26.56, 25.75, 24.94,
|
|
12
|
+
24.15, 23.37, 22.59, 21.83, 21.08, 20.34, 19.61, 18.89, 18.18, 17.48, 16.79, 16.11, 15.43,
|
|
13
|
+
14.76, 14.09, 13.44, 12.8, 12.16, 11.53, 10.92, 10.32, 9.74, 9.18, 8.64, 8.11, 7.6, 7.11,
|
|
14
|
+
6.64, 6.18, 5.75, 5.34, 4.94, 4.58, 4.23, 3.91, 3.6, 3.32, 3.06, 2.83, 2.63, 2.44, 2.28,
|
|
15
|
+
2.13, 2.0, 1.88, 1.76, 1.66, 1.56, 1.47, 1.39, 1.31, 1.23, 1.15, 1.08, 1.01, 0.94, 0.87,
|
|
16
|
+
0.81, 0.75, 0.7, 0.64, 0.59, 0.54, 0.5,
|
|
17
|
+
];
|
|
18
|
+
export const FEMALE = [
|
|
19
|
+
/* 0–17 */ 80.18, 79.6, 78.63, 77.65, 76.66, 75.67, 74.68, 73.69, 72.7, 71.71, 70.72, 69.72,
|
|
20
|
+
68.73, 67.74, 66.75, 65.76, 64.78, 63.8,
|
|
21
|
+
/* 18 */ 62.82, 61.84, 60.87, 59.9, 58.93, 57.97, 57.0, 56.04, 55.08, 54.12, 53.16, 52.2,
|
|
22
|
+
51.25, 50.3, 49.35, 48.41, 47.47, 46.53, 45.59, 44.65, 43.72, 42.79, 41.86, 40.93, 40.01,
|
|
23
|
+
39.09, 38.17, 37.25, 36.34, 35.43, 34.53, 33.63, 32.73, 31.84, 30.96, 30.08, 29.2, 28.34,
|
|
24
|
+
27.48, 26.63, 25.78, 24.95, 24.12, 23.31, 22.5, 21.7, 20.9, 20.12, 19.34, 18.56, 17.79,
|
|
25
|
+
17.03, 16.27, 15.53, 14.8, 14.08, 13.37, 12.68, 12.0, 11.35, 10.71, 10.09, 9.49, 8.9, 8.34,
|
|
26
|
+
7.79, 7.26, 6.76, 6.28, 5.83, 5.4, 5.0, 4.62, 4.27, 3.94, 3.64, 3.36, 3.1, 2.87, 2.66, 2.47,
|
|
27
|
+
2.3, 2.14, 2.0, 1.87, 1.75, 1.63, 1.52, 1.42, 1.32, 1.23, 1.14, 1.05, 0.97, 0.89, 0.82,
|
|
28
|
+
0.75, 0.7, 0.64, 0.59, 0.54, 0.5,
|
|
29
|
+
];
|
|
30
|
+
function at(table, age) {
|
|
31
|
+
const i = Math.round(age);
|
|
32
|
+
if (i < 0)
|
|
33
|
+
return table[0];
|
|
34
|
+
if (i >= table.length)
|
|
35
|
+
return table[table.length - 1];
|
|
36
|
+
return table[i];
|
|
37
|
+
}
|
|
38
|
+
/** SSA baseline: remaining years at integer age (linear between ages if fractional). */
|
|
39
|
+
export function baselineRemainingYears(age, sex) {
|
|
40
|
+
const a = Math.min(Math.max(age, 0), MALE.length - 1 - 1e-9);
|
|
41
|
+
const lo = Math.floor(a);
|
|
42
|
+
const hi = Math.ceil(a);
|
|
43
|
+
const t = a - lo;
|
|
44
|
+
const m = at(MALE, lo) * (1 - t) + at(MALE, hi) * t;
|
|
45
|
+
const f = at(FEMALE, lo) * (1 - t) + at(FEMALE, hi) * t;
|
|
46
|
+
if (sex === 'male')
|
|
47
|
+
return m;
|
|
48
|
+
if (sex === 'female')
|
|
49
|
+
return f;
|
|
50
|
+
return (m + f) / 2;
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type Sex = 'male' | 'female' | 'average';
|
|
2
|
+
export type BmiCategory = 'underweight' | 'normal' | 'overweight' | 'obese_i' | 'obese_ii_plus';
|
|
3
|
+
export type Smoking = 'never' | 'former_15y' | 'former_lt15y' | 'current_light' | 'current_moderate' | 'current_heavy';
|
|
4
|
+
export type Alcohol = 'never' | 'light' | 'moderate' | 'heavy';
|
|
5
|
+
export type Activity = 'high' | 'moderate' | 'low' | 'sedentary';
|
|
6
|
+
export type Diabetes = 'no' | 'yes_controlled' | 'yes_uncontrolled';
|
|
7
|
+
export type SelfRatedHealth = 'excellent' | 'good' | 'fair' | 'poor' | 'very_poor';
|
|
8
|
+
export type ParentalLongevity = 'both_80' | 'one_80' | 'neither_80' | 'unknown';
|
|
9
|
+
export interface LongevityAnswers {
|
|
10
|
+
age: number;
|
|
11
|
+
sex: Sex;
|
|
12
|
+
bmiCategory: BmiCategory;
|
|
13
|
+
smoking: Smoking;
|
|
14
|
+
alcohol: Alcohol;
|
|
15
|
+
activity: Activity;
|
|
16
|
+
diabetes: Diabetes;
|
|
17
|
+
selfRatedHealth: SelfRatedHealth;
|
|
18
|
+
parentalLongevity: ParentalLongevity;
|
|
19
|
+
}
|
|
20
|
+
export interface LongevityResult {
|
|
21
|
+
baselineRemainingYears: number;
|
|
22
|
+
/** Product of lifestyle/health factors (before clamp) */
|
|
23
|
+
rawMultiplier: number;
|
|
24
|
+
/** After conservative clamp */
|
|
25
|
+
appliedMultiplier: number;
|
|
26
|
+
/** Baseline × appliedMultiplier */
|
|
27
|
+
centralRemainingYears: number;
|
|
28
|
+
/** Illustrative band — not statistical confidence */
|
|
29
|
+
bandLowRemainingYears: number;
|
|
30
|
+
bandHighRemainingYears: number;
|
|
31
|
+
/** age + central (rounded) */
|
|
32
|
+
illustrativePlanningAge: number;
|
|
33
|
+
}
|
|
34
|
+
export interface LongevityPersisted {
|
|
35
|
+
version: 1;
|
|
36
|
+
answers: LongevityAnswers;
|
|
37
|
+
result: LongevityResult;
|
|
38
|
+
updatedAt: string;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan schema migrations.
|
|
3
|
+
*
|
|
4
|
+
* Every persisted or imported plan passes through `migratePlanToCurrent`
|
|
5
|
+
* before `parsePlan`. When the schema changes: bump CURRENT_PLAN_SCHEMA_VERSION,
|
|
6
|
+
* register a pure `fromVersion -> fromVersion+1` step here, and add a fixture
|
|
7
|
+
* test in migrations.test.ts. Steps must never throw on well-formed input of
|
|
8
|
+
* their version; unknown shapes fall through to parsePlan's validation.
|
|
9
|
+
*/
|
|
10
|
+
import { type Plan } from './plan.js';
|
|
11
|
+
export type MigrationStep = (raw: Record<string, unknown>) => Record<string, unknown>;
|
|
12
|
+
export type MigrateResult = {
|
|
13
|
+
ok: true;
|
|
14
|
+
plan: Plan;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
reason: 'not_object' | 'bad_version' | 'newer_than_app' | 'missing_step' | 'invalid_after_migration';
|
|
18
|
+
issues?: string[];
|
|
19
|
+
};
|
|
20
|
+
export declare function migratePlanToCurrent(input: unknown, registry?: Record<number, MigrationStep>, currentVersion?: number): MigrateResult;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan schema migrations.
|
|
3
|
+
*
|
|
4
|
+
* Every persisted or imported plan passes through `migratePlanToCurrent`
|
|
5
|
+
* before `parsePlan`. When the schema changes: bump CURRENT_PLAN_SCHEMA_VERSION,
|
|
6
|
+
* register a pure `fromVersion -> fromVersion+1` step here, and add a fixture
|
|
7
|
+
* test in migrations.test.ts. Steps must never throw on well-formed input of
|
|
8
|
+
* their version; unknown shapes fall through to parsePlan's validation.
|
|
9
|
+
*/
|
|
10
|
+
import { CURRENT_PLAN_SCHEMA_VERSION, parsePlan } from './plan.js';
|
|
11
|
+
/** Keyed by the version the step migrates FROM. v1 is current — empty today. */
|
|
12
|
+
const defaultRegistry = {};
|
|
13
|
+
function normalizeCurrentPlan(raw) {
|
|
14
|
+
const household = raw['household'];
|
|
15
|
+
const accounts = raw['accounts'];
|
|
16
|
+
if (typeof household !== 'object' || household === null || Array.isArray(household) || !Array.isArray(accounts)) {
|
|
17
|
+
return raw;
|
|
18
|
+
}
|
|
19
|
+
const people = household['people'];
|
|
20
|
+
if (!Array.isArray(people))
|
|
21
|
+
return raw;
|
|
22
|
+
const primary = people[0];
|
|
23
|
+
if (typeof primary !== 'object' || primary === null || Array.isArray(primary))
|
|
24
|
+
return raw;
|
|
25
|
+
const primaryId = primary['id'];
|
|
26
|
+
if (typeof primaryId !== 'string' || primaryId.length === 0)
|
|
27
|
+
return raw;
|
|
28
|
+
let changed = false;
|
|
29
|
+
const normalizedAccounts = accounts.map((account) => {
|
|
30
|
+
if (typeof account !== 'object' || account === null || Array.isArray(account))
|
|
31
|
+
return account;
|
|
32
|
+
const accountRecord = account;
|
|
33
|
+
if ((accountRecord['type'] === 'traditional' || accountRecord['type'] === 'roth' || accountRecord['type'] === 'hsa') &&
|
|
34
|
+
accountRecord['ownerPersonId'] === null) {
|
|
35
|
+
changed = true;
|
|
36
|
+
return { ...accountRecord, ownerPersonId: primaryId };
|
|
37
|
+
}
|
|
38
|
+
return account;
|
|
39
|
+
});
|
|
40
|
+
return changed ? { ...raw, accounts: normalizedAccounts } : raw;
|
|
41
|
+
}
|
|
42
|
+
export function migratePlanToCurrent(input, registry = defaultRegistry, currentVersion = CURRENT_PLAN_SCHEMA_VERSION) {
|
|
43
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
44
|
+
return { ok: false, reason: 'not_object' };
|
|
45
|
+
}
|
|
46
|
+
let raw = input;
|
|
47
|
+
const v = raw['schemaVersion'];
|
|
48
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 1) {
|
|
49
|
+
return { ok: false, reason: 'bad_version' };
|
|
50
|
+
}
|
|
51
|
+
if (v > currentVersion) {
|
|
52
|
+
// A backup from a newer app build; never destructively "fix" it.
|
|
53
|
+
return { ok: false, reason: 'newer_than_app' };
|
|
54
|
+
}
|
|
55
|
+
for (let from = v; from < currentVersion; from++) {
|
|
56
|
+
const step = registry[from];
|
|
57
|
+
if (!step)
|
|
58
|
+
return { ok: false, reason: 'missing_step' };
|
|
59
|
+
raw = { ...step(raw), schemaVersion: from + 1 };
|
|
60
|
+
}
|
|
61
|
+
raw = normalizeCurrentPlan(raw);
|
|
62
|
+
const parsed = parsePlan(raw);
|
|
63
|
+
if (!parsed.ok) {
|
|
64
|
+
return { ok: false, reason: 'invalid_after_migration', issues: parsed.issues };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, plan: parsed.plan };
|
|
67
|
+
}
|