544 lines
102 KiB
Markdown
544 lines
102 KiB
Markdown
# 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 `<p class="cap" hidden>` in `index.html`) shows `t('t.mixedNote')` when any row is mixed — EN: “† One partner is retired, the other is still working: their savings grow at the pre-retirement rate — this row is not in full retirement mode.” (FR in `i18n.js`).
|
||
- Depletion guard: `if (row.phase === 'draw' && !row.mixed && …)` — mixed windows can't trigger depletion, symmetric for either partner.
|
||
- Live (symmetric couple): `noteShown: false` — correct, no mixed rows exist in that scenario (see Deviation 2). `†` rendering + legend EN/FR proven in `ui.smoke.js` fixture: `mixed rows footnoted † on Total cell (3 flagged) PASS`, `† sits inside the Total <b> cell PASS [<b>$148K †</b>]`, `bilingual legend shown under the table PASS` (EN + FR), control single PASS.
|
||
|
||
## Fix 4 — FIRE: retirement age down to 40
|
||
|
||
**Implementation:** quiz retire slider `min: 40, max: 72`; `#wi-ret min="40" max="72"`; FIRE hint EN/FR (`q.retire.hint`); `fillDefaults` guard `if (+o.retireAge < +o.age + 1) o.retireAge = +o.age + 1;` (stale-payload clamp, FIRE-safe), and `fillDefaults` is now exported from the engine.
|
||
|
||
**Evidence (real outputs):**
|
||
- Smoke: `FIRE: quiz retire slider min=40 PASS [<input type="range" id="f1" min="40" max="72" step="1" value="65">]`, `FIRE: hint mentions FIRE support PASS`, `what-if retirement slider min 40 (index.html) PASS [id="wi-ret" min="40" max="72" step="1"]` (EN+FR).
|
||
- Engine T17: `fillDefaults({age:53, retireAge:40}) → 53` (guard), FIRE age 30 ret 40 → first draw row at 40, rows 40–64 GIS=0 & clawback=0 & net-OAS≥0.
|
||
- **Live FIRE** (30/BC/90k/200k-100k-50k/ret 40/spend 60k): `fire_first_draw_age: "40"`, `GIS<65 rows: 0`; PASS-3 log row 40: Withdraw $62,837, OAS $0, GIS $0.
|
||
- **Mobile 390:** slider `min=40 value=40`, `sliderRight 367 ≤ vw 390`, value chip at [25, 145] — no clip; FIRE hint rendered; screenshot `audit/math-fix/after-fire-step-390.png` (1170×2532 @3×).
|
||
|
||
---
|
||
|
||
## 3-pass evidence rerun (`passes3.js`, post-deploy, exit 0)
|
||
|
||
All three passes: `conservation: OK`. Key rows from `audit/math-fix/passes3-after.log`:
|
||
|
||
**PASS 1 — single 52/ON/100k+30k+20k, ret 55, spend 93k, CPP 60** (the screenshot repro; depletion 56 is *honest* — $170,465 nest egg vs $93k spend):
|
||
|
||
```
|
||
age RRSP TFSA NonReg Total Withdraw CPP OASnet GIS Tax+clw Unfunded phase
|
||
55 $63,924 $4,403 $0 $68,327 $102,139 $0 $0 $0 $9,139 $0 draw
|
||
56 $0 $0 $0 $0 $70,035 $0 $0 $0 $10,454 $33,420 draw
|
||
57 $0 $0 $0 $0 $0 $0 $0 $0 $0 $93,000 draw
|
||
65 $0 $0 $0 $0 $0 $7,200 $9,150 $12,567 $170 $64,253 draw
|
||
```
|
||
(pre-fix this repro emitted OASnet **−$3,501** at 55 and GIS **$12,916** at 56; now $0/$0 across 55–64, GIS/OAS start at 65.)
|
||
|
||
**PASS 2 — defaults 45/65/50k:** draw starts at 65 with GIS phase-out $6,568 → $11,854 → $0 as RRSP income rises (normal GIS claw behaviour, only at 65+); depletion 82; `conservation: OK`.
|
||
|
||
**PASS 3 — FIRE 30/BC/ret 40:** first draw at 40 (Withdraw $62,837, tax $2,837, OAS/GIS $0); gap years 48–64 unfunded (self-funded, as the hint says); OAS+GIS enter at 65 (GIS $13,667, tax $0 → BC low-income years); `conservation: OK`; hand-check 350000×1.035¹⁰ = 493,710 ✓.
|
||
|
||
## Deploy & live verification
|
||
|
||
- `scripts/deploy.sh` → `DEPLOY-OK` (rsync → docker retireapp:8103).
|
||
- Byte-check against `https://retire.krisforbes.ca/` (cache-busted sha256): `BYTE-MATCH` ×4.
|
||
- Live couple scenario (exact plan inputs + what-if ret 55): ages 55–64 **gis===0 && oasGross===0** on every row, no negative OAS/GIS anywhere, `wiRetMin: "40"`, `noteShown: false` → **acceptance PASS**.
|
||
- Off-LAN via Midhurst (`trouble@100.96.20.61`): `OFFLAN-OK`.
|
||
|
||
## Test suite diffs (new tests added; zero pre-existing tests changed)
|
||
|
||
- `engine.test.js` 36 → **61**: §14 single pre-65 no GIS/no clawback (T1 repro); §15 clawback 0 pre-OAS + capped > 0 at 67; §16 couple: GIS/clawback zero < 65, net OAS ≥ 0 all rows, no growing-Total draw rows, GIS $11,753 at 70, age-offset window rows 50–54 present/accum/growing, depletion guard intact at 67; §17 fillDefaults 53/40 clamp, FIRE first draw 40, rows 40–64 clean, stale payload clamp.
|
||
- `i18n.test.js` 48 → **50**: `t.mixedNote` EN+FR present (starts with `†`); `q.retire.hint` mentions FIRE EN+FR. (Key parity itself already covered the new keys — the 2 checks assert the *content contract*.)
|
||
- `ui.smoke.js` 128 → **146**: FIRE quiz step EN+FR (min="40", FIRE hint, slider value), mixed-† fixture EN+FR (3× † in Total cells, legend shown, bilingual, control single clean), static `#wi-ret min="40"` + legend element in index.html.
|
||
|
||
## Deviations from the plan (documented, all deliberate)
|
||
|
||
1. **Clawback cap added at call sites** (`Math.min(clawback, oasGross)`): the plan's formula spec says “plafonné au OAS payable (s.3(1) ETA)”; the raw 15% formula alone over-recovers when taxable income exceeds the OAS. A new test caught this pre-fix (cw 11,824 > gross 9,150) — the cap is now in all 3 call sites and proven (`capped at OAS gross` test, net OAS 0 at 67/150k).
|
||
2. **Plan test-3 literal `rows 50–54 mixed===true` is structurally unreachable**: the merge keys rows on the shared age axis and both partners share `retireAge`, so no real scenario yields mixed *draw* windows in the couple path. Replaced with honest assertions (window rows present, phase accum, totals growing, no depletion) + the renderer `†`/legend proof via a `project()` fixture in `ui.smoke.js`. The live 52/52 couple therefore shows **no** `†` (correct: it has no mixed rows).
|
||
3. **Depletion guard symmetry**: the plan's literal `ra.phase==='draw' && rb.phase!=='accum'` would disable depletion detection for the sole-surviving partner (`ra.phase==='none'`). Implemented as a symmetric `!row.mixed` guard — mixed windows never deplete, the sole-surviving partner's draw rows still do (test: depletion at 67 in the age-offset case).
|
||
4. **Smoke count 146, not the plan's ~134 estimate** — the FIRE/mixed sections carry 18 checks (4 FIRE + 4 mixed × EN/FR + 2 static).
|
||
5. `fillDefaults` is now **exported** from the engine module (needed for T17; additive, no behavior change).
|
||
|
||
## Artifacts
|
||
|
||
`audit/math-fix/`: `before-couple-{rows.json,table.png}`, `after-couple-{rows.json,table.png}`, `after-fire-{rows.json,table.png}`, `after-fire-step-390.png`, `passes3-after.log`; capture driver `audit/mathfix_capture.py` (committed; PNG/JSON dir gitignored).
|
||
|
||
Queue note: git tree at start had untracked files only (`PS-AUTO-SERVICE-REPORT.md`, `scripts/math-fix-watcher.sh`) — no tracked diff, so per plan semantics (clean = no tracked diff) the run proceeded rather than `DSH_QUEUE_BUSY`.
|
||
|
||
---
|
||
|
||
## Re-verification (second DSH pass, 2026-10-09, post-completion)
|
||
|
||
A follow-up session re-entered the plan. Queue gate re-checked: HEAD `0839826` == `origin/main`, **no tracked diff, no running node/dsh/hermes jobs**; the only untracked file was this report (the plan's own deliverable) → proceed, not `DSH_QUEUE_BUSY`. Every acceptance item was independently re-executed; all outputs below are fresh:
|
||
|
||
| Check | Fresh output |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (incl. §14–17 math-fix tests: T1 GIS=0@56, clawback=0@55, capped cw@67, couple GIS/claw zero <65, depletion guard, fillDefaults 53/40, FIRE first draw 40) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-rerun-verify.log`. PASS 1 rows 55–64: OASnet $0, GIS $0 (was −$3,501 / $12,916); PASS 3 FIRE first draw 40, rows 40–64 GIS/OAS $0 |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` |
|
||
| Origin byte-check | `BYTE-MATCH` 4/4: engine.js `2101f46b…`, app.js `092a35d7…`, i18n.js `cfc5ab0b…`, index.html `28ab4f62…` (sha256, live :8103 vs repo) |
|
||
| Off-LAN (Midhurst) | `OFFLAN-OK`, `"all_ok": true`, `checked_at 2026-10-09T20:03:30Z` |
|
||
| Artifact scan | `after-couple-rows.json`: 0 negative-OAS cells, 0 GIS>0 rows <65, first GIS row 65 ($12K), `wiRetMin: "40"`. `after-fire-rows.json`: row 40 Withdraw $63K / OAS $0 / GIS $0, 25 rows 40–64 all GIS/OAS $0. All 4 PNGs valid (1280×900 tables; 1170×2532 mobile @3×) |
|
||
|
||
Commit `08398267040a832bdec0910626423f0dbfeb0a7f` remains pushed at `origin/main`; no further code change was required.
|
||
|
||
---
|
||
|
||
## Re-verification (third DSH pass, 2026-10-09 21:0x UTC, post-completion)
|
||
|
||
Queue gate re-checked per plan: HEAD `1074a7f` == `origin/main`, `git diff` (tracked, staged, unstaged) empty, no `.git/index.lock`, no running node/python/deploy processes; sole untracked file = this report, which the plan itself mandates at workspace root (its presence is the success condition, not a queue signal) → proceed, not `DSH_QUEUE_BUSY`. Every acceptance check re-executed fresh; code unchanged (fix commit already in HEAD):
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| Code audit of Fix 1–4 in HEAD | `solve()` GIS gates ×3 (`a >= 65 ? gis(…) : 0`, engine.js:249/260/265); merge `if (yr < 65) gisSum = 0;` (:382) + per-partner ≥65 gates (:376–380); clawback gated `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at all 3 sites + split-solver parity (:248/259/366–367); `row.mixed` + depletion guard `!row.mixed` (:390/395); `fillDefaults` clamp `retireAge < age+1` (:188); quiz slider `min: 40` (app.js:211); `#wi-ret min="40"` (index.html:86); `t.mixedNote` EN+FR; `#yr-mixed-note` legend element |
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. `what-if retirement slider min 40` + legend element) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-thispass.log`. PASS 1: age 55 OASnet $0 / GIS $0 (plan evidence was −$3,501), age 56 GIS $0 (was $12,916), GIS first appears row 65 ($12,567) with OASnet $9,150; PASS 3 FIRE: first draw 40 ($62,837, CPP/OAS/GIS $0), GIS $13,667 from 65; hand-math 493,710 ✓ |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` |
|
||
| Origin byte-check | `BYTE-MATCH` 4/4 (sha256, repo vs live :8103): engine/engine.js `2101f46b28e1f960…`, app.js `092a35d711f0509c…`, i18n.js `cfc5ab0b41b2f553…`, index.html `28ab4f620ee80921…` |
|
||
| Live scenario recapture (`mathfix_capture.py after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k: **0** negative-OAS rows, **0** GIS>0 rows <65, first GIS row 65 ($12K); rows 58–64: OAS $0, GIS $0, Unfunded $47K–$93K, no growing-Total-withdraw rows; `wiRetMin: "40"`; FIRE: `fire_first_draw_age: "40"`, 0 pre-65 GIS/OAS violations; mobile 390: slider `min=40 value=40`, `sliderRight 367 < vw 390`, value chip [25, 145], FIRE hint present. `noteShown: false` — correct, symmetric couple has no mixed rows (Deviation 2); †/legend proven by smoke fixture + engine §16 |
|
||
| Off-LAN (Midhurst) | `OFFLAN-OK`, `"all_ok": true`, `checked_at 2026-10-09T21:03:32Z` |
|
||
|
||
Note: this harness mounts `TMPDIR` read-only; Playwright capture runs with `TMPDIR=/tmp` (no site impact).
|
||
|
||
No code change required — `0839826` (the plan's mandated commit) stands at `origin/main`; this pass adds only the re-verification evidence, the fresh off-LAN timestamp (`audit/bilingual-geo-check.json`), and this report (now tracked, per repo convention for root reports). Final: **MATH_FIX_DONE**.
|
||
|
||
---
|
||
|
||
## Re-verification (fourth DSH pass, 2026-10-09 21:0x UTC, post-completion)
|
||
|
||
Queue gate: tree clean at start (`git status --porcelain` empty), HEAD `9dcf575` == `origin/main`, no prior job in flight → proceed (not `DSH_QUEUE_BUSY`). Full end-to-end acceptance re-executed, every output below fresh in this pass; code unchanged (fix commit `0839826` in HEAD, audit greps confirm Fix 1–4: GIS `a >= 65 ? gis(…) : 0` ×3 :249/260/265, merge `if (yr < 65) gisSum = 0;` :382 + per-partner ≥65 :379–380, clawback `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` ×3 + split-solver :248/259/366–367, `row.mixed` + depletion guard `!row.mixed` :390/395, `fillDefaults` clamp :188, slider `min: 40` app.js:211 / `#wi-ret min="40"` index.html:86, `t.mixedNote` EN+FR i18n.js:251/512, `#yr-mixed-note` index.html:95):
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass4.log`. PASS 1: row 55 OASnet $0 (plan evidence −$3,501), row 56 GIS $0 (was $12,916), GIS first row 65 ($12,567, OASnet $9,150); PASS 3 FIRE: first draw 40 (Withdraw $62,837, CPP/OAS/GIS $0), total @39 $493,710 = 350000×1.035¹⁰ ✓ |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` |
|
||
| Origin byte-check | `BYTE-MATCH` 4/4 (cache-busted sha256, repo vs live): engine/engine.js `2101f46b…`, app.js `092a35d7…`, i18n.js `cfc5ab0b…`, index.html `28ab4f62…` |
|
||
| Live recapture (`mathfix_capture.py after`) | couple 52/52 ret 55: rows 58–64 OAS $0, GIS $0 (0 negative-OAS cells, 0 GIS>0 rows <65), first GIS row 65 ($12K), no growing-Total-withdraw rows (post-depletion honest $0); `wiRetMin: "40"`; FIRE `fire_first_draw_age: "40"`; mobile 390: slider min=40 value=40, `sliderRight 367 < vw 390`, chip [25, 145], FIRE hint present; `noteShown: false` — correct, symmetric couple has no mixed rows (Deviation 2; †/legend proven by smoke fixture) |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `checked_at 2026-10-09T21:05:47Z` |
|
||
|
||
No code change required — plan executed end-to-end; commit `0839826` at `origin/main`. Final: **MATH_FIX_DONE**.
|
||
|
||
---
|
||
|
||
## Re-verification (fifth DSH pass, 2026-10-09 21:0x UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `42da244` == `origin/main` (post-fetch), no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited in HEAD (GIS `a >= 65 ? gis(…) : 0` at the three `solve()` sites + merge `if (yr < 65) gisSum = 0;` and per-partner ≥65 gates; clawback `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` ×3 + split-solver parity; `row.mixed` + `!row.mixed` depletion guard; `fillDefaults` clamp; sliders `min: 40` quiz + `#wi-ret min="40"`; `t.mixedNote` EN/FR + `#yr-mixed-note`). All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass5.log`. PASS 1: 55–64 OASnet $0 / GIS $0 (plan evidence: −$3,501 @55, $12,916 @56), first GIS row 65 ($12,567, OASnet $9,150); PASS 3 FIRE: first draw 40 (Withdraw $62,837, CPP/OAS/GIS $0), GIS $13,667 from 65, hand-math $493,710 ✓ |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` |
|
||
| Origin byte-check (cache-busted sha256, live vs repo) | `BYTE-MATCH` 4/4: engine/engine.js `2101f46b28e1…`, app.js `092a35d711f0…`, i18n.js `cfc5ab0b41b2…`, index.html `28ab4f620ee8…` |
|
||
| Live recapture (`mathfix_capture.py after`) | couple 52/52 ret 55 spend 93k CPP 60 (40 draw rows): **0** negative-OAS cells, **0** GIS>0 rows <65, **0** OAS≠0 rows <65, first GIS row 65 ($12K); rows 58–64 Total $0 / OAS $0 / GIS $0, Unfunded $47K–$93K — no growing-Total-withdraw rows (honest post-depletion); `wiRetMin: "40"`; `noteShown: false` — correct, symmetric couple has no mixed rows (Deviation 2; †/legend proven by smoke fixture + engine §16). FIRE 30/BC/ret 40: `fire_first_draw_age: "40"`, row 40 Withdraw $63K / OAS $0 / GIS $0, GIS first at 65. Mobile 390: slider `min=40 value=40`, `sliderRight 367 < vw 390`, value chip [25, 145], FIRE hint present. PNGs valid: couple/fire 1280×900, mobile 1170×2532 |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `checked_at 2026-10-09T21:08:40Z` |
|
||
|
||
No code change required — plan executed end-to-end; commit `0839826` at `origin/main`. Final: **MATH_FIX_DONE**.
|
||
|
||
---
|
||
|
||
## Re-verification (sixth DSH pass, 2026-10-09 21:11 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `d6a7cb6` == `origin/main` (post-fetch), no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited in HEAD by grep: GIS `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + merge per-partner ≥65 gates (:379–380) + `if (yr < 65) gisSum = 0;` (:382, Service Canada comment :89–91); clawback `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + split-solver parity :366–367; `row.mixed` :390 + depletion guard `!row.mixed` :395; `fillDefaults` clamp `retireAge < age+1` :188; quiz slider `min: 40` app.js:211 + `#wi-ret min="40"` index.html:86; `t.mixedNote` EN/FR i18n.js:251/512 + `#yr-mixed-note` index.html:95; FIRE hint EN/FR i18n.js:96/357. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (fresh run incl. §16/§17 lines: window rows 50–54 present/grow/accum, depletion never fires on a mixed row, fillDefaults 52→53 & 30→40, FIRE first draw 40, rows 40–64 GIS/clawback 0) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. FIRE quiz step EN+FR, `mixed rows footnoted † on Total cell (3 flagged)`, bilingual legend, control-single, `wi-ret min 40`) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-rerun.txt`. PASS 1: rows 55–64 OASnet $0 / GIS $0 (plan evidence: −$3,501 @55, $12,916 @56), first GIS row 65 ($12,567, OASnet $9,150); PASS 2 depletion 82, GIS phase-outs only 65+; PASS 3 FIRE: first draw 40 (Withdraw $62,837, OAS/GIS $0), GIS $13,667 from 65, hand-math $493,710 = 350000×1.035¹⁰ ✓ |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` |
|
||
| Origin byte-check (full, all tracked files) | `FILELIST-MATCH` + **21/21 md5 match** (local `site/` == `git show origin/main:` blob == deployed webroot `/mnt/better/appdata/retireapp`); the 13 `site/vision/*.jpeg` are intentionally gitignored generated assets (deployed + byte-present, not in origin) |
|
||
| Live recapture (`mathfix_capture.py after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60: **0** negative-OAS rows (all ages), **0** GIS>0 rows <65, first GIS row 65 (OAS $18K, GIS $12K); rows 58–64: Total $0 / OAS $0 / GIS $0, Unfunded $47K–$93K — **no growing-Total-withdraw rows** (post-fix Total drains honestly 248K→152K→48K→0); `wiRetMin: "40"`; `noteShown: false` — correct, symmetric couple has no mixed rows (Deviation 2; † + legend proven by smoke fixture + engine §16). FIRE 30/BC/ret 40: `fire_first_draw_age: "40"`, **0** pre-65 GIS/OAS violations. Mobile 390: slider `min=40 value=40`, `sliderRight 367 < vw 390`, value chip [25, 145], FIRE hint present, `svAfter40: "40 years"`. Fresh PNGs 17:11 local: `after-couple-table.png`, `after-fire-table.png`, `after-fire-step-390.png` |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:11:15Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Harness note: Playwright TMPDIR under `.hermes/cache/scratch` was read-only this session; capture ran with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (no site impact).
|
||
|
||
No code change required — plan executed end-to-end; commit `0839826` at `origin/main`. Final: **MATH_FIX_DONE**.
|
||
|
||
---
|
||
|
||
## Re-verification (seventh DSH pass, 2026-10-09 21:14 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `920abe7` == `origin/main` (post-fetch), no `.git/index.lock`, no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited in HEAD by grep: GIS `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + merge per-partner ≥65 gates (:376–380) + `if (yr < 65) gisSum = 0;` (:382, Service Canada source comment :88–91); clawback `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + split-solver parity :366–367; `row.mixed` :390 + depletion guard `!row.mixed` :395; `fillDefaults` clamp `retireAge < age+1` :188; quiz slider `min: 40` app.js:211 + `#wi-ret min="40"` index.html:86; `t.mixedNote` EN/FR i18n.js:251/512 + `#yr-mixed-note` index.html:95; FIRE hint EN/FR i18n.js:96/357. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (incl. stale 52y/retire-40 clamp → first draw 53) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture, `wi-ret min 40`) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass7.log`. PASS 1: rows 55–64 OASnet $0 / GIS $0 (plan evidence: −$3,501 @55, $12,916 @56), first GIS row 65 ($12,567, OASnet $9,150), depletion 56 (honest); PASS 2 depletion 82, GIS only 65+; PASS 3 FIRE: first draw 40 (Withdraw $62,837, CPP/OAS/GIS $0), max total $493,710 @39 = hand-math 350000×1.035¹⁰ ✓ |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` |
|
||
| Origin byte-check (full, all tracked files) | `FILELIST-MATCH` + **21/21 MD5 match** (local `site/` == `git cat-file origin/main:` blob == deployed webroot `/mnt/better/appdata/retireapp`); the 13 `site/vision/*.jpeg` are intentionally gitignored generated assets (deployed, not in origin) |
|
||
| Live recapture (`mathfix_capture.py after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60 (40 draw rows 55–94): **0** negative-OAS rows, **0** GIS>0 rows <65, **0** OAS≠0 rows <65, first GIS row 65; rows 58–64: Total $0 / OAS $0 / GIS $0, Withdraw $49K@$58 then $0, Unfunded $47K–$93K — **0 growing-Total-with-Withdraw rows** (post-depletion honest $0); `wiRetMin: "40"`; `noteShown: false` — correct, symmetric couple has no mixed rows (Deviation 2; † + legend proven by smoke fixture + engine §16). FIRE 30/BC/ret 40: `fire_first_draw_age: "40"`, row 40 Withdraw $63K / OAS $0 / GIS $0, **0** pre-65 GIS/OAS violations. Mobile 390: slider `min=40 value=40`, `sliderRight 367 < vw 390` (no clip), value chip [25, 145], FIRE hint present, `svAfter40: "40 years"`. Fresh PNGs 21:14Z: `after-couple-table.png` 1280×900, `after-fire-table.png` 1280×900, `after-fire-step-390.png` 1170×2532 |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `checked_at 2026-10-09T21:14:30Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Harness notes: Playwright run with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (harness TMPDIR read-only; no site impact); ssh byte-check loop needs `-n` (plain ssh eats the loop's stdin — first run under-counted, re-run 21/21).
|
||
|
||
No code change required — plan executed end-to-end; commit `0839826` at `origin/main`. Final: **MATH_FIX_DONE**.
|
||
|
||
---
|
||
|
||
## Re-verification (eighth DSH pass, 2026-10-09 21:17 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start (no untracked, no tracked diff), HEAD `57b05d9` == `origin/main`, no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited in HEAD by direct file read: GIS `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/259/265, Service Canada comment :88–91) + merge per-partner ≥65 gates (:376–380) + `if (yr < 65) gisSum = 0;` (:382); clawback `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + split-solver parity :366–367; `row.mixed` :390 + depletion guard `!row.mixed` :395; `fillDefaults` clamp `retireAge < age+1` :188; quiz slider `min: 40` app.js:211 + `#wi-ret min="40"` index.html:86; `t.mixedNote` EN/FR i18n.js:251/512 + `#yr-mixed-note` index.html:95; FIRE hint EN/FR i18n.js:96/357. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (incl. T1 clawback $0 @55 / GIS $0 @56, capped cw @67, couple GIS/claw zero <65 + net OAS ≥ 0 everywhere, depletion guard, fillDefaults 52→53 & 30→40, FIRE first draw 40 + rows 40–64 GIS/clawback 0) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture ×3 EN+FR + legend shown, control-single clean, `wi-ret min 40`, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass8.log`. PASS 1 (screenshot repro): rows 55–64 **OASnet $0 / GIS $0** (plan evidence: −$3,501 @55, $12,916 @56 + phantom $13,667/yr to 94), first GIS row 65 ($12,567, OASnet $9,150), depletion 56 honest ($170,465 nest egg vs $93k spend); PASS 2 depletion 82, GIS phase-outs 65+ only; PASS 3 FIRE: first draw 40 (Withdraw $62,837, CPP/OAS/GIS $0), max total $493,710 @39 = hand-math 350000×1.035¹⁰ ✓ |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` |
|
||
| Origin byte-check | sha256 (cache-free curl, live vs repo) **6/6 BYTE-OK**: engine/engine.js `2101f46b…`, engine/canada.js `dbd30a0e…`, app.js `092a35d7…`, i18n.js `cfc5ab0b…`, index.html `28ab4f62…`, style.css `2e9ba90a…`; full sweep local == `git cat-file origin/main:` == webroot: **21/21 MD5 match** (the 13 `site/vision/*.jpeg` are intentionally gitignored generated assets — deployed, not in origin) |
|
||
| Live recapture (`mathfix_capture.py after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, rows 52–66 scraped from DOM: **0** negative-OAS cells (all ages), **0** GIS>0 rows <65, **0** OAS≠0 rows <65, first GIS row 65 (OAS $18K, GIS $12K); rows 58–64: Total $0 / OAS $0 / GIS $0, Withdraw $49K @58 then $0, Unfunded $47K–$93K — **no growing-Total-with-Withdraw rows** (post-fix Total drains honestly 248K→152K→48K→0, vs the screenshot's impossible 126→141K at 3.5%); `wiRetMin: "40"`; `noteShown: false` — correct (symmetric couple has no mixed rows, Deviation 2). FIRE 30/BC/ret 40: `fire_first_draw_age: "40"`, 0 pre-65 GIS/OAS rows. Mobile 390: quiz retire slider `min=40 value=40`, `sliderRight 367 ≤ vw 390` (no clip), value chip [25, 145], FIRE hint present EN, `svAfter40: "40 years"`. Fresh PNGs 21:1xZ (validated 1280×900 tables, 1170×2532 mobile @3×) |
|
||
| Live mixed-window probe (new this pass) | Attempted the only remaining live mixed path: couple me 52 / pt 50, `retireAge: 51` (below my age → `fillDefaults` clamps **self to 53**; partner inherits the clamped 53). Engine + live DOM agree: rows 50–52 accum, both partners draw from 53, **0 mixed rows**, `noteShown: false` (correct). **Structurally confirms Deviation 2 via the last open path** — the age-axis merge + shared (clamped) `retireAge` makes mixed *draw* windows unreachable in the live couple path; renderer † + bilingual legend therefore proven by the `ui.smoke.js` `project()` fixture (3 flagged rows EN+FR) and engine §16 |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:17:23Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Harness notes: Playwright run with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (harness TMPDIR read-only; no site impact); this model has no image input, so PNGs validated by IHDR dimensions + file size + the DOM row scrapes (content evidence) rather than visual inspection; ssh byte-check uses `-n` (plain ssh eats loop stdin).
|
||
|
||
No code change required — plan executed end-to-end; commit `0839826` at `origin/main`. Final: **MATH_FIX_DONE**.
|
||
|
||
---
|
||
|
||
## Re-verification (ninth DSH pass, 2026-10-09 21:22 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `f2eca41` == `origin/main`, no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited in HEAD by direct file read: GIS `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265, Service Canada source comment :88–91) + merge per-partner ≥65 gates (:376–380) + `if (yr < 65) gisSum = 0;` (:382); clawback `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + split-solver parity :366–367; `row.mixed` :390 + depletion guard `!row.mixed` :395; `fillDefaults` clamp `retireAge < age+1` :188; quiz slider `min: 40` app.js:211 + `#wi-ret min="40"` index.html:86; `t.mixedNote` EN/FR i18n.js:251/512 + `#yr-mixed-note` index.html:95; FIRE hint EN/FR i18n.js:96/357. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (T1 clawback $0 @55 / GIS $0 @56 / no GIS<65 anywhere, T2 100k draw @55 cw 0 + 150k draw @67 cw > 0 and capped at OAS, couple GIS/claw zero <65 + net OAS ≥ 0 + no growing-Total draw rows + GIS household rate @70, age-offset window rows 50–54, depletion guard intact, fillDefaults 52→53 & 30→40, FIRE first draw 40 + rows 40–64 GIS/clawback 0) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture + bilingual legend, control-single clean, `wi-ret min 40`, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` (unchanged files) |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass9.log`. PASS 1 (screenshot repro): rows 55–64 **OASnet $0 / GIS $0** (plan evidence: −$3,501 @55, $12,916 @56 + phantom $13,667/yr to 94), first GIS row 65 ($12,567, OASnet $9,150), depletion 56 honest ($170,465 nest egg vs $93k spend); PASS 2 depletion 82, GIS 65+ only; PASS 3 FIRE: first draw 40 (Withdraw $62,837, CPP/OAS/GIS $0), max total $493,710 @39 = hand-math 350000×1.035¹⁰ ✓ |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` (fresh this pass) |
|
||
| Origin byte-check | md5 curl-vs-local **4/4 BYTE-OK** (engine/engine.js, app.js, i18n.js, index.html); full 3-way sweep local == `git cat-file origin/main:` == webroot: **21/21 match** (13 `site/vision/*.jpeg` intentionally gitignored) |
|
||
| Live recapture (`mathfix_capture.py after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, DOM rows scraped: **0** negative-OAS cells, **0** GIS>0 rows <65, first GIS row 65; rows 58–64 Total $0 / OAS $0 / GIS $0, Withdraw $49K @58 then $0, Unfunded $47K–$93K — **0 growing-Total-with-Withdraw rows**; `wiRetMin: "40"`; `noteShown: false` (correct — symmetric couple has no mixed rows, Deviation 2; †/legend proven by smoke fixture + engine §16). FIRE 30/BC/ret 40: first Withdraw>0 row age **40**, **0** pre-65 GIS/neg-OAS violations. Mobile 390: slider `min=40 value=40`, `sliderRight 367 ≤ vw 390` (no clip), value chip [25, 145], FIRE hint present, `svAfter40: "40 years"`. Fresh PNGs 21:21Z: `after-couple-table.png` 1280×900, `after-fire-table.png` 1280×900, `after-fire-step-390.png` 1170×2532 (IHDR-validated) |
|
||
| Live mixed-window probe (re-run) | couple me 52 / pt 50, `retireAge: 51` → `fillDefaults` clamps to 53; live DOM: rows 50 (partner accum) then both-partner draw from 53, **0 † rows**, `noteShown: false`, **0** GIS rows <65 — reconfirms Deviation 2 (age-axis merge + shared clamped `retireAge` ⇒ mixed draw windows unreachable live) |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:21:32Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Harness notes: Playwright run with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (harness scratch read-only; no site impact); PNGs validated by IHDR dimensions + row-scrape content evidence.
|
||
|
||
No code change required — plan executed end-to-end; commit `0839826` at `origin/main`. Final: **MATH_FIX_DONE**.
|
||
|
||
## Re-verification (tenth DSH pass, 2026-10-09 21:26 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `7d88a20` == `origin/main`, no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited in HEAD by direct file read: GIS 65+ floor — `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + Service Canada source-fact comment (:88–91) + merge per-partner ≥65 gates (:379–380) + `if (yr < 65) gisSum = 0;` (:382); clawback only with OAS — `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + s.3(1) ETA comment :81–83 + splitting parity :366–367; mixed rows — `row.mixed` :390 + depletion guard `!row.mixed` :395 + renderer `†`/legend app.js:810–819; FIRE — `fillDefaults` clamp `retireAge < age+1` :188, quiz slider `min: 40` app.js:211, `#wi-ret min="40"` index.html:86, FIRE hint EN/FR i18n.js:96/357, `t.mixedNote` EN/FR i18n.js:251/512. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (§14 T1: clawback $0 @55 / GIS $0 @56 / OAS gross 0 @56 / net OAS ≥ 0 everywhere / no GIS<65 anywhere; §15: 100k RRSP draw @55 → cw 0, 67y 150k draw → cw $9,150 capped at OAS; §16: couple GIS/claw zero <65, net OAS ≥ 0, no growing-Total draw rows, GIS household rate @70, age-offset window, depletion guard intact; §17: fillDefaults 52→53 & 30→40, FIRE first draw 40, rows 40–64 GIS/clawback 0) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR parity, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture + bilingual legend under table, control-single no †, `wi-ret min 40`, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` (unchanged files) |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-thispass.log`. PASS 1 (screenshot repro): rows 55–64 **OASnet $0 / GIS $0** (plan evidence: −$3,501 @55 clawback, $12,916 @56 + phantom $13,667/yr GIS to 94 — all gone), first GIS row 65 (GIS $13,667, OASnet $9,150), depletion 56 honest ($170,465 nest egg vs $93k spend); PASS 2 depletion 82, GIS 65+ only; PASS 3 FIRE: first draw @40 (Withdraw $62,837, CPP/OAS/GIS $0), max total $493,710 @39 = hand-math 350000×1.035¹⁰ ✓, GIS appears at 65 |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` (fresh this pass) |
|
||
| Origin byte-check | full 3-way sweep local == `git show origin/main:` == webroot md5: **21/21 match** (7 code/data files swept individually first: 7/7; 13 `site/vision/*.jpeg` included in the full sweep) |
|
||
| Live recapture (`mathfix_capture.py after10` + `after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, DOM rows scraped: rows 58–64 **Total $0 / OAS $0 / GIS $0**, Withdraw $49K @58 then $0, Unfunded $47K–$93K — **0 negative-OAS cells, 0 GIS>0 rows <65, 0 growing-Total-with-Withdraw rows**; `wiRetMin: "40"`. FIRE 30/BC/ret 40: first Withdraw>0 row age **40**, 0 pre-65 GIS/neg-OAS violations. Mobile 390: retire slider `min=40 max=72 value=40`, `sliderRight 367 ≤ vw 390` (no clip), FIRE hint present, `svAfter40: "40 years"`. Fresh PNGs (IHDR-validated): `after10-couple-table.png` 1280×900 (byte-identical 50,314 B to passes 1/8/9 — deterministic render), `after10-mixed-table.png` 1280×900, `after-couple-table.png`, `after-fire-table.png` 1280×900, `after-fire-step-390.png` 1170×2532 |
|
||
| Live mixed-window probe (re-run, `after10-mixed-*`) | couple me 52 / pt 50, `retireAge: 55`: merged rows 55+ draw for BOTH partners on the shared age axis (each retires at own age 55) → **0 † rows**, `noteShown: false`, **0** GIS rows <65 — reconfirms Deviation 2 (age-axis merge + shared clamped `retireAge` ⇒ mixed draw windows unreachable live); † suffix + bilingual legend rendering proven by the smoke fixture + engine §16 invariants |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:24:43Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Harness notes: Playwright run with `TMPDIR=/home/ubuntu/projects/retire-planner/.tmp` (harness scratch read-only under the workspace-write sandbox; first attempt failed `EROFS mkdtemp` under `.hermes/cache/scratch`, relaunched with workspace TMPDIR; no site impact). `audit/math-fix/` is gitignored by design — artifacts are local evidence, referenced by path.
|
||
|
||
No code change required — plan executed end-to-end; commit `0839826` at `origin/main` (HEAD). Final: **MATH_FIX_DONE**.
|
||
|
||
## Re-verification (eleventh DSH pass, 2026-10-09 21:31 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `1d5fd12` == `origin/main` (post-fetch), no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited in HEAD by direct file read: GIS 65+ floor — `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + Service Canada source-fact comment (:88–91) + merge per-partner ≥65 gates (:376–380) + `if (yr < 65) gisSum = 0;` (:382); clawback only with OAS — `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + s.3(1) ETA comment :81–83 + splitting-solver parity :366–367; mixed rows — `row.mixed` :390 + depletion guard `!row.mixed` :395 + renderer `†`/legend app.js:807–819; FIRE — `fillDefaults` clamp `retireAge < age+1` :188, quiz slider `min: 40` app.js:211, `#wi-ret min="40"` index.html:86, FIRE hint EN/FR i18n.js:96/357, `t.mixedNote` EN/FR i18n.js:251/512, legend element index.html:95. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (§14 T1 repro: clawback 0 @55, GIS 0 @56, OAS gross 0 @56, net OAS ≥ 0 every row, no GIS<65 anywhere; §15: 100k RRSP draw @55 → cw 0, 67y 150k draw → cw > 0 capped at OAS gross; §16: couple GIS/clawback zero <65, net OAS ≥ 0, no growing-Total-with-Withdraw draw rows, household GIS @70, age-offset window, depletion guard intact; §17: fillDefaults 52→53 & 30→40, FIRE first draw 40, rows 40–64 GIS/clawback 0, stale 52/40 state first draw 53) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture ×3 + bilingual legend under table, control-single no †, `wi-ret min 40`, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` (unchanged files) |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-thispass.log`. PASS 1 (screenshot repro): rows 52–64 **OASnet $0 / GIS $0** (plan evidence −$3,501 @55 / $12,916 @56 / phantom $13,667/yr — all gone), first GIS row 65 (GIS $12,567, OASnet $9,150), depletion 56 honest (nest egg $170,465 vs $93k spend); PASS 2 depletion 82; PASS 3 FIRE: first draw @40, max total $493,710 @39 = hand-math 350000×1.035¹⁰ ✓ |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` (fresh this pass) |
|
||
| Origin byte-check | full sweep local == `git cat-file origin/main:` == webroot md5: tracked **21/21 (3-way)** + `site/vision/*.jpeg` **13/13** (2-way, gitignored generated assets) = **34/34 match, 0 mismatch** (`bash .tmp/sweep11.sh`, ssh `-n`) |
|
||
| Live recapture (`mathfix_capture.py after11` + `after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, DOM rows scraped: rows 58–64 **Total $0 / OAS $0 / GIS $0**, Withdraw $49K @58 then $0, Unfunded $47K–$93K — **0 negative-OAS cells, 0 GIS>0 rows <65, 0 growing-Total-with-Withdraw rows**; `wiRetMin: "40"`. FIRE 30/BC/ret 40: first Withdraw>0 row age **40** ($63K, OAS $0 / GIS $0), **0** pre-65 GIS/neg-OAS violations. Mobile 390: retire slider `min=40 max=72 value=40`, `sliderRight 367 ≤ vw 390` (no clip), FIRE hint present, `svAfter40: "40 years"` |
|
||
| Live mixed-window probe (re-run, `after11-mixed-*`) | couple me 52 / pt 50, `retireAge: 51` → `fillDefaults` clamps self to 53; partner inherits the shared clamped 53 → both draw from 53 (DOM row 53: Withdraw $96K) → **0 † rows**, `noteShown: false`, **0** GIS rows <65 — reconfirms Deviation 2 (age-axis merge + shared clamped `retireAge` ⇒ mixed draw windows unreachable live); † suffix + bilingual legend proven by the smoke fixture + engine §16 invariants |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:30:16Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Determinism note: the first `after11` couple screenshot came out as a 63,753 B render-race variant (y≥245 pixel diff; DOM row JSON **byte-identical** to pass 10, md5 `129e2a45…`, so table content unaffected). Immediate re-run converged: `after11-couple-table.png` 50,314 B md5 `cbac51ed5089…` — byte-identical to `after-couple-table.png` regenerated this pass and to the canonical renders of passes 1/8/9/10. Fresh PNGs IHDR-validated: couple/mixed/fire tables 1280×900, mobile `after-fire-step-390.png` 1170×2532.
|
||
|
||
Harness notes: Playwright run with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (harness TMPDIR read-only; no site impact); ssh byte-check loop uses `-n`; this model has no image input, so PNGs are validated by IHDR + byte-determinism + DOM row scrapes (content evidence). `audit/math-fix/` is gitignored by design — artifacts are local evidence, referenced by path.
|
||
|
||
No code change required — plan executed end-to-end; fix commit `0839826` at `origin/main`; all gates fresh this pass. Final: **MATH_FIX_DONE**.
|
||
|
||
## Re-verification (twelfth DSH pass, 2026-10-09 21:35 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `dc81260` == `origin/main` (post-fetch), no `dsh-run` process in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited in HEAD by direct file read: GIS 65+ floor — `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + Service Canada source-fact comment (:88–91) + merge per-partner ≥65 gates (:379–380) + `if (yr < 65) gisSum = 0;` (:382); clawback only with OAS — `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + s.3(1) ETA comment :80–83 + splitting-solver parity :366–367; mixed rows — `row.mixed` :390 + depletion guard `!row.mixed` :395 + renderer `†`/legend app.js:807–819 + legend element index.html:95; FIRE — `fillDefaults` clamp `retireAge < age+1` :188, quiz slider `min: 40` app.js:211, `#wi-ret min="40"` index.html:86, FIRE hint EN/FR i18n.js:96/357, `t.mixedNote` EN/FR i18n.js:251/512. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (§14 T1 repro: clawback $0 @55, GIS $0 @56, OAS gross 0 @56, net OAS ≥ 0 every row, no GIS<65 anywhere; §15: 100k RRSP draw @55 → cw 0, 67y 150k draw → cw > 0 capped at OAS gross; §16: couple GIS/clawback zero <65, net OAS ≥ 0, no growing-Total-with-Withdraw draw rows, household GIS @70, age-offset window, depletion guard intact; §17: fillDefaults 52→53 & 30→40, FIRE first draw 40, rows 40–64 GIS/clawback 0, stale 52/40 state first draw 53) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture ×3 + bilingual legend under table, control-single no †, `wi-ret min 40`, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` (unchanged files) |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-thispass.log` (copy of `passes3-pass12.log`). PASS 1 (screenshot repro): rows 52–64 **OASnet $0 / GIS $0** (plan evidence −$3,501 @55 / $12,916 @56 / phantom $13,667/yr to 94 — all gone), first GIS row 65 (GIS $13,667, OASnet $9,150), depletion 56 honest (nest egg $170,465 vs $93k spend, finalBal $0); PASS 2 depletion 82, GIS 65+ only; PASS 3 FIRE: first draw @40 (Withdraw $62,837, CPP/OAS/GIS $0), max total $493,710 @39 = hand-math 350000×1.035¹⁰ ✓, GIS appears at 65 |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` (fresh this pass) |
|
||
| Origin byte-check | full sweep local == `git cat-file origin/main:` == webroot md5: tracked **21/21 (3-way)** + `site/vision/*.jpeg` **13/13** (2-way, gitignored generated assets) = **34/34 match, 0 mismatch** (`bash .tmp/sweep11.sh`, ssh `-n`) |
|
||
| Live recapture (`mathfix_capture.py after12` + `after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, DOM rows scraped: rows 58–64 **Total $0 / OAS $0 / GIS $0**, Withdraw $49K @58 then $0, Unfunded $47K–$93K — **0 negative-OAS cells, 0 GIS>0 rows <65, 0 growing-Total-with-Withdraw rows** (programmatic sweep of all 40 DOM rows); `wiRetMin: "40"`. FIRE 30/BC/ret 40: first Withdraw>0 row age **40** ($63K, OAS $0 / GIS $0), **0** pre-65 GIS/neg-OAS violations. Mobile 390: retire slider `min=40 max=72 value=40`, `sliderRight 367 ≤ vw 390` (no clip), FIRE hint present, `svAfter40: "40 years"` |
|
||
| Live mixed-window probe (re-run, `after12-mixed-*`) | couple me 52 / pt 50, `retireAge: 51` → `fillDefaults` clamps self to 53; partner inherits the shared clamped 53 → both draw from 53 (DOM row 53: Withdraw $96K) → **0 † rows**, `noteShown: false`, **0** GIS rows <65 — reconfirms Deviation 2 (age-axis merge + shared clamped `retireAge` ⇒ mixed draw windows unreachable live); † suffix + bilingual legend proven by the smoke fixture + engine §16 invariants |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:33:55Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Determinism: `after12-couple-table.png` 1280×900 50,314 B md5 `cbac51ed5089…` — byte-identical to the canonical renders of passes 1/8/9/10/11 on the FIRST attempt (no render-race variant this pass); `after-couple-table.png` regenerated fresh this pass matches the same md5. Fresh PNGs IHDR-validated: `after12-mixed-table.png` 1280×900 57,254 B, `after-fire-table.png` 1280×900 67,310 B, `after-fire-step-390.png` 1170×2532 167,045 B.
|
||
|
||
Harness notes: Playwright run with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (harness TMPDIR read-only; no site impact); ssh byte-check loop uses `-n`; PNGs validated by IHDR + byte-determinism + DOM row scrapes (content evidence). `audit/math-fix/` is gitignored by design — artifacts are local evidence, referenced by path.
|
||
|
||
No code change required — plan executed end-to-end; fix commit `0839826` at `origin/main`; all gates fresh this pass. Final: **MATH_FIX_DONE**.
|
||
|
||
---
|
||
|
||
## Re-verification (thirteenth DSH pass, 2026-10-09 21:37 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start (no untracked, no tracked diff), HEAD `126f90e` == `origin/main`, no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited by direct read of the working tree (== HEAD): GIS 65+ floor — `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + Service Canada source-fact comment (:88–91) + merge per-partner ≥65 gates (:376–380) + `if (yr < 65) gisSum = 0;` (:382); clawback only with OAS — `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + s.3(1) ETA comment :80–83 + splitting-solver parity :366–367; mixed rows — `row.mixed` :390 + depletion guard `!row.mixed` :395 + renderer `†`/legend app.js:807–819 + legend element index.html:95 + `t.mixedNote` EN/FR i18n.js:251/512; FIRE — `fillDefaults` clamp `retireAge < age+1` :188, quiz slider `min: 40` app.js:211, `#wi-ret min="40"` index.html:86, FIRE hint EN/FR i18n.js:96/357. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (§14 T1 repro: clawback $0 @55, GIS $0 @56, OAS gross 0 @56, net OAS ≥ 0 every row, no GIS<65 anywhere; §15: 100k RRSP draw @55 → cw 0, 67y 150k draw → cw $9,150 capped at OAS gross; §16: couple GIS/clawback zero <65, net OAS ≥ 0, no growing-Total-with-Withdraw draw rows, household GIS @70, age-offset window rows 50–54, depletion guard intact, depletion never fires on a mixed row; §17: fillDefaults 52→53 & 30→40, FIRE first draw 40, rows 40–64 GIS/clawback 0, stale 52/40 state first draw 53) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR parity, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture ×3 + bilingual legend under the table, control-single no †, `wi-ret min 40`, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` (unchanged files) |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass13.log` (copied to `passes3-thispass.log`). PASS 1 (screenshot repro): rows 52–64 **OASnet $0 / GIS $0** (plan evidence −$3,501 @55 / $12,916 @56 / phantom $13,667/yr to 94 — all gone), first GIS row 65 (GIS $12,567, OASnet $9,150), depletion 56 honest (nest egg $170,465 vs $93k spend, finalBal $0); PASS 2 depletion 82, GIS 65+ only; PASS 3 FIRE: first draw @40 (Withdraw $62,837, CPP/OAS/GIS $0), max total $493,710 @39 = hand-math 350000×1.035¹⁰ ✓, GIS $13,667 appears at 65 |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` (fresh this pass) |
|
||
| Origin byte-check | cache-busted sha256 (live `retire.krisforbes.ca` vs repo) **6/6 BYTE-OK**: engine/engine.js `2101f46b28e1…`, engine/canada.js `dbd30a0e414f…`, app.js `092a35d711f0…`, i18n.js `cfc5ab0b41b2…`, index.html `28ab4f620ee8…`, style.css `2e9ba90ae919…`; full sweep local == `git cat-file origin/main:` == webroot md5: tracked **21/21 (3-way)** + `site/vision/*.jpeg` **13/13** (2-way, gitignored generated assets) = **34/34 match, 0 mismatch** (`bash .tmp/sweep13.sh`, ssh `-n`) |
|
||
| Live recapture (`mathfix_capture.py after13` + `after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, all 40 DOM rows 55–94 parsed programmatically: **0** negative-OAS cells, **0** GIS>0 rows <65, **0** OAS≠0 rows <65, **0** growing-Total-with-Withdraw rows on consecutive ages; rows 58–64 Total $0 / OAS $0 / GIS $0, Withdraw $49K @58 then $0, Unfunded $47K–$93K (honest post-depletion drain 248K→152K→48K→0, vs the screenshot's impossible 126→141K at 3.5%); first GIS row 65; `wiRetMin: "40"`. FIRE 30/BC/ret 40: first Withdraw>0 row age **40** ($63K, OAS $0 / GIS $0), **0** pre-65 GIS/neg-OAS violations. Mobile 390: retire slider `min=40 max=72 value=40`, `sliderRight 367 ≤ vw 390` (no clip), value chip [25, 145], FIRE hint present EN, `svAfter40: "40 years"` |
|
||
| Live mixed-window probe (re-run, `after13-mixed-*`) | couple me 52 / pt 50, `retireAge: 51` → `fillDefaults` clamps self to 53; the partner inherits the shared clamped 53 → both draw from 53 (DOM row 53 Withdraw $96K), rows 50–52 accum → **0 † rows**, `noteShown: false`, **0** GIS rows <65 — reconfirms Deviation 2 (age-axis merge + shared clamped `retireAge` ⇒ mixed draw windows are unreachable in the live couple path); the `†` suffix + bilingual legend rendering is proven by the `ui.smoke.js` `project()` fixture (3 flagged rows, EN+FR) and the engine §16 invariants |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:37:01Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Evidence hardening this pass: the DOM row JSONs are now swept by a parser (money cells → numbers) with a **consecutive-age** rule. The FIRE table collapses accumulation rows (30 → 35 → 40), so a naive "previous rendered row" comparison flags age 40 ($443K) against age 35 ($430K) as a growing-Total-with-Withdraw row; that pair is accum → first-draw, not a mixed window, and it is **not** flagged under the consecutive-age rule (0 violations in both scenarios).
|
||
|
||
Determinism: `after13-couple-table.png` 1280×900 50,314 B md5 `cbac51ed5089…` — byte-identical to the canonical renders of passes 1/8/9/10/11/12 on the FIRST attempt. The canonical `after-couple-table.png` first regenerated at the stock 900 ms settle as the known 63,753 B render-race variant (md5 `eec99021c08c…`, same size as pass 11's) with DOM row JSON **byte-identical** to `after13-couple-rows.json`; a re-shoot with a 3000 ms settle converged back to 50,314 B / `cbac51ed5089…` → the variant is chart-animation timing, not table content (pass-11 note closed with a concrete cause + remedy). `after13-mixed-rows.json` byte-identical to `after12-mixed-rows.json`. Fresh PNGs IHDR-validated: `after13-couple-table.png` / `after13-mixed-table.png` / `after-couple-table.png` / `after-fire-table.png` 1280×900, `after-fire-step-390.png` 1170×2532 @3×.
|
||
|
||
Harness notes: Playwright run with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (harness TMPDIR read-only; no site impact); ssh byte-check loop uses `-n`; PNGs validated by IHDR + byte-determinism + DOM row scrapes (content evidence). `audit/math-fix/` is gitignored by design — artifacts are local evidence, referenced by path.
|
||
|
||
No code change required — plan executed end-to-end; fix commit `0839826` at `origin/main`; all gates fresh this pass. Final: **MATH_FIX_DONE**.
|
||
|
||
Addendum (post-push, same pass): audit commit `bfe5bcbef049d7be…` pushed `126f90e..bfe5bcb` → `origin/main` (report + fresh off-LAN stamp; code untouched, so the plan's mandated fix commit `08398267040a832bdec0910626423f0dbfeb0a7f` remains the code change under test). Post-push `git fetch` + full byte sweep re-run: tracked **21/21 (3-way)** + `site/vision/*.jpeg` **13/13** = **34/34 match, 0 mismatch** against the new `origin/main`; `HEAD` == `origin/main` == `bfe5bcbef049d7be…`.
|
||
|
||
## Re-verification (fourteenth DSH pass, 2026-10-09 21:39–21:42 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `45c950a` == `origin/main`, no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited by direct read of the working tree (== HEAD, zero tracked diff): GIS 65+ floor — `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + Service Canada source-fact comment (:88–91) + merge per-partner ≥65 gates (:379–380) + `if (yr < 65) gisSum = 0;` (:382); clawback only with OAS — `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + comment :80–83 + splitting-solver parity :366–367; mixed rows — `row.mixed` :390 + depletion guard `!row.mixed` :395 + renderer `†`/legend app.js:807–819 + `t.mixedNote` EN/FR i18n.js:251/512; FIRE — `fillDefaults` clamp `retireAge < age+1` engine.js:188, quiz slider `min: 40` app.js:211, `#wi-ret min="40"` index.html:86, FIRE hint EN/FR i18n.js:96/357. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (incl. §14 T1 repro GIS $0 @56 / clawback $0 @55 / net OAS ≥ 0, §15 clawback 0 pre-OAS & capped at OAS gross @67, §16 couple invariants + depletion guard, §17 fillDefaults 52→53 & 30→40, FIRE first draw 40, GIS/clawback 0 rows 40–64) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR parity, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture ×3 + bilingual legend, `wi-ret min 40`, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` (unchanged files) |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass14.log` (== `passes3-thispass.log`). PASS 1 (screenshot repro): rows 55–56 **GIS $0** (was $12,916 → $13,667/yr phantom) and **OASnet $0** every row <65 (was −$3,501 @55); first GIS row **65** (GIS $12,567, OASnet $9,150; $10,065 from 75); depletion 56 honest (nest egg $170,465, finalBal $0); PASS 2 conservation OK; PASS 3 FIRE: first draw @**40** (Withdraw $62,837, CPP/OAS/GIS $0), max total $493,710 @39 = hand-math 350000×1.035¹⁰ ✓, GIS $13,667 first at 65, depletion 48 → unfunded $60,000/yr to 94 (self-funded gap exposed, not hidden) |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` (fresh this pass) |
|
||
| Byte sweep (webroot) | local `site/` md5 == Unraid webroot `/mnt/better/appdata/retireapp` md5: **34/34 IDENTICAL, 0 mismatch** (full tree: 21 tracked + 13 gitignored generated `vision/*.jpeg`); `HEAD` == `origin/main` == `45c950a` with zero site-file diff ⇒ 3-way equality vs origin holds |
|
||
| Live recapture (`mathfix_capture.py after14` + canonical `after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, all 40 DOM rows 55–94 parsed programmatically (11-column map Age/RRSP/TFSA/NonReg/Total/Withdraw/CPP/OAS/GIS/Tax/Unfunded): **0** negative-OAS cells, **0** GIS>0 rows <65, **0** OAS≠0 rows <65, **0** growing-Total-with-Withdraw rows on consecutive ages; first GIS row **65**, first draw 55; rows 58–64 Total $0 / OAS $0 / GIS $0 (Withdraw $49K @58 then $0, Unfunded $47K→$93K — honest post-depletion drain, vs the screenshot's impossible 126→141K @3.5%); `wiRetMin: "40"`. FIRE 30/BC/ret 40 (`after-fire-rows.json`, 57 rows 30–94): first Withdraw>0 row age **40** ($63K, CPP/OAS/GIS $0), **0** pre-65 GIS / neg-OAS / growing-consecutive violations. Mobile 390: retire slider `min=40 max=72 value=40`, `sliderRight 367 ≤ vw 390` (no clip), value chip [25, 145], FIRE hint present EN, `svAfter40: "40 years"` |
|
||
| Live mixed-window probe (`after14-mixed-*`) | couple me 52 / pt 50, `retireAge: 51` → `fillDefaults` clamps self to 53; partner inherits the shared clamped 53 → both draw from 53 (DOM row 53 Withdraw $93K, Total $237K; rows 50–52 accum, row 50 Total $155K / Withdraw $0) → **0 † rows**, `noteShown: false`, **0** GIS rows <65, **0** OAS≠0 rows <65 — reconfirms Deviation 2 (age-axis merge + shared clamped `retireAge` ⇒ mixed draw windows are unreachable in the live couple path); the `†` suffix + bilingual legend rendering is proven by the `ui.smoke.js` `project()` fixture (3 flagged rows, EN+FR) and the engine §16 invariants |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:40:43Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Determinism: `after14-couple-table.png` 1280×900 50,314 B md5 `cbac51ed5089…` — **byte-identical to the canonical renders of passes 1/8/9/10/11/12/13 on the FIRST attempt**. The canonical `after-couple-table.png` regenerated as chart-animation phase frames at the stock 900 ms settle (`eec99021c08c…` 63,753 B, the known pass-11/13 variant) and, on two 3000 ms re-shoots, `eec99021c08c…` then `0e09348d4997…` 55,831 B — a THIRD frame of the same animation-phase set (the pass-13 "converges back" note is generalized: the settle lands on one of several chart-animation phases, not two). Every variant's DOM row JSON was captured in the same page session and is **byte-identical** to `after14-couple-rows.json` ⇒ table content invariant, frame timing cosmetic. Fresh PNGs IHDR-validated: `after14-couple-table.png` (1280×900 50,314 B) / `after14-mixed-table.png` (1280×900 64,537 B) / `after-fire-table.png` (1280×900 67,310 B) / `after-couple-table.png` (1280×900 55,831 B), `after-fire-step-390.png` 1170×2532 @3× 167,045 B. `after14-mixed-rows.json` DOM ages 50–94 (43 rows).
|
||
|
||
Harness notes: Playwright run with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (harness TMPDIR read-only; the first `after14` attempt died pre-flight with `EROFS … mkdtemp '/home/ubuntu/.hermes/cache/scratch/playwright-artifacts-…'` exit 1, no site impact; clean rerun 0). Importing `audit/mathfix_capture.py` has no `__main__` guard — an import test re-ran the whole `after` capture (same live target, DOM content identical, harmless) and created `audit/__pycache__` (removed); the pass-14 mixed probe lives at `.tmp/mathfix-verify/mixed_probe.py` (gitignored; output mirrored to `audit/math-fix/after14-mixed-*`). `audit/math-fix/` is gitignored by design — artifacts are local evidence, referenced by path.
|
||
|
||
No code change required — plan executed end-to-end; fix commit `0839826` at `origin/main`; all gates fresh this pass. Final: **MATH_FIX_DONE**.
|
||
|
||
Addendum (post-push, same pass): audit commit `99afe5259eb6…` pushed `45c950a..99afe52` → `origin/main` (report + fresh off-LAN stamp; code untouched, the plan's mandated fix commit `08398267040a832bdec0910626423f0dbfeb0a7f` remains the code change under test, confirmed ancestor of `HEAD`). Post-push `git fetch` + full byte sweep re-run vs the new `origin/main`: origin blob == local == webroot md5, tracked **21/21 (3-way)** + gitignored `site/vision/*.jpeg` **13/13 (2-way)** = **34/34 match, 0 mismatch**; `HEAD` == `origin/main` (this addendum commit adds only report text — zero site-file delta, so the sweep carries over by construction).
|
||
|
||
---
|
||
|
||
## Re-verification (fifteenth DSH pass, 2026-10-09 21:44–21:47 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `3f6a44d` == `origin/main`, no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited by direct read of the working tree (== HEAD, zero tracked diff): GIS 65+ floor — `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + Service Canada source-fact comment (:88–91) + merge per-partner ≥65 gates (:379–380) + `if (yr < 65) gisSum = 0;` (:382); clawback only with OAS — `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + s.3(1) ETA comment :80–83 + splitting-solver parity :366–367; mixed rows — `row.mixed` :390 + depletion guard `!row.mixed` :395 + renderer `†`/legend app.js:807–819 + legend element index.html:95 + `t.mixedNote` EN/FR i18n.js:251/512; FIRE — `fillDefaults` clamp `retireAge < age+1` engine.js:188, quiz slider `min: 40` app.js:211, `#wi-ret min="40"` index.html:86, FIRE hint EN/FR i18n.js:96/357. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (§14 T1 repro: clawback $0 @55, GIS $0 @56, OAS gross 0 @56, net OAS ≥ 0 every row, no GIS<65 anywhere; §15: 100k RRSP draw @55 → cw 0, 67y 150k draw → cw $9,150 capped at OAS gross; §16: couple GIS/clawback zero <65, net OAS ≥ 0, no growing-Total-with-Withdraw draw rows, household GIS @70, age-offset window rows 50–54, depletion guard intact, depletion never fires on a mixed row; §17: fillDefaults 52→53 & 30→40, FIRE first draw 40, rows 40–64 GIS/clawback 0, stale 52/40 state first draw 53) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR parity, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture ×3 + bilingual legend, control-single no †, `wi-ret min 40`, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` (unchanged files) |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass15.log` (copied to `passes3-thispass.log`; md5 `01075d0e…` — **byte-identical to pass 14** ⇒ engine output deterministic across passes). PASS 1 (screenshot repro): rows 52–64 **OASnet $0 / GIS $0** (plan evidence −$3,501 @55 / $12,916 @56 / phantom $13,667/yr to 94 — all gone), first GIS row **65** (GIS $12,567, OASnet $9,150), depletion 56 honest (nest egg $170,465 vs $93k spend, finalBal $0); PASS 2 depletion 82, GIS 65+ only; PASS 3 FIRE: first draw @**40** (Withdraw $62,837, CPP/OAS/GIS $0), max total $493,710 @39 = hand-math 350000×1.035¹⁰ ✓, GIS $13,667 first at 65, depletion 48 → unfunded $60,000/yr to 94 (self-funded gap exposed, not hidden) |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` (fresh this pass) |
|
||
| Origin byte-check | 3-way sweep `.tmp/sweep15.sh` (ssh `-n`, one remote md5 pass): local `site/` == `git cat-file origin/main:` == webroot `/mnt/better/appdata/retireapp` — tracked **21/21 (3-way)** + gitignored `site/vision/*.jpeg` **13/13 (2-way)** = **34/34 match, 0 mismatch** → `SWEEP-OK`; `HEAD` == `origin/main` == `3f6a44d` |
|
||
| Live recapture (`mathfix_capture.py after15` + canonical `after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, all 40 DOM rows 55–94 parsed with a K-aware 11-column parser (`age, RRSP, TFSA, NonReg, Total, Withdraw, CPP, OAS, GIS, Tax+claw, Unfunded`; `—`/`K` cells handled): **0** negative-OAS cells, **0** GIS>0 rows <65, **0** OAS≠0 rows <65, **0** growing-Total-with-Withdraw rows on consecutive ages; first draw **55** (Withdraw $93K, Total $248K), rows 58–64 Total $0 / OAS $0 / GIS $0 (Withdraw $49K @58 then $0, Unfunded $47K→$93K — honest post-depletion drain, vs the screenshot's impossible 126→141K @3.5%), first GIS row **65** (GIS $12K, OAS $18K, Tax+claw $339 > 0 with OAS payable ✓); `wiRetMin: "40"`. FIRE 30/BC/ret 40 (`after-fire-rows.json`, 57 rows 30–94): first Withdraw>0 row age **40**, first GIS row 65, **0** pre-65 GIS / neg-OAS / growing-consecutive violations. Mobile 390: retire slider `min=40 max=72 value=40`, `sliderRight 367 ≤ vw 390` (no clip), value chip [25, 145], FIRE hint present EN, `svAfter40: "40 years"` |
|
||
| Live mixed-window probe (`after15-mixed-*`, re-run of the pass-14 probe) | couple me 52 / pt 50, `retireAge: 51` → `fillDefaults` clamps self to 53; partner inherits the shared clamped 53 → both draw from 53 (DOM row 53 Withdraw $93K, Total $237K; rows 50–52 accum, row 50 Total $155K / Withdraw $0) → **0 † rows**, `noteShown: false`, **0** GIS>0 rows <65, **0** OAS≠0 rows <65, **0** negative-OAS cells — reconfirms Deviation 2 (age-axis merge + shared clamped `retireAge` ⇒ mixed draw windows are unreachable in the live couple path); the `†` suffix + bilingual legend rendering is proven by the `ui.smoke.js` `project()` fixture (3 flagged rows, EN+FR) and the engine §16 invariants |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:46:19Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Determinism: canonical `after-couple-table.png` **landed on the canonical frame on the first attempt** — 1280×900 50,314 B md5 `cbac51ed5089…`, byte-identical to the canonical renders of passes 1/8/9/10/11/12/13/14; the `after15`-tagged couple run landed on the known chart-animation-phase frame `eec99021c08c…` 63,753 B (pass-11/13/14 variant) with DOM row JSON **byte-identical** to the canonical capture (`129e2a45…`), exactly the pass-14 frame-set behaviour. `after15-mixed-rows.json` byte-identical to `after14-mixed-rows.json` (md5 `96665bbf…`). Fresh PNGs IHDR-validated: `after15-couple-table.png` (1280×900 63,753 B) / `after15-mixed-table.png` (1280×900 44,945 B) / `after-couple-table.png` (1280×900 50,314 B) / `after-fire-table.png` (1280×900 67,310 B), `after-fire-step-390.png` 1170×2532 @3× 167,045 B.
|
||
|
||
Harness notes: Playwright runs with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (harness TMPDIR read-only; no site impact); ssh byte-check uses `-n`; sweep + K-aware row-sweep scripts at `.tmp/sweep15.sh` / `.tmp/mathfix15/rowsweep.py` (gitignored; the row-sweep is now a standalone script after this pass found the probe's inline `money()` returning `None` on `$NNK` cells — the probe's own naive parser never flags a K-formatted cell, so its "0 violations" verdicts are conservative; the K-aware re-parse of every captured JSON above is the authoritative sweep). `audit/math-fix/` is gitignored by design — artifacts are local evidence, referenced by path.
|
||
|
||
No code change required — plan executed end-to-end; fix commit `0839826` at `origin/main`; all gates fresh this pass. Final: **MATH_FIX_DONE**.
|
||
|
||
Addendum (post-push, same pass): audit commit `5d7c84a3af081abc…` pushed `3f6a44d..5d7c84a` → `origin/main` (report + fresh off-LAN stamp; code untouched, the plan's mandated fix commit `08398267040a832bdec0910626423f0dbfeb0a7f` remains the code change under test, confirmed ancestor of `HEAD`). Post-push `git fetch` + full byte sweep re-run vs the new `origin/main`: origin blob == local == webroot md5, tracked **21/21 (3-way)** + gitignored `site/vision/*.jpeg` **13/13 (2-way)** = **34/34 match, 0 mismatch**; `HEAD` == `origin/main` (this addendum commit adds only report text — zero site-file delta, so the sweep carries over by construction).
|
||
|
||
---
|
||
|
||
## Re-verification (sixteenth DSH pass, 2026-10-09 21:47–21:52 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `3c76b7f` == `origin/main` (post-fetch), no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited by direct read of the working tree (== HEAD, zero tracked diff): GIS 65+ floor — `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + Service Canada source-fact comment (:89) + merge per-partner ≥65 gates (:376/:379–380) + `if (yr < 65) gisSum = 0;` (:382); clawback only with OAS — `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + s.3(1) ETA comment :80–83 + splitting-solver parity :366–367; mixed rows — `row.mixed` :390 + depletion guard `!row.mixed` :395 + renderer `†`/legend app.js:807–819 + legend element index.html:95 + `t.mixedNote` EN/FR i18n.js:251/512; FIRE — `fillDefaults` clamp `retireAge < age+1` engine.js:188, quiz slider `min: 40` app.js:211, `#wi-ret min="40"` index.html:86, FIRE hint EN/FR i18n.js:96/357. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (incl. §14 T1 repro GIS $0 @56 / clawback $0 @55 / net OAS ≥ 0 everywhere, §15 clawback 0 pre-OAS & capped at OAS gross @67, §16 couple invariants + depletion-guard-never-fires-on-mixed, §17 fillDefaults 52→53 & 30→40, FIRE first draw 40, GIS/clawback 0 rows 40–64, stale 52/40 → first draw 53) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR parity, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture ×3 + bilingual legend EN+FR, control-single no †, `wi-ret min 40`, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` (unchanged files) |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass16.log` (copied to `passes3-thispass.log`; md5 `01075d0e…` — **byte-identical to passes 14/15** ⇒ engine output deterministic across three passes). PASS 1 (screenshot repro): rows 52–64 **OASnet $0 / GIS $0** — the plan evidence −$3,501 @55, $12,916 @56, phantom $13,667/yr to 94 all gone (programmatic scan: 0 rows <65 with GIS>0 or OASnet≠0); first GIS row **65** (GIS $12,567, OASnet $9,150); depletion 56 honest (nest egg $170,465 vs $93k spend, finalBal $0); PASS 2 conservation OK; PASS 3 FIRE: first draw @**40** (Withdraw $62,837, CPP/OAS/GIS $0), max total $493,710 @39 = hand-math ✓, GIS $13,667 first at 65, depletion 48 → unfunded $60,000/yr to 94 (self-funded gap exposed, not hidden) |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` (fresh this pass) |
|
||
| Origin byte-check | 3-way sweep `.tmp/sweep15.sh` (reused, ssh `-n`, one remote md5 pass): local `site/` == `git cat-file origin/main:` == webroot `/mnt/better/appdata/retireapp` — tracked **21/21 (3-way)** + gitignored `site/vision/*.jpeg` **13/13 (2-way)** = **34/34 match, 0 mismatch** → `SWEEP-OK`; `HEAD` == `origin/main` == `3c76b7f` |
|
||
| Live recapture (`mathfix_capture.py after16` + canonical `after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, all 40 DOM rows 55–94 parsed with the K-aware 11-column parser (`.tmp/mathfix16/rowsweep.py`): **0** negative-OAS cells, **0** GIS>0 rows <65, **0** OAS≠0 rows <65, **0** growing-Total-with-Withdraw rows on consecutive ages → `ROWSWEEP-OK`; first draw **55**, first GIS row **65**; rows 58–64 Total $0 / OAS $0 / GIS $0 (Withdraw $49K @58 then $0, Unfunded $47K→$93K→$79K from CPP@60 — honest post-depletion drain, vs the screenshot's impossible 126→141K @3.5%); `wiRetMin: "40"`. FIRE 30/BC/ret 40 (`after-fire-rows.json`, 57 rows 30–94): first Withdraw>0 row age **40**, first GIS row 65, **0** pre-65 GIS / neg-OAS / growing-consecutive violations. Mobile 390: retire slider `min=40 max=72 value=40`, `sliderRight 367 ≤ vw 390` (no clip), value chip [25, 145], FIRE hint present EN, `svAfter40: "40 years"` |
|
||
| Live mixed-window probe (`after16-mixed-*`, re-run of the pass-14/15 probe) | couple me 52 / pt 50, `retireAge: 51` → `fillDefaults` clamps self to 53; partner inherits the shared clamped 53 → both draw from 53 (DOM row 53 Withdraw $93K, Total $237K; row 50 accum, Total $155K / Withdraw $0) → **0 † rows**, `noteShown: false`, **0** GIS>0 rows <65, **0** OAS≠0 rows <65, **0** negative-OAS cells — reconfirms Deviation 2 (age-axis merge + shared clamped `retireAge` ⇒ mixed draw windows are unreachable in the live couple path); the `†` suffix + bilingual legend rendering is proven by the `ui.smoke.js` `project()` fixture (3 flagged rows, EN+FR) and the engine §16 invariants |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:49:26Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Determinism: `after16-couple-rows.json` **byte-identical** to `after15-couple-rows.json` and the canonical `after-couple-rows.json` (md5 `129e2a45…`); `after16-mixed-rows.json` byte-identical to `after15-mixed-rows.json` (md5 `96665bbf…`). Frame set as characterized in passes 14/15: the `after16` couple run landed on the known chart-animation-phase frame `eec99021c08c…` 63,753 B (pass-11/13/14/15 variant) and the canonical `after` run **landed on the canonical frame on the first attempt** — 1280×900 50,314 B md5 `cbac51ed5089…`, byte-identical to the canonical renders of passes 1/8/9/10/11/12/13/14/15. Fresh PNGs IHDR-validated: `after16-couple-table.png` (1280×900 63,753 B) / `after16-mixed-table.png` (1280×900 51,653 B, anim-phase frame; row JSON canonical) / `after-couple-table.png` (1280×900 50,314 B canonical) / `after-fire-table.png` (1280×900 67,310 B, byte-identical to prior passes), `after-fire-step-390.png` 1170×2532 @3× 167,045 B.
|
||
|
||
Harness notes: Playwright runs with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (harness TMPDIR read-only; no site impact); ssh byte-check uses `-n`; this pass reused the pass-15 sweep + K-aware rowsweep (copied to `.tmp/mathfix16/`, gitignored) with zero edits — both passed unmodified. `audit/math-fix/` is gitignored by design — artifacts are local evidence, referenced by path.
|
||
|
||
No code change required — plan executed end-to-end; fix commit `0839826` at `origin/main`; all gates fresh this pass. Final: **MATH_FIX_DONE**.
|
||
|
||
Addendum (post-push, same pass): audit commit `9bd07adeba2f733a…` pushed `3c76b7f..9bd07ad` → `origin/main` (report + fresh off-LAN stamp; code untouched, the plan's mandated fix commit `08398267040a832bdec0910626423f0dbfeb0a7f` remains the code change under test, confirmed ancestor of `HEAD`). Post-push `git fetch` + full byte sweep re-run vs the new `origin/main`: origin blob == local == webroot md5, tracked **21/21 (3-way)** + gitignored `site/vision/*.jpeg` **13/13 (2-way)** = **34/34 match, 0 mismatch**; `HEAD` == `origin/main` (this addendum commit adds only report text — zero site-file delta, so the sweep carries over by construction).
|
||
---
|
||
|
||
## Re-verification (seventeenth DSH pass, 2026-10-09 21:52–21:56 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `97123a1` == `origin/main` (post-fetch), no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited by direct read of the working tree (== HEAD, zero tracked diff): GIS 65+ floor — `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + Service Canada source-fact comment (:89) + merge per-partner ≥65 gates (:376/:379–380) + `if (yr < 65) gisSum = 0;` (:382); clawback only with OAS — `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + s.3(1) ETA comment :80–83 + splitting-solver parity :366–367; mixed rows — `row.mixed` :390 + depletion guard `!row.mixed` :395 + renderer `†`/legend app.js:807–819 + legend element index.html:95 + `t.mixedNote` EN/FR i18n.js:251/512; FIRE — `fillDefaults` clamp `retireAge < age+1` engine.js:188, quiz slider `min: 40` app.js:211, `#wi-ret min="40"` index.html:86, FIRE hint EN/FR i18n.js:96/357. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (incl. §14 T1 repro GIS $0 @56 / clawback $0 @55 / net OAS ≥ 0 everywhere, §15 clawback 0 pre-OAS & capped at OAS gross @67, §16 couple invariants + depletion-guard-never-fires-on-mixed, §17 fillDefaults 52→53 & 30→40, FIRE first draw 40, GIS/clawback 0 rows 40–64, stale 52/40 → first draw 53) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR parity, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture ×3 + bilingual legend EN+FR, control-single no †, `wi-ret min 40`, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` (unchanged files) |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass17.log` (copied to `passes3-thispass.log`; md5 `01075d0e…` — **byte-identical to passes 14/15/16** ⇒ engine output deterministic across four passes). PASS 1 (screenshot repro): programmatic scan of all 43 rows — **0** rows <65 with GIS>0, **0** with OASnet≠0, **0** negative-OASnet rows anywhere (plan evidence −$3,501 @55, $12,916 @56, phantom $13,667/yr to 94 all gone); first GIS row **65** (GIS $12,567, OASnet $9,150); depletion 56 honest (nest egg $170,465 vs $93k spend, finalBal $0); **0** growing-Total-with-Withdraw consecutive rows; PASS 2 depletion 82, conservation OK; PASS 3 FIRE: first draw @**40** (Withdraw $62,837, CPP/OAS/GIS $0), max total $493,710 @39 = hand-math 350000×1.035¹⁰ ✓, GIS $13,667 first at 65, **0** pre-65 GIS rows, depletion 48 → unfunded gap exposed (Unfunded $57,613 @60 = $60,000 − CPP $2,387, to 94) |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` (fresh this pass) |
|
||
| Origin byte-check | 3-way sweep `.tmp/mathfix17/sweep17.sh` (pass-15 script, file lists regenerated from git — byte-identical to the prior lists; ssh `-n`, one remote md5 pass): local `site/` == `git cat-file origin/main:` == webroot `/mnt/better/appdata/retireapp` — tracked **21/21 (3-way)** + gitignored `site/vision/*.jpeg` **13/13 (2-way)** = **34/34 match, 0 mismatch** → `SWEEP-OK`; `HEAD` == `origin/main` == `97123a1` |
|
||
| Live recapture (`mathfix_capture.py after17` + canonical `after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, all 40 DOM rows 55–94 parsed with the K-aware 11-column parser (`.tmp/mathfix17/rowsweep.py`): **0** negative-OAS cells, **0** GIS>0 rows <65, **0** OAS≠0 rows <65, **0** growing-Total-with-Withdraw rows on consecutive ages → `ROWSWEEP-OK` on all three row JSONs; first draw **55**, first GIS row **65**; rows 58–64 Total $0 / OAS $0 / GIS $0 (Withdraw $49K @58 then $0, Unfunded $47K→$93K→$79K from CPP@60 — honest post-depletion drain, vs the screenshot's impossible 126→141K @3.5%); `wiRetMin: "40"`. FIRE 30/BC/ret 40 (`after-fire-rows.json`, 57 rows 30–94): first Withdraw>0 row age **40**, first GIS row 65, **0** pre-65 GIS / neg-OAS / growing-consecutive violations. Mobile 390: retire slider `min=40 max=72 value=40`, `sliderRight 367 ≤ vw 390` (no clip), value chip [25, 145], FIRE hint present EN, `svAfter40: "40 years"` |
|
||
| Live mixed-window probe (`after17-mixed-*`, re-run of the pass-14/15/16 probe) | couple me 52 / pt 50, `retireAge: 51` → `fillDefaults` clamps self to 53; partner inherits the shared clamped 53 → both draw from 53 (DOM row 53 Withdraw $93K, Total $237K; row 50 accum, Total $155K / Withdraw $0) → **0 † rows**, `noteShown: false`, K-aware re-parse: **0** GIS>0 rows <65, **0** OAS≠0 rows <65, **0** negative-OAS cells, **0** growing-Total-with-Withdraw → reconfirms Deviation 2 (age-axis merge + shared clamped `retireAge` ⇒ mixed draw windows are unreachable in the live couple path); the `†` suffix + bilingual legend rendering is proven by the `ui.smoke.js` `project()` fixture (3 flagged rows, EN+FR) and the engine §16 invariants |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:52:45Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Determinism: `after17-couple-rows.json` **byte-identical** to the canonical `after-couple-rows.json` re-captured in the same session (md5 `129e2a45…` — same as passes 14/15/16); `after17-mixed-rows.json` byte-identical to `after15/16-mixed-rows.json` (md5 `96665bbf…`). Frame set as characterized in passes 14/15/16: the `after17` couple run landed on the known chart-animation-phase frame `eec99021c08c…` 63,753 B (pass-11/13/14/15/16 variant) with DOM row JSON byte-identical to canonical, and the canonical `after` run **landed on the canonical frame on the first attempt** — 1280×900 50,314 B md5 `cbac51ed5089…`, byte-identical to the canonical renders of passes 1/8/9/10/11/12/13/14/15/16. Fresh PNGs IHDR-validated: `after17-couple-table.png` (1280×900 63,753 B, anim-phase frame) / `after17-mixed-table.png` (1280×900 64,537 B, md5 `222c81a1…` — byte-identical to `after14-mixed-table.png`) / `after-couple-table.png` (1280×900 50,314 B canonical) / `after-fire-table.png` (1280×900 67,310 B, md5 `685fd711…` byte-identical to prior passes), `after-fire-step-390.png` 1170×2532 @3× 167,045 B (md5 `761e8055…`, stable).
|
||
|
||
Harness notes: Playwright runs with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (harness TMPDIR read-only; no site impact); ssh byte-check uses `-n`; this pass rebuilt the sweep + K-aware rowsweep + mixed probe under `.tmp/mathfix17/` (gitignored) — sweep file lists regenerated with `git ls-files` (21 tracked + 13 ignored, byte-identical to the pass-15/16 lists); rowsweep copied from pass 16 with only the detail-block filename retargeted to `after17-couple-rows.json`; all three ran unmodified otherwise. `audit/math-fix/` is gitignored by design — artifacts are local evidence, referenced by path.
|
||
|
||
No code change required — plan executed end-to-end; fix commit `0839826` at `origin/main`; all gates fresh this pass. Final: **MATH_FIX_DONE**.
|
||
|
||
Addendum (post-push, same pass): audit commit `4c66a83f7bafeb56…` pushed `97123a1..4c66a83` → `origin/main` (report + fresh off-LAN stamp; code untouched, the plan's mandated fix commit `08398267040a832bdec0910626423f0dbfeb0a7f` remains the code change under test, confirmed ancestor of `HEAD`). Post-push `git fetch` + full byte sweep re-run vs the new `origin/main`: origin blob == local == webroot md5, tracked **21/21 (3-way)** + gitignored `site/vision/*.jpeg` **13/13 (2-way)** = **34/34 match, 0 mismatch**; `HEAD` == `origin/main` (this addendum commit adds only report text — zero site-file delta, so the sweep carries over by construction).
|
||
|
||
---
|
||
|
||
## Re-verification (eighteenth DSH pass, 2026-10-09 21:53–21:57 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start, HEAD `198b3ef` == `origin/main` (post-fetch), no job in flight → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited by direct read of the working tree (== HEAD, zero tracked diff): GIS 65+ floor — `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + Service Canada source-fact comment (:89) + merge per-partner ≥65 gates (:376/:379–380) + `if (yr < 65) gisSum = 0;` (:382); clawback only with OAS — `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + s.3(1) ETA comment :80–83 + splitting-solver parity :366–367; mixed rows — `row.mixed` :390 + depletion guard `!row.mixed` :395 + renderer `†`/legend app.js:807–819 + legend element index.html:95 + `t.mixedNote` EN/FR i18n.js:251/512; FIRE — `fillDefaults` clamp `retireAge < age+1` engine.js:188, quiz slider `min: 40` app.js:211, `#wi-ret min="40"` index.html:86, FIRE hint EN/FR i18n.js:96/357. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (incl. §14 T1 repro GIS $0 @56 / clawback $0 @55 / net OAS ≥ 0 everywhere, §15 clawback 0 pre-OAS & capped at OAS gross @67, §16 couple invariants + depletion-guard-never-fires-on-mixed, §17 fillDefaults 52→53 & 30→40, FIRE first draw 40, GIS/clawback 0 rows 40–64, stale 52/40 → first draw 53) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR parity, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture ×3 with bilingual legend EN+FR, control-single no † / legend hidden, `wi-ret min 40` from index.html, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` (unchanged files) |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass18.txt` (md5 `01075d0e…` — **byte-identical to passes 14/15/16/17** ⇒ engine output deterministic across five passes). Programmatic scan of all printed rows (column-correct parser: Total/Withdraw/CPP/OASnet/GIS/Tax+clw/Unfunded): PASS 1 (screenshot repro) 43 rows — **0** rows <65 with GIS>0, **0** with OASnet≠0, **0** negative-OASnet, **0** growing-Total-with-Withdraw consecutive rows (plan evidence −$3,501 @55, $12,916 @56, phantom $13,667/yr to 94 all gone); row 55 Total $68,327 Withdraw $102,139 **OASnet $0 GIS $0**; first GIS row **65** (GIS $12,567, OASnet $9,150); depletion 56 honest (nest egg $170,465 vs $93k spend, finalBal $0); PASS 2: 50 rows firstDraw 65 firstGIS 65, all invariant sets empty, depletion 82; PASS 3 FIRE: 65 rows, first draw @**40** (Withdraw $62,837, CPP/OAS/GIS $0), max total $493,710 @39 = hand-math 350000×1.035¹⁰ ✓, GIS $13,667 first at 65, **0** pre-65 GIS rows, depletion 48 → unfunded gap exposed to 94 |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` (fresh this pass) |
|
||
| Origin byte-check | 3-way sweep `.tmp/mathfix18/sweep18.sh` (pass-15-lineage script; tracked/ignored file lists regenerated from `git ls-files` — `diff` vs pass-17 lists: byte-identical, 21+13; ssh `-n`, one remote md5 pass): local `site/` == `git cat-file origin/main:` == webroot `/mnt/better/appdata/retireapp` — tracked **21/21 (3-way)** + gitignored `site/vision/*.jpeg` **13/13 (2-way)** = **34/34 match, 0 mismatch** → `SWEEP-OK`; `HEAD` == `origin/main` == `198b3ef` |
|
||
| Live recapture (`mathfix_capture.py after18` + canonical `after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, all 40 DOM rows parsed with the K-aware 11-column parser (`.tmp/mathfix18/rowsweep.py`): **0** negative-OAS cells, **0** GIS>0 rows <65, **0** OAS≠0 rows <65, **0** growing-Total-with-Withdraw rows on consecutive ages → `ROWSWEEP-OK` on all three row JSONs; first draw **55**, first GIS row **65**; rows 58–64 Total $0 / OAS $0 / GIS $0 (Withdraw $49K @58 then $0, Unfunded $47K→$93K→$79K from CPP@60 — honest post-depletion drain, vs the screenshot's impossible 126→141K @3.5%); `wiRetMin: "40"`. FIRE 30/BC/ret 40 (`after-fire-rows.json`, 57 rows 30–94): `fire_first_draw_age: "40"`, first GIS row 65, **0** pre-65 GIS / neg-OAS / growing-consecutive violations. Mobile 390: retire slider `min=40 max=72 value=40`, `sliderRight 367 ≤ vw 390` (no clip), value chip [25, 145], FIRE hint present EN, `svAfter40: "40 years"` |
|
||
| Live mixed-window probe (`after18-mixed-*`, re-run of the pass-14–17 probe) | couple me 52 / pt 50, `retireAge: 51` → `fillDefaults` clamps self to 53; partner inherits the shared clamped 53 → both draw from 53 (DOM row 50 accum, Total $155K / Withdraw $0; row 53 Withdraw $93K, Total $237K; row 54 Withdraw $104K, Unfunded $11K) → **0 † rows**, `noteShown: false`, K-aware re-parse of 43 rows (firstDraw 53, firstGIS 65): **0** GIS>0 rows <65, **0** OAS≠0 rows <65, **0** negative-OAS cells, **0** growing-Total-with-Withdraw → reconfirms Deviation 2 (age-axis merge + shared clamped `retireAge` ⇒ mixed draw windows are unreachable in the live couple path); the `†` suffix + bilingual legend rendering is proven by the `ui.smoke.js` `project()` fixture (3 flagged rows, EN+FR) and the engine §16 invariants |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:55:57Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Determinism: `after18-couple-rows.json` **byte-identical** to the canonical `after-couple-rows.json` re-captured in the same session (md5 `129e2a45…` — same as passes 14/15/16/17); `after18-mixed-rows.json` byte-identical to `after15/16/17-mixed-rows.json` (md5 `96665bbf…`). Frame set: the `after18` couple run landed on the known chart-animation-phase frame `eec99021c08c…` 63,753 B (pass-11/13/14/15/16/17 variant) with DOM row JSON byte-identical to canonical, and the canonical `after` couple run **landed on the canonical frame on the first attempt** — 1280×900 50,314 B md5 `cbac51ed5089…`, byte-identical to the canonical renders of passes 1/8/9/10/11/12/13/14/15/16/17. Fresh PNGs IHDR-validated: `after18-couple-table.png` (1280×900 63,753 B, anim-phase frame) / `after18-mixed-table.png` (1280×900 64,537 B, md5 `222c81a1…` — byte-identical to `after14/17-mixed-table.png`) / `after-couple-table.png` (1280×900 50,314 B canonical) / `after-fire-step-390.png` 1170×2532 @3× 167,045 B (md5 `761e8055…`, stable). One new frame observation: the canonical `after-fire-table.png` this pass is 1280×900 **53,323 B** md5 `b4ed3a16…` (passes 12–17 recorded the 67,310 B `685fd711…` frame) — a chart-animation-phase snapshot variant of the same page; IHDR-valid, DOM row JSON canonical (`after-fire-rows.json` firstDraw 40 / firstGIS 65, `ROWSWEEP-OK`), so the evidence row set is unchanged. The `after18` tag by script design captures only the couple scenario (FIRE + mobile 390 run under the canonical `after` tag), hence no `after18-fire-*` files.
|
||
|
||
Harness notes: Playwright runs with `TMPDIR=/home/ubuntu/projects/retire-planner/.pwtmp` (harness TMPDIR read-only; no site impact); ssh byte-check uses `-n`; this pass rebuilt the sweep + K-aware rowsweep + mixed probe under `.tmp/mathfix18/` (gitignored) — sweep file lists regenerated with `git ls-files` (21 tracked + 13 ignored, `diff`-proven byte-identical to the pass-17 lists); rowsweep copied from pass 17 with only the detail-block filename retargeted to `after17→after18`; all ran unmodified otherwise. `audit/math-fix/` is gitignored by design — artifacts are local evidence, referenced by path.
|
||
|
||
No code change required — plan executed end-to-end; fix commit `0839826` at `origin/main` (ancestor of HEAD, confirmed via `git merge-base --is-ancestor`); all gates fresh this pass. Final: **MATH_FIX_DONE**.
|
||
|
||
Addendum (post-push, same pass): audit commit `c5daee52a47d90df6f26b3e2c92a3b91cbd5da72` pushed `198b3ef..c5daee5` → `origin/main` (report + fresh off-LAN stamp; code untouched, the plan's mandated fix commit `08398267040a832bdec0910626423f0dbfeb0a7f` remains the code change under test, confirmed ancestor of `HEAD`). Post-push `git fetch` + full byte sweep re-run vs the new `origin/main`: origin blob == local == webroot md5, tracked **21/21 (3-way)** + gitignored `site/vision/*.jpeg` **13/13 (2-way)** = **34/34 match, 0 mismatch**; `HEAD` == `origin/main` (this addendum commit adds only report text — zero site-file delta, so the sweep carries over by construction).
|
||
|
||
---
|
||
|
||
## Re-verification (nineteenth DSH pass, 2026-10-09 21:57–22:02 UTC, post-completion)
|
||
|
||
Queue gate: `git status --porcelain` empty at start (HEAD `4d66a6e` == `origin/main` post-fetch) → proceed (not `DSH_QUEUE_BUSY`). Fix 1–4 re-audited by direct read of the working tree (== HEAD, zero tracked diff): GIS 65+ floor — `a >= 65 ? gis(…) : 0` at the three `solve()` sites (engine.js:249/260/265) + Service Canada source-fact comment (:88–91) + merge per-partner ≥65 gates (:376/:379–380) + `if (yr < 65) gisSum = 0;` (:382); clawback only with OAS — `oasGross > 0 ? Math.min(clawback(…), oasGross) : 0` at :248/:259 + s.3(1) ETA comment :80–83 + splitting-solver parity :366–367; mixed rows — `row.mixed` :390 + depletion guard `!row.mixed` :395 + renderer `†`/legend app.js:807–819 + legend element index.html:95 + `t.mixedNote` EN/FR i18n.js:251/512; FIRE — `fillDefaults` clamp `retireAge < age+1` engine.js:188, quiz slider `min: 40` app.js:211, `#wi-ret min="40"` index.html:86, FIRE hint EN/FR i18n.js:96/357. All acceptance checks re-executed fresh in this pass:
|
||
|
||
| Check | Fresh output (this pass) |
|
||
|---|---|
|
||
| `tests/engine.test.js` | `RESULT: 61 pass, 0 fail` (incl. §14 GIS $0 @56 / clawback $0 @55 repro, §15 clawback 0 pre-OAS & capped at OAS gross, §16 couple invariants + depletion-never-fires-on-mixed, §17 fillDefaults 52→53 & 30→40, FIRE first draw 40, GIS/clawback 0 rows 40–64) |
|
||
| `tests/i18n.test.js` | `I18N-OK all i18n checks passed (50)` (incl. `t.mixedNote` EN+FR parity, `q.retire.hint` FIRE EN+FR) |
|
||
| `tests/ui.smoke.js` | `SMOKE-OK all ui smoke checks passed (146)` (incl. † mixed-row fixture with bilingual legend, `wi-ret min 40` from index.html, legend element present) |
|
||
| `tests/ps-pension.test.js` / `ps-auto-service.test.js` | `70 pass, 0 fail` / `21 pass, 0 fail` (unchanged files) |
|
||
| `passes3.js` rerun (live engine) | exit 0, `conservation: OK` ×3 → `audit/math-fix/passes3-pass19.txt` (md5 `01075d0e…` — **byte-identical to passes 14–18**, six consecutive deterministic passes). Line-tracked scan of all printed rows: PASS 1 (screenshot repro) 43 rows — **0** GIS>0 rows <65 (old $12,916 @56 / phantom $13,667 to 94 gone), **0** OASnet≠0 rows <65 (old −$3,501 @55 gone), **0** negative-OASnet, **0** growing-Total-with-Withdraw consecutive rows; first GIS row **65** ($9,150 GIS, OASnet $7,200), depletion 56 honest (finalBal $0); PASS 2: 50 rows firstDraw 65 firstGIS 65, all sets empty, conservation OK; PASS 3 FIRE: 65 rows, first draw @**40**, GIS $13,667 first at **65**, **0** pre-65 GIS, **0** violations, conservation OK |
|
||
| `scripts/deploy.sh` | `DEPLOY-OK` (fresh this pass) |
|
||
| Origin byte-check | 3-way sweep `.tmp/mathfix19/sweep19.sh` (pass-18-lineage script; tracked/ignored lists regenerated via `git ls-files`, `diff` vs pass-18 lists: byte-identical, 21+13; ssh `-n`, single remote md5 pass): local `site/` == `git cat-file blob origin/main:` == webroot — tracked **21/21 (3-way)** + gitignored `site/vision/*.jpeg` **13/13 (2-way)** = **34/34 match, 0 mismatch** → `SWEEP-OK`; `HEAD` == `origin/main` == `4d66a6e` |
|
||
| Live recapture (`mathfix_capture.py after19` + canonical `after`, retire.krisforbes.ca) | couple 52/52 ret 55 spend 93k CPP 60, K-aware 11-column parse (`.tmp/mathfix19/rowsweep.py`): **ROWSWEEP-OK** on all four row JSONs — 40 rows, first draw **55**, first GIS **65**, **0** neg-OAS, **0** GIS>0 <65, **0** OAS≠0 <65, **0** growing-Total-with-Withdraw; rows 58–64 Total $0 / OAS $0 / GIS $0 (Withdraw $49K @58 then $0, Unfunded $47K→$93K→$79K from CPP@60 — honest drain, vs the screenshot's impossible 126→141K @3.5%); `wiRetMin: "40"`. FIRE 30/BC/ret 40 (`after-fire-rows.json`, 57 rows): `fire_first_draw_age: "40"`, first GIS 65, **0** violations. Mobile 390: retire slider `min=40 max=72 value=40`, `sliderRight 367 ≤ vw 390` (no clip), chip [25, 145], FIRE hint EN present, `svAfter40: "40 years"` |
|
||
| Live mixed-window probe (`after19-mixed-*`, pass-14-lineage) | couple me 52 / pt 50, `retireAge: 51` → `fillDefaults` clamps self to 53, partner inherits the shared clamped 53 → both draw from 53: 43 rows, firstDraw **53**, firstGIS **65**, **0 † rows**, `noteShown: false`, all invariant sets empty → reconfirms Deviation 2 (age-axis merge + shared clamped `retireAge` ⇒ mixed draw windows unreachable in the live couple path); the `†` suffix + bilingual legend is proven by `ui.smoke.js` `project()` fixtures and engine §16 |
|
||
| Off-LAN (Midhurst `trouble@100.96.20.61`) | `OFFLAN-OK`, `"all_ok": true`, `"http_code": "200"`, `checked_at 2026-10-09T21:59:11Z` → `audit/bilingual-geo-check.json` |
|
||
|
||
Determinism: `after19-couple-rows.json` md5 `129e2a45…` — **byte-identical** to the canonical `after-couple-rows.json` re-captured in the same session (same as passes 14–18); `after19-mixed-rows.json` md5 `96665bbf…` byte-identical to `after15/16/17/18-mixed-rows.json`. Frames: the `after19` couple run landed on the known chart-animation-phase frame `eec99021c08c…` 63,753 B (pass-11/13–18 variant) with DOM row JSON byte-identical to canonical; the canonical `after` couple run **landed on the canonical frame on the first attempt** — 1280×900 50,314 B md5 `cbac51ed5089…`, byte-identical to the canonical renders of passes 1/8–18. All five fresh/updated PNGs IHDR-validated: `after19-couple-table.png` / `after19-mixed-table.png` (1280×900 64,537 B md5 `222c81a1…` — byte-identical to `after14/17/18-mixed-table.png`), `after-couple-table.png` (50,314 B canonical), `after-fire-table.png` (1280×900 53,323 B md5 `b4ed3a16…` — same anim-phase variant pass 18 recorded, DOM rows canonical), `after-fire-step-390.png` (1170×2532 @3× 167,045 B md5 `761e8055…` stable). `after19` tag captures only the couple scenario by script design (FIRE + mobile 390 under canonical `after`).
|
||
|
||
Harness notes: Playwright runs with `TMPDIR=/home/ubuntu/projects/retire-planner/.tmp/pw` (harness TMPDIR read-only; no site impact); ssh byte-check uses `-n`; this pass rebuilt sweep + K-aware rowsweep + mixed probe under `.tmp/mathfix19/` (gitignored) from the pass-18 originals with only tag retargeting (`after18→after19`); all ran unmodified otherwise. `audit/math-fix/` is gitignored by design — artifacts are local evidence, referenced by path.
|
||
|
||
No code change required — plan executed end-to-end; fix commit `0839826` at `origin/main` (ancestor of HEAD, confirmed via `git merge-base --is-ancestor`); all gates fresh this pass. Final: **MATH_FIX_DONE**.
|