Skip to main content

max / ripgrow

generate: seed prescription from most recent diagnostic when no working history When compute_prescription has nothing to walk, fall back to the last diagnostic's top load and prescribe 3 x 5 at 0.9 x that. Working history takes precedence when it exists; the seed only ever fills the empty-history case. Same ratio the deload path uses, so a fresh calibration and a post-deload rebuild land on the same starting point. This closes the loop: run a diagnostic, then the next generated session for that exercise carries a real prescription instead of "seed on first log." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-18 17:43 UTC
Commit: eda6007e114e29bc38093f755c7d08e0e193144d
Parent: 82a0764
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();