Files
echo/.references/references-only/reference vault/memory/decisions/2026-05-27-cpas-rolloff-clean-cycle-model.md
2026-06-05 00:49:20 -05:00

2.0 KiB

type, status, tags, updated
type status tags updated
decision shipped
cpas
scoring
roll-off
mpm
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.