fix(release): harden v1.31 arrival contracts

This commit is contained in:
Marc-André Bombeck committed 2026-07-20 00:40:20 +02:00
1 parent 44add3bc1e
commit 4591899657
98 files changed
+5560 -1600

No files matched your search

@@ -0,0 +1,165 @@
import { readdirSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import type { CoachScopeSource } from "@/lib/ai/coach/types";
import {
scopeSourceFromMetricKey,
scopeSourceMetricLabelKey,
} from "../coach-metric-scope";
const PROJECT_ROOT = resolve(import.meta.dirname, "../../../..");
const MESSAGE_BUNDLES = ["en", "de"].map((locale) =>
JSON.parse(
readFileSync(join(PROJECT_ROOT, `messages/${locale}.json`), "utf8"),
),
);
function translationAt(bundle: unknown, key: string): unknown {
return key.split(".").reduce<unknown>((current, segment) => {
if (
current == null ||
typeof current !== "object" ||
!(segment in current)
) {
return undefined;
}
return (current as Record<string, unknown>)[segment];
}, bundle);
}
function sourceFilesBelow(directory: string): string[] {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const path = join(directory, entry.name);
if (entry.isDirectory()) return sourceFilesBelow(path);
return entry.name.endsWith(".tsx") ? [path] : [];
});
}
function genericMetricCardInventory(): Set<string> {
const insightRouteSources = sourceFilesBelow(
join(PROJECT_ROOT, "src/app/insights"),
).map((path) => readFileSync(path, "utf8"));
const recoverySource = readFileSync(
join(PROJECT_ROOT, "src/components/insights/recovery/recovery-section.tsx"),
"utf8",
);
const statusMetrics = insightRouteSources.flatMap((source) =>
Array.from(
source.matchAll(/\bstatusMetric\s*=\s*"([A-Z0-9_]+)"/g),
(m) => m[1],
),
);
const directMetrics = insightRouteSources.flatMap((source) =>
Array.from(
source.matchAll(
/<MetricStatusCard\b[\s\S]*?\bmetric\s*=\s*"([A-Z0-9_]+)"[\s\S]*?\/>/g,
),
(m) => m[1],
),
);
const recoveryMetrics = Array.from(
recoverySource.matchAll(/\bstatusMetric\s*:\s*"([A-Z0-9_]+)"/g),
(m) => m[1],
);
return new Set([...statusMetrics, ...directMetrics, ...recoveryMetrics]);
}
/**
* Every generic assessment mount is classified here. A non-null value means
* the Coach snapshot has a source backed by the exact data on that card;
* null records that no matching source exists and prevents a tempting but
* incorrect near-match (for example wrist temperature → skin temperature).
*/
const GENERIC_METRIC_SCOPE_EXPECTATION: Readonly<
Record<string, CoachScopeSource | null>
> = {
ACTIVE_ENERGY: "active_energy",
AUDIO_EXPOSURE_EVENT: "audio_event",
AUDIO_EXPOSURE_ENV: "audio_env",
AUDIO_EXPOSURE_HEADPHONE: "audio_headphone",
BLOOD_GLUCOSE: "glucose",
BODY_TEMPERATURE: "body_temp",
BONE_MASS: "bone_mass",
FAT_FREE_MASS: "fat_free_mass",
FAT_MASS: "fat_mass",
FLIGHTS_CLIMBED: "flights",
HEART_RATE_VARIABILITY: "hrv",
LEAN_BODY_MASS: "lean_body_mass",
MUSCLE_MASS: "muscle_mass",
OXYGEN_SATURATION: "spo2",
PULSE_WAVE_VELOCITY: "pulse_wave_velocity",
RESPIRATORY_RATE: "respiratory_rate",
RESTING_HEART_RATE: "resting_hr",
SKIN_TEMPERATURE: "skin_temp",
SLEEP_DURATION: "sleep",
STEPS: "steps",
TIME_IN_DAYLIGHT: "daylight",
TOTAL_BODY_WATER: "total_body_water",
VASCULAR_AGE: "vascular_age",
VISCERAL_FAT: "visceral_fat",
VO2_MAX: "vo2_max",
WALKING_ASYMMETRY: "walking_asymmetry",
WALKING_DOUBLE_SUPPORT: "walking_double_support",
WALKING_HEART_RATE_AVERAGE: "walking_hr",
WALKING_RUNNING_DISTANCE: "distance",
WALKING_SPEED: "walking_speed",
WALKING_STEADINESS: "walking_steadiness",
WALKING_STEP_LENGTH: "walking_step_length",
ANS_CHARGE: null,
AVERAGE_HEART_RATE: null,
BREATHING_DISTURBANCES: null,
CARDIO_LOAD: null,
CARDIO_RECOVERY: null,
DAY_STRAIN: null,
ENERGY_EXPENDITURE_KJ: null,
FALL_COUNT: null,
GRIP_STRENGTH: null,
MAX_HEART_RATE: null,
PAIN_NRS: null,
SIX_MINUTE_WALK_DISTANCE: null,
STAIR_ASCENT_SPEED: null,
STAIR_DESCENT_SPEED: null,
WAIST_CIRCUMFERENCE: null,
WAIST_TO_HEIGHT: null,
WORKOUT_STRAIN: null,
WRIST_TEMPERATURE: null,
};
describe("generic MetricStatusCard Coach scope inventory", () => {
it("classifies every mounted status metric and scopes every exact Coach source", () => {
const mountedMetrics = genericMetricCardInventory();
expect([...mountedMetrics].sort()).toEqual(
Object.keys(GENERIC_METRIC_SCOPE_EXPECTATION).sort(),
);
for (const [metric, expectedSource] of Object.entries(
GENERIC_METRIC_SCOPE_EXPECTATION,
)) {
expect(scopeSourceFromMetricKey(metric), metric).toBe(expectedSource);
}
});
it("uses an existing localized measurement label for every scoped mount", () => {
const supportedSources = new Set(
Object.values(GENERIC_METRIC_SCOPE_EXPECTATION).filter(
(source): source is CoachScopeSource => source !== null,
),
);
for (const source of supportedSources) {
const labelKey = scopeSourceMetricLabelKey(source);
expect(labelKey, source).not.toBeNull();
for (const messages of MESSAGE_BUNDLES) {
expect(
translationAt(messages, labelKey!),
`${source}: ${labelKey}`,
).toEqual(expect.any(String));
}
}
});
});
+121 -20
View File
@@ -1,5 +1,6 @@
import type { CoachScopeSource, CoachScopeWindow } from "@/lib/ai/coach/types";
import { COACH_SOURCE_DOMAIN_LABEL } from "@/lib/ai/coach/tools/source-keys";
import type { MetricStatusMetricId } from "@/lib/insights/metric-status-registry";
/**
* v1.21.0 (C4 H1/H4) — metric → Coach scope + seed-question map.
@@ -126,32 +127,114 @@ export function metricScopeFromExplainer(
}
/**
* Resolve a recommendation / correlation `metricSource.type` — the
* model's snapshot-key vocabulary ("bloodPressure", "weight", "pulse",
* "mood", "medications.compliance30", "sleep", "steps", …) — to a single
* `CoachScopeSource`, or null when the key has no snapshot block to
* narrow to. Lets a card's "Ask the Coach" affordance pre-scope the
* conversation to the metric the card is about.
* Every generic metric-assessment id is classified here, including the ones
* that intentionally cannot scope the Coach. A null entry means the snapshot
* has no source backed by the exact data assessed on that card; near-matches
* (for example wrist temperature → skin temperature) must stay unscoped.
*/
const STATUS_METRIC_SCOPE_SOURCE = {
RESTING_HEART_RATE: "resting_hr",
HEART_RATE_VARIABILITY: "hrv",
OXYGEN_SATURATION: "spo2",
RESPIRATORY_RATE: "respiratory_rate",
BODY_TEMPERATURE: "body_temp",
SKIN_TEMPERATURE: "skin_temp",
BLOOD_GLUCOSE: "glucose",
WALKING_HEART_RATE_AVERAGE: "walking_hr",
PULSE_WAVE_VELOCITY: "pulse_wave_velocity",
VASCULAR_AGE: "vascular_age",
STEPS: "steps",
ACTIVE_ENERGY: "active_energy",
FLIGHTS_CLIMBED: "flights",
WALKING_RUNNING_DISTANCE: "distance",
TIME_IN_DAYLIGHT: "daylight",
VO2_MAX: "vo2_max",
TOTAL_BODY_WATER: "total_body_water",
BONE_MASS: "bone_mass",
FAT_FREE_MASS: "fat_free_mass",
FAT_MASS: "fat_mass",
MUSCLE_MASS: "muscle_mass",
LEAN_BODY_MASS: "lean_body_mass",
VISCERAL_FAT: "visceral_fat",
WALKING_STEADINESS: "walking_steadiness",
WALKING_ASYMMETRY: "walking_asymmetry",
WALKING_DOUBLE_SUPPORT: "walking_double_support",
WALKING_STEP_LENGTH: "walking_step_length",
WALKING_SPEED: "walking_speed",
AUDIO_EXPOSURE_ENV: "audio_env",
AUDIO_EXPOSURE_HEADPHONE: "audio_headphone",
AUDIO_EXPOSURE_EVENT: "audio_event",
SLEEP_DURATION: "sleep",
CARDIO_RECOVERY: null,
WRIST_TEMPERATURE: null,
FALL_COUNT: null,
SIX_MINUTE_WALK_DISTANCE: null,
STAIR_ASCENT_SPEED: null,
STAIR_DESCENT_SPEED: null,
BREATHING_DISTURBANCES: null,
SLEEP_SCORE: null,
ANS_CHARGE: null,
DAY_STRAIN: null,
WORKOUT_STRAIN: null,
CARDIO_LOAD: null,
AVERAGE_HEART_RATE: null,
MAX_HEART_RATE: null,
ENERGY_EXPENDITURE_KJ: null,
GRIP_STRENGTH: null,
PAIN_NRS: null,
WAIST_CIRCUMFERENCE: null,
WAIST_TO_HEIGHT: null,
} as const satisfies Readonly<
Record<MetricStatusMetricId, CoachScopeSource | null>
>;
/**
* Recommendation cards use the insight snapshot's section-key vocabulary.
* Keep those aliases declarative as well; status-card ids are resolved by the
* exhaustive table above.
*/
const SNAPSHOT_METRIC_SCOPE_SOURCE: Readonly<Record<string, CoachScopeSource>> =
{
bloodpressure: "bp",
blood_pressure: "bp",
weight: "weight",
pulse: "pulse",
resting_hr: "resting_hr",
restinghr: "resting_hr",
hrv: "hrv",
mood: "mood",
sleep: "sleep",
steps: "steps",
activity: "steps",
bloodglucose: "glucose",
blood_glucose: "glucose",
bmi: "bmi",
medication: "compliance",
"medications.compliance": "compliance",
"medications.compliance7": "compliance",
"medications.compliance30": "compliance",
"medications.compliance90": "compliance",
};
/**
* Resolve a generic assessment id or recommendation `metricSource.type` to
* the exact Coach snapshot source it is based on. Unsupported assessment ids
* resolve to null explicitly through `STATUS_METRIC_SCOPE_SOURCE`.
*/
export function scopeSourceFromMetricKey(
metricKey: string | undefined,
): CoachScopeSource | null {
if (!metricKey) return null;
const lower = metricKey.toLowerCase();
if (lower.startsWith("medications.compliance") || lower === "medication") {
return "compliance";
if (
Object.prototype.hasOwnProperty.call(STATUS_METRIC_SCOPE_SOURCE, metricKey)
) {
return STATUS_METRIC_SCOPE_SOURCE[
metricKey as MetricStatusMetricId
] as CoachScopeSource | null;
}
if (lower === "bloodpressure" || lower === "blood_pressure") return "bp";
if (lower === "weight") return "weight";
if (lower === "pulse") return "pulse";
if (lower === "resting_hr" || lower === "restinghr") return "resting_hr";
if (lower === "hrv") return "hrv";
if (lower === "mood") return "mood";
if (lower === "sleep" || lower.startsWith("sleep")) return "sleep";
if (lower === "steps" || lower === "activity") return "steps";
if (lower === "bloodglucose" || lower === "blood_glucose") return "glucose";
if (lower === "bmi") return "bmi";
return null;
return SNAPSHOT_METRIC_SCOPE_SOURCE[metricKey.toLowerCase()] ?? null;
}
/**
@@ -186,11 +269,14 @@ const SCOPE_SOURCE_METRIC_LABEL_KEY: Partial<Record<CoachScopeSource, string>> =
pulse: "measurements.typePulse",
hrv: "measurements.typeHeartRateVariability",
resting_hr: "measurements.typeRestingHeartRate",
walking_hr: "measurements.typeWalkingHeartRateAverage",
respiratory_rate: "measurements.typeRespiratoryRate",
spo2: "measurements.typeOxygenSaturation",
bmi: "measurements.typeBodyMassIndex",
body_temp: "measurements.typeBodyTemperature",
vo2_max: "measurements.typeVo2Max",
pulse_wave_velocity: "measurements.typePulseWaveVelocity",
vascular_age: "measurements.typeVascularAge",
sleep: "measurements.typeSleep",
body_fat: "measurements.typeBodyFat",
fat_mass: "measurements.typeFatMass",
@@ -198,8 +284,23 @@ const SCOPE_SOURCE_METRIC_LABEL_KEY: Partial<Record<CoachScopeSource, string>> =
muscle_mass: "measurements.typeMuscleMass",
lean_body_mass: "measurements.typeLeanBodyMass",
bone_mass: "measurements.typeBoneMass",
total_body_water: "measurements.typeTotalBodyWater",
visceral_fat: "measurements.typeVisceralFat",
active_energy: "measurements.typeActiveEnergyBurned",
flights: "measurements.typeFlightsClimbed",
steps: "measurements.typeSteps",
distance: "measurements.typeWalkingRunningDistance",
glucose: "measurements.typeBloodGlucose",
walking_steadiness: "measurements.typeWalkingSteadiness",
walking_asymmetry: "measurements.typeWalkingAsymmetry",
walking_double_support: "measurements.typeWalkingDoubleSupport",
walking_step_length: "measurements.typeWalkingStepLength",
walking_speed: "measurements.typeWalkingSpeed",
audio_env: "measurements.typeAudioExposureEnv",
audio_headphone: "measurements.typeAudioExposureHeadphone",
audio_event: "measurements.typeAudioExposureEvent",
daylight: "measurements.typeTimeInDaylight",
skin_temp: "measurements.typeSkinTemperature",
};
export function scopeSourceMetricLabelKey(
@@ -276,6 +276,9 @@ export function CoachConversation({
const [currentConversationId, setCurrentConversationId] = useState<
string | null
>(initialConversationId ?? null);
const [activeWorkoutId, setActiveWorkoutId] = useState<string | null>(
initialWorkoutId ?? null,
);
const [historyTrayOpen, setHistoryTrayOpen] = useState(false);
const [sourcesTrayOpen, setSourcesTrayOpen] = useState(false);
const [inputValue, setInputValue] = useResettableValue(prefill ?? "");
@@ -641,7 +644,7 @@ export function CoachConversation({
// Same first-turn gate as `scope` — see `initialWorkoutId`.
const workoutId =
currentConversationId === null
? (initialWorkoutId ?? undefined)
? (activeWorkoutId ?? undefined)
: undefined;
const wasFreshFenced = currentConversationId === null && fenced;
const resolvedId = await send.send({
@@ -725,7 +728,7 @@ export function CoachConversation({
workoutId:
fenced || currentConversationId !== null
? undefined
: (initialWorkoutId ?? undefined),
: (activeWorkoutId ?? undefined),
fenced,
pendingAttachmentIds:
currentConversationId === null ? pendingAttachmentIds : undefined,
@@ -739,6 +742,7 @@ export function CoachConversation({
// v1.29.x (S7) — a new chat is always a health thread; drop any staged
// attachments so a fenced scope can never leak onto a fresh health thread.
setPendingAttachmentIds([]);
setActiveWorkoutId(null);
dispatchGuided({ type: "RESET" });
send.reset();
}
@@ -115,23 +115,37 @@ describe("derived-score sheets — every sheet has an outbound coach edge", () =
);
});
it("anchors the sleep sheet on sleep and the strain sheet on workouts", () => {
expect(
(renderSheet("SLEEP_SCORE").coachScope as { metric: string }).metric,
).toBe("sleep");
expect(
(renderSheet("STRAIN_SCORE").coachScope as { metric: string }).metric,
).toBe("workouts");
});
it("widens recovery-style sheets to the inputs that drive them", () => {
// Recovery is a synthesis of HRV + resting HR + sleep; narrowing to one
// of the three would hide the reason the score moved.
const scope = renderSheet("RECOVERY_SCORE").coachScope as {
metric: string;
also?: string[];
};
expect(scope.metric).toBe("hrv");
expect(scope.also).toEqual(expect.arrayContaining(["resting_hr", "sleep"]));
});
it.each([
["SLEEP_SCORE", { metric: "sleep" }],
[
"READINESS",
{
metric: "hrv",
also: ["resting_hr", "sleep", "respiratory_rate", "mood"],
window: "last7days",
},
],
[
"RECOVERY_SCORE",
{
metric: "hrv",
also: ["resting_hr", "sleep", "respiratory_rate", "mood"],
window: "last7days",
},
],
["STRESS_SCORE", { metric: "hrv", window: "last7days" }],
[
"STRAIN_SCORE",
{
metric: "workouts",
also: ["active_energy", "resting_hr"],
window: "last7days",
},
],
] satisfies ReadonlyArray<[AnatomyMetricId, Record<string, unknown>]>)(
"%s carries exactly its engine contributor domains",
(metric, expectedScope) => {
expect(renderSheet(metric).coachScope).toEqual(expectedScope);
},
);
});
@@ -65,29 +65,34 @@ export interface CompositeScoreAnatomyProps {
/**
* v1.31.0 — the Coach scope each derived-score sheet hands off to.
*
* The score sheets were the one assessment surface with NO outbound edge: they
* render the same `<InsightStatusCard>` as every metric page but passed
* neither an opener nor a scope, so the card painted no action at all and the
* sheet read one-way. A composite has no snapshot block of its own — it is a
* synthesis — so each maps to the INPUTS that drive it, the same way the
* recovery metric page already anchors on HRV + resting HR + sleep.
* Every list below mirrors the score engine's real contributor domains:
* Sleep Score reads only sleep-stage rows; Readiness blends RHR, HRV, sleep,
* respiratory rate, and mood; Stress is derived only from HRV; and Strain uses
* workout HR series plus active energy and the separately loaded resting HR.
*
* Recovery is source-dependent. A COMPUTED row is the Readiness blend
* verbatim, while a canonical native WHOOP / Oura / Polar row is the vendor's
* already-computed score and exposes no reconstructable input domain through
* the launch DTO. Its static scope is therefore the conservative union of the
* actual reconstructable contributors: the five Readiness domains, with no
* guessed native-only sources.
*/
const METRIC_COACH_SCOPE: Record<AnatomyMetricId, CoachLaunchScope> = {
SLEEP_SCORE: { metric: "sleep", also: ["hrv", "resting_hr"] },
SLEEP_SCORE: { metric: "sleep" },
READINESS: {
metric: "hrv",
also: ["resting_hr", "sleep"],
also: ["resting_hr", "sleep", "respiratory_rate", "mood"],
window: "last7days",
},
RECOVERY_SCORE: {
metric: "hrv",
also: ["resting_hr", "sleep"],
also: ["resting_hr", "sleep", "respiratory_rate", "mood"],
window: "last7days",
},
STRESS_SCORE: { metric: "hrv", also: ["resting_hr"], window: "last7days" },
STRESS_SCORE: { metric: "hrv", window: "last7days" },
STRAIN_SCORE: {
metric: "workouts",
also: ["active_energy", "pulse"],
also: ["active_energy", "resting_hr"],
window: "last7days",
},
};
@@ -166,13 +166,13 @@ describe("dashboard layout save — every layout source is covered (source pins)
expect(pageSrc).not.toMatch(/analytics[A-Za-z]*\??\.(data\.)?layout/);
});
it("refetchOnMount stays false on the snapshot hook (the reason refetchType-all is required)", () => {
it("keeps remounts disabled while focus refresh remains unconditional", () => {
const hookSrc = readFileSync(
join(process.cwd(), "src/lib/queries/use-dashboard-snapshot.ts"),
"utf8",
);
expect(hookSrc).toContain("refetchOnMount: false");
// v1.28.28 — focus refetch stays on (cross-tab freshness); do not revert.
expect(hookSrc).toContain("refetchOnWindowFocus: true");
// Arrival freshness is out-of-band, so staleTime must not suppress focus.
expect(hookSrc).toContain('refetchOnWindowFocus: "always"');
});
});