incremnt 0.1.18 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/queries.js CHANGED
@@ -369,18 +369,36 @@ export function programDetail(snapshot, programId) {
369
369
  days: (program.days ?? []).map((day, index) => ({
370
370
  dayIndex: index,
371
371
  title: day.title ?? null,
372
- exercises: (day.exercises ?? []).map((exercise) => ({
373
- name: exercise.name,
374
- muscleGroup: exercise.muscleGroup ?? null,
375
- sets: (exercise.sets ?? []).map((set) => ({
376
- reps: set.reps ?? null,
377
- weight: set.weight ?? null
378
- }))
379
- }))
372
+ exercises: (day.exercises ?? []).map((exercise) => {
373
+ const rec = (snapshot.exerciseRecommendations ?? {})[exercise.name];
374
+ return {
375
+ name: exercise.name,
376
+ muscleGroup: exercise.muscleGroup ?? null,
377
+ sets: (exercise.sets ?? []).map((set) => ({
378
+ reps: set.reps ?? null,
379
+ weight: set.weight ?? null
380
+ })),
381
+ ...(rec ? { recommendation: formatRecommendation(rec) } : {})
382
+ };
383
+ })
380
384
  }))
381
385
  };
382
386
  }
383
387
 
388
+ function formatRecommendation(rec) {
389
+ if (!rec || !rec.kind) return null;
390
+ const amount = rec.amount ?? 0;
391
+ const unit = rec.unit === 'reps' ? 'reps' : 'kg';
392
+ switch (rec.kind) {
393
+ case 'increaseWeight': return `+${amount} ${unit}`;
394
+ case 'decreaseWeight': return `-${amount} ${unit}`;
395
+ case 'increaseReps': return rec.targetReps ? `target reps: ${rec.targetReps.join(', ')}` : `+${amount} reps`;
396
+ case 'deload': return amount > 0 ? `deload -${amount} ${unit}` : 'deload';
397
+ case 'retry': return 'retry same weight';
398
+ default: return rec.kind;
399
+ }
400
+ }
401
+
384
402
  export function goalsList(snapshot) {
385
403
  return (snapshot.strengthPlans ?? []).map(plan => ({
386
404
  planId: plan.id,
@@ -469,7 +487,7 @@ export function cycleSummaryShow(snapshot, summaryId) {
469
487
  };
470
488
  }
471
489
 
472
- export function cycleSummaryContext(snapshot, programId) {
490
+ export function cycleSummaryContext(snapshot, programId, { exclude = new Set() } = {}) {
473
491
  const programs = snapshot.programs ?? [];
474
492
  const program = programId
475
493
  ? programs.find((p) => p.id === programId)
@@ -624,10 +642,16 @@ export function cycleSummaryContext(snapshot, programId) {
624
642
  const totalVolume = weekSessions.reduce((sum, s) => sum + (s.summary?.totalVolume ?? s.volume ?? 0), 0);
625
643
  const matchingCycleSummary = programCycleSummaries[idx] ?? null;
626
644
 
645
+ const sessionVolumes = weekSessions.map((s) => ({
646
+ dayName: s.dayName ?? null,
647
+ volume: s.summary?.totalVolume ?? s.volume ?? 0
648
+ }));
649
+
627
650
  return {
628
651
  weekNumber: wk,
629
652
  sessionCount: weekSessions.length,
630
653
  totalVolume: Math.round(totalVolume),
654
+ sessionVolumes,
631
655
  previousAISummary: matchingCycleSummary?.aiSummary ?? null
632
656
  };
633
657
  });
@@ -705,11 +729,21 @@ export function cycleSummaryContext(snapshot, programId) {
705
729
  (p) => p.status === 'active' && p.programId === program.id
706
730
  );
707
731
  if (activePlan) {
708
- goalProgress = (activePlan.liftGoals ?? []).map((g) => {
732
+ const programExerciseNames = new Set(
733
+ (program.days ?? [])
734
+ .flatMap((day) => day.exercises ?? [])
735
+ .map((exercise) => canonicalExerciseName(exercise.name ?? exercise.exerciseName))
736
+ );
737
+ goalProgress = (activePlan.liftGoals ?? [])
738
+ .filter((g) => {
739
+ if (programExerciseNames.size === 0) return true;
740
+ return programExerciseNames.has(canonicalExerciseName(g.exerciseDisplayName));
741
+ })
742
+ .map((g) => {
709
743
  const range = g.targetE1RM - g.startingE1RM;
710
744
  const gained = g.currentBestE1RM - g.startingE1RM;
711
745
  const progressPct =
712
- range > 0 ? Math.round((gained / range) * 100) : null;
746
+ range > 0 ? Math.max(0, Math.round((gained / range) * 100)) : null;
713
747
  return {
714
748
  exerciseName: g.exerciseDisplayName,
715
749
  progressPercent: progressPct,
@@ -736,15 +770,15 @@ export function cycleSummaryContext(snapshot, programId) {
736
770
  const cycleStart = String(cycleSessions[0] ? completionDateForSession(cycleSessions[0]) : '');
737
771
  const cycleEnd = String(cycleSessions[cycleSessions.length - 1]
738
772
  ? completionDateForSession(cycleSessions[cycleSessions.length - 1]) : '');
739
- const cycleCardio = (snapshot.healthMetrics?.otherWorkouts ?? [])
773
+ const cycleCardio = exclude.has('otherWorkouts') ? [] : (snapshot.healthMetrics?.otherWorkouts ?? [])
740
774
  .filter((w) => w.date >= cycleStart && w.date <= cycleEnd);
741
- const cycleRestingHR = (snapshot.healthMetrics?.restingHR ?? [])
775
+ const cycleRestingHR = exclude.has('recovery') ? [] : (snapshot.healthMetrics?.restingHR ?? [])
742
776
  .filter((m) => m.date >= cycleStart && m.date <= cycleEnd);
743
- const cycleHRV = (snapshot.healthMetrics?.hrv ?? [])
777
+ const cycleHRV = exclude.has('recovery') ? [] : (snapshot.healthMetrics?.hrv ?? [])
744
778
  .filter((m) => m.date >= cycleStart && m.date <= cycleEnd);
745
- const cycleVO2Max = (snapshot.healthMetrics?.vo2Max ?? [])
779
+ const cycleVO2Max = exclude.has('recovery') ? [] : (snapshot.healthMetrics?.vo2Max ?? [])
746
780
  .filter((m) => m.date >= cycleStart && m.date <= cycleEnd);
747
- const cycleSleep = (snapshot.healthMetrics?.sleep ?? [])
781
+ const cycleSleep = exclude.has('recovery') ? [] : (snapshot.healthMetrics?.sleep ?? [])
748
782
  .filter((m) => m.date >= cycleStart && m.date <= cycleEnd);
749
783
 
750
784
  const avgRestingHR = cycleRestingHR.length > 0
@@ -759,7 +793,7 @@ export function cycleSummaryContext(snapshot, programId) {
759
793
  const avgSleepMins = cycleSleep.length > 0
760
794
  ? Math.round(cycleSleep.reduce((s, m) => s + m.durationMins, 0) / cycleSleep.length)
761
795
  : null;
762
- const cycleBodyWeight = (snapshot.healthMetrics?.bodyWeight ?? [])
796
+ const cycleBodyWeight = exclude.has('bodyWeight') ? [] : (snapshot.healthMetrics?.bodyWeight ?? [])
763
797
  .filter((m) => m.date >= cycleStart && m.date <= cycleEnd);
764
798
  const latestBodyWeightKg = cycleBodyWeight.length > 0
765
799
  ? Math.round(cycleBodyWeight.at(-1).value * 10) / 10
@@ -859,11 +893,12 @@ export function cycleSummaryContext(snapshot, programId) {
859
893
  latestVO2Max,
860
894
  avgSleepMins,
861
895
  latestBodyWeightKg,
862
- prioritySignals: rankPrioritySignals(cycleSignals)
896
+ prioritySignals: rankPrioritySignals(cycleSignals),
897
+ excludeNote: buildExcludeNote(exclude)
863
898
  };
864
899
  }
865
900
 
866
- export function checkpointContext(snapshot, programId, checkpointWeek) {
901
+ export function checkpointContext(snapshot, programId, checkpointWeek, { exclude = new Set() } = {}) {
867
902
  const programs = snapshot.programs ?? [];
868
903
  const program = programId
869
904
  ? programs.find((p) => p.id === programId)
@@ -948,11 +983,12 @@ export function checkpointContext(snapshot, programId, checkpointWeek) {
948
983
  checkpointWeek,
949
984
  totalWeeks,
950
985
  exercises,
951
- previousCycleNotes
986
+ previousCycleNotes,
987
+ excludeNote: buildExcludeNote(exclude)
952
988
  };
953
989
  }
954
990
 
955
- export function workoutSummaryContext(snapshot, sessionId) {
991
+ export function workoutSummaryContext(snapshot, sessionId, { exclude = new Set() } = {}) {
956
992
  const sessions = snapshot.sessions ?? [];
957
993
  const session = sessions.find((s) => s.id === sessionId);
958
994
  if (!session) return null;
@@ -999,7 +1035,8 @@ export function workoutSummaryContext(snapshot, sessionId) {
999
1035
  muscleGroup: exercise.muscleGroup ?? null,
1000
1036
  completedSets: completeSets.length,
1001
1037
  isBodyweight: bw,
1002
- topSet: topSet ? { weight: topSet.weight, reps: topSet.reps } : null
1038
+ topSet: topSet ? { weight: topSet.weight, reps: topSet.reps } : null,
1039
+ allSets: completeSets.map((s) => ({ weight: Number(s.weight), reps: Number(s.reps) }))
1003
1040
  };
1004
1041
  });
1005
1042
 
@@ -1025,6 +1062,7 @@ export function workoutSummaryContext(snapshot, sessionId) {
1025
1062
  const priorBestBWReps = new Map(); // key -> best reps for bodyweight exercises
1026
1063
  const exerciseSessionCounts = new Map();
1027
1064
  const priorBestReps = new Map(); // key -> Map(weight -> bestReps)
1065
+ const exerciseRecentWeights = new Map(); // key -> [{date, topWeight}], last 3 sessions
1028
1066
  for (const s of sessions) {
1029
1067
  if (s.id === sessionId) continue;
1030
1068
  const sDate = String(completionDateForSession(s));
@@ -1053,13 +1091,32 @@ export function workoutSummaryContext(snapshot, sessionId) {
1053
1091
  const currentBest = repsMap.get(w);
1054
1092
  if (!currentBest || r > currentBest) repsMap.set(w, r);
1055
1093
  }
1094
+ // Track per-session top weight for recent weight history (non-bodyweight only)
1095
+ const exerciseCompleteSets = (exercise.sets ?? []).filter((s) => s.isComplete);
1096
+ const maxWeight = exerciseCompleteSets.reduce((max, s) => Math.max(max, Number(s.weight)), 0);
1097
+ if (maxWeight > 0) {
1098
+ if (!exerciseRecentWeights.has(key)) exerciseRecentWeights.set(key, []);
1099
+ const history = exerciseRecentWeights.get(key);
1100
+ if (!history.find((h) => h.date === sDate)) {
1101
+ history.push({ date: sDate, topWeight: maxWeight });
1102
+ }
1103
+ }
1056
1104
  }
1057
1105
  }
1058
1106
 
1059
- // Attach prior session count to each exercise
1107
+ // Sort recent weight histories by date descending, keep last 3 sessions
1108
+ for (const [, history] of exerciseRecentWeights) {
1109
+ history.sort((a, b) => b.date.localeCompare(a.date));
1110
+ if (history.length > 3) history.length = 3;
1111
+ }
1112
+
1113
+ // Attach prior session count and recent weights to each exercise
1060
1114
  for (const ex of exercises) {
1061
1115
  const key = normalizeExerciseName(ex.exerciseName);
1062
1116
  ex.priorSessions = exerciseSessionCounts.get(key) ?? 0;
1117
+ if (!ex.isBodyweight) {
1118
+ ex.recentWeights = exerciseRecentWeights.get(key) ?? [];
1119
+ }
1063
1120
  }
1064
1121
 
1065
1122
  // Detect PRs — skip first-time exercises (every set is trivially a "PR")
@@ -1130,46 +1187,101 @@ export function workoutSummaryContext(snapshot, sessionId) {
1130
1187
  }
1131
1188
  }
1132
1189
 
1133
- // Plan comparison — prefer the logged point-in-time prescription snapshot.
1134
- let planComparison;
1190
+ // Resolve planned exercise list — prefer the logged point-in-time prescription snapshot.
1191
+ let plannedExerciseList = [];
1135
1192
  if (session.prescriptionSnapshot?.exercises?.length > 0) {
1136
- planComparison = buildPlanComparison(session, exercises, session.prescriptionSnapshot.exercises);
1193
+ plannedExerciseList = session.prescriptionSnapshot.exercises;
1137
1194
  } else if (session.programId) {
1138
1195
  const program = (snapshot.programs ?? []).find(p => p.id === session.programId);
1139
1196
  const matchingDay = Number.isInteger(session.programDayIndex)
1140
1197
  ? program?.days?.[session.programDayIndex]
1141
1198
  : program?.days?.find(d => d.title === dayName);
1142
1199
  if (matchingDay) {
1143
- planComparison = buildPlanComparison(session, exercises, matchingDay.exercises ?? []);
1200
+ plannedExerciseList = matchingDay.exercises ?? [];
1201
+ }
1202
+ }
1203
+
1204
+ // Plan comparison
1205
+ const planComparison = plannedExerciseList.length > 0
1206
+ ? buildPlanComparison(session, exercises, plannedExerciseList)
1207
+ : undefined;
1208
+
1209
+ // Attach planned weight/reps to each exercise for the AI coach context
1210
+ if (plannedExerciseList.length > 0) {
1211
+ const plannedByName = new Map(
1212
+ plannedExerciseList.map((ex) => [normalizeExerciseName(ex.exerciseName ?? ex.name), ex])
1213
+ );
1214
+ for (const ex of exercises) {
1215
+ const planned = plannedByName.get(normalizeExerciseName(ex.exerciseName));
1216
+ if (!planned) continue;
1217
+ const sets = planned.targetSets ?? planned.sets ?? [];
1218
+ const firstWeightedSet = sets.find((s) => s.weight != null && Number(s.weight) > 0);
1219
+ if (firstWeightedSet) {
1220
+ ex.plannedWeight = firstWeightedSet.weight;
1221
+ ex.plannedReps = firstWeightedSet.reps ?? null;
1222
+ ex.plannedSetCount = sets.length;
1223
+ }
1144
1224
  }
1145
1225
  }
1146
1226
 
1147
1227
  const isAdhoc = !session.programId;
1148
1228
 
1149
- // Include other workouts from the 7 days before this session for coach context
1229
+ // Include cardio from the 3 days before this session only the immediately preceding
1230
+ // window has meaningful acute recovery relevance.
1150
1231
  const sessionDateStr = String(sessionDate);
1151
- const weekBefore = new Date(new Date(sessionDateStr).getTime() - 7 * 24 * 60 * 60 * 1000)
1232
+ const threeDaysBefore = new Date(new Date(sessionDateStr).getTime() - 3 * 24 * 60 * 60 * 1000)
1152
1233
  .toISOString().slice(0, 10);
1153
- const nearbyCardio = (snapshot.healthMetrics?.otherWorkouts ?? [])
1154
- .filter((w) => w.date >= weekBefore && w.date <= sessionDateStr);
1234
+ const nearbyCardio = exclude.has('otherWorkouts') ? [] : (snapshot.healthMetrics?.otherWorkouts ?? [])
1235
+ .filter((w) => w.date >= threeDaysBefore && w.date <= sessionDateStr);
1155
1236
 
1156
- const restingHROnDay = (snapshot.healthMetrics?.restingHR ?? [])
1237
+ const restingHROnDay = exclude.has('recovery') ? null : (snapshot.healthMetrics?.restingHR ?? [])
1157
1238
  .find((m) => m.date === sessionDateStr);
1158
- const hrvOnDay = (snapshot.healthMetrics?.hrv ?? [])
1239
+ const hrvOnDay = exclude.has('recovery') ? null : (snapshot.healthMetrics?.hrv ?? [])
1159
1240
  .find((m) => m.date === sessionDateStr);
1160
- const sleepNight = (snapshot.healthMetrics?.sleep ?? [])
1241
+ const sleepNight = exclude.has('recovery') ? null : (snapshot.healthMetrics?.sleep ?? [])
1161
1242
  .find((m) => m.date === sessionDateStr);
1162
- const vo2MaxRecent = (snapshot.healthMetrics?.vo2Max ?? [])
1163
- .filter((m) => m.date >= weekBefore && m.date <= sessionDateStr);
1243
+
1244
+ // 14-day baselines for contextualising session-day vitals
1245
+ const fourteenDaysBefore = new Date(new Date(sessionDateStr).getTime() - 14 * 24 * 60 * 60 * 1000)
1246
+ .toISOString().slice(0, 10);
1247
+ const recentHRVReadings = exclude.has('recovery') ? [] : (snapshot.healthMetrics?.hrv ?? [])
1248
+ .filter((m) => m.date >= fourteenDaysBefore && m.date < sessionDateStr);
1249
+ const hrvBaseline = recentHRVReadings.length >= 3
1250
+ ? Math.round(recentHRVReadings.reduce((s, m) => s + m.value, 0) / recentHRVReadings.length)
1251
+ : null;
1252
+ const recentRestingHRReadings = exclude.has('recovery') ? [] : (snapshot.healthMetrics?.restingHR ?? [])
1253
+ .filter((m) => m.date >= fourteenDaysBefore && m.date < sessionDateStr);
1254
+ const restingHRBaseline = recentRestingHRReadings.length >= 3
1255
+ ? Math.round(recentRestingHRReadings.reduce((s, m) => s + m.value, 0) / recentRestingHRReadings.length)
1256
+ : null;
1257
+
1258
+ const vo2MaxRecent = exclude.has('recovery') ? [] : (snapshot.healthMetrics?.vo2Max ?? [])
1259
+ .filter((m) => m.date >= threeDaysBefore && m.date <= sessionDateStr);
1164
1260
  const vo2MaxLatest = vo2MaxRecent.length > 0
1165
1261
  ? Math.round(vo2MaxRecent.at(-1).value * 10) / 10
1166
1262
  : null;
1167
- const bodyWeightOnDay = (snapshot.healthMetrics?.bodyWeight ?? [])
1168
- .find((m) => m.date === sessionDateStr);
1169
- const bodyWeightKg = bodyWeightOnDay
1170
- ? Math.round(bodyWeightOnDay.value * 10) / 10
1263
+ const sevenDaysBefore = new Date(new Date(sessionDateStr).getTime() - 7 * 24 * 60 * 60 * 1000)
1264
+ .toISOString().slice(0, 10);
1265
+ const recentBodyWeight = exclude.has('bodyWeight') ? null : (snapshot.healthMetrics?.bodyWeight ?? [])
1266
+ .filter((m) => m.date >= sevenDaysBefore && m.date <= sessionDateStr)
1267
+ .sort((a, b) => b.date.localeCompare(a.date))[0] ?? null;
1268
+ const bodyWeightKg = recentBodyWeight
1269
+ ? Math.round(recentBodyWeight.value * 10) / 10
1171
1270
  : null;
1172
1271
 
1272
+ // Readiness context (gated on recovery exclusion)
1273
+ const readinessContext = exclude.has('recovery') ? null : (() => {
1274
+ const snap = session.readinessBandSnapshot;
1275
+ if (!snap) return null;
1276
+ return {
1277
+ band: snap.band,
1278
+ dominantSignal: snap.dominantSignal,
1279
+ adaptationApplied: snap.adaptationApplied,
1280
+ userOverrode: snap.userOverrode ?? false,
1281
+ tsbValue: snap.tsbValue ?? null
1282
+ };
1283
+ })();
1284
+
1173
1285
  const isFirstWorkout = earlierSessions.length === 0;
1174
1286
 
1175
1287
  const result = {
@@ -1187,10 +1299,13 @@ export function workoutSummaryContext(snapshot, sessionId) {
1187
1299
  repPrs: repPrs.length > 0 ? repPrs : [],
1188
1300
  nearbyCardio: nearbyCardio.length > 0 ? nearbyCardio : null,
1189
1301
  restingHROnDay: restingHROnDay?.value ?? null,
1302
+ restingHRBaseline,
1190
1303
  hrvOnDay: hrvOnDay?.value ?? null,
1304
+ hrvBaseline,
1191
1305
  vo2MaxLatest,
1192
1306
  sleepNight: sleepNight ?? null,
1193
- bodyWeightKg
1307
+ bodyWeightKg,
1308
+ readiness: readinessContext
1194
1309
  };
1195
1310
  if (programChange) result.programChange = programChange;
1196
1311
  if (planComparison) result.planComparison = planComparison;
@@ -1290,16 +1405,37 @@ export function workoutSummaryContext(snapshot, sessionId) {
1290
1405
  });
1291
1406
  }
1292
1407
  result.prioritySignals = rankPrioritySignals(workoutSignals);
1408
+ result.excludeNote = buildExcludeNote(exclude);
1293
1409
 
1294
1410
  return result;
1295
1411
  }
1296
1412
 
1297
- export function askContext(snapshot) {
1413
+ export function askContext(snapshot, { exclude = new Set() } = {}) {
1298
1414
  const sessions = snapshot.sessions ?? [];
1299
1415
  const lines = [];
1300
1416
 
1417
+ lines.push(`Today's date: ${new Date().toISOString().slice(0, 10)}.`);
1301
1418
  lines.push(`Training overview: ${sessions.length} total workouts logged.`);
1302
1419
 
1420
+ // Recovery context — hours since last session
1421
+ const sortedRecent = sessions
1422
+ .slice()
1423
+ .sort((a, b) => String(completionDateForSession(b)).localeCompare(String(completionDateForSession(a))));
1424
+ const lastSessionDate = sortedRecent[0] ? completionDateForSession(sortedRecent[0]) : null;
1425
+ if (lastSessionDate) {
1426
+ const lastSessionTime = new Date(String(lastSessionDate)).getTime();
1427
+ const hoursSinceLast = Number.isNaN(lastSessionTime) ? -1 : Math.round((Date.now() - lastSessionTime) / (1000 * 60 * 60));
1428
+ if (hoursSinceLast >= 0 && hoursSinceLast < 72) {
1429
+ const recoveryLabel =
1430
+ hoursSinceLast < 12
1431
+ ? 'less than 12 hours ago'
1432
+ : hoursSinceLast < 24
1433
+ ? 'less than 24 hours ago'
1434
+ : `${Math.floor(hoursSinceLast / 24)} day${Math.floor(hoursSinceLast / 24) === 1 ? '' : 's'} ago`;
1435
+ lines.push(`Last session: ${recoveryLabel} (${lastSessionDate}).`);
1436
+ }
1437
+ }
1438
+
1303
1439
  // Training frequency (last 4 weeks)
1304
1440
  const fourWeeksAgo = new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString();
1305
1441
  const recentCount = sessions.filter((s) => String(completionDateForSession(s)) >= fourWeeksAgo).length;
@@ -1308,10 +1444,68 @@ export function askContext(snapshot) {
1308
1444
  lines.push(`Recent frequency: ${perWeek} sessions/week (last 4 weeks).`);
1309
1445
  }
1310
1446
 
1311
- // Current program
1447
+ // Current program + week phase (derived from most recent session's stamped context,
1448
+ // not from completedCyclesCount which advances immediately on cycle completion)
1312
1449
  const program = activeProgram(snapshot);
1313
1450
  if (program) {
1314
- lines.push(`Current program: ${program.name}, ${program.daysPerWeek} days/week, equipment: ${program.equipmentTier ?? 'unknown'}.`);
1451
+ const programSessions = sessions
1452
+ .filter((s) => s.programId === program.id && s.historicalContext?.programWeekNumber)
1453
+ .sort((a, b) => String(completionDateForSession(b)).localeCompare(String(completionDateForSession(a))));
1454
+ const latestSession = programSessions[0];
1455
+ const currentWeek = latestSession?.historicalContext?.programWeekNumber ?? (Number(program.completedCyclesCount ?? 0) + 1);
1456
+ const weekPhase = latestSession?.historicalContext?.programProgressionType ?? null;
1457
+ const phaseLabel = weekPhase ? ` (${weekPhase} week)` : '';
1458
+ lines.push(`Current program: ${program.name}, week ${currentWeek}${phaseLabel}, ${program.daysPerWeek} days/week, equipment: ${program.equipmentTier ?? 'unknown'}.`);
1459
+ if (weekPhase === 'deload') {
1460
+ lines.push('Note: This is a planned deload week — reduced volume and intensity are intentional, not a regression.');
1461
+ }
1462
+
1463
+ // Days until next session
1464
+ const currentDayIndex = program.currentDayIndex ?? 0;
1465
+ const nextSessionWeekday = (program.trainingWeekdays ?? [])[currentDayIndex];
1466
+ if (nextSessionWeekday != null) {
1467
+ const jsDay = new Date().getDay(); // 0=Sun
1468
+ const todayIso = jsDay === 0 ? 7 : jsDay; // 1=Mon … 7=Sun
1469
+ let daysUntil = nextSessionWeekday - todayIso;
1470
+ if (daysUntil < 0) daysUntil += 7;
1471
+ const dayNames = ['', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
1472
+ const whenLabel = daysUntil === 0 ? 'today' : daysUntil === 1 ? 'tomorrow' : `in ${daysUntil} days`;
1473
+ const nextDayTitle = (program.days ?? [])[currentDayIndex]?.title ?? `Day ${currentDayIndex + 1}`;
1474
+ lines.push(`Next session: ${nextDayTitle} on ${dayNames[nextSessionWeekday]} (${whenLabel}).`);
1475
+ }
1476
+
1477
+ // Program days with planned sets
1478
+ const days = program.days ?? [];
1479
+ if (days.length > 0) {
1480
+ lines.push('');
1481
+ lines.push('Program schedule (planned sets):');
1482
+ for (let i = 0; i < days.length; i++) {
1483
+ const day = days[i];
1484
+ const upNext = i === currentDayIndex ? ' [UP NEXT]' : '';
1485
+ lines.push(` ${day.title ?? `Day ${i + 1}`}${upNext}:`);
1486
+ for (const exercise of day.exercises ?? []) {
1487
+ const sets = exercise.sets ?? [];
1488
+ if (sets.length === 0) continue;
1489
+ // Group identical sets for compact display: e.g. "4×10 @ 100kg"
1490
+ const groups = [];
1491
+ let run = 1;
1492
+ for (let j = 1; j <= sets.length; j++) {
1493
+ const prev = sets[j - 1];
1494
+ const curr = sets[j];
1495
+ if (curr && curr.weight === prev.weight && curr.reps === prev.reps) {
1496
+ run++;
1497
+ } else {
1498
+ groups.push(`${run}×${prev.reps}${prev.weight > 0 ? ` @ ${prev.weight}kg` : ''}`);
1499
+ run = 1;
1500
+ }
1501
+ }
1502
+ const rec = (snapshot.exerciseRecommendations ?? {})[exercise.name];
1503
+ const recLabel = rec ? formatRecommendation(rec) : null;
1504
+ const recSuffix = recLabel ? ` → next: ${recLabel}` : '';
1505
+ lines.push(` ${exercise.name}: ${groups.join(', ')}${recSuffix}`);
1506
+ }
1507
+ }
1508
+ }
1315
1509
  }
1316
1510
 
1317
1511
  // Best e1RM records (top 15)
@@ -1453,90 +1647,99 @@ export function askContext(snapshot) {
1453
1647
  }
1454
1648
  }
1455
1649
 
1456
- appendHealthMetricsContext(lines, snapshot.healthMetrics, { recentDays: 14 });
1650
+ appendHealthMetricsContext(lines, snapshot.healthMetrics, { recentDays: 14, exclude });
1651
+ appendExcludeNote(lines, exclude);
1457
1652
 
1458
1653
  return lines.join('\n');
1459
1654
  }
1460
1655
 
1461
- function appendHealthMetricsContext(lines, metrics, { recentDays = 14 } = {}) {
1656
+ function appendHealthMetricsContext(lines, metrics, { recentDays = 14, exclude = new Set() } = {}) {
1462
1657
  if (!metrics) return;
1463
1658
 
1464
1659
  const cutoff = new Date(Date.now() - recentDays * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
1465
1660
 
1466
- const recentWorkouts = (metrics.otherWorkouts ?? []).filter((w) => w.date >= cutoff);
1467
- if (recentWorkouts.length > 0) {
1468
- lines.push('');
1469
- lines.push(`Other workouts (last ${recentDays} days):`);
1470
- for (const w of recentWorkouts) {
1471
- const parts = [`${w.durationSecs ? Math.round(w.durationSecs / 60) : '?'} min`];
1472
- if (w.distanceKm) parts.push(`${w.distanceKm.toFixed(1)} km`);
1473
- if (w.avgHR) parts.push(`avg HR ${w.avgHR} bpm`);
1474
- if (w.calories) parts.push(`${w.calories} kcal`);
1475
- if (w.effortScore) parts.push(`effort ${w.effortScore}/10`);
1476
- lines.push(` ${w.date} ${w.workoutType}: ${parts.join(', ')}`);
1661
+ if (!exclude.has('otherWorkouts')) {
1662
+ const recentWorkouts = (metrics.otherWorkouts ?? []).filter((w) => w.date >= cutoff);
1663
+ if (recentWorkouts.length > 0) {
1664
+ lines.push('');
1665
+ lines.push(`Other workouts (last ${recentDays} days):`);
1666
+ for (const w of recentWorkouts) {
1667
+ const parts = [`${w.durationSecs ? Math.round(w.durationSecs / 60) : '?'} min`];
1668
+ if (w.distanceKm) parts.push(`${w.distanceKm.toFixed(1)} km`);
1669
+ if (w.avgHR) parts.push(`avg HR ${w.avgHR} bpm`);
1670
+ if (w.calories) parts.push(`${w.calories} kcal`);
1671
+ if (w.effortScore) parts.push(`effort ${w.effortScore}/10`);
1672
+ lines.push(` ${w.date} ${w.workoutType}: ${parts.join(', ')}`);
1673
+ }
1477
1674
  }
1478
- }
1479
1675
 
1480
- // Weekly cardio volume summary (always last 7 days regardless of recentDays)
1481
- const sevenDayCutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
1482
- const weekCardio = (metrics.otherWorkouts ?? []).filter((w) => w.date >= sevenDayCutoff);
1483
- if (weekCardio.length > 0) {
1484
- const totalSecs = weekCardio.reduce((sum, w) => sum + (w.durationSecs ?? 0), 0);
1485
- const totalMins = Math.round(totalSecs / 60);
1486
- const totalKm = weekCardio.reduce((sum, w) => sum + (w.distanceKm ?? 0), 0);
1487
- const distPart = totalKm > 0 ? `, ${totalKm.toFixed(1)} km total` : '';
1488
- lines.push(`Cardio last 7 days: ${weekCardio.length} sessions, ${totalMins} min${distPart}`);
1676
+ // Weekly cardio volume summary (always last 7 days regardless of recentDays)
1677
+ const sevenDayCutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
1678
+ const weekCardio = (metrics.otherWorkouts ?? []).filter((w) => w.date >= sevenDayCutoff);
1679
+ if (weekCardio.length > 0) {
1680
+ const totalSecs = weekCardio.reduce((sum, w) => sum + (w.durationSecs ?? 0), 0);
1681
+ const totalMins = Math.round(totalSecs / 60);
1682
+ const totalKm = weekCardio.reduce((sum, w) => sum + (w.distanceKm ?? 0), 0);
1683
+ const distPart = totalKm > 0 ? `, ${totalKm.toFixed(1)} km total` : '';
1684
+ lines.push(`Cardio last 7 days: ${weekCardio.length} sessions, ${totalMins} min${distPart}`);
1685
+ }
1489
1686
  }
1490
1687
 
1491
- const recentRestingHR = (metrics.restingHR ?? []).filter((m) => m.date >= cutoff);
1492
- if (recentRestingHR.length > 0) {
1493
- const avg = Math.round(recentRestingHR.reduce((s, m) => s + m.value, 0) / recentRestingHR.length);
1494
- const latest = recentRestingHR[recentRestingHR.length - 1];
1495
- lines.push('');
1496
- lines.push(`Resting HR (last ${recentDays} days): avg ${avg} bpm, latest ${Math.round(latest.value)} bpm (${latest.date})`);
1497
- }
1688
+ if (!exclude.has('recovery')) {
1689
+ const recentRestingHR = (metrics.restingHR ?? []).filter((m) => m.date >= cutoff);
1690
+ if (recentRestingHR.length > 0) {
1691
+ const avg = Math.round(recentRestingHR.reduce((s, m) => s + m.value, 0) / recentRestingHR.length);
1692
+ const latest = recentRestingHR[recentRestingHR.length - 1];
1693
+ lines.push('');
1694
+ lines.push(`Resting HR (last ${recentDays} days): avg ${avg} bpm, latest ${Math.round(latest.value)} bpm (${latest.date})`);
1695
+ }
1498
1696
 
1499
- const recentHRV = (metrics.hrv ?? []).filter((m) => m.date >= cutoff);
1500
- if (recentHRV.length > 0) {
1501
- const avg = Math.round(recentHRV.reduce((s, m) => s + m.value, 0) / recentHRV.length);
1502
- const latest = recentHRV[recentHRV.length - 1];
1503
- lines.push(`HRV (last ${recentDays} days): avg ${avg} ms, latest ${Math.round(latest.value)} ms (${latest.date})`);
1504
- }
1697
+ const recentHRV = (metrics.hrv ?? []).filter((m) => m.date >= cutoff);
1698
+ if (recentHRV.length > 0) {
1699
+ const avg = Math.round(recentHRV.reduce((s, m) => s + m.value, 0) / recentHRV.length);
1700
+ const latest = recentHRV[recentHRV.length - 1];
1701
+ lines.push(`HRV (last ${recentDays} days): avg ${avg} ms, latest ${Math.round(latest.value)} ms (${latest.date})`);
1702
+ }
1505
1703
 
1506
- const recentVO2Max = (metrics.vo2Max ?? []).filter((m) => m.date >= cutoff);
1507
- if (recentVO2Max.length > 0) {
1508
- const latest = recentVO2Max[recentVO2Max.length - 1];
1509
- lines.push(`VO2 Max: ${Math.round(latest.value * 10) / 10} ml/kg/min (${latest.date})`);
1510
- }
1704
+ const recentVO2Max = (metrics.vo2Max ?? []).filter((m) => m.date >= cutoff);
1705
+ if (recentVO2Max.length > 0) {
1706
+ const latest = recentVO2Max[recentVO2Max.length - 1];
1707
+ lines.push(`VO2 Max: ${Math.round(latest.value * 10) / 10} ml/kg/min (${latest.date})`);
1708
+ }
1511
1709
 
1512
- const recentSleep = (metrics.sleep ?? []).filter((m) => m.date >= cutoff);
1513
- if (recentSleep.length > 0) {
1514
- const avgMins = Math.round(recentSleep.reduce((s, m) => s + m.durationMins, 0) / recentSleep.length);
1515
- const avgHours = (avgMins / 60).toFixed(1);
1516
- lines.push(`Sleep (last ${recentDays} days): avg ${avgHours}h/night`);
1517
- }
1710
+ const recentSleep = (metrics.sleep ?? []).filter((m) => m.date >= cutoff);
1711
+ if (recentSleep.length > 0) {
1712
+ const avgMins = Math.round(recentSleep.reduce((s, m) => s + m.durationMins, 0) / recentSleep.length);
1713
+ const avgHours = (avgMins / 60).toFixed(1);
1714
+ const latest = recentSleep[recentSleep.length - 1];
1715
+ const latestHours = (latest.durationMins / 60).toFixed(1);
1716
+ lines.push(`Sleep (last ${recentDays} days): avg ${avgHours}h/night, last night ${latestHours}h (${latest.date})`);
1717
+ }
1518
1718
 
1519
- const recentBodyWeight = (metrics.bodyWeight ?? []).filter((m) => m.date >= cutoff);
1520
- if (recentBodyWeight.length > 0) {
1521
- const latest = recentBodyWeight[recentBodyWeight.length - 1];
1522
- const earliest = recentBodyWeight[0];
1523
- const delta = (latest.value - earliest.value).toFixed(1);
1524
- const trend = delta > 0 ? `+${delta}` : delta;
1525
- lines.push(`Body weight (last ${recentDays} days): latest ${latest.value.toFixed(1)} kg (${latest.date}), ${recentBodyWeight.length} readings, trend ${trend} kg`);
1526
- }
1719
+ const recentRespiratoryRate = (metrics.respiratoryRate ?? []).filter((m) => m.date >= cutoff);
1720
+ if (recentRespiratoryRate.length > 0) {
1721
+ const avg = recentRespiratoryRate.reduce((s, m) => s + m.value, 0) / recentRespiratoryRate.length;
1722
+ const latest = recentRespiratoryRate[recentRespiratoryRate.length - 1];
1723
+ lines.push(`Respiratory rate (last ${recentDays} days): avg ${avg.toFixed(1)} rpm, latest ${latest.value.toFixed(1)} rpm (${latest.date})`);
1724
+ }
1527
1725
 
1528
- const recentRespiratoryRate = (metrics.respiratoryRate ?? []).filter((m) => m.date >= cutoff);
1529
- if (recentRespiratoryRate.length > 0) {
1530
- const avg = recentRespiratoryRate.reduce((s, m) => s + m.value, 0) / recentRespiratoryRate.length;
1531
- const latest = recentRespiratoryRate[recentRespiratoryRate.length - 1];
1532
- lines.push(`Respiratory rate (last ${recentDays} days): avg ${avg.toFixed(1)} rpm, latest ${latest.value.toFixed(1)} rpm (${latest.date})`);
1726
+ const recentBodyTemp = (metrics.bodyTemperature ?? []).filter((m) => m.date >= cutoff);
1727
+ if (recentBodyTemp.length > 0) {
1728
+ const avg = recentBodyTemp.reduce((s, m) => s + m.value, 0) / recentBodyTemp.length;
1729
+ const latest = recentBodyTemp[recentBodyTemp.length - 1];
1730
+ lines.push(`Body temperature (last ${recentDays} days): avg ${avg.toFixed(1)}°C, latest ${latest.value.toFixed(1)}°C (${latest.date})`);
1731
+ }
1533
1732
  }
1534
1733
 
1535
- const recentBodyTemp = (metrics.bodyTemperature ?? []).filter((m) => m.date >= cutoff);
1536
- if (recentBodyTemp.length > 0) {
1537
- const avg = recentBodyTemp.reduce((s, m) => s + m.value, 0) / recentBodyTemp.length;
1538
- const latest = recentBodyTemp[recentBodyTemp.length - 1];
1539
- lines.push(`Body temperature (last ${recentDays} days): avg ${avg.toFixed(1)}°C, latest ${latest.value.toFixed(1)}°C (${latest.date})`);
1734
+ if (!exclude.has('bodyWeight')) {
1735
+ const recentBodyWeight = (metrics.bodyWeight ?? []).filter((m) => m.date >= cutoff);
1736
+ if (recentBodyWeight.length > 0) {
1737
+ const latest = recentBodyWeight[recentBodyWeight.length - 1];
1738
+ const earliest = recentBodyWeight[0];
1739
+ const delta = (latest.value - earliest.value).toFixed(1);
1740
+ const trend = delta > 0 ? `+${delta}` : delta;
1741
+ lines.push(`Body weight (last ${recentDays} days): latest ${latest.value.toFixed(1)} kg (${latest.date}), ${recentBodyWeight.length} readings, trend ${trend} kg`);
1742
+ }
1540
1743
  }
1541
1744
  }
1542
1745
 
@@ -1625,7 +1828,7 @@ function computeReadiness(dailyLoads, earliestDate, today, historyDays) {
1625
1828
  };
1626
1829
  }
1627
1830
 
1628
- function trainingLoad(snapshot) {
1831
+ export function trainingLoad(snapshot) {
1629
1832
  const metrics = snapshot.healthMetrics;
1630
1833
  const sessions = snapshot.sessions ?? [];
1631
1834
 
@@ -1774,7 +1977,7 @@ function trainingLoad(snapshot) {
1774
1977
  };
1775
1978
  }
1776
1979
 
1777
- function healthSummary(snapshot, days = 14) {
1980
+ export function healthSummary(snapshot, days = 14) {
1778
1981
  const metrics = snapshot.healthMetrics;
1779
1982
  const trainingLoadSummary = (() => {
1780
1983
  const tl = trainingLoad(snapshot);
@@ -1826,6 +2029,7 @@ function healthSummary(snapshot, days = 14) {
1826
2029
  },
1827
2030
  sleep: {
1828
2031
  avgHours: recentSleep.length > 0 ? Math.round(recentSleep.reduce((s, m) => s + m.durationMins, 0) / recentSleep.length / 60 * 10) / 10 : null,
2032
+ latestNight: recentSleep.length > 0 ? { hours: Math.round(recentSleep.at(-1).durationMins / 60 * 10) / 10, date: recentSleep.at(-1).date } : null,
1829
2033
  nights: recentSleep.length
1830
2034
  },
1831
2035
  bodyWeight: (() => {
@@ -1859,7 +2063,7 @@ function healthSummary(snapshot, days = 14) {
1859
2063
  };
1860
2064
  }
1861
2065
 
1862
- export function vitalsSummaryContext(snapshot) {
2066
+ export function vitalsSummaryContext(snapshot, { exclude = new Set() } = {}) {
1863
2067
  const lines = [];
1864
2068
  lines.push(`Date: ${new Date().toISOString().slice(0, 10)}`);
1865
2069
 
@@ -1899,15 +2103,27 @@ export function vitalsSummaryContext(snapshot) {
1899
2103
  lines.push('No training sessions in the last 7 days.');
1900
2104
  }
1901
2105
 
1902
- // Current program phase
2106
+ // Current program phase and schedule
1903
2107
  const prog = snapshot.currentProgram ?? activeProgram(snapshot);
1904
2108
  if (prog) {
1905
2109
  lines.push(`Current program: ${prog.name}.`);
2110
+ const currentDayIndex = prog.currentDayIndex ?? 0;
2111
+ const nextSessionWeekday = (prog.trainingWeekdays ?? [])[currentDayIndex];
2112
+ if (nextSessionWeekday != null) {
2113
+ const jsDay = new Date().getDay();
2114
+ const todayIso = jsDay === 0 ? 7 : jsDay;
2115
+ let daysUntil = nextSessionWeekday - todayIso;
2116
+ if (daysUntil < 0) daysUntil += 7;
2117
+ const dayNames = ['', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
2118
+ const whenLabel = daysUntil === 0 ? 'today' : daysUntil === 1 ? 'tomorrow' : `in ${daysUntil} days`;
2119
+ const nextDayTitle = (prog.days ?? [])[currentDayIndex]?.title ?? `Day ${currentDayIndex + 1}`;
2120
+ lines.push(`Next scheduled session: ${nextDayTitle} on ${dayNames[nextSessionWeekday]} (${whenLabel}).`);
2121
+ }
1906
2122
  }
1907
2123
 
1908
2124
  const vitals = healthSummary(snapshot, 14);
1909
2125
  const vitalsSignals = [];
1910
- if (vitals.trainingLoad?.available) {
2126
+ if (!exclude.has('trainingLoad') && vitals.trainingLoad?.available) {
1911
2127
  const load = vitals.trainingLoad;
1912
2128
  vitalsSignals.push({
1913
2129
  id: 'training-load',
@@ -1920,34 +2136,51 @@ export function vitalsSummaryContext(snapshot) {
1920
2136
  actionability: 9
1921
2137
  });
1922
2138
  }
1923
- if (vitals.restingHR?.avg != null && vitals.restingHR.latest?.value != null) {
1924
- const drift = vitals.restingHR.latest.value - vitals.restingHR.avg;
1925
- if (Math.abs(drift) >= 3) {
1926
- vitalsSignals.push({
1927
- id: 'resting-hr-drift',
1928
- category: 'recovery',
1929
- summary: `Resting HR drift ${drift > 0 ? '+' : ''}${drift} bpm vs 14d average`,
1930
- detail: `Latest ${vitals.restingHR.latest.value} bpm on ${vitals.restingHR.latest.date}, avg ${vitals.restingHR.avg}`,
1931
- impact: drift > 0 ? 8 : 6,
1932
- confidence: 8,
1933
- novelty: 7,
1934
- actionability: 8
1935
- });
2139
+ if (!exclude.has('recovery')) {
2140
+ if (vitals.restingHR?.avg != null && vitals.restingHR.latest?.value != null) {
2141
+ const drift = vitals.restingHR.latest.value - vitals.restingHR.avg;
2142
+ if (Math.abs(drift) >= 3) {
2143
+ vitalsSignals.push({
2144
+ id: 'resting-hr-drift',
2145
+ category: 'recovery',
2146
+ summary: `Resting HR drift ${drift > 0 ? '+' : ''}${drift} bpm vs 14d average`,
2147
+ detail: `Latest ${vitals.restingHR.latest.value} bpm on ${vitals.restingHR.latest.date}, avg ${vitals.restingHR.avg}`,
2148
+ impact: drift > 0 ? 8 : 6,
2149
+ confidence: 8,
2150
+ novelty: 7,
2151
+ actionability: 8
2152
+ });
2153
+ }
1936
2154
  }
1937
- }
1938
- if (vitals.hrv?.avg != null && vitals.hrv.latest?.value != null) {
1939
- const drift = vitals.hrv.latest.value - vitals.hrv.avg;
1940
- if (Math.abs(drift) >= 5) {
1941
- vitalsSignals.push({
1942
- id: 'hrv-drift',
1943
- category: 'recovery',
1944
- summary: `HRV drift ${drift > 0 ? '+' : ''}${drift} ms vs 14d average`,
1945
- detail: `Latest ${vitals.hrv.latest.value} ms on ${vitals.hrv.latest.date}, avg ${vitals.hrv.avg}`,
1946
- impact: drift < 0 ? 8 : 6,
1947
- confidence: 8,
1948
- novelty: 7,
1949
- actionability: 8
1950
- });
2155
+ if (vitals.hrv?.avg != null && vitals.hrv.latest?.value != null) {
2156
+ const drift = vitals.hrv.latest.value - vitals.hrv.avg;
2157
+ if (Math.abs(drift) >= 5) {
2158
+ vitalsSignals.push({
2159
+ id: 'hrv-drift',
2160
+ category: 'recovery',
2161
+ summary: `HRV drift ${drift > 0 ? '+' : ''}${drift} ms vs 14d average`,
2162
+ detail: `Latest ${vitals.hrv.latest.value} ms on ${vitals.hrv.latest.date}, avg ${vitals.hrv.avg}`,
2163
+ impact: drift < 0 ? 8 : 6,
2164
+ confidence: 8,
2165
+ novelty: 7,
2166
+ actionability: 8
2167
+ });
2168
+ }
2169
+ }
2170
+ if (vitals.sleep?.latestNight != null && vitals.sleep.avgHours != null) {
2171
+ const drift = vitals.sleep.latestNight.hours - vitals.sleep.avgHours;
2172
+ if (Math.abs(drift) >= 1) {
2173
+ vitalsSignals.push({
2174
+ id: 'sleep-drift',
2175
+ category: 'recovery',
2176
+ summary: `Last night's sleep ${drift > 0 ? '+' : ''}${drift.toFixed(1)}h vs 14d average`,
2177
+ detail: `Last night ${vitals.sleep.latestNight.hours}h (${vitals.sleep.latestNight.date}), avg ${vitals.sleep.avgHours}h`,
2178
+ impact: drift < 0 ? 8 : 6,
2179
+ confidence: 9,
2180
+ novelty: 7,
2181
+ actionability: 7
2182
+ });
2183
+ }
1951
2184
  }
1952
2185
  }
1953
2186
  const rankedVitalsSignals = rankPrioritySignals(vitalsSignals);
@@ -1963,12 +2196,33 @@ export function vitalsSummaryContext(snapshot) {
1963
2196
  // Health metrics
1964
2197
  const metrics = snapshot.healthMetrics;
1965
2198
  if (metrics) {
1966
- appendHealthMetricsContext(lines, metrics, { recentDays: 14 });
2199
+ appendHealthMetricsContext(lines, metrics, { recentDays: 14, exclude });
1967
2200
  }
1968
2201
 
2202
+ appendExcludeNote(lines, exclude);
2203
+
1969
2204
  return lines.join('\n');
1970
2205
  }
1971
2206
 
2207
+ function buildExcludeNote(exclude) {
2208
+ if (!exclude || exclude.size === 0) return null;
2209
+ const labels = [];
2210
+ if (exclude.has('recovery')) labels.push('recovery metrics (HR, HRV, sleep, VO2 max)');
2211
+ if (exclude.has('otherWorkouts')) labels.push('non-strength workouts');
2212
+ if (exclude.has('bodyWeight')) labels.push('body weight');
2213
+ if (exclude.has('trainingLoad')) labels.push('training load');
2214
+ if (labels.length === 0) return null;
2215
+ return `Note: The user has opted out of sharing ${labels.join(', ')} with the AI coach. Do not mention these data types or their absence. Instead, go deeper on the training data that is available — more detail on exercise progression, volume trends, and technique cues.`;
2216
+ }
2217
+
2218
+ function appendExcludeNote(lines, exclude) {
2219
+ const note = buildExcludeNote(exclude);
2220
+ if (note) {
2221
+ lines.push('');
2222
+ lines.push(note);
2223
+ }
2224
+ }
2225
+
1972
2226
  function requiredOption(options, primaryKey, legacyKey = null) {
1973
2227
  return options[primaryKey] ?? (legacyKey ? options[legacyKey] : undefined);
1974
2228
  }