Decay.php 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Domain\Reputation;
  4. /**
  5. * Pure decay math per SPEC §5. Stateless — easy to unit-test against
  6. * hand-computed values.
  7. *
  8. * - Linear : max(0, 1 − age_days / decay_param)
  9. * - Exponential: 0.5 ^ (age_days / decay_param) (half-life)
  10. *
  11. * `age_days` may be fractional. Negative ages (future-dated reports —
  12. * shouldn't happen, but guard anyway) clamp to zero so a fresh report
  13. * always counts at full weight.
  14. */
  15. final class Decay
  16. {
  17. public static function value(DecayFunction $function, float $ageDays, float $decayParam): float
  18. {
  19. if ($decayParam <= 0.0) {
  20. return 0.0;
  21. }
  22. $age = max(0.0, $ageDays);
  23. return match ($function) {
  24. DecayFunction::Linear => max(0.0, 1.0 - ($age / $decayParam)),
  25. DecayFunction::Exponential => 0.5 ** ($age / $decayParam),
  26. };
  27. }
  28. /**
  29. * Inverse of `value()`: roughly how many days until a current score
  30. * decays below `$threshold`, assuming no further reports.
  31. *
  32. * Exponential is exact (the sum scales by 0.5^(Δ/T) regardless of
  33. * report ages). Linear treats the current score as a lump at "now"
  34. * — an upper bound on time-to-threshold; the real curve drops faster
  35. * as individual reports age out. Used by the policy preview to give
  36. * operators an at-a-glance "when does this entry fall off?".
  37. *
  38. * Returns 0.0 if the score is already at or below the threshold,
  39. * `null` if the threshold is zero/negative (entry would never fall
  40. * off naturally).
  41. */
  42. public static function daysUntilThreshold(
  43. DecayFunction $function,
  44. float $decayParam,
  45. float $currentScore,
  46. float $threshold,
  47. ): ?float {
  48. if ($decayParam <= 0.0) {
  49. return 0.0;
  50. }
  51. if ($threshold <= 0.0) {
  52. return null;
  53. }
  54. if ($currentScore <= $threshold) {
  55. return 0.0;
  56. }
  57. return match ($function) {
  58. DecayFunction::Linear => $decayParam * (1.0 - $threshold / $currentScore),
  59. DecayFunction::Exponential => $decayParam * (log($currentScore / $threshold) / log(2.0)),
  60. };
  61. }
  62. }