From c29055be1c0eef047fe960f261127a70b6fd2f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 31 Jul 2026 18:03:53 +0200 Subject: [PATCH] Carry the configured flag onto the snapshot and digest wires The dashboard snapshot copies it off the composite, the daily digest copies it off the snapshot, and the derived-metric payload already carries it inside the composite value. Optional on both DTOs like the score identity fields beside it, so older cached bodies stay valid. The digest's score object is additionalProperties:false and had never declared deltaReason, scoreVersion or composition, all of which it has been sending. A generated strict decoder would have rejected the real payload, so they are declared here too rather than left standing next to a field that documents itself correctly. --- docs/api/openapi.yaml | 30 ++++++++++++++++++++- src/lib/daily/digest.ts | 6 +++++ src/lib/daily/load-digest.ts | 1 + src/lib/dashboard/snapshot.ts | 12 +++++++++ src/lib/openapi/routes/daily.ts | 31 +++++++++++++++++++++- src/lib/openapi/routes/insights/schemas.ts | 6 +++++ 6 files changed, 84 insertions(+), 2 deletions(-) diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index e1c32c136..0e779b136 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -28112,6 +28112,13 @@ components: - WELLBEING - FITNESS - LIPIDS + configured: + description: "True when the account's own recipe narrows the score's composition below what its defaults would resolve + to today. Server-resolved, so a client never interprets a configuration blob: an account that kept + every pillar reads false, and so does one whose disabled modules alone narrow the set. The + configuration itself is never on this wire. Optional so older cached snapshots without the field + stay valid." + type: boolean deltaReason: anyOf: - type: string @@ -35262,13 +35269,34 @@ components: anyOf: - type: number - type: "null" + configured: + description: True when the account's own recipe narrows the score's composition below what its defaults would resolve to + today. Resolved on the server and carried straight from the dashboard snapshot; the configuration + itself never rides this wire. Optional so older cached digests without the field stay valid. + type: boolean + deltaReason: + description: Why a delta was suppressed; null when it was not. + anyOf: + - type: string + - type: "null" + scoreVersion: + description: Algorithm version that produced the value. + type: integer + exclusiveMinimum: 0 + maximum: 9007199254740991 + composition: + description: Registry-ordered pillar ids the value was composed of. + type: array + items: + type: string required: - value - band - delta additionalProperties: false - type: "null" - description: Health score + band + week-over-week delta; null when none. + description: Health score + band + week-over-week delta, plus whether the account authored the composition; null when + none. topSignal: anyOf: - type: object diff --git a/src/lib/daily/digest.ts b/src/lib/daily/digest.ts index 1b6f3039b..61696103d 100644 --- a/src/lib/daily/digest.ts +++ b/src/lib/daily/digest.ts @@ -105,6 +105,12 @@ export interface DailyDigestScore { deltaReason?: ScoreDeltaReason | null; scoreVersion?: number; composition?: ScorePillarId[]; + /** + * v1.35.0 — the resolved "this score is configured" flag, carried + * straight from the snapshot's health-score block. Omitted by older + * cached digests, like its siblings above. + */ + configured?: boolean; } /** A broken integration, deterministically derived from `IntegrationStatus`. */ diff --git a/src/lib/daily/load-digest.ts b/src/lib/daily/load-digest.ts index 7864b11a0..7c60aa662 100644 --- a/src/lib/daily/load-digest.ts +++ b/src/lib/daily/load-digest.ts @@ -431,6 +431,7 @@ export async function loadDailyDigest( deltaReason: snapshot.healthScore.deltaReason, scoreVersion: snapshot.healthScore.scoreVersion, composition: snapshot.healthScore.composition, + configured: snapshot.healthScore.configured, } : null; diff --git a/src/lib/dashboard/snapshot.ts b/src/lib/dashboard/snapshot.ts index de3438d24..108a8a41e 100644 --- a/src/lib/dashboard/snapshot.ts +++ b/src/lib/dashboard/snapshot.ts @@ -329,6 +329,17 @@ export interface DashboardSnapshotHealthScore { delta: number | null; confidence?: DerivedConfidence; composition?: ScorePillarId[]; + /** + * v1.35.0 — the resolved "this score is configured" flag: true when the + * person's own recipe narrows the composition below what the account's + * defaults would resolve to today. Server-resolved so the hero, the + * widget and the complication acknowledge an authored recipe without + * interpreting a config blob; the blob itself never rides this wire, + * and neither does any per-pillar config detail. Optional on the type + * (additive contract) so older cached snapshots stay valid; the live + * builder always sets it. + */ + configured?: boolean; deltaReason?: ScoreDeltaReason | null; scoreVersion?: number; bandSetter?: ScorePillarId | null; @@ -792,6 +803,7 @@ async function buildExtras( delta: scoreResult.delta, confidence: scoreResult.composite.confidence, composition: scoreResult.composite.value.composition, + configured: scoreResult.composite.value.configured, deltaReason: scoreResult.deltaReason, scoreVersion: scoreResult.scoreVersion, bandSetter: scoreResult.composite.value.bandSetter, diff --git a/src/lib/openapi/routes/daily.ts b/src/lib/openapi/routes/daily.ts index 72213df18..dd4075f4f 100644 --- a/src/lib/openapi/routes/daily.ts +++ b/src/lib/openapi/routes/daily.ts @@ -69,9 +69,38 @@ const dailyDigestResponse = z value: z.number(), band: z.string(), delta: z.number().nullable(), + configured: z + .boolean() + .optional() + .describe( + "True when the account's own recipe narrows the score's composition below what its defaults would resolve to today. Resolved on the server and carried straight from the dashboard snapshot; the configuration itself never rides this wire. Optional so older cached digests without the field stay valid.", + ), + // The score's identity fields have ridden this wire since the + // digest first carried a score, but the object is + // `additionalProperties: false` and never declared them — a + // generated strict decoder would have rejected the real payload. + // Declared here rather than left standing beside a new field + // that documents itself correctly. + deltaReason: z + .string() + .nullable() + .optional() + .describe("Why a delta was suppressed; null when it was not."), + scoreVersion: z + .number() + .int() + .positive() + .optional() + .describe("Algorithm version that produced the value."), + composition: z + .array(z.string()) + .optional() + .describe("Registry-ordered pillar ids the value was composed of."), }) .nullable() - .describe("Health score + band + week-over-week delta; null when none."), + .describe( + "Health score + band + week-over-week delta, plus whether the account authored the composition; null when none.", + ), topSignal: z .object({ sourceMetric: z.string(), diff --git a/src/lib/openapi/routes/insights/schemas.ts b/src/lib/openapi/routes/insights/schemas.ts index c6b152647..bd7aae6f1 100644 --- a/src/lib/openapi/routes/insights/schemas.ts +++ b/src/lib/openapi/routes/insights/schemas.ts @@ -1109,6 +1109,12 @@ export const dashboardSnapshotResponse = z ]), ) .optional(), + configured: z + .boolean() + .optional() + .describe( + "True when the account's own recipe narrows the score's composition below what its defaults would resolve to today. Server-resolved, so a client never interprets a configuration blob: an account that kept every pillar reads false, and so does one whose disabled modules alone narrow the set. The configuration itself is never on this wire. Optional so older cached snapshots without the field stay valid.", + ), deltaReason: z .enum([ "algorithm_changed",