max / ripgrow
1 file changed,
+52 insertions,
-5 deletions
| @@ -295,15 +295,33 @@ impl Db { | |||
| 295 | 295 | .ok_or_else(|| Error::NotFound(format!("exercise {exercise_id}")))?; | |
| 296 | 296 | ||
| 297 | 297 | let sessions = self.list_sessions_for_exercise(exercise_id)?; | |
| 298 | - | let result = compute_prescription(&sessions, increment); | |
| 298 | + | let mut result = compute_prescription(&sessions, increment); | |
| 299 | + | ||
| 300 | + | // Fall back to a diagnostic seed when there is no working-set | |
| 301 | + | // history. A completed diagnostic hands back a top load; the | |
| 302 | + | // first prescribed working session sits at 0.9x that (same ratio | |
| 303 | + | // the deload path uses, so a fresh calibration converges with a | |
| 304 | + | // post-deload rebuild rather than diverging). | |
| 305 | + | if matches!(result, PrescriptionResult::NoHistory) | |
| 306 | + | && let Some(seed) = self.diagnostic_seed(exercise_id)? | |
| 307 | + | { | |
| 308 | + | let load = round_to_increment(seed * 0.9, increment); | |
| 309 | + | result = PrescriptionResult::Prescribed(Prescription { | |
| 310 | + | state: State::Progressing, | |
| 311 | + | sets: 3, | |
| 312 | + | reps: 5, | |
| 313 | + | load, | |
| 314 | + | }); | |
| 315 | + | } | |
| 299 | 316 | ||
| 300 | 317 | // Cache the state (with the most recent session date as since_date | |
| 301 | 318 | // for display purposes; not a precise "since when" transition marker | |
| 302 | 319 | // — see the history screen for the accurate walk). | |
| 303 | - | if let PrescriptionResult::Prescribed(pres) = &result | |
| 304 | - | && let Some(last_session) = sessions.last() | |
| 305 | - | { | |
| 306 | - | let since = last_session[0].session_date.to_string(); | |
| 320 | + | if let PrescriptionResult::Prescribed(pres) = &result { | |
| 321 | + | let since = sessions | |
| 322 | + | .last() | |
| 323 | + | .map(|s| s[0].session_date.to_string()) | |
| 324 | + | .unwrap_or_else(|| chrono::Local::now().date_naive().to_string()); | |
| 307 | 325 | self.conn().execute( | |
| 308 | 326 | "INSERT INTO progression_state (exercise_id, state, since_date) \ | |
| 309 | 327 | VALUES (?1, ?2, ?3) \ | |
| @@ -357,6 +375,35 @@ mod db_tests { | |||
| 357 | 375 | } | |
| 358 | 376 | ||
| 359 | 377 | #[test] | |
| 378 | + | fn compute_prescription_seeds_from_diagnostic_when_no_working_history() { | |
| 379 | + | let (db, id) = setup(); | |
| 380 | + | let d = NaiveDate::from_ymd_opt(2026, 7, 10).unwrap(); | |
| 381 | + | // Diagnostic finished at 100. Working weight = 0.9 * 100 = 90. | |
| 382 | + | db.append_diagnostic_set(id, d, 100.0, 5, 4).unwrap(); | |
| 383 | + | let PrescriptionResult::Prescribed(pres) = db.compute_prescription(id).unwrap() else { | |
| 384 | + | panic!("expected prescription"); | |
| 385 | + | }; | |
| 386 | + | assert_eq!(pres.state, State::Progressing); | |
| 387 | + | assert_eq!(pres.sets, 3); | |
| 388 | + | assert_eq!(pres.reps, 5); | |
| 389 | + | assert_eq!(pres.load, 90.0); | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | #[test] | |
| 393 | + | fn compute_prescription_prefers_working_history_over_diagnostic_seed() { | |
| 394 | + | let (db, id) = setup(); | |
| 395 | + | let d1 = NaiveDate::from_ymd_opt(2026, 7, 10).unwrap(); | |
| 396 | + | let d2 = NaiveDate::from_ymd_opt(2026, 7, 12).unwrap(); | |
| 397 | + | db.append_diagnostic_set(id, d1, 100.0, 5, 4).unwrap(); | |
| 398 | + | db.append_set(id, d2, 60.0, 5, 3).unwrap(); | |
| 399 | + | let PrescriptionResult::Prescribed(pres) = db.compute_prescription(id).unwrap() else { | |
| 400 | + | panic!("expected prescription"); | |
| 401 | + | }; | |
| 402 | + | // Working set at 60 with a clean first-session hit -> next 62.5. | |
| 403 | + | assert_eq!(pres.load, 62.5); | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | #[test] | |
| 360 | 407 | fn compute_prescription_reads_history_and_caches_state() { | |
| 361 | 408 | let (db, id) = setup(); | |
| 362 | 409 | let day = |n| NaiveDate::from_ymd_opt(2026, 7, n).unwrap(); |