[Review] moodToScore neutral-fallback (returns 3 for unknown moods) silently distorts the mood-trend signal #178

Closed
opened 2026-09-14 14:07:04 +00:00 by Mark · 0 comments
Owner

Finding

src/lib/intimate-state-engine.ts:182-192:

export function moodToScore(mood: string | undefined | null): number | null {
  if (!mood) return null;
  if (MOOD_VALUES[mood] != null) return MOOD_VALUES[mood];
  // ...case-insensitive fallbacks...
  return null;
}

But at line 200 (inside computeMoodTrend):

const scored = interactions
  .filter((i) => i.mood && i.date)
  .map((i) => ({
    date: new Date(i.date),
    mood: i.mood!,
    score: moodToScore(i.mood) ?? 3,    // <-- unknown moods scored as neutral 3
  }))

If a user types a freeform mood the engine doesn't recognize (e.g. "anxious", "exhausted", a non-English word, or a future mood string not in the lookup table), the engine treats it as a neutral 3. Combined with a known mood later in the window, the average gets pulled toward neutral without any signal that an unknown mood was present.

Why this matters

  • A user genuinely in distress, logging "anxiety attack" or "depressed" (neither in MOOD_VALUES), will get a "stable" mood trend because unknown moods score neutral.
  • The 7-day vs 30-day delta calculation runs on these neutral-averaged scores, so the user's actual distress signal is hidden in the data.
  • The moodToScore function returns null for unknowns (correct behavior). The downstream consumer ?? 3 overrides that null with neutral.

Suggested fix

Two viable approaches; pick one and document the choice:

A. Drop unknowns — score: moodToScore(i.mood) (no fallback). Update the filter at line 196 to .filter((i) => i.mood && i.date && moodToScore(i.mood) != null). Unknown moods don't enter the trend. The "stable" direction then reflects only known moods.

B. Score unknowns explicitly as low (1) — preserves the entry's signal that "something negative happened" without claiming the user was neutral. Use ?? 1 instead of ?? 3.

Either way, the schema should also be updated to constrain mood to VaultMoodValue | string (already typed that way in vault-db.ts:42) and the UI should validate against the known list at entry time, so unknown moods only appear via legacy/migration paths.

Severity

Medium. Signal integrity issue. The mood trend is a primary user-facing insight — silently neutering unknown moods makes the feature less trustworthy for users who use non-standard vocab.

Discovered by

Manual code review pass over #168, 2026-09-14.

## Finding `src/lib/intimate-state-engine.ts:182-192`: ```ts export function moodToScore(mood: string | undefined | null): number | null { if (!mood) return null; if (MOOD_VALUES[mood] != null) return MOOD_VALUES[mood]; // ...case-insensitive fallbacks... return null; } ``` But at line 200 (inside `computeMoodTrend`): ```ts const scored = interactions .filter((i) => i.mood && i.date) .map((i) => ({ date: new Date(i.date), mood: i.mood!, score: moodToScore(i.mood) ?? 3, // <-- unknown moods scored as neutral 3 })) ``` If a user types a freeform mood the engine doesn't recognize (e.g. "anxious", "exhausted", a non-English word, or a future mood string not in the lookup table), the engine treats it as a neutral `3`. Combined with a known mood later in the window, the average gets pulled toward neutral without any signal that an unknown mood was present. ## Why this matters - A user genuinely in distress, logging "anxiety attack" or "depressed" (neither in MOOD_VALUES), will get a "stable" mood trend because unknown moods score neutral. - The 7-day vs 30-day delta calculation runs on these neutral-averaged scores, so the user's actual distress signal is hidden in the data. - The `moodToScore` function returns `null` for unknowns (correct behavior). The downstream consumer `?? 3` overrides that null with neutral. ## Suggested fix Two viable approaches; pick one and document the choice: A. **Drop unknowns** — `score: moodToScore(i.mood)` (no fallback). Update the filter at line 196 to `.filter((i) => i.mood && i.date && moodToScore(i.mood) != null)`. Unknown moods don't enter the trend. The "stable" direction then reflects only known moods. B. **Score unknowns explicitly as low (1)** — preserves the entry's signal that "something negative happened" without claiming the user was neutral. Use `?? 1` instead of `?? 3`. Either way, the schema should also be updated to constrain `mood` to `VaultMoodValue | string` (already typed that way in `vault-db.ts:42`) and the UI should validate against the known list at entry time, so unknown moods only appear via legacy/migration paths. ## Severity Medium. Signal integrity issue. The mood trend is a primary user-facing insight — silently neutering unknown moods makes the feature less trustworthy for users who use non-standard vocab. ## Discovered by Manual code review pass over #168, 2026-09-14.
Mark 2026-09-14 16:01:03 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Mark/kimi_agent_kin_app_sync#178
No description provided.