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",