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

Engine: `calcEmergencyFund(inputs: EmergencyFundInputs)` in
`src/lib/engine.ts`. Months to reach a target of N months of expenses in
a high-yield savings account, with a month-by-month balance projection.

## Inputs

| Name | Type | Unit | Domain |
|---|---|---|---|
| monthlyExpenses | number | USD/mo | 500–20,000 |
| targetMonths | integer | months of coverage | 1–12 |
| currentSavings | number | USD | 0–100,000 |
| monthlySavings | number | USD/mo | 50–5,000 |
| savingsAccountRate | number | decimal/yr (0.048 = 4.8% APY) | 0.01–0.08 |

## Computation

Let `r_m = savingsAccountRate / 12` (nominal monthly convention),
`MAX = 240`.

1. `targetAmount = monthlyExpenses × targetMonths`.
2. Simulation, months m = 0..240 **inclusive** (241 iterations), balance
   starting at `currentSavings`, `monthsToGoal` initialized to the
   sentinel 240. Each iteration, in this exact order:
   1. record `projectedBalance[m] = {month: m, balance, target:
      targetAmount}` (`target` repeated in every element);
   2. record `coverageByMonth[m] = {month: m, monthsCovered:
      monthlyExpenses > 0 ? balance / monthlyExpenses : 0}`;
   3. if `balance ≥ targetAmount` **and** `monthsToGoal === 240`, set
      `monthsToGoal = m` (first-hit latch; the balance recorded is the
      **pre-update** balance, so month 0 tests `currentSavings` itself);
   4. update `balance = balance × (1 + r_m) + monthlySavings`.
3. `finalBalance = projectedBalance[monthsToGoal].balance`. (The code
   writes `?? targetAmount` as a fallback, but the array always has
   indices 0–240 and `monthsToGoal ≤ 240`, so the fallback is dead.)
4. `interestEarned = max(0, finalBalance − currentSavings −
   monthlySavings × monthsToGoal)`.
5. `yearsToGoal = monthsToGoal / 12` (float, no rounding).
6. The returned `projectedBalance` and `coverageByMonth` are the first
   `min(monthsToGoal + 1, 61)` elements of the recorded arrays — i.e.
   truncated at month 60 even when the goal is reached later (or never).

No `Math.round` anywhere; no wall-clock dependence.

## Output keys

`targetAmount`, `monthsToGoal`, `yearsToGoal`, `interestEarned`,
`projectedBalance` (fields `{month, balance, target}`), `coverageByMonth`
(fields `{month, monthsCovered}`). `finalBalance` is an intermediate, not
an output.

## Assumptions & exclusions (part of the contract)

- Deposits land end-of-month, after interest; the goal check sees the
  start-of-month balance, so a deposit that crosses the target counts in
  the **following** month.
- No inflation on the expense target; the target is fixed at t = 0.
- If `currentSavings ≥ targetAmount`, `monthsToGoal = 0` and
  `interestEarned = 0`.

## Known model issues (v1.0.0)

- **Sentinel collision:** `monthsToGoal = 240` means *either* "goal first
  reached at exactly month 240" *or* "never reached within 240 months" —
  the two are indistinguishable. The UI treats `≥ 240` as "Never".
  Correspondingly `yearsToGoal = 20` is the never-sentinel in years.
- When the goal is never reached, `interestEarned` is still computed —
  over the full 240-month window, not "to goal" as the name implies.
- Chart arrays are hard-truncated at 61 elements (months 0–60): for slow
  savers the plotted series ends well short of the goal/sentinel.
- `target` is duplicated into every `projectedBalance` element.
- The `monthlyExpenses > 0` guard on coverage returns 0 (not
  Infinity/NaN) at zero expenses; note `targetAmount` is then also 0, so
  the goal is met at month 0. Out of domain, but defined behavior.

## Verification

Vectors: `oracle/vectors/emergency-fund.json` (seeded fuzz + boundary
cases). Tolerance: rel 1e-9 / abs 1e-6 per numeric field;
`projectedBalance` and `coverageByMonth` element-wise.

Suggested curated edges:

- Already funded: `{monthlyExpenses: 500, targetMonths: 1,
  currentSavings: 100000, monthlySavings: 50, savingsAccountRate: 0.01}`
  → `monthsToGoal = 0`, single-element arrays, `interestEarned = 0`.
- Never-sentinel: `{monthlyExpenses: 20000, targetMonths: 12,
  currentSavings: 0, monthlySavings: 50, savingsAccountRate: 0.01}` →
  `monthsToGoal = 240`, arrays truncated at 61 elements.
- Truncation straddle: inputs tuned so the goal lands between months 61
  and 239 (e.g. `{monthlyExpenses: 4500, targetMonths: 6,
  currentSavings: 0, monthlySavings: 300, savingsAccountRate: 0.04}`) —
  exercises `monthsToGoal > 60` with 61-element arrays.
