§ Ember — habit & ritual tracker · The app — stack & data model
The streak engine
A streak sounds trivial ("count days in a row") until you add four cadences, rest days, and grace freezes. All of it lives in engine.js, computed from the completions ledger.
Walking backwards
For per-day cadences (daily / weekday / N-per-day) the engine walks backwards from today:
- Not scheduled today? skip the day entirely (a Mon/Wed/Fri habit isn't "broken" on Tuesday).
- Complete?
streak++. - Skipped (a rest day)? excused — neither counts nor breaks.
- Today, not done yet? don't break — the day's still in progress.
- Missed, but grace left this week? burn a grace day ("banked coals") and keep going.
- Otherwise the streak ends.
grace_per_week refills at each ISO-week boundary as we walk, so "2 freezes a week" means exactly that.
N-per-day and N-per-week
- times_per_day: a day is complete when
count >= n. The check-in increments the count (tap water 6×). - times_per_week: the streak is measured in weeks, not days — a week counts if you hit the target N times within that ISO week. The UI shows the unit ("5 wk").
Why compute, never store
Streaks are derived on every read. That sounds wasteful but it means there's no streak field to corrupt — undo a completion, change a cadence, backfill history, and the streak is simply recomputed correctly. The ledger is the single source of truth.
Tested against seeded history: an 18-day daily streak, a Mon/Wed/Fri streak of 22 (counting only scheduled days), a 5-week times-per-week streak — all verified in the e2e suite.