Background
Split out of #2169 (implemented in #2237) during plan review. #2237 gates the statistics pool on use_for_projections and makes a degenerate limits band evaluate as "unknown", but two pace-factor weaknesses in TimePredictor (app/services/time_predictor.rb) remain:
1. Latent crash: zero typical completed time
measurable_pace? checks truthiness only:
defmeasurable_pace?completed_lap_split.distance_from_start.positive? && actual_completed_time && typical_completed_timeend
typical_completed_time == 0.0 is truthy, so pace_factor = actual_completed_time / typical_completed_time is float division by zero → Infinity, and limits' (limit * pace_factor).to_i raises FloatDomainError (0 * Infinity → NaN does too). Fix: require actual_completed_time&.positive? && typical_completed_time&.positive?, falling back to the existing unmeasurable-pace convention of 1.
2. Unbounded pace factor
In the #2169 incident, a poisoned pool produced pace_factor ≈ 1e-4, collapsing the limits band to zero width so every real time flagged "bad". The pool gating in #2237 makes this far less likely but not impossible (bad data in flag-true events — mis-keyed year, timezone error). Clamping bounds the blast radius:
MIN_PACE_FACTOR=0.1MAX_PACE_FACTOR=10.0(actual_completed_time / typical_completed_time).clamp(MIN_PACE_FACTOR,MAX_PACE_FACTOR)
Legitimate pace factors sit roughly in 0.3–3, so [0.1, 10] never binds on real data; clamp-then-use beats treat-as-unmeasurable because an extreme pace signal still carries direction, just not magnitude.
Suggested specs
Build a predictor with calc_model: :stats and stub SplitTimeQuery.typical_segment_time (arity-preserving idiom already used in segment_time_calculator_spec):
- huge average (anachronistic pool):
data_status(sane_seconds) is not "bad"; pace_factor == 0.1 average: 0.0: no FloatDomainError; status nil; pace_factor == 1- negative average: status nil,
pace_factor == 1
Relates to #2169
🤖 Generated with Claude Code
Background
Split out of #2169 (implemented in #2237) during plan review. #2237 gates the statistics pool on
use_for_projectionsand makes a degenerate limits band evaluate as "unknown", but two pace-factor weaknesses inTimePredictor(app/services/time_predictor.rb) remain:1. Latent crash: zero typical completed time
measurable_pace?checks truthiness only:typical_completed_time == 0.0is truthy, sopace_factor = actual_completed_time / typical_completed_timeis float division by zero →Infinity, andlimits'(limit * pace_factor).to_iraisesFloatDomainError(0 * Infinity→NaNdoes too). Fix: requireactual_completed_time&.positive? && typical_completed_time&.positive?, falling back to the existing unmeasurable-pace convention of1.2. Unbounded pace factor
In the #2169 incident, a poisoned pool produced
pace_factor ≈ 1e-4, collapsing the limits band to zero width so every real time flagged "bad". The pool gating in #2237 makes this far less likely but not impossible (bad data in flag-true events — mis-keyed year, timezone error). Clamping bounds the blast radius:Legitimate pace factors sit roughly in 0.3–3, so [0.1, 10] never binds on real data; clamp-then-use beats treat-as-unmeasurable because an extreme pace signal still carries direction, just not magnitude.
Suggested specs
Build a predictor with
calc_model: :statsand stubSplitTimeQuery.typical_segment_time(arity-preserving idiom already used in segment_time_calculator_spec):data_status(sane_seconds)is not "bad";pace_factor == 0.1average: 0.0: noFloatDomainError; status nil;pace_factor == 1pace_factor == 1Relates to #2169
🤖 Generated with Claude Code