Add the per-user health-score composition column

A nullable JSONB column on users carrying which pillars the person
chose to count toward the composite score. Stored as a deselection
list so a pillar shipped later counts by default and cannot be
confused with one the person removed. NULL keeps its own meaning:
never chose, which is not the same as counting nothing.
This commit is contained in:
Marc-André Bombeck committed 2026-07-31 15:01:23 +02:00
1 parent 63b54587a8
commit c574ef78fb
2 files changed
+59

No files matched your search

@@ -0,0 +1,35 @@
-- v1.35.0 — per-user Health Score composition.
--
-- A single nullable JSONB column on `users` carrying which pillars the
-- person chose to count toward the composite Health Score. Follows the
-- established per-user JSON-column convention (`module_preferences_json`,
-- `dashboard_widgets_json`, `coach_prefs_json`).
--
-- Shape (validated by `src/lib/analytics/score/config.ts`):
-- { "excludedPillars": ["SLEEP", "LIPIDS"],
-- "version": 3,
-- "changedAt": "2026-07-31T09:12:44.000Z" }
--
-- The stored list is a DESELECTION list, the same posture as
-- `module_preferences_json`, and it is stored that way on purpose. A
-- pillar shipped after the person made their choice is simply absent
-- from the list, so it counts by default and the score keeps its
-- "everything eligible" starting point without a backfill. Storing the
-- positive selection instead would make a newly shipped pillar
-- indistinguishable from one the person deliberately removed.
--
-- NULL = the person never chose, which is NOT the same as choosing to
-- count nothing (an `excludedPillars` list naming every pillar). The
-- resolver keeps those two apart because only the first one may inherit
-- a later change of defaults.
--
-- `version` is the per-user recipe version: it increments on every
-- accepted write and, with `changedAt`, is what a score record and the
-- weekly delta key off to tell "you changed what counts" apart from a
-- real movement in the person's health.
--
-- Additive + nullable: every existing account keeps NULL and the
-- resolver treats NULL as "counts every pillar", which is exactly what
-- those accounts get today.
ALTER TABLE "users" ADD COLUMN "health_score_config_json" JSONB;
+24
View File
@@ -602,6 +602,30 @@ model User {
// `GET /api/auth/me` as a `modules: { <key>: boolean }` map.
modulePreferencesJson Json? @map("module_preferences_json")
// v1.35.0 — which pillars count toward the composite Health Score.
//
// A DESELECTION list, the same posture as `modulePreferencesJson`:
// { "excludedPillars": ["SLEEP"], "version": 2, "changedAt": "<ISO>" }
//
// A pillar shipped after the person made their choice is absent from
// the list and therefore counts, so the score keeps its "everything
// eligible" starting point with no backfill. Storing the positive
// selection instead would make a newly shipped pillar look exactly
// like one the person deliberately removed.
//
// NULL means the person never chose and is deliberately distinct from
// a list naming every pillar ("count nothing"): only the first may
// inherit a later change of defaults.
//
// `version` increments on every accepted write and, with `changedAt`,
// is the per-user recipe identity a score record and the weekly delta
// read to tell an authored change apart from a health movement.
//
// Schema + resolver: `src/lib/analytics/score/config.ts`. Written by
// `PATCH /api/auth/me/health-score-config` with field-by-field merge
// (no mass assignment) under the write-time breadth rule.
healthScoreConfigJson Json? @map("health_score_config_json")
// Document vault — per-user storage-quota override in bytes. NULL = the
// instance default (`AppSettings.documentQuotaBytes`) applies. Admin-set
// only (the admin user PUT); resolved by