feat(insights): correlate intake, alcohol days, training, rings, State of Mind and room climate

The discovery matrix gains the families people log but could not test:
water, caffeine and up to four varying micronutrients from the nutrient
log, alcohol days from the mood tag, workout minutes (one per session) and
the day's training impulse, exercise, stand and mindful minutes, State of
Mind as an outcome, and the overnight room climate against that night's
sleep only. Every channel runs through the same seasonal adjustment,
effective-n p-value, Benjamini-Hochberg control and effect-size floor, is
read only while its module is on, and drops out below the paired-day floor.

State of Mind, mindful minutes and alcohol days are statistics for the
person: the Coach, the period narrative and the assessment cards drop every
pair that touches them, after the shared scan so the remaining pairs keep
the statistics the correlations page shows. A guard walks the import
closure of the analysis modules and refuses any path to a provider.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 21:39:15 +02:00
1 parent 30248efaf1
commit faa3ef867b
31 files changed
+1706 -34

No files matched your search

@@ -80,8 +80,28 @@ const CHANNEL_LABEL_KEY: Record<string, string> = {
// v1.43 — the Dawarich movement channels.
MOVEMENT_AWAY: "environment.movement.awayFromHome",
MOVEMENT_DISTANCE: "environment.movement.distance",
// v1.44 — the extended channel families.
HRV_RMSSD: "measurements.typeHrvRmssd",
EXERCISE_MINUTES: "measurements.typeExerciseMinutes",
STAND_MINUTES: "measurements.typeStandMinutes",
MINDFUL_MINUTES: "measurements.typeMindfulMinutes",
HYDRATION: "nutrients.names.water",
CAFFEINE: "nutrients.names.caffeine",
ALCOHOL: "insights.mood.discovery.channel.ALCOHOL",
WORKOUT_MINUTES: "insights.mood.discovery.channel.WORKOUT_MINUTES",
TRAINING_LOAD: "insights.mood.discovery.channel.TRAINING_LOAD",
};
/** A micronutrient channel (`NUTRIENT:<code>`) reads under its catalog name. */
const NUTRIENT_PREFIX = "NUTRIENT:";
function channelLabelKey(channelKey: string): string {
if (channelKey.startsWith(NUTRIENT_PREFIX)) {
return `nutrients.names.${channelKey.slice(NUTRIENT_PREFIX.length)}`;
}
return CHANNEL_LABEL_KEY[channelKey] ?? channelKey;
}
/** Keep only the discovered pairs that involve the mood channel. */
export function moodPairsOf(
discovered: DiscoveredCorrelation[],
@@ -131,8 +151,7 @@ export function MoodDiscoveredRelations({
const dynamicLabel = moodIsOutcome
? pair.behaviourLabel
: pair.outcomeLabel;
const factorLabel =
dynamicLabel ?? t(CHANNEL_LABEL_KEY[channelKey] ?? channelKey);
const factorLabel = dynamicLabel ?? t(channelLabelKey(channelKey));
if (
patternId &&
dismissal.isDismissed(patternId, pair.dismissed === true)