refactor(diagnostics): split the flap window and episode rule apart
android / Build + lint + test (push) Successful in 4m8s

detekt ReturnCount. Extracting the pruning and the is-this-an-episode
predicate reads better than suppressing it, and the cooldown rule now
has a name and a docstring of its own.
This commit is contained in:
2026-08-18 10:00:43 -04:00
parent 72c0e96f92
commit 0103953953
@@ -33,20 +33,30 @@ class TransportFlapDetector(
*/
fun onChange(observation: TransportObservation): List<TransportObservation>? {
recent.addLast(observation)
while (recent.isNotEmpty() &&
observation.atElapsedMs - recent.first().atElapsedMs > windowMs
) {
recent.removeFirst()
}
if (recent.size < minChanges) return null
// One episode, one summary. A sustained fault would otherwise emit a
// summary per reading and bury the per-change events underneath them.
val since = lastSummaryAtMs
if (since != null && observation.atElapsedMs - since < summaryCooldownMs) return null
dropReadingsOlderThan(observation.atElapsedMs)
if (!isEpisode(observation.atElapsedMs)) return null
lastSummaryAtMs = observation.atElapsedMs
return recent.toList()
}
private fun dropReadingsOlderThan(nowMs: Long) {
while (recent.isNotEmpty() && nowMs - recent.first().atElapsedMs > windowMs) {
recent.removeFirst()
}
}
/**
* Enough changes packed together, and far enough from the last thing we
* wrote down. The cooldown is what keeps one episode to one summary: a
* sustained fault produces a change every poll, and a summary per reading
* would bury the per-change events underneath them.
*/
private fun isEpisode(nowMs: Long): Boolean {
val since = lastSummaryAtMs
val cooled = since == null || nowMs - since >= summaryCooldownMs
return recent.size >= minChanges && cooled
}
/** Forget everything — call when the route changes or casting ends. */
fun reset() {
recent.clear()