diff --git a/prisma/migrations/0289_health_score_config/migration.sql b/prisma/migrations/0289_health_score_config/migration.sql new file mode 100644 index 000000000..f9d0397fd --- /dev/null +++ b/prisma/migrations/0289_health_score_config/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6cd6fa6a8..6b6fc2ec3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -602,6 +602,30 @@ model User { // `GET /api/auth/me` as a `modules: { : 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": "" } + // + // 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