Skip to main content

max / ripgrow

diagnostic: restrict to reps exercises for now Diagnostic screen filters non-reps exercises out of the picker at load time and surfaces a status hint when anything was hidden. The DiagnosticStep protocol and its "escalate on easy sets" heuristic are inherently reps-shaped, and designing a plank / 5k-run diagnostic is a separate question that hasn't been settled. Filtering keeps the current protocol correct and prevents a user from selecting a timed exercise and having it write reps-shaped diagnostic rows.
Author: Max Johnson <me@maxj.phd> · 2026-07-18 20:32 UTC
Commit: f6ccabd1123b15293b8022503e1fda45894fcaa4
Parent: 5a2701a
1 file changed, +49 insertions, -3 deletions
@@ -22,7 +22,9 @@ use ratatui::style::{Modifier, Style};
22 22 use ratatui::text::{Line, Span};
23 23 use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph};
24 24 use ripgrow_core::diagnostic::{DiagnosticSet, DiagnosticStep, next_step};
25 - use ripgrow_core::{Db, Estimate, Exercise, Kind, Load, Reps, RepsKind, RepsPayload, RepsSet, Rpe};
25 + use ripgrow_core::{
26 + Db, EffortKind, Estimate, Exercise, Kind, Load, Reps, RepsKind, RepsPayload, RepsSet, Rpe,
27 + };
26 28
27 29 pub struct DiagnosticScreen {
28 30 date: NaiveDate,
@@ -75,11 +77,32 @@ enum RunField {
75 77
76 78 impl DiagnosticScreen {
77 79 pub fn load(db: &Db) -> Result<Self, ripgrow_core::Error> {
78 - let exercises = db.list_exercises()?;
80 + // Diagnostic is reps-only for now: the DiagnosticStep protocol
81 + // and its "escalate on easy sets" heuristic are reps-shaped.
82 + // Timed / distance diagnostics need their own protocol design
83 + // (what does an "easy" plank hold or 5k run look like?) which
84 + // is deferred; for now those exercises are filtered out of the
85 + // picker and the status hints at what was skipped.
86 + let all = db.list_exercises()?;
87 + let skipped = all
88 + .iter()
89 + .filter(|e| e.effort_kind != EffortKind::Reps)
90 + .count();
91 + let exercises: Vec<Exercise> = all
92 + .into_iter()
93 + .filter(|e| e.effort_kind == EffortKind::Reps)
94 + .collect();
79 95 let mut list_state = ListState::default();
80 96 if !exercises.is_empty() {
81 97 list_state.select(Some(0));
82 98 }
99 + let status = if skipped > 0 {
100 + format!(
101 + "hiding {skipped} non-reps exercise(s) — diagnostic is reps-only for now"
102 + )
103 + } else {
104 + String::new()
105 + };
83 106 Ok(Self {
84 107 date: Local::now().date_naive(),
85 108 exercises,
@@ -88,7 +111,7 @@ impl DiagnosticScreen {
88 111 list_state,
89 112 },
90 113 session: None,
91 - status: String::new(),
114 + status,
92 115 })
93 116 }
94 117
@@ -632,6 +655,29 @@ mod tests {
632 655 }
633 656
634 657 #[test]
658 + fn load_filters_out_non_reps_exercises_and_reports_skip_count() {
659 + let db = Db::open_in_memory().unwrap();
660 + db.init_profile("self", LoadUnit::Kg).unwrap();
661 + db.create_exercise("squat", ResistanceType::Freeweight, LoadUnit::Kg, 2.5, &[])
662 + .unwrap();
663 + db.create_exercise("plank", ResistanceType::CardioTime, LoadUnit::Kg, 10.0, &[])
664 + .unwrap();
665 + db.create_exercise("5k run", ResistanceType::CardioDistance, LoadUnit::Kg, 0.0, &[])
666 + .unwrap();
667 + let screen = DiagnosticScreen::load(&db).unwrap();
668 + let names: Vec<&str> = screen.exercises.iter().map(|e| e.name.as_str()).collect();
669 + assert_eq!(names, vec!["squat"]);
670 + assert!(screen.status.contains("hiding 2"), "status: {}", screen.status);
671 + }
672 +
673 + #[test]
674 + fn load_status_is_empty_when_every_exercise_is_reps() {
675 + let (db, _) = setup();
676 + let screen = DiagnosticScreen::load(&db).unwrap();
677 + assert!(screen.status.is_empty(), "status: {:?}", screen.status);
678 + }
679 +
680 + #[test]
635 681 fn picking_exercise_enters_seed_stage() {
636 682 let (db, _) = setup();
637 683 let mut screen = DiagnosticScreen::load(&db).unwrap();