From 5b406d58cdd6901496dcb5241a3de7fdac0269ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 26 Jun 2026 12:29:35 +0200 Subject: [PATCH] fix(insights): close the deterministic score assessment with one grounded step The per-score deterministic text named the score, its standing, and the weakest contributor, then stopped at the diagnosis. When the band is not green and the weakest driver is behaviourally addressable (sleep, mood, consistency, timing), append a single grounded pointer drawn from that same contributor. Physiology-only drivers add nothing, so the text affirms and watches rather than manufacturing a step. --- .../__tests__/derived-assessment.test.ts | 35 ++++++++++++++++ .../insights/derived/derived-assessment.ts | 40 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/lib/insights/derived/__tests__/derived-assessment.test.ts b/src/lib/insights/derived/__tests__/derived-assessment.test.ts index dde32e8fc..1fa60c195 100644 --- a/src/lib/insights/derived/__tests__/derived-assessment.test.ts +++ b/src/lib/insights/derived/__tests__/derived-assessment.test.ts @@ -158,6 +158,41 @@ describe("resolveDeterministicAssessment", () => { expect(a!.text).toContain("Schlafmenge"); }); + it("appends a grounded next step when the weakest contributor is addressable (en)", () => { + // sleep (30) is the lowest present contributor → a behaviourally + // addressable driver, so the assessment closes with a doable pointer. + const sleepWeak: ReadinessValue = { + score: 58, + band: "yellow", + components: [ + { key: "rhr", value: 80, weight: 0.3 }, + { key: "hrv", value: 75, weight: 0.3 }, + { key: "sleep", value: 30, weight: 0.25 }, + { key: "mood", value: 70, weight: 0.15 }, + ], + }; + const a = resolveDeterministicAssessment( + "READINESS", + okDerived(sleepWeak), + "en", + NOW, + ); + expect(a!.text).toContain("Held back most by"); + expect(a!.text).toContain("An earlier night would lift this most."); + }); + + it("does NOT manufacture a step when the weakest contributor is physiology-only (en)", () => { + // rhr (40) is the lowest in the base readiness fixture → no pointer. + const a = resolveDeterministicAssessment( + "READINESS", + okDerived(readiness), + "en", + NOW, + ); + expect(a!.text).not.toContain("An earlier night"); + expect(a!.text).not.toContain("most effective lever"); + }); + it("uses the trend for a contributor-less recovery score (en)", () => { const a = resolveDeterministicAssessment( "RECOVERY_SCORE", diff --git a/src/lib/insights/derived/derived-assessment.ts b/src/lib/insights/derived/derived-assessment.ts index f7926008d..0791f9d22 100644 --- a/src/lib/insights/derived/derived-assessment.ts +++ b/src/lib/insights/derived/derived-assessment.ts @@ -247,6 +247,41 @@ function contributorLabel( return key; } +/** + * v1.21.0 (coach C1 MEDIUM-2) — a single grounded next-step pointer keyed to + * the weakest BEHAVIOURALLY ADDRESSABLE contributor. Only the contributors a + * person can actually move (sleep, mood, consistency, timing) carry a + * pointer; physiology-only contributors (rhr, hrv, respiratory) return null, + * so the assessment affirms-and-watches rather than manufacturing a step — + * matching the `base-system.ts` "do NOT manufacture a step" rule. + */ +const CONTRIBUTOR_POINTERS: Record = { + sleep: { + de: "Eine etwas frühere Nacht würde das am ehesten anheben.", + en: "An earlier night would lift this most.", + }, + sufficiency: { + de: "Etwas mehr Schlafzeit ist hier der wirksamste Hebel.", + en: "A little more time asleep is the most effective lever here.", + }, + consistency: { + de: "Gleichmäßigere Schlafzeiten über die Woche helfen am meisten.", + en: "More even sleep and wake times across the week help most.", + }, + timing: { + de: "Ein gleichmäßigerer Rhythmus zieht das am ehesten nach oben.", + en: "A steadier sleep rhythm is the most likely thing to pull this up.", + }, + mood: { + de: "Ein kurzer Moment für etwas, das dir guttut, kann hier spürbar helfen.", + en: "A small moment for something that does you good can noticeably help here.", + }, +}; + +function contributorPointer(key: string, locale: Locale): string | null { + return CONTRIBUTOR_POINTERS[key]?.[locale] ?? null; +} + /** * Compose the deterministic per-score assessment from the score's signal + * its contributors. Always returns a non-empty, factual, non-causal text. @@ -294,6 +329,11 @@ export function buildDeterministicScoreAssessment( ? `Am stärksten gedämpft durch ${joined}.` : `Held back most by ${joined}.`, ); + // v1.21.0 (MEDIUM-2) — close with ONE grounded next step drawn from the + // weakest contributor, but only when it is behaviourally addressable. + // When the weakest driver is physiology-only, no step is manufactured. + const pointer = contributorPointer(ranked[0].key, locale); + if (pointer) sentences.push(pointer); } } else if (signal.delta !== null && signal.delta !== 0) { // No contributor breakdown (recovery/stress/strain): use the trend.