From 53f12f207e2f7c526dd2994b1b638ce7f3725b87 Mon Sep 17 00:00:00 2001 From: krisf Date: Fri, 9 Oct 2026 15:42:56 -0400 Subject: [PATCH] ps auto-service: report + math-fix queue watcher --- PS-AUTO-SERVICE-REPORT.md | 77 +++++++++++++++++++++++++++++++++++++ scripts/math-fix-watcher.sh | 17 ++++++++ 2 files changed, 94 insertions(+) create mode 100644 PS-AUTO-SERVICE-REPORT.md create mode 100644 scripts/math-fix-watcher.sh diff --git a/PS-AUTO-SERVICE-REPORT.md b/PS-AUTO-SERVICE-REPORT.md new file mode 100644 index 0000000..f887743 --- /dev/null +++ b/PS-AUTO-SERVICE-REPORT.md @@ -0,0 +1,77 @@ +# PS Auto-Service — Years of Pensionable Service, Derived Automatically + +**Date:** 2026-10-09 · **Plan:** `.hermes/plans/2026-10-09_ps-service-auto.md` · **Commit:** `807a966` (`ps: auto-derive years of service from join year + retirement age`, pushed to `origin/main`) + +The "Years of pensionable service" quiz step is now **prefilled by calculation** from the join year, +current age, and retirement age. The user never has to type it; typing becomes an explicit, visible +**override** with a one-click "Use calculated" reset — mirroring the CPP "Use maximum" pattern. + +## Derivation + +``` +yearsWorkedSoFar = CURRENT_YEAR − psJoinYear +ageAtJoin = age − yearsWorkedSoFar +serviceYears = retireAge − ageAtJoin (rounded, clamped to 0…60) +``` + +- `CURRENT_YEAR = new Date().getFullYear()` (2026), injectable for tests. +- Example: age 45, joined 2005, retiring at 65 → joined at 24 → **41 years**. +- Engine contract unchanged: `psCompute` feeds the **effective** value (override when set, auto + otherwise) to `computePSPension`, which applies its own 35-year cap → 41 → `serviceCapped` 35. +- Invalid/partial inputs (no join year, retireAge ≤ age-at-join, junk) → `null` → step falls back + to the plain manual entry, Next disabled until a valid number exists. + +## Changes + +| File | Change | +|---|---| +| `site/engine/pspension.js` | New exported `autoServiceYears(age, joinYear, retireAge, currentYear)` — guards (`1900…2999`, positive age/retire), rounds, clamps 0…60. | +| `site/app.js` | `state.a.psServiceOverride` (new; `psServiceYears` kept as legacy). `load()` migration: legacy saved `psServiceYears` → override (old plans keep their exact numbers). Helpers `psAutoServiceYears` / `psServiceEffective` / `psOverrideActive`. `psCompute` uses the **effective** value (the integration trap the plan flagged — engine reads `state.a.psServiceYears` directly via `psCompute`). ps_service step: prefills from effective value, `set()` writes only the override, min 0 / max 60, derivation hint EN+FR. Real-browser live swap on typing: `#ps-hint` text + `#ps-svc-reset` wrapper (always in DOM, `display` toggled) so the override hint and "Use calculated" button react without a re-render; clicking it clears the override and restores the auto prefill. | +| `site/i18n.js` | `q.ps_service.auto` / `.override` / `.useCalc` in EN and FR (parity + placeholder checks pass; FR goes through `frNorm`). | +| `site/style.css` | `.link` button style (bold blue underlined text button). | +| `tests/ps-auto-service.test.js` | **New**, 21 checks: derivation cases, future join → 0, retire ≤ join age → 0, invalid → null, clamp 60, string inputs, `currentYear` default, engine contract 41→35 and the 49-year plan example. | +| `tests/ui.smoke.js` | `runPsQuiz` rewritten: untouched auto prefill (`value="41"`, derivation hint EN+FR, Next enabled, reset hidden) → results card 35 years / 59 508,05 → **real route** (results → Edit answers → 6× Back) → type 30 → override hint + visible reset → results 30 years / 51 006,90 → "Use calculated" → auto restored → results 35 / 59 508,05; legacy-migration check in `runPs` (saved `psServiceYears: 30` → `psServiceOverride: 30`). Shim extensions: `#f1.value` synced from the rendered `value="…"` attribute (real DOM behaviour; a stale shim value let a slider's init wipe `retireAge` to 0), `.opt` registry. | +| `audit/ps_auto_service.py`, `audit/ps_auto_offlan.py`, `audit/ps-auto-service/`, `audit/ps-auto-offlan-check.json` | New live audits + evidence. | + +## Test results (all green) + +| Suite | Result | +|---|---| +| `tests/engine.test.js` | 36 pass, 0 fail | +| `tests/i18n.test.js` | 48 checks, I18N-OK | +| `tests/ps-pension.test.js` | 70 pass, 0 fail | +| `tests/ps-auto-service.test.js` | **21 pass, 0 fail (new)** | +| `tests/ui.smoke.js` | **128 checks, SMOKE-OK** (was 92; +36) | + +## Live Playwright audit (local server, 390×844, real Chromium) + +`audit/ps_auto_service.py` → **34 checks, 0 failed, PS-AUTO-AUDIT-OK**; screenshots + JSON in +`audit/ps-auto-service/`. Verified end-to-end: prefill 41 untouched, derivation hints EN+FR, real +typing of 30 **live-swaps** the hint and reveals "Use calculated"/"Utiliser le calcul", use-calc +restores 41 + clears the override in localStorage, results show 30 years → 51,006.90 and auto → +35 years → 59,508.05 (FR: 30 années / 51 006,90 $, 35 années / 59 508,05 $), module OFF parity +(15 steps, PS card hidden, no PS quiz text), no horizontal overflow and all controls + ps-card +inside the 390px shell on every audited screen. + +Two real-browser bugs this audit caught that the shim could not: +1. Typing did not update the hint (no re-render in a browser) → live swap added in `oninput`. +2. The reset button is now always in the DOM, hidden via inline `display` — visibility is the + tested contract, not presence. + +## Deploy + +- `scripts/deploy.sh` → **DEPLOY-OK** (marker `retire-planner v1` served). +- LAN `http://192.168.0.123:8103/`: marker present; `app.js`, `i18n.js`, `engine/pspension.js`, + `style.css` all **byte-identical** to the workspace (sha256). +- Public `https://retire.krisforbes.ca/` from the Midhurst vantage (paramiko, `audit/ps_auto_offlan.py`): + 200 + marker + `psServiceOverride`/`ps-usecalc` in app.js + FR auto hint in i18n.js + + `autoServiceYears` in engine → **OFFLAN-OK** (`audit/ps-auto-offlan-check.json`, 2026-10-09T19:41Z). + +## Compatibility notes + +- Old saved plans (pre-change `psServiceYears: 30`) migrate to `psServiceOverride: 30` on load → + identical numbers to before this change. +- Engine 35-year cap, G1/G2 tables, bridge benefit, IT grid: untouched. +- Module OFF path (15 steps) untouched. + +PS_AUTO_SERVICE_DONE diff --git a/scripts/math-fix-watcher.sh b/scripts/math-fix-watcher.sh new file mode 100644 index 0000000..b7c748c --- /dev/null +++ b/scripts/math-fix-watcher.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Queue watcher: launch the engine-math-fixes DSH job once no dsh-run is active +# and the retire-planner tree is clean; retries on DSH_QUEUE_BUSY. +cd /home/ubuntu/projects/retire-planner || exit 1 +LOG=/home/ubuntu/projects/retire-planner/dsh-math-fix.log +PROMPT='Read /home/ubuntu/projects/.hermes/plans/2026-10-09_engine-math-fixes-fire40.md FIRST — it is the complete spec for the engine math fixes (GIS age-65 floor, OAS clawback only when OAS payable, couple mixed-phase row footnote, FIRE retirement age 40 with fillDefaults guard). Execute it end-to-end including every acceptance check and the deploy. The user pre-approved every step: ask ZERO questions. When done, MATH-FIX-REPORT.md must exist in the workspace root and your FINAL output line must be exactly: MATH_FIX_DONE (or DSH_QUEUE_BUSY if the git tree was dirty at start).' +for i in $(seq 1 240); do + if pgrep -f "dsh-run" >/dev/null; then sleep 60; continue; fi + if ! git diff --quiet || [ -n "$(git status --porcelain --untracked-files=normal | grep -v '^?? ')" ]; then sleep 60; continue; fi + echo "[watcher] launching math-fix job (iteration $i)" | tee -a "$LOG" + ~/.local/bin/dsh-run /home/ubuntu/projects/retire-planner "$PROMPT" >> "$LOG" 2>&1 + rc=$? + if grep -q "DSH_QUEUE_BUSY" "$LOG"; then echo "[watcher] queue busy, retrying" | tee -a "$LOG"; sleep 60; continue; fi + echo "[watcher] math-fix job finished rc=$rc" | tee -a "$LOG" + exit $rc +done +echo "[watcher] gave up after 240 minutes" | tee -a "$LOG"; exit 1