# MATH-FIX REPORT — Engine math fixes + FIRE retirement age 40 **Date:** 2026-10-09 · **Commit:** `08398267040a832bdec0910626423f0dbfeb0a7f` (pushed to origin/main, HEAD == origin/main) **Plan:** `/home/ubuntu/projects/.hermes/plans/2026-10-09_engine-math-fixes-fire40.md` — executed end-to-end. | Gate | Result (real output) | |---|---| | `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (baseline 36) | | `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (baseline 48) | | `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (baseline 128) | | `tests/ps-pension.test.js` | `RESULT: 70 pass, 0 fail` (untouched, green) | | `tests/ps-auto-service.test.js` | `RESULT: 21 pass, 0 fail` (untouched, green) | | `scripts/deploy.sh` | `DEPLOY-OK` | | Origin byte-check | 4/4 `BYTE-MATCH` (engine/engine.js, app.js, i18n.js, index.html, sha256 vs live) | | Off-LAN (Midhurst) | `OFFLAN-OK` (`"all_ok": true`, `checked_at 2026-10-09T19:52:48Z`) | | 3-pass evidence rerun | exit 0, 3× `conservation: OK` — full log in `audit/math-fix/passes3-after.log` | No pre-existing test encoded the old behavior: all 36 baseline engine tests stayed green unmodified. New tests extended the suite (36 → 61). --- ## Fix 1 — GIS is payable only at age 65+ **Rule:** GIS is an OAS Supplement for Seniors — Service Canada pays it only once you receive OAS, i.e. from age 65. The engine credited GIS in pre-65 draw years. **Implementation (`site/engine/engine.js`):** - All three single-person `solve()` sites: `var g = a >= 65 ? gis(taxable1 - oasGross, 'single') : 0;` (main ladder, non-registered branch, TFSA branch). - Couple merge: both-draw GIS requires `ra.age >= 65 && rb.age >= 65`; the singles-sum branch applies a per-partner `>= 65` gate; then `if (yr < 65) gisSum = 0;`. - `gis()` header comment cites the stamp + Service Canada source-fact. **Before (live old engine, couple repro 52/52 ON 60k 100k/30k/20k ret 55 spend 93k CPP 60 — `audit/math-fix/before-couple-rows.json`):** | Age | OAS | GIS (phantom) | |---|---|---| | 55 | $0 | **$21K** | | 56 | $0 | **$13K** | | 59–64 | $0 | **$25K–$27K** | **After (live, `after-couple-rows.json`):** ages 55–64 → OAS $0, GIS $0 every row; first GIS row is age **65** ($12K). Screenshot pair: `audit/math-fix/before-couple-table.png` / `after-couple-table.png`. ## Fix 2 — OAS clawback only when OAS is payable, capped at OAS received **Rule (plan spec, ETA s.3(1)):** 15% × max(0, taxable − ~91k), *plafonné au OAS payable*. The recovery can never exceed the OAS actually received, and is 0 when no OAS is payable. **Implementation:** every clawback call site is now `oasGross > 0 ? Math.min(clawback(taxable…), oasGross) : 0` (single ladder, non-reg branch, couple split-solver `newClaw`). `clawback()` itself keeps the raw formula with a documented contract comment. **Net OAS (`oasGross − oasClawback`) is now mathematically never negative.** - Old single repro row 55: **OASnet −$3,501** (documented in plan) → new: **$0** (no OAS payable, no clawback). - Cap effect: 67y, 150k taxable → raw clawback **11,824** → capped at OAS gross **9,150** → net OAS $0 (was −2,674 uncapped). - Live after: `negative OAS/GIS rows anywhere: 0` (all 40 draw rows, all 11 columns scanned). ## Fix 3 — Couple mixed-phase rows: `†` footnote + bilingual legend **Rule:** in the merged couple table, a row's phase is `draw` from the FIRST retirement; while one partner is still accumulating, the row is *not* in full retirement mode. **Implementation:** - Merge: `row.mixed = row.phase === 'draw' && (ra.phase === 'accum' || rb.phase === 'accum');` - Renderer (`site/app.js`): Total cell gets `' †'` when `r.mixed`; `#yr-mixed-note` (new `