feat(mood): add rated mood factors (ingestion + write contract)

Extend the structured mood taxonomy so a tag can carry a magnitude. A
rated factor is a MoodTag of kind RATED that the user scores per entry;
the score persists on MoodEntryTagLink.rating. Binary tags keep
kind BINARY and a null rating, so every existing row and read path stays
byte-identical.

Migration 0119 is additive: four marker columns on mood_tags (kind,
scale_min, scale_max, inverse), a nullable rating on mood_entry_tag_links,
and a seeded factors category with five MVP factors (work, social, sleep
quality, stress, conflict). Stress and conflict are inverse-scaled
(higher means worse); conflict is a 1..2 Yes/No factor.

Write contract: POST /api/mood-entries and the bulk route accept an
optional ratedFactors: [{ key, rating }] array parallel to the binary
tagKeys. The resolver builds the join rows field-by-field, drops unknown
and non-RATED keys silently, and rejects a rating outside the factor's
own scaleMin..scaleMax (422 on the single-entry path, per-entry skipped
on bulk). Create and list responses surface the persisted factors split
from binary keys so a client hydrates without a refetch. GET
/api/mood/tags now emits kind, scaleMin, scaleMax, and inverse so a
client can render factors versus tags.

This is the ingestion half; analytics over rated factors follow later.
Factor labels ship in all six locales.
This commit is contained in:
Marc-André Bombeck committed 2026-06-04 23:09:03 +02:00
1 parent 793602f464
commit 81096b9553
20 files changed
+1100 -43

No files matched your search

+4
View File
@@ -31,7 +31,9 @@ import {
PartyPopper,
Pizza,
Plane,
SlidersHorizontal,
Smile,
Swords,
Tag,
Thermometer,
ThumbsUp,
@@ -84,7 +86,9 @@ const MOOD_TAG_ICONS: Record<string, LucideIcon> = {
PartyPopper,
Pizza,
Plane,
SlidersHorizontal,
Smile,
Swords,
Thermometer,
ThumbsUp,
Trees,