Skip to content

Extract SustainedConditionTracker: FastDrain / SlowCharge share one sustained-condition engine (#163)#190

Merged
almothafar merged 2 commits into
masterfrom
refactor/163-sustained-condition-core
Jul 18, 2026
Merged

Extract SustainedConditionTracker: FastDrain / SlowCharge share one sustained-condition engine (#163)#190
almothafar merged 2 commits into
masterfrom
refactor/163-sustained-condition-core

Conversation

@almothafar

Copy link
Copy Markdown
Owner

What

FastDrainDetector (#109) and SlowChargeDetector (#123) were structural twins — the persisted streak, the observation-gap lapse rule, sleep on an unmeasurable reading, re-arm hysteresis, the persist-only-on-change churn guard, and the load/save/clear boilerplate were duplicated near line-for-line (~150 lines that had to be kept in lock-step by hand).

New SustainedConditionTracker owns that machinery once:

  • generic Streak / Outcome records + a pure decide() core (streak-start, lapse, sustained-window)
  • the condition is pluggable — the caller passes a boolean "active" (ratePph >= limit vs milliwatts < floor)
  • the repeat policy is pluggable — fireOnce() (slow charge: one warning per session) vs withReminders(activelyUsed, gap) (fast drain: first alert always, then remind while the screen is off/locked)
  • StreakStore for persistence, constructed with each detector's own preference keys

Each detector keeps a thin, rate/power-oriented decide() wrapper — its behaviour contract — that just picks the condition + policy and delegates. evaluate() uses the shared StreakStore.

Safety / acceptance

  • Prefs keys unchanged → no in-progress episode lost on upgrade. There's a direct test that loads state written under the legacy _fast_drain_* keys.
  • Byte-for-byte behaviour-identical: the existing FastDrainDetectorTest (14) and SlowChargeDetectorTest (11) port over with only type/accessor renames (FastDrainState/SlowChargeState → shared Streak; .streakStart().start(), .lastSeenAbove()/.lastSeenBelow().lastSeen()) — every scenario and assertion is unchanged.
  • New SustainedConditionTrackerTest (9) covers the generic engine (sleep / re-arm / lapse / both policies) and StreakStore (round-trip, reminder-less variant, churn-guarded clear, legacy-key load).
  • Charging: session-relative drop detection for the slow-charge warning (v2 of #123) #132 unblocked: it can add its session-relative trigger as a second condition on the shared core instead of a third copy.

Pure refactor, no user-facing change (no new strings). testDebugUnitTest (429) + lintDebug green; debug build installed on the Mate 10 Pro.

Closes #163
Related: #109, #123, #132.

🤖 Generated with Claude Code

almothafar and others added 2 commits July 18, 2026 20:50
…ine (#163)

FastDrainDetector (#109) and SlowChargeDetector (#123) were structural twins:
persisted streak state, the observation-gap lapse rule, sleep-on-unmeasurable,
re-arm-on-clear hysteresis, the persist-only-on-change churn guard, and the
load/save/clear boilerplate were duplicated near line-for-line — ~150 lines that
had to be kept in lock-step by hand.

New SustainedConditionTracker owns that machinery once:
- generic Streak / Outcome records + a pure decide() core (streak-start, lapse,
  sustained-window, fire-once)
- pluggable condition (caller passes "active": drain >= limit vs power < floor)
- pluggable RepeatPolicy: fireOnce() (slow charge) vs withReminders() (fast
  drain reminds while the screen is off/locked)
- StreakStore for persistence, keyed by each detector's own preference keys

Each detector keeps a thin, rate/power-oriented decide() wrapper (its behaviour
contract), delegating the mechanics to the shared core. Prefs keys unchanged, so
no in-progress episode is lost on upgrade. The existing decision tests port over
with only type/accessor renames — behaviour identical — and a new
SustainedConditionTrackerTest covers the engine + StreakStore (incl. loading
state written under the legacy keys). #132 can now add its trigger on the shared
core instead of copying the pattern a third time.

Closes #163

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…local (#163)

Per review feedback: decide()/StreakStore signatures put each parameter on its
own line (4+ params), method parameters drop `final` (kept on locals), and the
inline condition expression is a named local (conditionActive = ratePph >= limit
/ milliwatts < floorMw) so its meaning is clear at the call site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@almothafar
almothafar merged commit b8725f7 into master Jul 18, 2026
2 checks passed
@almothafar
almothafar deleted the refactor/163-sustained-condition-core branch July 18, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DRY: extract the shared sustained-condition core from FastDrainDetector / SlowChargeDetector (before #132 adds a third copy)

1 participant