import '../about/external/data/ExercisePrescription.dart'; import '../about/external/data/SessionLog.dart'; import '../about/external/data/SetLog.dart'; /// Did you do the session, or a watered-down version of it? /// /// ``` /// integrity = 0.5 x (prescribed_sets_done / prescribed_sets) /// + 0.3 x (1 - mean |actual_reps - target| / target) /// + 0.2 x (hard_exercises_done / hard_exercises_prescribed) /// ``` /// /// Skipping the hard exercise while doing the easy ones is the most common /// form of self-deception in training, and the third term is what detects it. class IntegrityEngine { static const double setsWeight = 0.5; static const double repsWeight = 0.3; static const double hardWeight = 0.2; static double score( SessionLog session, List prescribed, ) { if (prescribed.isEmpty) { return 0; } return (setsWeight * _setCompletion(session, prescribed)) + (repsWeight * _repAccuracy(session, prescribed)) + (hardWeight * _hardCompletion(session, prescribed)); } /// Prescribed sets actually done, capped at 1 so overshooting one lift does /// not paper over skipping another. static double _setCompletion( SessionLog session, List prescribed, ) { int prescribedSets = 0; int doneSets = 0; for (ExercisePrescription item in prescribed) { prescribedSets = prescribedSets + item.sets; final int done = session.sets .where((entry) => entry.exerciseId == item.exerciseId && entry.isPrescribed) .length; doneSets = doneSets + (done > item.sets ? item.sets : done); } if (prescribedSets == 0) { return 0; } return doneSets / prescribedSets; } /// How close the reps landed to target, averaged across prescribed sets. static double _repAccuracy( SessionLog session, List prescribed, ) { double totalError = 0; int counted = 0; for (ExercisePrescription item in prescribed) { if (item.targetReps <= 0) { continue; } final List done = session.sets .where((entry) => entry.exerciseId == item.exerciseId && entry.isPrescribed) .toList(); for (SetLog entry in done) { final double error = (entry.reps - item.targetReps).abs() / item.targetReps; totalError = totalError + (error > 1 ? 1 : error); counted = counted + 1; } } if (counted == 0) { return 0; } return 1 - (totalError / counted); } /// The term that catches cherry-picking. "Hard" comes from your historical /// RPE on that exercise, not a static label. static double _hardCompletion( SessionLog session, List prescribed, ) { final List hard = prescribed.where((item) => item.hard).toList(); if (hard.isEmpty) { return 1; } int done = 0; for (ExercisePrescription item in hard) { final bool touched = session.sets.any((entry) => entry.exerciseId == item.exerciseId && entry.isPrescribed); if (touched) { done = done + 1; } } return done / hard.length; } /// Whether an exercise counts as hard for this user, derived from historical /// RPE rather than asserted up front. static bool isHard(List historicalRpe, {double cutoff = 8.0}) { if (historicalRpe.isEmpty) { return false; } double total = 0; for (double value in historicalRpe) { total = total + value; } return (total / historicalRpe.length) >= cutoff; } /// Weekly volume per muscle group, for under-target warnings — "you went to /// the gym" is not a metric. static Map weeklySetsByMuscleGroup(List sessions) { final Map volume = {}; for (SessionLog session in sessions) { for (SetLog entry in session.sets) { if (entry.muscleGroup.isEmpty) { continue; } volume[entry.muscleGroup] = (volume[entry.muscleGroup] ?? 0) + 1; } } return volume; } /// Total plyometric ground contacts across the week. static int weeklyContacts(List sessions) { int total = 0; for (SessionLog session in sessions) { total = total + session.contacts; } return total; } /// Plyo is the one modality where the app stops you rather than pushes you. /// CNS and connective tissue do not recover on a motivation schedule. static bool contactCeilingBreached(List sessions, int ceiling) { if (ceiling <= 0) { return false; } return weeklyContacts(sessions) >= ceiling; } /// Whether enough recovery has passed since the last high-intensity lower /// body session. static bool recoveredEnough( DateTime? lastHighIntensityLowerBody, int requiredHours, { DateTime? now, }) { if (lastHighIntensityLowerBody == null) { return true; } final DateTime moment = now ?? DateTime.now(); return moment.difference(lastHighIntensityLowerBody).inHours >= requiredHours; } /// Training through a deload logs as non-compliance, the same as skipping. static bool violatesDeload(SessionLog session, bool isDeloadWeek) { if (!isDeloadWeek) { return false; } return session.sets.any((entry) => entry.rpe >= 8); } }