# Model: relocation — v1.0.0 (as-implemented)

Engine: `calcRelocation(inputs: RelocationInputs)` in `src/lib/engine.ts`.
Compares staying vs. moving on salary, cost of living, one-time moving
costs, and invested surplus over a horizon.

## Inputs

| Name | Type | Unit | Domain |
|---|---|---|---|
| currentSalary | number | USD/yr (gross) | 30,000–300,000 |
| newSalary | number | USD/yr (gross) | 30,000–300,000 |
| currentMonthlyExpenses | number | USD/mo | 1,000–15,000 |
| newMonthlyExpenses | number | USD/mo | 1,000–15,000 |
| movingCosts | number | USD one-time | 0–50,000 |
| currentSavings | number | USD | 0–500,000 |
| annualReturn | number | decimal/yr | 0.02–0.12 |
| yearsHorizon | integer | years | 1–30 |

## Computation

1. `annualSalaryDelta = newSalary − currentSalary`
2. `annualExpenseDelta = (newMonthlyExpenses − currentMonthlyExpenses) × 12`
3. `annualNetDelta = annualSalaryDelta − annualExpenseDelta`
4. `monthlyNetDelta = annualNetDelta / 12`
5. `breakEvenMonths = ceil(movingCosts / monthlyNetDelta)` if
   `monthlyNetDelta > 0`, else the sentinel **9999** ("never").
   `movingCostRecoveryMonths` is the same value.
6. Path savings (gross, per month, floored at 0):
   `stayMonthlySavings = max(0, currentSalary/12 − currentMonthlyExpenses)`;
   `moveMonthlySavings = max(0, newSalary/12 − newMonthlyExpenses)`.
7. Wealth paths, annual steps, index y = 0..yearsHorizon. Starting
   balances: stay `currentSavings`; move `currentSavings − movingCosts`
   (may go negative and stays negative until savings outweigh it).
   `yearlyData[y] = {year: y, stay, move}` records the balance **before**
   that year's growth; then each balance updates as
   `bal = bal × (1 + annualReturn) + monthlySavings × 12`.
8. `wealthAtHorizonStay/Move = yearlyData[yearsHorizon]` values;
   `netWealthGain = move − stay` at horizon.

## Output keys

`annualSalaryDelta`, `annualExpenseDelta`, `annualNetDelta`,
`movingCostRecoveryMonths`, `breakEvenMonths`, `wealthAtHorizonStay`,
`wealthAtHorizonMove`, `netWealthGain`, `yearlyData` (fields
`{year, stay, move}`). `monthlyNetDelta` is an intermediate, not an
output.

## Assumptions & exclusions (part of the contract)

- Salaries are used **gross**; taxes are the user's job to bake into
  inputs. Break-even uses the net *delta* while the wealth paths use each
  path's own gross surplus — the two outputs answer different questions.
- No salary growth, no expense inflation, annual compounding of monthly
  contributions (contributions earn no intra-year return).
- A negative move balance still compounds at `annualReturn` (symmetric
  growth on negative balances — effectively borrowing at the investment
  rate). Flagged below.

## Known model issues (v1.0.0)

- **Negative-balance compounding** (above): overstates the move path's
  drag when `movingCosts > currentSavings`. Candidate v1.1 fix: floor at 0
  or use a distinct borrowing rate.
- Break-even sentinel is the number 9999, not `Infinity` — callers must
  treat it as "never".

## Verification

Vectors: `oracle/vectors/relocation.json` (seeded fuzz + boundary cases).
Tolerance: relative 1e-9 / absolute 1e-6 per numeric field; `yearlyData`
element-wise.
