From c6e3ac69e860188a6d2909e1abaa4f42462ee983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 10 May 2026 11:03:52 +0200 Subject: [PATCH] fix(achievements): cap consistentMonthCount and clarify mood-improvement window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit consistentMonthCount kept growing every calendar month with ≥25 active days even though the only achievement it backs unlocks at target 1. The unbounded count served no UI purpose, churned the DB on every poll, and inflated the wire payload over time. Cap at 1 once any month hits the threshold. Also rewrites the moodImprovementHit doc-comment to accurately describe the sliding-window semantics. The code slides over distinct logged days, not 7 calendar days, so a user with sparse logging compares the 7 most-recent logs against the 7 before that regardless of calendar gap. The previous comment claimed calendar-day windowing which the code never implemented. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/gamification/expansion-metrics.ts | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/lib/gamification/expansion-metrics.ts b/src/lib/gamification/expansion-metrics.ts index 899135d72..8216649c5 100644 --- a/src/lib/gamification/expansion-metrics.ts +++ b/src/lib/gamification/expansion-metrics.ts @@ -89,10 +89,14 @@ export function countMeasurementsByType( /** * Mood metrics. The longest day-streak is computed off the unique * Berlin-local `date` strings already stored on `MoodEntry`. The - * "improvement hit" is a one-shot boolean: did *any* 7-day window - * have a mean score at least 1.0 higher than the preceding 7-day - * window? We intentionally use the user's own baseline (no global - * comparison) so the badge is non-coercive. + * "improvement hit" is a one-shot boolean: did *any* contiguous run + * of 7 logged days have a mean score at least 1.0 higher than the + * preceding 7 logged days? The window slides over distinct logged + * days, not calendar days — so a user who logs daily picks up the + * achievement on a true 7d-vs-7d comparison; a user with sparse + * logging compares the 7 most-recent logs against the 7 before that + * regardless of calendar gap. We intentionally use the user's own + * baseline (no global comparison) so the badge is non-coercive. */ export function getMoodMetrics(entries: MoodEntryRecord[]): { moodEntryCount: number; @@ -174,8 +178,11 @@ export function getEngagementMetrics(input: { ).sort(); const entryDayStreak = calculateLongestStreak(uniqueDays); - // consistent-month: count Berlin-local calendar months that have ≥25 - // distinct active days. + // consistent-month: did any Berlin-local calendar month have ≥25 + // distinct active days? Effectively boolean — the only achievement + // this metric backs unlocks at target 1, so once a user hits one + // consistent month the count stops growing. Caps the wire-payload + // and avoids unbounded recomputation cost on long-tenured users. const monthBuckets = new Map>(); for (const day of uniqueDays) { const month = day.slice(0, 7); // YYYY-MM @@ -185,7 +192,10 @@ export function getEngagementMetrics(input: { } let consistentMonthCount = 0; for (const days of monthBuckets.values()) { - if (days.size >= 25) consistentMonthCount += 1; + if (days.size >= 25) { + consistentMonthCount = 1; + break; + } } // weekend-warrior: count of consecutive (Saturday + Sunday) pairs