5.6 KiB
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:
psComputefeeds the effective value (override when set, auto otherwise) tocomputePSPension, which applies its own 35-year cap → 41 →serviceCapped35. - 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:
- Typing did not update the hint (no re-render in a browser) → live swap added in
oninput. - 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 (markerretire-planner v1served).- LAN
http://192.168.0.123:8103/: marker present;app.js,i18n.js,engine/pspension.js,style.cssall 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-usecalcin app.js + FR auto hint in i18n.js +autoServiceYearsin engine → OFFLAN-OK (audit/ps-auto-offlan-check.json, 2026-10-09T19:41Z).
Compatibility notes
- Old saved plans (pre-change
psServiceYears: 30) migrate topsServiceOverride: 30on 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