fix(#644): reject floor-clamped velocity-1RM estimates so 80% scaling can't collapse to 2.2 lb/cable#645
Conversation
… can't collapse to 2.2 lb/cable AssessmentEngine extrapolates the velocity-1RM to a load and clamps the result to the 1.0 kg hardware floor. A 'passing' estimate that sits at the floor is a diagnostic, not a usable baseline — the regression didn't actually reach the 1RM velocity, so 80% rounds to 0.8 kg and the routine collapses to ~2.2 lb/cable even when the user has a real stored-1RM or max-weight PR. Fix this once in the shared velocity/baseline path: 1. Producer (VelocityOneRepMaxEstimator): mark estimates at/below the floor unpassing so they never pass the quality gate to begin with. 2. Producer (ComputeVelocityOneRepMaxUseCase): skip persisting a result at/below the floor, returning null instead — mirrors what distinctLoads<3 / r2<threshold already do. 3. Repository (SqlDelightVelocityOneRepMaxRepository): filter floor-clamped rows out of getLatestPassing / getAllPassing so users with a poisoned legacy row from a previous build get the stored-1RM / max-weight PR fallback at runtime. 4. Resolver (ResolveRoutineScalingBaselineUseCase.resolveEstimatedOneRepMax): apply the same floor predicate as a belt-and-suspenders guard, so a different repo impl plugged in here can't re-introduce the bug. Regression coverage: - VelocityOneRepMaxEstimatorTest: floor-clamped estimate is unpassing; predicate boundary and constant pinned. - ResolveRoutineWeightsUseCaseTest: floor-clamped row + stored-1RM → uses stored 1RM (96 kg at 80%); floor-clamped row + max-weight PR → uses PR (48 kg at 80%); sane row above the floor is still trusted (100 kg → 80 kg at 80%). Fixes #644
Code Review Roast 🔥Verdict: No Issues Found (incremental) | Recommendation: Merge Incremental diff against
No new correctness, safety, or Ponytail findings on the incremental diff. The SQL filter now sits where it should (before 🏆 Best part: Pushing the predicate into the SQL 💀 Worst part: Nothing to roast in the increment. I checked. 📊 Overall: Like a good patch-2 — the author didn't just fix the typo, they fixed the reason the typo mattered. Lean already. Files Reviewed (5 files in increment)
Correctness / Safety FindingsNo correctness or safety findings. Ponytail ReviewPonytail: Lean already. Ship. Ponytail net: 0 lines. Suggested Minimal PatchNo patch needed. Final Merge GuidanceCan merge as-is. Previous review's findings are resolved; no new findings on the increment. The SQL filter fix (filter before Previous Review Summary (commit 9fe412d)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 9fe412d)Verdict: 2 nitpicks found | Recommendation: Merge (nitpicks are optional) Overview
Issue Details (click to expand)
🏆 Best part: The four-tier fix (producer × 2, repo × 2, resolver) all driven by a single 💀 Worst part: The 14-line stream-of-consciousness math comment in 📊 Overall: Like a well-engineered bridge — slightly more scaffolding than strictly needed, but every bolt is load-tested and the lane markers glow in the dark. Ship it after a comment trim. Files Reviewed (6 files)
Correctness / Safety FindingsNo correctness or safety findings. The four-tier fix correctly prevents the floor-clamped estimate from short-circuiting stored-1RM / max-weight PR fallback, both for new assessments (producer+repo) and for poisoned legacy rows (repo+resolver). Tests cover all three fallback paths plus the sane-row sanity check. Ponytail Review
Ponytail net: -13 lines (1 expression inlined, ~11 comment lines removed). Suggested Minimal Patch
Final Merge GuidanceCan merge as-is. The two nitpicks are stylistic polish, not correctness. The fix itself is sound, well-tested, and appropriately defensive against legacy data. Reviewed by minimax-m3 · Input: 42.2K · Output: 6.8K · Cached: 426.1K |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9fe412dd1d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
Summary
Fixes #644.
AssessmentEngine.estimateOneRepMaxextrapolates the velocity-based 1RM to a load and clamps the result to the 1.0 kg hardware floor. A "passing" estimate that sits at the floor is a diagnostic, not a usable baseline — the regression didn't actually reach the 1RM velocity, so 80% rounds to 0.8 kg and the routine collapses to ~2.2 lb/cable even when the user has a real stored-1RM or max-weight PR.This patch rejects floor-clamped estimates at the shared velocity/baseline path so the fallback to stored-1RM / max-weight PR works for affected users.
Root cause
AssessmentEngine.estimateOneRepMaxclamps the extrapolated load to[1.0, 110]kg.VelocityOneRepMaxEstimator.estimatemarks the result aspassedQualityGate = truewhenever the regression's R² is healthy, without checking the value.ComputeVelocityOneRepMaxUseCasepersists that result unchanged.SqlDelightVelocityOneRepMaxRepository.getLatestPassingreturns any row withpassedQualityGate = 1, so a poisoned legacy row from a previous build short-circuits the resolver.ResolveRoutineScalingBaselineUseCase.resolveEstimatedOneRepMaxtakes any positive velocity value first and returns it, never falling through to stored-1RM / max-weight PR.RoutineExercise.resolveWeightapplies 80% to 1.0 kg and rounds 0.8 kg back up to 1.0 kg → ~2.2 lb/cable.Fix
Apply the floor-clamp guard at four shared points, all driven by a single
VelocityOneRepMaxEstimator.isUsableEstimatepredicate so the producer, repo, and resolver can't drift:VelocityOneRepMaxEstimator.estimate: mark estimates at/below the 1.0 kg floor aspassedQualityGate = falsebefore returning. Future floor-clamped assessments never get persisted as passing.ComputeVelocityOneRepMaxUseCase: skip persisting a result at/below the floor and returnnull(mirrors the existingdistinctLoads<3/r2<thresholdearly-returns).SqlDelightVelocityOneRepMaxRepository.getLatestPassing/getAllPassing: filter floor-clamped rows out of the read path so users with a poisoned legacy row from a previous build get the fallback chain at runtime.ResolveRoutineScalingBaselineUseCase.resolveEstimatedOneRepMax: tighten the velocity guard to the shared predicate as a belt-and-suspenders check (a different repo impl plugged in here can't re-introduce the bug).Regression coverage
VelocityOneRepMaxEstimatorTest:estimate that lands on the 1 kg hardware floor is not passing— floor-clamped result is unpassing.isUsableEstimate predicate matches the documented contract— boundary at 1.0 kg.MIN_USABLE_ESTIMATE_KG equals 1 kg— pins the constant.ResolveRoutineWeightsUseCaseTest:ESTIMATED_1RM ignores floor-clamped velocity row and falls back to stored 1RM— 80% of 120 kg = 96 kg, not 1.0 kg.ESTIMATED_1RM ignores floor-clamped velocity row and falls back to max-weight PR— 80% of 60 kg = 48 kg.ESTIMATED_1RM still trusts a sane velocity row above the floor— guard does not strip legitimate estimates (100 kg → 80 kg at 80%).Verification
./gradlew :shared:compileKotlinMetadata -Pskip.supabase.check=true✅./gradlew :shared:compileTestKotlinIosArm64 -Pskip.supabase.check=true✅./gradlew :shared:testAndroidHostTest -Pskip.supabase.check=true✅ (2426 tests, 0 failures; 4 new tests included)./gradlew :androidApp:testDebugUnitTest -Pskip.supabase.check=true✅./gradlew :androidApp:lintDebug -Pskip.supabase.check=true✅./gradlew :shared:verifyCommonMainVitruvianDatabaseMigration -Pskip.supabase.check=true✅Out of scope (per GPT-5.5 RCA non-goals)
ESTIMATED_1RM.ResolveRoutineScalingBaselineUseCaseorder is preserved; the velocity position is replaced with a stricter predicate instead.