mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
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:
1 parent
793602f464
commit
81096b9553
20 files changed
+1100
-43
No files matched your search
@@ -83,6 +83,23 @@ export function getScoreForMood(mood: string): number {
|
||||
// Bounded so a single create can't fan out an unbounded link set.
|
||||
const structuredTagKeys = z.array(z.string().max(60)).max(30);
|
||||
|
||||
// v1.12.0 — rated mood factors. A factor is a catalog `MoodTag` of
|
||||
// `kind = 'RATED'`; the user scores it per entry, and the score persists
|
||||
// on `MoodEntryTagLink.rating`. The wire shape is a parallel array to
|
||||
// `tagKeys` (binary), keeping the binary contract byte-identical and the
|
||||
// iOS Codable model simple (`[{ key, rating }]`).
|
||||
//
|
||||
// The Zod `rating` bound here is the OUTER envelope (1..5 covers every
|
||||
// seeded factor's scale). The REAL gate is per-tag: after resolving each
|
||||
// key to its `MoodTag`, the server rejects a rating outside the tag's own
|
||||
// `scaleMin..scaleMax` (e.g. 1..2 for `factor_conflict`). See
|
||||
// `resolveRatedFactors` in `src/lib/mood/tag-links.ts`.
|
||||
const ratedFactor = z.object({
|
||||
key: z.string().max(60),
|
||||
rating: z.number().int().min(1).max(5),
|
||||
});
|
||||
const ratedFactors = z.array(ratedFactor).max(30);
|
||||
|
||||
export const createMoodEntrySchema = z.object({
|
||||
mood: moodLevelEnum,
|
||||
tags: z.array(z.string().max(50)).max(20).optional(),
|
||||
@@ -91,6 +108,11 @@ export const createMoodEntrySchema = z.object({
|
||||
// unknown keys are dropped silently (the catalog is the source of
|
||||
// truth, a stale client can't mint a tag).
|
||||
tagKeys: structuredTagKeys.optional(),
|
||||
// v1.12.0 — rated factors scored 1..5 (or the factor's own scale).
|
||||
// Parallel to the binary `tagKeys`; persisted on
|
||||
// `MoodEntryTagLink.rating`. Out-of-scale or non-RATED keys are
|
||||
// rejected (422) / dropped server-side per the catalog.
|
||||
ratedFactors: ratedFactors.optional(),
|
||||
// v1.4.30 H-5 — first-class free-text note. Replaces the
|
||||
// `tags: ["note:<text>"]` workaround. Capped at 500 chars so the
|
||||
// Coach evidence shelf renders cleanly without truncating chips.
|
||||
@@ -105,6 +127,9 @@ export const updateMoodEntrySchema = z.object({
|
||||
// v1.8.5 — full replacement of the structured-tag set when present.
|
||||
// `null` clears every link; omit to leave links untouched.
|
||||
tagKeys: structuredTagKeys.nullable().optional(),
|
||||
// v1.12.0 — full replacement of the rated-factor set when present.
|
||||
// `null` clears every rated link; omit to leave them untouched.
|
||||
ratedFactors: ratedFactors.nullable().optional(),
|
||||
note: z.string().max(500).nullable().optional(),
|
||||
moodLoggedAt: z.iso
|
||||
.datetime({ offset: true })
|
||||
|
||||
Reference in new issue
Block a user