Skip to content

Commit 836fffb

Browse files
Yanchek99claude
andauthored
Redesign Personal Records: multi-record movement PRs + workout/benchmark PRs (#1831)
* Fix PRs page 500 and wrong-record bug in personal records measurable_reps_msg crashed on movement logs with no reps recorded (load/distance/duration/calorie-only PRs). User#personal_records also showed whichever movement log was logged first, not the best one, because the inherited Log#movement_logs order(:id) scope silently won over the intended value-based ordering. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Add implementation plan for PR view redesign Design approved in #1829. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Add MovementRecordSet to rank/group movement logs into distinct PRs Excludes MovementRecordSet's rule-table dispatch method from the complexity/AbcSize cops (same pattern already used for Ability), since its branch count tracks the PR rule table rows, not incidental complexity. * Show one personal record per distinct rep/distance/duration test, not just one per movement * Order multiple personal-record rows for a movement by rep/distance/duration Task 2 made User#personal_records return several MovementLog rows per movement (e.g. Deadlift at 1, 5, and 52 reps). The controller's sort only grouped by movement name, so multi-record rows for one movement appeared in insertion order instead of a readable progression. * Add User#workout_records for best-score-per-workout tracking * Add Workout Records page showing best score per workout Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix workout_records crash on logs with nil score_value User#workout_records raised NoMethodError (time-scored nil) or ArgumentError (mixed nil/real scores) for any user with an unscored log, since score_value is nullable but only score_type is required. Guard nil so an unscored log always loses the comparison instead of crashing or winning by accident. * Drop RuboCop excludes and committed plan doc from PR view branch candidate_for was restructured into guard-clause methods along the rule table's own groupings (load / distance-or-duration / bare metric), so it satisfies AbcSize, CyclomaticComplexity, and PerceivedComplexity on its own merits instead of carving out per-file excludes in .rubocop.yml. The implementation plan now lives as a comment on the design issue (#1829 (comment)) rather than committed to the repo, matching how the design spec itself is tracked. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Restructure MovementRecordSet to satisfy RuboCop without excludes candidate_for was flagged for AbcSize/CyclomaticComplexity/PerceivedComplexity. Split into guard-clause methods along the rule table's own groupings (load / distance-or-duration / bare metric) instead of carving out per-file excludes in .rubocop.yml -- behavior and test coverage are unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Narrow Workout Records to repeated metcons, excluding lift workouts Every workout the user had ever logged was showing up as a "record," including one-off attempts and weight-scored lift workouts -- the latter are already represented on the movement PR page (grouped by rep count), so showing them again here was redundant. A workout now only appears once it's been logged more than once, and only if it isn't weight-scored. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Merge Personal Records and Workout Records into one PersonalRecordsController Adds a Lifts / Repeated Workouts subnav under a single Personal Records section instead of two separate nav items. * Ignore .superpowers/ agent scratch workspace Matches the existing .claude/.worktrees exclusions -- prevents skill-generated brainstorming/SDD scratch files from being committed. * Add Barbell Lifts tab grouping rep-maxes by movement Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Wire up expand/collapse for Barbell Lifts rows * Fix Barbell Lifts tab to handle nil load/reps gracefully The barbell action grouped weightlifting PRs without checking that load was present, so a reps-only record (no load) could render as a fabricated "0 lbs" PR and even outrank the real headline due to the reps-based sort. Filter the barbell tab to loaded records only, and guard rep_max_label so a loaded record with no rep count renders the load without a bare "RM" label instead of dropping the row. * Exclude measurable_helper_test.rb from Metrics/ClassLength Grew past the 100-line limit purely from merging two unrelated branches' independent test additions to this shared file -- not from any single change growing it. Matches the existing precedent for other oversized test files in this same cop. * Replace Lifts/Barbell Lifts tabs with one tab per movement family Consolidates the Lifts and Barbell Lifts personal-records tabs into a single dynamic GET /users/:user_id/personal_records/:family route, constrained to weightlifting, gymnastics, or monostructural. The weightlifting family keeps the existing grouped/expand-in-place view (renamed from barbell.html.slim); gymnastics and monostructural reuse the old flat-list view (renamed from lifts.html.slim), each filtered to its own family. The global nav "PRs" link and the subnav partial are updated to point at the new route. The controller test file was split into personal_records_controller_test.rb and personal_records_weightlifting_test.rb (grouped by concern) to keep Metrics/ClassLength satisfied without a new RuboCop exclude. * Add empty-state messages to personal records family tabs The new default landing tab (weightlifting) and other family tabs rendered nothing but the heading and subnav when a user had no PRs in that family, giving no indication that data exists on another tab. Adds a text-muted empty-state line to both templates, matching the pattern already used in logs/index.html.slim. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix Weightlifting tab visual issues: cards, fake links, tiny caret Replace per-row card boxes with a plain divided list (list-group-flush) matching the flat family-list tabs. Reset the expand toggle button so the movement name reads as plain bold text instead of a blue underlined link -- only the load value, which is a real navigation link, should look like one. Bump the expand/collapse caret to fs-4 for a larger tap target. * Rank fixed load/reps/distance tests by elapsed time A movement logged with load, reps, and distance all fixed (e.g. "1000 Box Step-ups @ 45lb/20in for time") was ranking by load instead of time, so repeated attempts at the same load all survived as separate PRs that rendered identically. Add a load_for_time candidate, checked before the existing find-a-max-with-time-cap load_candidate, that ranks by the fastest duration when a fixed distance is also present. * Remove redundant family heading from the family tabs view The subnav's active tab already names the current family; a second "Gymnastics"/"Monostructural" heading right below it was noise. * Unify family-tab styling and fix stray parens on reps-less records Convert the Gymnastics/Monostructural flat list from a <table> to the same list-group-flush row markup as the Weightlifting tab, so movement names render with the same font-weight everywhere on the page instead of relying on the browser's default bold <th>. Add PersonalRecordsHelper#record_msg: measurable_additional_metrics always wraps its output in parens, meant to trail a reps message (e.g. "10 Rows (20 calories)"). A pure distance/duration test with no reps (Row, Run, Air Bike) had nothing for those parens to trail, rendering as a bare "(5000 meters)". record_msg only parenthesizes when there's a reps message to attach to. * Treat reps: 0 as unspecified in rep_max_label, not "0RM" reps: 0 is this app's existing convention for uncapped/unspecified reps (the "max reps X" prescription pattern), not a literal zero-rep set. rep_max_label's `unless movement_log.reps` guard didn't catch it since 0 is truthy in Ruby, so a loaded PR with reps: 0 rendered as "0RM" instead of falling back to a bare load, like nil reps already do. * Rename Repeated Workouts tab to Workouts, align its row styling Convert repeated_workouts.html.slim from a <table> to the same list-group-flush row markup as the other three tabs, and add an empty state, matching the treatment already applied to Weightlifting and the family tabs. * Label reps: 0 rows "Max" instead of leaving them blank Blank was correct in isolation but looked broken as one row among labeled "1RM"/"2RM" siblings in the expanded list -- no indication of what that row represented. Label it explicitly using the app's existing "max reps" terminology instead. * Fix INTERVAL_SCHEME backtracking into a false "1 set of 0 reps" scheme A plain interval count with no real "sets: reps" scheme after it (e.g. "On the Minute x 10 (5 Rounds): ...") let the first \d+ capture backtrack, splitting "10" into captures "1" and "0" -- since the optional "sets"/":" separator gave the regex nothing to require between the two numbers. This fabricated a false uniform scheme that won out over the correct single-rep fallback whenever the log happened to have exactly as many set_details as the fabricated "set" count (typically 1), producing reps: 0 imports like the real-world "Front Squat 1x0" workout traced from a live PR-preview import. Guarded with the same (?!\d) lookahead DASH_SCHEME already uses for this exact class of problem. Moved the two existing INTERVAL_SCHEME tests plus the new regression test into set_scheme_extractor_uniform_scheme_test.rb, which already exists specifically for UNIFORM_SET_SCHEMES coverage -- set_scheme_extractor_test.rb had no headroom left under Metrics/ClassLength. * Group Monostructural by movement, showing every distance and time Movements whose PRs are uniformly distance+time (no load) now match the Weightlifting tab's grouped/expand-in-place pattern: one row per movement, headlined by the most recently logged distance, expanding to show every distance in ascending order with its time. Reuses the existing generic rep-max-row Stimulus controller unchanged. Records that aren't distance+time shaped (calorie-based tests, reps-in-a-time-cap tests) stay in the existing flat list below the grouped movements -- nothing disappears, nothing gets forced into a row format that doesn't fit its shape. * Abbreviate distance units and drop hours when under an hour distance_label now renders "500m" instead of "500 meters". duration_msg reuses MeasurableHelper's existing duration_metric_msg, which already drops the leading "00:" when there are no hours (e.g. "20:00" instead of "00:20:00") -- an established format elsewhere in the app, not a new one. * Gate Weightlifting and Gymnastics rep PRs on a verified unbroken set Resolves #1860: a rep count > 1 (e.g. Diane's aggregated 45-rep Deadlift, Murph's 100 pull-ups) is only shown as a rep-max PR once MovementLog#set_breakdown verifies a genuine unbroken effort, via the new MovementLog#verified_unbroken_reps. The Weightlifting tab labels off the verified count instead of the raw total, and the same exclusion now applies to the Gymnastics flat list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Broaden set_breakdown auto-population to any set-based lifting day single_weightlifting_exercise_day? only recognized a workout with exactly one exercise total, missing the far more common variable set-based lifting structure (e.g. "Back Squat: 5-5-3-3-1-1", multiple sets of the same movement, scored by weight). Reuse the existing Workout#set_based_lifting? concept, which already covers both the fixed-round and variable-set shapes, instead of the narrower ad-hoc check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Suppress the set_breakdown bracket for a trivial single-entry breakdown A single recorded set (e.g. one line of a "5x5 Back Squat", set_breakdown: [5]) isn't a "breakdown" -- there's nothing split across multiple sets to reveal, so showing "[5]" next to "5 Back Squats" was just noise. The verification data itself (needed for the Weightlifting rep-max gate) is unchanged; only the display is suppressed when there's one set to show. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Add regression test for set_breakdown auto-population via real form submission Verifies the full LogsController#create path -- not just Log#build_movement_logs -- correctly auto-populates set_breakdown for a set-based lifting day even when the recording form's hidden set_breakdown_text field submits blank. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Verify set-based lifting day rep counts structurally, no backfill needed MovementLog#verified_unbroken_reps required a captured set_breakdown for any reps > 1, which meant historical set-based lifting logs (e.g. old "5x5 Back Squat" entries) stayed excluded from the Weightlifting tab even though set_based_lifting? already proves them unbroken by construction. That proof comes from workout structure (segments, exercises, score type) that already exists for every log, old or new -- trust reps directly in that case instead of requiring set_breakdown to have been written first. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Trust a single max-effort achievement test (e.g. "Max Pull-up") structurally too verified_unbroken_reps only recognized set_based_lifting? (weighted sets), so a bare "how many can you do" bodyweight test -- one exercise, prescribed reps: 0, no time cap -- still required a captured set_breakdown despite being unbroken by definition: going until failure in one continuous attempt is what the test is. Reuse the existing Workout#single_achievement_test? concept, which already identifies exactly this shape, regardless of movement family. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 428044f commit 836fffb

33 files changed

Lines changed: 1106 additions & 65 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,4 @@ yarn-debug.log*
6767
# AI
6868
/.claude
6969
/.worktrees/
70+
/.superpowers/

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ Metrics/ClassLength:
5252
- app/services/workout_extraction/llm_parser.rb
5353
- test/services/workout_extraction/llm_parser_test.rb
5454
- test/models/workout_test.rb
55+
# Grew past the limit purely from merging two unrelated branches' independent
56+
# additions to this shared file, not from any single change growing it.
57+
- test/helpers/measurable_helper_test.rb
5558
- test/models/workout_fingerprint_test.rb
5659
- test/migrations/backfill_segments_for_top_level_exercises_test.rb
5760
- test/system/workout_drag_ordering_test.rb

app/controllers/movement_logs_controller.rb

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
class PersonalRecordsController < ApplicationController
2+
before_action :set_user
3+
4+
def index
5+
redirect_to family_user_personal_records_path(@user, family: 'weightlifting')
6+
end
7+
8+
def family
9+
@family = params.expect(:family)
10+
case @family
11+
when 'weightlifting'
12+
@movement_rep_maxes = weightlifting_rep_maxes
13+
render :weightlifting
14+
when 'monostructural'
15+
@movement_distances, @other_records = monostructural_records
16+
render :monostructural
17+
else
18+
@movement_logs = family_movement_logs
19+
render :family
20+
end
21+
end
22+
23+
def repeated_workouts
24+
@logs = @user.workout_records.sort_by { |log| log.workout.name }
25+
end
26+
27+
private
28+
29+
def weightlifting_rep_maxes
30+
@user.personal_records
31+
.select { |record| weightlifting_rep_max?(record) }
32+
.group_by(&:movement)
33+
.sort_by { |movement, _records| movement.name }
34+
.map { |movement, records| [movement, records.sort_by { |record| record.verified_unbroken_reps || Float::INFINITY }] }
35+
end
36+
37+
def weightlifting_rep_max?(record)
38+
record.movement.family_weightlifting? && record.load.present? && !unverified_reps_claim?(record)
39+
end
40+
41+
def family_movement_logs
42+
@user.personal_records
43+
.select { |record| record.movement.public_send("family_#{@family}?") && !unverified_reps_claim?(record) }
44+
.sort_by { |m| [m.movement.name, m.reps.to_i, m.distance.to_i, m.duration_seconds.to_i] }
45+
end
46+
47+
# A record whose reps are blank (e.g. a load-only PR, no rep count tracked at all) or <= 1 makes
48+
# no unverified claim. A record with reps > 1 is only trustworthy once its set_breakdown backs up
49+
# a genuine unbroken effort (see MovementLog#verified_unbroken_reps) -- otherwise it'd render a
50+
# fabricated claim, like Diane's aggregated "45RM" Deadlift or Murph's raw "100" pull-ups. Shared
51+
# by both the Weightlifting rep-max grouping and the Gymnastics flat list, since a gymnastics
52+
# rep count carries the same implicit "your real capability at N reps" claim (e.g. "max unbroken
53+
# pull-ups") as a weightlifting rep-max does.
54+
def unverified_reps_claim?(record)
55+
record.reps.present? && record.reps > 1 && record.verified_unbroken_reps.blank?
56+
end
57+
58+
# Only a movement whose records are uniformly distance+time (no load) gets the grouped/expand
59+
# treatment -- other shapes (calorie-based, reps-in-a-time-cap) stay in the flat list below,
60+
# rather than forcing genuinely different kinds of record into one row format.
61+
def monostructural_records
62+
records = @user.personal_records.select { |record| record.movement.family_monostructural? }
63+
distance_time_records, other_records = records.partition { |record| distance_time_record?(record) }
64+
[grouped_distance_records(distance_time_records), sorted_other_records(other_records)]
65+
end
66+
67+
def distance_time_record?(record)
68+
record.load.blank? && record.distance.present? && record.duration_seconds.present?
69+
end
70+
71+
def grouped_distance_records(records)
72+
records.group_by(&:movement)
73+
.sort_by { |movement, _records| movement.name }
74+
.map { |movement, movement_records| [movement, movement_records.sort_by(&:distance), most_recent(movement_records)] }
75+
end
76+
77+
def most_recent(records)
78+
records.max_by { |record| record.log.created_at }
79+
end
80+
81+
def sorted_other_records(records)
82+
records.sort_by { |m| [m.movement.name, m.reps.to_i, m.distance.to_i, m.duration_seconds.to_i] }
83+
end
84+
85+
def set_user
86+
@user = User.find(params.expect(:user_id))
87+
end
88+
end

app/helpers/measurable_helper.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ def literal_single_rep?(measurable, rep_metric)
3838
!measurable.reps_defined_by_interval?
3939
end
4040

41-
def measurable_reps_msg(measurable) = metric_unit_msg(measurable.prescription_metrics.find(&:rep?))
41+
def measurable_reps_msg(measurable)
42+
rep_metric = measurable.prescription_metrics.find(&:rep?)
43+
return unless rep_metric
44+
45+
metric_unit_msg(rep_metric)
46+
end
4247

4348
def measurable_additional_metrics(measurable)
4449
metrics = additional_metrics(measurable)
@@ -48,11 +53,14 @@ def measurable_additional_metrics(measurable)
4853
"(#{metrics.map { |metric| metric_unit_msg(metric) }.join(' / ')})"
4954
end
5055

56+
# A single set (one round, one entry, e.g. one line of a "5x5 Back Squat") isn't a "breakdown"
57+
# at all -- it's just restating the reps count, so showing "[5]" would be noise rather than
58+
# information. Only render once there's more than one recorded set to actually reveal.
5159
def measurable_set_breakdown(measurable)
5260
return unless measurable.respond_to?(:set_breakdown_rounds)
5361

5462
rounds = measurable.set_breakdown_rounds
55-
return if rounds.blank?
63+
return if rounds.blank? || rounds.sum(&:size) <= 1
5664

5765
"[#{rounds.map { |round| round.join('-') }.join(', ')}]"
5866
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module PersonalRecordsHelper
2+
# reps: 0 is this app's existing convention for "uncapped/unspecified reps" (see the
3+
# "max reps X" prescription pattern in MeasurableHelper), not a literal zero-rep set -- label it
4+
# explicitly rather than rendering "0RM" or leaving it blank next to labeled sibling rows. The
5+
# labeled count is the verified rep-max (see MovementLog#verified_unbroken_reps), not the raw
6+
# reps total, since a record only reaches this helper once it's passed that verification.
7+
def rep_max_label(movement_log)
8+
reps = movement_log.verified_unbroken_reps
9+
return if reps.blank?
10+
return 'Max' if reps.zero?
11+
12+
"#{reps}RM"
13+
end
14+
15+
def rep_max_load_msg(movement_log)
16+
pluralize(load_input_value(movement_log.load).to_i, load_display_unit)
17+
end
18+
19+
# measurable_additional_metrics always parenthesizes its output, since it's meant to trail a
20+
# reps message (e.g. "10 Rows (20 calories / 135 lbs)"). A pure distance/duration test with no
21+
# reps has nothing for those parens to trail, so render the bare metrics instead.
22+
def record_msg(movement_log)
23+
reps_msg = measurable_reps_msg(movement_log)
24+
return "#{reps_msg} #{measurable_additional_metrics(movement_log)}".strip if reps_msg
25+
26+
additional_metrics(movement_log).map { |metric| metric_unit_msg(metric) }.join(' / ')
27+
end
28+
29+
DISTANCE_UNIT_ABBREVIATIONS = { 'meter' => 'm', 'foot' => 'ft', 'inch' => 'in' }.freeze
30+
31+
def distance_label(movement_log)
32+
unit = (movement_log.distance_unit || 'meter').to_s
33+
"#{movement_log.distance.to_i}#{DISTANCE_UNIT_ABBREVIATIONS.fetch(unit, unit)}"
34+
end
35+
36+
def duration_msg(movement_log)
37+
duration_metric_msg(Metric.new(measurement: :seconds, value: movement_log.duration_seconds))
38+
end
39+
end

app/javascript/controllers/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ application.register("movement-select", MovementSelectController)
3131
import NestedFormController from "./nested_form_controller"
3232
application.register("nested-form", NestedFormController)
3333

34+
import RepMaxRowController from "./rep_max_row_controller"
35+
application.register("rep-max-row", RepMaxRowController)
36+
3437
import SegmentCardController from "./segment_card_controller"
3538
application.register("segment-card", SegmentCardController)
3639

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
export default class extends Controller {
4+
static targets = ["toggle", "details", "caret"]
5+
static values = { expanded: Boolean }
6+
7+
toggle() {
8+
this.expandedValue = !this.expandedValue
9+
}
10+
11+
expandedValueChanged() {
12+
this.detailsTarget.hidden = !this.expandedValue
13+
this.toggleTarget.setAttribute("aria-expanded", this.expandedValue.toString())
14+
this.caretTarget.textContent = this.expandedValue ? "▴" : "▾"
15+
}
16+
}

app/models/concerns/log_set_breakdown.rb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ def round_sizes_for_set_breakdown(movement_log)
2323

2424
private
2525

26-
# A single logged rep, or a dedicated single-lift weightlifting day, is unbroken by
27-
# construction -- no self-report needed. reps: 0 is this app's existing "unspecified/max
28-
# effort" sentinel (see ExercisePrescription#rep_prescription_metric), not a literal single
29-
# rep, so it's excluded here and left unpopulated like any other not-yet-known case.
30-
# Excludes structured round-repeat movements (AMRAPs, ascending ladders) entirely: for these,
31-
# reps is a per-round value, not a lifetime total, so a build-time reps == 1 says nothing about
32-
# what the eventual set_breakdown target will be once score_value is known.
26+
# A single logged rep, or a set within a dedicated set-based lifting workout (see
27+
# Workout#set_based_lifting? -- a fixed "5x5 Back Squat", or a variable build like "Back Squat:
28+
# 5-5-3-3-1-1"), is unbroken by construction -- no self-report needed. reps: 0 is this app's
29+
# existing "unspecified/max effort" sentinel (see ExercisePrescription#rep_prescription_metric),
30+
# not a literal single rep, so it's excluded here and left unpopulated like any other
31+
# not-yet-known case. Excludes structured round-repeat movements (AMRAPs, ascending ladders)
32+
# entirely: for these, reps is a per-round value, not a lifetime total, so a build-time reps == 1
33+
# says nothing about what the eventual set_breakdown target will be once score_value is known.
3334
def auto_populate_set_breakdown(movement_log, exercise)
3435
return if movement_log.reps.blank? || movement_log.reps.zero?
3536
return if structured_round_repeat?(exercise)

app/models/movement_log.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,25 @@ def set_breakdown_rounds
5757
chunk_set_breakdown_by_round(round_sizes)
5858
end
5959

60+
# The largest rep count this movement log can actually back up as one continuous, unbroken
61+
# effort -- nil when that can't be verified. Three cases are unbroken by construction, not
62+
# self-report: reps <= 1 (a single rep, or the app's "max effort" reps: 0 sentinel, can't be
63+
# broken into sets); a set-based lifting day (Workout#set_based_lifting? -- a fixed "5x5" or a
64+
# variable build like "5-5-3-3-1-1"); and a single max-effort achievement test
65+
# (Workout#single_achievement_test? -- a bare "Max Pull-up"-style test, where going until
66+
# failure in one continuous attempt is what the test *is*). Each of those proofs comes from
67+
# workout structure that already exists for every log, old or new, so none of them need a
68+
# captured set_breakdown to trust reps directly. Anything else needs a captured set_breakdown,
69+
# and the largest entry in it is the real rep-max, not the raw (possibly WOD-aggregated) reps
70+
# total -- e.g. Diane's Deadlift logs reps: 45 (21+15+9 summed by Metric#calculated_value) but
71+
# set_breakdown: [21, 15, 9], so the verified rep-max is 21.
72+
def verified_unbroken_reps
73+
return if reps.blank?
74+
return reps if reps <= 1 || log.workout.set_based_lifting? || log.workout.single_achievement_test?
75+
76+
set_breakdown.presence&.max
77+
end
78+
6079
private
6180

6281
def compact_set_breakdown

0 commit comments

Comments
 (0)