38 lines
2.0 KiB
Markdown
38 lines
2.0 KiB
Markdown
---
|
|
type: decision
|
|
status: shipped
|
|
tags:
|
|
- cpas
|
|
- scoring
|
|
- roll-off
|
|
- mpm
|
|
updated: 2026-05-27
|
|
---
|
|
|
|
# [[CPAS]] roll-off: clean-cycle model, JS as source of truth
|
|
|
|
## Context
|
|
|
|
System was implementing a literal per-violation rolling 90-day window. Jason reported that the handbook actually says "5 points roll off after 90 consecutive days of no violations" — i.e. any new violation should reset the countdown for everyone, not just for itself.
|
|
|
|
## Decision
|
|
|
|
Adopt the clean-cycle model:
|
|
|
|
1. Roll-off amount per cycle: **5 points, oldest first**
|
|
2. Repeat cadence: **another full 90 clean days required** for each successive 5-point roll-off
|
|
3. Reset trigger: **any new non-negated violation** (negated violations don't reset)
|
|
|
|
Implement all standing math in JS (`lib/rolloff.js` `computeStanding()`). Drop the `active_cpas_scores` SQL view — oldest-first partial allocation isn't cleanly SQL-expressible, and having both a SQL and JS implementation invites divergence.
|
|
|
|
## Alternatives considered
|
|
|
|
- Keeping the SQL view with the aggregate formula `total - 5*cycles_since_last_violation` and computing per-violation breakdown only in JS. Rejected: two implementations of the same rule = drift risk; dashboard scale at MPM is small enough that per-request JS computation is fine.
|
|
- "All points roll off at once after 90 clean days" or "oldest entire violation rolls off." Rejected by Jason in clarifying questions.
|
|
|
|
## Consequences
|
|
|
|
- `/api/employees/:id/expiration` response shape changed (was per-violation list, now `{ schedule: [...] }`). Frontend `ExpirationTimeline.jsx` updated to match — any third-party consumer of that endpoint would break.
|
|
- Existing `prior_active_points` snapshots on pre-fix violation rows are stale; existing "Backfill Snapshots" admin button recomputes them under the new model on demand.
|
|
- `recomputeSnapshotsAfter` no longer caps the affected window at +90 days — backdated inserts now scan all later violations, since under the new model an earlier total shift propagates indefinitely.
|