Skip to content

refactor(training): one lane load reading for Training Load, Cardio and Strength - #27

Merged
DX23876 merged 2 commits into
mainfrom
redesign/b-shared-lane-reading
Sep 17, 2026
Merged

DX23876 merged 2 commits into
mainfrom
redesign/b-shared-lane-reading

Conversation

@DX23876

@DX23876 DX23876 commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Summary

Second package of the Training Load / Cardio / Strength redesign. The redesigned screens will show a large "+x % vs usual" on all three, and tapping a lane on Training Load opens Cardio or Strength. So the figure has to be identical, and before this PR it was not:

  • Training Load priced cardio from measured Edwards TRIMP, left unpriceable days out of both comparison windows, and used the staged relativeLoad reading.
  • Cardio used CardioSession.cardioLoadTrend. That could fall back to a non-TRIMP effort figure, knew no unknown days, and read only the week's fused non-strength sessions.
  • Strength used StrengthSession.strengthLoadTrend, a plain trend rather than the staged reading.

What changed:

  • Strand/Screens/TrainingLoadLanes.swift now holds the single strength and cardio lane computation, read through any day. It covers weighted sets, the TRIMP series with unknown days, relative load, coverage counts, lower bound, distribution, week over week, status, and the 56 daily ratios.
  • TrainingLoadModel.prepare reads it through today. The session lane, the provisional ring, history, adaptation and recovery stay where they were.
  • CardioModel / StrengthModel expose lane and laneRatios per selected week:
    • They read through that week's Sunday, or today while the week runs.
    • They load the history window plus TrainingLoadLanes.lookbackDays (84 days: 56 for the comparison, 28 for the RPE median that weights unrated sets). Data older than that cannot move a reading.
    • Inputs are read once per load. Stepping weeks reuses them through the existing per-Monday cache.
  • The existing "Load trend" / "Strength load" tiles and the coach context read the lane. The visual redesign comes in later packages.

CardioSession.cardioLoadTrend and StrengthSession.strengthLoadTrend have no app caller any more. They stay for now because their package tests still cover additiveLoad and the effort axis.

Behaviour change to be aware of

  • On Cardio, a week without a measured heart-rate trace no longer shows an effort-based percentage. It shows no comparison, which is what Training Load already did.
  • On both screens, a week containing an unpriceable training day no longer compares.

Verification

  • Oracle, commit 1: the unchanged computation was moved into TrainingLoadModel.prepare and its outputs were pinned as literals: rated/unrated sets, an unknown cardio day inside and outside the window, a duplicate awaiting review, a session rated twice. Commit 2 refactors, and the same literals still pass.
  • New tests:
    • history beyond the lookback does not move either lane (85 vs 200 days of fixture);
    • a past week ignores everything after its reading day, with the unknown day still flagged as a lower bound;
    • readingDay returns Sunday, or today while the week runs.
  • xcodebuild … -scheme Strand test -only-testing:StrandTests: 3,371 tests, 0 failures.
  • xcodebuild … -scheme NOOPiOS -destination 'generic/platform=iOS Simulator' build: succeeded.
  • python3 Tools/doc_comment_lint.py: OK.

Analysis migration required: no. No formula, window or stored value changes; Cardio and Strength now display the reading Training Load already showed.

Move the detached computation in TrainingLoadModel.load into a static
prepare function without changing a line of its arithmetic, so a fixture
can run it. The pinned figures cover rated and unrated sets, an unknown
cardio day inside and outside the window, a duplicate awaiting review and
a session rated twice.

Analysis migration required: no
…s lanes

The Cardio and Strength screens computed "load vs your usual" their own
way: Cardio could fall back to non-TRIMP effort and knew no unknown days,
and both compared only through today. Tapping from Training Load to either
screen could therefore show a different percentage for the same week.

TrainingLoadLanes now holds the one strength and cardio lane computation,
read through any day. Training Load reads it through today. Cardio and
Strength read it through the selected week's Sunday, over their history
window plus the 84-day lookback a reading needs, and their load tiles and
coach context use it.

Analysis migration required: no
Copilot AI balanced review requested due to automatic review settings September 17, 2026 15:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Address duplicated fusion work and preserve as-of correctness for historical cardio readings.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Refactors Training Load, Cardio, and Strength to share consistent lane-load calculations and weekly comparisons.

Changes:

  • Adds shared lane, ratio, coverage, and status calculations.
  • Updates Cardio and Strength models and views to consume shared lanes.
  • Adds oracle, lookback, and week-boundary regression tests.
File summaries
File Summary
StrandTests/TrainingLoadLanesTests.swift Adds shared lane and lookback behavior tests.
Strand/Screens/TrainingLoadView.swift Uses the extracted lane calculations.
Strand/Screens/TrainingLoadLanes.swift Implements shared lane computation.
Strand/Screens/StrengthView.swift Displays shared strength lane values.
Strand/Screens/StrengthModel.swift Loads and exposes strength lanes. Moderate concern: duplicated history reads and session fusion (2 votes).
Strand/Screens/CardioView.swift Displays shared cardio lane values.
Strand/Screens/CardioModel.swift Loads and exposes cardio lanes. Moderate concerns: duplicated fusion work (2 votes) and non-as-of deduplication affecting past readings (1 vote).
Review details

Suppressed comments (1)

Strand/Screens/CardioModel.swift:127

  • This resolution is computed over the entire loaded window, but cardioLoads resolves overlaps newest-first. If a past session has a later overlapping twin, the later record is kept and the past record is marked as a duplicate before cardioLane(... through: laneDay) truncates the series. The past week's reading can therefore change when data after its Sunday is added, contrary to the as-of semantics described by readingDay; resolve/deduplicate the input as of each reading day (or preserve an as-of result per session) before building the cached lane.
        let laneResolution = await repo.cardioLoads(for: laneFusion.sessions)
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +126 to +127
let laneFusion = await repo.trainingSessions(days: range.days + TrainingLoadLanes.lookbackDays)
let laneResolution = await repo.cardioLoads(for: laneFusion.sessions)

async let historyRead = repo.resolvedStrengthHistory(days: historyDays)
async let fusedRead = repo.trainingSessions(days: historyDays)
async let laneHistoryRead = repo.resolvedStrengthHistory(days: historyDays + TrainingLoadLanes.lookbackDays)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants