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
This commit is contained in:
Marc-André Bombeck committed 2026-08-08 00:37:41 +02:00
1 parent 9a390aacea
commit ad4f5c04f0
2 files changed
+56

No files matched your search

@@ -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;
+35
View File
@@ -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:<text>"]` workaround the iOS app shipped through