From ad4f5c04f0e5c4b21f41c22902b900b96fe9768a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 8 Aug 2026 00:37:41 +0200 Subject: [PATCH] feat(mood): add the five level-A columns to the mood entry - migration 0310 adds mood_a1, stress_a2, energy_a3, connection_a4 and stability_a5, all nullable, no table rewrite - the schema comment states the four things a later reader needs: score keeps its 1-5 axis, the columns are queryable plaintext on purpose, stress is stored literally, and a read delegate sees them by design - the backup guard is red until the carriers land alongside --- .../migration.sql | 21 +++++++++++ prisma/schema.prisma | 35 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 prisma/migrations/0310_mood_level_a_dimensions/migration.sql diff --git a/prisma/migrations/0310_mood_level_a_dimensions/migration.sql b/prisma/migrations/0310_mood_level_a_dimensions/migration.sql new file mode 100644 index 000000000..173bc03f2 --- /dev/null +++ b/prisma/migrations/0310_mood_level_a_dimensions/migration.sql @@ -0,0 +1,21 @@ +-- v1.37 — the five level-A self-state values on a mood entry. +-- +-- A mood entry has carried one 1-5 value since the first release. That value +-- keeps its scale and its meaning; these five ride beside it, each 0-10 with +-- its own anchors: pleasantness, stress, energy, connectedness and stability. +-- +-- All five are nullable, which is what makes this an additive change rather +-- than a table rewrite, and nullable is also the honest shape: only the first +-- one can be derived from what a row already carries, and the other four stay +-- empty until somebody answers them. A default would fill a hundred thousand +-- historical rows with a number nobody chose. +-- +-- No CHECK constraint on the range. Bounds are enforced server-side, the same +-- way `mood_entry_tag_links.rating` documents: a future change to the scale +-- must not strand rows written under the old one. +ALTER TABLE "mood_entries" + ADD COLUMN "mood_a1" INTEGER, + ADD COLUMN "stress_a2" INTEGER, + ADD COLUMN "energy_a3" INTEGER, + ADD COLUMN "connection_a4" INTEGER, + ADD COLUMN "stability_a5" INTEGER; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fa1aeaa28..169a853d9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -4016,6 +4016,41 @@ model MoodEntry { date String // YYYY-MM-DD — anchored to `tz` when not null, else legacy Europe/Berlin mood String // SUPER_GUT, GUT, OKAY, SCHLECHT, LAUSIG score Int // 1-5 + /// v1.37 — the five level-A self-state values, each 0-10 with its own + /// anchors. `MOOD_DIMENSIONS` (`src/lib/mood/dimensions.ts`) is the one + /// table binding a column to its label, its anchors and its orientation. + /// + /// `score` above stays the 1-5 legacy axis and is not re-scaled: eleven + /// surfaces derive numbers from it on that assumption, three of them into + /// prose an assistant then states as fact. The two axes coexist by design. + /// `moodA1` is derived from the five-point label on every write, so a quick + /// check-in is a full entry rather than a degraded one; A2 to A5 are only + /// ever set by hand, and stay NULL when nobody answered them. Absence reads + /// as absence — a fabricated middle value would poison both the trend and + /// any later model trained on these columns. + /// + /// Queryable plaintext, deliberately — the same trade `CycleDayLog` + /// documents at its `basalBodyTempC`. Encrypting them would remove the + /// trend read, the factor crosstab and every aggregate at once, and the + /// values are self-ratings on a fixed scale rather than prose. The free-text + /// note beside them stays encrypted, because that is where the prose is. + /// + /// `stressA2` is stored literally, exactly as the user set it against the + /// anchors they read, so a higher value means more stress and a worse day. + /// A surface that needs "up is better" flips the sign through the `inverse` + /// flag on the dimension table, never inline. The same rule already governs + /// `MoodTag.inverse`, and it exists because a stored value that disagrees + /// with the label the user saw cannot be recovered afterwards. + /// + /// A delegate holding a READ grant sees these columns, because a grant is + /// whole-record by design and carries no per-domain scope. That is + /// deliberate, not an oversight: withholding part of a record from a + /// delegate would be a new capability, not a field select. + moodA1 Int? @map("mood_a1") // 0-10, pleasantness + stressA2 Int? @map("stress_a2") // 0-10, inverse-oriented + energyA3 Int? @map("energy_a3") // 0-10 + connectionA4 Int? @map("connection_a4") // 0-10 + stabilityA5 Int? @map("stability_a5") // 0-10 tags String? // JSON array of tag keys /// v1.4.30 — first-class free-text note. Replaces the /// `tags: ["note:"]` workaround the iOS app shipped through